diff --git a/CODEOWNERS b/CODEOWNERS
index 69fc52c11ce4e..a8220146263a5 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -49,6 +49,7 @@
/bundles/org.openhab.binding.boschindego/ @jofleck @jlaur
/bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
+/bundles/org.openhab.binding.broadlink/ @themillhousegorup @rlarranaga
/bundles/org.openhab.binding.broadlinkthermostat/ @flo-02-mu
/bundles/org.openhab.binding.bsblan/ @hypetsch
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 28893eca65d64..cd0f0902cab2f 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -241,6 +241,11 @@
org.openhab.binding.bosesoundtouch
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.broadlink
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.broadlinkthermostat
diff --git a/bundles/org.openhab.binding.broadlink/NOTICE b/bundles/org.openhab.binding.broadlink/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/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.broadlink/README.md b/bundles/org.openhab.binding.broadlink/README.md
new file mode 100644
index 0000000000000..6928d4613587e
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/README.md
@@ -0,0 +1,126 @@
+# Broadlink Binding
+
+This binding supports a range of home networking devices made by (and occasionally OEM licensed from) [Broadlink](https://www.ibroadlink.com/).
+
+## Supported Things
+
+| Thing ID | Description |
+|------------|-------------------------------------------------------------------------|
+| a1 | Broadlink A1 multi sensor |
+| mp1 | Broadlink MP1 WiFi Smart Power Strip (4 sockets) |
+| mp1_1k3s2u | Broadlink MP1 1K3S2U WiFi Smart Power Strip (3 sockets, 2 USB) |
+| mp2 | Broadlink MP2 WiFi Smart Power Strip (3 sockets, 3 USB) |
+| sp1 | Broadlink SP1 WiFi Smart Socket |
+| sp2 | Broadlink SP2 WiFi Smart Socket with night light |
+| sp3 | Broadlink SP3/Mini WiFi Smart Socket with night light |
+| sp3s | Broadlink SP3s WiFi Smart Socket with Power Meter |
+| rm2 | Broadline RM2/Pro WiFi IR/RF Transmitter with temperature sensor |
+| rm3 | Broadlink RM3/Mini WiFi IR Transmitter |
+| rm3q | Broadlink RM3 WiFi IR Transmitter with Firmware v44057 |
+| rm4 | Broadlink RM4 WiFi IR Transmitter with temperature and humidity sensors |
+
+## Discovery
+
+Devices in the above list that are set up and working in the Broadlink mobile app should be discoverable by initiating a discovery from the openHAB UI.
+
+> The `Lock Device` setting must be switched off for your device via the Broadlink app to be discoverable in openHAB.
+
+## Thing Configuration
+
+| Name | Type | Default | description |
+|---------------------|---------|---------------|-----------------------------------------------------------------------------------|
+| ipAddress | String | | Sets the IP address of the Broadlink device |
+| staticIp | Boolean | true | Enabled if your broadlink device has a Static IP set |
+| port | Integer | 80 | The network port for the device |
+| macAddress | String | | The device's MAC Address |
+| pollingInterval | Integer | 30 | The interval in seconds for polling the status of the device |
+| ignoreFailedUpdates | Boolean | false | If enabled, failed status requests won't put the device `OFFLINE` |
+| mapFilename | String | broadlink.map | The map file that contains remote codes to send via IR |
+
+> The `mapFilename` setting is applicable to the RM series of devices only.
+
+## Channels
+
+| Channel | Supported Devices | Type | Description |
+|------------------|--------------------------|----------------------|-------------------------------------------------|
+| powerOn | MP2, all SPx | Switch | Power on/off for switches/strips |
+| nightLight | SP3 | Switch | Night light on/off |
+| temperature | A1, RM2, RM4 | Number:Temperature | Temperature |
+| humidity | A1, RM4 | Number:Dimensionless | Air humidity percentage |
+| noise | A1 | String | Noise level: `QUIET`/`NORMAL`/`NOISY`/`EXTREME` |
+| light | A1 | String | Light level: `DARK`/`DIM`/`NORMAL`/`BRIGHT` |
+| air | A1 | String | Air quality: `PERFECT`/`GOOD`/`NORMAL`/`BAD` |
+| s1powerOn | MP1, MP1_1k3s2u | Switch | Socket 1 power |
+| s2powerOn | MP1, MP1_1k3s2u | Switch | Socket 2 power |
+| s3powerOn | MP1, v_1k3s2u | Switch | Socket 3 power |
+| s4powerOn | MP1 | Switch | Socket 4 power |
+| usbPowerOn | MP1_1k3s2u | Switch | USB power |
+| powerConsumption | MP2, SP2s,SP3s | Number:Power | Power consumption |
+| command | all RMx | String | IR Command code to transmit |
+| learningControl | all RMx | String | Learn mode command channel (see below) |
+
+## Map File
+
+The Broadlink RM family of devices can transmit IR codes.
+The map file contains a list of IR command codes to send via the device.
+The file uses the [Java Properties File format](https://en.wikipedia.org/wiki/.properties) and is stored in the `/transform` folder.
+By default, the file name is `broadlink.map` but can be changed using the `mapFile` setting.
+
+Here is a map file example:
+
+```
+TV_POWER=26008c0092961039103a1039101510151014101510151039103a10391015101411141015101510141139101510141114101510151014103a10141139103911391037123a10391000060092961039103911391014111410151015101411391039103a101411141015101510141015103911141015101510141015101510391015103911391039103a1039103911000d05000000000000000000000000
+heatpump_off=2600760069380D0C0D0C0D290D0C0D290D0C0D0C0D0C0D290D290D0C0D0C0D0C0D290D290D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D290D0C0D0C0D0C0D0C0D0C0D0C0D0C0D290D0C0D0C0D0C0D0C0D290D0C0D0C0D0C0D0C0D0C0D0C0D290D0C0D290D290D290D290D290D290E0002900000
+
+```
+
+The above codes are power on/off for Samsung TVs and Power Off for a Fujitsu heat pump.
+To send either code, the string `TV_POWER` or `heatpump_off` must be sent to the `command` channel for the device.
+
+## Learning new remote codes
+
+To obtain the command codes, you can get this binding to put your Broadlink RMx device into
+"learn mode" and then ask it for the code it learnt. Here are the steps:
+
+1. In the openHAB web UI, navigate to your RMx Thing, and click on its *Channels* tab
+2. Find the *Remote Learning Control* channel and create an Item for it
+3. Click the item, and click the rectangular area that is marked NULL
+4. In the pop-up menu that appears, select "Enter infrared learn mode"
+5. *The LED on your RM device will illuminate solidly*
+6. Point your IR remote control at your RM device and press the button you'd like to learn
+7. *The LED on your RM device will extinguish once it has learnt the command*
+8. Now click the rectangular area again (which will now show "Enter infrared learn mode")
+9. Select the "Check last captured IR code" menu option in the pop-up menu
+10. Inspect the `openhab.log` file on your openHAB server - you should see the following:
+
+```
+[BroadlinkRemoteHandler] - BEGIN LAST LEARNT CODE (976 bytes)
+[BroadlinkRemoteHandler] - 2600bc017239100d0e2b0e0f100c107f55747be51a3e1d4ff4......f5be8b3d4ff4b
+4d77c44d105fa530546becaa2bcfbd348b30145447f55747be51a3747be51a3e1d4ff4b3f4f4f......a3e1d4ff4b348
+0145447f55747be51a3e1d4ff4b
+[BroadlinkRemoteHandler] - END LAST LEARNT CODE (1944 characters)
+```
+
+11. If you carefully copy the log line between the `BEGIN` and `END` messages,
+ it should have exactly the number of characters as advised in the `END` message.
+
+12. You can now paste a new entry into your `map` file, with the name of your choice; for example:
+
+```
+BLURAY_ON=2600bc017239100d0e2b0e0f100c107f55747be51a3e1d4ff4...0145447f55747be51a3e1d4ff4b
+```
+
+
+## Full Example
+
+Items file example; `sockets.items`:
+
+```
+Switch BroadlinkSP3 "Christmas Lights" [ "Lighting" ] { channel="broadlink:sp3:34-ea-34-22-44-66:powerOn" }
+```
+
+## Credits
+
+- [Cato Sognen](https://community.openhab.org/u/cato_sognen)
+- [JAD](http://www.javadecompilers.com/jad) (Java Decompiler)
+
diff --git a/bundles/org.openhab.binding.broadlink/pom.xml b/bundles/org.openhab.binding.broadlink/pom.xml
new file mode 100644
index 0000000000000..0c6030200123a
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.0.0-SNAPSHOT
+
+
+ org.openhab.binding.broadlink
+
+ openHAB Add-ons :: Bundles :: Broadlink Binding
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/feature/feature.xml b/bundles/org.openhab.binding.broadlink/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..397452eb63161
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/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.broadlink/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/BroadlinkBindingConstants.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/BroadlinkBindingConstants.java
new file mode 100644
index 0000000000000..6dc36dc07c022
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/BroadlinkBindingConstants.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Power;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link BroadlinkBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkBindingConstants {
+
+ public static final ThingTypeUID THING_TYPE_RM2 = new ThingTypeUID("broadlink", "rm2");
+ public static final ThingTypeUID THING_TYPE_RM3 = new ThingTypeUID("broadlink", "rm3");
+ public static final ThingTypeUID THING_TYPE_RM3Q = new ThingTypeUID("broadlink", "rm3q");
+ public static final ThingTypeUID THING_TYPE_RM4 = new ThingTypeUID("broadlink", "rm4");
+ public static final ThingTypeUID THING_TYPE_A1 = new ThingTypeUID("broadlink", "a1");
+ public static final ThingTypeUID THING_TYPE_MP1 = new ThingTypeUID("broadlink", "mp1");
+ public static final ThingTypeUID THING_TYPE_MP1_1K3S2U = new ThingTypeUID("broadlink", "mp1_1k3s2u");
+ public static final ThingTypeUID THING_TYPE_MP2 = new ThingTypeUID("broadlink", "mp2");
+ public static final ThingTypeUID THING_TYPE_SP1 = new ThingTypeUID("broadlink", "sp1");
+ public static final ThingTypeUID THING_TYPE_SP2 = new ThingTypeUID("broadlink", "sp2");
+ public static final ThingTypeUID THING_TYPE_SP2S = new ThingTypeUID("broadlink", "sp2s");
+ public static final ThingTypeUID THING_TYPE_SP3 = new ThingTypeUID("broadlink", "sp3");
+ public static final ThingTypeUID THING_TYPE_SP3S = new ThingTypeUID("broadlink", "sp3s");
+ public static final ThingTypeUID THING_TYPE_PIR = new ThingTypeUID("broadlink", "s1p");
+ public static final ThingTypeUID THING_TYPE_MAGNET = new ThingTypeUID("broadlink", "s1m");
+
+ public static final String RM2 = "Broadlink RM2";
+ public static final String RM3 = "Broadlink RM3";
+ public static final String RM3Q = "Broadlink RM3 v11057";
+ public static final String RM4 = "Broadlink RM4 / RM4 Mini / RM4 Pro";
+ public static final String A1 = "Broadlink A1";
+ public static final String MP1 = "Broadlink MP1";
+ public static final String MP1_1K3S2U = "Broadlink MP1 1K3S2U";
+ public static final String MP2 = "Broadlink MP2";
+
+ public static final String SP1 = "Broadlink SP1";
+ public static final String SP2 = "Broadlink SP2";
+ public static final String SP2S = "Broadlink SP2s";
+ public static final String SP3 = "Broadlink SP3";
+ public static final String SP3S = "Broadlink SP3s";
+ public static final String S1P = "Smart One PIR Sensor";
+ public static final String S1M = "Smart One Magnet Sensor";
+
+ public static final String BROADLINK_AUTH_KEY = "097628343fe99e23765c1513accf8b02";
+ public static final String BROADLINK_IV = "562e17996d093d28ddb3ba695a2e6f58";
+
+ public static final String COMMAND_CHANNEL = "command";
+ public static final String LEARNING_CONTROL_CHANNEL = "learningControl";
+ public static final String LEARNING_CONTROL_COMMAND_LEARN = "LEARN";
+ public static final String LEARNING_CONTROL_COMMAND_CHECK = "CHECK";
+ public static final String CHANNEL_TEMPERATURE = "temperature";
+ public static final String CHANNEL_HUMIDITY = "humidity";
+ public static final String CHANNEL_POWER_CONSUMPTION = "powerConsumption";
+
+ public static final String COMMAND_POWER_ON = "powerOn";
+ public static final String COMMAND_NIGHTLIGHT = "nightLight";
+
+ public static final Unit BROADLINK_TEMPERATURE_UNIT = SIUnits.CELSIUS;
+ public static final Unit BROADLINK_HUMIDITY_UNIT = Units.PERCENT;
+ public static final Unit BROADLINK_POWER_CONSUMPTION_UNIT = Units.WATT;
+
+ public static final Map SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP = new HashMap<>();
+
+ static {
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_RM2, RM2);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_RM3, RM3);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_RM3Q, RM3Q);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_RM4, RM4);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_A1, A1);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_MP1, MP1);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_MP1_1K3S2U, MP1_1K3S2U);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_MP2, MP2);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_SP1, SP1);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_SP2, SP2);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_SP2S, SP2S);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_SP3, SP3);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_SP3S, SP3S);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_PIR, S1P);
+ SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_MAGNET, S1M);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/config/BroadlinkDeviceConfiguration.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/config/BroadlinkDeviceConfiguration.java
new file mode 100644
index 0000000000000..03e74b507ad5e
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/config/BroadlinkDeviceConfiguration.java
@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Device configuration for the supported Broadlink devices.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkDeviceConfiguration {
+ private String ipAddress;
+ private boolean staticIp;
+ private int port;
+ private String macAddress;
+ private byte[] macAddressBytes;
+ private int pollingInterval;
+ private String mapFilename;
+ private boolean ignoreFailedUpdates;
+ private int deviceType;
+
+ public BroadlinkDeviceConfiguration() {
+ ipAddress = "";
+ staticIp = true;
+ macAddress = "";
+ macAddressBytes = new byte[0];
+ pollingInterval = 30;
+ mapFilename = "broadlink.map";
+ ignoreFailedUpdates = false;
+ }
+
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ public void setIpAddress(String ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ public boolean isStaticIp() {
+ return staticIp;
+ }
+
+ public void setStaticIp(boolean staticIp) {
+ this.staticIp = staticIp;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setMacAddress(String macAddress) {
+ this.macAddressBytes = new byte[0];
+ this.macAddress = macAddress;
+ }
+
+ public byte[] getMacAddress() {
+ if (macAddressBytes.length != 6) {
+ macAddressBytes = new byte[6];
+ String elements[] = macAddress.split(":");
+ for (int i = 0; i < 6; i++) {
+ String element = elements[i];
+ macAddressBytes[i] = (byte) Integer.parseInt(element, 16);
+ }
+ }
+ return macAddressBytes;
+ }
+
+ public String getMacAddressAsString() {
+ return macAddress;
+ }
+
+ public int getPollingInterval() {
+ return pollingInterval;
+ }
+
+ public void setPollingInterval(int pollingInterval) {
+ this.pollingInterval = pollingInterval;
+ }
+
+ public String getMapFilename() {
+ return mapFilename;
+ }
+
+ public void setMapFilename(String mapFilename) {
+ this.mapFilename = mapFilename;
+ }
+
+ public int getDeviceType() {
+ return deviceType;
+ }
+
+ public void setDeviceType(int newDeviceType) {
+ this.deviceType = newDeviceType;
+ }
+
+ public void setIgnoreFailedUpdates(boolean ignore) {
+ this.ignoreFailedUpdates = ignore;
+ }
+
+ public boolean isIgnoreFailedUpdates() {
+ return this.ignoreFailedUpdates;
+ }
+
+ public String toString() {
+ return (new StringBuilder("BroadlinkDeviceConfiguration [ipAddress=")).append(ipAddress).append(" (static: ")
+ .append(staticIp).append("), port=").append(port).append(", macAddress=").append(macAddress)
+ .append(", pollingInterval=").append(pollingInterval).append(", mapFilename=").append(mapFilename)
+ .append("]").toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkA1Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkA1Handler.java
new file mode 100644
index 0000000000000..fbe9e40a7438b
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkA1Handler.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.internal.ModelMapper;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Handles the A1 environmental sensor.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkA1Handler extends BroadlinkBaseThingHandler {
+
+ public BroadlinkA1Handler(Thing thing) {
+ super(thing);
+ }
+
+ protected boolean getStatusFromDevice() {
+ logger.trace("A1 getStatusFromDevice");
+ byte payload[];
+ payload = new byte[16];
+ payload[0] = 1;
+
+ try {
+ byte message[] = buildMessage((byte) 0x6a, payload);
+
+ byte[] response = sendAndReceiveDatagram(message, "A1 device status");
+ if (response == null) {
+ logger.warn("Got nothing back while getting device status");
+ return false;
+ }
+ byte decryptResponse[] = decodeDevicePacket(response);
+ double temperature = ((double) (decryptResponse[4] * 10 + decryptResponse[5]) / 10D);
+ logger.trace("A1 getStatusFromDevice got temperature {}", temperature);
+
+ updateTemperature(temperature);
+ updateHumidity((double) (decryptResponse[6] * 10 + decryptResponse[7]) / 10D);
+ updateState("light", ModelMapper.getLightValue(decryptResponse[8]));
+ updateState("air", ModelMapper.getAirValue(decryptResponse[10]));
+ updateState("noise", ModelMapper.getNoiseValue(decryptResponse[12]));
+ return true;
+ } catch (Exception ex) {
+ logger.warn("Failed while getting device status", ex);
+ return false;
+ }
+ }
+
+ protected boolean onBroadlinkDeviceBecomingReachable() {
+ return getStatusFromDevice();
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkBaseThingHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkBaseThingHandler.java
new file mode 100644
index 0000000000000..50dc9660b12ae
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkBaseThingHandler.java
@@ -0,0 +1,297 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.*;
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.BROADLINK_HUMIDITY_UNIT;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Random;
+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.broadlink.BroadlinkBindingConstants;
+import org.openhab.binding.broadlink.config.BroadlinkDeviceConfiguration;
+import org.openhab.binding.broadlink.internal.BroadlinkProtocol;
+import org.openhab.binding.broadlink.internal.NetworkUtils;
+import org.openhab.binding.broadlink.internal.Utils;
+import org.openhab.binding.broadlink.internal.discovery.DeviceRediscoveryAgent;
+import org.openhab.binding.broadlink.internal.discovery.DeviceRediscoveryListener;
+import org.openhab.binding.broadlink.internal.socket.NetworkTrafficObserver;
+import org.openhab.binding.broadlink.internal.socket.RetryableSocket;
+import org.openhab.core.library.types.QuantityType;
+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.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract superclass of all supported Broadlink devices.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public abstract class BroadlinkBaseThingHandler extends BaseThingHandler implements DeviceRediscoveryListener {
+
+ public final Logger logger = LoggerFactory.getLogger(getClass());
+ private static final String INITIAL_DEVICE_ID = "00000000";
+
+ protected BroadlinkDeviceConfiguration thingConfig = new BroadlinkDeviceConfiguration();
+
+ private @Nullable RetryableSocket socket;
+
+ // Can be injected for test purposes
+ private @Nullable NetworkTrafficObserver networkTrafficObserver;
+
+ private int count;
+ private @Nullable ScheduledFuture> refreshHandle;
+ private boolean authenticated = false;
+ // These get handed to us by the device after successful authentication:
+ private byte[] deviceId;
+ private byte[] deviceKey;
+
+ public BroadlinkBaseThingHandler(Thing thing) {
+ super(thing);
+ logger.debug("constructed: resetting deviceKey to '{}', length {}",
+ BroadlinkBindingConstants.BROADLINK_AUTH_KEY, BroadlinkBindingConstants.BROADLINK_AUTH_KEY.length());
+ this.deviceId = HexUtils.hexToBytes(INITIAL_DEVICE_ID);
+ this.deviceKey = HexUtils.hexToBytes(BroadlinkBindingConstants.BROADLINK_AUTH_KEY);
+ }
+
+ // For test purposes
+ void setSocket(RetryableSocket socket) {
+ this.socket = socket;
+ }
+
+ void setNetworkTrafficObserver(NetworkTrafficObserver networkTrafficObserver) {
+ this.networkTrafficObserver = networkTrafficObserver;
+ }
+
+ private boolean hasAuthenticated() {
+ return authenticated;
+ }
+
+ public void initialize() {
+ logger.debug("initializing handler");
+
+ this.thingConfig = getConfigAs(BroadlinkDeviceConfiguration.class);
+ count = (new Random()).nextInt(65535);
+
+ if (this.socket == null) {
+ this.socket = new RetryableSocket(thingConfig, logger);
+ }
+
+ updateItemStatus();
+
+ if (thingConfig.getPollingInterval() != 0) {
+ refreshHandle = scheduler.scheduleWithFixedDelay(new Runnable() {
+
+ public void run() {
+ updateItemStatus();
+ }
+ }, 1L, thingConfig.getPollingInterval(), TimeUnit.SECONDS);
+ }
+ }
+
+ @SuppressWarnings("null")
+ public void dispose() {
+ logger.debug("Disposing of {}", getThing().getLabel());
+ if (refreshHandle != null && !refreshHandle.isDone()) {
+ logger.debug("Cancelling refresh task");
+ boolean cancelled = refreshHandle.cancel(true);
+ logger.debug("Cancellation successful: {}", cancelled);
+ }
+ if (socket != null) {
+ socket.close();
+ socket = null;
+ }
+ super.dispose();
+ }
+
+ private boolean authenticate() {
+ logger.trace("Authenticating");
+ authenticated = false;
+ // When authenticating, we must ALWAYS use the initial values
+ this.deviceId = HexUtils.hexToBytes(INITIAL_DEVICE_ID);
+ this.deviceKey = HexUtils.hexToBytes(BroadlinkBindingConstants.BROADLINK_AUTH_KEY);
+
+ try {
+ byte authRequest[] = buildMessage((byte) 0x65, BroadlinkProtocol.buildAuthenticationPayload(), -1);
+ byte response[] = sendAndReceiveDatagram(authRequest, "authentication");
+ if (response == null) {
+ logger.warn("response from device during authentication was null");
+ return false;
+ }
+ byte decryptResponse[] = decodeDevicePacket(response);
+ this.deviceId = BroadlinkProtocol.getDeviceId(decryptResponse);
+ this.deviceKey = BroadlinkProtocol.getDeviceKey(decryptResponse);
+
+ // Update the properties, so that these values can be seen in the UI:
+ Map properties = editProperties();
+ properties.put("id", HexUtils.bytesToHex(deviceId));
+ properties.put("key", HexUtils.bytesToHex(deviceKey));
+ updateProperties(properties);
+ logger.debug("Authenticated with id '{}' and key '{}'", HexUtils.bytesToHex(deviceId),
+ HexUtils.bytesToHex(deviceKey));
+ authenticated = true;
+ return true;
+ } catch (Exception e) {
+ logger.warn("Authentication failed: ", e);
+ return false;
+ }
+ }
+
+ @SuppressWarnings("null")
+ protected byte @Nullable [] sendAndReceiveDatagram(byte message[], String purpose) {
+ return socket.sendAndReceive(message, purpose);
+ }
+
+ protected byte[] buildMessage(byte command, byte payload[]) throws IOException {
+ return buildMessage(command, payload, thingConfig.getDeviceType());
+ }
+
+ @SuppressWarnings("null")
+ private byte[] buildMessage(byte command, byte payload[], int deviceType) throws IOException {
+ count = count + 1 & 0xffff;
+
+ if (networkTrafficObserver != null) {
+ networkTrafficObserver.onCommandSent(command);
+ networkTrafficObserver.onBytesSent(payload);
+ }
+
+ return BroadlinkProtocol.buildMessage(command, payload, count, thingConfig.getMacAddress(), deviceId,
+ HexUtils.hexToBytes(BroadlinkBindingConstants.BROADLINK_IV), deviceKey, deviceType, logger);
+ }
+
+ @SuppressWarnings("null")
+ protected byte[] decodeDevicePacket(byte[] responseBytes) throws IOException {
+ byte[] rxBytes = BroadlinkProtocol.decodePacket(responseBytes, this.deviceKey,
+ BroadlinkBindingConstants.BROADLINK_IV);
+
+ if (networkTrafficObserver != null) {
+ networkTrafficObserver.onBytesReceived(rxBytes);
+ }
+ return rxBytes;
+ }
+
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ logger.trace("Refresh requested, updating item status ...");
+
+ updateItemStatus();
+ }
+ }
+
+ // Can be implemented by devices that should do something on being found; e.g. perform a first status query
+ protected boolean onBroadlinkDeviceBecomingReachable() {
+ return true;
+ }
+
+ // Implemented by devices that can update the openHAB state
+ // model. Return false if something went wrong that requires
+ // a change in the device's online state
+ protected boolean getStatusFromDevice() {
+ return true;
+ }
+
+ public void updateItemStatus() {
+ logger.trace("updateItemStatus; checking host availability at {}", thingConfig.getIpAddress());
+ if (NetworkUtils.hostAvailabilityCheck(thingConfig.getIpAddress(), 3000, logger)) {
+ logger.trace("updateItemStatus; host found at {}", thingConfig.getIpAddress());
+ if (!Utils.isOnline(getThing())) {
+ logger.trace("updateItemStatus; device not currently online, resolving");
+ transitionToOnline();
+ } else {
+ // Normal operation ...
+ boolean gotStatusOk = getStatusFromDevice();
+ if (!gotStatusOk) {
+ if (thingConfig.isIgnoreFailedUpdates()) {
+ logger.warn(
+ "Problem getting status. Not marking offline because configured to ignore failed updates ...");
+ } else {
+ forceOffline(ThingStatusDetail.GONE, "Problem getting status");
+ }
+ }
+ }
+ } else {
+ if (thingConfig.isStaticIp()) {
+ if (!Utils.isOffline(getThing())) {
+ forceOffline(ThingStatusDetail.NONE, "Couldn't find statically-IP-addressed device");
+ }
+ } else {
+ logger.debug("Dynamic IP device not found at {}, will search...", thingConfig.getIpAddress());
+ DeviceRediscoveryAgent dra = new DeviceRediscoveryAgent(thingConfig, this);
+ dra.attemptRediscovery();
+ logger.debug("Asynchronous dynamic IP device search initiated...");
+ }
+ }
+ }
+
+ public void onDeviceRediscovered(String newIpAddress) {
+ logger.debug("Rediscovered this device at IP {}", newIpAddress);
+ thingConfig.setIpAddress(newIpAddress);
+ transitionToOnline();
+ }
+
+ public void onDeviceRediscoveryFailure() {
+ if (!Utils.isOffline(getThing())) {
+ forceOffline(ThingStatusDetail.NONE,
+ "Couldn't rediscover dynamically-IP-addressedv device after network scan");
+ }
+ }
+
+ private void transitionToOnline() {
+ if (!hasAuthenticated()) {
+ logger.debug(
+ "We've never actually successfully authenticated with this device in this session. Doing so now");
+ if (authenticate()) {
+ logger.debug("Authenticated with newly-detected device, will now get its status");
+ } else {
+ forceOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Couldn't authenticate");
+ return;
+ }
+ }
+ if (onBroadlinkDeviceBecomingReachable()) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ forceOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Trouble getting status");
+ }
+ }
+
+ @SuppressWarnings("null")
+ private void forceOffline(ThingStatusDetail detail, String reason) {
+ logger.warn("Online -> Offline due to: {}", reason);
+ authenticated = false; // This session is dead; we'll need to re-authenticate next time
+ updateStatus(ThingStatus.OFFLINE, detail, reason);
+ if (socket != null) {
+ socket.close();
+ }
+ }
+
+ protected void updateTemperature(double temperature) {
+ updateState(CHANNEL_TEMPERATURE, new QuantityType<>(temperature, BROADLINK_TEMPERATURE_UNIT));
+ }
+
+ protected void updateHumidity(double humidity) {
+ updateState(CHANNEL_HUMIDITY, new QuantityType<>(humidity, BROADLINK_HUMIDITY_UNIT));
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteHandler.java
new file mode 100644
index 0000000000000..dd0a1ada5aa95
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteHandler.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.openhab.binding.broadlink.internal.BroadlinkMappingService;
+import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider;
+import org.openhab.binding.broadlink.internal.Utils;
+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.type.ChannelTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.util.HexUtils;
+
+/**
+ * Remote blaster handler superclass
+ *
+ * @author Cato Sognen - Initial contribution
+ * @author John Marshall - V3 rewrite with dynamic command description provider
+ */
+@NonNullByDefault
+public abstract class BroadlinkRemoteHandler extends BroadlinkBaseThingHandler {
+
+ public static final byte COMMAND_BYTE_SEND_CODE = 0x02;
+ public static final byte COMMAND_BYTE_ENTER_LEARNING = 0x03;
+ public static final byte COMMAND_BYTE_CHECK_LEARNT_DATA = 0x04;
+
+ private final BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider;
+ private @Nullable BroadlinkMappingService mappingService;
+
+ public BroadlinkRemoteHandler(Thing thing,
+ BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider) {
+ super(thing);
+ this.commandDescriptionProvider = commandDescriptionProvider;
+ }
+
+ public void initialize() {
+ super.initialize();
+ this.mappingService = new BroadlinkMappingService(thingConfig.getMapFilename(), commandDescriptionProvider,
+ new ChannelUID(thing.getUID(), BroadlinkBindingConstants.COMMAND_CHANNEL));
+ }
+
+ @SuppressWarnings("null")
+ public void dispose() {
+ if (this.mappingService != null) {
+ this.mappingService.dispose();
+ }
+ super.dispose();
+ }
+
+ protected byte @Nullable [] sendCommand(byte commandByte, String purpose) {
+ return sendCommand(commandByte, new byte[0], purpose);
+ }
+
+ private byte @Nullable [] sendCommand(byte commandByte, byte[] codeBytes, String purpose) {
+ try {
+ ByteArrayOutputStream outputStream = buildCommandMessage(commandByte, codeBytes);
+ byte[] padded = Utils.padTo(outputStream.toByteArray(), 16);
+ byte[] message = buildMessage((byte) 0x6a, padded);
+ return sendAndReceiveDatagram(message, purpose);
+ } catch (IOException e) {
+ logger.warn("Exception while sending command", e);
+ }
+
+ return null;
+ }
+
+ protected void sendCode(byte[] code) {
+ sendCommand(COMMAND_BYTE_SEND_CODE, code, "send remote code");
+ }
+
+ protected ByteArrayOutputStream buildCommandMessage(byte commandByte, byte[] codeBytes) throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ byte[] preamble = new byte[4];
+ preamble[0] = commandByte;
+ outputStream.write(preamble);
+ if (codeBytes.length > 0) {
+ outputStream.write(codeBytes);
+ }
+
+ return outputStream;
+ }
+
+ protected byte[] extractResponsePayload(byte[] responseBytes) throws IOException {
+ byte decryptedResponse[] = decodeDevicePacket(responseBytes);
+ // Interesting stuff begins at the fourth byte
+ return Utils.slice(decryptedResponse, 4, decryptedResponse.length);
+ }
+
+ private void sendCheckDataCommandAndLog() {
+ try {
+ byte[] response = sendCommand(COMMAND_BYTE_CHECK_LEARNT_DATA, "send learnt code check command");
+ if (response == null) {
+ logger.warn("Got nothing back while getting learnt code");
+ } else {
+ String hexString = Utils.toHexString(extractResponsePayload(response));
+ logger.info("BEGIN LAST LEARNT CODE");
+ logger.info("{}", hexString);
+ logger.info("END LAST LEARNT CODE ({} characters)", hexString.length());
+ }
+ } catch (IOException e) {
+ logger.warn("Exception while attempting to check learnt code", e);
+ }
+ }
+
+ void handleLearningCommand(String learningCommand) {
+ logger.trace("Sending learning-channel command {}", learningCommand);
+ switch (learningCommand) {
+ case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_LEARN:
+ sendCommand(COMMAND_BYTE_ENTER_LEARNING, "enter remote code learning mode");
+ break;
+ case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_CHECK:
+ sendCheckDataCommandAndLog();
+ break;
+ default:
+ logger.warn("Unrecognised learning channel command: {}", learningCommand);
+ }
+ }
+
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (!Utils.isOnline(getThing())) {
+ logger.debug("Can't handle command {} because handler for thing {} is not ONLINE", command,
+ getThing().getLabel());
+ return;
+ }
+ if (command instanceof RefreshType) {
+ updateItemStatus();
+ return;
+ }
+
+ Channel channel = thing.getChannel(channelUID.getId());
+ if (channel == null) {
+ logger.warn("Unexpected null channel while handling command {}", command.toFullString());
+ return;
+ }
+ ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
+ if (channelTypeUID == null) {
+ logger.warn("Unexpected null channelTypeUID while handling command {}", command.toFullString());
+ return;
+ }
+ switch (channelTypeUID.getId()) {
+ case BroadlinkBindingConstants.COMMAND_CHANNEL: {
+ logger.debug("Handling ir/rf command '{}' on channel {} of thing {}", command, channelUID.getId(),
+ getThing().getLabel());
+ byte code[] = lookupCode(command, channelUID);
+ if (code != null) {
+ sendCode(code);
+ }
+ break;
+ }
+ case BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL:
+ handleLearningCommand(command.toString());
+ break;
+ default:
+ logger.debug("Thing {} has unknown channel type '{}'", getThing().getLabel(), channelTypeUID.getId());
+ }
+ }
+
+ @SuppressWarnings("null")
+ private byte @Nullable [] lookupCode(Command command, ChannelUID channelUID) {
+ byte code[] = null;
+ String value = this.mappingService.lookup(command.toString());
+
+ if (value == null || value.isEmpty()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "No entries found for command in map file, or the file is missing.");
+ return null;
+ }
+
+ code = HexUtils.hexToBytes(value);
+
+ logger.debug("Transformed command '{}' for thing {}", command, getThing().getLabel());
+ return code;
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel2Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel2Handler.java
new file mode 100644
index 0000000000000..0f2c588d5622f
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel2Handler.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Remote blaster handler
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkRemoteModel2Handler extends BroadlinkRemoteHandler {
+
+ public BroadlinkRemoteModel2Handler(Thing thing,
+ BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider) {
+ super(thing, commandDescriptionProvider);
+ }
+
+ protected boolean onBroadlinkDeviceBecomingReachable() {
+ return getStatusFromDevice();
+ }
+
+ protected boolean getStatusFromDevice() {
+ try {
+ byte payload[] = new byte[16];
+ payload[0] = 1;
+ byte message[] = buildMessage((byte) 0x6a, payload);
+ byte response[] = sendAndReceiveDatagram(message, "RM2 device status");
+ if (response == null) {
+ logger.warn("response from RM2 device was null");
+ return false;
+ }
+ byte decodedPayload[] = decodeDevicePacket(response);
+ double temperature = ((double) (decodedPayload[4] * 10 + decodedPayload[5]) / 10D);
+ updateTemperature(temperature);
+ return true;
+ } catch (Exception e) {
+ logger.warn("Could not get status: ", e);
+ return false;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel3Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel3Handler.java
new file mode 100644
index 0000000000000..2403a606ff6f4
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel3Handler.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Remote blaster handler for RM mini 3 devices
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkRemoteModel3Handler extends BroadlinkRemoteHandler {
+
+ public BroadlinkRemoteModel3Handler(Thing thing,
+ BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider) {
+ super(thing, commandDescriptionProvider);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel3V44057Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel3V44057Handler.java
new file mode 100644
index 0000000000000..1b12ac1cfbf9b
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel3V44057Handler.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Supports quirks in V44057 firmware.
+ *
+ * @author Stewart Cossey - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkRemoteModel3V44057Handler extends BroadlinkRemoteModel4Handler {
+
+ public BroadlinkRemoteModel3V44057Handler(Thing thing,
+ BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider) {
+ super(thing, commandDescriptionProvider);
+ }
+
+ protected boolean getStatusFromDevice() {
+ return true;
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel4Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel4Handler.java
new file mode 100644
index 0000000000000..6c95fe2307543
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel4Handler.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider;
+import org.openhab.binding.broadlink.internal.Utils;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Remote blaster handler, "generation" 4
+ *
+ * These devices place a 6-byte preamble before their payload.
+ * The format is:
+ * Byte 00 01 02 03 04 05
+ * PLl PLh CMD 00 00 00
+ *
+ * Where PL is the 16-bit unsigned length of the payload PLUS 4 BYTES
+ * so PLl is the lower byte and PLh is the high byte
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkRemoteModel4Handler extends BroadlinkRemoteHandler {
+
+ public BroadlinkRemoteModel4Handler(Thing thing,
+ BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider) {
+ super(thing, commandDescriptionProvider);
+ }
+
+ protected boolean onBroadlinkDeviceBecomingReachable() {
+ return getStatusFromDevice();
+ }
+
+ protected boolean getStatusFromDevice() {
+ try {
+ // These devices use a 2-byte preamble to the normal protocol;
+ // https://github.com/mjg59/python-broadlink/blob/0bd58c6f598fe7239246ad9d61508febea625423/broadlink/__init__.py#L666
+ byte[] response = sendCommand((byte) 0x24, "RM4 device status"); // Status check is now Ox24, not 0x01 as in
+ // earlier devices
+ if (response == null) {
+ logger.warn("response from RM4 device was null");
+ return false;
+ }
+ byte decodedPayload[] = extractResponsePayload(response);
+
+ // Temps and humidity get divided by 100 now, not 10
+ double temperature = ((double) (decodedPayload[0] * 100 + decodedPayload[1]) / 100D);
+ updateTemperature(temperature);
+ double humidity = ((double) (decodedPayload[2] * 100 + decodedPayload[3]) / 100D);
+ updateHumidity(humidity);
+ return true;
+ } catch (Exception e) {
+ logger.warn("Could not get status: ", e);
+ return false;
+ }
+ }
+
+ // These devices use a 6-byte sendCode preamble instead of the previous 4:
+ // https://github.com/mjg59/python-broadlink/blob/822b3c326631c1902b5892a83db126291acbf0b6/broadlink/remote.py#L78
+ @Override
+ protected ByteArrayOutputStream buildCommandMessage(byte commandByte, byte[] codeBytes) throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ byte[] preamble = new byte[6];
+ int length = codeBytes.length + 4;
+ preamble[0] = (byte) (length & 0xFF);
+ preamble[1] = (byte) ((length >> 8) & 0xFF);
+ preamble[2] = commandByte;
+ outputStream.write(preamble);
+ if (codeBytes.length > 0) {
+ outputStream.write(codeBytes);
+ }
+
+ return outputStream;
+ }
+
+ // Interesting stuff begins at the 6th byte, and runs for the length indicated
+ // in the first two bytes of the response (little-endian) + 2, as opposed to
+ // whatever the "natural" decrypted length is
+ @Override
+ protected byte[] extractResponsePayload(byte[] responseBytes) throws IOException {
+ byte decryptedResponse[] = decodeDevicePacket(responseBytes);
+ int lsb = decryptedResponse[0] & 0xFF;
+ int msb = decryptedResponse[1] & 0xFF;
+ int payloadLength = (msb << 8) + lsb;
+ return Utils.slice(decryptedResponse, 6, payloadLength + 2);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketHandler.java
new file mode 100644
index 0000000000000..7f11226362505
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketHandler.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+
+/**
+ * Abstract superclass for power socket devices
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public abstract class BroadlinkSocketHandler extends BroadlinkBaseThingHandler {
+
+ public BroadlinkSocketHandler(Thing thing) {
+ super(thing);
+ }
+
+ protected abstract void setStatusOnDevice(int state) throws IOException;
+
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ try {
+ if (channelUID.getId().equals("powerOn")) {
+ if (command == OnOffType.ON) {
+ setStatusOnDevice(1);
+ } else if (command == OnOffType.OFF) {
+ setStatusOnDevice(0);
+ }
+ }
+ } catch (IOException e) {
+ logger.warn("Could not send command to socket device", e);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel1Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel1Handler.java
new file mode 100644
index 0000000000000..d2d6b12b97e5a
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel1Handler.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Smart power socket handler
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkSocketModel1Handler extends BroadlinkSocketHandler {
+
+ public BroadlinkSocketModel1Handler(Thing thing) {
+ super(thing);
+ }
+
+ public void setStatusOnDevice(int state) throws IOException {
+ byte payload[] = new byte[16];
+ payload[0] = (byte) state;
+ byte message[] = buildMessage((byte) 102, payload);
+ sendAndReceiveDatagram(message, "Setting SP1 status");
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel2Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel2Handler.java
new file mode 100644
index 0000000000000..e09289b098367
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel2Handler.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.*;
+
+import java.io.IOException;
+
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+
+/**
+ * Smart power socket handler with optional power consumption (SP2s)
+ *
+ * @author Cato Sognen - Initial contribution
+ * @author John Marshall - rework for correct SP2s handling
+ */
+@NonNullByDefault
+public class BroadlinkSocketModel2Handler extends BroadlinkSocketHandler {
+ protected boolean supportsPowerConsumptionMeasurement;
+
+ public BroadlinkSocketModel2Handler(Thing thing, boolean supportsPowerConsumptionMeasurement) {
+ super(thing);
+ this.supportsPowerConsumptionMeasurement = supportsPowerConsumptionMeasurement;
+ }
+
+ protected void setStatusOnDevice(int status) throws IOException {
+ byte payload[] = new byte[16];
+ payload[0] = 2;
+ payload[4] = (byte) status;
+ byte message[] = buildMessage((byte) 0x6a, payload);
+ sendAndReceiveDatagram(message, "Setting switch device status to " + status);
+ }
+
+ protected OnOffType deriveOnOffBitFromStatusPayload(byte[] statusPayload, byte mask) {
+ byte statusByte = statusPayload[4];
+ if ((statusByte & mask) == mask) {
+ return OnOffType.ON;
+ }
+ return OnOffType.OFF;
+ }
+
+ OnOffType derivePowerStateFromStatusBytes(byte[] statusPayload) {
+ return deriveOnOffBitFromStatusPayload(statusPayload, (byte) 0x01);
+ }
+
+ // https://github.com/mjg59/python-broadlink/blob/822b3c326631c1902b5892a83db126291acbf0b6/broadlink/switch.py#L186
+ double derivePowerConsumption(byte[] statusPayload) throws IOException {
+ if (statusPayload.length > 6) {
+ // Bytes are little-endian, at positions 4,5 and 6
+ int highByte = statusPayload[6] & 0xFF;
+ int midByte = statusPayload[5] & 0xFF;
+ int lowByte = statusPayload[4] & 0xFF;
+ int intValue = (highByte << 16) + (midByte << 8) + lowByte;
+ return (double) intValue / 1000;
+ }
+ return 0D;
+ }
+
+ protected int toPowerOnOffBits(Command powerOnOff) {
+ return powerOnOff == OnOffType.ON ? 0x01 : 0x00;
+ }
+
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ try {
+ if (channelUID.getId().equals(COMMAND_POWER_ON)) {
+ setStatusOnDevice(toPowerOnOffBits(command));
+ }
+ } catch (IOException e) {
+ logger.warn("Could not send command to socket device", e);
+ }
+ }
+
+ protected void updatePowerConsumption(double consumptionWatts) {
+ updateState(CHANNEL_POWER_CONSUMPTION,
+ new QuantityType(consumptionWatts, BROADLINK_POWER_CONSUMPTION_UNIT));
+ }
+
+ protected boolean getStatusFromDevice() {
+ try {
+ logger.trace("SP2/SP2s getting status...");
+ byte[] statusBytes = getStatusBytesFromDevice();
+ updateState(COMMAND_POWER_ON, derivePowerStateFromStatusBytes(statusBytes));
+ if (supportsPowerConsumptionMeasurement) {
+ updatePowerConsumption(derivePowerConsumption(statusBytes));
+ }
+ return true;
+ } catch (Exception ex) {
+ logger.warn("Exception while getting status from device", ex);
+ return false;
+ }
+ }
+
+ protected byte[] getStatusBytesFromDevice() throws IOException {
+ byte payload[] = new byte[16];
+ payload[0] = 1;
+ byte message[] = buildMessage((byte) 0x6a, payload);
+ byte response[] = sendAndReceiveDatagram(message, "SP2/SP2s status byte");
+ if (response == null) {
+ throw new IOException("No response while fetching status byte from SP2/SP2s device");
+ }
+ return decodeDevicePacket(response);
+ }
+
+ protected boolean onBroadlinkDeviceBecomingReachable() {
+ return getStatusFromDevice();
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3Handler.java
new file mode 100644
index 0000000000000..cfd1c1074953f
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3Handler.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.COMMAND_NIGHTLIGHT;
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.COMMAND_POWER_ON;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+
+/**
+ * Smart power socket handler with nightlight
+ *
+ * @author Cato Sognen - Initial contribution
+ * @author John Marshall - Nightlight support
+ */
+@NonNullByDefault
+public class BroadlinkSocketModel3Handler extends BroadlinkSocketModel2Handler {
+
+ public BroadlinkSocketModel3Handler(Thing thing) {
+ super(thing, false);
+ }
+
+ OnOffType deriveNightLightStateFromStatusBytes(byte[] statusPayload) {
+ return deriveOnOffBitFromStatusPayload(statusPayload, (byte) 0x02);
+ }
+
+ static int mergeOnOffBits(Command powerOnOff, Command nightLightOnOff) {
+ int powerBit = powerOnOff == OnOffType.ON ? 0x01 : 0x00;
+ int nightLightBit = nightLightOnOff == OnOffType.ON ? 0x02 : 0x00;
+ return powerBit | nightLightBit;
+ }
+
+ protected boolean getStatusFromDevice() {
+ try {
+ logger.debug("SP3 getting status...");
+ byte[] statusBytes = getStatusBytesFromDevice();
+ updateState(COMMAND_POWER_ON, derivePowerStateFromStatusBytes(statusBytes));
+ updateState(COMMAND_NIGHTLIGHT, deriveNightLightStateFromStatusBytes(statusBytes));
+ return true;
+ } catch (Exception ex) {
+ logger.warn("Exception while getting status from device", ex);
+ return false;
+ }
+ }
+
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ try {
+ // Always pull back the latest device status and merge it:
+ byte[] statusBytes = getStatusBytesFromDevice();
+ OnOffType powerStatus = derivePowerStateFromStatusBytes(statusBytes);
+ OnOffType nightLightStatus = deriveNightLightStateFromStatusBytes(statusBytes);
+
+ if (channelUID.getId().equals(COMMAND_POWER_ON)) {
+ setStatusOnDevice(mergeOnOffBits(command, nightLightStatus));
+ }
+
+ if (channelUID.getId().equals(COMMAND_NIGHTLIGHT)) {
+ setStatusOnDevice(mergeOnOffBits(powerStatus, command));
+ }
+ } catch (IOException e) {
+ logger.warn("Could not send command to SP3 device", e);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3SHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3SHandler.java
new file mode 100644
index 0000000000000..d399772e8c7c1
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3SHandler.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Smart power socket handler with power consumption measurement.
+ * Despite its name, it is more closely related to the SP2 than the SP3,
+ * as it doesn't have a nightlight.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkSocketModel3SHandler extends BroadlinkSocketModel2Handler {
+
+ public BroadlinkSocketModel3SHandler(Thing thing) {
+ super(thing, true);
+ }
+
+ // The SP3S device needs to be explicitly
+ // asked to obtain the power consumption (as opposed to the SP2S)
+ // and it has other quirks too
+ // @see
+ // https://github.com/mjg59/python-broadlink/blob/822b3c326631c1902b5892a83db126291acbf0b6/broadlink/switch.py#L247
+ @Override
+ double derivePowerConsumption(byte[] unusedPayload) throws IOException {
+ byte payload[] = { 8, 0, (byte) 254, 1, 5, 1, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0 };
+ byte message[] = buildMessage((byte) 0x6a, payload);
+ byte response[] = sendAndReceiveDatagram(message, "SP3s power consumption byte");
+ if (response != null) {
+ byte consumptionResponsePayload[] = decodeDevicePacket(response);
+ return deriveSP3sPowerConsumption(consumptionResponsePayload);
+ }
+ return 0D;
+ }
+
+ // Reading between the lines at:
+ // https://github.com/mjg59/python-broadlink/issues/492
+ // It appears that this is basically BCD with the major part
+ // of the wattage as LSB in payload[7] and payload[6] and the decimal part in payload[5]
+ // so for example, with a payload: 0 0 0 0 0x33 0x75 0x00
+ // this actually should be interpreted as 75.33W!
+ // For a larger example:
+ // 0 0 0 0 0x44 0x66 0x02
+ // Would be 266.44W
+ double deriveSP3sPowerConsumption(byte[] consumptionResponsePayload) {
+ if (consumptionResponsePayload.length > 7) {
+ return (fromBCD(consumptionResponsePayload[7]) * 100) + fromBCD(consumptionResponsePayload[6])
+ + (fromBCD(consumptionResponsePayload[5]) / 100);
+ }
+ return 0D;
+ }
+
+ private double fromBCD(byte bcdDigit) {
+ int highNibble = ((int) bcdDigit & 0xF0) >> 4;
+ int lowNibble = bcdDigit & 0x0F;
+ return highNibble * 10 + lowNibble;
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkStripModel11K3S2UHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkStripModel11K3S2UHandler.java
new file mode 100644
index 0000000000000..e2fbbd2240725
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkStripModel11K3S2UHandler.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+
+/**
+ * Multiple power socket plus USB strip device - 3 AC outlets and 2 USB outlets
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkStripModel11K3S2UHandler extends BroadlinkBaseThingHandler {
+
+ public BroadlinkStripModel11K3S2UHandler(Thing thing) {
+ super(thing);
+ }
+
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ updateItemStatus();
+ return;
+ }
+
+ switch (channelUID.getId()) {
+ case "s1powerOn":
+ interpretCommandForSocket(1, command);
+ break;
+ case "s2powerOn":
+ interpretCommandForSocket(2, command);
+ break;
+ case "s3powerOn":
+ interpretCommandForSocket(3, command);
+ break;
+ case "usbPowerOn":
+ interpretCommandForSocket(4, command);
+ break;
+ default:
+ break;
+
+ }
+ }
+
+ private void interpretCommandForSocket(int sid, Command command) {
+ try {
+ if (command == OnOffType.ON) {
+ setStatusOnDevice((byte) sid, (byte) 1);
+ } else if (command == OnOffType.OFF) {
+ setStatusOnDevice((byte) sid, (byte) 0);
+ }
+ } catch (IOException e) {
+ logger.warn("Couldn't interpret command for strip device MP13K2U", e);
+ }
+ }
+
+ private void setStatusOnDevice(byte sid, byte state) throws IOException {
+ int sidMask = 1 << sid - 1;
+ byte payload[] = new byte[16];
+ payload[0] = 13;
+ payload[2] = -91;
+ payload[3] = -91;
+ payload[4] = 90;
+ payload[5] = 90;
+ if (state == 1) {
+ payload[6] = (byte) (178 + (sidMask << 1));
+ } else {
+ payload[6] = (byte) (byte) (178 + sidMask);
+ }
+ payload[7] = -64;
+ payload[8] = 2;
+ payload[10] = 3;
+ payload[13] = (byte) sidMask;
+ if (state == 1) {
+ payload[14] = (byte) sidMask;
+ } else {
+ payload[14] = 0;
+ }
+ byte message[] = buildMessage((byte) 106, payload);
+ sendAndReceiveDatagram(message, "Setting MP13K2U status");
+ }
+
+ protected boolean getStatusFromDevice() {
+ byte payload[] = new byte[16];
+ payload[0] = 10;
+ payload[2] = -91;
+ payload[3] = -91;
+ payload[4] = 90;
+ payload[5] = 90;
+ payload[6] = -82;
+ payload[7] = -64;
+ payload[8] = 1;
+ try {
+ byte message[] = buildMessage((byte) 106, payload);
+ byte response[] = sendAndReceiveDatagram(message, "status for MP13K2U strip");
+ if (response == null) {
+ logger.warn("response from MP13K2U strip device was null");
+ return false;
+ }
+ byte decodedPayload[] = decodeDevicePacket(response);
+ final int status = decodedPayload[14];
+
+ this.updateState("s1powerOn", (status & 0x01) == 0x01 ? OnOffType.ON : OnOffType.OFF);
+ this.updateState("s2powerOn", (status & 0x02) == 0x02 ? OnOffType.ON : OnOffType.OFF);
+ this.updateState("s3powerOn", (status & 0x04) == 0x04 ? OnOffType.ON : OnOffType.OFF);
+ this.updateState("usbPowerOn", (status & 0x08) == 0x08 ? OnOffType.ON : OnOffType.OFF);
+ } catch (Exception ex) {
+ logger.warn("Exception while getting status from device", ex);
+ return false;
+ }
+ return true;
+ }
+
+ protected boolean onBroadlinkDeviceBecomingReachable() {
+ return getStatusFromDevice();
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkStripModel1Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkStripModel1Handler.java
new file mode 100644
index 0000000000000..753fc9b2830cb
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkStripModel1Handler.java
@@ -0,0 +1,132 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+
+/**
+ * Multiple power socket strip device
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkStripModel1Handler extends BroadlinkBaseThingHandler {
+
+ public BroadlinkStripModel1Handler(Thing thing) {
+ super(thing);
+ }
+
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ updateItemStatus();
+ return;
+ }
+
+ switch (channelUID.getId()) {
+ case "s1powerOn":
+ interpretCommandForSocket(1, command);
+ break;
+ case "s2powerOn":
+ interpretCommandForSocket(2, command);
+ break;
+ case "s3powerOn":
+ interpretCommandForSocket(3, command);
+ break;
+ case "s4powerOn":
+ interpretCommandForSocket(4, command);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void interpretCommandForSocket(int sid, Command command) {
+ try {
+ if (command == OnOffType.ON) {
+ setStatusOnDevice((byte) sid, (byte) 1);
+ } else if (command == OnOffType.OFF) {
+ setStatusOnDevice((byte) sid, (byte) 0);
+ }
+ } catch (IOException e) {
+ logger.warn("Couldn't interpret command for strip device", e);
+ }
+ }
+
+ private void setStatusOnDevice(byte sid, byte state) throws IOException {
+ int sidMask = 1 << sid - 1;
+ byte payload[] = new byte[16];
+ payload[0] = 13;
+ payload[2] = -91;
+ payload[3] = -91;
+ payload[4] = 90;
+ payload[5] = 90;
+ if (state == 1) {
+ payload[6] = (byte) (178 + (sidMask << 1));
+ } else {
+ payload[6] = (byte) (byte) (178 + sidMask);
+ }
+ payload[7] = -64;
+ payload[8] = 2;
+ payload[10] = 3;
+ payload[13] = (byte) sidMask;
+ if (state == 1) {
+ payload[14] = (byte) sidMask;
+ } else {
+ payload[14] = 0;
+ }
+ byte message[] = buildMessage((byte) 106, payload);
+ sendAndReceiveDatagram(message, "Setting MPx status");
+ }
+
+ protected boolean getStatusFromDevice() {
+ byte payload[] = new byte[16];
+ payload[0] = 10;
+ payload[2] = -91;
+ payload[3] = -91;
+ payload[4] = 90;
+ payload[5] = 90;
+ payload[6] = -82;
+ payload[7] = -64;
+ payload[8] = 1;
+ try {
+ byte message[] = buildMessage((byte) 106, payload);
+ byte response[] = sendAndReceiveDatagram(message, "status for strip");
+ if (response == null) {
+ logger.warn("response from strip device was null");
+ return false;
+ }
+ byte decodedPayload[] = decodeDevicePacket(response);
+ final int status = decodedPayload[14];
+
+ this.updateState("s1powerOn", (status & 1) == 1 ? OnOffType.ON : OnOffType.OFF);
+ this.updateState("s2powerOn", (status & 2) == 2 ? OnOffType.ON : OnOffType.OFF);
+ this.updateState("s3powerOn", (status & 4) == 4 ? OnOffType.ON : OnOffType.OFF);
+ this.updateState("s4powerOn", (status & 8) == 8 ? OnOffType.ON : OnOffType.OFF);
+ } catch (Exception ex) {
+ logger.warn("Exception while getting status from device", ex);
+ return false;
+ }
+ return true;
+ }
+
+ protected boolean onBroadlinkDeviceBecomingReachable() {
+ return getStatusFromDevice();
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkStripModel2Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkStripModel2Handler.java
new file mode 100644
index 0000000000000..e3576ad9c9805
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/handler/BroadlinkStripModel2Handler.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Multiple power socket strip device
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkStripModel2Handler extends BroadlinkSocketModel2Handler {
+
+ public BroadlinkStripModel2Handler(Thing thing) {
+ super(thing, false);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkBindingConstants.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkBindingConstants.java
new file mode 100644
index 0000000000000..a885190a5b1a5
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkBindingConstants.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link BroadlinkBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkBindingConstants {
+
+ private static final String BINDING_ID = "broadlink";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_SAMPLE = new ThingTypeUID(BINDING_ID, "sample");
+
+ // List of all Channel ids
+ public static final String CHANNEL_1 = "channel1";
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkHandlerFactory.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkHandlerFactory.java
new file mode 100644
index 0000000000000..28266ce6adf7f
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkHandlerFactory.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.openhab.binding.broadlink.handler.BroadlinkA1Handler;
+import org.openhab.binding.broadlink.handler.BroadlinkRemoteModel2Handler;
+import org.openhab.binding.broadlink.handler.BroadlinkRemoteModel3Handler;
+import org.openhab.binding.broadlink.handler.BroadlinkRemoteModel3V44057Handler;
+import org.openhab.binding.broadlink.handler.BroadlinkRemoteModel4Handler;
+import org.openhab.binding.broadlink.handler.BroadlinkSocketModel1Handler;
+import org.openhab.binding.broadlink.handler.BroadlinkSocketModel2Handler;
+import org.openhab.binding.broadlink.handler.BroadlinkSocketModel3Handler;
+import org.openhab.binding.broadlink.handler.BroadlinkSocketModel3SHandler;
+import org.openhab.binding.broadlink.handler.BroadlinkStripModel11K3S2UHandler;
+import org.openhab.binding.broadlink.handler.BroadlinkStripModel1Handler;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link BroadlinkHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = ThingHandlerFactory.class, configurationPid = "binding.broadlink")
+public class BroadlinkHandlerFactory extends BaseThingHandlerFactory {
+
+ private final Logger logger = LoggerFactory.getLogger(BroadlinkHandlerFactory.class);
+ private final BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider;
+
+ @Activate
+ public BroadlinkHandlerFactory(
+ final @Reference BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider) {
+ this.commandDescriptionProvider = commandDescriptionProvider;
+ }
+
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return BroadlinkBindingConstants.SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.keySet().contains(thingTypeUID);
+ }
+
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Creating Thing handler for '{}'", thingTypeUID.getAsString());
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_RM2)) {
+ logger.debug("RM 2 handler requested created");
+ return new BroadlinkRemoteModel2Handler(thing, commandDescriptionProvider);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_RM3)) {
+ logger.debug("RM 3 handler requested created");
+ return new BroadlinkRemoteModel3Handler(thing, commandDescriptionProvider);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_RM3Q)) {
+ logger.debug("RM 3 v11057 handler requested created");
+ return new BroadlinkRemoteModel3V44057Handler(thing, commandDescriptionProvider);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_RM4)) {
+ logger.debug("RM 4 handler requested created");
+ return new BroadlinkRemoteModel4Handler(thing, commandDescriptionProvider);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_A1)) {
+ logger.debug("A1 handler requested created");
+ return new BroadlinkA1Handler(thing);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_MP1)) {
+ return new BroadlinkStripModel1Handler(thing);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_MP1_1K3S2U)) {
+ return new BroadlinkStripModel11K3S2UHandler(thing);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_SP1)) {
+ return new BroadlinkSocketModel1Handler(thing);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_SP2)) {
+ return new BroadlinkSocketModel2Handler(thing, false);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_SP2S)) {
+ return new BroadlinkSocketModel2Handler(thing, true);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_SP3)) {
+ return new BroadlinkSocketModel3Handler(thing);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_SP3S)) {
+ return new BroadlinkSocketModel3SHandler(thing);
+ }
+ if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_MP2)) {
+ return new BroadlinkStripModel1Handler(thing);
+ } else {
+ logger.warn("Can't create handler for thing type UID: {}", thingTypeUID.getAsString());
+ return null;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkMappingService.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkMappingService.java
new file mode 100644
index 0000000000000..9e67ff95e2e10
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkMappingService.java
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.OpenHAB;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.CommandOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This Broadlink-specific mapper watches the given map file and loads the contents
+ * into a Map in order to offer keys that it then dynamically supplies to the provided
+ * BroadlinkRemoteDynamicCommandDescriptionProvider.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkMappingService {
+ private static final String TRANSFORM_DIR = OpenHAB.getConfigFolder() + File.separator + "transform"
+ + File.separator;
+ private final Logger logger = LoggerFactory.getLogger(BroadlinkMappingService.class);
+ private final String mapFileName;
+ private final BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider;
+ private final ChannelUID targetChannelUID;
+ private final Map commandMap = new HashMap<>();
+
+ private @Nullable WatchService watchService = null;
+ private @Nullable WatchKey transformDirWatchKey = null;
+ private @Nullable Thread watchThread = null;
+
+ public BroadlinkMappingService(String mapFileName,
+ BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider, ChannelUID targetChannelUID) {
+ this.mapFileName = mapFileName;
+ this.commandDescriptionProvider = commandDescriptionProvider;
+ this.targetChannelUID = targetChannelUID;
+ reloadFromFile();
+ startWatching();
+ logger.debug("BroadlinkMappingService constructed on behalf of {}", this.targetChannelUID);
+ }
+
+ @SuppressWarnings("null")
+ public void dispose() {
+ try {
+ if (watchThread != null && !watchThread.isInterrupted()) {
+ watchThread.interrupt();
+ watchThread = null;
+ }
+ if (this.transformDirWatchKey != null) {
+ this.transformDirWatchKey.cancel();
+ this.transformDirWatchKey = null;
+ }
+ if (this.watchService != null) {
+ this.watchService.close();
+ this.watchService = null;
+ }
+ } catch (IOException ioe) {
+ logger.warn("Cannot deactivate watcher", ioe);
+ }
+ }
+
+ public @Nullable String lookup(String command) {
+ return commandMap.get(command);
+ }
+
+ @SuppressWarnings({ "null", "unchecked" })
+ private Runnable watchingRunnable = new Runnable() {
+ @Override
+ public void run() {
+ while (!Thread.currentThread().isInterrupted()) {
+ try {
+ WatchKey key = watchService.take(); // Blocks
+ List> events = key.pollEvents();
+
+ Stream> modificationEvents = events.stream()
+ .filter(e -> e.kind() == StandardWatchEventKinds.ENTRY_MODIFY);
+
+ if (modificationEvents
+ .anyMatch(e -> ((WatchEvent) e).context().toString().equals(mapFileName))) {
+ logger.debug("File {} has changed - reloading", mapFileName);
+ reloadFromFile();
+ }
+ key.reset();
+ } catch (InterruptedException x) {
+ return;
+ }
+ }
+ }
+ };
+
+ @SuppressWarnings("null")
+ private void startWatching() {
+ try {
+ this.watchService = FileSystems.getDefault().newWatchService();
+ this.transformDirWatchKey = Paths.get(TRANSFORM_DIR).register(watchService,
+ StandardWatchEventKinds.ENTRY_MODIFY);
+ this.watchThread = new Thread(watchingRunnable, "BroadlinkMappingService-mapfile-watcher");
+ this.watchThread.setDaemon(true);
+ this.watchThread.start();
+ } catch (IOException ioe) {
+ logger.warn("Couldn't setup automatic watch:", ioe);
+ }
+ }
+
+ private void reloadFromFile() {
+ Properties props = new Properties();
+ Path mapFilePath = Paths.get(TRANSFORM_DIR + mapFileName);
+ try (FileReader reader = new FileReader(mapFilePath.toFile())) {
+ props.load(reader);
+ commandMap.clear();
+ props.stringPropertyNames().forEach(k -> {
+ commandMap.put(k, props.getProperty(k));
+ });
+ logger.debug("Read {} commands from {}", commandMap.size(), mapFilePath);
+ notifyAvailableCommands(commandMap.keySet());
+ } catch (IOException e) {
+ logger.warn("Couldn't read {}: {}", mapFilePath, e.getMessage());
+ }
+ }
+
+ private void notifyAvailableCommands(Set commandNames) {
+ List commandOptions = new ArrayList<>();
+ commandNames.forEach((c) -> commandOptions.add(new CommandOption(c, null)));
+ logger.debug("notifying framework about {} commands from {}", commandOptions.size(), mapFileName);
+ this.commandDescriptionProvider.setCommandOptions(targetChannelUID, commandOptions);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkProtocol.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkProtocol.java
new file mode 100644
index 0000000000000..1e5c1ff9007c5
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkProtocol.java
@@ -0,0 +1,211 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.Calendar;
+import java.util.TimeZone;
+
+import javax.crypto.spec.IvParameterSpec;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.Logger;
+
+/**
+ * Static methods for working with the Broadlink network prototcol.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkProtocol {
+
+ public static byte[] buildMessage(byte command, byte[] payload, int count, byte[] mac, byte[] deviceId, byte[] iv,
+ byte[] key, int deviceType, Logger logger) {
+ byte packet[] = new byte[0x38];
+ packet[0x00] = 0x5a;
+ packet[0x01] = (byte) 0xa5; // https://stackoverflow.com/questions/20026942/type-mismatch-cannot-convert-int-to-byte
+ /*
+ * int 0b10000000 is 128 byte 0b10000000 is -128
+ */
+ packet[0x02] = (byte) 0xaa;
+ packet[0x03] = 0x55;
+ packet[0x04] = 0x5a;
+ packet[0x05] = (byte) 0xa5;
+ packet[0x06] = (byte) 0xaa;
+ packet[0x07] = 0x55;
+ packet[0x24] = (byte) (deviceType & 0xff);
+ packet[0x25] = (byte) (deviceType >> 8);
+ packet[0x26] = command;
+ packet[0x28] = (byte) (count & 0xff);
+ packet[0x29] = (byte) (count >> 8);
+ packet[0x2a] = mac[0];
+ packet[0x2b] = mac[1];
+ packet[0x2c] = mac[2];
+ packet[0x2d] = mac[3];
+ packet[0x2e] = mac[4];
+ packet[0x2f] = mac[5];
+ packet[0x30] = deviceId[0];
+ packet[0x31] = deviceId[1];
+ packet[0x32] = deviceId[2];
+ packet[0x33] = deviceId[3];
+ int checksum = 0xBEAF;
+ int i = 0;
+ byte abyte0[];
+ int k = (abyte0 = payload).length;
+ for (int j = 0; j < k; j++) {
+ byte b = abyte0[j];
+ i = Byte.toUnsignedInt(b);
+ checksum += i;
+ checksum &= 0xffff;
+ }
+ packet[0x34] = (byte) (checksum & 0xff);
+ packet[0x35] = (byte) (checksum >> 8);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ try {
+ outputStream.write(packet);
+ outputStream.write(Utils.encrypt(key, new IvParameterSpec(iv), payload));
+ } catch (IOException e) {
+ logger.warn("IOException while building message", e);
+ return packet;
+ }
+ byte data[] = outputStream.toByteArray();
+ checksum = 0xBEAF;
+ byte abyte1[];
+ int i1 = (abyte1 = data).length;
+ for (int l = 0; l < i1; l++) {
+ byte b = abyte1[l];
+ i = Byte.toUnsignedInt(b);
+ checksum += i;
+ checksum &= 0xffff;
+ }
+ data[0x20] = (byte) (checksum & 0xff);
+ data[0x21] = (byte) (checksum >> 8);
+ return data;
+ }
+
+ public static byte[] buildAuthenticationPayload() {
+ // https://github.com/mjg59/python-broadlink/blob/master/protocol.md
+ byte payload[] = new byte[0x50];
+ payload[0x04] = 0x31;
+ payload[0x05] = 0x31;
+ payload[0x06] = 0x31;
+ payload[0x07] = 0x31;
+ payload[0x08] = 0x31;
+ payload[0x09] = 0x31;
+ payload[0x0a] = 0x31;
+ payload[0x0b] = 0x31;
+ payload[0x0c] = 0x31;
+ payload[0x0d] = 0x31;
+ payload[0x0e] = 0x31;
+ payload[0x0f] = 0x31;
+ payload[0x10] = 0x31;
+ payload[0x11] = 0x31;
+ payload[0x12] = 0x31;
+ payload[0x1e] = 0x01;
+ payload[0x2d] = 0x01;
+ payload[0x30] = (byte) 'T';
+ payload[0x31] = (byte) 'e';
+ payload[0x32] = (byte) 's';
+ payload[0x33] = (byte) 't';
+ payload[0x34] = (byte) ' ';
+ payload[0x35] = (byte) ' ';
+ payload[0x36] = (byte) '1';
+
+ return payload;
+ }
+
+ public static byte[] buildDiscoveryPacket(String host, int port) {
+ String localAddress[] = null;
+ localAddress = host.toString().split("\\.");
+ int ipAddress[] = new int[4];
+ for (int i = 0; i < 4; i++) {
+ ipAddress[i] = Integer.parseInt(localAddress[i]);
+ }
+ Calendar calendar = Calendar.getInstance();
+ calendar.setFirstDayOfWeek(2);
+ TimeZone timeZone = TimeZone.getDefault();
+ int timezone = timeZone.getRawOffset() / 0x36ee80;
+ byte packet[] = new byte[48];
+ if (timezone < 0) {
+ packet[8] = (byte) ((255 + timezone) - 1);
+ packet[9] = -1;
+ packet[10] = -1;
+ packet[11] = -1;
+ } else {
+ packet[8] = 8;
+ packet[9] = 0;
+ packet[10] = 0;
+ packet[11] = 0;
+ }
+ packet[12] = (byte) (calendar.get(1) & 0xff);
+ packet[13] = (byte) (calendar.get(1) >> 8);
+ packet[14] = (byte) calendar.get(12);
+ packet[15] = (byte) calendar.get(11);
+ packet[16] = (byte) (calendar.get(1) - 2000);
+ packet[17] = (byte) (calendar.get(7) + 1);
+ packet[18] = (byte) calendar.get(5);
+ packet[19] = (byte) (calendar.get(2) + 1);
+ packet[24] = (byte) ipAddress[0];
+ packet[25] = (byte) ipAddress[1];
+ packet[26] = (byte) ipAddress[2];
+ packet[27] = (byte) ipAddress[3];
+ packet[28] = (byte) (port & 0xff);
+ packet[29] = (byte) (port >> 8);
+ packet[38] = 6;
+ int checksum = 0xBEAF;
+ byte abyte0[];
+ int k = (abyte0 = packet).length;
+ for (int j = 0; j < k; j++) {
+ byte b = abyte0[j];
+ checksum += Byte.toUnsignedInt(b);
+ }
+
+ checksum &= 0xffff;
+ packet[32] = (byte) (checksum & 0xff);
+ packet[33] = (byte) (checksum >> 8);
+ return packet;
+ }
+
+ public static final int MIN_RESPONSE_PACKET_LENGTH = 0x24;
+
+ public static byte[] decodePacket(byte[] packet, byte[] authorizationKey, String initializationVector)
+ throws IOException {
+ if (packet.length < MIN_RESPONSE_PACKET_LENGTH) {
+ throw new ProtocolException("Unexpectedly short packet; length " + packet.length
+ + " is shorter than protocol minimum " + MIN_RESPONSE_PACKET_LENGTH);
+ }
+ boolean error = (int) packet[0x22] != 0 || (int) packet[0x23] != 0;
+ if (error) {
+ throw new ProtocolException(String.format("Response from device is not valid. (0x22=0x%02X,0x23=0x%02X)",
+ packet[0x22], packet[0x23]));
+ }
+
+ try {
+ IvParameterSpec ivSpec = new IvParameterSpec(HexUtils.hexToBytes(initializationVector));
+ return Utils.decrypt(authorizationKey, ivSpec, Utils.padTo(Utils.slice(packet, 56, packet.length), 16));
+ } catch (Exception ex) {
+ throw new IOException("Failed while getting device status", ex);
+ }
+ }
+
+ public static byte[] getDeviceId(byte response[]) {
+ return Utils.slice(response, 0, 4);
+ }
+
+ public static byte[] getDeviceKey(byte response[]) {
+ return Utils.slice(response, 4, 20);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkRemoteDynamicCommandDescriptionProvider.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkRemoteDynamicCommandDescriptionProvider.java
new file mode 100644
index 0000000000000..efd400e9e827b
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkRemoteDynamicCommandDescriptionProvider.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link BroadlinkRemoteDynamicCommandDescriptionProvider} allows learnt remote codes
+ * (from the mapping file) to be selected from the openHAB UI without needing to be
+ * triggered from a rule etc.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@Component(service = { DynamicCommandDescriptionProvider.class,
+ BroadlinkRemoteDynamicCommandDescriptionProvider.class })
+@NonNullByDefault
+public class BroadlinkRemoteDynamicCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider {
+ @Activate
+ public BroadlinkRemoteDynamicCommandDescriptionProvider(
+ final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/ModelMapper.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/ModelMapper.java
new file mode 100644
index 0000000000000..e0c194f9a3bbd
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/ModelMapper.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ThingTypeUID;
+import org.slf4j.Logger;
+
+/**
+ * Mappings of internal values to user-visible ones.
+ *
+ * @author Cato Sognen - Initial contribution
+ * @author John Marshall - V2 and V3 updates
+ */
+@NonNullByDefault
+public class ModelMapper {
+ private static final StringType UNKNOWN = new StringType("UNKNOWN");
+
+ public static ThingTypeUID getThingType(int model, Logger logger) {
+ logger.debug("Looking for thing type corresponding to model {}", model);
+
+ switch (model) {
+ case 0x0000:
+ return BroadlinkBindingConstants.THING_TYPE_SP1;
+ case 0x2717:
+ case 0x2719:
+ case 0x271A:
+ case 0x2720:
+ case 0x2728:
+ case 0x273E:
+ case 0x7530:
+ case 0x7539:
+ case 0x753E:
+ case 0x7540:
+ case 0x7544:
+ case 0x7546:
+ case 0x7547:
+ case 0x7918:
+ case 0x7919:
+ case 0x791A:
+ case 0x7D0D:
+ return BroadlinkBindingConstants.THING_TYPE_SP2;
+ case 0x2711:
+ case 0x2716:
+ case 0x271D:
+ case 0x2736:
+ return BroadlinkBindingConstants.THING_TYPE_SP2S;
+ case 0x2733:
+ case 0x7D00:
+ return BroadlinkBindingConstants.THING_TYPE_SP3;
+ case 0x9479:
+ case 0x947A:
+ return BroadlinkBindingConstants.THING_TYPE_SP3S;
+ case 0x2737:
+ case 0x278F:
+ case 0x27C2:
+ case 0x27C7:
+ case 0x27CC:
+ case 0x27CD:
+ case 0x27D0:
+ case 0x27D1:
+ case 0x27D3:
+ case 0x27DE:
+ return BroadlinkBindingConstants.THING_TYPE_RM3;
+ case 0x2712:
+ case 0x272A:
+ case 0x273D:
+ case 0x277C:
+ case 0x2783:
+ case 0x2787:
+ case 0x278B:
+ case 0x2797:
+ case 0x279D:
+ case 0x27A1:
+ case 0x27A6:
+ case 0x27A9:
+ case 0x27C3:
+ return BroadlinkBindingConstants.THING_TYPE_RM2;
+ case 0x5F36:
+ case 0x6508:
+ return BroadlinkBindingConstants.THING_TYPE_RM3Q;
+ case 0x51DA:
+ case 0x6070:
+ case 0x610E:
+ case 0x610F:
+ case 0x62BC:
+ case 0x62BE:
+ case 0x6364:
+ case 0x648D:
+ case 0x6539:
+ case 0x653A:
+ case 0x6026:
+ case 0x61A2:
+ case 0x649B:
+ case 0x653C:
+ return BroadlinkBindingConstants.THING_TYPE_RM4;
+ case 0x2714:
+ return BroadlinkBindingConstants.THING_TYPE_A1;
+ case 0x4EB5:
+ case 0x4EF7:
+ case 0x4F1B:
+ case 0x4F65:
+ return BroadlinkBindingConstants.THING_TYPE_MP1;
+ default: {
+ String modelAsHexString = Integer.toHexString(model);
+ logger.warn(
+ "Device identifying itself as '{}' (0x{}) is not currently supported. Please report this to the developer!",
+ model, modelAsHexString);
+ logger.warn(
+ "Join the discussion at https://community.openhab.org/t/broadlink-binding-for-rmx-a1-spx-and-mp-any-interest/22768/616");
+ throw new UnsupportedOperationException("Device identifying itself as '" + model + "' (hex 0x"
+ + modelAsHexString + ") is not currently supported. Please report this to the developer!");
+ }
+ }
+ }
+
+ private static StringType lookup(StringType[] values, byte b) {
+ int index = Byte.toUnsignedInt(b);
+ if (index < values.length) {
+ return values[index];
+ } else {
+ return UNKNOWN;
+ }
+ }
+
+ private static final StringType[] AIR_VALUES = { new StringType("PERFECT"), new StringType("GOOD"),
+ new StringType("NORMAL"), new StringType("BAD") };
+
+ public static StringType getAirValue(byte b) {
+ return lookup(AIR_VALUES, b);
+ }
+
+ private static final StringType[] LIGHT_VALUES = { new StringType("DARK"), new StringType("DIM"),
+ new StringType("NORMAL"), new StringType("BRIGHT") };
+
+ public static StringType getLightValue(byte b) {
+ return lookup(LIGHT_VALUES, b);
+ }
+
+ private static final StringType[] NOISE_VALUES = { new StringType("QUIET"), new StringType("NORMAL"),
+ new StringType("NOISY"), new StringType("EXTREME") };
+
+ public static StringType getNoiseValue(byte b) {
+ return lookup(NOISE_VALUES, b);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/NetworkUtils.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/NetworkUtils.java
new file mode 100644
index 0000000000000..4aecdd0930f29
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/NetworkUtils.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.ServerSocket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Enumeration;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+
+/**
+ * Utilities for working with the local network.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class NetworkUtils {
+
+ public static boolean hostAvailabilityCheck(@Nullable String host, int timeout, Logger logger) {
+ if (host == null) {
+ logger.warn("Can't check availability of a null host");
+ return false;
+ }
+ try {
+ InetAddress address = InetAddress.getByName(host);
+ return address.isReachable(timeout);
+ } catch (Exception e) {
+ logger.warn("Exception while trying to determine reachability of {}", host, e);
+ }
+ return false;
+ }
+
+ public static @Nullable InetAddress findNonLoopbackAddress() throws SocketException {
+ Enumeration ifaces = NetworkInterface.getNetworkInterfaces();
+ while (ifaces.hasMoreElements()) {
+ NetworkInterface iface = ifaces.nextElement();
+ Enumeration inetAddrs = iface.getInetAddresses();
+ while (inetAddrs.hasMoreElements()) {
+ InetAddress inetAddr = inetAddrs.nextElement();
+ if (inetAddr.isLoopbackAddress()) {
+ continue; /* Loop/switch isn't completed */
+ }
+
+ if (inetAddr.isSiteLocalAddress()) {
+ return inetAddr;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static InetAddress getLocalHostLANAddress() throws UnknownHostException {
+ try {
+ InetAddress candidateAddress = findNonLoopbackAddress();
+
+ if (candidateAddress != null) {
+ return candidateAddress;
+ }
+ InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
+ if (jdkSuppliedAddress == null) {
+ throw new UnknownHostException("The JDK InetAddress.getLocalHost() method unexpectedly returned null.");
+ } else {
+ return jdkSuppliedAddress;
+ }
+ } catch (Exception e) {
+ UnknownHostException unknownHostException = new UnknownHostException(
+ (new StringBuilder("Failed to determine LAN address: ")).append(e).toString());
+ unknownHostException.initCause(e);
+ throw unknownHostException;
+ }
+ }
+
+ public static int nextFreePort(InetAddress host, int from, int to) {
+ int port = randInt(from, to);
+ do {
+ if (isLocalPortFree(host, port)) {
+ return port;
+ }
+ port = ThreadLocalRandom.current().nextInt(from, to);
+ } while (true);
+ }
+
+ public static boolean isLocalPortFree(InetAddress host, int port) {
+ try {
+ (new ServerSocket(port, 50, host)).close();
+ } catch (IOException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public static int randInt(int min, int max) {
+ int randomNum = ThreadLocalRandom.current().nextInt((max - min) + 1) + min;
+ return randomNum;
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/Utils.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/Utils.java
new file mode 100644
index 0000000000000..b495a5163b0a9
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/Utils.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import java.io.IOException;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+
+/**
+ * Utilities for working with the Broadlink devices.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class Utils {
+ public static boolean isOnline(Thing thing) {
+ return thing.getStatus().equals(ThingStatus.ONLINE);
+ }
+
+ public static boolean isOffline(Thing thing) {
+ return thing.getStatus().equals(ThingStatus.OFFLINE);
+ }
+
+ public static byte[] slice(byte source[], int from, int to) throws IllegalArgumentException {
+ if (from > to) {
+ throw new IllegalArgumentException("Can't slice; from: " + from + " is larger than to: " + to);
+ }
+ if (to - from > source.length) {
+ throw new IllegalArgumentException(
+ "Can't slice; from: " + from + " - to: " + to + " is longer than source length: " + source.length);
+ }
+ if (to == from) {
+ byte sliced[] = new byte[1];
+ sliced[0] = source[from];
+ return sliced;
+ } else {
+ byte sliced[] = new byte[to - from];
+ System.arraycopy(source, from, sliced, 0, to - from);
+ return sliced;
+ }
+ }
+
+ public static byte[] padTo(byte[] source, int quotient) {
+ int modulo = source.length % quotient;
+ if (modulo == 0) {
+ return source;
+ }
+
+ int requiredNewSize = source.length + (quotient - modulo);
+ byte[] padded = new byte[requiredNewSize];
+ System.arraycopy(source, 0, padded, 0, source.length);
+
+ return padded;
+ }
+
+ public static String toHexString(byte[] source) {
+ StringBuilder stringBuilder = new StringBuilder(source.length * 2);
+ for (byte b : source) {
+ stringBuilder.append(String.format("%02x", b));
+ }
+ return stringBuilder.toString();
+ }
+
+ public static byte[] encrypt(byte key[], IvParameterSpec ivSpec, byte data[]) throws IOException {
+ SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
+ try {
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ cipher.init(1, secretKey, ivSpec);
+ return cipher.doFinal(data);
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ }
+
+ public static byte[] decrypt(byte key[], IvParameterSpec ivSpec, byte data[]) throws IOException {
+ SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
+ try {
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ cipher.init(2, secretKey, ivSpec);
+ return cipher.doFinal(data);
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/BroadlinkDiscoveryService.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/BroadlinkDiscoveryService.java
new file mode 100644
index 0000000000000..d9796c918e770
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/BroadlinkDiscoveryService.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal.discovery;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.openhab.binding.broadlink.internal.socket.BroadlinkSocketListener;
+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.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Thing;
+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;
+
+/**
+ * Broadlink discovery implementation.
+ *
+ * @author Cato Sognen - Initial contribution
+ * @author John Marshall - Rewrite for V2 and V3
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, configurationPid = "discovery.broadlink")
+public class BroadlinkDiscoveryService extends AbstractDiscoveryService
+ implements BroadlinkSocketListener, DiscoveryFinishedListener {
+
+ private final Logger logger = LoggerFactory.getLogger(BroadlinkDiscoveryService.class);
+ private int foundCount = 0;
+
+ public BroadlinkDiscoveryService() {
+ super(BroadlinkBindingConstants.SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.keySet(), 10, true);
+ logger.debug("BroadlinkDiscoveryService - Constructed");
+ }
+
+ public void startScan() {
+ foundCount = 0;
+ logger.warn("BroadlinkDiscoveryService - Beginning Broadlink device scan...");
+ DiscoveryProtocol.beginAsync(this, 10000L, this, logger);
+ }
+
+ public void onDiscoveryFinished() {
+ logger.info("Discovery complete. Found {} Broadlink devices", foundCount);
+ }
+
+ protected synchronized void stopScan() {
+ super.stopScan();
+ removeOlderResults(getTimestampOfLastScan());
+ }
+
+ public void onDataReceived(String remoteAddress, int remotePort, String remoteMAC, ThingTypeUID thingTypeUID,
+ int model) {
+ logger.trace("Data received during Broadlink device discovery: from {}:{} [{}]", remoteAddress, remotePort,
+ remoteMAC);
+ foundCount++;
+ discoveryResultSubmission(remoteAddress, remotePort, remoteMAC, thingTypeUID, model);
+ }
+
+ private void discoveryResultSubmission(String remoteAddress, int remotePort, String remoteMAC,
+ ThingTypeUID thingTypeUID, int model) {
+ String modelAsHexString = String.format("%x", model);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Adding new Broadlink device ({} => {}) at {} with mac '{}' to Smarthome inbox",
+ modelAsHexString, thingTypeUID, remoteAddress, remoteMAC);
+ }
+ Map properties = new HashMap(6);
+ properties.put("ipAddress", remoteAddress);
+ properties.put("port", Integer.valueOf(remotePort));
+ properties.put(Thing.PROPERTY_MAC_ADDRESS, remoteMAC);
+ properties.put("deviceType", modelAsHexString);
+ ThingUID thingUID = new ThingUID(thingTypeUID, remoteMAC.replace(":", "-"));
+ if (logger.isDebugEnabled()) {
+ logger.debug("Device '{}' discovered at '{}'.", thingUID, remoteAddress);
+ }
+
+ if (BroadlinkBindingConstants.SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.containsKey(thingTypeUID)) {
+ notifyThingDiscovered(thingTypeUID, thingUID, remoteAddress, properties);
+ } else {
+ logger.info("Discovered a {} but do not know how to support it at this time :-(", thingTypeUID);
+ }
+ }
+
+ private void notifyThingDiscovered(ThingTypeUID thingTypeUID, ThingUID thingUID, String remoteAddress,
+ Map properties) {
+ String deviceHumanName = BroadlinkBindingConstants.SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.get(thingTypeUID);
+ String label = deviceHumanName + " [" + remoteAddress + "]";
+ DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
+ .withProperties(properties).withLabel(label).withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS)
+ .build();
+
+ thingDiscovered(result);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DeviceRediscoveryAgent.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DeviceRediscoveryAgent.java
new file mode 100644
index 0000000000000..6113b0ebf907f
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DeviceRediscoveryAgent.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal.discovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.config.BroadlinkDeviceConfiguration;
+import org.openhab.binding.broadlink.internal.socket.BroadlinkSocketListener;
+import org.openhab.core.thing.ThingTypeUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This agent exploits the well-known Broadlink device discovery process
+ * to attempt to "rediscover" a previously-discovered dynamically-addressed
+ * Broadlink device that may have recently changed IP address.
+ *
+ * This agent has NOTHING TO DO WITH the initial device discovery process.
+ * It is explicitly initiated when a dynamically-addressed Broadlink device
+ * appears to have dropped off the network.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceRediscoveryAgent implements BroadlinkSocketListener, DiscoveryFinishedListener {
+
+ private final Logger logger = LoggerFactory.getLogger(DeviceRediscoveryAgent.class);
+ private final BroadlinkDeviceConfiguration missingThingConfig;
+ private final DeviceRediscoveryListener drl;
+ private boolean foundDevice = false;
+
+ public DeviceRediscoveryAgent(BroadlinkDeviceConfiguration missingThingConfig, DeviceRediscoveryListener drl) {
+ this.missingThingConfig = missingThingConfig;
+ this.drl = drl;
+ }
+
+ public void attemptRediscovery() {
+ logger.warn("DeviceRediscoveryAgent - Beginning Broadlink device scan for missing {}",
+ missingThingConfig.toString());
+ DiscoveryProtocol.beginAsync(this, 5000L, this, logger);
+ }
+
+ public void onDataReceived(String remoteAddress, int remotePort, String remoteMAC, ThingTypeUID thingTypeUID,
+ int model) {
+ logger.trace("Data received during Broadlink device rediscovery: from {}:{} [{}]", remoteAddress, remotePort,
+ remoteMAC);
+
+ // if this thing matches the missingThingConfig, we've found it!
+ logger.trace("Comparing with desired mac: {}", missingThingConfig.getMacAddressAsString());
+
+ if (missingThingConfig.getMacAddressAsString().equals(remoteMAC)) {
+ logger.debug("We have a match for target MAC {} at {} - reassociate!", remoteMAC, remoteAddress);
+ foundDevice = true;
+ this.drl.onDeviceRediscovered(remoteAddress);
+ }
+ }
+
+ public void onDiscoveryFinished() {
+ if (!foundDevice) {
+ this.drl.onDeviceRediscoveryFailure();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DeviceRediscoveryListener.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DeviceRediscoveryListener.java
new file mode 100644
index 0000000000000..1fb063e512330
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DeviceRediscoveryListener.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal.discovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Broadlink discovery implementation.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public interface DeviceRediscoveryListener {
+ void onDeviceRediscovered(String newIpAddress);
+
+ void onDeviceRediscoveryFailure();
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryFinishedListener.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryFinishedListener.java
new file mode 100644
index 0000000000000..60a2dee0aba3d
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryFinishedListener.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal.discovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Broadlink discovery implementation.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public interface DiscoveryFinishedListener {
+ void onDiscoveryFinished();
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryProtocol.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryProtocol.java
new file mode 100644
index 0000000000000..e8fdd912ab689
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryProtocol.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal.discovery;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.broadlink.internal.BroadlinkProtocol;
+import org.openhab.binding.broadlink.internal.NetworkUtils;
+import org.openhab.binding.broadlink.internal.socket.BroadlinkSocket;
+import org.openhab.binding.broadlink.internal.socket.BroadlinkSocketListener;
+import org.slf4j.Logger;
+
+/**
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class DiscoveryProtocol {
+
+ private static class AsyncDiscoveryThread extends Thread {
+ private final BroadlinkSocketListener listener;
+ private final long timeoutMillis;
+ private final DiscoveryFinishedListener finishedListener;
+ private final Logger logger;
+
+ AsyncDiscoveryThread(BroadlinkSocketListener listener, long timeoutMillis,
+ DiscoveryFinishedListener finishedListener, Logger logger) {
+ this.listener = listener;
+ this.timeoutMillis = timeoutMillis;
+ this.finishedListener = finishedListener;
+ this.logger = logger;
+ }
+
+ public void run() {
+ BroadlinkSocket.registerListener(listener, logger);
+ DiscoveryProtocol.discoverDevices(logger);
+ DiscoveryProtocol.waitUntilEnded(timeoutMillis, logger);
+ logger.warn("Ended Broadlink device scan...");
+ BroadlinkSocket.unregisterListener(listener, logger);
+ finishedListener.onDiscoveryFinished();
+ }
+ }
+
+ public static void beginAsync(BroadlinkSocketListener listener, long discoveryTimeoutMillis,
+ DiscoveryFinishedListener discoveryFinishedListener, Logger logger) {
+ logger.warn("Beginning async Broadlink device scan; will wait {} ms for responses", discoveryTimeoutMillis);
+ AsyncDiscoveryThread adt = new AsyncDiscoveryThread(listener, discoveryTimeoutMillis, discoveryFinishedListener,
+ logger);
+ adt.start();
+ }
+
+ public static void discoverDevices(Logger logger) {
+ try {
+ InetAddress localAddress = NetworkUtils.getLocalHostLANAddress();
+ int localPort = NetworkUtils.nextFreePort(localAddress, 1024, 3000);
+ byte message[] = BroadlinkProtocol.buildDiscoveryPacket(localAddress.getHostAddress(), localPort);
+ BroadlinkSocket.sendMessage(message, "255.255.255.255", 80, logger);
+ } catch (UnknownHostException e) {
+ logger.warn("Failed to initiate discovery", e);
+ }
+ }
+
+ private static void waitUntilEnded(long discoveryTimeoutMillis, Logger logger) {
+ try {
+ logger.warn("Broadlink device scan waiting for {} ms to complete ...", discoveryTimeoutMillis);
+ Thread.sleep(discoveryTimeoutMillis);
+ logger.warn("Device scan: wait complete ...");
+
+ } catch (InterruptedException e) {
+ logger.warn("problem {}", e.getMessage());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocket.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocket.java
new file mode 100644
index 0000000000000..ae77ea862b6cb
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocket.java
@@ -0,0 +1,160 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal.socket;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.broadlink.internal.ModelMapper;
+import org.slf4j.Logger;
+
+/**
+ * Threaded socket implementation
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+
+@NonNullByDefault
+public class BroadlinkSocket {
+ private static byte buffer[];
+ private static DatagramPacket datagramPacket;
+ @Nullable
+ private static MulticastSocket socket = null;
+ @Nullable
+ private static Thread socketReceiveThread;
+ private static List listeners = new ArrayList();
+
+ static {
+ buffer = new byte[1024];
+ datagramPacket = new DatagramPacket(buffer, buffer.length);
+ }
+
+ public static String decodeMAC(byte mac[]) {
+ if (mac.length < 6) {
+ throw new IllegalArgumentException("Insufficient MAC bytes provided, cannot decode it");
+ }
+
+ StringBuilder sb = new StringBuilder(18);
+ for (int i = 5; i >= 0; i--) {
+ if (sb.length() > 0) {
+ sb.append(':');
+ }
+ sb.append(String.format("%02x", new Object[] { Byte.valueOf(mac[i]) }));
+ }
+
+ return sb.toString();
+ }
+
+ private static class ReceiverThread extends Thread {
+ private Logger logger;
+
+ public void run() {
+ receiveData(BroadlinkSocket.socket, BroadlinkSocket.datagramPacket);
+ }
+
+ @SuppressWarnings("null")
+ private void receiveData(@Nullable MulticastSocket socket, DatagramPacket dgram) {
+ try {
+ while (true) {
+ socket.receive(dgram);
+ BroadlinkSocketListener listener;
+ byte remoteMAC[];
+ org.openhab.core.thing.ThingTypeUID deviceType;
+ int model;
+ for (Iterator iterator = (new ArrayList(
+ BroadlinkSocket.listeners)).iterator(); iterator.hasNext(); listener.onDataReceived(
+ dgram.getAddress().getHostAddress(), dgram.getPort(), decodeMAC(remoteMAC),
+ deviceType, model)) {
+ listener = iterator.next();
+ byte receivedPacket[] = dgram.getData();
+ remoteMAC = Arrays.copyOfRange(receivedPacket, 58, 64);
+ model = Byte.toUnsignedInt(receivedPacket[52]) | Byte.toUnsignedInt(receivedPacket[53]) << 8;
+ deviceType = ModelMapper.getThingType(model, logger);
+ }
+ }
+ } catch (IOException e) {
+ if (!isInterrupted()) {
+ logger.warn("Error while receiving", e);
+ }
+ }
+ logger.debug("Receiver thread ended");
+ }
+
+ private ReceiverThread(Logger logger) {
+ this.logger = logger;
+ }
+ }
+
+ public static void registerListener(BroadlinkSocketListener listener, Logger logger) {
+ listeners.add(listener);
+ if (socket == null) {
+ setupSocket(logger);
+ }
+ }
+
+ public static void unregisterListener(BroadlinkSocketListener listener, Logger logger) {
+ listeners.remove(listener);
+ if (listeners.isEmpty() && socket != null) {
+ closeSocket(logger);
+ }
+ }
+
+ @SuppressWarnings("null")
+ private static void setupSocket(Logger logger) {
+ synchronized (BroadlinkSocket.class) {
+ try {
+ socket = new MulticastSocket();
+ } catch (IOException e) {
+ logger.warn("Setup socket error '{}'.", e.getMessage());
+ }
+ socketReceiveThread = new ReceiverThread(logger);
+ socketReceiveThread.start();
+ }
+ }
+
+ @SuppressWarnings("null")
+ private static void closeSocket(Logger logger) {
+ synchronized (BroadlinkSocket.class) {
+ if (socketReceiveThread != null) {
+ socketReceiveThread.interrupt();
+ }
+ if (socket != null) {
+ logger.debug("Socket closed");
+ socket.close();
+ socket = null;
+ }
+ }
+ }
+
+ public static void sendMessage(byte message[], Logger logger) {
+ sendMessage(message, "255.255.255.255", 80, logger);
+ }
+
+ @SuppressWarnings("null")
+ public static void sendMessage(byte message[], String host, int port, Logger logger) {
+ try {
+ DatagramPacket sendPacket = new DatagramPacket(message, message.length, InetAddress.getByName(host), port);
+ socket.send(sendPacket);
+ } catch (IOException e) {
+ logger.warn("IO Error sending message: '{}'", e.getMessage());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocketListener.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocketListener.java
new file mode 100644
index 0000000000000..590ef4da3fbd3
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocketListener.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal.socket;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * Interface for something that is interested in being informed when data arrives on a socket
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public interface BroadlinkSocketListener {
+
+ public abstract void onDataReceived(String remoteAddress, int remotePort, String remoteMAC,
+ ThingTypeUID thingTypeUID, int model);
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/NetworkTrafficObserver.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/NetworkTrafficObserver.java
new file mode 100644
index 0000000000000..a1e56e3f6b172
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/NetworkTrafficObserver.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal.socket;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public interface NetworkTrafficObserver {
+
+ public void onCommandSent(byte command);
+
+ public void onBytesSent(byte[] sentBytes);
+
+ public void onBytesReceived(byte[] receivedBytes);
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/RetryableSocket.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/RetryableSocket.java
new file mode 100644
index 0000000000000..b35d96fcd16dd
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/RetryableSocket.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal.socket;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketTimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.broadlink.config.BroadlinkDeviceConfiguration;
+import org.slf4j.Logger;
+
+/**
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class RetryableSocket {
+ private final BroadlinkDeviceConfiguration thingConfig;
+ private final Logger logger;
+
+ private @Nullable DatagramSocket socket = null;
+
+ public RetryableSocket(BroadlinkDeviceConfiguration thingConfig, Logger logger) {
+ this.thingConfig = thingConfig;
+ this.logger = logger;
+ }
+
+ /**
+ * Send a packet to the device, and expect a response.
+ * We'll try again if we fail to get any response.
+ */
+ public byte @Nullable [] sendAndReceive(byte message[], String purpose) {
+ byte[] firstAttempt = sendAndReceiveOneTime(message, purpose);
+
+ if (firstAttempt != null) {
+ return firstAttempt;
+ } else {
+ return sendAndReceiveOneTime(message, purpose);
+ }
+ }
+
+ private byte @Nullable [] sendAndReceiveOneTime(byte message[], String purpose) {
+ // To avoid the possibility of a very fast response not being heard,
+ // we set up as much as possible for the response ahead of time:
+ byte response[] = new byte[1024];
+ DatagramPacket receivePacket = new DatagramPacket(response, response.length);
+ if (sendDatagram(message, purpose)) {
+ receivePacket = receiveDatagram(purpose, receivePacket);
+ if (receivePacket != null) {
+ return receivePacket.getData();
+ }
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("null")
+ private boolean sendDatagram(byte message[], String purpose) {
+ try {
+ logger.trace("Sending {} to {}:{}", purpose, thingConfig.getIpAddress(), thingConfig.getPort());
+ if (socket == null || socket.isClosed()) {
+ logger.trace("No existing socket ... creating");
+ socket = new DatagramSocket();
+ socket.setBroadcast(true);
+ socket.setReuseAddress(true);
+ socket.setSoTimeout(5000);
+ }
+ InetAddress host = InetAddress.getByName(thingConfig.getIpAddress());
+ int port = thingConfig.getPort();
+ DatagramPacket sendPacket = new DatagramPacket(message, message.length, new InetSocketAddress(host, port));
+ socket.send(sendPacket);
+ logger.trace("Sending {} complete", purpose);
+ return true;
+ } catch (IOException e) {
+ logger.warn("IO error during UDP command sending {}:", purpose, e);
+ return false;
+ }
+ }
+
+ @SuppressWarnings("null")
+ private @Nullable DatagramPacket receiveDatagram(String purpose, DatagramPacket receivePacket) {
+ logger.trace("Awaiting {} response", purpose);
+
+ try {
+ if (socket == null) {
+ logger.warn("receiveDatagram {} for socket was unexpectedly null", purpose);
+ } else {
+ socket.receive(receivePacket);
+ logger.trace("Received {} ({} bytes)", purpose, receivePacket.getLength());
+ return receivePacket;
+ }
+ } catch (SocketTimeoutException ste) {
+ logger.debug("No further {} response received for device", purpose);
+ } catch (Exception e) {
+ logger.warn("While {}", purpose, e);
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("null")
+ public void close() {
+ if (socket != null) {
+ socket.close();
+ socket = null;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..235788007bd1a
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,8 @@
+
+
+
+ Broadlink Binding
+ This is the binding for Broadlink devices.
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 0000000000000..63b7ebbad1a28
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+ network-address
+
+ Network address of the device.
+ true
+
+
+ static-ip
+
+ Will the device always be given this network address?
+ true
+ true
+ true
+
+
+ 80
+ network-port
+
+ Network port of the device.
+ true
+ true
+
+
+ mac-address
+
+ MAC address of the device.
+ true
+
+
+
+ The interval in seconds for polling the status of the device.
+ true
+ 30
+ true
+
+
+ ignore-failed-updates
+
+ Should failed status requests force the device offline?
+ true
+ true
+ false
+
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/rm-config.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/rm-config.xml
new file mode 100644
index 0000000000000..5be431f406ee3
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/rm-config.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+ network-address
+
+ Network address of the device.
+ true
+
+
+ static-ip
+
+ Will the device always be given this network address?
+ true
+ true
+ true
+
+
+ 80
+ network-port
+
+ Network port of the device.
+ true
+ true
+
+
+ mac-address
+
+ MAC address of the device.
+ true
+
+
+
+ The interval in seconds for polling the status of the device.
+ true
+ 30
+ true
+
+
+
+ Enter name of file containing mapping of commands to IR codes
+ broadlink.map
+ true
+
+
+ ignore-failed-updates
+
+ Should failed status requests force the device offline?
+ true
+ true
+ false
+
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/i18n/broadlink_xx.properties b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/i18n/broadlink_xx.properties
new file mode 100644
index 0000000000000..96fcc218eba0d
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/i18n/broadlink_xx.properties
@@ -0,0 +1,21 @@
+# FIXME: please substitute the xx with a proper locale, ie. de
+# FIXME: please do not add the file to the repo if you add or change no content
+# binding
+binding.broadlink.name =
+binding.broadlink.description =
+
+# thing types
+thing-type.broadlink.sample.label =
+thing-type.broadlink.sample.description =
+
+# thing type config description
+thing-type.config.broadlink.sample.hostname.label =
+thing-type.config.broadlink.sample.hostname.description =
+thing-type.config.broadlink.sample.password.label =
+thing-type.config.broadlink.sample.password.description =
+thing-type.config.broadlink.sample.refreshInterval.label =
+thing-type.config.broadlink.sample.refreshInterval.description =
+
+# channel types
+channel-type.broadlink.sample-channel.label =
+channel-type.broadlink.sample-channel.description =
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/a-types.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/a-types.xml
new file mode 100644
index 0000000000000..b85e5a4ba5a9d
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/a-types.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ The Broadlink A1 is a multi sensor that can can detect temperature, humidity, illumination, background
+ noise and air quality.
+
+
+
+
+
+
+
+ macAddress
+
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/channels.xml
new file mode 100644
index 0000000000000..851632593eab1
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/channels.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ String
+
+ The IR/RF command sent to the device
+ Text
+
+
+ String
+
+ Instructs the Broadlink device to perform tasks related to code learning
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+ Temperature
+
+
+
+ Number:Dimensionless
+
+ Humidity
+
+
+
+ Switch
+
+ PowerOutlet
+
+
+ Switch
+
+ Lightbulb
+
+
+ Number:Power
+
+
+
+
+ String
+
+ Light conditions
+
+
+
+ String
+
+ Noise conditions
+
+
+
+ String
+
+ Air quality conditions
+
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/mp-types.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/mp-types.xml
new file mode 100644
index 0000000000000..b8ad500ff9631
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/mp-types.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+ The Broadlink MP1 Smart Home Wifi Plug Power Strip 4 Ports Individual Wireless Remote Control.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ macAddress
+
+
+
+
+ The Broadlink MP1 1K3S2U Smart Home Wifi Plug Power Strip 3 AC, 2 USB Individual Wireless Remote Control.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ macAddress
+
+
+
+
+ The Broadlink MP2 is a smart socket with Wi-Fi connectivity. It does not support switching by individual
+ ports.
+
+
+
+
+ macAddress
+
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/rm-types.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/rm-types.xml
new file mode 100644
index 0000000000000..876a529391eb2
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/rm-types.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ The Broadlink RM 2/Pro is a Wi-Fi IR/RF transmitter with a temperature sensor.
+
+
+
+
+
+ macAddress
+
+
+
+
+ The Broadlink RM 3/Mini is a Wi-Fi IR transmitter.
+
+
+
+
+ macAddress
+
+
+
+
+ The Broadlink RM 3/Mini is a Wi-Fi IR transmitter running v44057 firmware.
+
+
+
+
+ macAddress
+
+
+
+
+ The Broadlink RM 4 is a Wi-Fi IR transmitter with temperature and humidity sensors.
+
+
+
+
+
+
+ macAddress
+
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/sp-types.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/sp-types.xml
new file mode 100644
index 0000000000000..2fb827d91a5ca
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/sp-types.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+ The Broadlink SP1 is a Wi-Fi smart socket.
+
+
+
+ macAddress
+
+
+
+
+ The Broadlink SP2 is a Wi-Fi smart socket.
+
+
+
+ macAddress
+
+
+
+
+ The Broadlink SP2s is a Wi-Fi smart socket with power consumption measurement.
+
+
+
+
+ macAddress
+
+
+
+
+ The Broadlink SP3 is a Wi-Fi smart socket with night light. The SP mini 3 omits the night light.
+
+
+
+
+
+
+ macAddress
+
+
+
+
+ The Broadlink SP3s is a Wi-Fi smart socket with power consumption measurement
+
+
+
+
+ macAddress
+
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..7dd2d6f7d2eb9
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+ Sample thing for Broadlink Binding
+
+
+
+
+
+
+
+ network-address
+
+ Hostname or IP address of the device
+
+
+ password
+
+ Passwort to access the device
+
+
+
+ Interval the device is polled in sec.
+
+
+
+
+
+
+ Number:Temperature
+
+ Sample channel for Broadlink Binding
+
+
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/AbstractBroadlinkTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/AbstractBroadlinkTest.java
new file mode 100644
index 0000000000000..2d8632af0d046
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/AbstractBroadlinkTest.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.openhab.core.OpenHAB;
+
+/**
+ * Abstract superclass for all Broadlink unit tests;
+ * ensures that the mapping file will be found
+ * in a testing context
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractBroadlinkTest {
+ protected static final Path TEST_CONF_DIRECTORY = Paths.get("src", "test", "resources", "conf");
+
+ @BeforeAll
+ public static void beforeClass() {
+ System.setProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT, TEST_CONF_DIRECTORY.toFile().getAbsolutePath());
+ }
+
+ @AfterAll
+ public static void afterClass() {
+ System.clearProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/AbstractBroadlinkThingHandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/AbstractBroadlinkThingHandlerTest.java
new file mode 100644
index 0000000000000..1a48973e08fcf
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/AbstractBroadlinkThingHandlerTest.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.BROADLINK_AUTH_KEY;
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.BROADLINK_IV;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.mockito.Mockito;
+import org.openhab.binding.broadlink.AbstractBroadlinkTest;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.openhab.binding.broadlink.internal.BroadlinkProtocol;
+import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider;
+import org.openhab.binding.broadlink.internal.socket.NetworkTrafficObserver;
+import org.openhab.binding.broadlink.internal.socket.RetryableSocket;
+import org.openhab.core.config.core.Configuration;
+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.ThingTypeUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.thing.internal.ThingImpl;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract thing handler test.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractBroadlinkThingHandlerTest extends AbstractBroadlinkTest {
+
+ protected Map properties = new HashMap<>();
+ protected Configuration config = new Configuration();
+ protected ThingImpl thing = new ThingImpl(BroadlinkBindingConstants.THING_TYPE_A1, "a1");
+
+ protected RetryableSocket mockSocket = Mockito.mock(RetryableSocket.class);
+ protected NetworkTrafficObserver trafficObserver = Mockito.mock(NetworkTrafficObserver.class);
+ protected ThingHandlerCallback mockCallback = Mockito.mock(ThingHandlerCallback.class);
+ protected BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider = Mockito
+ .mock(BroadlinkRemoteDynamicCommandDescriptionProvider.class);
+
+ protected void configureUnderlyingThing(ThingTypeUID thingTypeUID, String thingId) {
+ properties = new HashMap<>();
+ properties.put("authorizationKey", "097628343fe99e23765c1513accf8b02");
+ properties.put(Thing.PROPERTY_MAC_ADDRESS, "AB:CD:AB:CD:AB:CD");
+ properties.put("iv", "562e17996d093d28ddb3ba695a2e6f58");
+ config = new Configuration(properties);
+
+ thing = new ThingImpl(thingTypeUID, thingId);
+ thing.setConfiguration(config);
+ thing.setStatusInfo(new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
+ }
+
+ protected void setMocksForTesting(BroadlinkBaseThingHandler handler) {
+ handler.setSocket(mockSocket);
+ handler.setNetworkTrafficObserver(trafficObserver);
+ handler.setCallback(mockCallback);
+ handler.initialize();
+ }
+
+ protected byte[] generateReceivedBroadlinkMessage(byte[] payload) {
+ byte[] mac = { 0x11, 0x22, 0x11, 0x22, 0x11, 0x22 };
+ byte[] devId = { 0x11, 0x22, 0x11, 0x22 };
+ return BroadlinkProtocol.buildMessage((byte) 0x6a, payload, 99, mac, devId, HexUtils.hexToBytes(BROADLINK_IV),
+ HexUtils.hexToBytes(BROADLINK_AUTH_KEY), 0x2714, LoggerFactory.getLogger(getClass()));
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel2HandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel2HandlerTest.java
new file mode 100644
index 0000000000000..8a684100333bc
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel2HandlerTest.java
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+
+/**
+ * Tests the Remote Model 2 handler.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkRemoteModel2HandlerTest extends AbstractBroadlinkThingHandlerTest {
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ configureUnderlyingThing(BroadlinkBindingConstants.THING_TYPE_RM2, "rm2-test");
+ MockitoAnnotations.openMocks(this).close();
+ Mockito.when(mockSocket.sendAndReceive(Mockito.any(byte[].class), Mockito.anyString())).thenReturn(response);
+ }
+
+ private byte[] response = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
+
+ @Test
+ public void sendsExpectedBytesWhenGettingDeviceStatus() {
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class);
+ ArgumentCaptor byteArrayCaptor = ArgumentCaptor.forClass(byte[].class);
+ BroadlinkRemoteHandler model2 = new BroadlinkRemoteModel2Handler(thing, commandDescriptionProvider);
+ setMocksForTesting(model2);
+ reset(trafficObserver);
+ model2.getStatusFromDevice();
+
+ verify(trafficObserver).onCommandSent(commandCaptor.capture());
+ assertEquals(0x6a, commandCaptor.getValue().byteValue());
+
+ verify(trafficObserver).onBytesSent(byteArrayCaptor.capture());
+
+ byte[] sentBytes = byteArrayCaptor.getValue();
+ assertEquals(16, sentBytes.length);
+ assertEquals(0x01, sentBytes[0]);
+ }
+
+ @Test
+ public void sendsExpectedBytesWhenSendingCode() throws IOException {
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class);
+ ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class);
+ BroadlinkRemoteHandler model2 = new BroadlinkRemoteModel2Handler(thing, commandDescriptionProvider);
+ setMocksForTesting(model2);
+ // Note the length is 12 so as to not require padding
+ byte[] code = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a };
+
+ reset(trafficObserver);
+ model2.sendCode(code);
+
+ verify(trafficObserver).onCommandSent(commandCaptor.capture());
+ assertEquals(0x6a, commandCaptor.getValue().byteValue());
+
+ verify(trafficObserver).onBytesSent(byteCaptor.capture());
+
+ byte[] sentBytes = byteCaptor.getValue();
+ assertEquals(16, sentBytes.length);
+
+ assertEquals(0x02, sentBytes[0]); // 0x00, 0x00, 0x00
+
+ assertEquals(0x01, sentBytes[4]);
+ assertEquals(0x02, sentBytes[5]);
+ assertEquals(0x03, sentBytes[6]);
+ assertEquals(0x04, sentBytes[7]);
+ assertEquals(0x05, sentBytes[8]);
+ assertEquals(0x06, sentBytes[9]);
+ assertEquals(0x07, sentBytes[10]);
+ assertEquals(0x08, sentBytes[11]);
+ assertEquals(0x09, sentBytes[12]);
+ assertEquals(0x0a, sentBytes[13]);
+ }
+
+ @Test
+ public void sendsExpectedBytesWhenSendingCodeIncludingPadding() throws IOException {
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class);
+ ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class);
+ BroadlinkRemoteHandler model2 = new BroadlinkRemoteModel2Handler(thing, commandDescriptionProvider);
+ setMocksForTesting(model2);
+ // Note the length is such that padding up to the next multiple of 16 will be needed
+ byte[] code = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
+ 0x11 };
+ reset(trafficObserver);
+ model2.sendCode(code);
+
+ verify(trafficObserver).onCommandSent(commandCaptor.capture());
+ assertEquals(0x6a, commandCaptor.getValue().byteValue());
+
+ verify(trafficObserver).onBytesSent(byteCaptor.capture());
+
+ byte[] sentBytes = byteCaptor.getValue();
+ assertEquals(32, sentBytes.length);
+
+ assertEquals(0x02, sentBytes[0]); // 0x00, 0x00, 0x00
+
+ assertEquals(0x01, sentBytes[4]);
+ assertEquals(0x02, sentBytes[5]);
+ assertEquals(0x03, sentBytes[6]);
+ assertEquals(0x04, sentBytes[7]);
+ assertEquals(0x05, sentBytes[8]);
+ assertEquals(0x06, sentBytes[9]);
+ assertEquals(0x07, sentBytes[10]);
+ assertEquals(0x08, sentBytes[11]);
+ assertEquals(0x09, sentBytes[12]);
+ assertEquals(0x0a, sentBytes[13]);
+ assertEquals(0x0b, sentBytes[14]);
+ assertEquals(0x0c, sentBytes[15]);
+ assertEquals(0x0d, sentBytes[16]);
+ assertEquals(0x0e, sentBytes[17]);
+ assertEquals(0x0f, sentBytes[18]);
+ assertEquals(0x10, sentBytes[19]);
+ assertEquals(0x11, sentBytes[20]);
+ assertEquals(0x00, sentBytes[21]);
+ assertEquals(0x00, sentBytes[31]);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel3HandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel3HandlerTest.java
new file mode 100644
index 0000000000000..96584cd944919
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel3HandlerTest.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_LEARN;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+
+/**
+ * Tests the Remote Model 3 handler.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkRemoteModel3HandlerTest extends AbstractBroadlinkThingHandlerTest {
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ configureUnderlyingThing(BroadlinkBindingConstants.THING_TYPE_RM3, "rm3-test");
+ MockitoAnnotations.openMocks(this).close();
+ Mockito.when(mockSocket.sendAndReceive(Mockito.any(byte[].class), Mockito.anyString())).thenReturn(response);
+ }
+
+ private byte[] response = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
+
+ @Test
+ public void sendsExpectedBytesWhenEnteringLearnMode() throws IOException {
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class);
+ ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class);
+ BroadlinkRemoteHandler model3 = new BroadlinkRemoteModel3Handler(thing, commandDescriptionProvider);
+ setMocksForTesting(model3);
+
+ reset(trafficObserver);
+ model3.handleLearningCommand(LEARNING_CONTROL_COMMAND_LEARN);
+
+ verify(trafficObserver).onCommandSent(commandCaptor.capture());
+ assertEquals(0x6a, commandCaptor.getValue().byteValue());
+
+ verify(trafficObserver).onBytesSent(byteCaptor.capture());
+
+ byte[] sentBytes = byteCaptor.getValue();
+ assertEquals(16, sentBytes.length);
+
+ assertEquals(0x03, sentBytes[0]); // 0x03, then fifteen zeroes
+ assertEquals(0x00, sentBytes[1]);
+ assertEquals(0x00, sentBytes[2]);
+ assertEquals(0x00, sentBytes[3]);
+ assertEquals(0x00, sentBytes[4]);
+ assertEquals(0x00, sentBytes[5]);
+ assertEquals(0x00, sentBytes[6]);
+ assertEquals(0x00, sentBytes[7]);
+ assertEquals(0x00, sentBytes[8]);
+ assertEquals(0x00, sentBytes[9]);
+ assertEquals(0x00, sentBytes[10]);
+ assertEquals(0x00, sentBytes[11]);
+ assertEquals(0x00, sentBytes[12]);
+ assertEquals(0x00, sentBytes[13]);
+ assertEquals(0x00, sentBytes[14]);
+ assertEquals(0x00, sentBytes[15]);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel4HandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel4HandlerTest.java
new file mode 100644
index 0000000000000..1b0f57bc6c3a9
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkRemoteModel4HandlerTest.java
@@ -0,0 +1,233 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+/**
+ * Tests the Remote Model 4 handler.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkRemoteModel4HandlerTest extends AbstractBroadlinkThingHandlerTest {
+
+ private byte[] response = { (byte) 0x5a, (byte) 0xa5, (byte) 0xaa, (byte) 0x55, (byte) 0x5a, (byte) 0xa5,
+ (byte) 0xaa, (byte) 0x55, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0xf6, (byte) 0xcd, (byte) 0x00, (byte) 0x00, (byte) 0x14, (byte) 0x27,
+ (byte) 0x6a, (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x11, (byte) 0x22,
+ (byte) 0x11, (byte) 0x22, (byte) 0x11, (byte) 0x22, (byte) 0x11, (byte) 0x22, (byte) 0x00, (byte) 0xc0,
+ (byte) 0x00, (byte) 0x00, (byte) 0x09, (byte) 0x9a, (byte) 0x60, (byte) 0xfb, (byte) 0x72, (byte) 0x07,
+ (byte) 0x4f, (byte) 0x89, (byte) 0xf8, (byte) 0xb4, (byte) 0xdb, (byte) 0xb0, (byte) 0x72, (byte) 0xe7,
+ (byte) 0x1f, (byte) 0x86, };
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ configureUnderlyingThing(BroadlinkBindingConstants.THING_TYPE_RM4, "rm4-test");
+ MockitoAnnotations.openMocks(this).close();
+ Mockito.when(mockSocket.sendAndReceive(Mockito.any(byte[].class), Mockito.anyString())).thenReturn(response);
+ }
+
+ @Test
+ public void sendsExpectedBytesWhenGettingDeviceStatus() {
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class);
+
+ ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class);
+ BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4Handler(thing, commandDescriptionProvider);
+ setMocksForTesting(model4);
+ reset(trafficObserver);
+ model4.getStatusFromDevice();
+
+ verify(trafficObserver).onCommandSent(commandCaptor.capture());
+ assertEquals(0x6a, commandCaptor.getValue().byteValue());
+
+ verify(trafficObserver).onBytesSent(byteCaptor.capture());
+
+ byte[] sentBytes = byteCaptor.getValue();
+ assertEquals(16, sentBytes.length);
+
+ assertEquals(0x04, sentBytes[0]);
+ assertEquals(0x00, sentBytes[1]);
+ assertEquals(0x24, sentBytes[2]);
+ }
+
+ @Test
+ public void sendsExpectedBytesWhenSendingCode() throws IOException {
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class);
+ ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class);
+ BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4Handler(thing, commandDescriptionProvider);
+ setMocksForTesting(model4);
+ // Note the length is 10 so as to not require padding (6 byte preamble)
+ byte[] code = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a };
+
+ reset(trafficObserver);
+ model4.sendCode(code);
+
+ verify(trafficObserver).onCommandSent(commandCaptor.capture());
+ assertEquals(0x6a, commandCaptor.getValue().byteValue());
+
+ verify(trafficObserver).onBytesSent(byteCaptor.capture());
+
+ byte[] sentBytes = byteCaptor.getValue();
+ assertEquals(16, sentBytes.length);
+
+ assertEquals((byte) 0x0e, (byte) sentBytes[0]);
+ assertEquals(0x00, sentBytes[1]);
+
+ assertEquals(0x02, sentBytes[2]);
+ assertEquals(0x00, sentBytes[3]);
+ assertEquals(0x00, sentBytes[4]);
+ assertEquals(0x00, sentBytes[5]);
+
+ assertEquals(0x01, sentBytes[6]);
+ assertEquals(0x02, sentBytes[7]);
+ assertEquals(0x03, sentBytes[8]);
+ assertEquals(0x04, sentBytes[9]);
+ assertEquals(0x05, sentBytes[10]);
+ assertEquals(0x06, sentBytes[11]);
+ assertEquals(0x07, sentBytes[12]);
+ assertEquals(0x08, sentBytes[13]);
+ assertEquals(0x09, sentBytes[14]);
+ assertEquals(0x0a, sentBytes[15]);
+ }
+
+ @Test
+ public void sendsExpectedBytesWhenSendingCodeIncludingPadding() throws IOException {
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class);
+ ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class);
+ BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4Handler(thing, commandDescriptionProvider);
+ setMocksForTesting(model4);
+ // Note the length is such that padding up to the next multiple of 16 will be needed
+ byte[] code = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b };
+ reset(trafficObserver);
+ model4.sendCode(code);
+
+ verify(trafficObserver).onCommandSent(commandCaptor.capture());
+ assertEquals(0x6a, commandCaptor.getValue().byteValue());
+
+ verify(trafficObserver).onBytesSent(byteCaptor.capture());
+
+ byte[] sentBytes = byteCaptor.getValue();
+ assertEquals(32, sentBytes.length);
+
+ // Calculated length field is len(data) + 4 ===> 11 + 4 = 15 = 0x0f
+
+ assertEquals((byte) 0x0f, (byte) sentBytes[0]);
+ assertEquals(0x00, sentBytes[1]);
+
+ assertEquals(0x02, sentBytes[2]); // The "send code" command
+
+ assertEquals(0x01, sentBytes[6]); // The payload
+ assertEquals(0x02, sentBytes[7]);
+ assertEquals(0x03, sentBytes[8]);
+ assertEquals(0x04, sentBytes[9]);
+ assertEquals(0x05, sentBytes[10]);
+ assertEquals(0x06, sentBytes[11]);
+ assertEquals(0x07, sentBytes[12]);
+ assertEquals(0x08, sentBytes[13]);
+ assertEquals(0x09, sentBytes[14]);
+ assertEquals(0x0a, sentBytes[15]);
+ assertEquals(0x0b, sentBytes[16]);
+ assertEquals(0x00, sentBytes[17]);
+ }
+
+ @Test
+ public void setsTheTemperatureAndHumidityChannelsAfterGettingStatus() {
+ BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4Handler(thing, commandDescriptionProvider);
+ setMocksForTesting(model4);
+ reset(mockCallback);
+
+ model4.getStatusFromDevice();
+
+ ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(State.class);
+ verify(mockCallback, times(2)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+
+ List channelCaptures = channelCaptor.getAllValues();
+ List stateCaptures = stateCaptor.getAllValues();
+
+ ChannelUID expectedTemperatureChannel = new ChannelUID(thing.getUID(), CHANNEL_TEMPERATURE);
+ assertEquals(expectedTemperatureChannel, channelCaptures.get(0));
+
+ QuantityType expectedTemperature = new QuantityType<>(21.22,
+ BroadlinkBindingConstants.BROADLINK_TEMPERATURE_UNIT);
+ assertEquals(expectedTemperature, stateCaptures.get(0));
+
+ ChannelUID expectedHumidityChannel = new ChannelUID(thing.getUID(), CHANNEL_HUMIDITY);
+ assertEquals(expectedHumidityChannel, channelCaptures.get(1));
+
+ QuantityType expectedHumidity = new QuantityType<>(39.4,
+ BroadlinkBindingConstants.BROADLINK_HUMIDITY_UNIT);
+ assertEquals(expectedHumidity, stateCaptures.get(1));
+ }
+
+ @Test
+ public void sendsExpectedBytesWhenEnteringLearnMode() throws IOException {
+ ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class);
+ ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class);
+ BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4Handler(thing, commandDescriptionProvider);
+ setMocksForTesting(model4);
+
+ reset(trafficObserver);
+ model4.handleLearningCommand(LEARNING_CONTROL_COMMAND_LEARN);
+
+ verify(trafficObserver).onCommandSent(commandCaptor.capture());
+ assertEquals(0x6a, commandCaptor.getValue().byteValue());
+
+ verify(trafficObserver).onBytesSent(byteCaptor.capture());
+
+ byte[] sentBytes = byteCaptor.getValue();
+ assertEquals(16, sentBytes.length);
+
+ // Expecting:
+ // PLl, PLh, 0 0 0 then padding for the rest up to 16
+ // Where PL = length(data) + 4 - so in this case, 4
+ assertEquals(0x04, sentBytes[0]); // Low length byte
+ assertEquals(0x00, sentBytes[1]); // High length byte
+ assertEquals(0x03, sentBytes[2]);
+ assertEquals(0x00, sentBytes[3]);
+ assertEquals(0x00, sentBytes[4]);
+ assertEquals(0x00, sentBytes[5]);
+ assertEquals(0x00, sentBytes[6]);
+ assertEquals(0x00, sentBytes[7]);
+ assertEquals(0x00, sentBytes[8]);
+ assertEquals(0x00, sentBytes[9]);
+ assertEquals(0x00, sentBytes[10]);
+ assertEquals(0x00, sentBytes[11]);
+ assertEquals(0x00, sentBytes[12]);
+ assertEquals(0x00, sentBytes[13]);
+ assertEquals(0x00, sentBytes[14]);
+ assertEquals(0x00, sentBytes[15]);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel2HandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel2HandlerTest.java
new file mode 100644
index 0000000000000..15d3b78bc1d9e
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel2HandlerTest.java
@@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.CHANNEL_POWER_CONSUMPTION;
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.COMMAND_POWER_ON;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+/**
+ * Tests the Socket Model 2 handler.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkSocketModel2HandlerTest extends AbstractBroadlinkThingHandlerTest {
+
+ private final byte[] response = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ configureUnderlyingThing(BroadlinkBindingConstants.THING_TYPE_SP2, "sp2-test");
+ MockitoAnnotations.openMocks(this).close();
+ Mockito.when(mockSocket.sendAndReceive(Mockito.any(byte[].class), Mockito.anyString())).thenReturn(response);
+ }
+
+ @Test
+ public void derivePowerStateBitsOff() {
+ BroadlinkSocketModel2Handler model2 = new BroadlinkSocketModel2Handler(thing, false);
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x00 };
+ OnOffType result = model2.derivePowerStateFromStatusBytes(payload);
+ assertEquals(OnOffType.OFF, result);
+ }
+
+ @Test
+ public void derivePowerStateBitsOn1() {
+ BroadlinkSocketModel2Handler model2 = new BroadlinkSocketModel2Handler(thing, false);
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x01 };
+ OnOffType result = model2.derivePowerStateFromStatusBytes(payload);
+ assertEquals(OnOffType.ON, result);
+ }
+
+ @Test
+ public void derivePowerStateBitsOn3() {
+ BroadlinkSocketModel2Handler model2 = new BroadlinkSocketModel2Handler(thing, false);
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x03 };
+ OnOffType result = model2.derivePowerStateFromStatusBytes(payload);
+ assertEquals(OnOffType.ON, result);
+ }
+
+ @Test
+ public void derivePowerStateBitsOnFD() {
+ BroadlinkSocketModel2Handler model2 = new BroadlinkSocketModel2Handler(thing, false);
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, (byte) 0xFD };
+ OnOffType result = model2.derivePowerStateFromStatusBytes(payload);
+ assertEquals(OnOffType.ON, result);
+ }
+
+ @Test
+ public void derivePowerConsumptionFromStatusBytesTooShort() throws IOException {
+ BroadlinkSocketModel2Handler model2 = new BroadlinkSocketModel2Handler(thing, false);
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x33 };
+ double result = model2.derivePowerConsumption(payload);
+ assertEquals(0D, result, 0.1D);
+ }
+
+ @Test
+ public void derivePowerConsumptionFromStatusBytesCorrect() throws IOException {
+ BroadlinkSocketModel2Handler model2 = new BroadlinkSocketModel2Handler(thing, false);
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x01, 0x00 };
+ double result = model2.derivePowerConsumption(payload);
+ assertEquals(66.051D, result, 0.1D);
+ }
+
+ @Test
+ public void setsThePowerChannelOnlyAfterGettingStatusOnSP2() {
+ byte[] response = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, };
+ Mockito.when(mockSocket.sendAndReceive(Mockito.any(byte[].class), Mockito.anyString())).thenReturn(response);
+ BroadlinkSocketHandler model2 = new BroadlinkSocketModel2Handler(thing, false);
+ setMocksForTesting(model2);
+
+ reset(mockCallback);
+
+ model2.getStatusFromDevice();
+
+ ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(State.class);
+ verify(mockCallback).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+
+ ChannelUID expectedPowerChannel = new ChannelUID(thing.getUID(), COMMAND_POWER_ON);
+ assertEquals(expectedPowerChannel, channelCaptor.getValue());
+
+ assertEquals(OnOffType.ON, stateCaptor.getValue());
+ }
+
+ @Test
+ public void setsThePowerAndPowerConsumptionAfterGettingStatusOnSP2S() {
+ // Power bytes are 4, 5, 6 (little-endian)
+ // So here it's 0x38291 => 230033, divided by 1000 ==> 230.033W
+ byte[] payload = { 0x08, 0x00, 0x11, 0x22, (byte) 0x91, (byte) 0x82, 0x3, 0x16, 0x27, 0x28, 0x01, 0x02, 0x03,
+ 0x04, 0x05, 0x16 };
+ byte[] responseMessage = generateReceivedBroadlinkMessage(payload);
+ Mockito.when(mockSocket.sendAndReceive(Mockito.any(byte[].class), Mockito.anyString()))
+ .thenReturn(responseMessage);
+ BroadlinkSocketHandler model2s = new BroadlinkSocketModel2Handler(thing, true);
+ setMocksForTesting(model2s);
+
+ reset(mockCallback);
+
+ model2s.getStatusFromDevice();
+
+ ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(State.class);
+ verify(mockCallback, Mockito.times(2)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+
+ List channels = channelCaptor.getAllValues();
+ List states = stateCaptor.getAllValues();
+
+ ChannelUID expectedPowerChannel = new ChannelUID(thing.getUID(), COMMAND_POWER_ON);
+ assertEquals(expectedPowerChannel, channels.get(0));
+
+ assertEquals(OnOffType.ON, states.get(0));
+
+ ChannelUID expectedConsumptionChannel = new ChannelUID(thing.getUID(), CHANNEL_POWER_CONSUMPTION);
+ assertEquals(expectedConsumptionChannel, channels.get(1));
+
+ QuantityType expectedPower = new QuantityType<>(230.033,
+ BroadlinkBindingConstants.BROADLINK_POWER_CONSUMPTION_UNIT);
+ assertEquals(expectedPower, states.get(1));
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3HandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3HandlerTest.java
new file mode 100644
index 0000000000000..61090e008aa25
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3HandlerTest.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.*;
+import static org.openhab.binding.broadlink.handler.BroadlinkSocketModel3Handler.mergeOnOffBits;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+/**
+ * Tests the Socket Model 3 handler.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkSocketModel3HandlerTest extends AbstractBroadlinkThingHandlerTest {
+
+ private final byte[] response = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
+
+ private final BroadlinkSocketModel3Handler model3;
+
+ public BroadlinkSocketModel3HandlerTest() {
+ super();
+ configureUnderlyingThing(BroadlinkBindingConstants.THING_TYPE_SP3, "sp3-test");
+ model3 = new BroadlinkSocketModel3Handler(thing);
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this).close();
+ Mockito.when(mockSocket.sendAndReceive(Mockito.any(byte[].class), Mockito.anyString())).thenReturn(response);
+ }
+
+ @Test
+ public void mergeOnOffBitsAllZero() {
+ int result = mergeOnOffBits(OnOffType.OFF, OnOffType.OFF);
+ assertEquals(0x00, result);
+ }
+
+ @Test
+ public void mergeOnOffBitsPowerOn() {
+ int result = mergeOnOffBits(OnOffType.ON, OnOffType.OFF);
+ assertEquals(0x01, result);
+ }
+
+ @Test
+ public void mergeOnOffBitsNightlightOn() {
+ int result = mergeOnOffBits(OnOffType.OFF, OnOffType.ON);
+ assertEquals(0x02, result);
+ }
+
+ @Test
+ public void mergeOnOffBitsAllOn() {
+ int result = mergeOnOffBits(OnOffType.ON, OnOffType.ON);
+ assertEquals(0x03, result);
+ }
+
+ @Test
+ public void deriveNightLightStateBitsOff() {
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x00 };
+ OnOffType result = model3.deriveNightLightStateFromStatusBytes(payload);
+ assertEquals(OnOffType.OFF, result);
+ }
+
+ @Test
+ public void deriveNightLightStateBitsOn2() {
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x02 };
+ OnOffType result = model3.deriveNightLightStateFromStatusBytes(payload);
+ assertEquals(OnOffType.ON, result);
+ }
+
+ @Test
+ public void deriveNightLightStateBitsOn3() {
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x03 };
+ OnOffType result = model3.deriveNightLightStateFromStatusBytes(payload);
+ assertEquals(OnOffType.ON, result);
+ }
+
+ @Test
+ public void deriveNightLightStateBitsOnFF() {
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, (byte) 0xFF };
+ OnOffType result = model3.deriveNightLightStateFromStatusBytes(payload);
+ assertEquals(OnOffType.ON, result);
+ }
+
+ @Test
+ public void setsThePowerChannelAndNightlightAfterGettingStatusOnSP3() {
+ Mockito.when(mockSocket.sendAndReceive(Mockito.any(byte[].class), Mockito.anyString())).thenReturn(response);
+ setMocksForTesting(model3);
+
+ reset(mockCallback);
+
+ model3.getStatusFromDevice();
+
+ ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(State.class);
+ verify(mockCallback, Mockito.times(2)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+
+ List channels = channelCaptor.getAllValues();
+ List states = stateCaptor.getAllValues();
+
+ ChannelUID expectedPowerChannel = new ChannelUID(thing.getUID(), COMMAND_POWER_ON);
+ assertEquals(expectedPowerChannel, channels.get(0));
+
+ assertEquals(OnOffType.ON, states.get(0));
+
+ ChannelUID expectedNightlightChannel = new ChannelUID(thing.getUID(), COMMAND_NIGHTLIGHT);
+ assertEquals(expectedNightlightChannel, channels.get(1));
+
+ assertEquals(OnOffType.OFF, states.get(1));
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3SHandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3SHandlerTest.java
new file mode 100644
index 0000000000000..fa5408dd4d3fa
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/handler/BroadlinkSocketModel3SHandlerTest.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.handler;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.openhab.binding.broadlink.BroadlinkBindingConstants.*;
+
+import java.util.List;
+
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+/**
+ * Tests the Socket Model 3S (SP3S) handler.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkSocketModel3SHandlerTest extends AbstractBroadlinkThingHandlerTest {
+
+ private final BroadlinkSocketModel3SHandler model3s;
+
+ public BroadlinkSocketModel3SHandlerTest() {
+ super();
+ configureUnderlyingThing(BroadlinkBindingConstants.THING_TYPE_SP3S, "sp3s-test");
+ model3s = new BroadlinkSocketModel3SHandler(thing);
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this).close();
+ }
+
+ @Test
+ public void deriveSp3sPowerConsumptionTooShort() {
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x33 };
+ double result = model3s.deriveSP3sPowerConsumption(payload);
+ assertEquals(0D, result, 0.1D);
+ }
+
+ @Test
+ public void deriveSp3sPowerConsumptionCorrectSmallValue() {
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x02, 0x00 };
+ double result = model3s.deriveSP3sPowerConsumption(payload);
+ assertEquals(2.19D, result, 0.1D);
+ }
+
+ @Test
+ public void deriveSp3sPowerConsumptionCorrectMediumValue() {
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x75, 0x00 };
+ double result = model3s.deriveSP3sPowerConsumption(payload);
+ assertEquals(75.33D, result, 0.1D);
+ }
+
+ @Test
+ public void deriveSp3sPowerConsumptionCorrectLargeValue() {
+ byte[] payload = { 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x99, (byte) 0x88, 0x07 };
+ double result = model3s.deriveSP3sPowerConsumption(payload);
+ assertEquals(788.99D, result, 0.1D);
+ }
+
+ @Test
+ public void setsThePowerChannelAndConsumptionAfterGettingStatusOnSP3S() {
+ // Power bytes are 5, 6, 7 (little-endian) in BCD
+ // So here it's 0x38291 => 38291, divided by 100 ==> 382.91W
+ byte[] payload = { 0x08, 0x00, 0x11, 0x22, 0x01, (byte) 0x91, (byte) 0x82, 0x3, 0x16, 0x27, 0x28, 0x01, 0x02,
+ 0x04, 0x05, 0x16 };
+ byte[] responseMessage = generateReceivedBroadlinkMessage(payload);
+ Mockito.when(mockSocket.sendAndReceive(Mockito.any(byte[].class), Mockito.anyString()))
+ .thenReturn(responseMessage);
+ setMocksForTesting(model3s);
+
+ reset(mockCallback);
+
+ model3s.getStatusFromDevice();
+
+ ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
+ ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(State.class);
+ verify(mockCallback, Mockito.times(2)).stateUpdated(channelCaptor.capture(), stateCaptor.capture());
+
+ List channels = channelCaptor.getAllValues();
+ List states = stateCaptor.getAllValues();
+
+ ChannelUID expectedPowerChannel = new ChannelUID(thing.getUID(), COMMAND_POWER_ON);
+ assertEquals(expectedPowerChannel, channels.get(0));
+
+ assertEquals(OnOffType.ON, states.get(0));
+
+ ChannelUID expectedConsumptionChannel = new ChannelUID(thing.getUID(), CHANNEL_POWER_CONSUMPTION);
+ assertEquals(expectedConsumptionChannel, channels.get(1));
+
+ QuantityType expectedPower = new QuantityType<>(382.91,
+ BroadlinkBindingConstants.BROADLINK_POWER_CONSUMPTION_UNIT);
+ assertEquals(expectedPower, states.get(1));
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/BroadlinkMappingServiceTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/BroadlinkMappingServiceTest.java
new file mode 100644
index 0000000000000..67afac2315743
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/BroadlinkMappingServiceTest.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.openhab.binding.broadlink.AbstractBroadlinkTest;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.CommandOption;
+
+/**
+ * Tests the Broadlink mapping service.
+ *
+ * @author John Marshall - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkMappingServiceTest extends AbstractBroadlinkTest {
+ private static final String TEST_MAP_FILE = "broadlink.map";
+ private static final ChannelUID TEST_CHANNEL_UID = new ChannelUID("bsm:test:channel:uid");
+
+ private BroadlinkRemoteDynamicCommandDescriptionProvider mockProvider = Mockito
+ .mock(BroadlinkRemoteDynamicCommandDescriptionProvider.class);
+
+ @Test
+ public void canReadFromAMapFile() {
+ BroadlinkMappingService bms = new BroadlinkMappingService(TEST_MAP_FILE, mockProvider, TEST_CHANNEL_UID);
+
+ assertEquals("00112233", bms.lookup("TEST_COMMAND_ON"));
+ assertEquals("33221100", bms.lookup("TEST_COMMAND_OFF"));
+ }
+
+ @Test
+ public void notifiesTheFrameworkOfTheAvailableCommands() {
+ new BroadlinkMappingService(TEST_MAP_FILE, mockProvider, TEST_CHANNEL_UID);
+
+ List expected = new ArrayList<>();
+ expected.add(new CommandOption("TEST_COMMAND_ON", null));
+ expected.add(new CommandOption("TEST_COMMAND_OFF", null));
+ verify(mockProvider).setCommandOptions(TEST_CHANNEL_UID, expected);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/BroadlinkProtocolTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/BroadlinkProtocolTest.java
new file mode 100644
index 0000000000000..01d2bb91e7bc9
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/BroadlinkProtocolTest.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.openhab.core.util.HexUtils;
+import org.slf4j.Logger;
+
+/**
+ * Tests the Broadlink protocol.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkProtocolTest {
+
+ private final Logger mockLogger = Mockito.mock(Logger.class);
+
+ byte[] mac = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, };
+
+ byte[] deviceId = { 0x10, 0x11, 0x12, 0x13 };
+
+ byte[] iv = HexUtils.hexToBytes("562e17996d093d28ddb3ba695a2e6f58");
+
+ byte[] deviceKey = HexUtils.hexToBytes("097628343fe99e23765c1513accf8b02");
+
+ @Test
+ public void canBuildMessageWithCorrectChecksums() {
+ byte[] payload = {};
+ byte[] result = BroadlinkProtocol.buildMessage((byte) 0x0, payload, 0, mac, deviceId, iv, deviceKey, 1234,
+ mockLogger);
+
+ assertEquals(56, result.length);
+
+ // bytes 0x34 and 0x35 contain the payload checksum,
+ // which given we have an empty payload, should be the initial
+ // 0xBEAF
+ int payloadChecksum = ((result[0x35] & 0xff) << 8) + (result[0x34] & 0xff);
+ assertEquals(0xbeaf, payloadChecksum);
+
+ // bytes 0x20 and 0x21 contain the overall checksum
+ int overallChecksum = ((result[0x21] & 0xff) << 8) + (result[0x20] & 0xff);
+ assertEquals(0xc549, overallChecksum);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/ModelMapperTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/ModelMapperTest.java
new file mode 100644
index 0000000000000..af08ac7bfded8
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/ModelMapperTest.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.openhab.binding.broadlink.BroadlinkBindingConstants;
+import org.slf4j.Logger;
+
+/**
+ * Tests that each Thing Type maps to the right model number.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class ModelMapperTest {
+
+ private Logger mockLogger = Mockito.mock(Logger.class);
+
+ @Test
+ public void mapsSpMini2ASp2() {
+ assertEquals(BroadlinkBindingConstants.THING_TYPE_SP2, ModelMapper.getThingType(0x7539, mockLogger));
+ }
+
+ @Test
+ public void mapsRmMini3AsRm3() {
+ assertEquals(BroadlinkBindingConstants.THING_TYPE_RM3, ModelMapper.getThingType(0x27c2, mockLogger));
+ }
+
+ @Test
+ public void mapsRm35f36AsRm3Q() {
+ assertEquals(BroadlinkBindingConstants.THING_TYPE_RM3Q, ModelMapper.getThingType(0x5f36, mockLogger));
+ }
+
+ @Test
+ public void mapsRm4bAsRm4() {
+ assertEquals(BroadlinkBindingConstants.THING_TYPE_RM4, ModelMapper.getThingType(0x51da, mockLogger));
+ }
+
+ @Test
+ public void mapsRm4ProAsRm4() {
+ assertEquals(BroadlinkBindingConstants.THING_TYPE_RM4, ModelMapper.getThingType(0x61a2, mockLogger));
+ }
+
+ @Test
+ public void mapsRm462bcAsRm4() {
+ assertEquals(BroadlinkBindingConstants.THING_TYPE_RM4, ModelMapper.getThingType(0x62bc, mockLogger));
+ }
+
+ @Test
+ public void mapsRm4Model6026AsRm4() {
+ assertEquals(BroadlinkBindingConstants.THING_TYPE_RM4, ModelMapper.getThingType(0x6026, mockLogger));
+ }
+
+ @Test
+ public void mapsRm4Model24846AsRm4() {
+ assertEquals(BroadlinkBindingConstants.THING_TYPE_RM4, ModelMapper.getThingType(24846, mockLogger));
+ }
+
+ @Test
+ public void throwsOnUnrecognisedDeviceModel() {
+ try {
+ ModelMapper.getThingType(0x6666, mockLogger);
+ Assertions.fail("Should have thrown on unmapped device model");
+ } catch (Exception e) {
+ assertEquals(
+ "Device identifying itself as '26214' (hex 0x6666) is not currently supported. Please report this to the developer!",
+ e.getMessage());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/UtilsTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/UtilsTest.java
new file mode 100644
index 0000000000000..6fd22db0b7363
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/UtilsTest.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2010-2023 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.broadlink.internal;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the generic utility functions.
+ *
+ * @author John Marshall/Cato Sognen - Initial contribution
+ */
+@NonNullByDefault
+public class UtilsTest {
+ @Test
+ public void padToDoesNothingOnQuotientSizedBuffer() {
+ byte[] source = { 0x01, 0x02, 0x03, 0x04 };
+
+ byte[] result = Utils.padTo(source, 4);
+ assertEquals(source, result);
+ }
+
+ @Test
+ public void padToExtendsOversizedBuffer() {
+ byte[] source = { 0x01, 0x02, 0x03, 0x04 };
+
+ byte[] result = Utils.padTo(source, 3);
+ assertEquals(6, result.length);
+ byte[] expected = { 0x01, 0x02, 0x03, 0x04, 0x0, 0x0, };
+
+ assertArrayEquals(expected, result);
+ }
+
+ @Test
+ public void padToExtendsUndersizedBuffer() {
+ byte[] source = { 0x01, 0x02, 0x03, 0x04 };
+
+ byte[] result = Utils.padTo(source, 8);
+ assertEquals(8, result.length);
+ byte[] expected = { 0x01, 0x02, 0x03, 0x04, 0x0, 0x0, 0x0, 0x0, };
+
+ assertArrayEquals(expected, result);
+ }
+
+ @Test
+ public void padToExtendsUndersizedBufferMultiple() {
+ byte[] source = { 0x01, 0x02, 0x03, 0x04 };
+
+ byte[] result = Utils.padTo(source, 16);
+ assertEquals(16, result.length);
+ byte[] expected = { 0x01, 0x02, 0x03, 0x04, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, };
+
+ assertArrayEquals(expected, result);
+ }
+
+ @Test
+ public void toHexStringWorksOnSingleByte() {
+ byte[] source = { (byte) 0x81 };
+
+ String result = Utils.toHexString(source);
+
+ assertEquals("81", result);
+ }
+
+ @Test
+ public void toHexStringWorksOnMultipleBytesLowValues() {
+ byte[] source = { 0x01, 0x02, 0x03, 0x04 };
+
+ String result = Utils.toHexString(source);
+
+ assertEquals("01020304", result);
+ }
+
+ @Test
+ public void toHexStringWorksOnMultipleBytesMixedValues() {
+ byte[] source = { (byte) 0x81, (byte) 0xC2, 0x03, (byte) 0xA3, (byte) 0xF4 };
+
+ String result = Utils.toHexString(source);
+
+ assertEquals("81c203a3f4", result);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlink/src/test/resources/conf/transform/broadlink.map b/bundles/org.openhab.binding.broadlink/src/test/resources/conf/transform/broadlink.map
new file mode 100644
index 0000000000000..5d178b84e5377
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlink/src/test/resources/conf/transform/broadlink.map
@@ -0,0 +1,3 @@
+TEST_COMMAND_ON=00112233
+
+TEST_COMMAND_OFF=33221100
\ No newline at end of file
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 23bf367cd0e9e..d0fe3c6814b1e 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -82,6 +82,7 @@
org.openhab.binding.boschindego
org.openhab.binding.boschshc
org.openhab.binding.bosesoundtouch
+ org.openhab.binding.broadlink
org.openhab.binding.broadlinkthermostat
org.openhab.binding.bsblan
org.openhab.binding.bticinosmarther