diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 9982447988..72a91a99dd 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -27,6 +27,7 @@ from esphome.const import ( CONF_CARRIER_FREQUENCY, CONF_RC_CODE_1, CONF_RC_CODE_2, + CONF_LEVEL, ) from esphome.core import coroutine from esphome.jsonschema import jschema_extractor @@ -1163,6 +1164,58 @@ async def panasonic_action(var, config, args): cg.add(var.set_command(template_)) +# Nexa +NexaData, NexaBinarySensor, NexaTrigger, NexaAction, NexaDumper = declare_protocol( + "Nexa" +) +NEXA_SCHEMA = cv.Schema( + { + cv.Required(CONF_DEVICE): cv.hex_uint32_t, + cv.Required(CONF_GROUP): cv.hex_uint8_t, + cv.Required(CONF_STATE): cv.hex_uint8_t, + cv.Required(CONF_CHANNEL): cv.hex_uint8_t, + cv.Required(CONF_LEVEL): cv.hex_uint8_t, + } +) + + +@register_binary_sensor("nexa", NexaBinarySensor, NEXA_SCHEMA) +def nexa_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + NexaData, + ("device", config[CONF_DEVICE]), + ("group", config[CONF_GROUP]), + ("state", config[CONF_STATE]), + ("channel", config[CONF_CHANNEL]), + ("level", config[CONF_LEVEL]), + ) + ) + ) + + +@register_trigger("nexa", NexaTrigger, NexaData) +def nexa_trigger(var, config): + pass + + +@register_dumper("nexa", NexaDumper) +def nexa_dumper(var, config): + pass + + +@register_action("nexa", NexaAction, NEXA_SCHEMA) +def nexa_action(var, config, args): + cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint32)))) + cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.uint8)))) + cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, cg.uint8)))) + cg.add( + var.set_channel((yield cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) + ) + cg.add(var.set_level((yield cg.templatable(config[CONF_LEVEL], args, cg.uint8)))) + + # Midea MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol( "Midea" diff --git a/esphome/components/remote_base/nexa_protocol.cpp b/esphome/components/remote_base/nexa_protocol.cpp new file mode 100644 index 0000000000..814b46135a --- /dev/null +++ b/esphome/components/remote_base/nexa_protocol.cpp @@ -0,0 +1,235 @@ +#include "nexa_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.nexa"; + +static const uint8_t NBITS = 32; +static const uint32_t HEADER_HIGH_US = 319; +static const uint32_t HEADER_LOW_US = 2610; +static const uint32_t BIT_HIGH_US = 319; +static const uint32_t BIT_ONE_LOW_US = 1000; +static const uint32_t BIT_ZERO_LOW_US = 140; + +static const uint32_t TX_HEADER_HIGH_US = 250; +static const uint32_t TX_HEADER_LOW_US = TX_HEADER_HIGH_US * 10; +static const uint32_t TX_BIT_HIGH_US = 250; +static const uint32_t TX_BIT_ONE_LOW_US = TX_BIT_HIGH_US * 5; +static const uint32_t TX_BIT_ZERO_LOW_US = TX_BIT_HIGH_US * 1; + +void NexaProtocol::one(RemoteTransmitData *dst) const { + // '1' => '10' + dst->item(TX_BIT_HIGH_US, TX_BIT_ONE_LOW_US); + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); +} + +void NexaProtocol::zero(RemoteTransmitData *dst) const { + // '0' => '01' + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); + dst->item(TX_BIT_HIGH_US, TX_BIT_ONE_LOW_US); +} + +void NexaProtocol::sync(RemoteTransmitData *dst) const { dst->item(TX_HEADER_HIGH_US, TX_HEADER_LOW_US); } + +void NexaProtocol::encode(RemoteTransmitData *dst, const NexaData &data) { + dst->set_carrier_frequency(0); + + // Send SYNC + this->sync(dst); + + // Device (26 bits) + for (int16_t i = 26 - 1; i >= 0; i--) { + if (data.device & (1 << i)) + this->one(dst); + else + this->zero(dst); + } + + // Group (1 bit) + if (data.group != 0) + this->one(dst); + else + this->zero(dst); + + // State (1 bit) + if (data.state == 2) { + // Special case for dimmers...send 00 as state + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); + } else if (data.state == 1) + this->one(dst); + else + this->zero(dst); + + // Channel (4 bits) + for (int16_t i = 4 - 1; i >= 0; i--) { + if (data.channel & (1 << i)) + this->one(dst); + else + this->zero(dst); + } + + // Level (4 bits) + if (data.state == 2) { + for (int16_t i = 4 - 1; i >= 0; i--) { + if (data.level & (1 << i)) + this->one(dst); + else + this->zero(dst); + } + } + + // Send finishing Zero + dst->item(TX_BIT_HIGH_US, TX_BIT_ZERO_LOW_US); +} + +optional NexaProtocol::decode(RemoteReceiveData src) { + NexaData out{ + .device = 0, + .group = 0, + .state = 0, + .channel = 0, + .level = 0, + }; + + // From: http://tech.jolowe.se/home-automation-rf-protocols/ + // New data: http://tech.jolowe.se/old-home-automation-rf-protocols/ + /* + + SHHHH HHHH HHHH HHHH HHHH HHHH HHGO EE BB DDDD 0 P + + S = Sync bit. + H = The first 26 bits are transmitter unique codes, and it is this code that the reciever "learns" to recognize. + G = Group code, set to one for the whole group. + O = On/Off bit. Set to 1 for on, 0 for off. + E = Unit to be turned on or off. The code is inverted, i.e. '11' equals 1, '00' equals 4. + B = Button code. The code is inverted, i.e. '11' equals 1, '00' equals 4. + D = Dim level bits. + 0 = packet always ends with a zero. + P = Pause, a 10 ms pause in between re-send. + + Update: First of all the '1' and '0' bit seems to be reversed (and be the same as Jula I protocol below), i.e. + + */ + + // Require a SYNC pulse + long gap + if (!src.expect_pulse_with_gap(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + // Device + for (uint8_t i = 0; i < 26; i++) { + out.device <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.device |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.device |= 0x00; + } else { + // This should not happen...failed command + return {}; + } + } + + // GROUP + for (uint8_t i = 0; i < 1; i++) { + out.group <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.group |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.group |= 0x00; + } else { + // This should not happen...failed command + return {}; + } + } + + // STATE + for (uint8_t i = 0; i < 1; i++) { + out.state <<= 1UL; + + // Special treatment as we should handle 01, 10 and 00 + // We need to care for the advance made in the expect functions + // hence take them one at a time so that we do not get out of sync + // in decoding + + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US)) { + // Starts with '1' + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + // '10' => 1 + out.state |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US)) { + // '11' => NOT OK + // This case is here to make sure we advance through the correct index + // This should not happen...failed command + return {}; + } + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + // Starts with '0' + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US)) { + // '01' => 0 + out.state |= 0x00; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + // '00' => Special case for dimmer! => 2 + out.state |= 0x02; + } + } + } + + // CHANNEL (EE and BB bits) + for (uint8_t i = 0; i < 4; i++) { + out.channel <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.channel |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.channel |= 0x00; + } else { + // This should not happen...failed command + return {}; + } + } + + // Optional to transmit LEVEL data (8 bits more) + if (int32_t(src.get_index() + 8) >= src.size()) { + return out; + } + + // LEVEL + for (uint8_t i = 0; i < 4; i++) { + out.level <<= 1UL; + if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US))) { + // '1' => '10' + out.level |= 0x01; + } else if (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ZERO_LOW_US) && + (src.expect_pulse_with_gap(BIT_HIGH_US, BIT_ONE_LOW_US))) { + // '0' => '01' + out.level |= 0x00; + } else { + // This should not happen...failed command + break; + } + } + + return out; +} + +void NexaProtocol::dump(const NexaData &data) { + ESP_LOGD(TAG, "Received NEXA: device=0x%04X group=%d state=%d channel=%d level=%d", data.device, data.group, + data.state, data.channel, data.level); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/nexa_protocol.h b/esphome/components/remote_base/nexa_protocol.h new file mode 100644 index 0000000000..f1ce380780 --- /dev/null +++ b/esphome/components/remote_base/nexa_protocol.h @@ -0,0 +1,52 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct NexaData { + uint32_t device; + uint8_t group; + uint8_t state; + uint8_t channel; + uint8_t level; + bool operator==(const NexaData &rhs) const { + return device == rhs.device && group == rhs.group && state == rhs.state && channel == rhs.channel && + level == rhs.level; + } +}; + +class NexaProtocol : public RemoteProtocol { + public: + void one(RemoteTransmitData *dst) const; + void zero(RemoteTransmitData *dst) const; + void sync(RemoteTransmitData *dst) const; + + void encode(RemoteTransmitData *dst, const NexaData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const NexaData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Nexa) + +template class NexaAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, device) + TEMPLATABLE_VALUE(uint8_t, group) + TEMPLATABLE_VALUE(uint8_t, state) + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint8_t, level) + void encode(RemoteTransmitData *dst, Ts... x) override { + NexaData data{}; + data.device = this->device_.value(x...); + data.group = this->group_.value(x...); + data.state = this->state_.value(x...); + data.channel = this->channel_.value(x...); + data.level = this->level_.value(x...); + NexaProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index dd6f7c3482..e1af41274e 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -116,6 +116,16 @@ class RemoteReceiveData { return false; } + bool expect_pulse_with_gap(uint32_t mark, uint32_t space) { + if (this->peek_mark(mark, 0) && this->peek_space_at_least(space, 1)) { + this->advance(2); + return true; + } + return false; + } + + uint32_t get_index() { return index_; } + void reset() { this->index_ = 0; } int32_t pos(uint32_t index) const { return (*this->data_)[index]; }