Skip to content

Commit

Permalink
[tuya] Add support for IR controller (#501)
Browse files Browse the repository at this point in the history
Add support for IR controller

Signed-off-by: Dmitry Pyatykh <dimonich110@gmail.com>
  • Loading branch information
d51x committed Jul 23, 2023
1 parent 8945291 commit ea79085
Show file tree
Hide file tree
Showing 8 changed files with 651 additions and 5 deletions.
59 changes: 59 additions & 0 deletions bundles/org.smarthomej.binding.tuya/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<Integer, String> dpToChannelId = new HashMap<>();
Expand Down Expand Up @@ -128,7 +137,6 @@ public void processDeviceStatus(Map<Integer, Object> deviceStatus) {
if (tuyaDevice != null) {
tuyaDevice.set(commandRequest);
}

return;
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
}

Expand Down Expand Up @@ -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
Expand All @@ -328,6 +389,7 @@ public void dispose() {
tuyaDevice.dispose();
this.tuyaDevice = null;
}
irStopLearning();
}

@Override
Expand Down Expand Up @@ -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<CommandOption> toCommandOptionList(List<String> options) {
Expand All @@ -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<Integer, @Nullable Object> commandRequest = new HashMap<>();
commandRequest.put(1, "study_exit");
TuyaDevice tuyaDevice = this.tuyaDevice;
if (!commandRequest.isEmpty() && tuyaDevice != null) {
tuyaDevice.set(commandRequest);
}
}

private void repeatStudyCode() {
Map<Integer, @Nullable Object> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit ea79085

Please sign in to comment.