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