diff --git a/README.md b/README.md index 366178a..b26c187 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,78 @@ # broadlink-java-api [![Build Status](https://travis-ci.org/mob41/broadlink-java-api.svg?branch=master)](https://travis-ci.org/mob41/broadlink-java-api) [![CodeFactor](https://www.codefactor.io/repository/github/mob41/broadlink-java-api/badge)](https://www.codefactor.io/repository/github/mob41/broadlink-java-api) [![codecov](https://codecov.io/gh/mob41/broadlink-java-api/branch/master/graph/badge.svg)](https://codecov.io/gh/mob41/broadlink-java-api) [![Maven Central](https://img.shields.io/maven-central/v/com.github.mob41.blapi/broadlink-java-api.svg)](http://central.maven.org/maven2/com/github/mob41/blapi/broadlink-java-api) + A clean Java API to Broadlink devices! This is a Java version of [mjg59](https://github.com/mjg59)'s [python-broadlink](https://github.com/mjg59/python-broadlink) library. + + +**yeeuyz**: + +This is a patched version of [a1aw](https://github.com/a1aw)‘s [broadlink-java-api](https://github.com/a1aw/broadlink-java-api) library. + +Add support of broadlink Smart Plug 4 series. + +```java + public static final short DEV_SP4L_CN = 0x7568; // added, tested + public static final short DEV_SPMINI_3 = 0x7d11; // added, tested + public static final short DEV_SP4M_JP = 0x756B; // added + public static final short DEV_SP4M = 0x756C; // added + public static final short DEV_MCB1 = 0x756F; // added + public static final short DEV_SP4L_EU = 0x7579; // added + public static final short DEV_SP4L_AU = 0x757B; // added + public static final short DEV_SPMINI_3_2 = 0x7583; // added + public static final short DEV_SP4L_UK = 0x7587; // added + + // The following models are not supported because their lengths exceed + // 'short', and I don't want to easily modify the variable types in the + // subsequent code. Making changes lightly may cause bugs on devices + // such as SP1 and SP2, which I do not own and cannot test. + public static final int DEV_WS4 = 0xA4F9; + public static final int DEV_SP4L_UK_2 = 0xA569; + public static final int DEV_MCB1_2 = 0xA56A; + public static final int DEV_SCB1E = 0xA56B; + public static final int DEV_SP4L_EU_2 = 0xA56C; + public static final int DEV_SP4L_AU_2 = 0xA576; + public static final int DEV_SP4L_UK_3 = 0xA589; + public static final int DEV_SP4L_EU_3 = 0xA5D3; + public static final int DEV_SP4D_US = 0xA6F4; +``` + ## Adding this API This API is distributed via **Maven Central**. You can import via adding this as dependency in Maven ```pom.xml``` or clone/download this as ZIP and import in IDE. ->[![Maven Central](https://img.shields.io/maven-central/v/com.github.mob41.blapi/broadlink-java-api.svg)](http://central.maven.org/maven2/com/github/mob41/blapi/broadlink-java-api) [![Maven Snapshot](https://img.shields.io/maven-metadata/v/http/oss.sonatype.org/content/repositories/snapshots/com/github/mob41/blapi/broadlink-java-api/maven-metadata.xml.svg?maxAge=2592000&label=maven%20snapshot)](https://oss.sonatype.org/content/repositories/snapshots/com/github/mob41/blapi/broadlink-java-api/) [![Maven Latest](https://img.shields.io/maven-metadata/v/http/oss.sonatype.org/content/groups/public/com/github/mob41/blapi/broadlink-java-api/maven-metadata.xml.svg?maxAge=2592000&label=maven%20latest)](https://oss.sonatype.org/content/groups/public/com/github/mob41/blapi/broadlink-java-api/) +> [![Maven Central](https://img.shields.io/maven-central/v/com.github.mob41.blapi/broadlink-java-api.svg)](http://central.maven.org/maven2/com/github/mob41/blapi/broadlink-java-api) [![Maven Snapshot](https://img.shields.io/maven-metadata/v/http/oss.sonatype.org/content/repositories/snapshots/com/github/mob41/blapi/broadlink-java-api/maven-metadata.xml.svg?maxAge=2592000&label=maven%20snapshot)](https://oss.sonatype.org/content/repositories/snapshots/com/github/mob41/blapi/broadlink-java-api/) [![Maven Latest](https://img.shields.io/maven-metadata/v/http/oss.sonatype.org/content/groups/public/com/github/mob41/blapi/broadlink-java-api/maven-metadata.xml.svg?maxAge=2592000&label=maven%20latest)](https://oss.sonatype.org/content/groups/public/com/github/mob41/blapi/broadlink-java-api/) 1. **Via Maven Central**: (Release Builds) Add the following to your ```pom.xml``` under `````` + + ```xml + + com.github.mob41.blapi + broadlink-java-api + 1.0.1 + + ``` - ```xml - - com.github.mob41.blapi - broadlink-java-api - 1.0.1 - - ``` - 2. **Via OSSRH Snapshots**: (Development Builds) To access snapshots/development builds e.g. ```1.0.1-SNAPSHOT```, you have to add the OSSRH snapshot repository + + ```xml + + + allow-snapshots + true + + + snapshots-repo + https://oss.sonatype.org/content/repositories/snapshots + false + true + + + + + ``` - ```xml - - - allow-snapshots - true - - - snapshots-repo - https://oss.sonatype.org/content/repositories/snapshots - false - true - - - - - ``` - 3. **Via Cloning**: (Eclipse) Clone the project via ```git clone https://github.com/mob41/broadlink-java-api.git``` or via the [download ZIP](https://github.com/mob41/broadlink-java-api/archive/master.zip) and extract the ZIP to a folder. And add the project into your Eclipse IDE by right clicking the ```Package Explorer```, and, @@ -53,72 +88,146 @@ This API is distributed via **Maven Central**. You can import via adding this as broadlink-java-api 1.0.1 - ``` + ``` -## Tutorial +## Tutorial-SP4 1. Import necessary libraries + + ```java + import com.github.mob41.blapi.BLDevice; + import com.github.mob41.blapi.SP4Device; + import com.github.mob41.blapi.mac.Mac; + ``` - ```java - import com.github.mob41.blapi.BLDevice; - import com.github.mob41.blapi.RM2Device; //Necessary if using 2.ii - import com.github.mob41.blapi.mac.Mac; //Necessary if using 2.ii - ``` +2. Creating/Discovering ```BLDevice``` instances by two methods: + + ```java + // + // === Method 1. By Discovering Devices In Local Network === + // + + BLDevice[] devs = BLDevice.discoverDevices(); //Default with 10000 ms (10 sec) timeout, search for multiple devices + + //BLDevice[] devs = BLDevice.discoverDevices(0); //No timeout will block the thread and search for one device only + //BLDevice[] devs = BLDevice.discoverDevices(5000); //With 5000 ms (5 sec) timeout + + //The BLDevice[] array stores the found devices in the local network + + System.out.println("Number of devices: " + devs.length); + + BLDevice blDevice = null; + for (BLDevice dev : devs){ + System.out.println("Type: " + Integer.toHexString(dev.getDeviceType()) + " Host: " + dev.getHost() + " Mac: " + dev.getMac()); + } + + //BLDevice dev = devs[0] + + // + // === Method 2. Create a "RM2Device" or another "BLDevice" child according to your device type === + // + + BLDevice dev = new RM2Device("192.168.1.123", new Mac("01:12:23:34:43:320")); + //~do stuff + //dev.auth(); + ``` + +3. Before any commands like ```getTemp()``` and ```enterLearning()```, ```BLDevice.auth()``` must be ran to connect and authenticate with the Broadlink device. + + ```java + boolean success = dev.auth(); + System.out.println("Auth status: " + (success ? "Success!" : "Failed!")); + ``` + +4. Every BLDevice has its very own methods. Please refer to their own source code in the repository (as the main documentation still not completed...). Here's an example: + + ```java + if(dev instanceof SP4Device) + { + SP4Device spd = (SP4Device) dev; + + // Obtain remote signaling + Map status= spd.getState(); + + // Note: Since the keys in the messages returned by the SP are + // enclosed in double quotes, make sure to add double quotes to + // the key names when retrieving their values. + System.out.println("getState pwr: " + status.get("\"pwr\"")); + System.out.println("getState ntlight: " + status.get("\"ntlight\"")); + System.out.println("getState indicator: " + status.get("\"indicator\"")); + System.out.println("getState ntlbrightness: " + status.get("\"ntlbrightness\"")); + System.out.println("getState maxworktime: " + status.get("\"maxworktime\"")); + System.out.println("getState childlock: " + status.get("\"childlock\"")); + + // Execute remote control + spd.setState(false, false, true, 100, 0, false); + } + ``` + +## Tutorial + +1. Import necessary libraries + + ```java + import com.github.mob41.blapi.BLDevice; + import com.github.mob41.blapi.RM2Device; //Necessary if using 2.ii + import com.github.mob41.blapi.mac.Mac; //Necessary if using 2.ii + ``` 2. Creating/Discovering ```BLDevice``` instances by two methods: - - ```java - // - // === Method 1. By Discovering Devices In Local Network === - // - - BLDevice[] devs = BLDevice.discoverDevices(); //Default with 10000 ms (10 sec) timeout, search for multiple devices - - //BLDevice[] devs = BLDevice.discoverDevices(0); //No timeout will block the thread and search for one device only - //BLDevice[] devs = BLDevice.discoverDevices(5000); //With 5000 ms (5 sec) timeout - - //The BLDevice[] array stores the found devices in the local network - - System.out.println("Number of devices: " + devs.length); - - BLDevice blDevice = null; - for (BLDevice dev : devs){ - System.out.println("Type: " + Integer.toHexString(dev.getDeviceType()) + " Host: " + dev.getHost() + " Mac: " + dev.getMac()); - } - - //BLDevice dev = devs[0] - - // - // === Method 2. Create a "RM2Device" or another "BLDevice" child according to your device type === - // - - BLDevice dev = new RM2Device("192.168.1.123", new Mac("01:12:23:34:43:320")); - //~do stuff - //dev.auth(); - ``` + ```java + // + // === Method 1. By Discovering Devices In Local Network === + // + + BLDevice[] devs = BLDevice.discoverDevices(); //Default with 10000 ms (10 sec) timeout, search for multiple devices + + //BLDevice[] devs = BLDevice.discoverDevices(0); //No timeout will block the thread and search for one device only + //BLDevice[] devs = BLDevice.discoverDevices(5000); //With 5000 ms (5 sec) timeout + + //The BLDevice[] array stores the found devices in the local network + + System.out.println("Number of devices: " + devs.length); + + BLDevice blDevice = null; + for (BLDevice dev : devs){ + System.out.println("Type: " + Integer.toHexString(dev.getDeviceType()) + " Host: " + dev.getHost() + " Mac: " + dev.getMac()); + } + + //BLDevice dev = devs[0] + + // + // === Method 2. Create a "RM2Device" or another "BLDevice" child according to your device type === + // + + BLDevice dev = new RM2Device("192.168.1.123", new Mac("01:12:23:34:43:320")); + //~do stuff + //dev.auth(); + ``` + 3. Before any commands like ```getTemp()``` and ```enterLearning()```, ```BLDevice.auth()``` must be ran to connect and authenticate with the Broadlink device. + + ```java + boolean success = dev.auth(); + System.out.println("Auth status: " + (success ? "Success!" : "Failed!")); + ``` - ```java - boolean success = dev.auth(); - System.out.println("Auth status: " + (success ? "Success!" : "Failed!")); - ``` - -3. Every BLDevice has its very own methods. Please refer to their own source code in the repository (as the main documentation still not completed...). Here's an example: - - ```java - if (dev instanceof RM2Device){ - RM2Device rm2 = (RM2Device) dev; - - boolean success = rm2.enterLearning(); - System.out.println("Enter Learning status: " + (success ? "Success!" : "Failed!")); - - float temp = rm2.getTemp(); - System.out.println("Current temperature reported from RM2: " + temp + " degrees"); - } else { - System.out.println("The \"dev\" is not a RM2Device instance."); - } - ``` +4. Every BLDevice has its very own methods. Please refer to their own source code in the repository (as the main documentation still not completed...). Here's an example: + + ```java + if (dev instanceof RM2Device){ + RM2Device rm2 = (RM2Device) dev; + + boolean success = rm2.enterLearning(); + System.out.println("Enter Learning status: " + (success ? "Success!" : "Failed!")); + + float temp = rm2.getTemp(); + System.out.println("Current temperature reported from RM2: " + temp + " degrees"); + } else { + System.out.println("The \"dev\" is not a RM2Device instance."); + } + ``` ## License diff --git a/src/main/java/com/github/mob41/blapi/BLDevice.java b/src/main/java/com/github/mob41/blapi/BLDevice.java index da60a5f..6719de1 100644 --- a/src/main/java/com/github/mob41/blapi/BLDevice.java +++ b/src/main/java/com/github/mob41/blapi/BLDevice.java @@ -57,7 +57,8 @@ * @author Anthony * */ -public abstract class BLDevice implements Closeable { +public abstract class BLDevice implements Closeable +{ /** * The specific logger for this class @@ -104,10 +105,32 @@ public abstract class BLDevice implements Closeable { public static final short DEV_SPMINI_PLUS = 0x2736; + // 2026.01.09 + public static final short DEV_SP4L_CN = 0x7568; + public static final short DEV_SPMINI_3 = 0x7d11; + public static final short DEV_SP4M_JP = 0x756B; + public static final short DEV_SP4M = 0x756C; + public static final short DEV_MCB1 = 0x756F; + public static final short DEV_SP4L_EU = 0x7579; + public static final short DEV_SP4L_AU = 0x757B; + public static final short DEV_SPMINI_3_2 = 0x7583; + public static final short DEV_SP4L_UK = 0x7587; + //public static final int DEV_WS4 = 0xA4F9; + //public static final int DEV_SP4L_UK_2 = 0xA569; + //public static final int DEV_MCB1_2 = 0xA56A; + //public static final int DEV_SCB1E = 0xA56B; + //public static final int DEV_SP4L_EU_2 = 0xA56C; + //public static final int DEV_SP4L_AU_2 = 0xA576; + //public static final int DEV_SP4L_UK_3 = 0xA589; + //public static final int DEV_SP4L_EU_3 = 0xA5D3; + //public static final int DEV_SP4D_US = 0xA6F4; + public static final short DEV_RM_2 = 0x2712; public static final short DEV_RM_MINI = 0x2737; + public static final short DEV_RM_MINI_OEM_1 = 0xffffffde; // 2026.01.09 + public static final short DEV_RM_MINI_3 = 0x27c2; public static final short DEV_RM_PRO_PHICOMM = 0x273d; @@ -148,6 +171,8 @@ public abstract class BLDevice implements Closeable { public static final String DESC_RM_MINI = "RM Mini"; + public static final String DESC_RM_MINI_OEM_1 = "RM Mini 0xffffffde"; // 2026.01.09 + public static final String DESC_RM_MINI_3 = "RM Mini 3"; public static final String DESC_RM_PRO_PHICOMM = "RM Pro"; @@ -198,6 +223,26 @@ public abstract class BLDevice implements Closeable { public static final String DESC_SPMINI_PLUS = "Smart Plug Mini Plus"; + // 2026.01.09 + public static final String DESC_SP4L_CN = "Smart Plug V4L-CN"; + public static final String DESC_SPMINI3 = "Smart Plug Mini 3"; + public static final String DESC_SP4M_JP = "Smart Plug 4M JP"; + public static final String DESC_SP4M = "Smart Plug 4M"; + public static final String DESC_MCB1 = "Smart Plug MCB1"; + public static final String DESC_SP4L_EU = "Smart Plug 4L EU"; + public static final String DESC_SP4L_AU = "Smart Plug 4L AU"; + public static final String DESC_SPMINI_3_2 = "Smart Plug Mini 3"; + public static final String DESC_SP4L_UK = "Smart Plug 4L Uk"; + //public static final String DESC_WS4 = "Smart Plug WS4"; + //public static final String DESC_SP4L_UK_2 = "Smart Plug 4L UK"; + //public static final String DESC_MCB1_2 = "Smart Plug MCB1"; + //public static final String DESC_SCB1E = "Smart Plug SCB1E"; + //public static final String DESC_SP4L_EU_2 ="Smart Plug 4L EU"; + //public static final String DESC_SP4L_AU_2 = "Smart Plug 4L AU"; + //public static final String DESC_SP4L_UK_3 = "Smart Plug 4L Uk"; + //public static final String DESC_SP4L_EU_3 = "Smart Plug 4L EU"; + //public static final String DESC_SP4D_US = "Smart Plug 4D US"; + public static final String DESC_HYSEN = "Hysen Thermostat"; public static final String DESC_FLOUREON = "Floureon Thermostat"; @@ -294,7 +339,8 @@ public abstract class BLDevice implements Closeable { * @throws IOException * Problems on constructing a datagram socket */ - protected BLDevice(short deviceType, String deviceDesc, String host, Mac mac) throws IOException { + protected BLDevice(short deviceType, String deviceDesc, String host, Mac mac) throws IOException + { key = INITIAL_KEY; iv = INITIAL_IV; id = new byte[] { 0, 0, 0, 0 }; @@ -366,7 +412,8 @@ public String getDeviceDescription() { * @return Boolean whether this method is success or not * @throws IOException If I/O goes wrong */ - public boolean auth() throws IOException { + public boolean auth() throws IOException + { return auth(false); } @@ -378,9 +425,11 @@ public boolean auth() throws IOException { * @throws IOException * If I/O goes wrong */ - public boolean auth(boolean reauth) throws IOException { + public boolean auth(boolean reauth) throws IOException + { log.debug("auth Authentication method starts"); - if(alreadyAuthorized && !reauth) { + if(alreadyAuthorized && !reauth) + { log.debug("auth Already Authorized."); return true; } @@ -547,43 +596,58 @@ public DatagramPacket sendCmdPkt(InetAddress sourceIpAddr, int sourcePort, int t * @throws IOException * Problems when constucting a datagram socket */ - public static BLDevice createInstance(short deviceType, String host, Mac mac) throws IOException { + public static BLDevice createInstance(short deviceType, String host, Mac mac) throws IOException + { String desc = BLDevice.getDescOfType(deviceType); - switch (deviceType) { - case DEV_SP1: - return new SP1Device(host, mac); - case DEV_SP2: - case DEV_SP2_HONEYWELL_ALT1: - case DEV_SP2_HONEYWELL_ALT2: - case DEV_SP2_HONEYWELL_ALT3: - case DEV_SP2_HONEYWELL_ALT4: - case DEV_SPMINI: - case DEV_SP3: - case DEV_SPMINI2: - case DEV_SPMINI_OEM_ALT1: - case DEV_SPMINI_OEM_ALT2: - case DEV_SPMINI_PLUS: - return new SP2Device(deviceType, desc, host, mac); - case DEV_RM_2: - case DEV_RM_MINI: - case DEV_RM_MINI_3: - return new RM2Device(deviceType, desc, host, mac); - case DEV_RM_PRO_PHICOMM: - case DEV_RM_2_HOME_PLUS: - case DEV_RM_2_2HOME_PLUS_GDT: - case DEV_RM_2_PRO_PLUS: - case DEV_RM_2_PRO_PLUS_2: - case DEV_RM_2_PRO_PLUS_2_BL: - case DEV_RM_MINI_SHATE: - return new RM2Device(deviceType, desc, host, mac); - case DEV_A1: - return new A1Device(host, mac); - case DEV_MP1: - return new MP1Device(host, mac); - case DEV_FLOUREON: - return new FloureonDevice(host, mac); - case DEV_HYSEN: - return new HysenDevice(host, mac); + switch (deviceType) + { + case DEV_SP1: + return new SP1Device(host, mac); + case DEV_SP2: + case DEV_SP2_HONEYWELL_ALT1: + case DEV_SP2_HONEYWELL_ALT2: + case DEV_SP2_HONEYWELL_ALT3: + case DEV_SP2_HONEYWELL_ALT4: + case DEV_SPMINI: + case DEV_SP3: + case DEV_SPMINI2: + case DEV_SPMINI_OEM_ALT1: + case DEV_SPMINI_OEM_ALT2: + case DEV_SPMINI_PLUS: + return new SP2Device(deviceType, desc, host, mac); + // 2026.01.09 + case DEV_SPMINI_3: + case DEV_SP4L_CN: + case DEV_SP4M_JP: + case DEV_SP4M: + case DEV_MCB1: + case DEV_SP4L_EU: + case DEV_SP4L_AU: + case DEV_SPMINI_3_2: + case DEV_SP4L_UK: + return new SP4Device(deviceType, desc, host, mac); + + case DEV_RM_2: + case DEV_RM_MINI: + case DEV_RM_MINI_OEM_1: + case DEV_RM_MINI_3: + return new RM2Device(deviceType, desc, host, mac); + case DEV_RM_PRO_PHICOMM: + case DEV_RM_2_HOME_PLUS: + case DEV_RM_2_2HOME_PLUS_GDT: + case DEV_RM_2_PRO_PLUS: + case DEV_RM_2_PRO_PLUS_2: + case DEV_RM_2_PRO_PLUS_2_BL: + case DEV_RM_MINI_SHATE: + return new RM2Device(deviceType, desc, host, mac); + case DEV_A1: + return new A1Device(host, mac); + case DEV_MP1: + return new MP1Device(host, mac); + case DEV_FLOUREON: + return new FloureonDevice(host, mac); + case DEV_HYSEN: + return new HysenDevice(host, mac); } return null; } @@ -627,7 +691,8 @@ public static BLDevice[] discoverDevices(int timeout) throws IOException { * @throws IOException * Problems when discovering */ - public static BLDevice[] discoverDevices(InetAddress sourceIpAddr, int sourcePort, int timeout) throws IOException { + public static BLDevice[] discoverDevices(InetAddress sourceIpAddr, int sourcePort, int timeout) throws IOException + { boolean debug = log.isDebugEnabled(); if (debug) @@ -657,7 +722,8 @@ public static BLDevice[] discoverDevices(InetAddress sourceIpAddr, int sourcePor byte[] receBytes = new byte[DISCOVERY_RECEIVE_BUFFER_SIZE]; DatagramPacket recePacket = new DatagramPacket(receBytes, 0, receBytes.length); - if (timeout == 0) { + if (timeout == 0) + { if (debug) log.debug("No timeout was set. Blocking thread until received"); log.debug("Waiting for datagrams"); @@ -674,18 +740,20 @@ public static BLDevice[] discoverDevices(InetAddress sourceIpAddr, int sourcePor short deviceType = (short) (receBytes[0x34] | receBytes[0x35] << 8); if (debug) - log.debug("Info: host=" + host + " mac=" + mac.getMacString() + " deviceType=0x" - + Integer.toHexString(deviceType)); + log.debug("Info: host=" + host + " mac=" + mac.getMacString() + " deviceType=0x" + Integer.toHexString(deviceType)); log.debug("Creating BLDevice instance"); BLDevice inst = createInstance(deviceType, host, mac); - if (inst != null) { + if (inst != null) + { if (debug) log.debug("Adding to found devices list"); devices.add(inst); - } else if (debug) { + } + else if (debug) + { log.debug("Cannot create instance, returned null, not adding to found devices list"); } } else { @@ -755,76 +823,101 @@ public static BLDevice[] discoverDevices(InetAddress sourceIpAddr, int sourcePor return out; } - public static String getDescOfType(short devType){ - switch (devType) { - - // - // RM Series - // - - case BLDevice.DEV_RM_2: - return DESC_RM_2; - case BLDevice.DEV_RM_MINI: - return DESC_RM_MINI; - case BLDevice.DEV_RM_MINI_3: - return DESC_RM_MINI_3; - case BLDevice.DEV_RM_PRO_PHICOMM: - return DESC_RM_PRO_PHICOMM; - case BLDevice.DEV_RM_2_HOME_PLUS: - return DESC_RM_2_HOME_PLUS; - case BLDevice.DEV_RM_2_2HOME_PLUS_GDT: - return DESC_RM_2_2HOME_PLUS_GDT; - case BLDevice.DEV_RM_2_PRO_PLUS: - return DESC_RM_2_PRO_PLUS; - case BLDevice.DEV_RM_2_PRO_PLUS_2: - return DESC_RM_2_PRO_PLUS_2; - case BLDevice.DEV_RM_2_PRO_PLUS_2_BL: - return DESC_RM_2_PRO_PLUS_2_BL; - case BLDevice.DEV_RM_MINI_SHATE: - return DESC_RM_MINI_SHATE; + public static String getDescOfType(short devType) + { + switch (devType) + { - // - // SP2 Series - // - - case BLDevice.DEV_SP2: - return DESC_SP2; - case BLDevice.DEV_SP2_HONEYWELL_ALT1: - return DESC_SP2_HONEYWELL_ALT1; - case BLDevice.DEV_SP2_HONEYWELL_ALT2: - return DESC_SP2_HONEYWELL_ALT2; - case BLDevice.DEV_SP2_HONEYWELL_ALT3: - return DESC_SP2_HONEYWELL_ALT3; - case BLDevice.DEV_SP2_HONEYWELL_ALT4: - return DESC_SP2_HONEYWELL_ALT4; - case BLDevice.DEV_SP3: - return DESC_SP3; - case BLDevice.DEV_SPMINI: - return DESC_SPMINI; - case BLDevice.DEV_SPMINI2: - return DESC_SPMINI2; - case BLDevice.DEV_SPMINI_OEM_ALT1: - return DESC_SPMINI_OEM_ALT1; - case BLDevice.DEV_SPMINI_OEM_ALT2: - return DESC_SPMINI_OEM_ALT2; - case BLDevice.DEV_SPMINI_PLUS: - return DESC_SPMINI_PLUS; - - case BLDevice.DEV_SP1: - return BLDevice.DESC_SP1; - case BLDevice.DEV_MP1: - return BLDevice.DESC_MP1; - case BLDevice.DEV_A1: - return BLDevice.DESC_A1; - case BLDevice.DEV_HYSEN: - return BLDevice.DESC_HYSEN; - case BLDevice.DEV_FLOUREON: - return BLDevice.DESC_FLOUREON; - // - // Unregonized - // - default: - return DESC_UNKNOWN; + // + // RM Series + // + + case BLDevice.DEV_RM_2: + return DESC_RM_2; + case BLDevice.DEV_RM_MINI: + return DESC_RM_MINI; + case BLDevice.DEV_RM_MINI_OEM_1: + return DESC_RM_MINI_OEM_1; + case BLDevice.DEV_RM_MINI_3: + return DESC_RM_MINI_3; + case BLDevice.DEV_RM_PRO_PHICOMM: + return DESC_RM_PRO_PHICOMM; + case BLDevice.DEV_RM_2_HOME_PLUS: + return DESC_RM_2_HOME_PLUS; + case BLDevice.DEV_RM_2_2HOME_PLUS_GDT: + return DESC_RM_2_2HOME_PLUS_GDT; + case BLDevice.DEV_RM_2_PRO_PLUS: + return DESC_RM_2_PRO_PLUS; + case BLDevice.DEV_RM_2_PRO_PLUS_2: + return DESC_RM_2_PRO_PLUS_2; + case BLDevice.DEV_RM_2_PRO_PLUS_2_BL: + return DESC_RM_2_PRO_PLUS_2_BL; + case BLDevice.DEV_RM_MINI_SHATE: + return DESC_RM_MINI_SHATE; + + // + // SP2 Series + // + + case BLDevice.DEV_SP2: + return DESC_SP2; + case BLDevice.DEV_SP2_HONEYWELL_ALT1: + return DESC_SP2_HONEYWELL_ALT1; + case BLDevice.DEV_SP2_HONEYWELL_ALT2: + return DESC_SP2_HONEYWELL_ALT2; + case BLDevice.DEV_SP2_HONEYWELL_ALT3: + return DESC_SP2_HONEYWELL_ALT3; + case BLDevice.DEV_SP2_HONEYWELL_ALT4: + return DESC_SP2_HONEYWELL_ALT4; + case BLDevice.DEV_SP3: + return DESC_SP3; + case BLDevice.DEV_SPMINI: + return DESC_SPMINI; + + // 2026.01.09 + case BLDevice.DEV_SP4L_CN: + return DESC_SP4L_CN; + case BLDevice.DEV_SPMINI_3: + return DESC_SPMINI3; + case BLDevice.DEV_SP4M_JP: + return DESC_SP4M_JP; + case BLDevice.DEV_SP4M: + return DESC_SP4M; + case BLDevice.DEV_MCB1: + return DESC_MCB1; + case BLDevice.DEV_SP4L_EU: + return DESC_SP4L_EU; + case BLDevice.DEV_SP4L_AU: + return DESC_SP4L_AU; + case BLDevice.DEV_SPMINI_3_2: + return DESC_SPMINI_3_2; + case BLDevice.DEV_SP4L_UK: + return DESC_SP4L_UK; + + case BLDevice.DEV_SPMINI2: + return DESC_SPMINI2; + case BLDevice.DEV_SPMINI_OEM_ALT1: + return DESC_SPMINI_OEM_ALT1; + case BLDevice.DEV_SPMINI_OEM_ALT2: + return DESC_SPMINI_OEM_ALT2; + case BLDevice.DEV_SPMINI_PLUS: + return DESC_SPMINI_PLUS; + + case BLDevice.DEV_SP1: + return BLDevice.DESC_SP1; + case BLDevice.DEV_MP1: + return BLDevice.DESC_MP1; + case BLDevice.DEV_A1: + return BLDevice.DESC_A1; + case BLDevice.DEV_HYSEN: + return BLDevice.DESC_HYSEN; + case BLDevice.DEV_FLOUREON: + return BLDevice.DESC_FLOUREON; + // + // Unregonized + // + default: + return DESC_UNKNOWN; } } diff --git a/src/main/java/com/github/mob41/blapi/Map_JSON.java b/src/main/java/com/github/mob41/blapi/Map_JSON.java new file mode 100644 index 0000000..2a05736 --- /dev/null +++ b/src/main/java/com/github/mob41/blapi/Map_JSON.java @@ -0,0 +1,302 @@ + +/******************************************************************************* + * MIT License + * + * Copyright (c) 2016, 2017 Anthony Law + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + *******************************************************************************/ + +package com.github.mob41.blapi; + + +import java.util.*; + +public class Map_JSON +{ + // -------------------------- Map --> JSON String -------------------------- + /** + * 原生实现:将Map转为紧凑JSON字符串(无空格、无缩进) + * 适配常见类型:String、Integer、Boolean、Long(可根据实际需求扩展) + * @param map 待序列化的Map + * @return 紧凑JSON字符串 + */ + public static String map_2_json(Map map) + { + if (map == null || map.isEmpty()) + { + return "{}"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("{"); + Set> entrySet = map.entrySet(); + boolean first = true; + + for (Map.Entry entry : entrySet) + { + if (!first) + { + sb.append(","); // 无空格,对应Python的separators=(",", ":") + } + first = false; + + // 拼接键(字符串类型,需加双引号) + sb.append("\"").append(escapeJson(entry.getKey())).append("\":"); + + // 拼接值(处理常见类型) + Object value = entry.getValue(); + if (value == null) + { + sb.append("null"); + } + else if (value instanceof String) + { + sb.append("\"").append(escapeJson((String) value)).append("\""); + } + else if (value instanceof Number) + { + sb.append(value.toString()); // 数字类型直接拼接(int/long/float等) + } + else if (value instanceof Boolean) + { + sb.append(value.toString()); // true/false,无需加引号 + } + else + { + // 若有其他类型(如List),可在此扩展,此处默认转字符串 + sb.append("\"").append(escapeJson(value.toString())).append("\""); + } + } + + sb.append("}"); + return sb.toString(); + } + + /** + * 辅助方法:JSON字符串转义(处理双引号、反斜杠等) + * @param str 原始字符串 + * @return 转义后的字符串 + */ + private static String escapeJson(String str) + { + if (str == null) + { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (char c : str.toCharArray()) + { + switch (c) + { + case '"': sb.append("\\\"");break; + case '\\': sb.append("\\\\");break; + case '/': sb.append("\\/");break; + case '\b': sb.append("\\b");break; + case '\f': sb.append("\\f");break; + case '\n': sb.append("\\n");break; + case '\r': sb.append("\\r");break; + case '\t': sb.append("\\t");break; + default: sb.append(c);break; + } + } + return sb.toString(); + } + + + + + + // -------------------------- JSON String --> Map -------------------------- + /** + * 原生实现:解析紧凑JSON字符串为Map + * 适配场景:仅解析键为字符串、值为String/Integer/Boolean的扁平JSON(和之前序列化的格式匹配) + * 若需支持嵌套/List,可扩展此方法 + * @param jsonStr 紧凑JSON字符串 + * @return 解析后的Map + * @throws Exception JSON格式错误异常 + */ + public static Map json_2_map(String jsonStr) throws Exception + { + Map result = new HashMap<>(); + // 去除首尾的{},并去除可能的空白(兼容少量意外空格) + String content = jsonStr.trim(); + if (!content.startsWith("{") || !content.endsWith("}")) + { + throw new IllegalArgumentException("无效的JSON对象格式:必须以{}包裹"); + } + content = content.substring(1, content.length() - 1).trim(); + if (content.isEmpty()) + { + return result; // 空JSON对象 + } + + // 拆分键值对(处理JSON字符串内的逗号,避免误拆分) + List keyValuePairs = splitJsonKeyValuePairs(content); + for (String pair : keyValuePairs) + { + // 拆分键和值(处理值内的冒号,避免误拆分) + int colonIndex = findFirstUnescapedColon(pair); + if (colonIndex == -1) + { + throw new IllegalArgumentException("无效的键值对:缺少冒号 " + pair); + } + // 解析键 + String key = pair.substring(0, colonIndex).trim(); + key = unescapeJson(key); // 去除双引号并转义 + // 解析值 + String valueStr = pair.substring(colonIndex + 1).trim(); + Object value = parseJsonValue(valueStr); + // 存入Map + result.put(key, value); + } + return result; + } + + /** + * 辅助方法:拆分JSON键值对(避免拆分字符串内的逗号) + */ + private static List splitJsonKeyValuePairs(String content) + { + List pairs = new ArrayList<>(); + int start = 0; + int quoteCount = 0; + char[] chars = content.toCharArray(); + for (int i = 0; i < chars.length; i++) + { + char c = chars[i]; + // 统计双引号数量(奇数=在字符串内,偶数=字符串外) + if (c == '"' && (i == 0 || chars[i-1] != '\\')) + { + quoteCount++; + } + // 仅当在字符串外时,才拆分逗号 + if (c == ',' && quoteCount % 2 == 0) + { + pairs.add(content.substring(start, i).trim()); + start = i + 1; + } + } + // 添加最后一个键值对 + pairs.add(content.substring(start).trim()); + return pairs; + } + + /** + * 辅助方法:找到第一个不在字符串内的冒号(键值分隔符) + */ + private static int findFirstUnescapedColon(String pair) + { + int quoteCount = 0; + char[] chars = pair.toCharArray(); + for (int i = 0; i < chars.length; i++) + { + char c = chars[i]; + if (c == '"' && (i == 0 || chars[i-1] != '\\')) + { + quoteCount++; + } + if (c == ':' && quoteCount % 2 == 0) + { + return i; + } + } + return -1; + } + + /** + * 辅助方法:解析JSON值(支持String/Integer/Boolean/null) + */ + private static Object parseJsonValue(String valueStr) throws Exception + { + if (valueStr.startsWith("\"") && valueStr.endsWith("\"")) + { + // 字符串类型:去除双引号并转义 + return unescapeJson(valueStr.substring(1, valueStr.length() - 1)); + } + else if ("true".equals(valueStr) /*|| "1".equals(valueStr)*/) + { + // 布尔值true + return true; + } + else if ("false".equals(valueStr) /*|| "0".equals(valueStr)*/) + { + // 布尔值false + return false; + } + else if ("null".equals(valueStr)) + { + // null值 + return null; + } + else + { + // 数字类型(适配整数,若需浮点数可扩展) + try + { + return Integer.parseInt(valueStr); + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("不支持的JSON值类型:" + valueStr); + } + } + } + + /** + * 辅助方法:JSON字符串反转义(还原特殊字符) + */ + private static String unescapeJson(String str) + { + if (str == null) + { + return ""; + } + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < str.length()) + { + char c = str.charAt(i); + if (c == '\\' && i + 1 < str.length()) + { + // 处理转义字符 + char next = str.charAt(i + 1); + switch (next) + { + case '"': sb.append("\""); break; + case '\\': sb.append("\\"); break; + case '/': sb.append("/"); break; + case 'b': sb.append("\b"); break; + case 'f': sb.append("\f"); break; + case 'n': sb.append("\n"); break; + case 'r': sb.append("\r"); break; + case 't': sb.append("\t"); break; + default: sb.append(next); break; + } + i += 2; + } + else + { + sb.append(c); + i++; + } + } + return sb.toString(); + } +} diff --git a/src/main/java/com/github/mob41/blapi/SP4Device.java b/src/main/java/com/github/mob41/blapi/SP4Device.java new file mode 100644 index 0000000..86ac9d3 --- /dev/null +++ b/src/main/java/com/github/mob41/blapi/SP4Device.java @@ -0,0 +1,233 @@ +/******************************************************************************* + * MIT License + * + * Copyright (c) 2016, 2017 Anthony Law + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + *******************************************************************************/ + +package com.github.mob41.blapi; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.util.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Map; + +import javax.xml.bind.DatatypeConverter; + +import com.github.mob41.blapi.mac.Mac; +import com.github.mob41.blapi.pkt.CmdPayload; +import com.github.mob41.blapi.pkt.Payload; + +public class SP4Device extends BLDevice +{ + + protected SP4Device(short deviceType, String deviceDesc, String host, Mac mac) throws IOException + { + super(deviceType, deviceDesc, host, mac); + } + + + + public SP4Device(String host, Mac mac) throws IOException + { + super(BLDevice.DEV_SP4L_CN, BLDevice.DESC_SP4L_CN, host, mac); + } + + + + + public void setState(final boolean pwr, final boolean ntlight, final boolean indicator, final int ntlbrightness, final int maxworktime, final boolean childlock) throws Exception // set_power + { + DatagramPacket packet = sendCmdPkt(new CmdPayload() + { + @Override + public byte getCommand() + { + return 0x6a; + } + + @Override + public Payload getPayload() + { + return new Payload() + { + @Override + public byte[] getData() + { + Map state = new HashMap<>(); + state.put("pwr", pwr ? 1 : 0); + state.put("ntlight", ntlight ? 1 : 0); + state.put("indicator", indicator ? 1 : 0); + state.put("ntlbrightness", ntlbrightness); + state.put("maxworktime", maxworktime); + state.put("childlock", childlock ? 1 : 0); + return _encode(2, state); + } + }; + } + + }); + + byte[] data = packet.getData(); + log.debug("SP4 set state received encrypted bytes: " + DatatypeConverter.printHexBinary(data)); + + int err = data[0x22] | (data[0x23] << 8); + if (err == 0) + { + Map status = _decode(data); + log.debug("setState returned pwr: " + status.get("\"pwr\"")); + log.debug("setState returned ntlight: " + status.get("\"ntlight\"")); + log.debug("setState returned indicator: " + status.get("\"indicator\"")); + log.debug("setState returned ntlbrightness: " + status.get("\"ntlbrightness\"")); + log.debug("setState returned maxworktime: " + status.get("\"maxworktime\"")); + log.debug("setState returned childlock: " + status.get("\"childlock\"")); + } + else + { + log.warn("SP4 set state received returned err: " + Integer.toHexString(err) + " / " + err); + } + } + + + + public Map getState() throws Exception // check_power + { + DatagramPacket packet = sendCmdPkt(new CmdPayload() + { + @Override + public byte getCommand() + { + return 0x6a; + } + + @Override + public Payload getPayload() + { + return new Payload() + { + + @Override + public byte[] getData() + { + return _encode(1, new HashMap<>()); + } + }; + } + + }); + + byte[] data = packet.getData(); + log.debug("SP4 get state received encrypted bytes: " + DatatypeConverter.printHexBinary(data)); + + int err = data[0x22] | (data[0x23] << 8); + if (err == 0) + { + return _decode(data); + } + else + { + log.warn("SP4 get state received an error: " + Integer.toHexString(err) + " / " + err); + } + return new HashMap<>(); + } + + + + /** + * 编码消息,等价于Python的_encode方法(纯原生实现,无第三方库) + * @param flag 标志位整数 + * @param state 待序列化的字典(Java用Map替代) + * @return 编码后的字节数组 + */ + private byte[] _encode(int flag, Map state) + { + // 1. 原生实现:将Map序列化为紧凑JSON字符串(对应Python的json.dumps(separators=(",", ":"))) + String jsonStr = Map_JSON.map_2_json(state); + log.debug(jsonStr); + // 转字节数组(默认UTF-8编码,和Python的encode()一致) + byte[] data = jsonStr.getBytes(); + + // 2. 初始化12字节的缓冲区,设置小端序(对应Python的bytearray(12)和struct的<) + ByteBuffer packetBuffer = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN); + + // 3. 按 _decode(byte[] data) throws Exception + { + + byte[] payload = decryptFromDeviceMessage(data); + log.debug("SP4 get state received bytes (decrypted): " + DatatypeConverter.printHexBinary(payload)); + + // 解析js_len:struct.unpack_from(" payload.length) + { + throw new IllegalArgumentException("JSON长度超出payload范围,可能是数据损坏"); + } + byte[] jsonBytes = Arrays.copyOfRange(payload, jsonStart, jsonEnd); + String jsonStr = new String(jsonBytes); + log.debug("SP4 get state received jsonStr: " + jsonStr); + + // 5. 原生解析JSON字符串为Map(替代Python的json.loads) + return Map_JSON.json_2_map(jsonStr); + } +}