Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tuya] Add support for IR controller #501

Merged
merged 16 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that automatically added by discovery?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be
as i know, there are two types of ir controllers - old version (works via dp201) and new version (works via dp1,2,3,4,7,10,13)
How automaticaly detects it, I don't know

and new version can work in two modes - DIY (now I can convert base64 tuya key code to nec format) and Tuya Library Mode, where key code consists from head and other data


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
Loading