diff --git a/sonoff/_changelog.ino b/sonoff/_changelog.ino index d2bdd4751..b94815cd0 100644 --- a/sonoff/_changelog.ino +++ b/sonoff/_changelog.ino @@ -1,6 +1,7 @@ /*********************************************************************************************\ * 6.6.0.9 20190828 * Change theoretical baudrate range to 300..19660500 bps in 300 increments (#6294) + * Add Full support of all protocols in IRremoteESP8266, to be used on dedicated-IR Tasmota version. Warning: +81k Flash when compiling with USE_IR_REMOTE_FULL * * 6.6.0.8 20190827 * Add Tuya Energy monitoring by Shantur Rathore diff --git a/sonoff/i18n.h b/sonoff/i18n.h index cb68dcd4c..38f524e4c 100644 --- a/sonoff/i18n.h +++ b/sonoff/i18n.h @@ -382,14 +382,29 @@ #define D_JSON_IR_PROTOCOL "Protocol" #define D_JSON_IR_BITS "Bits" #define D_JSON_IR_DATA "Data" + #define D_JSON_IR_DATALSB "DataLSB" #define D_JSON_IR_RAWDATA "RawData" #define D_JSON_IR_REPEAT "Repeat" #define D_CMND_IRHVAC "IRHVAC" - #define D_JSON_IRHVAC_VENDOR "VENDOR" - #define D_JSON_IRHVAC_POWER "POWER" - #define D_JSON_IRHVAC_MODE "MODE" - #define D_JSON_IRHVAC_FANSPEED "FANSPEED" - #define D_JSON_IRHVAC_TEMP "TEMP" + #define D_JSON_IRHVAC_VENDOR "Vendor" + #define D_JSON_IRHVAC_PROTOCOL "Protocol" + #define D_JSON_IRHVAC_MODEL "Model" + #define D_JSON_IRHVAC_POWER "Power" + #define D_JSON_IRHVAC_MODE "Mode" + #define D_JSON_IRHVAC_FANSPEED "FanSpeed" + #define D_JSON_IRHVAC_TEMP "Temp" + #define D_JSON_IRHVAC_CELSIUS "Celsius" + #define D_JSON_IRHVAC_SWINGV "SwingV" + #define D_JSON_IRHVAC_SWINGH "SwingH" + #define D_JSON_IRHVAC_LIGHT "Light" + #define D_JSON_IRHVAC_BEEP "Beep" + #define D_JSON_IRHVAC_ECONO "Econo" + #define D_JSON_IRHVAC_FILTER "Filter" + #define D_JSON_IRHVAC_TURBO "Turbo" + #define D_JSON_IRHVAC_QUIET "Quiet" + #define D_JSON_IRHVAC_CLEAN "Clean" + #define D_JSON_IRHVAC_SLEEP "Sleep" + #define D_JSON_IRHVAC_CLOCK "Clock" #define D_JSON_IRRECEIVED "IrReceived" // Commands xdrv_06_snfbridge.ino diff --git a/sonoff/my_user_config.h b/sonoff/my_user_config.h index 8e180a6f6..8b72d0f27 100644 --- a/sonoff/my_user_config.h +++ b/sonoff/my_user_config.h @@ -450,6 +450,8 @@ #define MAX31865_PTD_BIAS 0 // To calibrate your not-so-good PTD // -- IR Remote features -------------------------- +//#define USE_IR_REMOTE_FULL // Activate all protocols from IRremoteESP8266 + // actovating this option will ignore all other USE_IR_REMOTE_* options and set them all to active #define USE_IR_REMOTE // Send IR remote commands using library IRremoteESP8266 and ArduinoJson (+4k3 code, 0k3 mem, 48 iram) // #define USE_IR_SEND_AIWA // Support IRsend Aiwa protocol #define USE_IR_SEND_DISH // Support IRsend Dish protocol diff --git a/sonoff/sonoff_template.h b/sonoff/sonoff_template.h index f83f488ad..402ca22a2 100644 --- a/sonoff/sonoff_template.h +++ b/sonoff/sonoff_template.h @@ -527,9 +527,9 @@ const uint8_t kGpioNiceList[] PROGMEM = { #if defined(USE_LIGHT) && defined(USE_WS2812) GPIO_WS2812, // WS2812 Led string #endif -#ifdef USE_IR_REMOTE +#if defined(USE_IR_REMOTE) || defined(USE_IR_REMOTE_FULL) GPIO_IRSEND, // IR remote -#ifdef USE_IR_RECEIVE +#if defined(USE_IR_RECEIVE) || defined(USE_IR_REMOTE_FULL) GPIO_IRRECV, // IR receiver #endif #endif diff --git a/sonoff/support_command.ino b/sonoff/support_command.ino index 5d6d265f6..6c2dc78a8 100644 --- a/sonoff/support_command.ino +++ b/sonoff/support_command.ino @@ -647,7 +647,7 @@ void CmndSetoption(void) LightUpdateColorMapping(); break; #endif -#if defined(USE_IR_REMOTE) && defined(USE_IR_RECEIVE) +#if (defined(USE_IR_REMOTE) && defined(USE_IR_RECEIVE)) || defined(USE_IR_REMOTE_FULL) case P_IR_UNKNOW_THRESHOLD: IrReceiveUpdateThreshold(); break; diff --git a/sonoff/support_features.ino b/sonoff/support_features.ino index dfccdbd33..42144d191 100644 --- a/sonoff/support_features.ino +++ b/sonoff/support_features.ino @@ -76,7 +76,7 @@ void GetFeatures(void) #ifdef USE_WS2812_DMA feature_drv1 |= 0x00010000; // xdrv_04_light.ino #endif -#ifdef USE_IR_REMOTE +#if defined(USE_IR_REMOTE) || defined(USE_IR_REMOTE_FULL) feature_drv1 |= 0x00020000; // xdrv_05_irremote.ino #endif #ifdef USE_IR_HVAC diff --git a/sonoff/xdrv_05_irremote.ino b/sonoff/xdrv_05_irremote.ino index cb60f2af3..59b152a32 100644 --- a/sonoff/xdrv_05_irremote.ino +++ b/sonoff/xdrv_05_irremote.ino @@ -17,7 +17,7 @@ along with this program. If not, see . */ -#ifdef USE_IR_REMOTE +#if defined(USE_IR_REMOTE) && !defined(USE_IR_REMOTE_FULL) /*********************************************************************************************\ * IR Remote send and receive using IRremoteESP8266 library \*********************************************************************************************/ @@ -671,6 +671,7 @@ uint32_t IrRemoteCmndIrHvacJson(void) const char *HVAC_Mode; const char *HVAC_FanMode; const char *HVAC_Vendor; + char parm_uc[12]; int HVAC_Temp = 21; bool HVAC_Power = true; @@ -687,11 +688,11 @@ uint32_t IrRemoteCmndIrHvacJson(void) return IE_INVALID_JSON; } - HVAC_Vendor = root[D_JSON_IRHVAC_VENDOR]; - HVAC_Power = root[D_JSON_IRHVAC_POWER]; - HVAC_Mode = root[D_JSON_IRHVAC_MODE]; - HVAC_FanMode = root[D_JSON_IRHVAC_FANSPEED]; - HVAC_Temp = root[D_JSON_IRHVAC_TEMP]; + HVAC_Vendor = root[UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_VENDOR))]; + HVAC_Power = root[UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_POWER))]; + HVAC_Mode = root[UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_MODE))]; + HVAC_FanMode = root[UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_FANSPEED))]; + HVAC_Temp = root[UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_TEMP))]; // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("IRHVAC: Received Vendor %s, Power %d, Mode %s, FanSpeed %s, Temp %d"), HVAC_Vendor, HVAC_Power, HVAC_Mode, HVAC_FanMode, HVAC_Temp); @@ -1073,4 +1074,4 @@ bool Xdrv05(uint8_t function) return result; } -#endif // USE_IR_REMOTE +#endif // defined(USE_IR_REMOTE) && !defined(USE_IR_REMOTE_FULL) diff --git a/sonoff/xdrv_05_irremote_full.ino b/sonoff/xdrv_05_irremote_full.ino new file mode 100644 index 000000000..48dfa1498 --- /dev/null +++ b/sonoff/xdrv_05_irremote_full.ino @@ -0,0 +1,682 @@ +/* + xdrv_05_irremote_full.ino - complete intefration of IRremoteESP8266 + + Copyright (C) 2019 Heiko Krupp, Lazar Obradovic, Theo Arends, Stephan Hadinger + + 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_IR_REMOTE_FULL +/*********************************************************************************************\ + * IR Remote send and receive using IRremoteESP8266 library +\*********************************************************************************************/ + +#define XDRV_05 5 + +#include +#include +#include +#include +#include + +enum IrErrors { IE_RESPONSE_PROVIDED, IE_NO_ERROR, IE_INVALID_RAWDATA, IE_INVALID_JSON, IE_SYNTAX_IRSEND, IE_SYNTAX_IRHVAC, + IE_UNSUPPORTED_HVAC, IE_UNSUPPORTED_PROTOCOL }; + +const char kIrRemoteCommands[] PROGMEM = "|" D_CMND_IRHVAC "|" D_CMND_IRSEND ; // No prefix + +void (* const IrRemoteCommand[])(void) PROGMEM = { &CmndIrHvac, &CmndIrSend }; + +/*********************************************************************************************\ + * IR Send +\*********************************************************************************************/ + +IRsend *irsend = nullptr; +bool irsend_active = false; + +void IrSendInit(void) +{ + irsend = new IRsend(pin[GPIO_IRSEND]); // an IR led is at GPIO_IRSEND + irsend->begin(); +} + +// from https://stackoverflow.com/questions/2602823/in-c-c-whats-the-simplest-way-to-reverse-the-order-of-bits-in-a-byte +// First the left four bits are swapped with the right four bits. Then all adjacent pairs are swapped and then all adjacent single bits. This results in a reversed order. +uint8_t reverseBitsInByte(uint8_t b) { + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; + b = (b & 0xCC) >> 2 | (b & 0x33) << 2; + b = (b & 0xAA) >> 1 | (b & 0x55) << 1; + return b; +} + +// reverse bits in each byte +uint64_t reverseBitsInBytes64(uint64_t b) { + union { + uint8_t b[8]; + uint64_t i; + } a; + a.i = b; + for (uint32_t i=0; i<8; i++) { + a.b[i] = reverseBitsInByte(a.b[i]); + } + return a.i; +} + +char* IrUint64toHex(uint64_t value, char *str, uint16_t bits) +{ + ulltoa(value, str, 16); // Get 64bit value + + int fill = 8; + if ((bits > 3) && (bits < 65)) { + fill = bits / 4; // Max 16 + if (bits % 4) { fill++; } + } + int len = strlen(str); + fill -= len; + if (fill > 0) { + memmove(str + fill, str, len +1); + memset(str, '0', fill); + } + memmove(str + 2, str, strlen(str) +1); + str[0] = '0'; + str[1] = 'x'; + return str; +} + +/*********************************************************************************************\ + * IR Receive +\*********************************************************************************************/ + +const bool IR_FULL_RCV_SAVE_BUFFER = false; // false = do not use buffer, true = use buffer for decoding +const uint32_t IR_TIME_AVOID_DUPLICATE = 500; // Milliseconds + +// Below is from IRrecvDumpV2.ino +// As this program is a special purpose capture/decoder, let us use a larger +// than normal buffer so we can handle Air Conditioner remote codes. +const uint16_t IR_FULL_BUFFER_SIZE = 1024; + +// Some A/C units have gaps in their protocols of ~40ms. e.g. Kelvinator +// A value this large may swallow repeats of some protocols +const uint8_t IR__FULL_RCV_TIMEOUT = 50; + +IRrecv *irrecv = nullptr; + +unsigned long ir_lasttime = 0; + +void IrReceiveUpdateThreshold() +{ + if (irrecv != nullptr) { + if (Settings.param[P_IR_UNKNOW_THRESHOLD] < 6) { Settings.param[P_IR_UNKNOW_THRESHOLD] = 6; } + irrecv->setUnknownThreshold(Settings.param[P_IR_UNKNOW_THRESHOLD]); + } +} + +void IrReceiveInit(void) +{ + // an IR led is at GPIO_IRRECV + irrecv = new IRrecv(pin[GPIO_IRRECV], IR_FULL_BUFFER_SIZE, IR__FULL_RCV_TIMEOUT, IR_FULL_RCV_SAVE_BUFFER); + irrecv->setUnknownThreshold(Settings.param[P_IR_UNKNOW_THRESHOLD]); + irrecv->enableIRIn(); // Start the receiver +} + +String sendACJsonState(const stdAc::state_t &state) { + DynamicJsonBuffer jsonBuffer; + JsonObject& json = jsonBuffer.createObject(); + json[D_JSON_IRHVAC_VENDOR] = typeToString(state.protocol); + json[D_JSON_IRHVAC_MODEL] = state.model; + json[D_JSON_IRHVAC_POWER] = IRac::boolToString(state.power); + json[D_JSON_IRHVAC_MODE] = IRac::opmodeToString(state.mode); + // Home Assistant wants mode to be off if power is also off & vice-versa. + if (state.mode == stdAc::opmode_t::kOff || !state.power) { + json[D_JSON_IRHVAC_MODE] = IRac::opmodeToString(stdAc::opmode_t::kOff); + json[D_JSON_IRHVAC_POWER] = IRac::boolToString(false); + } + json[D_JSON_IRHVAC_CELSIUS] = IRac::boolToString(state.celsius); + if (floorf(state.degrees) == state.degrees) { + json[D_JSON_IRHVAC_TEMP] = floorf(state.degrees); // integer + } else { + json[D_JSON_IRHVAC_TEMP] = RawJson(String(state.degrees, 1)); // non-integer, limit to only 1 sub-digit + } + json[D_JSON_IRHVAC_FANSPEED] = IRac::fanspeedToString(state.fanspeed); + json[D_JSON_IRHVAC_SWINGV] = IRac::swingvToString(state.swingv); + json[D_JSON_IRHVAC_SWINGH] = IRac::swinghToString(state.swingh); + json[D_JSON_IRHVAC_QUIET] = IRac::boolToString(state.quiet); + json[D_JSON_IRHVAC_TURBO] = IRac::boolToString(state.turbo); + json[D_JSON_IRHVAC_ECONO] = IRac::boolToString(state.econo); + json[D_JSON_IRHVAC_LIGHT] = IRac::boolToString(state.light); + json[D_JSON_IRHVAC_FILTER] = IRac::boolToString(state.filter); + json[D_JSON_IRHVAC_CLEAN] = IRac::boolToString(state.clean); + json[D_JSON_IRHVAC_BEEP] = IRac::boolToString(state.beep); + json[D_JSON_IRHVAC_SLEEP] = state.sleep; + + String payload = ""; + payload.reserve(200); + json.printTo(payload); + return payload; +} + +String sendIRJsonState(const struct decode_results &results) { + String json("{"); + json += "\"" D_JSON_IR_PROTOCOL "\":\""; + json += typeToString(results.decode_type); + json += "\",\"" D_JSON_IR_BITS "\":"; + json += results.bits; + + if (hasACState(results.decode_type)) { + json += ",\"" D_JSON_IR_DATA "\":\"0x"; + json += resultToHexidecimal(&results); + json += "\""; + } else { + json += ",\"" D_JSON_IR_DATA "\":"; + if (Settings.flag.ir_receive_decimal) { + char svalue[32]; + ulltoa(results.value, svalue, 10); + json += svalue; + } else { + char hvalue[64]; + IrUint64toHex(results.value, hvalue, results.bits); // Get 64bit value as hex 0x00123456 + json += "\""; + json += hvalue; + json += "\",\"" D_JSON_IR_DATALSB "\":\""; + IrUint64toHex(reverseBitsInBytes64(results.value), hvalue, results.bits); // Get 64bit value as hex 0x00123456, LSB + json += hvalue; + json += "\""; + } + } + json += ",\"" D_JSON_IR_REPEAT "\":"; + json += results.repeat; + + stdAc::state_t ac_result; + if (IRAcUtils::decodeToState(&results, &ac_result, nullptr)) { + // we have a decoded state + json += ",\"" D_CMND_IRHVAC "\":"; + json += sendACJsonState(ac_result); + } + + return json; +} + +void IrReceiveCheck(void) +{ + char sirtype[14]; // Max is AIWA_RC_T501 + int8_t iridx = 0; + + decode_results results; + + if (irrecv->decode(&results)) { + uint32_t now = millis(); + + +// if ((now - ir_lasttime > IR_TIME_AVOID_DUPLICATE) && (UNKNOWN != results.decode_type) && (results.bits > 0)) { + if (!irsend_active && (now - ir_lasttime > IR_TIME_AVOID_DUPLICATE)) { + ir_lasttime = now; + Response_P(PSTR("{\"" D_JSON_IRRECEIVED "\":%s"), sendIRJsonState(results).c_str()); + + if (Settings.flag3.receive_raw) { + ResponseAppend_P(PSTR(",\"" D_JSON_IR_RAWDATA "\":[")); + uint16_t i; + for (i = 1; i < results.rawlen; i++) { + if (i > 1) { ResponseAppend_P(PSTR(",")); } + uint32_t usecs; + for (usecs = results.rawbuf[i] * kRawTick; usecs > UINT16_MAX; usecs -= UINT16_MAX) { + ResponseAppend_P(PSTR("%d,0,"), UINT16_MAX); + } + ResponseAppend_P(PSTR("%d"), usecs); + if (strlen(mqtt_data) > sizeof(mqtt_data) - 40) { break; } // Quit if char string becomes too long + } + uint16_t extended_length = results.rawlen - 1; + for (uint32_t j = 0; j < results.rawlen - 1; j++) { + uint32_t usecs = results.rawbuf[j] * kRawTick; + // Add two extra entries for multiple larger than UINT16_MAX it is. + extended_length += (usecs / (UINT16_MAX + 1)) * 2; + } + ResponseAppend_P(PSTR("],\"" D_JSON_IR_RAWDATA "Info\":[%d,%d,%d]"), extended_length, i -1, results.overflow); + } + + ResponseAppend_P(PSTR("}}")); + MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_IRRECEIVED)); + + if (iridx) { + XdrvRulesProcess(); +#ifdef USE_DOMOTICZ + unsigned long value = results.value | (iridx << 28); // [Protocol:4, Data:28] + DomoticzSensor(DZ_COUNT, value); // Send data as Domoticz Counter value +#endif // USE_DOMOTICZ + } + } + + irrecv->resume(); + } +} + + +/*********************************************************************************************\ + * IR Heating, Ventilation and Air Conditioning +\*********************************************************************************************/ + +// list all supported protocols, either for IRSend or for IRHVAC, separated by '|' +String listSupportedProtocols(bool hvac) { + String l(""); + bool first = true; + for (uint32_t i = UNUSED + 1; i <= kLastDecodeType; i++) { + bool found = false; + if (hvac) { + found = IRac::isProtocolSupported((decode_type_t)i); + } else { + found = (IRsend::defaultBits((decode_type_t)i) > 0) && (!IRac::isProtocolSupported((decode_type_t)i)); + } + if (found) { + if (first) { + first = false; + } else { + l += "|"; + } + l += typeToString((decode_type_t)i); + } + } + return l; +} + +// used to convert values 0-5 to fanspeed_t +const stdAc::fanspeed_t IrHvacFanSpeed[] PROGMEM = { stdAc::fanspeed_t::kAuto, + stdAc::fanspeed_t::kMin, stdAc::fanspeed_t::kLow,stdAc::fanspeed_t::kMedium, + stdAc::fanspeed_t::kHigh, stdAc::fanspeed_t::kMax }; + +uint32_t IrRemoteCmndIrHvacJson(void) +{ + stdAc::state_t state, prev; + char parm_uc[12]; + + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("IRHVAC: Received %s"), XdrvMailbox.data); + char dataBufUc[XdrvMailbox.data_len]; + UpperCase(dataBufUc, XdrvMailbox.data); + RemoveSpace(dataBufUc); + if (strlen(dataBufUc) < 8) { return IE_INVALID_JSON; } + + DynamicJsonBuffer jsonBuf; + JsonObject &json = jsonBuf.parseObject(dataBufUc); + if (!json.success()) { return IE_INVALID_JSON; } + + // from: https://github.com/crankyoldgit/IRremoteESP8266/blob/master/examples/CommonAcControl/CommonAcControl.ino + state.protocol = decode_type_t::UNKNOWN; + state.model = 1; // Some A/C's have different models. Let's try using just 1. + state.mode = stdAc::opmode_t::kAuto; // Run in cool mode initially. + state.power = false; // Initially start with the unit off. + state.celsius = true; // Use Celsius for units of temp. False = Fahrenheit + state.degrees = 21.0f; // 21 degrees. + state.fanspeed = stdAc::fanspeed_t::kMedium; // Start with the fan at medium. + state.swingv = stdAc::swingv_t::kOff; // Don't swing the fan up or down. + state.swingh = stdAc::swingh_t::kOff; // Don't swing the fan left or right. + state.light = false; // Turn off any LED/Lights/Display that we can. + state.beep = false; // Turn off any beep from the A/C if we can. + state.econo = false; // Turn off any economy modes if we can. + state.filter = false; // Turn off any Ion/Mold/Health filters if we can. + state.turbo = false; // Don't use any turbo/powerful/etc modes. + state.quiet = false; // Don't use any quiet/silent/etc modes. + state.sleep = -1; // Don't set any sleep time or modes. + state.clean = false; // Turn off any Cleaning options if we can. + state.clock = -1; // Don't set any current time if we can avoid it. + + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_VENDOR)); + if (json.containsKey(parm_uc)) { state.protocol = strToDecodeType(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_PROTOCOL)); + if (json.containsKey(parm_uc)) { state.protocol = strToDecodeType(json[parm_uc]); } // also support 'protocol' + if (decode_type_t::UNKNOWN == state.protocol) { return IE_UNSUPPORTED_HVAC; } + if (!IRac::isProtocolSupported(state.protocol)) { return IE_UNSUPPORTED_HVAC; } + + // for fan speed, we also support 1-5 values + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_FANSPEED)); + if (json.containsKey(parm_uc)) { + uint32_t fan_speed = json[parm_uc]; + if ((fan_speed >= 1) && (fan_speed <= 5)) { + state.fanspeed = (stdAc::fanspeed_t) pgm_read_byte(&IrHvacFanSpeed[fan_speed]); + } else { + state.fanspeed = IRac::strToFanspeed(json[parm_uc]); + } + } + + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_MODEL)); + if (json.containsKey(parm_uc)) { state.model = IRac::strToModel(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_MODE)); + if (json.containsKey(parm_uc)) { state.mode = IRac::strToOpmode(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_SWINGV)); + if (json.containsKey(parm_uc)) { state.swingv = IRac::strToSwingV(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_SWINGH)); + if (json.containsKey(parm_uc)) { state.swingh = IRac::strToSwingH(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_TEMP)); + if (json.containsKey(parm_uc)) { state.degrees = json[parm_uc]; } + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("model %d, mode %d, fanspeed %d, swingv %d, swingh %d"), + // state.model, state.mode, state.fanspeed, state.swingv, state.swingh); + + // decode booleans + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_POWER)); + if (json.containsKey(parm_uc)) { state.power = IRac::strToBool(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_CELSIUS)); + if (json.containsKey(parm_uc)) { state.celsius = IRac::strToBool(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_LIGHT)); + if (json.containsKey(parm_uc)) { state.light = IRac::strToBool(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_BEEP)); + if (json.containsKey(parm_uc)) { state.beep = IRac::strToBool(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_ECONO)); + if (json.containsKey(parm_uc)) { state.econo = IRac::strToBool(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_FILTER)); + if (json.containsKey(parm_uc)) { state.filter = IRac::strToBool(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_TURBO)); + if (json.containsKey(parm_uc)) { state.turbo = IRac::strToBool(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_QUIET)); + if (json.containsKey(parm_uc)) { state.quiet = IRac::strToBool(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_CLEAN)); + if (json.containsKey(parm_uc)) { state.clean = IRac::strToBool(json[parm_uc]); } + + // optional timer and clock + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_SLEEP)); + if (json[parm_uc]) { state.sleep = json[parm_uc]; } + //if (json[D_JSON_IRHVAC_CLOCK]) { state.clock = json[D_JSON_IRHVAC_CLOCK]; } // not sure it's useful to support 'clock' + + IRac ac(pin[GPIO_IRSEND]); + bool success = ac.sendAc(state, &prev); + if (!success) { return IE_SYNTAX_IRHVAC; } + + Response_P(PSTR("{\"" D_CMND_IRHVAC "\":%s}"), sendACJsonState(state).c_str()); + return IE_RESPONSE_PROVIDED; +} + +void CmndIrHvac(void) +{ + uint8_t error = IE_SYNTAX_IRHVAC; + + if (XdrvMailbox.data_len) { + error = IrRemoteCmndIrHvacJson(); + } + if (error != IE_RESPONSE_PROVIDED) { IrRemoteCmndResponse(error); } // otherwise response was already provided +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +uint32_t IrRemoteCmndIrSendJson(void) +{ + char parm_uc[12]; // used to convert JSON keys to uppercase + // ArduinoJSON entry used to calculate jsonBuf: JSON_OBJECT_SIZE(3) + 40 = 96 + // IRsend { "protocol": "RC5", "bits": 12, "data":"0xC86" } + // IRsend { "protocol": "SAMSUNG", "bits": 32, "data": 551502015 } + char dataBufUc[XdrvMailbox.data_len]; + UpperCase(dataBufUc, XdrvMailbox.data); + RemoveSpace(dataBufUc); + if (strlen(dataBufUc) < 8) { return IE_INVALID_JSON; } + + DynamicJsonBuffer jsonBuf; + JsonObject &json = jsonBuf.parseObject(dataBufUc); + if (!json.success()) { return IE_INVALID_JSON; } + + // IRsend { "protocol": "SAMSUNG", "bits": 32, "data": 551502015 } + // IRsend { "protocol": "NEC", "bits": 32, "data":"0x02FDFE80", "repeat": 2 } + decode_type_t protocol = decode_type_t::UNKNOWN; + uint16_t bits = 0; + uint64_t data; + uint8_t repeat = 0; + + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_VENDOR)); + if (json.containsKey(parm_uc)) { protocol = strToDecodeType(json[parm_uc]); } + UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_PROTOCOL)); + if (json.containsKey(parm_uc)) { protocol = strToDecodeType(json[parm_uc]); } // also support 'protocol' + if (decode_type_t::UNKNOWN == protocol) { return IE_UNSUPPORTED_PROTOCOL; } + + UpperCase_P(parm_uc, PSTR(D_JSON_IR_BITS)); + if (json.containsKey(parm_uc)) { bits = json[parm_uc]; } + UpperCase_P(parm_uc, PSTR(D_JSON_IR_REPEAT)); + if (json.containsKey(parm_uc)) { repeat = json[parm_uc]; } + UpperCase_P(parm_uc, PSTR(D_JSON_IR_DATALSB)); // accept LSB values + if (json.containsKey(parm_uc)) { data = reverseBitsInBytes64(strtoull(json[parm_uc], nullptr, 0)); } + UpperCase_P(parm_uc, PSTR(D_JSON_IR_DATA)); // or classical MSB (takes priority) + if (json.containsKey(parm_uc)) { data = strtoull(json[parm_uc], nullptr, 0); } + if (0 == bits) { return IE_SYNTAX_IRSEND; } + + // check if the IRSend is greater than repeat, but can be overriden with JSON + if (XdrvMailbox.index > repeat + 1) { repeat = XdrvMailbox.index - 1; } + + char dvalue[32]; + char hvalue[32]; + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("IRS: protocol %d, bits %d, data %s (%s), repeat %d"), + protocol, bits, ulltoa(data, dvalue, 10), IrUint64toHex(data, hvalue, bits), repeat); + + irsend_active = true; // deactivate receive + bool success = irsend->send(protocol, data, bits, repeat); + + if (!success) { + irsend_active = false; + ResponseCmndChar(D_JSON_PROTOCOL_NOT_SUPPORTED); + } + return IE_NO_ERROR; +} + +uint32_t IrRemoteCmndIrSendRaw(void) +{ + // IRsend ,, ... + // or + // IRsend raw,,, (one space = zero space *2) + // IRsend raw,,,, + // IRsend raw,,,, + // IRsend raw,,
,
,,,, + + char *p; + char *str = strtok_r(XdrvMailbox.data, ", ", &p); + if (p == nullptr) { + return IE_INVALID_RAWDATA; + } + + // repeat + uint16_t repeat = XdrvMailbox.index > 0 ? XdrvMailbox.index - 1 : 0; + + uint16_t freq = atoi(str); + if (!freq && (*str != '0')) { // First parameter is any string + uint16_t count = 0; + char *q = p; + for (; *q; count += (*q++ == ',')); + if (count < 2) { + return IE_INVALID_RAWDATA; + } // Parameters must be at least 3 + + uint16_t parm[count]; + for (uint32_t i = 0; i < count; i++) { + parm[i] = strtol(strtok_r(nullptr, ", ", &p), nullptr, 0); + if (!parm[i]) { + if (!i) { + parm[0] = 38000; // Frequency default to 38kHz + } else { + return IE_INVALID_RAWDATA; // Other parameters may not be 0 + } + } + } + + uint16_t i = 0; + if (count < 4) { + // IRsend raw,0,889,000000100110000001001 + uint16_t mark = parm[1] *2; // Protocol where 0 = t, 1 = 2t (RC5) + if (3 == count) { + if (parm[2] < parm[1]) { + // IRsend raw,0,889,2,000000100110000001001 + mark = parm[1] * parm[2]; // Protocol where 0 = t1, 1 = t1*t2 (Could be RC5) + } else { + // IRsend raw,0,889,1778,000000100110000001001 + mark = parm[2]; // Protocol where 0 = t1, 1 = t2 (Could be RC5) + } + } + uint16_t raw_array[strlen(p)]; // Bits + for (; *p; *p++) { + if (*p == '0') { + raw_array[i++] = parm[1]; // Space + } + else if (*p == '1') { + raw_array[i++] = mark; // Mark + } + } + irsend_active = true; + for (uint32_t r = 0; r <= repeat; r++) { + irsend->sendRaw(raw_array, i, parm[0]); + if (r < repeat) { // if it's not the last message + irsend->space(40000); // since we don't know the inter-message gap, place an arbitrary 40ms gap + } + } + } + else if (6 == count) { // NEC Protocol + // IRsend raw,0,8620,4260,544,411,1496,010101101000111011001110000000001100110000000001100000000000000010001100 + uint16_t raw_array[strlen(p)*2+3]; // Header + bits + end + raw_array[i++] = parm[1]; // Header mark + raw_array[i++] = parm[2]; // Header space + uint32_t inter_message_32 = (parm[1] + parm[2]) * 3; // compute an inter-message gap (32 bits) + uint16_t inter_message = (inter_message_32 > 65000) ? 65000 : inter_message_32; // avoid 16 bits overflow + for (; *p; *p++) { + if (*p == '0') { + raw_array[i++] = parm[3]; // Bit mark + raw_array[i++] = parm[4]; // Zero space + } + else if (*p == '1') { + raw_array[i++] = parm[3]; // Bit mark + raw_array[i++] = parm[5]; // One space + } + } + raw_array[i++] = parm[3]; // Trailing mark + irsend_active = true; + for (uint32_t r = 0; r <= repeat; r++) { + irsend->sendRaw(raw_array, i, parm[0]); + if (r < repeat) { // if it's not the last message + irsend->space(inter_message); // since we don't know the inter-message gap, place an arbitrary 40ms gap + } + } + } + else { + return IE_INVALID_RAWDATA; // Invalid number of parameters + } + } else { + if (!freq) { freq = 38000; } // Default to 38kHz + uint16_t count = 0; + char *q = p; + for (; *q; count += (*q++ == ',')); + if (0 == count) { + return IE_INVALID_RAWDATA; + } + + // IRsend 0,896,876,900,888,894,876,1790,874,872,1810,1736,948,872,880,872,936,872,1792,900,888,1734 + count++; + if (count < 200) { + uint16_t raw_array[count]; // It's safe to use stack for up to 200 packets (limited by mqtt_data length) + for (uint32_t i = 0; i < count; i++) { + raw_array[i] = strtol(strtok_r(nullptr, ", ", &p), nullptr, 0); // Allow decimal (20496) and hexadecimal (0x5010) input + } + +// AddLog_P2(LOG_LEVEL_DEBUG, PSTR("DBG: stack count %d"), count); + + irsend_active = true; + for (uint32_t r = 0; r <= repeat; r++) { + irsend->sendRaw(raw_array, count, freq); + } + } else { + uint16_t *raw_array = reinterpret_cast(malloc(count * sizeof(uint16_t))); + if (raw_array == nullptr) { + return IE_INVALID_RAWDATA; + } + + for (uint32_t i = 0; i < count; i++) { + raw_array[i] = strtol(strtok_r(nullptr, ", ", &p), nullptr, 0); // Allow decimal (20496) and hexadecimal (0x5010) input + } + +// AddLog_P2(LOG_LEVEL_DEBUG, PSTR("DBG: heap count %d"), count); + + irsend_active = true; + for (uint32_t r = 0; r <= repeat; r++) { + irsend->sendRaw(raw_array, count, freq); + } + free(raw_array); + } + } + + return IE_NO_ERROR; +} + +void CmndIrSend(void) +{ + uint8_t error = IE_SYNTAX_IRSEND; + + if (XdrvMailbox.data_len) { + if (strstr(XdrvMailbox.data, "{") == nullptr) { + error = IrRemoteCmndIrSendRaw(); + } else { + error = IrRemoteCmndIrSendJson(); + } + } + IrRemoteCmndResponse(error); +} + +void IrRemoteCmndResponse(uint32_t error) +{ + switch (error) { + case IE_INVALID_RAWDATA: + ResponseCmndChar(D_JSON_INVALID_RAWDATA); + break; + case IE_INVALID_JSON: + ResponseCmndChar(D_JSON_INVALID_JSON); + break; + case IE_SYNTAX_IRSEND: + Response_P(PSTR("{\"" D_CMND_IRSEND "\":\"" D_JSON_NO " " D_JSON_IR_BITS " " D_JSON_OR " " D_JSON_IR_DATA "\"}")); + break; + case IE_SYNTAX_IRHVAC: + Response_P(PSTR("{\"" D_CMND_IRHVAC "\":\"" D_JSON_WRONG " " D_JSON_IRHVAC_VENDOR ", " D_JSON_IRHVAC_MODE " " D_JSON_OR " " D_JSON_IRHVAC_FANSPEED "\"}")); + break; + case IE_UNSUPPORTED_HVAC: + Response_P(PSTR("{\"" D_CMND_IRHVAC "\":\"" D_JSON_WRONG " " D_JSON_IRHVAC_VENDOR " (%s)\"}"), listSupportedProtocols(true).c_str()); + break; + case IE_UNSUPPORTED_PROTOCOL: + Response_P(PSTR("{\"" D_CMND_IRSEND "\":\"" D_JSON_WRONG " " D_JSON_IRHVAC_PROTOCOL " (%s)\"}"), listSupportedProtocols(false).c_str()); + break; + default: // IE_NO_ERROR + ResponseCmndDone(); + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv05(uint8_t function) +{ + bool result = false; + + if ((pin[GPIO_IRSEND] < 99) || (pin[GPIO_IRRECV] < 99)) { + switch (function) { + case FUNC_PRE_INIT: + if (pin[GPIO_IRSEND] < 99) { + IrSendInit(); + } + if (pin[GPIO_IRRECV] < 99) { + IrReceiveInit(); + } + break; + case FUNC_EVERY_50_MSECOND: + if (pin[GPIO_IRRECV] < 99) { + IrReceiveCheck(); // check if there's anything on IR side + } + irsend_active = false; // re-enable IR reception + break; + case FUNC_COMMAND: + if (pin[GPIO_IRSEND] < 99) { + result = DecodeCommand(kIrRemoteCommands, IrRemoteCommand); + } + break; + } + } + return result; +} + +#endif // USE_IR_REMOTE_FULL