From f137cc10f4124f0ab891e606fbbd096d2573c38e Mon Sep 17 00:00:00 2001 From: ImSorryButWho <91785526+ImSorryButWho@users.noreply.github.com> Date: Fri, 18 Feb 2022 16:22:41 -0500 Subject: [PATCH] Remote magiquest protocol (#2963) Co-authored-by: Aaron Hertz Co-authored-by: Otto Winter --- esphome/components/remote_base/__init__.py | 50 +++++++++++ .../remote_base/magiquest_protocol.cpp | 83 +++++++++++++++++++ .../remote_base/magiquest_protocol.h | 50 +++++++++++ esphome/const.py | 2 + tests/test1.yaml | 5 ++ 5 files changed, 190 insertions(+) create mode 100644 esphome/components/remote_base/magiquest_protocol.cpp create mode 100644 esphome/components/remote_base/magiquest_protocol.h diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 531761ee95..f1b3e32c18 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -27,6 +27,8 @@ from esphome.const import ( CONF_CARRIER_FREQUENCY, CONF_RC_CODE_1, CONF_RC_CODE_2, + CONF_MAGNITUDE, + CONF_WAND_ID, CONF_LEVEL, ) from esphome.core import coroutine @@ -391,6 +393,54 @@ async def lg_action(var, config, args): cg.add(var.set_nbits(template_)) +# MagiQuest +( + MagiQuestData, + MagiQuestBinarySensor, + MagiQuestTrigger, + MagiQuestAction, + MagiQuestDumper, +) = declare_protocol("MagiQuest") + +MAGIQUEST_SCHEMA = cv.Schema( + { + cv.Required(CONF_WAND_ID): cv.hex_uint32_t, + cv.Optional(CONF_MAGNITUDE, default=0xFFFF): cv.hex_uint16_t, + } +) + + +@register_binary_sensor("magiquest", MagiQuestBinarySensor, MAGIQUEST_SCHEMA) +def magiquest_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + MagiQuestData, + ("magnitude", config[CONF_MAGNITUDE]), + ("wand_id", config[CONF_WAND_ID]), + ) + ) + ) + + +@register_trigger("magiquest", MagiQuestTrigger, MagiQuestData) +def magiquest_trigger(var, config): + pass + + +@register_dumper("magiquest", MagiQuestDumper) +def magiquest_dumper(var, config): + pass + + +@register_action("magiquest", MagiQuestAction, MAGIQUEST_SCHEMA) +async def magiquest_action(var, config, args): + template_ = await cg.templatable(config[CONF_WAND_ID], args, cg.uint32) + cg.add(var.set_wand_id(template_)) + template_ = await cg.templatable(config[CONF_MAGNITUDE], args, cg.uint16) + cg.add(var.set_magnitude(template_)) + + # NEC NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol("NEC") NEC_SCHEMA = cv.Schema( diff --git a/esphome/components/remote_base/magiquest_protocol.cpp b/esphome/components/remote_base/magiquest_protocol.cpp new file mode 100644 index 0000000000..20b40ef201 --- /dev/null +++ b/esphome/components/remote_base/magiquest_protocol.cpp @@ -0,0 +1,83 @@ +#include "magiquest_protocol.h" +#include "esphome/core/log.h" + +/* Based on protocol analysis from + * https://arduino-irremote.github.io/Arduino-IRremote/ir__MagiQuest_8cpp_source.html + */ + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.magiquest"; + +static const uint32_t MAGIQUEST_UNIT = 288; // us +static const uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; +static const uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; +static const uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; +static const uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; + +void MagiQuestProtocol::encode(RemoteTransmitData *dst, const MagiQuestData &data) { + dst->reserve(101); // 2 start bits, 48 data bits, 1 stop bit + dst->set_carrier_frequency(38000); + + // 2 start bits + dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE); + dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE); + for (uint32_t mask = 1 << 31; mask; mask >>= 1) { + if (data.wand_id & mask) { + dst->item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE); + } else { + dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE); + } + } + + for (uint16_t mask = 1 << 15; mask; mask >>= 1) { + if (data.magnitude & mask) { + dst->item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE); + } else { + dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE); + } + } + + dst->mark(MAGIQUEST_UNIT); +} +optional MagiQuestProtocol::decode(RemoteReceiveData src) { + MagiQuestData data{ + .magnitude = 0, + .wand_id = 0, + }; + // Two start bits + if (!src.expect_item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE) || + !src.expect_item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE)) { + return {}; + } + + for (uint32_t mask = 1 << 31; mask; mask >>= 1) { + if (src.expect_item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE)) { + data.wand_id |= mask; + } else if (src.expect_item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE)) { + data.wand_id &= ~mask; + } else { + return {}; + } + } + + for (uint16_t mask = 1 << 15; mask; mask >>= 1) { + if (src.expect_item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE)) { + data.magnitude |= mask; + } else if (src.expect_item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE)) { + data.magnitude &= ~mask; + } else { + return {}; + } + } + + src.expect_mark(MAGIQUEST_UNIT); + return data; +} +void MagiQuestProtocol::dump(const MagiQuestData &data) { + ESP_LOGD(TAG, "Received MagiQuest: wand_id=0x%08X, magnitude=0x%04X", data.wand_id, data.magnitude); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/magiquest_protocol.h b/esphome/components/remote_base/magiquest_protocol.h new file mode 100644 index 0000000000..909be346d0 --- /dev/null +++ b/esphome/components/remote_base/magiquest_protocol.h @@ -0,0 +1,50 @@ +#pragma once + +#include "remote_base.h" + +/* Based on protocol analysis from + * https://arduino-irremote.github.io/Arduino-IRremote/ir__MagiQuest_8cpp_source.html + */ + +namespace esphome { +namespace remote_base { + +struct MagiQuestData { + uint16_t magnitude; + uint32_t wand_id; + + bool operator==(const MagiQuestData &rhs) const { + // Treat 0xffff as a special, wildcard magnitude + // In testing, the wand never produces this value, and this allows us to match + // on just the wand_id if wanted. + if (rhs.wand_id != this->wand_id) { + return false; + } + return (this->wand_id == 0xffff || rhs.wand_id == 0xffff || this->wand_id == rhs.wand_id); + } +}; + +class MagiQuestProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const MagiQuestData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const MagiQuestData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(MagiQuest) + +template class MagiQuestAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, magnitude) + TEMPLATABLE_VALUE(uint32_t, wand_id) + + void encode(RemoteTransmitData *dst, Ts... x) override { + MagiQuestData data{}; + data.magnitude = this->magnitude_.value(x...); + data.wand_id = this->wand_id_.value(x...); + MagiQuestProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 1494920385..721f78f26a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -344,6 +344,7 @@ CONF_LOOP_TIME = "loop_time" CONF_LOW = "low" CONF_LOW_VOLTAGE_REFERENCE = "low_voltage_reference" CONF_MAC_ADDRESS = "mac_address" +CONF_MAGNITUDE = "magnitude" CONF_MAINS_FILTER = "mains_filter" CONF_MAKE_ID = "make_id" CONF_MANUAL_IP = "manual_ip" @@ -735,6 +736,7 @@ CONF_VOLTAGE_DIVIDER = "voltage_divider" CONF_WAIT_TIME = "wait_time" CONF_WAIT_UNTIL = "wait_until" CONF_WAKEUP_PIN = "wakeup_pin" +CONF_WAND_ID = "wand_id" CONF_WARM_WHITE = "warm_white" CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature" CONF_WATCHDOG_THRESHOLD = "watchdog_threshold" diff --git a/tests/test1.yaml b/tests/test1.yaml index e2d5fdc0c5..9b38d549b3 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1861,6 +1861,11 @@ switch: turn_on_action: remote_transmitter.transmit_jvc: data: 0x10EF + - platform: template + name: MagiQuest + turn_on_action: + remote_transmitter.transmit_magiquest: + wand_id: 0x01234567 - platform: template name: NEC id: living_room_lights_off