diff --git a/CHANGELOG.md b/CHANGELOG.md index 0364f465a..c8470d738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. ### Changed - Reverted Flash Mode back from ``DIO`` to ``DOUT`` for ESP8266/ESP8285 (#17019) - ESP32 Framework (Core) from v2.0.5.2 to v2.0.5.3 (#17034) +- TuyaMcu rewrite by btsimonh (#17051) ### Fixed - SenseAir S8 module detection (#17033) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 353786be4..6de9a4302 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -133,6 +133,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl http://ota.tasmo - DS18x20 ``DS18Alias`` to ``DS18Sens`` [#16833](https://github.com/arendst/Tasmota/issues/16833) - Compiling with reduced boards manifests in favour of Autoconfig [#16848](https://github.com/arendst/Tasmota/issues/16848) - ADE7953 monitoring from instant power to accumulated energy [#16941](https://github.com/arendst/Tasmota/issues/16941) +- TuyaMcu rewrite by btsimonh [#17051](https://github.com/arendst/Tasmota/issues/17051) ### Fixed - Serial bridge default serial configuration from 5N1 to 8N1 regression from v10.1.0.3 diff --git a/tasmota/tasmota.ino b/tasmota/tasmota.ino index 688deed4a..4681a56ef 100644 --- a/tasmota/tasmota.ino +++ b/tasmota/tasmota.ino @@ -331,6 +331,7 @@ struct TasmotaGlobal_t { uint8_t module_type; // Current copy of Settings->module or user template type uint8_t emulated_module_type; // Emulated module type as requested by ESP32 uint8_t last_source; // Last command source + uint8_t last_command_source; // Last command source uint8_t shutters_present; // Number of actual define shutters uint8_t discovery_counter; // Delayed discovery counter uint8_t power_on_delay; // Delay relay power on to reduce power surge (SetOption47) @@ -681,7 +682,7 @@ void SleepDelay(uint32_t mseconds) { if (!TasmotaGlobal.backlog_nodelay && mseconds) { uint32_t wait = millis() + mseconds; while (!TimeReached(wait) && !Serial.available()) { // We need to service serial buffer ASAP as otherwise we get uart buffer overrun - XdrvCall(FUNC_SLEEP_LOOP); + XdrvCall(FUNC_SLEEP_LOOP); // Main purpose is reacting ASAP on serial data availability or interrupt handling (ADE7880) delay(1); } } else { diff --git a/tasmota/tasmota_support/support_command.ino b/tasmota/tasmota_support/support_command.ino index 6086572db..aaedcf494 100644 --- a/tasmota/tasmota_support/support_command.ino +++ b/tasmota/tasmota_support/support_command.ino @@ -329,6 +329,7 @@ void ExecuteCommand(const char *cmnd, uint32_t source) // cmnd: "var1=1" = stopic "var1" and svalue "=1" SHOW_FREE_MEM(PSTR("ExecuteCommand")); ShowSource(source); + TasmotaGlobal.last_command_source = source; const char *pos = cmnd; while (*pos && isspace(*pos)) { diff --git a/tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu.ino b/tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu_v1.ino similarity index 99% rename from tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu.ino rename to tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu_v1.ino index 35ef17ccc..6bd5a655e 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu_v1.ino @@ -18,7 +18,10 @@ */ #ifdef USE_LIGHT -#ifdef USE_TUYA_MCU +#ifdef USE_TUYA_MCU_V1 +/*********************************************************************************************\ + * Tuya MCU V1 +\*********************************************************************************************/ #define XDRV_16 16 #define XNRG_32 32 // Needs to be the last XNRG_xx @@ -551,7 +554,7 @@ void TuyaSendHexString(uint8_t id, char data[]) { TuyaSendCmd(TUYA_CMD_SET_DP, payload_buffer, payload_len); } -void TuyaSendString(uint8_t id, char data[]) { +void TuyaSendString(uint8_t id, const char data[]) { uint16_t len = strlen(data); uint16_t payload_len = 4 + len; diff --git a/tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu_v2.ino b/tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu_v2.ino new file mode 100644 index 000000000..21df2e281 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_16_tuyamcu_v2.ino @@ -0,0 +1,2579 @@ +/* + 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 . +*/ + +#ifdef USE_LIGHT +#ifdef USE_TUYA_MCU +/*********************************************************************************************\ + * 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_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 TUYA_MORE_DEBUG + + + +#include + + +#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 + +}; + + +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 + + 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 *)malloc(sizeof(TUYA_STRUCT)); + if (!pTuya) return 0; + memset(pTuya, 0, sizeof(TUYA_STRUCT)); + 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 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<dimDelay_ms[dimindex]); + pTuya->dimDebug &= (0xff ^ (1<dimCmdEnable & (1<dimDebug & (1<dimDebug &= (0xff ^ (1<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<dimCmdEnable |= (1<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) { + AddLog(LOG_LEVEL_DEBUG, PSTR("T:fn%d Switch%d --> M%d T%d"),fnId, fnId - TUYA_MCU_FUNC_SWT1 + 1, value, SwitchGetVirtual(fnId - TUYA_MCU_FUNC_SWT1)); + + if (SwitchGetVirtual(fnId - TUYA_MCU_FUNC_SWT1) != value) { + SwitchSetVirtual(fnId - TUYA_MCU_FUNC_SWT1, value); + SwitchHandler(1); + } + } + //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; + TasmotaGlobal.devices_present++; + } + } + + 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); + TasmotaGlobal.devices_present++; + 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(); } + // 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; + } + } + // 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) +{ + if (!XdrvMailbox.index && ((PRESSED == XdrvMailbox.payload) && (NOT_PRESSED == Button.last_state[XdrvMailbox.index]))) { + AddLog(LOG_LEVEL_DEBUG, PSTR("TYA: Reset GPIO triggered")); + TuyaResetWifi(); + return true; // Reset GPIO served here + } + return false; // 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("
")); + char stemp[33]; + snprintf_P(stemp, sizeof(stemp), PSTR("" D_JSON_IRHVAC_MODE "")); + WSContentSend_P(HTTP_DEVICE_CONTROL, 26, TasmotaGlobal.devices_present + 1, + (strlen(SettingsText(SET_BUTTON1 + TasmotaGlobal.devices_present))) ? SettingsText(SET_BUTTON1 + TasmotaGlobal.devices_present) : stemp, ""); + WSContentSend_P(PSTR("")); + } +} + +#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 + } + } + return result; +} + +#endif // USE_TUYA_MCU +#endif // USE_LIGHT \ No newline at end of file