IR: Extended the A/C protocol with Command/iFeel/SensorTemp (#18612)

* IR: Added support for new A/C protocol fields

New fields introduced in IRremoteESP8266 v2.8.5:
1. Command (one of: [Control | Config | iFeel | Timer], default: Control) - allows to set IR command type (e.g. iFeel for no-beep ambient sensor report)
2. iFeel (boolean, default: false) - configures whether iFeel (ambient sensor temp. is used by the A/C unit)
3. SensorTemp - the ambient sensor temperature reading to send

Note:
 - Fan setting via int is now 6-value (breaking change -> 6 is the new max)

* IR A/C: Model now pretty-printed to JSON

Model string is the default (falls back to int if n/a)

* IR A/C: Added separate JSON resp. per command type

* No change for "kControlCommand" (default/legacy).
* SensorReport/Timer/Config commands only contain relevant JSON nodes

Additionally:
 Clock has time semantics (input: minutes, output: HH:MM), except for "config" mode,
 where state.clock is used as ConfigKey and state.sleep is used as ConfigValue.

---------

Co-authored-by: Mateusz Bronk <2566147+mbronk@users.noreply.github.com>
This commit is contained in:
Mateusz Bronk 2023-05-09 21:35:56 +02:00 committed by GitHub
parent fec3a21c8a
commit 6a7b2b0c6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 93 additions and 33 deletions

View File

@ -123,6 +123,7 @@
#define D_JSON_MODEL "Model"
#define D_JSON_MOISTURE "Moisture"
#define D_JSON_MQTT_COUNT "MqttCount"
#define D_JSON_NA "n/a"
#define D_JSON_NO "No"
#define D_JSON_NOISE "Noise"
#define D_JSON_NONE "None"
@ -535,6 +536,7 @@
#define D_JSON_IRHVAC_VENDOR "Vendor"
#define D_JSON_IRHVAC_PROTOCOL "Protocol"
#define D_JSON_IRHVAC_MODEL "Model"
#define D_JSON_IRHVAC_COMMAND "Command"
#define D_JSON_IRHVAC_POWER "Power"
#define D_JSON_IRHVAC_MODE "Mode"
#define D_JSON_IRHVAC_FANSPEED "FanSpeed"
@ -551,6 +553,10 @@
#define D_JSON_IRHVAC_CLEAN "Clean"
#define D_JSON_IRHVAC_SLEEP "Sleep"
#define D_JSON_IRHVAC_CLOCK "Clock"
#define D_JSON_IRHVAC_IFEEL "iFeel"
#define D_JSON_IRHVAC_CONFIG_KEY "ConfigKey"
#define D_JSON_IRHVAC_CONFIG_VALUE "ConfigValue"
#define D_JSON_IRHVAC_SENSOR_TEMP "SensorTemp"
#define D_JSON_IRHVAC_STATE_MODE "StateMode"
#define D_JSON_IRHVAC_STATE_MODE_SEND_ONLY "SendOnly"
#define D_JSON_IRHVAC_STATE_MODE_STORE_ONLY "StoreOnly"

View File

@ -221,37 +221,77 @@ void IrReceiveInit(void)
irrecv->enableIRIn(); // Start the receiver
}
namespace {
void addFloatToJson(JsonGeneratorObject& json, const char* key, float value, float noValueConstant = NAN) {
if (!isnan(noValueConstant) && value == noValueConstant) {
//The "no sensor value" may not be straightforward (e.g.-100.0), hence replacing with explicit n/a
json.add(key, PSTR(D_JSON_NA));
return;
}
char s[6]; // Range: -99.9 <> 999.9 should be fine for any sensible temperature value :)
ext_snprintf_P(s, sizeof(s), PSTR("%-1_f"), &value);
json.addStrRaw(key, s);
}
void addModelToJson(JsonGeneratorObject& json, const char* key, decode_type_t protocol, const int16_t model) {
String modelStr = irutils::modelToStr(protocol, model);
if (modelStr != kUnknownStr) {
json.add(key, modelStr);
} else { // Fallback to int value
json.add(key, model);
}
}
} // namespace {
String sendACJsonState(const stdAc::state_t &state) {
JsonGeneratorObject json;
json.add(PSTR(D_JSON_IRHVAC_VENDOR), typeToString(state.protocol));
json.add(PSTR(D_JSON_IRHVAC_MODEL), state.model);
addModelToJson(json, PSTR(D_JSON_IRHVAC_MODEL), state.protocol, state.model);
json.add(PSTR(D_JSON_IRHVAC_COMMAND), IRac::commandTypeToString(state.command));
json.add(PSTR(D_JSON_IRHVAC_MODE), IRac::opmodeToString(state.mode));
// Home Assistant wants power to be off if mode is also off.
if (state.mode == stdAc::opmode_t::kOff) {
json.add(PSTR(D_JSON_IRHVAC_POWER), IRac::boolToString(false));
} else {
json.add(PSTR(D_JSON_IRHVAC_POWER), IRac::boolToString(state.power));
}
json.add(PSTR(D_JSON_IRHVAC_CELSIUS), IRac::boolToString(state.celsius));
if (floorf(state.degrees) == state.degrees) {
json.add(PSTR(D_JSON_IRHVAC_TEMP), (int32_t) floorf(state.degrees)); // integer
} else {
// TODO can do better here
json.addStrRaw(PSTR(D_JSON_IRHVAC_TEMP), String(state.degrees, 1).c_str()); // non-integer, limit to only 1 sub-digit
}
switch (state.command) {
case stdAc::ac_command_t::kSensorTempReport:
addFloatToJson(json, PSTR(D_JSON_IRHVAC_SENSOR_TEMP), state.sensorTemperature, kNoTempValue);
break;
case stdAc::ac_command_t::kConfigCommand:
// Note: for `kConfigCommand` the semantics of clock/sleep is abused IRremoteESP8266 lib-side for key/value pair
// Ref: lib/lib_basic/IRremoteESP8266/IRremoteESP8266/src/IRac.cpp[L3062-3066]
json.add(PSTR(D_JSON_IRHVAC_CONFIG_KEY), state.clock);
json.add(PSTR(D_JSON_IRHVAC_CONFIG_VALUE), state.sleep);
break;
case stdAc::ac_command_t::kTimerCommand:
json.add(PSTR(D_JSON_IRHVAC_POWER), IRac::boolToString(state.power));
if(state.clock != -1) { json.add(PSTR(D_JSON_IRHVAC_CLOCK), irutils::minsToString(state.clock)); }
json.add(PSTR(D_JSON_IRHVAC_SLEEP), state.sleep);
break;
case stdAc::ac_command_t::kControlCommand:
default:
json.add(PSTR(D_JSON_IRHVAC_MODE), IRac::opmodeToString(state.mode));
// Home Assistant wants power to be off if mode is also off.
if (state.mode == stdAc::opmode_t::kOff) {
json.add(PSTR(D_JSON_IRHVAC_POWER), IRac::boolToString(false));
} else {
json.add(PSTR(D_JSON_IRHVAC_POWER), IRac::boolToString(state.power));
}
json.add(PSTR(D_JSON_IRHVAC_CELSIUS), IRac::boolToString(state.celsius));
addFloatToJson(json, PSTR(D_JSON_IRHVAC_TEMP), state.degrees);
json.add(PSTR(D_JSON_IRHVAC_FANSPEED), IRac::fanspeedToString(state.fanspeed));
json.add(PSTR(D_JSON_IRHVAC_SWINGV), IRac::swingvToString(state.swingv));
json.add(PSTR(D_JSON_IRHVAC_SWINGH), IRac::swinghToString(state.swingh));
json.add(PSTR(D_JSON_IRHVAC_QUIET), IRac::boolToString(state.quiet));
json.add(PSTR(D_JSON_IRHVAC_TURBO), IRac::boolToString(state.turbo));
json.add(PSTR(D_JSON_IRHVAC_ECONO), IRac::boolToString(state.econo));
json.add(PSTR(D_JSON_IRHVAC_LIGHT), IRac::boolToString(state.light));
json.add(PSTR(D_JSON_IRHVAC_FILTER), IRac::boolToString(state.filter));
json.add(PSTR(D_JSON_IRHVAC_CLEAN), IRac::boolToString(state.clean));
json.add(PSTR(D_JSON_IRHVAC_BEEP), IRac::boolToString(state.beep));
json.add(PSTR(D_JSON_IRHVAC_SLEEP), state.sleep);
json.add(PSTR(D_JSON_IRHVAC_FANSPEED), IRac::fanspeedToString(state.fanspeed));
json.add(PSTR(D_JSON_IRHVAC_SWINGV), IRac::swingvToString(state.swingv));
json.add(PSTR(D_JSON_IRHVAC_SWINGH), IRac::swinghToString(state.swingh));
json.add(PSTR(D_JSON_IRHVAC_QUIET), IRac::boolToString(state.quiet));
json.add(PSTR(D_JSON_IRHVAC_TURBO), IRac::boolToString(state.turbo));
json.add(PSTR(D_JSON_IRHVAC_ECONO), IRac::boolToString(state.econo));
json.add(PSTR(D_JSON_IRHVAC_LIGHT), IRac::boolToString(state.light));
json.add(PSTR(D_JSON_IRHVAC_FILTER), IRac::boolToString(state.filter));
json.add(PSTR(D_JSON_IRHVAC_CLEAN), IRac::boolToString(state.clean));
json.add(PSTR(D_JSON_IRHVAC_BEEP), IRac::boolToString(state.beep));
json.add(PSTR(D_JSON_IRHVAC_SLEEP), state.sleep);
if(state.clock != -1) { json.add(PSTR(D_JSON_IRHVAC_CLOCK), state.clock); }
json.add(PSTR(D_JSON_IRHVAC_IFEEL), IRac::boolToString(state.iFeel));
addFloatToJson(json, PSTR(D_JSON_IRHVAC_SENSOR_TEMP), state.sensorTemperature, kNoTempValue);
break;
}
String payload = json.toString(); // copy string before returning, the original is on the stack
return payload;
@ -393,7 +433,7 @@ StateModes strToStateMode(class JsonParserToken token, StateModes def) {
// 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::kMin, stdAc::fanspeed_t::kLow,stdAc::fanspeed_t::kMedium, stdAc::fanspeed_t::kMediumHigh,
stdAc::fanspeed_t::kHigh, stdAc::fanspeed_t::kMax };
uint32_t IrRemoteCmndIrHvacJson(void)
@ -408,6 +448,7 @@ uint32_t IrRemoteCmndIrHvacJson(void)
// 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.command = stdAc::ac_command_t::kControlCommand;
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
@ -424,6 +465,8 @@ uint32_t IrRemoteCmndIrHvacJson(void)
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.
state.iFeel = false;
state.sensorTemperature = kNoTempValue; // Don't set any sensor (ambient) temperature if not provided
JsonParserToken val;
if (val = root[PSTR(D_JSON_IRHVAC_VENDOR)]) { state.protocol = strToDecodeType(val.getStr()); }
@ -431,18 +474,20 @@ uint32_t IrRemoteCmndIrHvacJson(void)
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
if (val = root[PSTR(D_JSON_IRHVAC_MODEL)]) { state.model = IRac::strToModel(val.getStr()); }
if (val = root[PSTR(D_JSON_IRHVAC_COMMAND)]) { state.command = IRac::strToCommandType(val.getStr()); }
// for fan speed, we also support 1-6 values
JsonParserToken tok_fan_speed = root[PSTR(D_JSON_IRHVAC_FANSPEED)];
if (tok_fan_speed) {
uint32_t fan_speed = tok_fan_speed.getUInt();
if ((fan_speed >= 1) && (fan_speed <= 5)) {
if ((fan_speed >= 1) && (fan_speed <= 6)) {
state.fanspeed = (stdAc::fanspeed_t) pgm_read_byte(&IrHvacFanSpeed[fan_speed]);
} else {
state.fanspeed = IRac::strToFanspeed(tok_fan_speed.getStr());
}
}
if (val = root[PSTR(D_JSON_IRHVAC_MODEL)]) { state.model = IRac::strToModel(val.getStr()); }
if (val = root[PSTR(D_JSON_IRHVAC_MODE)]) { state.mode = IRac::strToOpmode(val.getStr()); }
if (val = root[PSTR(D_JSON_IRHVAC_SWINGV)]) { state.swingv = IRac::strToSwingV(val.getStr()); }
if (val = root[PSTR(D_JSON_IRHVAC_SWINGH)]) { state.swingh = IRac::strToSwingH(val.getStr()); }
@ -465,9 +510,18 @@ uint32_t IrRemoteCmndIrHvacJson(void)
state.quiet = strToBool(root[PSTR(D_JSON_IRHVAC_QUIET)], state.quiet);
state.clean = strToBool(root[PSTR(D_JSON_IRHVAC_CLEAN)], state.clean);
// optional timer and clock
state.sleep = root.getInt(PSTR(D_JSON_IRHVAC_SLEEP), state.sleep);
//if (json[D_JSON_IRHVAC_CLOCK]) { state.clock = json[D_JSON_IRHVAC_CLOCK]; } // not sure it's useful to support 'clock'
if (state.command == stdAc::ac_command_t::kConfigCommand) {
// Note: for `kConfigCommand` the semantics of clock/sleep is abused IRremoteESP8266 lib-side for key/value pair
state.clock = root.getInt(PSTR(D_JSON_IRHVAC_CONFIG_KEY), state.clock);
state.sleep = root.getInt(PSTR(D_JSON_IRHVAC_CONFIG_VALUE), state.sleep);
} else {
// optional timer and clock (Note: different json field names w/ time semantics)
state.clock = root.getInt(PSTR(D_JSON_IRHVAC_CLOCK), state.clock);
state.sleep = root.getInt(PSTR(D_JSON_IRHVAC_SLEEP), state.sleep);
}
state.iFeel = strToBool(root[PSTR(D_JSON_IRHVAC_IFEEL)], state.iFeel);
state.sensorTemperature = root.getFloat(PSTR(D_JSON_IRHVAC_SENSOR_TEMP), state.sensorTemperature);
if (!IR_RCV_WHILE_SENDING && (irrecv != nullptr)) { irrecv->pause(); }
if (stateMode == StateModes::SEND_ONLY || stateMode == StateModes::SEND_STORE) {