From ea79085820c9b767e2cfc64dae8849901f8cb38b Mon Sep 17 00:00:00 2001 From: d51x Date: Sun, 23 Jul 2023 18:54:19 +0300 Subject: [PATCH] [tuya] Add support for IR controller (#501) Add support for IR controller Signed-off-by: Dmitry Pyatykh --- bundles/org.smarthomej.binding.tuya/README.md | 59 +++ .../tuya/internal/TuyaBindingConstants.java | 1 + .../tuya/internal/TuyaHandlerFactory.java | 3 +- .../internal/config/ChannelConfiguration.java | 5 + .../internal/handler/TuyaDeviceHandler.java | 143 ++++++- .../tuya/internal/local/dto/IrCode.java | 28 ++ .../binding/tuya/internal/util/IrUtils.java | 368 ++++++++++++++++++ .../resources/OH-INF/thing/thing-types.xml | 49 ++- 8 files changed, 651 insertions(+), 5 deletions(-) create mode 100644 bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/local/dto/IrCode.java create mode 100644 bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/util/IrUtils.java diff --git a/bundles/org.smarthomej.binding.tuya/README.md b/bundles/org.smarthomej.binding.tuya/README.md index dc6fe38dc5..c2adb1a6c5 100644 --- a/bundles/org.smarthomej.binding.tuya/README.md +++ b/bundles/org.smarthomej.binding.tuya/README.md @@ -117,6 +117,65 @@ The `min` and `max` parameters define the range allowed (e.g. 0-86400 for turn-o The `string` channel has one additional (optional) parameter `range`. It contains a comma-separated list of command options for this channel (e.g. `white,colour,scene,music` for the "workMode" channel). +### Type `ir-code` + +IR code types: ++ `Tuya DIY-mode` - use study codes from real remotes. + + Make a virtual remote control in DIY, learn virtual buttons. + ++ `Tuya Codes Library (check Advanced options)` - use codes from templates library. + + Make a virtual remote control from pre-defined type of devices. + + Select Advanced checkbox to configure other parameters: + + `irCode` - Decoding parameter + + `irSendDelay` - used as `Send delay` parameter + + `irCodeType` - used as `type library` parameter + ++ `NEC` - IR Code in NEC format ++ `Samsung` - IR Code in Samsung format. + +**Additional options:** +* `Active Listening` - Device will be always in learning mode. + After send command with key code device stays in the learning mode +* `DP Study Key` - **Advanced**. DP number for study key. Uses for receive key code in learning mode. Change it own your + risk. + + +If linked item received a command with `Key Code` (Code Library Parameter) then device sends appropriate key code. + +#### How to use IR Code in NEC format. + +Example, from Tasmota you need to use **_Data_** parameter, it can be with or without **_0x_** +```json +{"Time": "2023-07-05T18:17:42", "IrReceived": {"Protocol": "NEC", "Bits": 32, "Data": "0x10EFD02F"}} +``` + +Another example, use **_hex_** parameter +```json +{ "type": "nec", "uint32": 284151855, "address": 8, "data": 11, "hex": "10EFD02F" } +``` + +#### How to get key codes without Tasmota and other + +Channel can receive learning key (autodetect format and put autodetected code in channel). + +To start learning codes add new channel with Type String and DP = 1 and Range with `send_ir,study,study_exit,study_key`. + +Link Item to this added channel and send command `study`. + +Device will be in learning mode and be able to receive codes from remote control. + +Just press a button on the remote control and see key code in channel `ir-code`. + +If type of channel `ir-code` is **_NEC_** or **_Samsung_** you will see just a hex code. + +If type of channel `ir-code` is **_Tuya DIY-mode_** you will see a type of code format and a hex code. + +Pressing buttons and copying codes, then assign codes with Item which control device (adjust State Description and Command Options you want). + +After receiving the key code, the learning mode automatically continues until you send command `study_exit` or send key code by Item with code ## Troubleshooting - If the `project` thing is not coming `ONLINE` check if you see your devices in the cloud-account on `iot.tuya.com`. diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/TuyaBindingConstants.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/TuyaBindingConstants.java index 3170653e52..1a6e54a21a 100644 --- a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/TuyaBindingConstants.java +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/TuyaBindingConstants.java @@ -56,6 +56,7 @@ public class TuyaBindingConstants { public static final ChannelTypeUID CHANNEL_TYPE_UID_NUMBER = new ChannelTypeUID(BINDING_ID, "number"); public static final ChannelTypeUID CHANNEL_TYPE_UID_STRING = new ChannelTypeUID(BINDING_ID, "string"); public static final ChannelTypeUID CHANNEL_TYPE_UID_SWITCH = new ChannelTypeUID(BINDING_ID, "switch"); + public static final ChannelTypeUID CHANNEL_TYPE_UID_IR_CODE = new ChannelTypeUID(BINDING_ID, "ir-code"); public static final int TCP_CONNECTION_HEARTBEAT_INTERVAL = 10; // in s public static final int TCP_CONNECTION_TIMEOUT = 60; // in s; diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/TuyaHandlerFactory.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/TuyaHandlerFactory.java index a43ef24e97..40b256379e 100644 --- a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/TuyaHandlerFactory.java +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/TuyaHandlerFactory.java @@ -12,7 +12,8 @@ */ package org.smarthomej.binding.tuya.internal; -import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.*; +import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.THING_TYPE_PROJECT; +import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.THING_TYPE_TUYA_DEVICE; import java.lang.reflect.Type; import java.util.List; diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/config/ChannelConfiguration.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/config/ChannelConfiguration.java index 428a7a9f24..092d723211 100644 --- a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/config/ChannelConfiguration.java +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/config/ChannelConfiguration.java @@ -26,4 +26,9 @@ public class ChannelConfiguration { public int min = Integer.MIN_VALUE; public int max = Integer.MAX_VALUE; public String range = ""; + public String irCode = ""; + public int irSendDelay = 300; + public int irCodeType = 0; + public String irType = ""; + public Boolean activeListen = Boolean.FALSE; } diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/handler/TuyaDeviceHandler.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/handler/TuyaDeviceHandler.java index 2bb464b858..5d4f39bfc4 100644 --- a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/handler/TuyaDeviceHandler.java +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/handler/TuyaDeviceHandler.java @@ -12,7 +12,13 @@ */ package org.smarthomej.binding.tuya.internal.handler; -import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.*; +import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_COLOR; +import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_DIMMER; +import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_IR_CODE; +import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_NUMBER; +import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_STRING; +import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.CHANNEL_TYPE_UID_SWITCH; +import static org.smarthomej.binding.tuya.internal.TuyaBindingConstants.SCHEMAS; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -59,11 +65,14 @@ import org.smarthomej.binding.tuya.internal.local.TuyaDevice; import org.smarthomej.binding.tuya.internal.local.UdpDiscoveryListener; import org.smarthomej.binding.tuya.internal.local.dto.DeviceInfo; +import org.smarthomej.binding.tuya.internal.local.dto.IrCode; import org.smarthomej.binding.tuya.internal.util.ConversionUtil; +import org.smarthomej.binding.tuya.internal.util.IrUtils; import org.smarthomej.binding.tuya.internal.util.SchemaDp; import org.smarthomej.commons.SimpleDynamicCommandDescriptionProvider; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import io.netty.channel.EventLoopGroup; @@ -91,7 +100,7 @@ public class TuyaDeviceHandler extends BaseThingHandler implements DeviceInfoSub private @Nullable ScheduledFuture reconnectFuture; private @Nullable ScheduledFuture pollingJob; - + private @Nullable ScheduledFuture irLearnJob; private boolean disposing = false; private final Map dpToChannelId = new HashMap<>(); @@ -128,7 +137,6 @@ public void processDeviceStatus(Map deviceStatus) { if (tuyaDevice != null) { tuyaDevice.set(commandRequest); } - return; } @@ -174,6 +182,14 @@ private void processChannelStatus(Integer dp, Object value) { && CHANNEL_TYPE_UID_SWITCH.equals(channelTypeUID)) { updateState(channelId, OnOffType.from((boolean) value)); return; + } else if (value instanceof String && CHANNEL_TYPE_UID_IR_CODE.equals(channelTypeUID)) { + if (configuration.dp == 2) { + String decoded = convertBase64Code(configuration, (String) value); + logger.info("thing {} received ir code: {}", thing.getUID(), decoded); + updateState(channelId, new StringType(decoded)); + irStartLearning(configuration.activeListen); + } + return; } logger.warn("Could not update channel '{}' of thing '{}' with value '{}'. Datatype incompatible.", channelId, getThing().getUID(), value); @@ -204,6 +220,16 @@ public void connectionStatus(boolean status) { pollingJob = scheduler.scheduleWithFixedDelay(tuyaDevice::refreshStatus, pollingInterval, pollingInterval, TimeUnit.SECONDS); } + + // start learning code if thing is online and presents 'ir-code' channel + this.getThing().getChannels().stream() + .filter(channel -> CHANNEL_TYPE_UID_IR_CODE.equals(channel.getChannelTypeUID())).findFirst() + .ifPresent(channel -> { + ChannelConfiguration config = channelIdToConfiguration.get(channel.getChannelTypeUID()); + if (config != null) { + irStartLearning(config.activeListen); + } + }); } else { updateStatus(ThingStatus.OFFLINE); ScheduledFuture pollingJob = this.pollingJob; @@ -218,6 +244,7 @@ public void connectionStatus(boolean status) { if (tuyaDevice != null && !disposing && (reconnectFuture == null || reconnectFuture.isDone())) { this.reconnectFuture = scheduler.schedule(tuyaDevice::connect, 5000, TimeUnit.MILLISECONDS); } + irStopLearning(); } } @@ -300,12 +327,46 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof OnOffType) { commandRequest.put(configuration.dp, OnOffType.ON.equals(command)); } + } else if (CHANNEL_TYPE_UID_IR_CODE.equals(channelTypeUID)) { + if (command instanceof StringType) { + if (configuration.irType.equals("base64")) { + commandRequest.put(1, "study_key"); + commandRequest.put(7, command.toString()); + } else if (configuration.irType.equals("tuya-head")) { + if (configuration.irCode != null && !configuration.irCode.isEmpty()) { + commandRequest.put(1, "send_ir"); + commandRequest.put(3, configuration.irCode); + commandRequest.put(4, command.toString()); + commandRequest.put(10, configuration.irSendDelay); + commandRequest.put(13, configuration.irCodeType); + } else { + logger.warn("irCode is not set for channel {}", channelUID); + } + } else if (configuration.irType.equals("nec")) { + long code = convertHexCode(command.toString()); + String base64Code = IrUtils.necToBase64(code); + commandRequest.put(1, "study_key"); + commandRequest.put(7, base64Code); + } else if (configuration.irType.equals("samsung")) { + long code = convertHexCode(command.toString()); + String base64Code = IrUtils.samsungToBase64(code); + commandRequest.put(1, "study_key"); + commandRequest.put(7, base64Code); + } + irStopLearning(); + } } TuyaDevice tuyaDevice = this.tuyaDevice; if (!commandRequest.isEmpty() && tuyaDevice != null) { tuyaDevice.set(commandRequest); } + + if (CHANNEL_TYPE_UID_IR_CODE.equals(channelTypeUID)) { + if (command instanceof StringType) { + irStartLearning(configuration.activeListen); + } + } } @Override @@ -328,6 +389,7 @@ public void dispose() { tuyaDevice.dispose(); this.tuyaDevice = null; } + irStopLearning(); } @Override @@ -488,6 +550,9 @@ private void configureChannel(Channel channel) { .requireNonNull(dp2ToChannelId.computeIfAbsent(configuration.dp2, ArrayList::new)); list.add(channelId); } + if (CHANNEL_TYPE_UID_IR_CODE.equals(channelTypeUID)) { + irStartLearning(configuration.activeListen); + } } private List toCommandOptionList(List options) { @@ -505,4 +570,76 @@ protected void updateState(String channelId, State state) { channelStateCache.put(channelId, state); super.updateState(channelId, state); } + + private long convertHexCode(String code) { + String sCode = code.startsWith("0x") ? code.substring(2) : code; + return Long.parseLong(sCode, 16); + } + + private String convertBase64Code(ChannelConfiguration channelConfig, String encoded) { + String decoded = ""; + try { + if (channelConfig.irType.equals("nec")) { + decoded = IrUtils.base64ToNec(encoded); + IrCode code = Objects.requireNonNull(gson.fromJson(decoded, IrCode.class)); + decoded = "0x" + code.hex; + } else if (channelConfig.irType.equals("samsung")) { + decoded = IrUtils.base64ToSamsung(encoded); + IrCode code = Objects.requireNonNull(gson.fromJson(decoded, IrCode.class)); + decoded = "0x" + code.hex; + } else { + if (encoded.length() > 68) { + decoded = IrUtils.base64ToNec(encoded); + if (decoded == null || decoded.isEmpty()) { + decoded = IrUtils.base64ToSamsung(encoded); + } + IrCode code = Objects.requireNonNull(gson.fromJson(decoded, IrCode.class)); + decoded = code.type + ": 0x" + code.hex; + } else { + decoded = encoded; + } + } + } catch (JsonSyntaxException e) { + logger.error("Incorrect json response: {}", e.getMessage()); + decoded = encoded; + } catch (NullPointerException e) { + logger.error("unable decode key code'{}', reason: {}", decoded, e.getMessage()); + } + return decoded; + } + + private void finishStudyCode() { + Map commandRequest = new HashMap<>(); + commandRequest.put(1, "study_exit"); + TuyaDevice tuyaDevice = this.tuyaDevice; + if (!commandRequest.isEmpty() && tuyaDevice != null) { + tuyaDevice.set(commandRequest); + } + } + + private void repeatStudyCode() { + Map commandRequest = new HashMap<>(); + commandRequest.put(1, "study"); + TuyaDevice tuyaDevice = this.tuyaDevice; + if (!commandRequest.isEmpty() && tuyaDevice != null) { + tuyaDevice.set(commandRequest); + } + } + + private void irStopLearning() { + logger.debug("[tuya:ir-controller] stop ir learning"); + ScheduledFuture feature = irLearnJob; + if (feature != null) { + feature.cancel(true); + this.irLearnJob = null; + } + } + + private void irStartLearning(Boolean available) { + irStopLearning(); + if (available) { + logger.debug("[tuya:ir-controller] start ir learning"); + irLearnJob = scheduler.scheduleWithFixedDelay(this::repeatStudyCode, 200, 29000, TimeUnit.MILLISECONDS); + } + } } diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/local/dto/IrCode.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/local/dto/IrCode.java new file mode 100644 index 0000000000..83ced8e9ac --- /dev/null +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/local/dto/IrCode.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2021-2023 Contributors to the SmartHome/J 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.smarthomej.binding.tuya.internal.local.dto; + +import org.eclipse.jdt.annotation.*; + +/** + * The {@link IrCode} represents the IR code decoded messages sent by Tuya devices + * + * @author Dmitry Pyatykh - Initial contribution + */ +@NonNullByDefault +public class IrCode { + public String type = ""; + public String hex = ""; + public Integer address = 0; + public Integer data = 0; +} diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/util/IrUtils.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/util/IrUtils.java new file mode 100644 index 0000000000..b786a85ef6 --- /dev/null +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/util/IrUtils.java @@ -0,0 +1,368 @@ +/** + * Copyright (c) 2021-2023 Contributors to the SmartHome/J 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.smarthomej.binding.tuya.internal.util; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link IrUtils} is a support class for decode/encode infra-red codes + *

+ * Based on https://github.com/jasonacox/tinytuya/blob/master/tinytuya/Contrib/IRRemoteControlDevice.py + * + * @author Dmitry Pyatykh - Initial contribution + */ +public class IrUtils { + private static final Logger logger = LoggerFactory.getLogger(IrUtils.class); + + private IrUtils() { + } + + /** + * Convert Base64 code format from Tuya to nec-format. + * + * @param base64Code the base64 code format from Tuya + * @return the nec-format code + */ + public static String base64ToNec(String base64Code) { + String result = null; + List pulses = base64ToPulse(base64Code); + if (pulses != null && !pulses.isEmpty()) { + List res = pulsesToNec(pulses); + if (res != null && !res.isEmpty()) { + result = res.get(0); + } + } + return result; + } + + /** + * Convert Base64 code format from Tuya to samsung-format. + * + * @param base64Code the base64 code format from Tuya + * @return the samsung-format code + */ + public static String base64ToSamsung(String base64Code) { + String result = null; + List pulses = base64ToPulse(base64Code); + if (pulses != null && !pulses.isEmpty()) { + List res = pulsesToSamsung(pulses); + if (res != null && !res.isEmpty()) { + result = res.get(0); + } + } + return result; + } + + private static List base64ToPulse(String base64Code) { + List pulses = new ArrayList<>(); + String key = (base64Code.length() % 4 == 1 && base64Code.startsWith("1")) ? base64Code.substring(1) + : base64Code; + byte[] raw_bytes = Base64.getDecoder().decode(key.getBytes(StandardCharsets.UTF_8)); + + int i = 0; + try { + while (i < raw_bytes.length) { + int word = ((raw_bytes[i] & 0xFF) + (raw_bytes[i + 1] & 0xFF) * 256) & 0xFFFF; + pulses.add(word); + i += 2; + + // dirty hack because key not aligned by 4 byte ? + if (i >= raw_bytes.length) { + break; + } + } + } catch (ArrayIndexOutOfBoundsException e) { + logger.error("Failed to convert base64 key code to pulses: {}", e.getMessage()); + } + return pulses; + } + + private static List pulsesToWidthEncoded(List pulses, Integer startMark, Integer startSpace, + Integer pulseThreshold, Integer spaceThreshold) { + List ret = new ArrayList<>(); + if (pulses.size() < 68) { + return null; + } + + if (pulseThreshold == null && spaceThreshold == null) { + return null; + } + + if (startMark != null) { + while (pulses.size() >= 68 && (pulses.get(0) < (startMark * 0.75) || pulses.get(0) > (startMark * 1.25))) { + pulses.remove(0); + } + + while (pulses.size() >= 68) { + if (pulses.get(0) < startMark * 0.75 || pulses.get(0) > startMark * 1.25) { + return null; + } + + if (startSpace != null + && (pulses.get(1) < (startSpace * 0.75) || pulses.get(1) > (startSpace * 1.25))) { + return null; + } + + // remove two first elements + pulses.remove(0); + pulses.remove(0); + + Integer res = 0; + long x = 0L; + + for (int i = 31; i >= 0; i--) { + Integer pulseMatch = null; + Integer spaceMatch = null; + + if (pulseThreshold != null) { + pulseMatch = pulses.get(0) >= pulseThreshold ? 1 : 0; + } + if (spaceThreshold != null) { + spaceMatch = pulses.get(1) >= spaceThreshold ? 1 : 0; + } + + if (pulseMatch != null && spaceMatch != null) { + if (!pulseMatch.equals(spaceMatch)) { + return null; + } + res = spaceMatch; + } else if (pulseMatch == null) { + res = spaceMatch; + } else { + res = pulseMatch; + } + + if (res != null) { + x |= (long) (res) << i; + } + + // remove two first elements + pulses.remove(0); + pulses.remove(0); + } + + if (!ret.contains(x)) { + ret.add(x); + } + } + } + + return ret; + } + + private static List widthEncodedToPulses(long data, PulseParams param) { + List pulses = new ArrayList<>(); + pulses.add(param.startMark); + pulses.add(param.startSpace); + + for (int i = 31; i >= 0; i--) { + if ((data & (1 << i)) > 0L) { + pulses.add(param.pulseOne); + pulses.add(param.spaceOne); + } else { + pulses.add(param.pulseZero); + pulses.add(param.spaceZero); + } + } + pulses.add(param.trailingPulse); + pulses.add(param.trailingSpace); + return pulses; + } + + private static long mirrorBits(long data, int bits) { + int shift = bits - 1; + long out = 0; + + for (int i = 0; i < bits; i++) { + if ((data & (1L << i)) > 0L) { + out |= 1L << shift; + } + shift -= 1; + } + return out & 0xFF; + } + + private static List pulsesToNec(List pulses) { + List ret = new ArrayList<>(); + List res = pulsesToWidthEncoded(pulses, 9000, null, null, 1125); + if (res == null || res.isEmpty()) { + logger.warn("[tuya:ir-controller] No ir key-code detected"); + return null; + } + for (Long code : res) { + long addr = mirrorBits((code >> 24) & 0xFF, 8); + long addrNot = mirrorBits((code >> 16) & 0xFF, 8); + long data = mirrorBits((code >> 8) & 0xFF, 8); + long dataNot = mirrorBits(code & 0xFF, 8); + + if (addr != (addrNot ^ 0xFF)) { + addr = (addr << 8) | addrNot; + } + String d = String.format( + "{ \"type\": \"nec\", \"uint32\": %d, \"address\": None, \"data\": None, \"hex\": \"%08X\" }", code, + code); + if (data == (dataNot ^ 0xFF)) { + d = String.format( + "{ \"type\": \"nec\", \"uint32\": %d, \"address\": %d, \"data\": %d, \"hex\": \"%08X\" }", code, + addr, data, code); + } + ret.add(d); + } + return ret; + } + + private static String bytesToHex(byte[] bytes) { + final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + private static List necToPulses(long address, Long data) { + Long newAddress, newData; + if (data == null) { + newAddress = address; + } else { + if (address < 256) { + newAddress = mirrorBits(address, 8); + newAddress = (newAddress << 8) | (newAddress ^ 0xFF); + } else { + newAddress = (mirrorBits((address >> 8) & 0xFF, 8) << 8) | mirrorBits(address & 0xFF, 8); + } + newData = mirrorBits(data, 8); + newData = (newData << 8) | (newData & 0xFF); + newAddress = (newAddress << 16) | newData; + } + + return widthEncodedToPulses(newAddress, new PulseParams()); + } + + private static String pulsesToBase64(List pulses) { + byte[] bytes = new byte[pulses.size() * 2]; + + final Integer[] i = { 0 }; + + pulses.forEach(p -> { + int val = p.shortValue(); + bytes[i[0]] = (byte) (val & 0xFF); + bytes[i[0] + 1] = (byte) ((val >> 8) & 0xFF); + i[0] = i[0] + 2; + }); + + return new String(Base64.getEncoder().encode(bytes)); + } + + /** + * Convert Nec-format code to base64-format code from Tuya + * + * @param code nec-format code + * @return the string + */ + public static String necToBase64(long code) { + List pulses = necToPulses(code, null); + return pulsesToBase64(pulses); + } + + /** + * Convert Samsung-format code to base64-format code from Tuya + * + * @param code samsung-format code + * @return the string + */ + public static String samsungToBase64(long code) { + List pulses = samsungToPulses(code, null); + return pulsesToBase64(pulses); + } + + private static List samsungToPulses(long address, Long data) { + Long newAddress, newData; + if (data == null) { + newAddress = address; + } else { + newAddress = mirrorBits(address, 8); + newData = mirrorBits(data, 8); + newData = (newData << 8) | (newData & 0xFF); + newAddress = (newAddress << 24) + (newAddress << 16) + (newData << 8) + (newData ^ 0xFF); + } + return widthEncodedToPulses(newAddress, new PulseParams()); + } + + private static List pulsesToSamsung(List pulses) { + List ret = new ArrayList<>(); + List res = pulsesToWidthEncoded(pulses, 4500, null, null, 1125); + for (Long code : res) { + long addr = (code >> 24) & 0xFF; + long addrNot = (code >> 16) & 0xFF; + long data = (code >> 8) & 0xFF; + long dataNot = code & 0xFF; + + String d = String.format( + "{ \"type\": \"samsung\", \"uint32\": %d, \"address\": None, \"data\": None, \"hex\": \"%08X\" }", + code, code); + if (addr == addrNot && data == (dataNot ^ 0xFF)) { + addr = mirrorBits(addr, 8); + data = mirrorBits(data, 8); + d = String.format( + "{ \"type\": \"samsung\", \"uint32\": %d, \"address\": %d, \"data\": %d, \"hex\": \"%08X\" }", + code, addr, data, code); + } + ret.add(d); + } + return ret; + } + + private static class PulseParams { + /** + * The Start mark. + */ + public long startMark = 9000; + /** + * The Start space. + */ + public long startSpace = 4500; + /** + * The Pulse one. + */ + public long pulseOne = 563; + /** + * The Pulse zero. + */ + public long pulseZero = 563; + /** + * The Space one. + */ + public long spaceOne = 1688; + /** + * The Space zero. + */ + public long spaceZero = 563; + /** + * The Trailing pulse. + */ + public long trailingPulse = 563; + /** + * The Trailing space. + */ + public long trailingSpace = 30000; + } +} diff --git a/bundles/org.smarthomej.binding.tuya/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.smarthomej.binding.tuya/src/main/resources/OH-INF/thing/thing-types.xml index ad292a8872..d295cb733b 100644 --- a/bundles/org.smarthomej.binding.tuya/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.smarthomej.binding.tuya/src/main/resources/OH-INF/thing/thing-types.xml @@ -58,7 +58,7 @@ - + A generic Tuya device. Can be extended with channels. @@ -185,4 +185,51 @@ + + String + + Supported codes: tuya base64 codes diy mode, nec-format codes, samsung-format codes + + + + + + + + + + + true + + + + Device will be always in learning mode. After send command with key code device stays in the learning + mode + + + + Only for Tuya Codes Library: Decoding parameter + true + + + + Only for Tuya Codes Library: Send delay + 300 + true + + + + Only for Tuya Codes Library: Code library label + 0 + true + + + + DP number for study key. Uses for receive key code in learning mode + 2 + true + + + +