diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 22b4e2b1f2f89..0604b3a395641 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -276,6 +276,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..2d44fc08cc132 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/README.md @@ -0,0 +1,235 @@ +# 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 | +| sp2-s | OEM SP2 Mini WiFi Smart Socket with night light | +| sp3 | Broadlink SP3/Mini WiFi Smart Socket with night light | +| sp3-s | Broadlink SP3s WiFi Smart Socket with Power Meter | +| rm-pro | Broadline RM Pro WiFi IR/RF Transmitter with temperature sensor | +| rm3 | Broadlink RM3/Mini WiFi IR Transmitter | +| rm3-q | Broadlink RM3 WiFi IR Transmitter with Firmware v44057 | +| rm4-pro | Broadlink RM4 Pro WiFi RF/IR Transmitter with temperature and humidity sensors| +| rm4-mini | Broadlink RM4 mini WiFi IR Transmitter | + +## 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 | +| nameOfCommandToLearn| String | DEVICE_ON | The name of the IR or RF command to learn when using the learn command channel | + +## Channels + +| Channel | Supported Devices | Type | Description | +|-------------------|--------------------------|----------------------|-------------------------------------------------| +| power-on | MP2, all SPx | Switch | Power on/off for switches/strips | +| night-light | SP3 | Switch | Night light on/off | +| temperature | A1, RM Pro, 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` | +| power-on-s1 | MP1, MP1_1k3s2u | Switch | Socket 1 power | +| power-on-s2 | MP1, MP1_1k3s2u | Switch | Socket 2 power | +| power-on-s3 | MP1, v_1k3s2u | Switch | Socket 3 power | +| power-on-s4 | MP1 | Switch | Socket 4 power | +| power-on-usb | MP1_1k3s2u | Switch | USB power | +| power-consumption | MP2, SP2s,SP3s | Number:Power | Power consumption | +| command | all RMx | String | IR Command code to transmit | +| rf-command | RM Pro, RM4 Pro | String | RF Command code to transmit +| learning-control | all RMx | String | Learn mode command channel (see below) | + +## Learning 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: + +0. In the openHAB web UI, navigate to your RMx Thing +1. Set the *Name of IR/RF command to learn* property to the name of the command you want the device to learn +2. Click on its *Channels* tab +3. For IR find the *Remote Learning Control* channel and create an Item for it, for RF use the *Remote RF Learning Control* channel instead.(Only needed the first time) +4. Click the item, and click the rectangular area that is marked NULL +5. In the pop-up menu that appears, select *Learn IR command* for IR or *Learn RF command* for RF +6. *The LED on your RM device will illuminate solidly* +7. Point your IR/RF remote control at your RM device and keep pressing the button you'd like to learn. For RF, this can take 10-30 seconds +8. *The LED on your RM device will extinguish once it has identified the command* +9. If the command has been identified succesfully, the channel will have changed it name to "Learn command" or *RF command learnt* +10. If no succes, the channel will be named "NULL". Inspect the `openhab.log` file on your openHAB server for any issues +11. Check and save the IR/RF command by clicking the item once more and select "Check and save command". +12. Keep pressing the remote control with the command to check and save +13. If succesfull, the channel will change name to the command saved +14. If no succes, the channel be named "NULL", restart from step 3. + +### Modify or Delete Remote Codes + +The binding is also capable of modifying a previously stored code, and to delete a previously stored code. + +To modify a previously stored code, the procedure is the same as the one shown above, except that in step 4, the option to choose is *Modify IR command* or *Modify RF Command* + +*Please note that the "Learn command" will not modify a previously existent code, and the "Modify" command will not create a new command. +This is done to avoid accidentally overwriting commands* + +In order to delete a previously stored code, the procedure is as follows: + +0. In the openHAB web UI, navigate to your RMx Thing +1. Set the *Name of IR/RF command to learn* property to the name of the command you want the device to learn +2. Click on its *Channels* tab +3. For IR find the *Remote Learning Control* channel and create an Item for it, for RF use the *Remote RF Learning Control* channel instead (Only needed the first time). +4. Click the item, and click the rectangular area that is marked NULL +5. In the pop-up menu that appears, select *Delete IR command* for IR or *Delete RF command* for RF + +*VERY IMPORTANT NOTE: As of openHAB version 4.3.0, writing the codes into the files is handled by openHAB. While it is possible to create a file externally, copy it in the proper location and use it as a remote codes database (As it is done in the case of Remote codes file migration) IT IS STRONGLY DISCOURAGED to modify the file while the binding is acive. Please make sure the binding is stopped before you modify a remote codes file manually. Also, have the following things in mind:* + +*-openHAB does not interpret a missing code file as empty. It will assume the file is corrupt and try to read from one of the backups, which can lead to confusion. if you want to empty your code file, create an empty file with a set of culry brackets, one per line* + +*-Remember if you manipulate the code file manually, remember to provide the proper location, and the proper ownership and permissions (Location is `$OPENHAB_USERDATA`, and permissions are `-rw-r--r-- 1 openhab openhab* + +## Full Example + +Items file example; `sockets.items`: + +```java +Switch BroadlinkSP3 "Christmas Lights" [ "Lighting" ] { channel="broadlink:sp3:34-ea-34-22-44-66:power-on" } +``` + + +Thing file example; `rm.things`: + +```java +Thing broadlink:rm4pro:IR_Downstairs "RM 4 Pro IR controller" [ macAddress="e8:16:56:1c:7e:b9", ipAddress="192.168.178.234" ] +Thing broadlink:rm3q:IR_Upstairs "RM 3 IR controller" [ macAddress="24:df:a7:df:0d:53", ipAddress="192.168.178.232" ] +``` + +Items file example; `rm.items`: + +```java +Switch DownstairsAC + +Number:Temperature DownstairsTemperature "Temperature downstairs" ["Temperature", "Measurement"] { channel="broadlink:rm4pro:IR_Downstairs:temperature", unit="°C", stateDescription=" " [pattern="%.1f %unit%"]} +Number:Dimensionless DownstairsHumidity "Humidity downstairs" { channel="broadlink:rm4pro:IR_Downstairs:humidity", unit="%" } + +String IR_Downstairs "Downstairs IR control" { channel="broadlink:rm4pro:IR_Downstairs:command" } +``` + +Rule file example; `AC.rules`: + +```java +rule " AC_Control started" +when + System started +then + DownstairsAC.sendCommand(OFF) + IR_Downstairs.sendCommand("AC_OFF") +end + +rule "Downstairs AC Off" +when + Item DownstairsAC changed to OFF +then + IR_Downstairs.sendCommand("AC_OFF") +end + +rule "Downstairs AC On" +when + Item DownstairsAC changed to ON +then + IR_Downstairs.sendCommand("AC_ON") +end +``` + +This rule file assumes you previously have learned the "AC_ON" and "AC_OFF" IR commands. + + +## Migrating Legacy Map File + +Up to openHAB version 3.3, there was a previous version of this binding that was not part of the openHAB distribution. +It stored the IR/RF commands in a different place and a different format. +If you want to mirgrate from those versions to this version of the binding, please read this section. + +The Broadlink RM family of devices can transmit IR codes. The pro models add RF codes. +The map file contains a list of IR/RF command codes to send via the device. + +IR codes are store in `$OPENHAB_USERDATA/jsondb/broadlink_ir.json` and for the RM Pro series of devices the RF codes are store in `$OPENHAB_USERDATA/jsondb/broadlink_rf.json` + +Before openHAB version 4.3.0, the file used the [Java Properties File format](https://en.wikipedia.org/wiki/.properties) and was stored in the `/transform` folder. +By default, the file name was `broadlink.map` for the IR codes, but could be changed using the `mapFile` setting. +In similar fashion, the RM pro models stored the RF codes in the `broadlinkrf.map` file. + +Here is a map file example of the previous file format: + +``` +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. +For RF, the `rf-command` channel is used. + +Storage of codes is handled by openHAB. The map files are stored in the $OPENHAB_USERDATA/jsondb directory. +As an advantage, the files are now backed up by openHAB, which is more practical for migrations, data robustness, etc. having the storage of the codes handled by openHAB also provides uniformity in where the files are stored. + +With the change of the storage mechanism, the files are also changing format, and codes are now stored in json. +With the change of the storage mechanism, the files are also changing format, and codes are now stored in json. + +```json +{ + "TV_POWER": { + "value": "26008c0092961039103a1039101510151014101510151039103a10391015101411141015101510141139101510141114101510151014103a10141139103911391037123a10391000060092961039103911391014111410151015101411391039103a101411141015101510141015103911141015101510141015101510391015103911391039103a1039103911000d05000000000000000000000000" + }, + "heatpump_off": { + "value": "2600760069380D0C0D0C0D290D0C0D290D0C0D0C0D0C0D290D290D0C0D0C0D0C0D290D290D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D0C0D290D0C0D0C0D0C0D0C0D0C0D0C0D0C0D290D0C0D0C0D0C0D0C0D290D0C0D0C0D0C0D0C0D0C0D0C0D290D0C0D290D290D290D290D290D290E0002900000" + } +} +``` + +The code shown below is a Python script that can be used to convert from the old format to the new one: + +```python +import csv +import json +import sys +import argparse + +parser=argparse.ArgumentParser(description= "Broadlink converter argument parser") +parser.add_argument('-i','--input_filename', help='Input File Name', required=True) +parser.add_argument('-o','--output_filename', help='Output File Name') +args=parser.parse_args() + +result={} +with open(args.input_filename,'r') as f: + red=csv.reader(f, delimiter='=') + for d in red: + result[d[0]] = { "class": "java.lang.String" , "value":d[1]} +if args.output_filename: + with open(args.output_filename, 'w', encoding='utf-8') as f: + json.dump(result, f, ensure_ascii=False, indent=2) +else: + print(json.dumps(result,indent=2)) +``` + +## Credits + +- [Cato Sognen](https://community.openhab.org/u/cato_sognen) +- [JAD](http://www.javadecompilers.com/jad) (Java Decompiler) +- [Ricardo] (https://github.com/rlarranaga) diff --git a/bundles/org.openhab.binding.broadlink/pom.xml b/bundles/org.openhab.binding.broadlink/pom.xml new file mode 100644 index 0000000000000..3f4fa7d0fb3eb --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/pom.xml @@ -0,0 +1,25 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.3.0-SNAPSHOT + + + org.openhab.binding.broadlink + + openHAB Add-ons :: Bundles :: Broadlink Binding + + + + junit + junit + 4.13 + test + + + 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/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..92a089a31e15d --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkBindingConstants.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2024 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.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 String BINDING_ID = "broadlink"; + public static final ThingTypeUID THING_TYPE_RM_PRO = new ThingTypeUID(BINDING_ID, "rm-pro"); + public static final ThingTypeUID THING_TYPE_RM3 = new ThingTypeUID(BINDING_ID, "rm3"); + public static final ThingTypeUID THING_TYPE_RM3Q = new ThingTypeUID(BINDING_ID, "rm3-q"); + public static final ThingTypeUID THING_TYPE_RM4_MINI = new ThingTypeUID(BINDING_ID, "rm4-mini"); + public static final ThingTypeUID THING_TYPE_RM4_PRO = new ThingTypeUID(BINDING_ID, "rm4-pro"); + public static final ThingTypeUID THING_TYPE_A1 = new ThingTypeUID(BINDING_ID, "a1"); + public static final ThingTypeUID THING_TYPE_MP1 = new ThingTypeUID(BINDING_ID, "mp1"); + public static final ThingTypeUID THING_TYPE_MP1_1K3S2U = new ThingTypeUID(BINDING_ID, "mp1-1k3s2u"); + public static final ThingTypeUID THING_TYPE_MP2 = new ThingTypeUID(BINDING_ID, "mp2"); + public static final ThingTypeUID THING_TYPE_SP1 = new ThingTypeUID(BINDING_ID, "sp1"); + public static final ThingTypeUID THING_TYPE_SP2 = new ThingTypeUID(BINDING_ID, "sp2"); + public static final ThingTypeUID THING_TYPE_SP2S = new ThingTypeUID(BINDING_ID, "sp2-s"); + public static final ThingTypeUID THING_TYPE_SP3 = new ThingTypeUID(BINDING_ID, "sp3"); + public static final ThingTypeUID THING_TYPE_SP3S = new ThingTypeUID(BINDING_ID, "sp3-s"); + + public static final String RM_PRO = "Broadlink RM pro / pro+ / plus"; + public static final String RM3 = "Broadlink RM3"; + public static final String RM3Q = "Broadlink RM3 v11057"; + public static final String RM4_MINI = "Broadlink RM4 Mini"; + public static final String RM4_PRO = "Broadlink 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 SP2-s"; + public static final String SP3 = "Broadlink SP3"; + public static final String SP3S = "Broadlink SP3-s"; + + 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 = "learning-control"; + public static final String RF_COMMAND_CHANNEL = "rf-command"; + public static final String RF_LEARNING_CONTROL_CHANNEL = "learning-rf-control"; + public static final String LEARNING_CONTROL_COMMAND_LEARN = "LEARN"; + public static final String LEARNING_CONTROL_COMMAND_CHECK = "CHECK"; + public static final String LEARNING_CONTROL_COMMAND_MODIFY = "MODIFY"; + public static final String LEARNING_CONTROL_COMMAND_DELETE = "DELETE"; + public static final String TEMPERATURE_CHANNEL = "temperature"; + public static final String HUMIDITY_CHANNEL = "humidity"; + public static final String LIGHT_CHANNEL = "light"; + public static final String AIR_CHANNEL = "air"; + public static final String NOISE_CHANNEL = "noise"; + public static final String POWER_CONSUMPTION_CHANNEL = "power-consumption"; + + public static final String COMMAND_POWER_ON = "power-on"; + public static final String COMMAND_NIGHTLIGHT = "night-light"; + public static final String COMMAND_POWER_ON_S1 = "s1power-on"; + public static final String COMMAND_POWER_ON_S2 = "s2power-on"; + public static final String COMMAND_POWER_ON_S3 = "s3power-on"; + public static final String COMMAND_POWER_ON_S4 = "s4power-on"; + public static final String COMMAND_POWER_ON_USB = "power-on-usb"; + + public static final String IR_MAP_NAME = "broadlink_ir"; + public static final String RF_MAP_NAME = "broadlink_rf"; + + /** + * Enum type to make a distinction between IR and RF codes being managed by a device + */ + public static enum CodeType { + IR, + RF + }; + + 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_RM_PRO, RM_PRO); + 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_MINI, RM4_MINI); + SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.put(THING_TYPE_RM4_PRO, RM4_PRO); + 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); + } +} 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..1a8d8fa146b4b --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkHandlerFactory.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2010-2024 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.internal.handler.BroadlinkA1Handler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkRemoteModel3Handler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkRemoteModel3V44057Handler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkRemoteModel4MiniHandler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkRemoteModel4ProHandler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkRemoteModelProHandler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkSocketModel1Handler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkSocketModel2Handler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkSocketModel3Handler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkSocketModel3SHandler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkStripModel11K3S2UHandler; +import org.openhab.binding.broadlink.internal.handler.BroadlinkStripModel1Handler; +import org.openhab.core.storage.StorageService; +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; + private final StorageService storageService; + + @Activate + public BroadlinkHandlerFactory( + final @Reference BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider, + @Reference StorageService storageService) { + this.commandDescriptionProvider = commandDescriptionProvider; + this.storageService = storageService; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return BroadlinkBindingConstants.SUPPORTED_THING_TYPES_UIDS_TO_NAME_MAP.keySet().contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + logger.debug("Creating Thing handler for '{}'", thingTypeUID.getAsString()); + + if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_RM_PRO)) { + return new BroadlinkRemoteModelProHandler(thing, commandDescriptionProvider, storageService); + } + if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_RM3)) { + return new BroadlinkRemoteModel3Handler(thing, commandDescriptionProvider, storageService); + } + if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_RM3Q)) { + return new BroadlinkRemoteModel3V44057Handler(thing, commandDescriptionProvider, storageService); + } + if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_RM4_MINI)) { + return new BroadlinkRemoteModel4MiniHandler(thing, commandDescriptionProvider, storageService); + } + if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_RM4_PRO)) { + return new BroadlinkRemoteModel4ProHandler(thing, commandDescriptionProvider, storageService); + } + if (thingTypeUID.equals(BroadlinkBindingConstants.THING_TYPE_A1)) { + 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..a6d8285abb071 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkMappingService.java @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2010-2024 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.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.CodeType; +import org.openhab.core.storage.Storage; +import org.openhab.core.storage.StorageService; +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 final Logger logger = LoggerFactory.getLogger(BroadlinkMappingService.class); + private final BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider; + private final ChannelUID irTargetChannelUID; + private final ChannelUID rfTargetChannelUID; + private final StorageService storageService; + private final Storage irStorage; + private final Storage rfStorage; + private static ArrayList mappingInstances = new ArrayList(); + + public BroadlinkMappingService(BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider, + ChannelUID irTargetChannelUID, ChannelUID rfTargetChannelUID, StorageService storageService) { + this.commandDescriptionProvider = commandDescriptionProvider; + this.irTargetChannelUID = irTargetChannelUID; + this.rfTargetChannelUID = rfTargetChannelUID; + this.storageService = storageService; + irStorage = this.storageService.getStorage(BroadlinkBindingConstants.IR_MAP_NAME, + String.class.getClassLoader()); + rfStorage = this.storageService.getStorage(BroadlinkBindingConstants.RF_MAP_NAME, + String.class.getClassLoader()); + mappingInstances.add(this); + notifyAvailableCommands(irStorage.getKeys(), CodeType.IR, false); + notifyAvailableCommands(rfStorage.getKeys(), CodeType.RF, false); + logger.debug("BroadlinkMappingService constructed on behalf of {} and {}", this.irTargetChannelUID, + this.rfTargetChannelUID); + } + + public void dispose() { + mappingInstances.remove(this); + } + + public @Nullable String lookupCode(String command, CodeType codeType) { + String response; + switch (codeType) { + case IR: + response = lookupKey(command, irStorage, codeType); + break; + case RF: + response = lookupKey(command, rfStorage, codeType); + break; + default: + response = null; + } + return response; + } + + public @Nullable String storeCode(String command, String code, CodeType codeType) { + String response; + switch (codeType) { + case IR: + response = storeKey(command, code, irStorage, codeType, irTargetChannelUID); + break; + case RF: + response = storeKey(command, code, rfStorage, codeType, rfTargetChannelUID); + break; + default: + response = null; + } + return response; + } + + public @Nullable String replaceCode(String command, String code, CodeType codeType) { + String response; + switch (codeType) { + case IR: + response = replaceKey(command, code, irStorage, codeType, irTargetChannelUID); + break; + case RF: + response = replaceKey(command, code, rfStorage, codeType, rfTargetChannelUID); + break; + default: + response = null; + } + return response; + } + + public @Nullable String deleteCode(String command, CodeType codeType) { + String response; + switch (codeType) { + case IR: + response = deleteKey(command, irStorage, codeType, irTargetChannelUID); + break; + case RF: + response = deleteKey(command, rfStorage, codeType, rfTargetChannelUID); + break; + default: + return null; + } + return response; + } + + public @Nullable String lookupKey(String command, Storage storage, CodeType codeType) { + String value = storage.get(command); + if (value != null) { + logger.debug("{} Command label found. Key value pair is {},{}", codeType, command, value); + } else { + logger.debug("{} Command label not found.", codeType); + } + return value; + } + + public @Nullable String storeKey(String command, String code, Storage storage, CodeType codeType, + ChannelUID targetChannelUID) { + if (storage.get(command) == null) { + logger.debug("{} Command label not found. Proceeding to store key value pair {},{} and reload Command list", + codeType, command, code); + storage.put(command, code); + notifyAvailableCommands(storage.getKeys(), codeType, true); + return command; + } else { + logger.debug("{} Command label {} found. This is not a replace operation. Skipping", codeType, command); + return null; + } + } + + public @Nullable String replaceKey(String command, String code, Storage storage, CodeType codeType, + ChannelUID targetChannelUID) { + if (storage.get(command) != null) { + logger.debug("{} Command label found. Proceeding to store key value pair {},{} and reload Command list", + codeType, command, code); + storage.put(command, code); + notifyAvailableCommands(storage.getKeys(), codeType, true); + return command; + } else { + logger.debug("{} Command label {} not found. This is not an add method. Skipping", codeType, command); + return null; + } + } + + public @Nullable String deleteKey(String command, Storage storage, CodeType codeType, + ChannelUID targetChannelUID) { + String value = storage.get(command); + if (value != null) { + logger.debug("{} Command label found. Proceeding to remove key pair {},{} and reload command list", + codeType, command, value); + storage.remove(command); + notifyAvailableCommands(storage.getKeys(), codeType, true); + return command; + } else { + logger.debug("{} Command label {} not found. Can't delete a command that does not exist", codeType, + command); + return null; + } + } + + void notifyAvailableCommands(Collection commandNames, CodeType codeType, boolean refreshAllInstances) { + List commandOptions = new ArrayList<>(); + commandNames.forEach((c) -> commandOptions.add(new CommandOption(c, null))); + if (refreshAllInstances) { + logger.debug("notifying framework about {} commands: {} - All instances", commandOptions.size(), + commandNames.toString()); + for (BroadlinkMappingService w : mappingInstances) { + switch (codeType) { + case IR: + w.commandDescriptionProvider.setCommandOptions(w.irTargetChannelUID, commandOptions); + case RF: + w.commandDescriptionProvider.setCommandOptions(w.rfTargetChannelUID, commandOptions); + } + } + } else { + logger.debug("notifying framework about {} commands: {} for single {} device", commandOptions.size(), + commandNames.toString(), codeType); + switch (codeType) { + case IR: + this.commandDescriptionProvider.setCommandOptions(irTargetChannelUID, commandOptions); + case RF: + this.commandDescriptionProvider.setCommandOptions(rfTargetChannelUID, 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..2e22d83f2cbab --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkProtocol.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2010-2024 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[5]; + packet[0x2b] = mac[4]; + packet[0x2c] = mac[3]; + packet[0x2d] = mac[2]; + packet[0x2e] = mac[1]; + packet[0x2f] = mac[0]; + 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.getMessage()); + 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[0x13] = 0x31; + payload[0x14] = 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 = packet[0x22] != 0 || 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..ee32103c6fa84 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/BroadlinkRemoteDynamicCommandDescriptionProvider.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.events.EventPublisher; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +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 EventPublisher eventPublisher, + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService, + final @Reference ThingRegistry thingRegistry) { + this.eventPublisher = eventPublisher; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + 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..6f6577dde6091 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/ModelMapper.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2010-2024 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.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 0x27DC: + 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_RM_PRO; + case 0x5F36: + case 0x6507: + case 0x6508: + return BroadlinkBindingConstants.THING_TYPE_RM3Q; + case 0x51DA: + case 0x5209: + case 0x520C: + case 0x520D: + case 0x5211: + case 0x5212: + case 0x5216: + case 0x6070: + case 0x610E: + case 0x610F: + case 0x62BC: + case 0x62BE: + case 0x6364: + case 0x648D: + case 0x6539: + case 0x653A: + return BroadlinkBindingConstants.THING_TYPE_RM4_MINI; + case 0x520B: + case 0x5213: + case 0x5218: + case 0x6026: + case 0x6184: + case 0x61A2: + case 0x649B: + case 0x653C: + return BroadlinkBindingConstants.THING_TYPE_RM4_PRO; + 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); + 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(T[] values, byte b) { + int index = Byte.toUnsignedInt(b); + return index < values.length ? new StringType(values[index].toString()) : UNKNOWN; + } + + private enum AirValue { + PERFECT, + GOOD, + NORMAL, + BAD + } + + public static StringType getAirValue(byte b) { + return lookup(AirValue.values(), b); + } + + private enum LightValues { + DARK, + DIM, + NORMAL, + BRIGHT + } + + public static StringType getLightValue(byte b) { + return lookup(LightValues.values(), b); + } + + private enum NoiseValues { + QUIET, + NORMAL, + NOISY, + EXTREME + } + + public static StringType getNoiseValue(byte b) { + return lookup(NoiseValues.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..d52780c9c8627 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/NetworkUtils.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2010-2024 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.Inet4Address; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.net.NetUtil; + +/** + * Utilities for working with the local network. + * + * @author John Marshall/Cato Sognen - Initial contribution + */ +@NonNullByDefault +public class NetworkUtils { + /** + * Finds an InetAddress that is associated to a non-loopback device. + * + * @return null, if there is no non-loopback device address, otherwise the InetAddress associated to a non-loopback + * device + * @throws SocketException thrown when no socket can be opened. + */ + public static @Nullable InetAddress findNonLoopbackAddress() throws SocketException { + for (InetAddress address : NetUtil.getAllInterfaceAddresses().stream() + .filter(a -> a.getAddress() instanceof Inet4Address).map(a -> a.getAddress()).toList()) { + if (address.isSiteLocalAddress()) { + return address; + } + } + return null; + } + + /** + * Find the address of the local lan host + * + * @return InetAddress of the local lan host + * @throws UnknownHostException if no local lan address can be found + */ + 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; + } + } + + /** + * Randomly find a free port on the host in the defined range + * + * @param host The address of the host to find a free port on + * @param from port number of the start of the range + * @param to port number of the end of the range + * @return number of the available port + * @throws TimeoutException when no available port can be found in 30 seconds + */ + public static int nextFreePort(InetAddress host, int from, int to) throws TimeoutException { + if (to < from) { + throw new IllegalArgumentException("To value is smaller than from value."); + } + int port = randInt(from, to); + long startTime = System.currentTimeMillis(); + do { + if (isLocalPortFree(host, port)) { + return port; + } + port = ThreadLocalRandom.current().nextInt(from, to); + if (System.currentTimeMillis() - startTime > 30000) { + throw new TimeoutException("Cannot find an available port in the specified range"); + } + } while (true); + } + + /** + * Test whether the port is available on the host + * + * @param host the host to check the port of + * @param port the port to check for availability + * @return true when available, otherwise false + */ + public static boolean isLocalPortFree(InetAddress host, int port) { + try { + (new ServerSocket(port, 50, host)).close(); + } catch (IOException e) { + return false; + } + return true; + } + + /** + * Return a random integer in the range (min, max) + * + * @param min the lower limit of the range + * @param max the upper limit of the range + * @return the random integer + */ + public static int randInt(int min, int max) { + return ThreadLocalRandom.current().nextInt((max - min) + 1) + min; + } +} 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..2e7c5c232f1b8 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/Utils.java @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2010-2024 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 { + /** + * Checks whether the status of the thing is online + * + * @param thing + * @return true if thing status is online, false otherwise + */ + public static boolean isOnline(Thing thing) { + return thing.getStatus().equals(ThingStatus.ONLINE); + } + + /** + * Checks whether the status of the thing is offline + * + * @param thing + * @return true if thing status is offline, false otherwise + */ + public static boolean isOffline(Thing thing) { + return thing.getStatus().equals(ThingStatus.OFFLINE); + } + + /** + * Slice the source array using the range(from, to) + * + * @param source the byte[] array to slice + * @param from the starting point + * @param to the end point + * @return the sliced part of the byte array + * @throws IllegalArgumentException if the slice is not possible + */ + 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; + } + } + + /** + * Pad the source byte[] based on the quotient + * + * @param source the byte[] to pad + * @param quotient the quotient / part to pad + * @return the padded byte[] + */ + 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; + } + + /** + * Convert the source byte[] tp a hex string + * + * @param source the byte[] to convert + * @return a string with a hex representation of the source + */ + 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(); + } + + /** + * Encrypt the dat[] using the key[] with the ivSpec AES algorithm parameter + * + * @param key the key to use for the AES encryption + * @param ivSpec the parameter for the AES encryption + * @param data the byte[] to encrypt + * @return the encrypted data[] + * @throws IOException if the encryption has an issue + */ + 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); + } + } + + /** + * Decrypt the data byte[] using the supplied key and AES algorithm parameter ivSpec + * + * @param key the key to use for the AES decrypt + * @param ivSpec the AES algorithm parameter to use + * @param data the data to decrypt + * @return the decrypted byte[] + * @throws IOException + */ + 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/config/BroadlinkDeviceConfiguration.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/config/BroadlinkDeviceConfiguration.java new file mode 100644 index 0000000000000..76e02315507ac --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/config/BroadlinkDeviceConfiguration.java @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2010-2024 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.config; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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 = true; + private int port; + private String macAddress = ""; + private byte[] macAddressBytes = new byte[0]; + private int pollingInterval = 30; + private String nameOfCommandToLearn = "DEVICE_ON"; + private int deviceType; + + 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 int getDeviceType() { + return deviceType; + } + + public void setDeviceType(int newDeviceType) { + this.deviceType = newDeviceType; + } + + public String getNameOfCommandToLearn() { + return nameOfCommandToLearn; + } + + public void setNameOfCommandToLearn(String nameOfCommandToLearn) { + this.nameOfCommandToLearn = nameOfCommandToLearn; + } + + public String isValidConfiguration() { + if (ipAddress.length() == 0) { + return "Not a valid IP address"; + } + if (port == 0) { + return "Port cannot be 0"; + } + if (macAddress.isBlank()) { + return "No MAC address defined"; + } + // Regex to check valid MAC address + String regex = "^([0-9A-Fa-f]{2}[:-])" + "{5}([0-9A-Fa-f]{2})|" + "([0-9a-fA-F]{4}\\." + "[0-9a-fA-F]{4}\\." + + "[0-9a-fA-F]{4})$"; + + // Compile the ReGex + Pattern p = Pattern.compile(regex); + // Find match between given string and regular expression using Pattern.matcher() + Matcher m = p.matcher(macAddress); + // Return if the string matched the regular expression + if (!m.matches()) { + return "MAC address is not of the form XX:XX:XX:XX:XX:XX"; + } + if (pollingInterval == 0) { + return "Polling interval cannot be 0"; + } + if (nameOfCommandToLearn.isBlank()) { + return "Name of command to learn needs to be defined"; + } + if (deviceType == 0) { + return "Device type cannot be 0"; + } + + return ""; + } + + @Override + 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(", nameOfCommandToLearn=") + .append(nameOfCommandToLearn).append("]").toString(); + } +} 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..9c9283043758f --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/BroadlinkDiscoveryService.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2024 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.internal.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"); + } + + @Override + public void startScan() { + foundCount = 0; + DiscoveryProtocol.beginAsync(this, 10000L, this, logger); + } + + @Override + protected synchronized void stopScan() { + super.stopScan(); + removeOlderResults(getTimestampOfLastScan()); + } + + @Override + 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.warn("Discovered a {} but do not know how to support it at this time, please report!", 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); + } + + @Override + public void onDiscoveryFinished() { + logger.info("Discovery complete. Found {} Broadlink devices", foundCount); + } +} 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..4cb3c00a1bafc --- /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-2024 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.internal.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.debug("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..e30cebc9e86d4 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DeviceRediscoveryListener.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 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 { + /** + * Discovered a device on the supplied ip address * + * + * @param newIpAddress + */ + void onDeviceRediscovered(String newIpAddress); + + /** + * Method triggered when device discovery fails + */ + 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..5231b8a69ca67 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryFinishedListener.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2024 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 { + /** + * Method triggered when discovery is finished. + */ + 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..d4841f47ab3b4 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/discovery/DiscoveryProtocol.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2024 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 java.util.concurrent.TimeoutException; + +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; + } + + @Override + public void run() { + BroadlinkSocket.registerListener(listener, logger); + DiscoveryProtocol.discoverDevices(logger); + DiscoveryProtocol.waitUntilEnded(timeoutMillis, logger); + BroadlinkSocket.unregisterListener(listener, logger); + finishedListener.onDiscoveryFinished(); + } + } + + public static void beginAsync(BroadlinkSocketListener listener, long discoveryTimeoutMillis, + DiscoveryFinishedListener discoveryFinishedListener, Logger logger) { + 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.getMessage()); + } catch (IllegalArgumentException e) { + logger.warn("Failed to find free port: {}", e.getMessage()); + } catch (TimeoutException e) { + logger.warn("Cannot find a port to discovber new devices"); + } + } + + private static void waitUntilEnded(long discoveryTimeoutMillis, Logger logger) { + try { + Thread.sleep(discoveryTimeoutMillis); + } catch (InterruptedException e) { + logger.warn("Unexpected problem during discovery: {}", e.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkA1Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkA1Handler.java new file mode 100644 index 0000000000000..9f035fd0dcaf0 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkA1Handler.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants; +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); + } + + @Override + protected void getStatusFromDevice() throws IOException, BroadlinkException { + byte payload[]; + payload = new byte[16]; + payload[0] = 1; + + byte message[] = buildMessage((byte) 0x6a, payload); + + byte[] response = sendAndReceiveDatagram(message, "A1 device status"); + if (response == null) { + throw new BroadlinkStatusException("No status response received."); + } + byte decryptResponse[] = decodeDevicePacket(response); + double temperature = ((decryptResponse[4] * 10 + decryptResponse[5]) / 10D); + logger.trace("A1 getStatusFromDevice got temperature {}", temperature); + + updateTemperature(temperature); + updateHumidity((decryptResponse[6] * 10 + decryptResponse[7]) / 10D); + updateState(BroadlinkBindingConstants.LIGHT_CHANNEL, ModelMapper.getLightValue(decryptResponse[8])); + updateState(BroadlinkBindingConstants.AIR_CHANNEL, ModelMapper.getAirValue(decryptResponse[10])); + updateState(BroadlinkBindingConstants.NOISE_CHANNEL, ModelMapper.getNoiseValue(decryptResponse[12])); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkAuthenticationException.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkAuthenticationException.java new file mode 100644 index 0000000000000..56a251aab17b8 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkAuthenticationException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception to handle authentication issues. + * + * @author Anton Jansen - Initial contribution + */ +@NonNullByDefault +public class BroadlinkAuthenticationException extends BroadlinkException { + + private static final long serialVersionUID = 6332210773192650617L; + + public BroadlinkAuthenticationException(String message) { + super(message); + } + + public BroadlinkAuthenticationException(String message, Exception e) { + super(message, e); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkBaseThingHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkBaseThingHandler.java new file mode 100644 index 0000000000000..d363d9a70c13c --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkBaseThingHandler.java @@ -0,0 +1,294 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import static org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.*; + +import java.io.IOException; +import java.net.InetAddress; +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.internal.BroadlinkBindingConstants; +import org.openhab.binding.broadlink.internal.BroadlinkProtocol; +import org.openhab.binding.broadlink.internal.Utils; +import org.openhab.binding.broadlink.internal.config.BroadlinkDeviceConfiguration; +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); + this.deviceId = HexUtils.hexToBytes(INITIAL_DEVICE_ID); + this.deviceKey = HexUtils.hexToBytes(BroadlinkBindingConstants.BROADLINK_AUTH_KEY); + } + + /** + * Method to set the socket manually for test purposes + * + * @param socket the socket to use + */ + void setSocket(RetryableSocket socket) { + this.socket = socket; + } + + /** + * Method to define a network traffic observer, who can react to the traffic being received. + * + * @param networkTrafficObserver + */ + void setNetworkTrafficObserver(NetworkTrafficObserver networkTrafficObserver) { + this.networkTrafficObserver = networkTrafficObserver; + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + this.thingConfig = getConfigAs(BroadlinkDeviceConfiguration.class); + // Validate whether the configuration makes any sense + if (thingConfig.isValidConfiguration().isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + thingConfig.isValidConfiguration()); + return; + } + count = (new Random()).nextInt(65535); + + if (this.socket == null) { + this.socket = new RetryableSocket(thingConfig, logger); + } + + if (thingConfig.getPollingInterval() != 0) { + refreshHandle = scheduler.scheduleWithFixedDelay(this::updateItemStatus, 1L, + thingConfig.getPollingInterval(), TimeUnit.SECONDS); + } + } + + @Override + public void dispose() { + ScheduledFuture refreshHandle = this.refreshHandle; + if (refreshHandle != null) { + refreshHandle.cancel(true); + this.refreshHandle = null; + } + RetryableSocket socket = this.socket; + if (socket != null) { + socket.close(); + this.socket = null; + } + super.dispose(); + } + + private void authenticate() throws BroadlinkAuthenticationException { + 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) { + throw new BroadlinkAuthenticationException( + "response from device during authentication was null, check if correct mac address is used for device."); + } + 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; + } catch (Exception e) { + throw new BroadlinkAuthenticationException("Authentication failed:" + e.getMessage(), e); + } + } + + protected byte @Nullable [] sendAndReceiveDatagram(byte message[], String purpose) { + RetryableSocket socket = this.socket; + if (socket != null) { + return socket.sendAndReceive(message, purpose); + } else { + return null; + } + } + + protected byte[] buildMessage(byte command, byte payload[]) throws IOException { + return buildMessage(command, payload, thingConfig.getDeviceType()); + } + + private byte[] buildMessage(byte command, byte payload[], int deviceType) throws IOException { + count = count + 1 & 0xffff; + + NetworkTrafficObserver networkTrafficObserver = this.networkTrafficObserver; + + 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); + } + + protected byte[] decodeDevicePacket(byte[] responseBytes) throws IOException { + byte[] rxBytes = BroadlinkProtocol.decodePacket(responseBytes, this.deviceKey, + BroadlinkBindingConstants.BROADLINK_IV); + + NetworkTrafficObserver networkTrafficObserver = this.networkTrafficObserver; + if (networkTrafficObserver != null) { + networkTrafficObserver.onBytesReceived(rxBytes); + } + return rxBytes; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateItemStatus(); + } + } + + // Can be implemented by devices that should do something on being found; e.g. perform a first status query + // protected void onBroadlinkDeviceBecomingReachable() { + // updateItemStatus(); + // } + + // 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 void getStatusFromDevice() throws IOException, BroadlinkException { + InetAddress address = InetAddress.getByName(thingConfig.getIpAddress()); + if (!address.isReachable(3000)) { + throw new BroadlinkHostNotReachableException("Cannot reach " + thingConfig.getIpAddress()); + } + } + + public void updateItemStatus() { + if ((thingConfig.getIpAddress().length() == 0) && (thingConfig.getMacAddressAsString().length() == 0)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Neither an IP address or MAC address has been defined."); + } else { + int tries = 0; + while (tries < 4) { + try { + // Check if we need to authenticate + if (!authenticated) { + authenticate(); + } + // Normal operation ... + getStatusFromDevice(); + updateStatus(ThingStatus.ONLINE); + return; + } catch (BroadlinkHostNotReachableException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + if (!thingConfig.isStaticIp()) { + 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..."); + } + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Cannot establish a communication channel with the device: " + e.getMessage()); + } catch (BroadlinkAuthenticationException e) { + logger.debug("Authentication exception: {}", e.getMessage()); + forceOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Couldn't authenticate: " + e.getMessage()); + } catch (BroadlinkException e) { + logger.warn("Received unexpected exception: {}", e.getClass().getCanonicalName()); + } + authenticated = false; + tries += 1; + } + } + } + + @Override + public void onDeviceRediscovered(String newIpAddress) { + logger.debug("Rediscovered this device at IP {}", newIpAddress); + thingConfig.setIpAddress(newIpAddress); + updateItemStatus(); + } + + @Override + public void onDeviceRediscoveryFailure() { + if (!Utils.isOffline(getThing())) { + forceOffline(ThingStatusDetail.NONE, + "Couldn't rediscover dynamically-IP-addressedv device after network scan"); + } + } + + 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); + RetryableSocket socket = this.socket; + if (socket != null) { + socket.close(); + } + } + + protected void updateTemperature(double temperature) { + updateState(TEMPERATURE_CHANNEL, new QuantityType<>(temperature, BROADLINK_TEMPERATURE_UNIT)); + } + + protected void updateHumidity(double humidity) { + updateState(HUMIDITY_CHANNEL, new QuantityType<>(humidity, BROADLINK_HUMIDITY_UNIT)); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkException.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkException.java new file mode 100644 index 0000000000000..dd491787c91e6 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * General exception class of the Broadlink binding + * + * @author Anton Jansen - Initial contribution + */ +@NonNullByDefault +public class BroadlinkException extends Exception { + + private static final long serialVersionUID = 1L; + + public BroadlinkException(String message) { + super(message); + } + + public BroadlinkException(String message, Exception e) { + super(message, e); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkHostNotReachableException.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkHostNotReachableException.java new file mode 100644 index 0000000000000..2a2b7df62fabd --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkHostNotReachableException.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown when the broadlink device is not reachable. + * + * @author Anton Jansen - Initial contribution + */ +@NonNullByDefault +public class BroadlinkHostNotReachableException extends BroadlinkException { + + private static final long serialVersionUID = 1L; + + public BroadlinkHostNotReachableException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteHandler.java new file mode 100644 index 0000000000000..e0a512962b77b --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteHandler.java @@ -0,0 +1,486 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.ProtocolException; +import java.util.HexFormat; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants; +import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.CodeType; +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.library.types.StringType; +import org.openhab.core.storage.StorageService; +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; + + // IR commands + public static final byte COMMAND_BYTE_ENTER_LEARNING = 0x03; + public static final byte COMMAND_BYTE_CHECK_LEARNT_DATA = 0x04; + + // RF commands + public static final byte COMMAND_BYTE_ENTER_RF_FREQ_LEARNING = 0x19; // Sweep frequency + public static final byte COMMAND_BYTE_CHECK_RF_FREQ_LEARNING = 0x1A; // Check frequency + public static final byte COMMAND_BYTE_EXIT_RF_FREQ_LEARNING = 0x1E; // Cancel sweep frequency + public static final byte COMMAND_BYTE_FIND_RF_PACKET = 0x1B; // Find RF packet + public static final byte COMMAND_BYTE_CHECK_RF_DATA = 0x4; // Check data + + private final BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider; + private final StorageService storageService; + protected @Nullable BroadlinkMappingService mappingService; + + public BroadlinkRemoteHandler(Thing thing, + BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider, + StorageService storageService) { + super(thing); + this.commandDescriptionProvider = commandDescriptionProvider; + this.storageService = storageService; + } + + @Override + public void initialize() { + super.initialize(); + this.mappingService = new BroadlinkMappingService(commandDescriptionProvider, + new ChannelUID(thing.getUID(), BroadlinkBindingConstants.COMMAND_CHANNEL), + new ChannelUID(thing.getUID(), BroadlinkBindingConstants.RF_COMMAND_CHANNEL), this.storageService); + } + + @Override + public void dispose() { + BroadlinkMappingService mappingService = this.mappingService; + if (mappingService != null) { + mappingService.dispose(); + this.mappingService = null; + } + 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) { + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType("Error found during when entering IR learning mode")); + logger.warn("Exception while sending command", e); + } + + return null; + } + + protected void sendCode(byte[] code) { + logger.debug("Sending code: {}", Utils.toHexString(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 handleIRCommand(String irCommand, boolean replacement) { + try { + String message = ""; + if (replacement) { + message = "Modifying "; + } else { + message = "Adding "; + } + + BroadlinkMappingService mappingService = this.mappingService; + if (mappingService == null) { + logger.warn("Mapping service is null, this should not happen"); + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, new StringType("NULL")); + return; + } + + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType(message + irCommand + "...")); + + 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"); + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, new StringType("NULL")); + } else { + String hexString = Utils.toHexString(extractResponsePayload(response)); + String cmdLabel = null; + if (replacement) { + cmdLabel = mappingService.replaceCode(irCommand, hexString, CodeType.IR); + message = "modified"; + } else { + cmdLabel = mappingService.storeCode(irCommand, hexString, CodeType.IR); + message = "saved"; + } + if (cmdLabel != null) { + logger.info("Learnt code '{}' ", hexString); + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType("IR command " + irCommand + " " + message)); + } else { + if (replacement) { + logger.debug("Command label not previously stored. Skipping"); + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType("IR command " + irCommand + " does not exist")); + } else { + logger.debug("Command label previously stored. Skipping"); + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType("IR command " + irCommand + " already exists")); + } + } + } + } catch (IOException e) { + logger.warn("Exception while attempting to check learnt code: {}", e.getMessage()); + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, new StringType("NULL")); + } + } + + @SuppressWarnings("null") + private void deleteIRCommand(String irCommand) { + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType(BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_DELETE)); + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType("Deleting IR command " + irCommand + "...")); + String cmdLabel = mappingService.deleteCode(irCommand, CodeType.IR); + if (cmdLabel != null) { + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType("IR command " + irCommand + " deleted")); + } else { + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType("IR command " + irCommand + " not found")); + } + } + + void handleLearningCommand(String learningCommand) { + logger.trace("Sending learning-channel command {}", learningCommand); + switch (learningCommand) { + case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_LEARN: { + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, + new StringType(BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_LEARN)); + sendCommand(COMMAND_BYTE_ENTER_LEARNING, "enter remote code learning mode"); + break; + } + case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_CHECK: { + handleIRCommand(thingConfig.getNameOfCommandToLearn(), false); + break; + } + case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_MODIFY: { + handleIRCommand(thingConfig.getNameOfCommandToLearn(), true); + break; + } + case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_DELETE: { + deleteIRCommand(thingConfig.getNameOfCommandToLearn()); + break; + } + default: { + logger.warn("Unrecognised learning channel command: {}", learningCommand); + } + } + } + + @Override + 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; + } + + ChannelTypeUID channelTypeUID = extractChannelType(channelUID, command); + if (channelTypeUID == null) { + return; + } + + switch (channelTypeUID.getId()) { + case BroadlinkBindingConstants.COMMAND_CHANNEL: { + byte code[] = lookupIRCode(command, channelUID); + if (code != null) { + sendCode(code); + } else { + logger.warn("Cannot find the data to send out for command {}", command.toString()); + } + break; + } + case BroadlinkBindingConstants.RF_COMMAND_CHANNEL: { + byte code[] = lookupRFCode(command, channelUID); + if (code != null) { + sendCode(code); + } else { + logger.warn("Cannot find the data to send out for command {}", command.toString()); + } + break; + } + case BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL: { + handleLearningCommand(command.toString()); + break; + } + case BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL: { + switch (command.toString()) { + case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_LEARN: { + handleFindRFFrequencies(); + break; + } + case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_CHECK: { + handleFindRFCommand(false); + break; + } + case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_MODIFY: { + handleFindRFCommand(true); + break; + } + case BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_DELETE: { + deleteRFCommand(); + break; + } + default: { + logger.debug("Thing {} has unknown channel type '{}'", getThing().getLabel(), + channelTypeUID.getId()); + break; + } + } + } + default: + logger.debug("Thing {} has unknown channel type '{}'", getThing().getLabel(), channelTypeUID.getId()); + break; + } + } + + protected @Nullable ChannelTypeUID extractChannelType(ChannelUID channelUID, Command command) { + Channel channel = thing.getChannel(channelUID.getId()); + if (channel == null) { + logger.warn("Unexpected null channel while handling command {}", command.toFullString()); + return null; + } + ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID == null) { + logger.warn("Unexpected null channelTypeUID while handling command {}", command.toFullString()); + return null; + } + return channelTypeUID; + } + + private byte @Nullable [] lookupIRCode(Command command, ChannelUID channelUID) { + byte code[] = null; + BroadlinkMappingService mappingService = this.mappingService; + if (mappingService == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Mapping service not defined."); + return null; + } + + String value = mappingService.lookupCode(command.toString(), CodeType.IR); + + 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; + } + + private byte @Nullable [] lookupRFCode(Command command, ChannelUID channelUID) { + byte code[] = null; + + BroadlinkMappingService mappingService = this.mappingService; + if (mappingService == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Mapping service not defined."); + return null; + } + + String value = mappingService.lookupCode(command.toString(), CodeType.RF); + + if (value == null || value.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "No entries found for command " + command + " in RF map file, or the file is missing."); + return null; + } + + code = HexUtils.hexToBytes(value); + + logger.debug("Transformed command '{}' for thing {}", command, getThing().getLabel()); + return code; + } + + private void handleFindRFFrequencies() { + // Let the user know we are processing his / her command + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, + new StringType(BroadlinkBindingConstants.LEARNING_CONTROL_COMMAND_LEARN)); + updateState(BroadlinkBindingConstants.LEARNING_CONTROL_CHANNEL, new StringType("Learning new RF code...")); + sendCommand(COMMAND_BYTE_ENTER_RF_FREQ_LEARNING, "Enter remote rf frequency learning mode"); + boolean freqFound = false; + + long timeout = System.currentTimeMillis() + 30 * 1000; + HexFormat hex = HexFormat.of(); + + try { + while ((System.currentTimeMillis() < timeout) && (freqFound)) { + TimeUnit.MILLISECONDS.sleep(500); + logger.trace("Checking rf frequency"); + byte[] resp = (sendCommand(COMMAND_BYTE_CHECK_RF_FREQ_LEARNING, "check rf frequency")); + if (resp != null) { + resp = extractResponsePayload(resp); + logger.trace("Response: {}", hex.formatHex(resp)); + if (resp[0] == 1) { + freqFound = true; + logger.trace("Freq found!"); + } + } + } + } catch (IOException | InterruptedException e) { + logger.warn("RF learning unexpected interrupted:{}", e.getMessage()); + freqFound = false; + } + + if (freqFound) { + sendCommand(COMMAND_BYTE_EXIT_RF_FREQ_LEARNING, "exit remote rf frequency learning mode"); + logger.info("No RF frequency found."); + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, new StringType("NULL")); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Cannot locate the appropriate RF frequency."); + return; + } + + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, new StringType("RF command learnt")); + } + + private void handleFindRFCommand(boolean replacement) { + String statusInfo = (replacement) ? "Replacing" : "Adding"; + statusInfo = statusInfo + " RF command " + thingConfig.getNameOfCommandToLearn() + ".."; + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, new StringType(statusInfo)); + + BroadlinkMappingService mappingService = this.mappingService; + if (mappingService == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Mapping service not defined."); + return; + } + + sendCommand(COMMAND_BYTE_FIND_RF_PACKET, "find the rf packet data"); + + long timeout = System.currentTimeMillis() + 30 * 1000; + + boolean dataFound = false; + + try { + byte[] response = new byte[0]; + while ((System.currentTimeMillis() < timeout) && (!dataFound)) { + TimeUnit.MILLISECONDS.sleep(500); + byte[] data = sendCommand(COMMAND_BYTE_CHECK_RF_DATA, "check the rf packet data"); + if (data != null) { + try { + response = extractResponsePayload(data); + String hexString = Utils.toHexString(response); + String cmdLabel = null; + if (replacement) { + cmdLabel = mappingService.replaceCode(thingConfig.getNameOfCommandToLearn(), hexString, + CodeType.RF); + } else { + cmdLabel = mappingService.storeCode(thingConfig.getNameOfCommandToLearn(), hexString, + CodeType.RF); + } + + if (cmdLabel != null) { + logger.info("Learnt code '{}' ", hexString); + dataFound = true; + } + } catch (ProtocolException ex) { + statusInfo = statusInfo + "."; + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, new StringType(statusInfo)); + } + } + } + } catch (IOException | InterruptedException e) { + logger.warn("Unexpected exception while checking RF packet data: {}", e.getMessage()); + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, new StringType("Unexpected error")); + } + + if (dataFound) { + if (replacement) { + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, + new StringType("RF command " + thingConfig.getNameOfCommandToLearn() + " updated")); + } else { + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, + new StringType("RF command " + thingConfig.getNameOfCommandToLearn() + " saved")); + } + } else { + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, new StringType("No data found")); + } + } + + private void deleteRFCommand() { + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, + new StringType("Deleting RF command " + thingConfig.getNameOfCommandToLearn() + "...")); + + BroadlinkMappingService mappingService = this.mappingService; + if (mappingService == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Mapping service not defined."); + return; + } + + String cmdLabel = mappingService.deleteCode(thingConfig.getNameOfCommandToLearn(), CodeType.RF); + if (cmdLabel != null) { + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, + new StringType("RF command " + thingConfig.getNameOfCommandToLearn() + " deleted")); + } else { + updateState(BroadlinkBindingConstants.RF_LEARNING_CONTROL_CHANNEL, + new StringType("RF command " + thingConfig.getNameOfCommandToLearn() + " not found")); + } + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel3Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel3Handler.java new file mode 100644 index 0000000000000..eb2d285bf92b0 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel3Handler.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider; +import org.openhab.core.storage.StorageService; +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, + StorageService storageService) { + super(thing, commandDescriptionProvider, storageService); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel3V44057Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel3V44057Handler.java new file mode 100644 index 0000000000000..57bd6fbf46179 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel3V44057Handler.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Thing; + +/** + * Supports quirks in V44057 firmware. + * + * @author Stewart Cossey - Initial contribution + */ +@NonNullByDefault +public class BroadlinkRemoteModel3V44057Handler extends BroadlinkRemoteModel4MiniHandler { + + public BroadlinkRemoteModel3V44057Handler(Thing thing, + BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider, + StorageService storageService) { + super(thing, commandDescriptionProvider, storageService); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel4MiniHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel4MiniHandler.java new file mode 100644 index 0000000000000..bf914a7291ad9 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel4MiniHandler.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2010-2024 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.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.storage.StorageService; +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 BroadlinkRemoteModel4MiniHandler extends BroadlinkRemoteHandler { + + public BroadlinkRemoteModel4MiniHandler(Thing thing, + BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider, + StorageService storageService) { + super(thing, commandDescriptionProvider, storageService); + } + + @Override + protected void getStatusFromDevice() throws BroadlinkStatusException, IOException { + // 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) { + throw new BroadlinkStatusException( + "response from RM4 device was null, did you configure the right address for the device?"); + } + byte decodedPayload[] = extractResponsePayload(response); + + // Temps and humidity get divided by 100 now, not 10 + double temperature = ((decodedPayload[0] * 100 + decodedPayload[1]) / 100D); + updateTemperature(temperature); + double humidity = ((decodedPayload[2] * 100 + decodedPayload[3]) / 100D); + updateHumidity(humidity); + } + + // 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; + if ((payloadLength + 2) > decryptedResponse.length) { + logger.warn("Received incomplete message, expected length: {}, received: {}", payloadLength + 2, + decryptedResponse.length); + payloadLength = decryptedResponse.length - 2; + } + return Utils.slice(decryptedResponse, 6, payloadLength + 2); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel4ProHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel4ProHandler.java new file mode 100644 index 0000000000000..581e2e02ec937 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel4ProHandler.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Thing; + +/** + * Extension for the RM 4 Pro + * + * @author Anton Jansen - Initial contribution + */ + +@NonNullByDefault +public class BroadlinkRemoteModel4ProHandler extends BroadlinkRemoteModel4MiniHandler { + + public BroadlinkRemoteModel4ProHandler(Thing thing, + BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider, + StorageService storageService) { + super(thing, commandDescriptionProvider, storageService); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModelProHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModelProHandler.java new file mode 100644 index 0000000000000..de3c798a10805 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModelProHandler.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.broadlink.internal.BroadlinkRemoteDynamicCommandDescriptionProvider; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Thing; + +/** + * Remote blaster handler + * + * @author John Marshall/Cato Sognen - Initial contribution + */ +@NonNullByDefault +public class BroadlinkRemoteModelProHandler extends BroadlinkRemoteHandler { + + public BroadlinkRemoteModelProHandler(Thing thing, + BroadlinkRemoteDynamicCommandDescriptionProvider commandDescriptionProvider, + StorageService storageService) { + super(thing, commandDescriptionProvider, storageService); + } + + @Override + protected void getStatusFromDevice() throws IOException, BroadlinkStatusException { + byte payload[] = new byte[16]; + payload[0] = 1; + byte message[] = buildMessage((byte) 0x6a, payload); + byte response[] = sendAndReceiveDatagram(message, "RM Pro device status"); + if (response == null) { + throw new BroadlinkStatusException( + "response from RM Pro device was null, did you configure the right address for the device?"); + } + byte decodedPayload[] = decodeDevicePacket(response); + double temperature = ((decodedPayload[4] * 10 + decodedPayload[5]) / 10D); + updateTemperature(temperature); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketHandler.java new file mode 100644 index 0000000000000..82ed413192cc8 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketHandler.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2024 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.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; + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + try { + if (channelUID.getId().equals("power-on")) { + 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.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel1Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel1Handler.java new file mode 100644 index 0000000000000..ed213afa6fea7 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel1Handler.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 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.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/internal/handler/BroadlinkSocketModel2Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel2Handler.java new file mode 100644 index 0000000000000..ee81ef011fdf8 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel2Handler.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import static org.openhab.binding.broadlink.internal.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; + } + + @Override + 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]; + return OnOffType.from((statusByte & mask) == mask); + } + + 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; + } + + @Override + 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.getMessage()); + } + } + + protected void updatePowerConsumption(double consumptionWatts) { + updateState(POWER_CONSUMPTION_CHANNEL, + new QuantityType(consumptionWatts, BROADLINK_POWER_CONSUMPTION_UNIT)); + } + + @Override + protected void getStatusFromDevice() throws IOException { + byte[] statusBytes = getStatusBytesFromDevice(); + updateState(COMMAND_POWER_ON, derivePowerStateFromStatusBytes(statusBytes)); + if (supportsPowerConsumptionMeasurement) { + updatePowerConsumption(derivePowerConsumption(statusBytes)); + } + } + + 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); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel3Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel3Handler.java new file mode 100644 index 0000000000000..d07f862c6d912 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel3Handler.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import static org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.*; + +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; + } + + @Override + protected void getStatusFromDevice() throws IOException { + logger.debug("SP3 getting status..."); + byte[] statusBytes = getStatusBytesFromDevice(); + updateState(COMMAND_POWER_ON, derivePowerStateFromStatusBytes(statusBytes)); + updateState(COMMAND_NIGHTLIGHT, deriveNightLightStateFromStatusBytes(statusBytes)); + } + + @Override + 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/internal/handler/BroadlinkSocketModel3SHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel3SHandler.java new file mode 100644 index 0000000000000..bda6fcc45a6d9 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel3SHandler.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2024 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.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 = (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/internal/handler/BroadlinkStatusException.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStatusException.java new file mode 100644 index 0000000000000..0b25377351361 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStatusException.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Anton Jansen - Initial contribution + */ +@NonNullByDefault +public class BroadlinkStatusException extends BroadlinkException { + + private static final long serialVersionUID = 1L; + + public BroadlinkStatusException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStripModel11K3S2UHandler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStripModel11K3S2UHandler.java new file mode 100644 index 0000000000000..dcd3d4290c99d --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStripModel11K3S2UHandler.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants; +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); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateItemStatus(); + return; + } + + switch (channelUID.getId()) { + case BroadlinkBindingConstants.COMMAND_POWER_ON_S1: + interpretCommandForSocket(1, command); + break; + case BroadlinkBindingConstants.COMMAND_POWER_ON_S2: + interpretCommandForSocket(2, command); + break; + case BroadlinkBindingConstants.COMMAND_POWER_ON_S3: + interpretCommandForSocket(3, command); + break; + case BroadlinkBindingConstants.COMMAND_POWER_ON_USB: + 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.getMessage()); + } + } + + 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) (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"); + } + + @Override + protected void getStatusFromDevice() throws IOException, BroadlinkStatusException { + 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; + byte message[] = buildMessage((byte) 106, payload); + byte response[] = sendAndReceiveDatagram(message, "status for MP13K2U strip"); + if (response == null) { + throw new BroadlinkStatusException( + "response from MP13K2U strip device was null, did you define the address of the device correctly?"); + } + byte decodedPayload[] = decodeDevicePacket(response); + final int status = decodedPayload[14]; + + this.updateState(BroadlinkBindingConstants.COMMAND_POWER_ON_S1, + (status & 0x01) == 0x01 ? OnOffType.ON : OnOffType.OFF); + this.updateState(BroadlinkBindingConstants.COMMAND_POWER_ON_S2, + (status & 0x02) == 0x02 ? OnOffType.ON : OnOffType.OFF); + this.updateState(BroadlinkBindingConstants.COMMAND_POWER_ON_S3, + (status & 0x04) == 0x04 ? OnOffType.ON : OnOffType.OFF); + this.updateState(BroadlinkBindingConstants.COMMAND_POWER_ON_USB, + (status & 0x08) == 0x08 ? OnOffType.ON : OnOffType.OFF); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStripModel1Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStripModel1Handler.java new file mode 100644 index 0000000000000..4bdb3255a9e08 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStripModel1Handler.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants; +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); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + updateItemStatus(); + return; + } + + switch (channelUID.getId()) { + case BroadlinkBindingConstants.COMMAND_POWER_ON_S1: + interpretCommandForSocket(1, command); + break; + case BroadlinkBindingConstants.COMMAND_POWER_ON_S2: + interpretCommandForSocket(2, command); + break; + case BroadlinkBindingConstants.COMMAND_POWER_ON_S3: + interpretCommandForSocket(3, command); + break; + case BroadlinkBindingConstants.COMMAND_POWER_ON_S4: + 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.getMessage()); + } + } + + 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) (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"); + } + + @Override + protected void getStatusFromDevice() throws IOException, BroadlinkStatusException { + 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; + + byte message[] = buildMessage((byte) 106, payload); + byte response[] = sendAndReceiveDatagram(message, "status for strip"); + if (response == null) { + throw new BroadlinkStatusException( + "response from strip device was null, did you setup the address of the device correctly?"); + } + byte decodedPayload[] = decodeDevicePacket(response); + final int status = decodedPayload[14]; + + this.updateState(BroadlinkBindingConstants.COMMAND_POWER_ON_S1, + (status & 1) == 1 ? OnOffType.ON : OnOffType.OFF); + this.updateState(BroadlinkBindingConstants.COMMAND_POWER_ON_S2, + (status & 2) == 2 ? OnOffType.ON : OnOffType.OFF); + this.updateState(BroadlinkBindingConstants.COMMAND_POWER_ON_S3, + (status & 4) == 4 ? OnOffType.ON : OnOffType.OFF); + this.updateState(BroadlinkBindingConstants.COMMAND_POWER_ON_S4, + (status & 8) == 8 ? OnOffType.ON : OnOffType.OFF); + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStripModel2Handler.java b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStripModel2Handler.java new file mode 100644 index 0000000000000..f3536e348d633 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/handler/BroadlinkStripModel2Handler.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2024 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.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/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..c7c9169e92427 --- /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-2024 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; + + @Override + 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 data: {}", e.getMessage()); + } + } + } + + 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..cb03c90b66603 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/BroadlinkSocketListener.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2024 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 { + + /** + * Method triggered when the defined socket receives data + * + * @param remoteAddress the remote address to receive data from + * @param remotePort the remote port to receive data from + * @param remoteMAC the remote MAC address to receive data from + * @param thingTypeUID the defined thing type id to receive data from + * @param model the device type of thing to receive data from + */ + 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..79bcba4ff923d --- /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-2024 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..f3dd618f2c09d --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/java/org/openhab/binding/broadlink/internal/socket/RetryableSocket.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2024 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.internal.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; + } + + private boolean sendDatagram(byte message[], String purpose) { + try { + DatagramSocket socket = this.socket; + if (socket == null || socket.isClosed()) { + socket = new DatagramSocket(); + socket.setBroadcast(true); + socket.setReuseAddress(true); + socket.setSoTimeout(5000); + this.socket = socket; + } + InetAddress host = InetAddress.getByName(thingConfig.getIpAddress()); + int port = thingConfig.getPort(); + DatagramPacket sendPacket = new DatagramPacket(message, message.length, new InetSocketAddress(host, port)); + socket.send(sendPacket); + return true; + } catch (IOException e) { + logger.warn("IO error during UDP command sending {}:{}", purpose, e.getMessage()); + return false; + } + } + + private @Nullable DatagramPacket receiveDatagram(String purpose, DatagramPacket receivePacket) { + DatagramSocket socket = this.socket; + try { + if (socket == null) { + logger.warn("receiveDatagram {} for socket was unexpectedly null", purpose); + } else { + socket.receive(receivePacket); + return receivePacket; + } + } catch (SocketTimeoutException ste) { + logger.debug("No further {} response received for device", purpose); + } catch (Exception e) { + logger.warn("While {} got unexpected exception: {}", purpose, e.getMessage()); + } + + return null; + } + + public void close() { + DatagramSocket socket = this.socket; + if (socket != null) { + socket.close(); + this.socket = null; + } + } +} diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..300ae035ac2ad --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,12 @@ + + + + binding + Broadlink Binding + This is the binding for Broadlink devices. It includes support for A1 multi-sensor, MP1/MP2 WiFi Smart + Power Strip, SP1/2/3 WiFi smart socket, and RM Pro/3/4 IR/RF transmitter + local + + 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..13ca45b2f57ff --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,45 @@ + + + + + + network-address + + IP address or hostname 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 + + + 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..65cfb962feebf --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/rm-config.xml @@ -0,0 +1,51 @@ + + + + + + network-address + + IP address or hostname 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 the IR or RF command to learn when using the learn Command channel + DEVICE_ON + true + + + diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/rmpro-config.xml b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/rmpro-config.xml new file mode 100644 index 0000000000000..1a6826ccecf30 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/config/rmpro-config.xml @@ -0,0 +1,51 @@ + + + + + + network-address + + IP address or hostname 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 the IR or RF command to learn when using the learn Command channel + DEVICE_ON + true + + + diff --git a/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/i18n/broadlink.properties b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/i18n/broadlink.properties new file mode 100644 index 0000000000000..7632eb9635d08 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/i18n/broadlink.properties @@ -0,0 +1,143 @@ +# add-on + +addon.broadlink.name = Broadlink Binding +addon.broadlink.description = This is the binding for Broadlink devices. It includes support for A1 multi-sensor, MP1/MP2 WiFi Smart Power Strip, SP1/2/3 WiFi smart socket, and RM Pro/3/4 IR/RF transmitter + +# thing types + +thing-type.broadlink.a1.label = Broadlink A1 +thing-type.broadlink.a1.description = The Broadlink A1 is a multi sensor that can can detect temperature, humidity, illumination, background noise and air quality. +thing-type.broadlink.mp1-1k3s2u.label = Broadlink MP1 1K3S2U +thing-type.broadlink.mp1-1k3s2u.description = The Broadlink MP1 1K3S2U Smart Home Wifi Plug Power Strip 3 AC, 2 USB Individual Wireless Remote Control. +thing-type.broadlink.mp1-1k3s2u.channel.power-on-s1.label = Socket 1 Power +thing-type.broadlink.mp1-1k3s2u.channel.power-on-s2.label = Socket 2 Power +thing-type.broadlink.mp1-1k3s2u.channel.power-on-s3.label = Socket 3 Power +thing-type.broadlink.mp1-1k3s2u.channel.power-on-usb.label = USB Power +thing-type.broadlink.mp1.label = Broadlink MP1 +thing-type.broadlink.mp1.description = The Broadlink MP1 Smart Home Wifi Plug Power Strip 4 Ports Individual Wireless Remote Control. +thing-type.broadlink.mp1.channel.power-on-s1.label = Socket 1 Power +thing-type.broadlink.mp1.channel.power-on-s2.label = Socket 2 Power +thing-type.broadlink.mp1.channel.power-on-s3.label = Socket 3 Power +thing-type.broadlink.mp1.channel.power-on-s4.label = Socket 4 Power +thing-type.broadlink.mp2.label = Broadlink MP2 +thing-type.broadlink.mp2.description = The Broadlink MP2 is a smart socket with Wi-Fi connectivity. It does not support switching individual ports. +thing-type.broadlink.rm-pro.label = Broadlink RM Pro +thing-type.broadlink.rm-pro.description = The Broadlink RM Pro is a Wi-Fi IR transmitter with a temperature sensor. +thing-type.broadlink.rm3-q.label = Broadlink RM3 v44057 +thing-type.broadlink.rm3-q.description = The Broadlink RM 3/Mini is a Wi-Fi IR transmitter running v44057 firmware. +thing-type.broadlink.rm3.label = Broadlink RM3 +thing-type.broadlink.rm3.description = The Broadlink RM 3/Mini is a Wi-Fi IR transmitter. +thing-type.broadlink.rm4-mini.label = Broadlink RM4 Mini +thing-type.broadlink.rm4-mini.description = The Broadlink RM 4 Mini is a Wi-Fi IR transmitter with temperature and humidity sensors. +thing-type.broadlink.rm4-pro.label = Broadlink RM4 Pro +thing-type.broadlink.rm4-pro.description = The Broadlink RM 4 Pro is a Wi-Fi IR and RF transmitter with temperature and humidity sensors. +thing-type.broadlink.sp1.label = Broadlink SP1 +thing-type.broadlink.sp1.description = The Broadlink SP1 is a Wi-Fi smart socket. +thing-type.broadlink.sp2-s.label = Broadlink SP2-s +thing-type.broadlink.sp2-s.description = The Broadlink SP2-s is a Wi-Fi smart socket with power consumption measurement. +thing-type.broadlink.sp2.label = Broadlink SP2 +thing-type.broadlink.sp2.description = The Broadlink SP2 is a Wi-Fi smart socket. +thing-type.broadlink.sp3-s.label = Broadlink SP3-s +thing-type.broadlink.sp3-s.description = The Broadlink SP3-s is a Wi-Fi smart socket with power consumption measurement +thing-type.broadlink.sp3.label = Broadlink SP3 +thing-type.broadlink.sp3.description = The Broadlink SP3 is a Wi-Fi smart socket with night light. The SP mini 3 omits the night light. +thing-type.broadlink.sp3.channel.night-light.label = Night Light Power + +# thing types config + +thing-type.config.broadlink.config.ipAddress.label = Network Address +thing-type.config.broadlink.config.ipAddress.description = IP address or hostname of the device. +thing-type.config.broadlink.config.macAddress.label = MAC Address +thing-type.config.broadlink.config.macAddress.description = MAC address of the device. +thing-type.config.broadlink.config.pollingInterval.label = Polling Interval +thing-type.config.broadlink.config.pollingInterval.description = The interval in seconds for polling the status of the device. +thing-type.config.broadlink.config.port.label = Network Port +thing-type.config.broadlink.config.port.description = Network port of the device. +thing-type.config.broadlink.config.staticIp.label = Static IP +thing-type.config.broadlink.config.staticIp.description = Will the device always be given this network address? +thing-type.config.broadlink.rmconfig.ipAddress.label = Network Address +thing-type.config.broadlink.rmconfig.ipAddress.description = IP address or hostname of the device. +thing-type.config.broadlink.rmconfig.macAddress.label = MAC Address +thing-type.config.broadlink.rmconfig.macAddress.description = MAC address of the device. +thing-type.config.broadlink.rmconfig.nameOfCommandToLearn.label = Name of IR/RF command to learn +thing-type.config.broadlink.rmconfig.nameOfCommandToLearn.description = Enter name of the IR or RF command to learn when using the learn Command channel +thing-type.config.broadlink.rmconfig.pollingInterval.label = Polling Interval +thing-type.config.broadlink.rmconfig.pollingInterval.description = The interval in seconds for polling the status of the device. +thing-type.config.broadlink.rmconfig.port.label = Network Port +thing-type.config.broadlink.rmconfig.port.description = Network port of the device. +thing-type.config.broadlink.rmconfig.staticIp.label = Static IP +thing-type.config.broadlink.rmconfig.staticIp.description = Will the device always be given this network address? +thing-type.config.broadlink.rmproconfig.ipAddress.label = Network Address +thing-type.config.broadlink.rmproconfig.ipAddress.description = IP address or hostname of the device. +thing-type.config.broadlink.rmproconfig.macAddress.label = MAC Address +thing-type.config.broadlink.rmproconfig.macAddress.description = MAC address of the device. +thing-type.config.broadlink.rmproconfig.nameOfCommandToLearn.label = Name of IR/RF command to learn +thing-type.config.broadlink.rmproconfig.nameOfCommandToLearn.description = Enter name of the IR or RF command to learn when using the learn Command channel +thing-type.config.broadlink.rmproconfig.pollingInterval.label = Polling Interval +thing-type.config.broadlink.rmproconfig.pollingInterval.description = The interval in seconds for polling the status of the device. +thing-type.config.broadlink.rmproconfig.port.label = Network Port +thing-type.config.broadlink.rmproconfig.port.description = Network port of the device. +thing-type.config.broadlink.rmproconfig.staticIp.label = Static IP +thing-type.config.broadlink.rmproconfig.staticIp.description = Will the device always be given this network address? + +# channel types + +channel-type.broadlink.air.label = Air Quality +channel-type.broadlink.air.description = Air quality conditions +channel-type.broadlink.command.label = IR Command +channel-type.broadlink.command.description = The IR command sent to the device +channel-type.broadlink.learning-control.label = Remote IR Learning Control +channel-type.broadlink.learning-control.description = Instructs the Broadlink device to perform tasks related to IR code learning +channel-type.broadlink.learning-control.state.option.LEARN = Learn IR command +channel-type.broadlink.learning-control.state.option.CHECK = Check and save IR command +channel-type.broadlink.learning-control.state.option.MODIFY = Modify a previously created IR command +channel-type.broadlink.learning-control.state.option.DELETE = Delete IR command +channel-type.broadlink.learning-rf-control.label = Remote RF Learning Control +channel-type.broadlink.learning-rf-control.description = Instructs the Broadlink device to perform tasks related to RF code learning +channel-type.broadlink.learning-rf-control.state.option.LEARN = Learn RF command +channel-type.broadlink.learning-rf-control.state.option.CHECK = Check and save RF command +channel-type.broadlink.learning-rf-control.state.option.MODIFY = Modify a previously created RF command +channel-type.broadlink.learning-rf-control.state.option.DELETE = Delete RF command +channel-type.broadlink.light.label = Illumination +channel-type.broadlink.light.description = Light conditions +channel-type.broadlink.night-light.label = Night Light +channel-type.broadlink.noise.label = Background Noise +channel-type.broadlink.noise.description = Noise conditions +channel-type.broadlink.rf-command.label = RF Command +channel-type.broadlink.rf-command.description = The RF command sent to the device + +# thing types + +thing-type.broadlink.rm3q.label = Broadlink RM3 v44057 +thing-type.broadlink.rm3q.description = The Broadlink RM 3/Mini is a Wi-Fi IR transmitter running v44057 firmware. +thing-type.broadlink.rm4mini.label = Broadlink RM4 Mini +thing-type.broadlink.rm4mini.description = The Broadlink RM 4 Mini is a Wi-Fi IR transmitter with temperature and humidity sensors. +thing-type.broadlink.rm4pro.label = Broadlink RM4 Pro +thing-type.broadlink.rm4pro.description = The Broadlink RM 4 Pro is a Wi-Fi IR and RF transmitter with temperature and humidity sensors. +thing-type.broadlink.sp2s.label = Broadlink SP2s +thing-type.broadlink.sp2s.description = The Broadlink SP2s is a Wi-Fi smart socket with power consumption measurement. +thing-type.broadlink.sp3s.label = Broadlink SP3s +thing-type.broadlink.sp3s.description = The Broadlink SP3s is a Wi-Fi smart socket with power consumption measurement + +# thing types + +thing-type.broadlink.mp1.channel.s1power-on.label = Socket 1 Power +thing-type.broadlink.mp1.channel.s2power-on.label = Socket 2 Power +thing-type.broadlink.mp1.channel.s3power-on.label = Socket 3 Power +thing-type.broadlink.mp1.channel.s4power-on.label = Socket 4 Power + +# thing types config + +thing-type.config.broadlink.config.ignoreFailedUpdates.label = Ignore Failed Updates +thing-type.config.broadlink.config.ignoreFailedUpdates.description = Should failed status requests force the device offline? +thing-type.config.broadlink.rmconfig.ignoreFailedUpdates.label = Ignore Failed Updates +thing-type.config.broadlink.rmconfig.ignoreFailedUpdates.description = Should failed status requests force the device offline? +thing-type.config.broadlink.rmconfig.mapFilename.label = Map File +thing-type.config.broadlink.rmconfig.mapFilename.description = Enter name of file containing mapping of commands to IR codes + +# channel types + +channel-type.broadlink.humidity.label = Humidity +channel-type.broadlink.power-consumption.label = Power Consumption +channel-type.broadlink.power-on.label = Socket Power +channel-type.broadlink.temperature.label = Temperature 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..d9e1622859ac2 --- /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..e2c4e33943839 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/channels.xml @@ -0,0 +1,68 @@ + + + + + String + + The IR command sent to the device + Text + + + String + + Instructs the Broadlink device to perform tasks related to IR code learning + + + + + + + + + + + String + + The RF command sent to the device + Text + + + String + + Instructs the Broadlink device to perform tasks related to RF code learning + + + + + + + + + + + Switch + + Lightbulb + + + 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..1c03f28f77c46 --- /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 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..67370d701ba46 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/main/resources/OH-INF/thing/rm-types.xml @@ -0,0 +1,66 @@ + + + + + + The Broadlink RM Pro is a Wi-Fi IR 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 Mini is a Wi-Fi IR transmitter with temperature and humidity sensors. + + + + + + + macAddress + + + + + The Broadlink RM 4 Pro is a Wi-Fi IR and RF 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..36d23dbf648e9 --- /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 SP2-s 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 SP3-s is a Wi-Fi smart socket with power consumption measurement + + + + + macAddress + + + 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..a9a5e1fde8ab1 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/AbstractBroadlinkTest.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2024 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 static org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.openhab.core.storage.Storage; +import org.openhab.core.test.storage.VolatileStorageService; + +/** + * 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 VolatileStorageService storageService = new VolatileStorageService(); + protected static Storage irStorage = storageService.getStorage( + org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.IR_MAP_NAME, + String.class.getClassLoader()); + protected static Storage rfStorage = storageService.getStorage( + org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.RF_MAP_NAME, + String.class.getClassLoader()); + + @BeforeEach + public void setUp() throws Exception { + for (String s : irStorage.getKeys()) { + irStorage.remove(s); + } + for (String s : rfStorage.getKeys()) { + rfStorage.remove(s); + } + irStorage.put("IR_TEST_COMMAND_ON", "00112233"); + irStorage.put("IR_TEST_COMMAND_OFF", "33221100"); + rfStorage.put("RF_TEST_COMMAND_ON", "00112233"); + rfStorage.put("RF_TEST_COMMAND_OFF", "33221100"); + } + + @BeforeAll + public static void beforeClass() { + } + + @AfterAll + public static void afterClass() { + } +} 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..030b050e846a1 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/BroadlinkMappingServiceTest.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2024 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 org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.openhab.binding.broadlink.AbstractBroadlinkTest; +import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.CodeType; +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 ChannelUID TEST_CHANNEL_UID = new ChannelUID("bsm:test:channel:uid"); + private static final ChannelUID TEST_CHANNEL_UID2 = new ChannelUID("bsm:test:channel:uid2"); + + private BroadlinkRemoteDynamicCommandDescriptionProvider mockProvider = Mockito + .mock(BroadlinkRemoteDynamicCommandDescriptionProvider.class); + + @Test + public void canReadFromAMapFile() { + BroadlinkMappingService bms = new BroadlinkMappingService(mockProvider, TEST_CHANNEL_UID, TEST_CHANNEL_UID2, + storageService); + + assertEquals("00112233", bms.lookupCode("IR_TEST_COMMAND_ON", CodeType.IR)); + assertEquals("33221100", bms.lookupCode("IR_TEST_COMMAND_OFF", CodeType.IR)); + assertEquals(null, bms.lookupCode("IR_TEST_COMMAND_DUMMY", CodeType.IR)); + assertEquals("00112233", bms.lookupCode("RF_TEST_COMMAND_ON", CodeType.RF)); + assertEquals("33221100", bms.lookupCode("RF_TEST_COMMAND_OFF", CodeType.RF)); + assertEquals(null, bms.lookupCode("RF_TEST_COMMAND_DUMMY", CodeType.RF)); + } + + @Test + public void canStoreOnAMapFile() { + BroadlinkMappingService bms = new BroadlinkMappingService(mockProvider, TEST_CHANNEL_UID, TEST_CHANNEL_UID2, + storageService); + + assertEquals("IR_TEST_COMMAND_UP", bms.storeCode("IR_TEST_COMMAND_UP", "44556677", CodeType.IR)); + assertEquals(null, bms.storeCode("IR_TEST_COMMAND_ON", "77665544", CodeType.IR)); + assertEquals("44556677", irStorage.get("IR_TEST_COMMAND_UP")); + assertEquals("RF_TEST_COMMAND_UP", bms.storeCode("RF_TEST_COMMAND_UP", "44556677", CodeType.RF)); + assertEquals(null, bms.storeCode("RF_TEST_COMMAND_ON", "77665544", CodeType.RF)); + assertEquals("44556677", rfStorage.get("RF_TEST_COMMAND_UP")); + } + + @Test + public void canReplaceOnAMapFile() { + BroadlinkMappingService bms = new BroadlinkMappingService(mockProvider, TEST_CHANNEL_UID, TEST_CHANNEL_UID2, + storageService); + + assertEquals(null, bms.replaceCode("IR_TEST_COMMAND_UP", "55667788", CodeType.IR)); + assertEquals("IR_TEST_COMMAND_ON", bms.replaceCode("IR_TEST_COMMAND_ON", "55667788", CodeType.IR)); + assertEquals("55667788", irStorage.get("IR_TEST_COMMAND_ON")); + assertEquals(null, bms.replaceCode("RF_TEST_COMMAND_UP", "55667788", CodeType.RF)); + assertEquals("RF_TEST_COMMAND_ON", bms.replaceCode("RF_TEST_COMMAND_ON", "55667788", CodeType.RF)); + assertEquals("55667788", rfStorage.get("RF_TEST_COMMAND_ON")); + } + + @Test + public void canDeleteFromAMapFile() { + BroadlinkMappingService bms = new BroadlinkMappingService(mockProvider, TEST_CHANNEL_UID, TEST_CHANNEL_UID2, + storageService); + + assertEquals("IR_TEST_COMMAND_ON", bms.deleteCode("IR_TEST_COMMAND_ON", CodeType.IR)); + assertEquals(null, irStorage.get("IR_TEST_COMMAND_ON")); + assertEquals(null, bms.deleteCode("IR_TEST_COMMAND_DUMMY", CodeType.IR)); + assertEquals("RF_TEST_COMMAND_ON", bms.deleteCode("RF_TEST_COMMAND_ON", CodeType.RF)); + assertEquals(null, rfStorage.get("RF_TEST_COMMAND_ON")); + assertEquals(null, bms.deleteCode("RF_TEST_COMMAND_DUMMY", CodeType.RF)); + } + + @Test + public void notifiesTheFrameworkOfTheAvailableCommands() { + new BroadlinkMappingService(mockProvider, TEST_CHANNEL_UID, TEST_CHANNEL_UID2, storageService); + + ArrayList expected = new ArrayList<>(); + ArrayList expected2 = new ArrayList<>(); + expected.add(new CommandOption("IR_TEST_COMMAND_ON", null)); + expected.add(new CommandOption("IR_TEST_COMMAND_OFF", null)); + expected2.add(new CommandOption("RF_TEST_COMMAND_ON", null)); + expected2.add(new CommandOption("RF_TEST_COMMAND_OFF", null)); + verify(mockProvider).setCommandOptions(TEST_CHANNEL_UID, expected); + verify(mockProvider).setCommandOptions(TEST_CHANNEL_UID2, expected2); + } +} 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..8ba70c1796190 --- /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-2024 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 { // NOPMD + + 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..ea43d6289e81c --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/ModelMapperTest.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2024 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.slf4j.Logger; + +/** + * Tests that each Thing Type maps to the right model number. + * + * @author John Marshall/Cato Sognen - Initial contribution + */ +@NonNullByDefault +public class ModelMapperTest { // NOPMD + + 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_MINI, ModelMapper.getThingType(0x51da, mockLogger)); + } + + @Test + public void mapsRm4ProAsRm4() { + assertEquals(BroadlinkBindingConstants.THING_TYPE_RM4_PRO, ModelMapper.getThingType(0x61a2, mockLogger)); + } + + @Test + public void mapsRm462bcAsRm4() { + assertEquals(BroadlinkBindingConstants.THING_TYPE_RM4_MINI, ModelMapper.getThingType(0x62bc, mockLogger)); + } + + @Test + public void mapsRm4Model6026AsRm4() { + assertEquals(BroadlinkBindingConstants.THING_TYPE_RM4_PRO, ModelMapper.getThingType(0x6026, mockLogger)); + } + + @Test + public void mapsRm4Model24846AsRm4() { + assertEquals(BroadlinkBindingConstants.THING_TYPE_RM4_MINI, 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..ac66fe4efae0b --- /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-2024 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/java/org/openhab/binding/broadlink/internal/handler/AbstractBroadlinkThingHandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/AbstractBroadlinkThingHandlerTest.java new file mode 100644 index 0000000000000..3d99ad6a99df4 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/AbstractBroadlinkThingHandlerTest.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import static org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.*; + +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.internal.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/internal/handler/BroadlinkRemoteModel3HandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel3HandlerTest.java new file mode 100644 index 0000000000000..cbb60ca4ceedd --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel3HandlerTest.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2010-2024 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.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.internal.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.internal.BroadlinkBindingConstants; +import org.openhab.core.test.storage.VolatileStorageService; + +/** + * 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 { + VolatileStorageService storageService = new VolatileStorageService(); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class); + ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class); + BroadlinkRemoteHandler model3 = new BroadlinkRemoteModel3Handler(thing, commandDescriptionProvider, + storageService); + 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/internal/handler/BroadlinkRemoteModel4HandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel4HandlerTest.java new file mode 100644 index 0000000000000..ac4867fdbb057 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModel4HandlerTest.java @@ -0,0 +1,255 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; +import static org.openhab.binding.broadlink.internal.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.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.test.storage.VolatileStorageService; +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, }; + + @Override + @BeforeEach + public void setUp() throws Exception { + configureUnderlyingThing(BroadlinkBindingConstants.THING_TYPE_RM4_MINI, "rm4-test"); + MockitoAnnotations.openMocks(this).close(); + Mockito.when(mockSocket.sendAndReceive(ArgumentMatchers.any(byte[].class), ArgumentMatchers.anyString())) + .thenReturn(response); + } + + @Test + public void sendsExpectedBytesWhenGettingDeviceStatus() { + VolatileStorageService storageService = new VolatileStorageService(); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class); + ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class); + BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4MiniHandler(thing, commandDescriptionProvider, + storageService); + setMocksForTesting(model4); + reset(trafficObserver); + try { + model4.getStatusFromDevice(); + } catch (IOException | BroadlinkException e) { + fail("Unexpected exception: " + e.getClass().getCanonicalName()); + } + + 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 { + VolatileStorageService storageService = new VolatileStorageService(); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class); + ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class); + BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4MiniHandler(thing, commandDescriptionProvider, + storageService); + 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, 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 { + VolatileStorageService storageService = new VolatileStorageService(); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class); + ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class); + BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4MiniHandler(thing, commandDescriptionProvider, + storageService); + 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, 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() { + VolatileStorageService storageService = new VolatileStorageService(); + BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4MiniHandler(thing, commandDescriptionProvider, + storageService); + setMocksForTesting(model4); + reset(mockCallback); + + try { + model4.getStatusFromDevice(); + } catch (IOException | BroadlinkException e) { + fail("Unexpected exception: " + e.getClass().getCanonicalName()); + } + + 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(), TEMPERATURE_CHANNEL); + 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(), HUMIDITY_CHANNEL); + 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 { + VolatileStorageService storageService = new VolatileStorageService(); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class); + ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class); + BroadlinkRemoteHandler model4 = new BroadlinkRemoteModel4MiniHandler(thing, commandDescriptionProvider, + storageService); + 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/internal/handler/BroadlinkRemoteModelProHandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModelProHandlerTest.java new file mode 100644 index 0000000000000..49804bc1e769a --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkRemoteModelProHandlerTest.java @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +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.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.openhab.binding.broadlink.internal.BroadlinkBindingConstants; +import org.openhab.core.test.storage.VolatileStorageService; + +/** + * Tests the Remote Model Pro handler. + * + * @author John Marshall - Initial contribution + */ +@NonNullByDefault +public class BroadlinkRemoteModelProHandlerTest extends AbstractBroadlinkThingHandlerTest { + + @Override + @BeforeEach + public void setUp() throws Exception { + configureUnderlyingThing(BroadlinkBindingConstants.THING_TYPE_RM_PRO, "rm_pro-test"); + MockitoAnnotations.openMocks(this).close(); + Mockito.when(mockSocket.sendAndReceive(ArgumentMatchers.any(byte[].class), ArgumentMatchers.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() { + VolatileStorageService storageService = new VolatileStorageService(); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class); + ArgumentCaptor byteArrayCaptor = ArgumentCaptor.forClass(byte[].class); + BroadlinkRemoteHandler model2 = new BroadlinkRemoteModelProHandler(thing, commandDescriptionProvider, + storageService); + setMocksForTesting(model2); + reset(trafficObserver); + try { + model2.getStatusFromDevice(); + } catch (IOException | BroadlinkException e) { + fail("Unexpected exception: " + e.getClass().getCanonicalName()); + } + + 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 { + VolatileStorageService storageService = new VolatileStorageService(); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class); + ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class); + BroadlinkRemoteHandler model2 = new BroadlinkRemoteModelProHandler(thing, commandDescriptionProvider, + storageService); + 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 { + VolatileStorageService storageService = new VolatileStorageService(); + ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(Byte.class); + ArgumentCaptor byteCaptor = ArgumentCaptor.forClass(byte[].class); + BroadlinkRemoteHandler modelPro = new BroadlinkRemoteModelProHandler(thing, commandDescriptionProvider, + storageService); + setMocksForTesting(modelPro); + // 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); + modelPro.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/internal/handler/BroadlinkSocketModel2HandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel2HandlerTest.java new file mode 100644 index 0000000000000..bc14c2c5ebfb4 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel2HandlerTest.java @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; +import static org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.*; + +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.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.openhab.binding.broadlink.internal.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, }; + + @Override + @BeforeEach + public void setUp() throws Exception { + configureUnderlyingThing(BroadlinkBindingConstants.THING_TYPE_SP2, "sp2-test"); + MockitoAnnotations.openMocks(this).close(); + Mockito.when(mockSocket.sendAndReceive(ArgumentMatchers.any(byte[].class), ArgumentMatchers.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(ArgumentMatchers.any(byte[].class), ArgumentMatchers.anyString())) + .thenReturn(response); + BroadlinkSocketHandler model2 = new BroadlinkSocketModel2Handler(thing, false); + setMocksForTesting(model2); + + reset(mockCallback); + + try { + model2.getStatusFromDevice(); + } catch (IOException | BroadlinkException e) { + fail("Unexpected exception: " + e.getClass().getCanonicalName()); + } + + 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(ArgumentMatchers.any(byte[].class), ArgumentMatchers.anyString())) + .thenReturn(responseMessage); + BroadlinkSocketHandler model2s = new BroadlinkSocketModel2Handler(thing, true); + setMocksForTesting(model2s); + + reset(mockCallback); + + try { + model2s.getStatusFromDevice(); + } catch (IOException | BroadlinkException e) { + fail("Unexpected exception: " + e.getClass().getCanonicalName()); + } + + 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(), POWER_CONSUMPTION_CHANNEL); + 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/internal/handler/BroadlinkSocketModel3HandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel3HandlerTest.java new file mode 100644 index 0000000000000..025eb94e49a24 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel3HandlerTest.java @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; +import static org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.*; +import static org.openhab.binding.broadlink.internal.handler.BroadlinkSocketModel3Handler.mergeOnOffBits; + +import java.io.IOException; +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.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.openhab.binding.broadlink.internal.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); + } + + @Override + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this).close(); + Mockito.when(mockSocket.sendAndReceive(ArgumentMatchers.any(byte[].class), ArgumentMatchers.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(ArgumentMatchers.any(byte[].class), ArgumentMatchers.anyString())) + .thenReturn(response); + setMocksForTesting(model3); + + reset(mockCallback); + + try { + model3.getStatusFromDevice(); + } catch (IOException e) { + fail("Unexpected exception: " + e.getClass().getCanonicalName()); + } + + 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/internal/handler/BroadlinkSocketModel3SHandlerTest.java b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel3SHandlerTest.java new file mode 100644 index 0000000000000..b54a7ab000394 --- /dev/null +++ b/bundles/org.openhab.binding.broadlink/src/test/java/org/openhab/binding/broadlink/internal/handler/BroadlinkSocketModel3SHandlerTest.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2024 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.handler; + +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; +import static org.openhab.binding.broadlink.internal.BroadlinkBindingConstants.*; + +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.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.openhab.binding.broadlink.internal.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); + } + + @Override + @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(ArgumentMatchers.any(byte[].class), ArgumentMatchers.anyString())) + .thenReturn(responseMessage); + setMocksForTesting(model3s); + + reset(mockCallback); + + try { + model3s.getStatusFromDevice(); + } catch (IOException e) { + fail("Unexpected exception: " + e.getClass().getCanonicalName()); + } + + 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(), POWER_CONSUMPTION_CHANNEL); + 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/pom.xml b/bundles/pom.xml index f8a7c1b0e3caa..b89c5cc8ea193 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -91,6 +91,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