Tasmota/tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu_v2.ino

2590 lines
94 KiB
C++

/*
xdrv_16_tuyamcu.ino - Tuya MCU support for Tasmota
Copyright (C) 2021 Federico Leoni, digiblur, Joel Stein and Theo Arends
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef USE_LIGHT
#ifdef USE_TUYA_MCU_V2
/*********************************************************************************************\
* Tuya MCU V2
rework 2022-04-03 SH:
the intent of the rework is:
1/ make the protocol parsing more robust.
to this end, it now observes a byte timeout on receive, and looks for 55AA rather than just 55 to reset.
Parsing has been split from message processing for better readability.
2/ try to observe to 'tuya' state machine as per thier documentation.
- at least for startup.
ALL sends should originate from the state machine (except manual sends?).
ALL sends which need a return (synchronous) wait for an expected command before
the next command can be sent. (commands were observed being dropped by the MCU).
3/ sending of DP data is now 'requested', and only sent if the DP value is
different to the one the MCU has (fixes an issue with Athom dimmer).
(data is stored in DPStore[], and serviced in the state machine)
\*********************************************************************************************/
#define XDRV_16 16
#define XNRG_32 32 // Needs to be the last XNRG_xx
#ifndef TUYA_DIMMER_ID
#define TUYA_DIMMER_ID 0
#endif
//#define TUYA_MORE_DEBUG
/*********************************************************************************************/
#define TUYA_CMD_HEARTBEAT 0x00
#define TUYA_CMD_QUERY_PRODUCT 0x01
#define TUYA_CMD_MCU_CONF 0x02
#define TUYA_CMD_WIFI_STATE 0x03
#define TUYA_CMD_WIFI_RESET 0x04
#define TUYA_CMD_WIFI_SELECT 0x05
#define TUYA_CMD_SET_DP 0x06
#define TUYA_CMD_STATE 0x07
#define TUYA_CMD_QUERY_STATE 0x08
#define TUYA_CMD_INITIATING_UPGRADE 0x0A // not implemented
#define TUYA_CMD_UPGRADE_PACKAGE 0x0B // not implemented
// MCU sends to request a 0x0c return of GMT
#define TUYA_CMD_GET_TIME_GMT 0x0C // not implemented
// MCU sends
#define TUYA_CMD_TEST_WIFI 0x0E // not implemented
// MCU sends
#define TUYA_CMD_GET_MEMORY 0x0F // not implemented
// MCU sends to request a 0x1c return of Local Time
// we send unsolicited??
#define TUYA_CMD_SET_TIME 0x1C
// MCU Sends, we must respond with 0x22, len1 00 failure, 01 success
#define TUYA_CMD_STATUS_SYNC 0x22 // not implemented
#define TUYA_CMD_STATUS_SYNC_RES 0x23 // not implemented
// From MCU. Response should be 0x0b 00/01/02
// subcmd 03 -> request weather data
// subcmd 06 -> get map id
#define TUYA_CMD_REPORT_STATUS_RECORD_TYPE 0x34 // not implemented
// MCU sends 'after 01 bute before 02'. Response should be 0x37, len 2, 00, 00/01/02
// subcmd 01->file download notification
#define TUYA_CMD_NEW_FEATURES 0x37 // not implemented
// MCU sends
#define TUYA_CMD_ENABLE_WEATHER 0x20 // not implemented
// we send every 30 mins, if enabled, MCU acks with 0x21
#define TUYA_CMD_SEND_WEATHER 0x21 // not implemented
// MCU sends, response 0x24, len 1, -ve dB or 0
#define TUYA_CMD_GET_WIFI_STRENGTH 0x24 // not implemented
// MCU sends, response 0x25
#define TUYA_CMD_DISABLE_HEARTBEAT 0x25 // not implemented
// MCU sends JSON, response 0x2A, len1 00/01/02/03
#define TUYA_CMD_SERIAL_PAIRING 0x2A // not implemented
#define TUYA_CMD_VACUUM_MAP_STREAMING 0x28 // not implemented
#define TUYA_CMD_VACUUM_MAP_STREAMING_MULTIPLE 0x30 // not implemented
// MCU sends, response 0x2D
#define TUYA_CMD_GET_MAC 0x2D // not implemented
// MCU sends, response 0x2E
#define TUYA_CMD_IR_STATUS 0x2E // not implemented
// MCU sends, response 0x2E
#define TUYA_CMD_IR_TEST 0x2F // not implemented
// We send, response 0x33
// uses subcommands, 01->learning, 02->data, 03->report.
#define TUYA_CMD_RF 0x33 // not implemented
#define TUYA_LOW_POWER_CMD_WIFI_STATE 0x02
#define TUYA_LOW_POWER_CMD_WIFI_RESET 0x03
#define TUYA_LOW_POWER_CMD_WIFI_CONFIG 0x04
#define TUYA_LOW_POWER_CMD_STATE 0x05
#define TUYA_TYPE_RAW 0x00
#define TUYA_TYPE_BOOL 0x01
#define TUYA_TYPE_VALUE 0x02
#define TUYA_TYPE_STRING 0x03
#define TUYA_TYPE_ENUM 0x04
// limit to what we store for DPs of type string
#define TUYA_MAX_STRING_SIZE 16
#define TUYA_BUFFER_SIZE 256
#define TUYA_BYTE_TIMEOUT_MS 500
#define TUYAREAD32FROMPTR(x) (((uint8_t*)x)[0] << 24 | ((uint8_t*)x)[1] << 16 | ((uint8_t*)x)[2] << 8 | ((uint8_t*)x)[3])
enum {
TUYA_STARTUP_STATE_INIT = 0,
TUYA_STARTUP_STATE_WAIT_ACK_INIT, // 1
TUYA_STARTUP_STATE_PRODUCT, //2
TUYA_STARTUP_STATE_WAIT_ACK_PRODUCT, // 3
TUYA_STARTUP_STATE_WAIT_OPTIONAL_NEW_FEATURES, // 4
TUYA_STARTUP_STATE_CONF, // 5
TUYA_STARTUP_STATE_WAIT_ACK_CONF, //6
TUYA_STARTUP_STATE_WIFI_STATE, //7
TUYA_STARTUP_STATE_WAIT_ACK_WIFI,//8
TUYA_STARTUP_STATE_QUERY_STATE,//9
TUYA_STARTUP_STATE_WAIT_ACK_QUERY,//10
TUYA_STARTUP_STATE_SEND_CMD,//11
TUYA_STARTUP_STATE_WAIT_ACK_CMD,//12
TUYA_STARTUP_STATE_SEND_HEARTBEAT,//13
TUYA_STARTUP_STATE_WAIT_ACK_HEARTBEAT,//14
};
#include <TasmotaSerial.h>
TasmotaSerial *TuyaSerial = nullptr;
#define TUYA_MAX_STORED_DPs 10
typedef struct TUYA_DP_STORE_tag {
uint8_t DPid;
uint8_t Type;
uint8_t rxedValueLen;
uint8_t desiredValueLen;
// NOTE - THESE MUST BE 32 bit ALIGNED, hence uint32_t
// DPValues are changed by every TUYA_CMD_STATE
uint32_t rxedValue[(TUYA_MAX_STRING_SIZE+3)/4];
// desired DPValues are set, and toSet is set to request
uint32_t desiredValue[(TUYA_MAX_STRING_SIZE+3)/4];
// set to 1 if desired value changed
uint8_t toSet;
// flag to indicate we saw it at least once from MCU.
uint8_t rxed;
} TUYA_DP_STORE;
typedef struct TUYA_STRUCT_tag {
// variables to handle setting of a DP.
// DPIds are filled out by initial TUYA_CMD_QUERY_STATE
// or by an easrly set request.
TUYA_DP_STORE DPStore[TUYA_MAX_STORED_DPs];
uint8_t numRxedDPids;
// set to indicate the command which is an ack to the last sent command.
// e.g. 7 is ack to 6 and 8
// if state is TUYA_STARTUP_STATE_WAIT_ACK_CMD
uint8_t expectedResponseCmd;
// uint8_t rxedDPids[TUYA_MAX_STORED_DPs];
// uint8_t rxedDPidType[TUYA_MAX_STORED_DPs];
// DPValues are changed by every TUYA_CMD_STATE
// uint32_t rxedDPvalues[TUYA_MAX_STORED_DPs];
// DPValues are changed by every TUYA_CMD_STATE
// uint32_t desiredDPvalues[TUYA_MAX_STORED_DPs];
// set to 1 if desired value changed
// uint8_t toSet[TUYA_MAX_STORED_DPs];
// set to 1 if a DP is to be sent
uint8_t requestSend;
uint16_t Levels[5]; // Array to store the values of TuyaMCU channels
uint16_t Snapshot[5]; // Array to store a snapshot of Tasmota actual channel values
uint16_t EnumState[4]; // Array to store up to four Enum type 4 values
char RGBColor[7]; // Stores RGB Color string in Hex format
uint16_t CTMin; // Minimum CT level allowed - When SetOption82 is enabled will default to 200
uint16_t CTMax; // Maximum CT level allowed - When SetOption82 is enabled will default to 380
int16_t Sensors[14]; // Stores the values of Sensors connected to the Tuya Device
bool ModeSet; // Controls 0 - Single Tone light, 1 - RGB Light
bool SensorsValid[14]; // Bool used for nullify the sensor value until a real value is received from the MCU
bool SuspendTopic; // Used to reduce the load at init time or when polling the configuraton on demand
bool ignore_dim; // Flag to skip serial send to prevent looping when processing inbound states from the faceplate interaction
uint32_t ignore_topic_timeout; // Suppress the /STAT topic (if enabled) to avoid data overflow until the configuration is over
uint8_t cmd_status; // Current status of serial-read
uint8_t cmd_checksum; // Checksum of tuya command
uint8_t data_len; // Data lenght of command
uint8_t wifi_state; // Keep MCU wifi-status in sync with WifiState()
uint8_t heartbeat_timer; // 10 second heartbeat timer for tuya module
// flasg which trigger sends of certain messages
uint8_t send_heartbeat; // trigger heartbeat when we can next send.
uint8_t send_time; // trigger time send when we can in running mode.
#ifdef USE_ENERGY_SENSOR
uint32_t lastPowerCheckTime; // Time when last power was checked
#endif // USE_ENERGY_SENSOR
unsigned char buffer[TUYA_BUFFER_SIZE]; // Serial receive buffer
int byte_counter; // Index in serial receive buffer
uint32_t ignore_dimmer_cmd_timeout; // Time until which received dimmer commands should be ignored
bool low_power_mode; // Normal or Low power mode protocol
bool send_success_next_second; // Second command success in low power mode
bool active;
int timeout; // command timeout in ms
// every time we get a long press, add 10000. If we get to 20000, then go to wifimanager mode, if enabled
// decremented by 1000 each second.
int wifiTimer;
char inStateMachine;
char startup_state;
char timeout_state; // state to go to if timeout.
unsigned char lastByte;
unsigned int lastByteTime; // time of last byte receipt.
unsigned int errorcnt; // increment every time something goes awry
unsigned int lasterrorcnt; // used to choose when to log errorcnt
int dimDelay_ms[2]; // SIGNED the delay after a dim result after which we are allowed to send a dim value
int defaultDimDelay_ms; // the delay after a dim result after which we are allowed to send a dim value
uint8_t dimCmdEnable; // we are allowed to send a dim command - bitfield
uint8_t dimDebug; // enables a single dim debug - bitfield
uint8_t last_button;
int sends;
int rxs;
} TUYA_STRUCT;
TUYA_STRUCT *pTuya = (TUYA_STRUCT *)0;
//void TuyaSendCmd(uint8_t cmd, uint8_t payload[] = nullptr, uint16_t payload_len = 0);
void TuyaSendState(uint8_t id, uint8_t type, uint8_t* value, int len);
int init_tuya_struct() {
if (pTuya) return 0; // done already
pTuya = (TUYA_STRUCT *)calloc(sizeof(TUYA_STRUCT), 1);
if (!pTuya) return 0;
strcpy(pTuya->RGBColor, "000000"); // Stores RGB Color string in Hex format
pTuya->CTMin = 153; // Minimum CT level allowed - When SetOption82 is enabled will default to 200
pTuya->CTMax = 500; // Maximum CT level allowed - When SetOption82 is enabled will default to 380
pTuya->wifi_state = -2; // Keep MCU wifi-status in sync with WifiState()
pTuya->defaultDimDelay_ms = 2000; // 2s delay from a power command to a dim command, or from an rxed dim command to sending one.
#ifdef TUYA_MORE_DEBUG
AddLog(LOG_LEVEL_INFO, PSTR("TYA: Init struct done"));
#endif
return 1;
}
#define D_JSON_TUYA_MCU_RECEIVED "TuyaReceived"
#define D_PRFX_TUYA "Tuya"
#define D_CMND_TUYA_MCU "MCU"
#define D_CMND_TUYA_MCU_SEND_STATE "Send"
#define D_CMND_TUYARGB "RGB"
#define D_CMND_TUYA_ENUM "Enum"
#define D_CMND_TUYA_ENUM_LIST "EnumList"
// #define D_CMND_TUYA_SET_TEMP "SetTemp"
// #define D_CMND_TUYA_SET_HUM "SetHum"
// #define D_CMND_TUYA_SET_TIMER "SetTimer"
const char kTuyaSensors[] PROGMEM = // List of available sensors (can be expanded in the future)
// 71 72 73 74 75
"" D_JSON_TEMPERATURE "|TempSet|" D_JSON_HUMIDITY "|HumSet|" D_JSON_ILLUMINANCE
// 76 77 78 79 80 81 82 83 84
"|" D_JSON_TVOC "|" D_JSON_ECO2 "|" D_JSON_CO2 "|" D_JSON_GAS "|" D_ENVIRONMENTAL_CONCENTRATION "||Timer1|Timer2|Timer3|TImer4";
const char kTuyaCommand[] PROGMEM = D_PRFX_TUYA "|" // Prefix
D_CMND_TUYA_MCU "|" D_CMND_TUYA_MCU_SEND_STATE "|" D_CMND_TUYARGB "|" D_CMND_TUYA_ENUM "|" D_CMND_TUYA_ENUM_LIST "|TempSetRes|DimDelay";
void (* const TuyaCommand[])(void) PROGMEM = {
&CmndTuyaMcu, &CmndTuyaSend, &CmndTuyaRgb, &CmndTuyaEnum, &CmndTuyaEnumList, &CmndTuyaTempSetRes, &CmdTuyaSetDimDelay
};
const uint8_t TuyaExcludeCMDsFromMQTT[] PROGMEM = { // don't publish this received commands via MQTT if SetOption66 and SetOption137 is active (can be expanded in the future)
TUYA_CMD_HEARTBEAT, TUYA_CMD_WIFI_STATE, TUYA_CMD_SET_TIME, TUYA_CMD_UPGRADE_PACKAGE
};
/*********************************************************************************************\
* Web Interface
\*********************************************************************************************/
bool IsModuleTuya(void) {
if (!pTuya) return false;
bool is_tuya = pTuya->active;
//#ifdef ESP8266
// This is not a Tuya driven device. It uses a Tuya provided ESP8266. Why it was here is a mystery to me.
// if (SK03_TUYA == TasmotaGlobal.module_type) {
// is_tuya = true;
// }
//#endif
return is_tuya;
}
bool AsModuleTuyaMS(void) // ModeSet Layout
{
return ((TasmotaGlobal.light_type > LT_RGB) && TuyaGetDpId(TUYA_MCU_FUNC_MODESET) != 0);
}
bool TuyaModeSet(void) // ModeSet Status
{
if (!pTuya) return false;
return pTuya->ModeSet;
}
/*********************************************************************************************\
* Web Interface
\*********************************************************************************************/
/*
TuyaSend<x> dpId,data
TuyaSend0 -> Sends TUYA_CMD_QUERY_STATE
TuyaSend1 11,1 -> Sends boolean (Type 1) data 0/1 to dpId 11 (Max data length 1 byte)
TuyaSend2 11,100 -> Sends integer (Type 2) data 100 to dpId 11 (Max data length 4 bytes)
TuyaSend2 11,0xAABBCCDD -> Sends 4 bytes (Type 2) data to dpId 11 (Max data length 4 bytes)
TuyaSend3 11,ThisIsTheData -> Sends the supplied string (Type 3) to dpId 11 ( Max data length not-known)
TuyaSend4 11,1 -> Sends enum (Type 4) data 1 to dpId 11 (Max data length 1 bytes)
TuyaSend5 11,ABCD -> Sends an HEX string (Type 3) data to dpId
TuyaSend6 11,ABCD -> Sends raw (Type 0) data to dpId
TuyaSend8 -> Sends TUYA_CMD_QUERY_PRODUCT ?
*/
void CmndTuyaSend(void) {
if (!pTuya) return;
switch(XdrvMailbox.index){
case 0:
TuyaRequestState(0);
break;
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
{
if (XdrvMailbox.data_len > 0) {
char *p;
const char *data = "";
uint8_t i = 0;
uint8_t dpId = 0;
for (char *str = strtok_r(XdrvMailbox.data, ", ", &p); str && i < 2; str = strtok_r(nullptr, ", ", &p)) {
if ( i == 0) {
// note: can be a number, or 0xnn for hex because base is 0
dpId = strtoul(str, nullptr, 0);
} else {
data = str;
}
i++;
}
if (1 == XdrvMailbox.index) {
TuyaSendBool(dpId, strtoul(data, nullptr, 0));
} else if (2 == XdrvMailbox.index) {
TuyaSendValue(dpId, strtoull(data, nullptr, 0));
} else if (3 == XdrvMailbox.index) {
TuyaSendString(dpId, data);
} else if (5 == XdrvMailbox.index) {
TuyaSendHexString(dpId, data);
} else if (4 == XdrvMailbox.index) {
TuyaSendEnum(dpId, strtoul(data, nullptr, 0));
} else if (6 == XdrvMailbox.index) {
TuyaSendRaw(dpId, data);
} else if (7 == XdrvMailbox.index) {
uint8_t cmd = dpId;
// send ANY cmd with payload from hex string
// calculates length and checksum for you.
// like "0," to send a heartbeat, "3,4" to set wifi led mode,
// "0x1c,0110041305060702" - set local time
// sends immediately....
TuyaSendRawCmd(cmd, data);
}
}
} break;
case 8:
// product info
TuyaRequestState(8);
break;
case 9:
Settings->tuyamcu_topic = !Settings->tuyamcu_topic;
AddLog(LOG_LEVEL_INFO, PSTR("TYA: TuyaMCU Stat Topic %s"), (Settings->tuyamcu_topic ? PSTR("enabled") : PSTR("disabled")));
break;
}
ResponseCmndDone();
}
void CmdTuyaSetDimDelay(void) {
if (!pTuya) return;
switch(XdrvMailbox.index){
case 1: {
if (XdrvMailbox.data_len > 0) {
int32_t delay = strtol(XdrvMailbox.data, nullptr, 0);
pTuya->defaultDimDelay_ms = delay;
} else {
// report only
Response_P(PSTR("{\"%s\":{\"dimdelay\":%d}}"), XdrvMailbox.command, pTuya->defaultDimDelay_ms); // Builds TuyaMCU
return;
}
} break;
default: // no response
return;
}
ResponseCmndDone();
}
// TuyaMcu fnid,dpid
void CmndTuyaMcu(void) {
if (!pTuya) return;
if (XdrvMailbox.data_len > 0) {
char *p;
uint8_t i = 0;
uint8_t parm[3] = { 0 };
for (char *str = strtok_r(XdrvMailbox.data, ", ", &p); str && i < 2; str = strtok_r(nullptr, ", ", &p)) {
parm[i] = strtoul(str, nullptr, 0);
i++;
}
if (TuyaFuncIdValid(parm[0])) {
bool DualDim;
if (TUYA_MCU_FUNC_DIMMER2 == parm[0] && parm[1] != 0) {
if (TuyaGetDpId(TUYA_MCU_FUNC_DIMMER) != 0) { DualDim = true; }
} else if (TUYA_MCU_FUNC_DIMMER == parm[0] && parm[1] != 0) {
if (TuyaGetDpId(TUYA_MCU_FUNC_DIMMER2) != 0) { DualDim = true; }
} else if ((TUYA_MCU_FUNC_DIMMER == parm[0] && parm[1] == 0) || (TUYA_MCU_FUNC_DIMMER2 == parm[0] && parm[1] == 0)) { DualDim = false; };
if (DualDim) { // If the second dimmer is enabled CT, RGB or WHITE function must be removed
if (TuyaGetDpId(TUYA_MCU_FUNC_CT) != 0) { TuyaAddMcuFunc(TUYA_MCU_FUNC_CT, 0); }
if (TuyaGetDpId(TUYA_MCU_FUNC_RGB) != 0) { TuyaAddMcuFunc(TUYA_MCU_FUNC_RGB, 0); }
if (TuyaGetDpId(TUYA_MCU_FUNC_WHITE) != 0) { TuyaAddMcuFunc(TUYA_MCU_FUNC_WHITE, 0); }
Settings->flag3.pwm_multi_channels = 1;
} else { Settings->flag3.pwm_multi_channels = 0; }
TuyaAddMcuFunc(parm[0], parm[1]);
TasmotaGlobal.restart_flag = 2;
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: TuyaMcu Invalid function id=%d"), parm[0]);
}
}
Response_P(PSTR("{\"%s\":["), XdrvMailbox.command); // Builds TuyaMCU
bool added = false;
for (uint8_t i = 0; i < MAX_TUYA_FUNCTIONS; i++) {
if (Settings->tuya_fnid_map[i].fnid != 0) {
if (added) {
ResponseAppend_P(PSTR(","));
}
ResponseAppend_P(PSTR("{\"fnId\":%d,\"dpId\":%d}" ), Settings->tuya_fnid_map[i].fnid, Settings->tuya_fnid_map[i].dpid);
added = true;
}
}
ResponseAppend_P(PSTR("]}"));
}
void CmndTuyaRgb(void) { // Command to control the RGB format
if (!pTuya) return;
uint16_t payload = XdrvMailbox.payload;
if (XdrvMailbox.data_len > 0) {
if (payload < 0 || payload > 3 || TuyaGetDpId(TUYA_MCU_FUNC_RGB) == 0) {
return;
} else {
if (payload != Settings->tuya_fnid_map[230].dpid) { // fnid 230 is reserved for RGB
Settings->tuya_fnid_map[230].fnid = 230;
Settings->tuya_fnid_map[230].dpid = payload;
}
}
}
ResponseCmndNumber(Settings->tuya_fnid_map[230].dpid);
}
void CmndTuyaTempSetRes(void)
{
if (!pTuya) return;
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 3)) {
Settings->mbflag2.temperature_set_res = XdrvMailbox.payload;
}
ResponseCmndNumber(Settings->mbflag2.temperature_set_res);
}
void CmndTuyaEnum(void) { // Command to control up to four type 4 Enum
if (!pTuya) return;
uint16_t EnumIdx = XdrvMailbox.index;
int32_t payload = XdrvMailbox.payload;
if (EnumIdx > 4 || TuyaGetDpId(TUYA_MCU_FUNC_ENUM1 + (EnumIdx-1)) == 0) {
return;
}
if (XdrvMailbox.data_len > 0) {
if (payload < 0 || payload > Settings->tuya_fnid_map[EnumIdx + 230].dpid ) {
return;
} else {
if (payload != pTuya->EnumState[EnumIdx-1]) {
pTuya->EnumState[EnumIdx-1] = payload;
TuyaSendEnum(TuyaGetDpId(TUYA_MCU_FUNC_ENUM1 + (EnumIdx-1)), payload);
}
ResponseCmndIdxNumber(pTuya->EnumState[EnumIdx-1]);
}
} else {
Response_P(PSTR("{\"%s\":{"), XdrvMailbox.command); // Builds TuyaEnum
bool added = false;
for (uint8_t i = 0; i <= 3; i++) {
if (TuyaGetDpId(TUYA_MCU_FUNC_ENUM1 + i) != 0) {
if (added) {
ResponseAppend_P(PSTR(","));
}
ResponseAppend_P(PSTR("\"Enum%d\":%d"), i + 1, pTuya->EnumState[i]); // Returns the actual values of Enum as list
added = true;
}
}
ResponseAppend_P(PSTR("}}"));
}
}
void CmndTuyaEnumList(void) { // Command to declare the number of items in list for up to four type 4 enum. Min = 0, Max = 31, Default = 0
if (!pTuya) return;
if (XdrvMailbox.data_len > 0) {
char *p;
uint8_t i = 0;
uint8_t parm[3] = { 0 };
for (char *str = strtok_r(XdrvMailbox.data, ", ", &p); str && i < 2; str = strtok_r(nullptr, ", ", &p)) {
parm[i] = strtoul(str, nullptr, 0);
i++;
}
if ((parm[0] >= 1 && parm[0] <= 4) && (parm[1] >= 1 && parm[1] <= 31)) {
uint16_t idx = parm[0] + 230; // fnid 231, 232, 233 and 234 are reserved for enum
Settings->tuya_fnid_map[idx].fnid = idx;
Settings->tuya_fnid_map[idx].dpid = parm[1];
}
}
if ((TuyaGetDpId(TUYA_MCU_FUNC_ENUM1) != 0) || (TuyaGetDpId(TUYA_MCU_FUNC_ENUM3) != 0) ||
(TuyaGetDpId(TUYA_MCU_FUNC_ENUM3) != 0) || (TuyaGetDpId(TUYA_MCU_FUNC_ENUM4) != 0)) {
Response_P(PSTR("{\"%s\":{"), XdrvMailbox.command); // Builds TuyaEnumList
bool added = false;
for (uint8_t i = 0; i <= 3; i++) {
if (TuyaGetDpId(TUYA_MCU_FUNC_ENUM1 + i) != 0) {
if (added) {
ResponseAppend_P(PSTR(","));
if ( Settings->tuya_fnid_map[i + 231].dpid > 31 ) { Settings->tuya_fnid_map[i + 231].dpid = 0; } // default to 0 it the value exceed the range
}
ResponseAppend_P(PSTR("\"Enum%d\":%d"), i + 1, Settings->tuya_fnid_map[i + 231].dpid); // fnid 231, 232, 233 and 234 are reserved for Enum
added = true;
}
}
ResponseAppend_P(PSTR("}}"));
} else { return; }
}
int StrCmpNoCase(char const *Str1, char const *Str2) // Compare case sensistive RGB strings
{
for (;; Str1++, Str2++) {
int StrCmp = tolower((unsigned char)*Str1) - tolower((unsigned char)*Str2);
if (StrCmp != 0 || !*Str1) { return StrCmp; }
}
}
float TuyaAdjustedTemperature(int16_t packetValue, uint8_t res)
{
switch (res)
{
case 1:
return (float)packetValue / 10.0;
break;
case 2:
return (float)packetValue / 100.0;
break;
case 3:
return (float)packetValue / 1000.0;
break;
default:
return (float)packetValue;
break;
}
}
/*********************************************************************************************\
* Internal Functions
\*********************************************************************************************/
void TuyaAddMcuFunc(uint8_t fnId, uint8_t dpId) {
bool added = false;
if (fnId == 0 || dpId == 0) { // Delete entry
for (uint8_t i = 0; i < MAX_TUYA_FUNCTIONS; i++) {
if ((dpId > 0 && Settings->tuya_fnid_map[i].dpid == dpId) || (fnId > TUYA_MCU_FUNC_NONE && Settings->tuya_fnid_map[i].fnid == fnId)) {
Settings->tuya_fnid_map[i].fnid = TUYA_MCU_FUNC_NONE;
Settings->tuya_fnid_map[i].dpid = 0;
break;
}
}
} else { // Add or update
for (uint8_t i = 0; i < MAX_TUYA_FUNCTIONS; i++) {
if (Settings->tuya_fnid_map[i].dpid == dpId || Settings->tuya_fnid_map[i].dpid == 0 || Settings->tuya_fnid_map[i].fnid == fnId || Settings->tuya_fnid_map[i].fnid == 0) {
if (!added) { // Update entry if exisiting entry or add
Settings->tuya_fnid_map[i].fnid = fnId;
Settings->tuya_fnid_map[i].dpid = dpId;
added = true;
} else if (Settings->tuya_fnid_map[i].dpid == dpId || Settings->tuya_fnid_map[i].fnid == fnId) { // Remove existing entry if added to empty place
Settings->tuya_fnid_map[i].fnid = TUYA_MCU_FUNC_NONE;
Settings->tuya_fnid_map[i].dpid = 0;
}
}
}
}
UpdateDevices();
}
void UpdateDevices() {
for (uint8_t i = 0; i < MAX_TUYA_FUNCTIONS; i++) {
uint8_t fnId = Settings->tuya_fnid_map[i].fnid;
if (fnId > TUYA_MCU_FUNC_NONE && Settings->tuya_fnid_map[i].dpid > 0) {
if (fnId >= TUYA_MCU_FUNC_REL1 && fnId <= TUYA_MCU_FUNC_REL8) { //Relay
bitClear(TasmotaGlobal.rel_inverted, fnId - TUYA_MCU_FUNC_REL1);
} else if (fnId >= TUYA_MCU_FUNC_REL1_INV && fnId <= TUYA_MCU_FUNC_REL8_INV) { // Inverted Relay
bitSet(TasmotaGlobal.rel_inverted, fnId - TUYA_MCU_FUNC_REL1_INV);
}
}
}
}
inline bool TuyaFuncIdValid(uint8_t fnId) {
return (fnId >= TUYA_MCU_FUNC_SWT1 && fnId <= TUYA_MCU_FUNC_SWT4) ||
(fnId >= TUYA_MCU_FUNC_REL1 && fnId <= TUYA_MCU_FUNC_REL8) ||
(fnId >= TUYA_MCU_FUNC_DIMMER && fnId <= TUYA_MCU_FUNC_REPORT2) ||
(fnId >= TUYA_MCU_FUNC_POWER && fnId <= TUYA_MCU_FUNC_POWER_TOTAL) ||
(fnId >= TUYA_MCU_FUNC_REL1_INV && fnId <= TUYA_MCU_FUNC_REL8_INV) ||
(fnId >= TUYA_MCU_FUNC_ENUM1 && fnId <= TUYA_MCU_FUNC_ENUM4) ||
(fnId >= TUYA_MCU_FUNC_MOTOR_DIR && fnId <= TUYA_MCU_FUNC_DUMMY) ||
(fnId == TUYA_MCU_FUNC_LOWPOWER_MODE) ||
(fnId >= TUYA_MCU_FUNC_TEMP && fnId <= TUYA_MCU_FUNC_HUMSET) ||
(fnId >= TUYA_MCU_FUNC_LX && fnId <= TUYA_MCU_FUNC_PM25) ||
(fnId >= TUYA_MCU_FUNC_TIMER1 && fnId <= TUYA_MCU_FUNC_TIMER4);
}
uint8_t TuyaGetFuncId(uint8_t dpid) {
for (uint8_t i = 0; i < MAX_TUYA_FUNCTIONS; i++) {
if (Settings->tuya_fnid_map[i].dpid == dpid) {
return Settings->tuya_fnid_map[i].fnid;
}
}
return TUYA_MCU_FUNC_NONE;
}
uint8_t TuyaGetDpId(uint8_t fnId) {
for (uint8_t i = 0; i < MAX_TUYA_FUNCTIONS; i++) {
if (Settings->tuya_fnid_map[i].fnid == fnId) {
return Settings->tuya_fnid_map[i].dpid;
}
}
return 0;
}
uint8_t TuyaFnIdIsDimmer(uint8_t fnId){
//TUYA_MCU_FUNC_DIMMER = 21, TUYA_MCU_FUNC_DIMMER2, TUYA_MCU_FUNC_CT, TUYA_MCU_FUNC_RGB, TUYA_MCU_FUNC_WHITE,
if ((fnId >= TUYA_MCU_FUNC_DIMMER) &&
(fnId <= TUYA_MCU_FUNC_WHITE)){
return 1;
}
return 0;
}
uint8_t TuyaDpIdIsDimmer(uint8_t dpId){
uint8_t fnId = TuyaGetFuncId(dpId);
return TuyaFnIdIsDimmer(fnId);
}
// pTuya->timeout hit zero
void Tuya_timeout(){
// timeout_state should have been set to the correct state to go to after the timeout
if (pTuya->startup_state != TUYA_STARTUP_STATE_WAIT_ACK_QUERY){
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Protocol timeout state %d -> %d"), pTuya->startup_state, pTuya->timeout_state);
}
pTuya->startup_state = pTuya->timeout_state;
pTuya->timeout = 0;
}
void TuyaSendCmd(uint8_t cmd, uint8_t payload[] = nullptr, uint16_t payload_len = 0, int noerror = 0)
{
if (!pTuya->inStateMachine && !noerror){
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: TuyaSendCmd should only be called from within the state machine! - if this was a manual command, then ok."));
}
// include ver = 0 in checksum for completeness
uint8_t checksum = (0xFF + 0x00 + cmd + (payload_len >> 8) + (payload_len & 0xFF));
TuyaSerial->write(0x55); // Tuya header 55AA
TuyaSerial->write(0xAA);
TuyaSerial->write((uint8_t)0x00); // version 00 - send TO MCU
TuyaSerial->write(cmd); // Tuya command
TuyaSerial->write(payload_len >> 8); // following data length (Hi)
TuyaSerial->write(payload_len & 0xFF); // following data length (Lo)
char log_data[700]; // Was MAX_LOGSZ
snprintf_P(log_data, sizeof(log_data), PSTR("T:>\"55aa00%02x%02x%02x"), cmd, payload_len >> 8, payload_len & 0xFF);
for (uint32_t i = 0; i < payload_len; ++i) {
TuyaSerial->write(payload[i]);
checksum += payload[i];
snprintf_P(log_data, sizeof(log_data), PSTR("%s%02x"), log_data, payload[i]);
}
TuyaSerial->write(checksum);
TuyaSerial->flush();
snprintf_P(log_data, sizeof(log_data), PSTR("%s%02x\""), log_data, checksum);
AddLogData(LOG_LEVEL_DEBUG, log_data);
pTuya->sends ++;
}
// note: normally regularly called with defaults
// ONLY called with cmd, len, data from an incoming command with ver 03.
// - and this is ONLY used to recognise acks when they occur/are needed.
void Tuya_statemachine(int cmd = -1, int len = 0, unsigned char *payload = (unsigned char *)0) {
pTuya->inStateMachine = 1;
int state = pTuya->startup_state;
switch (pTuya->startup_state){
case TUYA_STARTUP_STATE_INIT://0
if (cmd >= 0) break;
TuyaSendCmd(TUYA_CMD_HEARTBEAT);
pTuya->timeout = 3000;
pTuya->timeout_state = TUYA_STARTUP_STATE_INIT;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_INIT;
break;
case TUYA_STARTUP_STATE_WAIT_ACK_INIT: // 1
if (cmd == TUYA_CMD_HEARTBEAT){
pTuya->timeout = 0;
pTuya->timeout_state = TUYA_STARTUP_STATE_INIT;
pTuya->startup_state = TUYA_STARTUP_STATE_PRODUCT;
}
break;
case TUYA_STARTUP_STATE_PRODUCT: //2
if (cmd >= 0) break;
TuyaSendCmd(TUYA_CMD_QUERY_PRODUCT);
pTuya->timeout = 1000;
pTuya->timeout_state = TUYA_STARTUP_STATE_INIT;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_PRODUCT;
break;
case TUYA_STARTUP_STATE_WAIT_ACK_PRODUCT: // 3
if (cmd == TUYA_CMD_QUERY_PRODUCT){
pTuya->timeout = 300; // Optional - we will wait 300ms for this after TUYA_CMD_QUERY_PRODUCT response
pTuya->timeout_state = TUYA_STARTUP_STATE_CONF; // move on at timeout.
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_OPTIONAL_NEW_FEATURES;
}
break;
case TUYA_STARTUP_STATE_WAIT_OPTIONAL_NEW_FEATURES: // 4
/* Optional - we will wait 300ms for this after TUYA_CMD_QUERY_PRODUCT response
After the device is powered on,
the MCU sends this command to notify the module of feature configuration
after the command 0x01 and before the command 0x02.*/
if (cmd == TUYA_CMD_NEW_FEATURES){
pTuya->timeout = 0;
pTuya->timeout_state = TUYA_STARTUP_STATE_CONF;
pTuya->startup_state = TUYA_STARTUP_STATE_CONF;
}
break;
case TUYA_STARTUP_STATE_CONF: // 5
if (cmd >= 0) break;
TuyaSendCmd(TUYA_CMD_MCU_CONF);
pTuya->timeout = 1000;
pTuya->timeout_state = TUYA_STARTUP_STATE_INIT;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_CONF;
break;
case TUYA_STARTUP_STATE_WAIT_ACK_CONF: //
if (cmd == TUYA_CMD_MCU_CONF){
if (len > 0){
pTuya->startup_state = TUYA_STARTUP_STATE_QUERY_STATE;
} else {
pTuya->startup_state = TUYA_STARTUP_STATE_WIFI_STATE;
}
pTuya->timeout = 0;
pTuya->timeout_state = TUYA_STARTUP_STATE_INIT;
}
break;
case TUYA_STARTUP_STATE_WIFI_STATE: {//
uint8_t t = 0x04;
if (cmd >= 0) break;
TuyaSendCmd(TUYA_CMD_WIFI_STATE, &t, 1);
pTuya->timeout = 1000;
pTuya->timeout_state = TUYA_STARTUP_STATE_INIT;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_WIFI;
} break;
case TUYA_STARTUP_STATE_WAIT_ACK_WIFI://
if (cmd == TUYA_CMD_WIFI_STATE){
pTuya->timeout = 0;
pTuya->timeout_state = TUYA_STARTUP_STATE_INIT;
pTuya->startup_state = TUYA_STARTUP_STATE_QUERY_STATE;
}
break;
case TUYA_STARTUP_STATE_QUERY_STATE://
if (cmd >= 0) break;
TuyaSendCmd(TUYA_CMD_QUERY_STATE);
pTuya->timeout = 1000;
pTuya->timeout_state = TUYA_STARTUP_STATE_INIT;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_QUERY;
break;
case TUYA_STARTUP_STATE_WAIT_ACK_QUERY://
if (cmd == TUYA_CMD_STATE){
// wait a further 500ms for the next command.
// only on timeout, move on to runtime
pTuya->timeout = 500;
pTuya->timeout_state = TUYA_STARTUP_STATE_SEND_CMD;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_QUERY;
}
break;
// we hit runtime.....
case TUYA_STARTUP_STATE_SEND_CMD://
if (pTuya->send_time & 1){
TuyaSetTime();
pTuya->send_time &= 0xfe;
// ??? TODO - if no MCU support, we'll just wait 100ms.
// we should *not* get a response - as this is normally a command which is requested
pTuya->expectedResponseCmd = TUYA_CMD_SET_TIME;
// always wait a bit before sending anything else, otherwise we may overflow the MCU input buffer.
pTuya->timeout = 200;
pTuya->timeout_state = TUYA_STARTUP_STATE_SEND_CMD;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_CMD;
break;
}
if (pTuya->wifi_state != TuyaGetTuyaWifiState()) {
pTuya->wifi_state = TuyaGetTuyaWifiState();
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Set WiFi LED %d (%d)"), pTuya->wifi_state, WifiState());
if (pTuya->low_power_mode) {
TuyaSendCmd(TUYA_LOW_POWER_CMD_WIFI_STATE, &pTuya->wifi_state, 1);
pTuya->expectedResponseCmd = TUYA_CMD_WIFI_STATE; // ??? TODO
pTuya->timeout = 300;
pTuya->timeout_state = TUYA_STARTUP_STATE_SEND_CMD;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_CMD;
break;
} else {
TuyaSendCmd(TUYA_CMD_WIFI_STATE, &pTuya->wifi_state, 1);
pTuya->expectedResponseCmd = TUYA_CMD_WIFI_STATE;
pTuya->timeout = 300;
pTuya->timeout_state = TUYA_STARTUP_STATE_SEND_CMD;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_CMD;
break;
}
}
if (pTuya->low_power_mode) {
if (pTuya->send_success_next_second) {
uint8_t success = 1;
TuyaSendCmd(TUYA_LOW_POWER_CMD_STATE, &success, 1);
pTuya->expectedResponseCmd = TUYA_LOW_POWER_CMD_STATE; // ?? TODO - if no resp, we'll wait 300ms
pTuya->timeout = 300;
pTuya->timeout_state = TUYA_STARTUP_STATE_SEND_CMD;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_CMD;
pTuya->send_success_next_second = false;
break;
}
//TuyaSendLowPowerSuccessIfNeeded();
}
// send any DP chang requests
// only if different from the received or last send values
int i;
for (i = 0; i < pTuya->numRxedDPids; i++){
TUYA_DP_STORE *dp = &pTuya->DPStore[i];
// if set requested, and MCU has reported at least once
if (dp->toSet && dp->rxed){
// if value is different
if ((dp->rxedValueLen != dp->desiredValueLen) || memcmp(dp->rxedValue, dp->desiredValue, dp->desiredValueLen)){
uint8_t send = 1;
if (TuyaDpIdIsDimmer(dp->DPid)){
uint8_t fnId = TuyaGetFuncId(dp->DPid);
uint8_t dimindex = 0;
if (fnId == TUYA_MCU_FUNC_DIMMER2){ // first dimmer
dimindex = 1;
}
// set every time a dim value is rxed, and if just turned on
if (pTuya->dimDelay_ms[dimindex]){
if (pTuya->dimDebug & (1<<dimindex)){
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Dim%d wait %d"), dimindex, pTuya->dimDelay_ms[dimindex]);
pTuya->dimDebug &= (0xff ^ (1<<dimindex));
}
send = 0;
}
// only enabled if on.
if (!(pTuya->dimCmdEnable & (1<<dimindex))){
if (pTuya->dimDebug & (1<<dimindex)){
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Dim%d disabled"), dimindex);
pTuya->dimDebug &= (0xff ^ (1<<dimindex));
}
send = 0;
}
}
if (send){
TuyaSendState(dp->DPid,
dp->Type,
(uint8_t*)dp->desiredValue, dp->desiredValueLen);
// assume it worked? maybe not
//dp->rxedValueLen = dp->desiredValueLen;
//memcpy(dp->rxedValue, dp->desiredValue, dp->desiredValueLen);
dp->toSet = 0;
pTuya->expectedResponseCmd = TUYA_CMD_STATE;
pTuya->timeout = 300;
pTuya->timeout_state = TUYA_STARTUP_STATE_SEND_CMD;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_CMD;
#ifdef TUYA_MORE_DEBUG
MqttPublishLoggingAsync(false);
SyslogAsync(false);
#endif
break;
}
} else {
// if equal values, ignore set
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: DPset ign-same value dp%d=0x%x,%d"), dp->DPid, dp->desiredValue[0], dp->desiredValueLen);
dp->toSet = 0;
}
}
}
// triggered from second timer
if (pTuya->send_heartbeat) {
pTuya->send_heartbeat = 0;
pTuya->startup_state = TUYA_STARTUP_STATE_SEND_HEARTBEAT;
break;
}
break;
case TUYA_STARTUP_STATE_WAIT_ACK_CMD:
if (cmd == pTuya->expectedResponseCmd){
pTuya->timeout = 0;
pTuya->timeout_state = TUYA_STARTUP_STATE_SEND_CMD;
pTuya->startup_state = TUYA_STARTUP_STATE_SEND_CMD;
}
break;
case TUYA_STARTUP_STATE_SEND_HEARTBEAT://
// send a heartbeat, and wait up to a second
if (cmd >= 0) break;
TuyaSendCmd(TUYA_CMD_HEARTBEAT);
pTuya->timeout = 1000;
pTuya->timeout_state = TUYA_STARTUP_STATE_INIT;
pTuya->startup_state = TUYA_STARTUP_STATE_WAIT_ACK_HEARTBEAT;
break;
// wait for heartbeat return
case TUYA_STARTUP_STATE_WAIT_ACK_HEARTBEAT://
if (cmd == TUYA_CMD_HEARTBEAT){
pTuya->timeout = 0;
pTuya->timeout_state = TUYA_STARTUP_STATE_SEND_CMD;
pTuya->startup_state = TUYA_STARTUP_STATE_SEND_CMD;
if (len > 0 && payload[0] == 0){
pTuya->startup_state = TUYA_STARTUP_STATE_INIT;
}
}
if (cmd == TUYA_CMD_STATE){
// dealt with in receive
}
break;
}
if (state != pTuya->startup_state){
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("TYA: %d->%d-%d/%d"), state, pTuya->startup_state, pTuya->sends, pTuya->rxs);
}
pTuya->inStateMachine = 0;
}
void TuyaDumpDPStore(){
#ifdef TUYA_MORE_DEBUG
for (int i = 0; i < pTuya->numRxedDPids; i++){
TUYA_DP_STORE *dp = &pTuya->DPStore[i];
/*
dp->DPid;
dp->Type;
dp->rxedValue[TUYA_MAX_STRING_SIZE];
dp->desiredValue[TUYA_MAX_STRING_SIZE];
dp->toSet;
dp->rxed;
*/
switch(dp->Type){
case TUYA_TYPE_RAW:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: DP%d T%d m:0x%X d0x%X s%d r%d"),
dp->DPid, dp->Type, dp->rxedValue[0], dp->desiredValue[0], dp->toSet, dp->rxed);
break;
case TUYA_TYPE_BOOL:
case TUYA_TYPE_ENUM:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: DP%d T%d m0x%X d0x%X s%d r%d"),
dp->DPid, dp->Type, dp->rxedValue[0], dp->desiredValue[0], dp->toSet, dp->rxed);
break;
case TUYA_TYPE_VALUE:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: DP%d T%d m%d d%d s%d r%d"),
dp->DPid, dp->Type, TUYAREAD32FROMPTR(dp->rxedValue), TUYAREAD32FROMPTR(dp->desiredValue), dp->toSet, dp->rxed);
break;
case TUYA_TYPE_STRING:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: DP%d T%d m0x%X d0x%X s%d r%d"),
dp->DPid, dp->Type, dp->rxedValue[0], dp->desiredValue[0], dp->toSet, dp->rxed);
break;
}
}
#endif
}
// sets a desired value for a DPId,
// and sets a flag to say we want this value.
// this is then serviced later to ensure we don't send the same value twice.
// this solves an issue with some wallplate touch dimmers
// which are killed by sending off when the device is already off.
void TuyaPostState(uint8_t id, uint8_t type, uint8_t *value, int len = 4){
int i;
if (!pTuya){
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: PostState before pTuya DP %d"), id);
return;
}
for (i = 0; i < pTuya->numRxedDPids; i++){
TUYA_DP_STORE *dp = &pTuya->DPStore[i];
/*
dp->DPid;
dp->Type;
dp->rxedValue[TUYA_MAX_STRING_SIZE];
dp->desiredValue[TUYA_MAX_STRING_SIZE];
dp->toSet;
dp->rxed;
*/
if (id == dp->DPid){
if (type == dp->Type){
if (len <= TUYA_MAX_STRING_SIZE){
memcpy(dp->desiredValue, value, len);
dp->desiredValueLen = len;
dp->toSet = 1;
pTuya->requestSend = 1;
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: DP%d des v set (0x%x,%d)"), id, dp->desiredValue[0], dp->desiredValueLen);
if (TuyaDpIdIsDimmer(id)){
pTuya->dimDebug = 1; // enable debug to be printed once.
}
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: DP %d value over len (%d > %d)"), id, len, TUYA_MAX_STRING_SIZE);
}
break;
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: set of dpid %d ignored - type %d != requred %d"), id, type, dp->Type);
TuyaDumpDPStore();
return;
}
}
}
if (i == pTuya->numRxedDPids){
if (pTuya->numRxedDPids < TUYA_MAX_STORED_DPs){
TUYA_DP_STORE *dp = &pTuya->DPStore[pTuya->numRxedDPids];
dp->DPid = id;
dp->Type = type;
if (len <= TUYA_MAX_STRING_SIZE){
memcpy(dp->desiredValue, value, len);
dp->desiredValueLen = len;
dp->toSet = 1;
pTuya->requestSend = 1;
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: NEW DP %d desiredvalue set (0x%08x len %d)"), id, dp->desiredValue[0], dp->desiredValueLen);
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: DP %d value over len (%d > %d)"), id, len, TUYA_MAX_STRING_SIZE);
}
pTuya->numRxedDPids++;
#ifdef TUYA_MORE_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Add unknown dpid %d in set - num DP:%d"), id, pTuya->numRxedDPids);
#endif
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Max Stored DPs exceeded at set of unknown dpid %d ignored - num DP:%d"), id, pTuya->numRxedDPids);
}
}
#ifdef TUYA_MORE_DEBUG
switch(type){
case TUYA_TYPE_RAW:
case TUYA_TYPE_BOOL:
case TUYA_TYPE_ENUM:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: dp%d to %d req"), id, *(uint8_t*)value);
break;
case TUYA_TYPE_VALUE:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: dp%d to %d req"), id, TUYAREAD32FROMPTR(value));
break;
case TUYA_TYPE_STRING:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: dp%d to (?) req"), id);
break;
}
TuyaDumpDPStore();
#endif
}
// note - direct send using TuyaSendCmd
void TuyaSendState(uint8_t id, uint8_t type, uint8_t* value, int len)
{
uint16_t payload_len = 4;
uint8_t payload_buffer[8+TUYA_MAX_STRING_SIZE];
payload_buffer[0] = id;
payload_buffer[1] = type;
switch (type) {
case TUYA_TYPE_BOOL:
case TUYA_TYPE_ENUM:
payload_len += 1;
payload_buffer[2] = 0x00;
payload_buffer[3] = 0x01;
payload_buffer[4] = value[0];
break;
case TUYA_TYPE_VALUE:
payload_len += 4;
payload_buffer[2] = 0x00;
payload_buffer[3] = 0x04;
// note - already reversed
payload_buffer[4] = value[0];
payload_buffer[5] = value[1];
payload_buffer[6] = value[2];
payload_buffer[7] = value[3];
break;
case TUYA_TYPE_STRING:
case TUYA_TYPE_RAW:
payload_buffer[2] = len >> 8;
payload_buffer[3] = len & 0xFF;
if (len <= TUYA_MAX_STRING_SIZE){
for (uint16_t i = 0; i < len; i++) {
payload_buffer[payload_len++] = value[i];
}
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: SendState: DP %d value over len (%d > %d)"), id, len, TUYA_MAX_STRING_SIZE);
return;
}
break;
}
TuyaSendCmd(TUYA_CMD_SET_DP, payload_buffer, payload_len);
}
void TuyaSendBool(uint8_t id, bool value)
{
TuyaPostState(id, TUYA_TYPE_BOOL, (uint8_t*)&value, 1);
}
void TuyaSendValue(uint8_t id, uint32_t value)
{
uint32_t reversed = TUYAREAD32FROMPTR(&value);
TuyaPostState(id, TUYA_TYPE_VALUE, (uint8_t*)&reversed, 4);
}
void TuyaSendEnum(uint8_t id, uint32_t value)
{
TuyaPostState(id, TUYA_TYPE_ENUM, (uint8_t*)&value, 1);
}
static uint16_t convertHexStringtoBytes (uint8_t * dest, const char src[], uint16_t dest_len){
if (NULL == dest || NULL == src || 0 == dest_len){
return 0;
}
char hexbyte[3];
hexbyte[2] = 0;
uint16_t i;
for (i = 0; i < dest_len; i++) {
hexbyte[0] = src[2*i];
hexbyte[1] = src[2*i+1];
dest[i] = strtol(hexbyte, NULL, 16);
}
return i;
}
// note - send immediate, not deferred
void TuyaSendHexString(uint8_t id, const char data[]) {
uint16_t len = strlen(data)/2;
uint8_t value[len];
convertHexStringtoBytes(value, data, len);
TuyaPostState(id, TUYA_TYPE_STRING, value, len);
}
// note - send immediate, not deferred
void TuyaSendString(uint8_t id, const char data[]) {
uint16_t len = strlen(data);
TuyaPostState(id, TUYA_TYPE_STRING, (uint8_t*) data, len);
}
void TuyaSendRaw(uint8_t id, const char data[]) {
const char* beginPos = strchr(data, 'x');
if(!beginPos) {
beginPos = strchr(data, 'X');
}
if(!beginPos) {
beginPos = data;
} else {
beginPos += 1;
}
uint16_t strSize = strlen(beginPos);
uint16_t len = strSize/2;
uint8_t value[len];
convertHexStringtoBytes(value, beginPos, len);
TuyaPostState(id, TUYA_TYPE_RAW, value, len);
}
// send ANY cmd with payload from hex string
void TuyaSendRawCmd(uint8_t cmd, const char data[]) {
uint16_t strSize = strlen(data);
uint16_t len = strSize/2;
uint8_t value[len];
convertHexStringtoBytes(value, data, len);
TuyaSendCmd(cmd, value, len, 1);
}
bool TuyaSetPower(void)
{
bool status = false;
uint8_t rpower = XdrvMailbox.index;
int16_t source = XdrvMailbox.payload;
uint8_t dpid = TuyaGetDpId(TUYA_MCU_FUNC_REL1 + TasmotaGlobal.active_device - 1);
if (dpid == 0) dpid = TuyaGetDpId(TUYA_MCU_FUNC_REL1_INV + TasmotaGlobal.active_device - 1);
uint8_t dev = TasmotaGlobal.active_device-1;
uint8_t value = bitRead(rpower, dev) ^ bitRead(TasmotaGlobal.rel_inverted, dev);
if (source != SRC_SWITCH && TuyaSerial && dpid) { // ignore to prevent loop from pushing state from faceplate interaction
TuyaSendBool(dpid, value);
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: post rpower%d v%d dp%d s%d d%d"), rpower, value, dpid, source, dev);
// no longer needed as commands wait for ack.
//delay(20); // Hack when power is off and dimmer is set then both commands go too soon to Serial out.
status = true;
} else {
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: rpower%d v%d dp%d ignored s%d d%d"), rpower, value, dpid, source, dev);
}
return status;
}
bool TuyaSetChannels(void)
{
uint16_t hue, TuyaData;
uint8_t sat, bri;
uint8_t TuyaIdx = 0;
char hex_char[15];
bool noupd = false;
if ((SRC_SWITCH == TasmotaGlobal.last_source) || (SRC_SWITCH == TasmotaGlobal.last_command_source)) {
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: setchan disbl SRC_SWITCH"));
// but pretend we did set them
return true;
}
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: setchan"));
bool LightMode = TuyaGetDpId(TUYA_MCU_FUNC_MODESET) != 0;
uint8_t idx = 0;
snprintf_P(hex_char, sizeof(hex_char), PSTR("000000000000"));
if (LT_SERIAL1 == TasmotaGlobal.light_type) {
pTuya->Snapshot[0] = light_state.getDimmer();
}
if (LT_SERIAL2 == TasmotaGlobal.light_type || LT_RGBWC == TasmotaGlobal.light_type) {
idx = 1;
if (LT_SERIAL2 == TasmotaGlobal.light_type &&
Settings->flag3.pwm_multi_channels &&
(TuyaGetDpId(TUYA_MCU_FUNC_DIMMER2) != 0)) {
// Special setup for dual dimmer (like the MOES 2 Way Dimmer) emulating 2 PWM channels
pTuya->Snapshot[0] = changeUIntScale(Light.current_color[0], 0, 255, 0, 100);
pTuya->Snapshot[1] = changeUIntScale(Light.current_color[1], 0, 255, 0, 100);
} else { // CT Light or RGBWC
getCTRange(&pTuya->CTMin, &pTuya->CTMax); // SetOption82 - Reduce the CT range from 153..500 to 200..380 to accomodate with Alexa range
pTuya->Snapshot[0] = light_state.getDimmer();
pTuya->Snapshot[1] = light_state.getCT();
}
}
if (LT_RGBW == TasmotaGlobal.light_type) {
idx = 1;
pTuya->Snapshot[0] = light_state.getDimmer(1);
pTuya->Snapshot[1] = light_state.getDimmer(2);
}
if (TasmotaGlobal.light_type > LT_BASIC) {
if (LT_RGB != TasmotaGlobal.light_type) {
for (uint8_t i = 0; i <= idx; i++) {
if (pTuya->Snapshot[i] != pTuya->Levels[i]) {
if (i == 0 && LightMode && pTuya->ModeSet ) { noupd = true;}
if (!noupd) {
LightSerialDuty(pTuya->Snapshot[i], &hex_char[0], i+1);
//pTuya->Levels[i] = pTuya->Snapshot[i];
}
noupd = false;
}
}
}
if (TasmotaGlobal.light_type >= LT_RGB) {
// There are two types of rgb format, configure the correct one using TuyaRGB command.
// The most common is 0HUE0SAT0BRI0 and the less common is RRGGBBFFFF6464 and sometimes both are case sensitive:
// 0 type 1 Uppercase - 00DF00DC0244
// 1 Type 1 Lowercase - 008003e8037a
// 2 Type 2 Uppercase - 00FF00FFFF6464
// 3 Type 2 Lowercase - 00e420ffff6464
uint8_t RGBType = Settings->tuya_fnid_map[230].dpid; // Select the type of RGB payload
char scolor[7];
LightGetColor(scolor, 1); // Always get the color in hex format
light_state.getHSB(&hue, &sat, &bri);
sat = changeUIntScale(sat, 0, 255, 0, 100);
bri = changeUIntScale(bri, 0, 255, 0, 100);
if ((RGBType > 1 && (StrCmpNoCase(scolor, pTuya->RGBColor) != 0)) ||
(RGBType <= 1 && ((hue != pTuya->Snapshot[2]) || (sat != pTuya->Snapshot[3]) || (bri != pTuya->Snapshot[4])) )) {
if ((LightMode && pTuya->ModeSet) ||
LT_RGB == TasmotaGlobal.light_type) {
if (TuyaGetDpId(TUYA_MCU_FUNC_RGB) != 0) {
switch (RGBType) {
case 0: // Uppercase Type 1 payload
snprintf_P(hex_char, sizeof(hex_char), PSTR("%04X%04X%04X"), hue, sat * 10, bri * 10);
break;
case 1: // Lowercase Type 1 payload
snprintf_P(hex_char, sizeof(hex_char), PSTR("%04x%04x%04x"), hue, sat * 10, bri * 10);
break;
case 2: // Uppercase Type 2 payload
snprintf_P(hex_char, sizeof(hex_char), PSTR("%sFFFF6464"), scolor);
break;
case 3: // Lowercase Type 2 payload
snprintf_P(hex_char, sizeof(hex_char), PSTR("%sffff6464"), LowerCase(scolor, scolor));
break;
}
memcpy_P(pTuya->RGBColor, scolor, strlen(scolor));
pTuya->Snapshot[2] = hue;
pTuya->Snapshot[3] = sat;
pTuya->Snapshot[4] = bri;
}
LightSerialDuty(0, &hex_char[0], 3);
}
}
}
}
return true;
}
void LightSerialDuty(uint16_t duty, char *hex_char, uint8_t TuyaIdx)
{
uint8_t dpid = TuyaGetDpId(TUYA_MCU_FUNC_DIMMER);
bool CTLight = false;
if (TuyaIdx > 0 && TuyaIdx <= 2) {
if (TuyaIdx == 2) {
if (!Settings->flag3.pwm_multi_channels) {
CTLight = true;
dpid = TuyaGetDpId(TUYA_MCU_FUNC_CT);
} else {
dpid = TuyaGetDpId(TUYA_MCU_FUNC_DIMMER2);
}
}
// if (pTuya->ignore_dim && pTuya->ignore_dimmer_cmd_timeout < millis()) {
// pTuya->ignore_dim = false;
// }
pTuya->ignore_dim = false;
if (duty > 0 && !pTuya->ignore_dim && TuyaSerial && dpid > 0) {
if (TuyaIdx == 2 && CTLight) {
duty = changeUIntScale(duty, pTuya->CTMin, pTuya->CTMax, Settings->dimmer_hw_max, 0);
} else {
duty = changeUIntScale(duty, 0, 100, Settings->dimmer_hw_min, Settings->dimmer_hw_max);
}
// dimming acts odd below 25(10%) - this mirrors the threshold set on the faceplate itself
if (duty < Settings->dimmer_hw_min) {
duty = Settings->dimmer_hw_min;
}
//pTuya->ignore_dimmer_cmd_timeout = millis() + 250; // Ignore serial received dim commands for the next 250ms
if (pTuya->ModeSet &&
(TuyaGetDpId(TUYA_MCU_FUNC_MODESET) != 0) &&
TasmotaGlobal.light_type > LT_RGB) {
TuyaSendEnum(TuyaGetDpId(TUYA_MCU_FUNC_MODESET), 0);
}
TuyaSendValue(dpid, duty);
} else {
if (dpid > 0 && TuyaIdx <= 2) {
int tasduty = duty;
pTuya->ignore_dim = false; // reset flag
if (TuyaIdx == 2 && CTLight) {
duty = changeUIntScale(duty, pTuya->CTMin, pTuya->CTMax, Settings->dimmer_hw_max, 0);
} else {
duty = changeUIntScale(duty, 0, 100, Settings->dimmer_hw_min, Settings->dimmer_hw_max);
}
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: dim skip duty%d v%d dp%d"), tasduty, duty, dpid); // due to 0 or already set
} else {
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Cannot set dimmer. Dimmer Id unknown"));
}
}
}
if (TuyaIdx == 3) {
dpid = TuyaGetDpId(TUYA_MCU_FUNC_RGB);
if (!pTuya->ModeSet &&
(TuyaGetDpId(TUYA_MCU_FUNC_MODESET) != 0) &&
TasmotaGlobal.light_type > LT_RGB) {
TuyaSendEnum(TuyaGetDpId(TUYA_MCU_FUNC_MODESET), 1);
}
TuyaSendString(dpid, hex_char);
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: TX RGB hex %s to dpId %d"), hex_char, dpid);
}
}
// only use manually!!!
void TuyaRequestState(uint8_t state_type)
{
if (TuyaSerial) {
// Get current status of MCU
#ifdef TUYA_MORE_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Read MCU state"));
#endif
pTuya->SuspendTopic = true;
pTuya->ignore_topic_timeout = millis() + 1000; // suppress /STAT topic for 1000ms to limit data
switch (state_type) {
case 0:
TuyaSendCmd(TUYA_CMD_QUERY_STATE);
break;
case 8:
TuyaSendCmd(TUYA_CMD_QUERY_PRODUCT);
break;
}
}
}
void TuyaResetWifi(void)
{
if (!Settings->flag.button_restrict) { // SetOption1 - Control button multipress
if (pTuya->wifiTimer > 20000){
char scmnd[20];
snprintf_P(scmnd, sizeof(scmnd), D_CMND_WIFICONFIG " %d", 2);
ExecuteCommand(scmnd, SRC_BUTTON);
}
pTuya->wifiTimer += 10000;
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Wifi reset button pressed. Press repeatedly to achieve reset"));
}
}
///////////////////////////////////////
// store all DPs received with values,
// these are used so we don't send DP value which are the same
// as the MCU has already - e.g. double 'off' can crash dimmers
void TuyaStoreRxedDP(uint8_t dpid, uint8_t type, uint8_t *data, int dpDataLen){
int i;
for (i = 0; i < pTuya->numRxedDPids; i++){
TUYA_DP_STORE *dp = &pTuya->DPStore[i];
if (dp->DPid == dpid){
if (type != dp->Type){
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Type change in rxed dpId=%d type %d -> %d"), dpid, dp->Type, type);
dp->Type = type;
}
// if a relay command, then set dim delay, and if value is zero, disable dim command.
// if 1, enable dim command.
// make it work for single or dual dimmers...
uint8_t fnId = TuyaGetFuncId(dpid);
if ((fnId == TUYA_MCU_FUNC_REL1) ||
(fnId == TUYA_MCU_FUNC_REL1_INV) ||
(fnId == TUYA_MCU_FUNC_REL2) ||
(fnId == TUYA_MCU_FUNC_REL2_INV)){
uint8_t dimindex = 0;
if ((fnId == TUYA_MCU_FUNC_REL2) ||
(fnId == TUYA_MCU_FUNC_REL2_INV)){
dimindex = 1;
}
pTuya->dimDelay_ms[dimindex] = pTuya->defaultDimDelay_ms;
int value = data[0];
if (!value){
pTuya->dimCmdEnable &= 0xfe; (0xff ^ (1<<dimindex));
} else {
pTuya->dimCmdEnable |= (1<<dimindex);
}
}
if (dpDataLen <= TUYA_MAX_STRING_SIZE){
memcpy(dp->rxedValue, data, dpDataLen);
dp->rxedValueLen = dpDataLen;
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: DP len exceeded dpId=%d type %d (%d > %d"), dpid, type, dpDataLen, TUYA_MAX_STRING_SIZE);
}
dp->rxed = 1;
break;
}
}
if (i == pTuya->numRxedDPids){
if (i < TUYA_MAX_STORED_DPs){
TUYA_DP_STORE *dp = &pTuya->DPStore[i];
dp->DPid = dpid;
dp->Type = type;
if (dpDataLen <= TUYA_MAX_STRING_SIZE){
memcpy(dp->rxedValue, data, dpDataLen);
dp->rxedValueLen = dpDataLen;
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: DP len exceeded dpId=%d type %d (%d > %d"), dpid, type, dpDataLen, TUYA_MAX_STRING_SIZE);
}
dp->rxed = 1;
pTuya->numRxedDPids++;
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Max stored DPs exceeded dpId=%d type %d"), dpid, type);
}
}
TuyaDumpDPStore();
}
////////////////////////////////////////////////
// process ONE rxed DP value
void TuyaProcessRxedDP(uint8_t dpid, uint8_t type, uint8_t *data, int dpDataLen){
char scmnd[20];
bool PowerOff = false;
bool tuya_energy_enabled = (XNRG_32 == TasmotaGlobal.energy_driver);
uint8_t fnId = TuyaGetFuncId(dpid);
uint32_t value = 0;
if (TuyaFnIdIsDimmer(fnId)){
if (fnId == TUYA_MCU_FUNC_DIMMER){ // first dimmer
pTuya->dimDelay_ms[0] = pTuya->defaultDimDelay_ms;
} else {
if (fnId == TUYA_MCU_FUNC_DIMMER2){ // second dimmer
pTuya->dimDelay_ms[1] = pTuya->defaultDimDelay_ms;
} else { // must be other light, single dimmable thing
pTuya->dimDelay_ms[0] = pTuya->defaultDimDelay_ms;
}
}
}
////////////////////
// get value for types that fit in 4 byte as uint32_t
switch(type){
case TUYA_TYPE_RAW: // variable length combined?
break;
case TUYA_TYPE_BOOL: // 1 = 1 byte bool
value = data[0];
break;
case TUYA_TYPE_VALUE: // 2 = 32 bit int
value = TUYAREAD32FROMPTR(data);
break;
case TUYA_TYPE_STRING: // 3
break;
case TUYA_TYPE_ENUM: // 4 = 1 byte value
value = data[0];
break;
}
//////////////////
// incorporated into logs below to save one log line.
//AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: fnId=%d is set for dpId=%d"), fnId, dpid);
switch(type) {
case TUYA_TYPE_RAW: {
#ifdef USE_ENERGY_SENSOR
if (tuya_energy_enabled && fnId == TUYA_MCU_FUNC_POWER_COMBINED) {
if (dpDataLen >= 8) {
// data is pTuya->buffer[dpidStart + 4];
uint16_t tmpVol = data[4 - 4] << 8 | data[5 - 4];
uint16_t tmpCur = data[7 - 4] << 8 | data[8 - 4];
uint16_t tmpPow = data[10 - 4] << 8 | data[11 - 4];
/* uint16_t tmpVol = pTuya->buffer[dpidStart + 4] << 8 | pTuya->buffer[dpidStart + 5];
uint16_t tmpCur = pTuya->buffer[dpidStart + 7] << 8 | pTuya->buffer[dpidStart + 8];
uint16_t tmpPow = pTuya->buffer[dpidStart + 10] << 8 | pTuya->buffer[dpidStart + 11];*/
Energy->voltage[0] = (float)tmpVol / 10;
Energy->current[0] = (float)tmpCur / 1000;
Energy->active_power[0] = (float)tmpPow;
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: fnId=%d Rx ID=%d Voltage=%d Current=%d Active_Power=%d"), fnId, dpid, tmpVol, tmpCur, tmpPow);
if (RtcTime.valid) {
if (pTuya->lastPowerCheckTime != 0 && Energy->active_power[0] > 0) {
Energy->kWhtoday[0] += Energy->active_power[0] * (float)(Rtc.utc_time - pTuya->lastPowerCheckTime) / 36.0;
EnergyUpdateToday();
}
pTuya->lastPowerCheckTime = Rtc.utc_time;
}
} else {
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: fnId=%d Rx ID=%d INV_LEN=%d"), fnId, dpid, dpDataLen);
}
}
#endif // USE_ENERGY_SENSOR
} break;
case TUYA_TYPE_BOOL: { // Data Type 1
if (fnId >= TUYA_MCU_FUNC_REL1 && fnId <= TUYA_MCU_FUNC_REL8) {
AddLog(LOG_LEVEL_DEBUG, PSTR("T:fn%d Relay%d-->M%s T%s"), fnId, fnId - TUYA_MCU_FUNC_REL1 + 1, value?"On":"Off",bitRead(TasmotaGlobal.power, fnId - TUYA_MCU_FUNC_REL1)?"On":"Off");
if (value != bitRead(TasmotaGlobal.power, fnId - TUYA_MCU_FUNC_REL1)) {
if (!value) { PowerOff = true; }
ExecuteCommandPower(fnId - TUYA_MCU_FUNC_REL1 + 1, value, SRC_SWITCH); // send SRC_SWITCH? to use as flag to prevent loop from inbound states from faceplate interaction
}
} else if (fnId >= TUYA_MCU_FUNC_REL1_INV && fnId <= TUYA_MCU_FUNC_REL8_INV) {
AddLog(LOG_LEVEL_DEBUG, PSTR("T:fn%d Relay%d-Inv-->M%s T%s"), fnId, fnId - TUYA_MCU_FUNC_REL1_INV + 1, value?"Off":"On",bitRead(TasmotaGlobal.power, fnId - TUYA_MCU_FUNC_REL1_INV) ^ 1?"Off":"On");
if (value != bitRead(TasmotaGlobal.power, fnId - TUYA_MCU_FUNC_REL1_INV) ^ 1) {
ExecuteCommandPower(fnId - TUYA_MCU_FUNC_REL1_INV + 1, value ^ 1, SRC_SWITCH); // send SRC_SWITCH? to use as flag to prevent loop from inbound states from faceplate interaction
if (value) { PowerOff = true; }
}
} else if (fnId >= TUYA_MCU_FUNC_SWT1 && fnId <= TUYA_MCU_FUNC_SWT4) {
uint32_t switch_index = fnId - TUYA_MCU_FUNC_SWT1;
uint32_t switch_state = SwitchGetState(switch_index);
AddLog(LOG_LEVEL_DEBUG, PSTR("T:fn%d Switch%d --> M%d T%d"), fnId, switch_index +1, value, switch_state);
if (switch_state != value) {
SwitchSetState(switch_index, value);
}
}
//if (PowerOff) { pTuya->ignore_dimmer_cmd_timeout = millis() + 250; }
} break;
case TUYA_TYPE_VALUE: { // Data Type 2
uint32_t packetValue = value; // TYpe 2 is a 32 bit integer
uint8_t dimIndex;
bool SnsUpdate = false;
if ((fnId >= TUYA_MCU_FUNC_TEMP) && (fnId <= TUYA_MCU_FUNC_TIMER4)) { // Sensors start from fnId 71
if (packetValue != pTuya->Sensors[fnId-71]) {
pTuya->SensorsValid[fnId-71] = true;
pTuya->Sensors[fnId-71] = packetValue;
SnsUpdate = true;
}
}
if (SnsUpdate) {
char sname[20];
char tempval[5];
uint8_t res;
bool dont_publish = Settings->flag5.tuyasns_no_immediate;
if (TasmotaGlobal.uptime < 8) { // delay to avoid multiple topics at the same time at boot time
return;
} else {
if (fnId > 80 || fnId == 74 || fnId == 72) {
dont_publish = false;
}
if (fnId > 74) {
res = 0;
} else if (fnId > 72) {
res = Settings->flag2.humidity_resolution;
} else if (fnId == 72) {
res = Settings->mbflag2.temperature_set_res;
} else {
res = Settings->flag2.temperature_resolution;
}
GetTextIndexed(sname, sizeof(sname), (fnId-71), kTuyaSensors);
ResponseClear(); // Clear retained message
Response_P(PSTR("{\"TuyaSNS\":{\"%s\":%s}}"), sname, dtostrfd(TuyaAdjustedTemperature(packetValue, res), res, tempval)); // sensor update is just on change
if (dont_publish) {
XdrvRulesProcess(0);
} else {
MqttPublishPrefixTopicRulesProcess_P(TELE, PSTR(D_CMND_SENSOR));
}
}
}
if (fnId == TUYA_MCU_FUNC_DIMMER || fnId == TUYA_MCU_FUNC_REPORT1) { dimIndex = 0; }
if (fnId == TUYA_MCU_FUNC_DIMMER2 || fnId == TUYA_MCU_FUNC_REPORT2 || fnId == TUYA_MCU_FUNC_CT) { dimIndex = 1; }
if (dimIndex == 1 && !Settings->flag3.pwm_multi_channels) {
pTuya->Levels[1] = changeUIntScale(packetValue, 0, Settings->dimmer_hw_max, pTuya->CTMax, pTuya->CTMin);
} else {
pTuya->Levels[dimIndex] = changeUIntScale(packetValue, Settings->dimmer_hw_min, Settings->dimmer_hw_max, 0, 100);
}
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: fn%d value %d dp%d "), fnId, packetValue, dpid);
if ((fnId == TUYA_MCU_FUNC_DIMMER) || (fnId == TUYA_MCU_FUNC_REPORT1) ||
(fnId == TUYA_MCU_FUNC_DIMMER2) || (fnId == TUYA_MCU_FUNC_REPORT2) ||
(fnId == TUYA_MCU_FUNC_CT) || (fnId == TUYA_MCU_FUNC_WHITE)) {
// SetOption54 - Apply SetOption20 settings to Tuya device / SetOption131 Allow save dimmer = 0 receved by MCU
if (1) {//pTuya->ignore_dimmer_cmd_timeout < millis()) {
if ((TasmotaGlobal.power || Settings->flag3.tuya_apply_o20) &&
((pTuya->Levels[dimIndex] > 0 || Settings->flag5.tuya_allow_dimmer_0) &&
(pTuya->Levels[dimIndex] != pTuya->Snapshot[dimIndex]))) {
//pTuya->ignore_dim = true;
TasmotaGlobal.skip_light_fade = true;
scmnd[0] = '\0';
if ((fnId == TUYA_MCU_FUNC_DIMMER) || (fnId == TUYA_MCU_FUNC_REPORT1)) {
if (Settings->flag3.pwm_multi_channels && (abs(pTuya->Levels[0] - changeUIntScale(Light.current_color[0], 0, 255, 0, 100))) > 1) {
snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_CHANNEL "1 %d"), pTuya->Levels[0]);
}
else if ((abs(pTuya->Levels[0] - light_state.getDimmer())) > 1) {
snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_DIMMER "3 %d"), pTuya->Levels[0]);
}
}
if (((fnId == TUYA_MCU_FUNC_DIMMER2) || (fnId == TUYA_MCU_FUNC_REPORT2)) &&
Settings->flag3.pwm_multi_channels && (abs(pTuya->Levels[1] - changeUIntScale(Light.current_color[1], 0, 255, 0, 100))) > 1) {
snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_CHANNEL "2 %d"), pTuya->Levels[1]);
}
if ((fnId == TUYA_MCU_FUNC_CT) && (abs(pTuya->Levels[1] - light_state.getCT())) > 1) {
snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_COLORTEMPERATURE " %d"), pTuya->Levels[1]);
}
if ((fnId == TUYA_MCU_FUNC_WHITE) && (abs(pTuya->Levels[1] - light_state.getDimmer(2))) > 1) {
snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_WHITE " %d"), pTuya->Levels[1]);
}
if (scmnd[0] != '\0') {
ExecuteCommand(scmnd, SRC_SWITCH);
}
}
pTuya->Snapshot[dimIndex] = pTuya->Levels[dimIndex];
}
}
#ifdef USE_ENERGY_SENSOR
else if (tuya_energy_enabled && fnId == TUYA_MCU_FUNC_VOLTAGE) {
Energy->voltage[0] = (float)packetValue / 10;
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: fnId=%d Rx ID=%d Voltage=%d"), fnId, dpid, packetValue);
} else if (tuya_energy_enabled && fnId == TUYA_MCU_FUNC_CURRENT) {
Energy->current[0] = (float)packetValue / 1000;
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: fnId=%d Rx ID=%d Current=%d"), fnId, dpid, packetValue);
} else if (tuya_energy_enabled && fnId == TUYA_MCU_FUNC_POWER) {
Energy->active_power[0] = (float)packetValue / 10;
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: fnId=%d Rx ID=%d Active_Power=%d"), fnId, dpid, packetValue);
if (RtcTime.valid) {
if (pTuya->lastPowerCheckTime != 0 && Energy->active_power[0] > 0) {
Energy->kWhtoday[0] += Energy->active_power[0] * (float)(Rtc.utc_time - pTuya->lastPowerCheckTime) / 36.0;
EnergyUpdateToday();
}
pTuya->lastPowerCheckTime = Rtc.utc_time;
}
} else if (tuya_energy_enabled && fnId == TUYA_MCU_FUNC_POWER_TOTAL) {
Energy->import_active[0] = (float)packetValue / 100;
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: fnId=%d Rx ID=%d Total_Power=%d"), fnId, dpid, packetValue);
EnergyUpdateTotal();
}
#endif // USE_ENERGY_SENSOR
} break;
case TUYA_TYPE_STRING: { // Data Type 3
const unsigned char *dpData = (unsigned char*)data;
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: (str) fnId=%d is set for dpId=%d"), dpid);
if ((TuyaGetDpId(TUYA_MCU_FUNC_RGB) != 0)) {
uint8_t RGBType = Settings->tuya_fnid_map[230].dpid; // Select the type of hex configured
char RgbData[15];
char RGB[7];
char HSB1[5], HSB2[5], HSB3[5];
scmnd[0] = '\0';
snprintf_P(RgbData, sizeof(RgbData), PSTR("%.*s"), dpDataLen, dpData);
if (RGBType <= 1 && dpDataLen == 12) {
snprintf_P(HSB1, sizeof(HSB1), PSTR("%.4s\n"), &RgbData[0]);
snprintf_P(HSB2, sizeof(HSB2), PSTR("%.4s\n"), &RgbData[4]);
snprintf_P(HSB3, sizeof(HSB3), PSTR("%.4s\n"), &RgbData[8]);
if ((pTuya->Snapshot[2] != ((int)strtol(HSB1, NULL, 16)) ||
(pTuya->Snapshot[3] != ((int)strtol(HSB2, NULL, 16)) / 10) ||
(pTuya->Snapshot[4] !=((int)strtol(HSB3, NULL, 16)) / 10)) ) {
pTuya->Snapshot[2] = ((int)strtol(HSB1, NULL, 16));
pTuya->Snapshot[3] = ((int)strtol(HSB2, NULL, 16)) / 10;
pTuya->Snapshot[4] = ((int)strtol(HSB3, NULL, 16)) / 10;
snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_HSBCOLOR " %d,%d,%d"), ((int)strtol(HSB1, NULL, 16)),
((int)strtol(HSB2, NULL, 16)) / 10, ((int)strtol(HSB3, NULL, 16)) / 10);
}
}
if (RGBType > 1 && dpDataLen == 14) {
snprintf_P(RGB, sizeof(RGB), PSTR("%.6s\n"), &RgbData[0]);
if (StrCmpNoCase(RGB, pTuya->RGBColor) != 0) {
snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_COLOR " %s"), RGB);
memcpy_P(pTuya->RGBColor, RGB, strlen(RGB));
}
}
if (scmnd[0] != '\0') {
ExecuteCommand(scmnd, SRC_SWITCH);
}
}
} break;
case TUYA_TYPE_ENUM: { // Data Type 4
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: (enum) fnId=%d is set for dpId=%d"), dpid);
if ((fnId == TUYA_MCU_FUNC_MODESET)) { // Toggle light type
pTuya->ModeSet = value;
pTuya->Levels[3] = value;
}
if ((fnId >= TUYA_MCU_FUNC_ENUM1) && (fnId <= TUYA_MCU_FUNC_ENUM4)) {
for (uint8_t i = 0; i <= 3; i++) {
bool noupdate = false;
if ((TUYA_MCU_FUNC_ENUM1 + i) == fnId) {
if (pTuya->EnumState[i] != value) {
pTuya->EnumState[i] = value;
snprintf_P(scmnd, sizeof(scmnd), PSTR(D_PRFX_TUYA D_CMND_TUYA_ENUM "%d %d"), i+1, value);
ExecuteCommand(scmnd, SRC_SWITCH);
}
}
}
}
} break;
}
}
void TuyaProcessStatePacket(void) {
uint8_t dpidStart = 6;
while (dpidStart + 4 < pTuya->byte_counter) {
uint8_t dpid = pTuya->buffer[dpidStart];
uint8_t type = pTuya->buffer[dpidStart + 1];
uint8_t *data = pTuya->buffer + dpidStart + 4;
uint16_t dpDataLen = pTuya->buffer[dpidStart + 2] << 8 | pTuya->buffer[dpidStart + 3];
TuyaStoreRxedDP(dpid, type, data, dpDataLen);
TuyaProcessRxedDP(dpid, type, data, dpDataLen);
dpidStart += dpDataLen + 4;
}
}
void TuyaLowPowerModePacketProcess(void) {
switch (pTuya->buffer[3]) {
case TUYA_CMD_QUERY_PRODUCT:
TuyaHandleProductInfoPacket();
//TuyaSetWifiLed();
break;
case TUYA_LOW_POWER_CMD_STATE:
TuyaProcessStatePacket();
pTuya->send_success_next_second = true;
break;
}
}
void TuyaHandleProductInfoPacket(void) {
uint16_t dataLength = pTuya->buffer[4] << 8 | pTuya->buffer[5];
char *data = (char *)&pTuya->buffer[6];
AddLog(LOG_LEVEL_INFO, PSTR("TYA: MCU Product ID: %.*s"), dataLength, data);
}
void TuyaNormalPowerModePacketProcess(void)
{
switch (pTuya->buffer[3]) {
case TUYA_CMD_QUERY_PRODUCT:
TuyaHandleProductInfoPacket();
// next send now done as part of startup state machine
//TuyaSendCmd(TUYA_CMD_MCU_CONF);
break;
case TUYA_CMD_HEARTBEAT:
#ifdef TUYA_MORE_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Hbt"));
#endif
if (pTuya->buffer[6] == 0) {
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Detected MCU restart"));
pTuya->wifi_state = -2;
}
break;
case TUYA_CMD_STATE:
TuyaProcessStatePacket();
break;
case TUYA_CMD_WIFI_RESET:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: RX WiFi Reset"));
// ack MCU request immediately
// https://developer.tuya.com/en/docs/iot/wifi-module-mcu-development-overview-for-homekit?id=Kaa8fvusmgapc
// set 'noerror', as we are responding
TuyaSendCmd(TUYA_CMD_WIFI_RESET, nullptr, 0, 1);
TuyaResetWifi();
break;
case TUYA_CMD_WIFI_SELECT:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: RX WiFi Select"));
// ack MCU request immediately
// https://developer.tuya.com/en/docs/iot/wifi-module-mcu-development-overview-for-homekit?id=Kaa8fvusmgapc
// set 'noerror', as we are responding
TuyaSendCmd(TUYA_CMD_WIFI_SELECT, nullptr, 0, 1);
TuyaResetWifi();
break;
case TUYA_CMD_WIFI_STATE:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: RX WiFi LED set ACK"));
pTuya->wifi_state = TuyaGetTuyaWifiState();
break;
case TUYA_CMD_MCU_CONF:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: RX MCU configuration Mode=%d"), pTuya->buffer[5]);
if (pTuya->buffer[5] == 2) { // Processing by ESP module mode
uint8_t led1_gpio = pTuya->buffer[6];
uint8_t key1_gpio = pTuya->buffer[7];
bool key1_set = false;
bool led1_set = false;
for (uint32_t i = 0; i < nitems(Settings->my_gp.io); i++) {
if (Settings->my_gp.io[i] == AGPIO(GPIO_LED1)) led1_set = true;
else if (Settings->my_gp.io[i] == AGPIO(GPIO_KEY1)) key1_set = true;
}
if (!Settings->my_gp.io[led1_gpio] && !led1_set) {
Settings->my_gp.io[led1_gpio] = AGPIO(GPIO_LED1);
TasmotaGlobal.restart_flag = 2;
}
if (!Settings->my_gp.io[key1_gpio] && !key1_set) {
Settings->my_gp.io[key1_gpio] = AGPIO(GPIO_KEY1);
TasmotaGlobal.restart_flag = 2;
}
}
// next send now done as part of startup state machine
//TuyaRequestState(0);
break;
#ifdef USE_TUYA_TIME
case TUYA_CMD_SET_TIME:
// send from state machine.
pTuya->send_time = 3;
//TuyaSetTime();
break;
#endif
default:
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: RX unknown command"));
}
}
/*********************************************************************************************\
* API Functions
\*********************************************************************************************/
bool TuyaModuleSelected(void) {
#ifdef ESP8266
if (TUYA_DIMMER != TasmotaGlobal.module_type) { return false; }
if (!PinUsed(GPIO_TUYA_RX) || !PinUsed(GPIO_TUYA_TX)) { // fallback to hardware-serial if not explicitly selected
SetPin(1, AGPIO(GPIO_TUYA_TX));
SetPin(3, AGPIO(GPIO_TUYA_RX));
Settings->my_gp.io[1] = AGPIO(GPIO_TUYA_TX);
Settings->my_gp.io[3] = AGPIO(GPIO_TUYA_RX);
TasmotaGlobal.restart_flag = 2;
}
#endif
if (!PinUsed(GPIO_TUYA_RX) || !PinUsed(GPIO_TUYA_TX)) { return false; }
// allocate and initialise the sturcture only if we will use it.
init_tuya_struct();
if (!pTuya){ return false; }
if (TuyaGetDpId(TUYA_MCU_FUNC_DIMMER) == 0 && TUYA_DIMMER_ID > 0) {
TuyaAddMcuFunc(TUYA_MCU_FUNC_DIMMER, TUYA_DIMMER_ID);
}
bool relaySet = false;
for (uint8_t i = 0 ; i < MAX_TUYA_FUNCTIONS; i++) {
if ((Settings->tuya_fnid_map[i].fnid >= TUYA_MCU_FUNC_REL1 && Settings->tuya_fnid_map[i].fnid <= TUYA_MCU_FUNC_REL8 ) ||
(Settings->tuya_fnid_map[i].fnid >= TUYA_MCU_FUNC_REL1_INV && Settings->tuya_fnid_map[i].fnid <= TUYA_MCU_FUNC_REL8_INV )) {
relaySet = true;
UpdateDevicesPresent(1);
}
}
if (!relaySet && TuyaGetDpId(TUYA_MCU_FUNC_DUMMY) == 0) { //by default the first relay is created automatically the dummy let remove it if not needed
TuyaAddMcuFunc(TUYA_MCU_FUNC_REL1, 1);
UpdateDevicesPresent(1);
SettingsSaveAll();
}
// Possible combinations for Lights:
// 0: NONE = LT_BASIC
// 1: DIMMER = LT_SERIAL1 - Common one channel dimmer
// 2: DIMMER, DIMMER2 = LT_SERIAL2 - Two channels dimmer (special setup used with SetOption68)
// 3: DIMMER, CT = LT_SERIAL2 - Dimmable light and White Color Temperature
// 4: DIMMER, RGB = LT_RGB - RGB Light
// 5: DIMMER, RGB, CT = LT_RGBWC - RGB LIght and White Color Temperature
// 6: DIMMER, RGB, WHITE = LT_RGBW - RGB LIght and White
if (TuyaGetDpId(TUYA_MCU_FUNC_DIMMER) != 0) {
if (TuyaGetDpId(TUYA_MCU_FUNC_RGB) != 0) {
if (TuyaGetDpId(TUYA_MCU_FUNC_CT) != 0) {
TasmotaGlobal.light_type = LT_RGBWC;
} else if (TuyaGetDpId(TUYA_MCU_FUNC_WHITE) != 0) {
TasmotaGlobal.light_type = LT_RGBW;
} else { TasmotaGlobal.light_type = LT_RGB; }
} else if (TuyaGetDpId(TUYA_MCU_FUNC_CT) != 0 || TuyaGetDpId(TUYA_MCU_FUNC_DIMMER2) != 0) {
if (TuyaGetDpId(TUYA_MCU_FUNC_RGB) != 0) {
TasmotaGlobal.light_type = LT_RGBWC;
} else { TasmotaGlobal.light_type = LT_SERIAL2; }
} else { TasmotaGlobal.light_type = LT_SERIAL1; }
} else {
TasmotaGlobal.light_type = LT_BASIC;
}
if (TuyaGetDpId(TUYA_MCU_FUNC_LOWPOWER_MODE) != 0) {
pTuya->low_power_mode = true;
Settings->flag3.fast_power_cycle_disable = true; // SetOption65 - Disable fast power cycle detection for device reset
}
UpdateDevices();
return true;
}
void TuyaInit(void) {
int baudrate = 9600;
if (Settings->flag4.tuyamcu_baudrate) { baudrate = 115200; } // SetOption97 - Set Baud rate for TuyaMCU serial communication (0 = 9600 or 1 = 115200)
TuyaSerial = new TasmotaSerial(Pin(GPIO_TUYA_RX), Pin(GPIO_TUYA_TX), 2);
if (TuyaSerial->begin(baudrate)) {
if (TuyaSerial->hardwareSerial()) { ClaimSerial(); }
#ifdef ESP32
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Serial UART%d"), TuyaSerial->getUart());
#endif // ESP32
// Get MCU Configuration
pTuya->SuspendTopic = true;
pTuya->ignore_topic_timeout = millis() + 1000; // suppress /STAT topic for 1000ms to avoid data overflow
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Request MCU configuration at %d bps"), baudrate);
pTuya->heartbeat_timer = 0; // init heartbeat timer when dimmer init is done
return;
}
pTuya->active = false;
}
// dump a buffer to debug
void TuyaDump(unsigned char *buffer, int len){
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Raw Data %*_H"), len, buffer);
}
// here we have a complete packet with valid checksum
void TuyaProcessCommand(unsigned char *buffer){
uint16_t len = buffer[4] << 8 | buffer[5];
uint16_t totallen = len + 7;
uint8_t ver = buffer[2];
uint8_t cmd = buffer[3];
pTuya->rxs ++;
// see if we are awaiting this cmd ack
// some MCU send 0x00 in ver field,
// so we could assume some send 0x01, some 0x02, some 0x03?
// I'v not seen any docs about what the VER means!....
//if (0x03 == ver) {
Tuya_statemachine(cmd, len, (unsigned char *)buffer+6);
//}
Response_P(PSTR("{\"" D_JSON_TUYA_MCU_RECEIVED "\":{\"Data\":\"%*_H\",\"Cmnd\":%d"), totallen, buffer, cmd);
uint16_t DataVal = 0;
uint8_t dpId = 0;
uint8_t dpDataType = 0;
char DataStr[15];
bool isCmdToSuppress = false;
if (len > 0) {
ResponseAppend_P(PSTR(",\"CmndData\":\"%*_H\""), len, (unsigned char*)&buffer[6]);
if (TUYA_CMD_STATE == cmd) {
//55 AA 03 07 00 0D 01 04 00 01 02 02 02 00 04 00 00 00 1A 40
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
uint8_t dpidStart = 6;
while (dpidStart + 4 < totallen-1) {
dpId = buffer[dpidStart];
dpDataType = buffer[dpidStart + 1];
uint16_t dpDataLen = buffer[dpidStart + 2] << 8 | buffer[dpidStart + 3];
const unsigned char *dpData = (unsigned char*)&buffer[dpidStart + 4];
if (TUYA_CMD_STATE == buffer[3]) {
ResponseAppend_P(PSTR(",\"DpType%uId%u\":"), dpDataType, dpId);
if (TUYA_TYPE_BOOL == dpDataType && dpDataLen == 1) {
ResponseAppend_P(PSTR("%u"), dpData[0]);
DataVal = dpData[0];
} else if (TUYA_TYPE_VALUE == dpDataType && dpDataLen == 4) {
uint32_t dpValue = (uint32_t)dpData[0] << 24 | (uint32_t)dpData[1] << 16 | (uint32_t)dpData[2] << 8 | (uint32_t)dpData[3] << 0;
ResponseAppend_P(PSTR("%u"), dpValue);
DataVal = dpValue;
} else if (TUYA_TYPE_STRING == dpDataType) {
ResponseAppend_P(PSTR("\"%.*s\""), dpDataLen, dpData);
snprintf_P(DataStr, sizeof(DataStr), PSTR("%.*s"), dpDataLen, dpData);
} else if (TUYA_TYPE_ENUM == dpDataType && dpDataLen == 1) {
ResponseAppend_P(PSTR("%u"), dpData[0]);
DataVal = dpData[0];
} else {
ResponseAppend_P(PSTR("\"0x%*_H\""), dpDataLen, dpData);
snprintf_P(DataStr, sizeof(DataStr), PSTR("%*_H"), dpDataLen, dpData);
}
}
ResponseAppend_P(PSTR(",\"%d\":{\"DpId\":%d,\"DpIdType\":%d,\"DpIdData\":\"%*_H\""), dpId, dpId, dpDataType, dpDataLen, dpData);
if (TUYA_TYPE_STRING == dpDataType) {
ResponseAppend_P(PSTR(",\"Type3Data\":\"%.*s\""), dpDataLen, dpData);
}
ResponseAppend_P(PSTR("}"));
dpidStart += dpDataLen + 4;
}
}
}
ResponseAppend_P(PSTR("}}"));
// SetOption66 - Enable TuyaMcuReceived messages over Mqtt
if (Settings->flag3.tuya_serial_mqtt_publish) {
/*
for (uint8_t cmdsID = 0; sizeof(TuyaExcludeCMDsFromMQTT) > cmdsID; cmdsID++){
if (TuyaExcludeCMDsFromMQTT[cmdsID] == cmd) {
isCmdToSuppress = true;
break;
}
}
*/
for (uint8_t cmdsID = 0; cmdsID < sizeof(TuyaExcludeCMDsFromMQTT); cmdsID++) {
if (pgm_read_byte(TuyaExcludeCMDsFromMQTT +cmdsID) == cmd) {
isCmdToSuppress = true;
break;
}
}
// SetOption137 - (Tuya) When Set, avoid the (MQTT-) publish of defined Tuya CMDs
// (see TuyaExcludeCMDsFromMQTT) if SetOption66 is active
if (!(isCmdToSuppress && Settings->flag5.tuya_exclude_from_mqtt)) {
MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_TUYA_MCU_RECEIVED));
}
} else {
AddLog(LOG_LEVEL_DEBUG, ResponseData());
}
XdrvRulesProcess(0);
if (dpId != 0 && Settings->tuyamcu_topic) { // Publish a /STAT Topic ready to use for any home automation system
if (!pTuya->SuspendTopic) {
char scommand[13];
snprintf_P(scommand, sizeof(scommand), PSTR("DpType%uId%u"), dpDataType, dpId);
if (dpDataType != 3 && dpDataType != 5) { Response_P(PSTR("%u"), DataVal); }
else { Response_P(PSTR("%s"), DataStr); }
MqttPublishPrefixTopic_P(STAT, scommand);
}
}
if (!pTuya->low_power_mode) {
TuyaNormalPowerModePacketProcess();
} else {
TuyaLowPowerModePacketProcess();
}
}
// processes one byte of Tuya input.
// on packet complete with good CS, calls TuyaProcessCommand
void TuyaProcessByte(unsigned char serial_in_byte){
/* I don't think it's safe to do this with RGB possibly containing 55AA in the message.
if ((pTuya->cmd_status != 1) &&
(pTuya->lastByte == 0x55) &&
(serial_in_byte == 0xAA)){
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Reset by unexpected 55AA"));
TuyaDump(pTuya->buffer, pTuya->byte_counter);
pTuya->byte_counter = 0;
pTuya->buffer[pTuya->byte_counter++] = 0x55;
pTuya->cmd_status = 1;
pTuya->errorcnt ++;
}
*/
unsigned int now = millis();
if ((pTuya->cmd_status != 0) && (now - pTuya->lastByteTime > TUYA_BYTE_TIMEOUT_MS)){
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Reset by char timeout %u > %dms"), now - pTuya->lastByteTime, TUYA_BYTE_TIMEOUT_MS);
TuyaDump(pTuya->buffer, pTuya->byte_counter);
pTuya->cmd_status = 0;
pTuya->errorcnt ++;
}
pTuya->lastByteTime = now;
switch(pTuya->cmd_status){
case 0: {// wait 55
if (serial_in_byte == 0x55) { // Start TUYA Packet
pTuya->cmd_status = 1;
pTuya->byte_counter = 0;
pTuya->buffer[pTuya->byte_counter++] = serial_in_byte;
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: E55 0x%02X"), serial_in_byte);
pTuya->errorcnt ++;
}
} break;
case 1: {// wait AA
if (serial_in_byte == 0xAA) { // Start TUYA Packet
pTuya->cmd_status = 2;
pTuya->buffer[pTuya->byte_counter++] = serial_in_byte;
pTuya->cmd_checksum = 0xFF;
} else {
// no AA, return to wait for 55
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: EAA 0x%02X"), serial_in_byte);
pTuya->errorcnt ++;
pTuya->cmd_status = 0;
}
} break;
case 2: {// wait len
if (pTuya->byte_counter == 5) { // Get length of data
pTuya->data_len = serial_in_byte;
pTuya->data_len += pTuya->buffer[4] << 8;
if (pTuya->data_len == 0){
// straight to CS
pTuya->cmd_status = 4;
} else {
pTuya->cmd_status = 3;
}
}
pTuya->cmd_checksum += serial_in_byte;
pTuya->cmd_checksum &= 0xff;
pTuya->buffer[pTuya->byte_counter++] = serial_in_byte;
} break;
case 3: {// wait Data
pTuya->buffer[pTuya->byte_counter++] = serial_in_byte;
pTuya->cmd_checksum += serial_in_byte;
pTuya->cmd_checksum &= 0xff;
if (pTuya->byte_counter == (6 + pTuya->data_len)) {
pTuya->cmd_status = 4;
}
if (pTuya->byte_counter > TUYA_BUFFER_SIZE-3){
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: input overflow"));
TuyaDump(pTuya->buffer, pTuya->byte_counter);
pTuya->cmd_status = 0;
pTuya->byte_counter = 0;
}
} break;
case 4: {// wait CS
pTuya->buffer[pTuya->byte_counter++] = serial_in_byte;
pTuya->cmd_checksum &= 0xff;
if (pTuya->cmd_checksum == serial_in_byte) { // Compare checksum and process packet
TuyaProcessCommand(pTuya->buffer);
pTuya->cmd_status = 0;
pTuya->byte_counter = 0;
} else {
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Bad CS 0x%02X != 0x%02X"), serial_in_byte, pTuya->cmd_checksum);
TuyaDump(pTuya->buffer, pTuya->byte_counter);
pTuya->cmd_status = 0;
pTuya->byte_counter = 0;
pTuya->errorcnt ++;
}
} break;
}
pTuya->lastByte = serial_in_byte;
}
void TuyaSerialInput(void)
{
while (TuyaSerial->available()) {
yield();
uint8_t serial_in_byte = TuyaSerial->read();
TuyaProcessByte(serial_in_byte);
}
}
bool TuyaButtonPressed(void) {
bool result = false;
uint32_t button = XdrvMailbox.payload;
if (!XdrvMailbox.index && ((PRESSED == button) && (NOT_PRESSED == pTuya->last_button))) {
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Reset GPIO triggered"));
TuyaResetWifi();
result = true; // Reset GPIO served here
}
pTuya->last_button = button;
return result; // Don't serve other buttons
}
uint8_t TuyaGetTuyaWifiState(void) {
uint8_t wifi_state = 0x02;
switch(WifiState()){
case WIFI_MANAGER:
wifi_state = 0x01;
break;
case WIFI_RESTART:
wifi_state = 0x03;
break;
}
if (MqttIsConnected()) {
wifi_state = 0x04;
}
return wifi_state;
}
#ifdef USE_TUYA_TIME
void TuyaSetTime(void) {
if (!RtcTime.valid) { return; }
uint16_t payload_len = 8;
uint8_t payload_buffer[8];
uint8_t tuya_day_of_week;
if (RtcTime.day_of_week == 1) {
tuya_day_of_week = 7;
} else {
tuya_day_of_week = RtcTime.day_of_week-1;
}
payload_buffer[0] = 0x01;
payload_buffer[1] = RtcTime.year %100;
payload_buffer[2] = RtcTime.month;
payload_buffer[3] = RtcTime.day_of_month;
payload_buffer[4] = RtcTime.hour;
payload_buffer[5] = RtcTime.minute;
payload_buffer[6] = RtcTime.second;
payload_buffer[7] = tuya_day_of_week; //1 for Monday in TUYA Doc
TuyaSendCmd(TUYA_CMD_SET_TIME, payload_buffer, payload_len);
}
#endif //USE_TUYA_TIME
/*********************************************************************************************\
* Sensors
\*********************************************************************************************/
void TuyaSensorsShow(bool json)
{
bool RootName = false;
bool added = false;
char sname[20];
char tempval[5];
uint8_t res;
for (uint8_t sensor = TUYA_MCU_FUNC_TEMP; sensor <= TUYA_MCU_FUNC_TIMER4; sensor++) { // Sensors start from fnId 71
if (json) {
if (TuyaGetDpId(sensor) != 0) {
if (!RootName) {
ResponseAppend_P(PSTR(",\"TuyaSNS\":{"));
RootName = true;
}
if (added) {
ResponseAppend_P(PSTR(","));
}
if (sensor > 74) {
res = 0;
} else if (sensor > 72) {
res = Settings->flag2.humidity_resolution;
} else if (sensor == 72) {
res = Settings->mbflag2.temperature_set_res;
} else {
res = Settings->flag2.temperature_resolution;
}
GetTextIndexed(sname, sizeof(sname), (sensor-71), kTuyaSensors);
ResponseAppend_P(PSTR("\"%s\":%s"), sname,
(pTuya->SensorsValid[sensor-71] ? dtostrfd(TuyaAdjustedTemperature(pTuya->Sensors[sensor-71], res), res, tempval) : PSTR("null")));
added = true;
}
#ifdef USE_WEBSERVER
} else {
if (TuyaGetDpId(sensor) != 0) {
switch (sensor) {
case 71:
WSContentSend_Temp("", TuyaAdjustedTemperature(pTuya->Sensors[0], Settings->flag2.temperature_resolution));
break;
case 72:
WSContentSend_PD(PSTR("{s}" D_TEMPERATURE " Set{m}%s " D_UNIT_DEGREE "%c{e}"),
dtostrfd(TuyaAdjustedTemperature(pTuya->Sensors[1], Settings->mbflag2.temperature_set_res), Settings->mbflag2.temperature_set_res, tempval), TempUnit());
break;
case 73:
WSContentSend_PD(HTTP_SNS_HUM, "", dtostrfd(TuyaAdjustedTemperature(pTuya->Sensors[2], Settings->flag2.humidity_resolution), Settings->flag2.humidity_resolution, tempval));
break;
case 74:
WSContentSend_PD(PSTR("{s}" D_HUMIDITY " Set{m}%s " D_UNIT_PERCENT "{e}"),
dtostrfd(TuyaAdjustedTemperature(pTuya->Sensors[3], Settings->flag2.humidity_resolution), Settings->flag2.humidity_resolution, tempval));
break;
case 75:
WSContentSend_PD(HTTP_SNS_ILLUMINANCE, "", pTuya->Sensors[4]);
break;
case 76:
WSContentSend_PD(PSTR("{s}" D_TVOC "{m}%d " D_UNIT_PARTS_PER_MILLION "{e}"), pTuya->Sensors[5]);
break;
case 77:
WSContentSend_PD(HTTP_SNS_CO2, "", pTuya->Sensors[6]);
break;
case 78:
WSContentSend_PD(HTTP_SNS_CO2EAVG, "", pTuya->Sensors[7]);
break;
case 79:
WSContentSend_PD(HTTP_SNS_GAS, "", pTuya->Sensors[8]);
break;
case 80:
WSContentSend_PD(PSTR("{s}" D_ENVIRONMENTAL_CONCENTRATION " 2.5 " D_UNIT_MICROMETER "{m}%d " D_UNIT_MICROGRAM_PER_CUBIC_METER "{e}"), pTuya->Sensors[9]);
break;
case 81:
case 82:
case 83:
case 84:
WSContentSend_PD(PSTR("{s}Timer%d{m}%d{e}"), (sensor-80), pTuya->Sensors[sensor-71]); // No UoM for timers since they can be sec or min
break;
}
}
#endif // USE_WEBSERVER
}
}
#ifdef USE_WEBSERVER
if (AsModuleTuyaMS()) {
WSContentSend_P(PSTR("{s}" D_JSON_IRHVAC_MODE "{m}%d{e}"), pTuya->ModeSet);
}
#endif // USE_WEBSERVER
if (RootName) { ResponseJsonEnd();}
}
#ifdef USE_WEBSERVER
void TuyaAddButton(void) {
if (AsModuleTuyaMS()) {
WSContentSend_P(HTTP_TABLE100);
WSContentSend_P(PSTR("<tr><div></div>"));
char stemp[33];
snprintf_P(stemp, sizeof(stemp), PSTR("" D_JSON_IRHVAC_MODE ""));
WSContentSend_P(HTTP_DEVICE_CONTROL, 26, TasmotaGlobal.devices_present + 1,
(strlen(GetWebButton(TasmotaGlobal.devices_present))) ? HtmlEscape(GetWebButton(TasmotaGlobal.devices_present)).c_str() : stemp, "");
WSContentSend_P(PSTR("</tr></table>"));
}
}
#endif // USE_WEBSERVER
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
#ifdef USE_ENERGY_SENSOR
bool Xnrg32(uint32_t function)
{
bool result = false;
if (pTuya && pTuya->active) {
if (FUNC_PRE_INIT == function) {
if (TuyaGetDpId(TUYA_MCU_FUNC_POWER) != 0 || TuyaGetDpId(TUYA_MCU_FUNC_POWER_COMBINED) != 0) {
if (TuyaGetDpId(TUYA_MCU_FUNC_CURRENT) == 0 && TuyaGetDpId(TUYA_MCU_FUNC_POWER_COMBINED) == 0) {
Energy->current_available = false;
}
if (TuyaGetDpId(TUYA_MCU_FUNC_VOLTAGE) == 0 && TuyaGetDpId(TUYA_MCU_FUNC_POWER_COMBINED) == 0) {
Energy->voltage_available = false;
}
TasmotaGlobal.energy_driver = XNRG_32;
}
}
}
return result;
}
#endif // USE_ENERGY_SENSOR
bool Xdrv16(uint32_t function) {
bool result = false;
if (FUNC_MODULE_INIT == function) {
result = TuyaModuleSelected();
if (pTuya) pTuya->active = result;
}
else if (pTuya && pTuya->active) {
switch (function) {
case FUNC_LOOP:
case FUNC_SLEEP_LOOP:
if (TuyaSerial) { TuyaSerialInput(); }
break;
case FUNC_PRE_INIT:
TuyaInit();
break;
case FUNC_SET_DEVICE_POWER:
result = TuyaSetPower();
#ifdef TUYA_MORE_DEBUG
MqttPublishLoggingAsync(false);
SyslogAsync(false);
#endif
break;
case FUNC_BUTTON_PRESSED:
result = TuyaButtonPressed();
#ifdef TUYA_MORE_DEBUG
MqttPublishLoggingAsync(false);
SyslogAsync(false);
#endif
break;
case FUNC_EVERY_100_MSECOND:
if (pTuya->timeout){
pTuya->timeout -= 100;
if (pTuya->timeout <= 0){
Tuya_timeout();
}
}
for (int i = 0; i < 2; i++){
if (pTuya->dimDelay_ms[i]){
pTuya->dimDelay_ms[i] -= 100;
if (pTuya->dimDelay_ms[i] <= 0){
pTuya->dimDelay_ms[i] = 0;
AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: DimDelay%d->0"), i);
}
}
}
Tuya_statemachine();
break;
case FUNC_EVERY_SECOND:
if (pTuya->wifiTimer > 0){
pTuya->wifiTimer -= 1000;
if (pTuya->wifiTimer <= 0){
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Wifi reset aborted."));
pTuya->wifiTimer = 0;
}
}
// now done in state machine
//if (TuyaSerial && pTuya->wifi_state != TuyaGetTuyaWifiState()) { TuyaSetWifiLed(); }
if (!pTuya->low_power_mode) {
pTuya->heartbeat_timer++;
if (pTuya->heartbeat_timer > 10) {
pTuya->heartbeat_timer = 0;
pTuya->send_heartbeat = 1;
}
#ifdef USE_TUYA_TIME
if (!(TasmotaGlobal.uptime % 60)) {
// if we have never been asked for time, send it ourselves
// as this was the original TAS implementation.
// if we DO get asked for time, then send_time & 2 will be set, so we no longer send from here.
if (!pTuya->send_time & 2){
pTuya->send_time |= 1;
}
}
#endif //USE_TUYA_TIME
} else {
// done in state machine
//TuyaSendLowPowerSuccessIfNeeded();
}
if (pTuya->ignore_topic_timeout < millis()) { pTuya->SuspendTopic = false; }
if (pTuya->lasterrorcnt != pTuya->errorcnt){
AddLog(LOG_LEVEL_ERROR, PSTR("TYA: Errorcnt %d->%d"), pTuya->lasterrorcnt, pTuya->errorcnt);
pTuya->lasterrorcnt = pTuya->errorcnt;
}
break;
case FUNC_SET_CHANNELS:
result = TuyaSetChannels();
#ifdef TUYA_MORE_DEBUG
MqttPublishLoggingAsync(false);
SyslogAsync(false);
#endif
break;
case FUNC_MQTT_INIT:
// why here? done as part of startup state machine
//TuyaSendCmd(TUYA_CMD_QUERY_PRODUCT);
break;
case FUNC_COMMAND:
result = DecodeCommand(kTuyaCommand, TuyaCommand);
break;
case FUNC_JSON_APPEND:
TuyaSensorsShow(1);
break;
#ifdef USE_WEBSERVER
case FUNC_WEB_ADD_MAIN_BUTTON:
TuyaAddButton();
break;
case FUNC_WEB_SENSOR:
TuyaSensorsShow(0);
break;
#endif // USE_WEBSERVER
case FUNC_ACTIVE:
result = true;
break;
}
}
return result;
}
#endif // USE_TUYA_MCU
#endif // USE_LIGHT