From 379b1d84dd5983eb7b54858d80d3ef95366ceec3 Mon Sep 17 00:00:00 2001 From: marshn Date: Mon, 1 May 2023 05:12:53 +0100 Subject: [PATCH] RF Codec for Drayton Digistat heating controller (#4494) --- esphome/components/remote_base/__init__.py | 51 +++++ .../remote_base/drayton_protocol.cpp | 213 ++++++++++++++++++ .../components/remote_base/drayton_protocol.h | 44 ++++ 3 files changed, 308 insertions(+) create mode 100644 esphome/components/remote_base/drayton_protocol.cpp create mode 100644 esphome/components/remote_base/drayton_protocol.h diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 4d9196c9c5..2ef33f3711 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -791,6 +791,57 @@ async def raw_action(var, config, args): cg.add(var.set_carrier_frequency(templ)) +# Drayton +( + DraytonData, + DraytonBinarySensor, + DraytonTrigger, + DraytonAction, + DraytonDumper, +) = declare_protocol("Drayton") +DRAYTON_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFF)), + cv.Required(CONF_CHANNEL): cv.All(cv.hex_int, cv.Range(min=0, max=0x1F)), + cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x7F)), + } +) + + +@register_binary_sensor("drayton", DraytonBinarySensor, DRAYTON_SCHEMA) +def drayton_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + DraytonData, + ("address", config[CONF_ADDRESS]), + ("channel", config[CONF_CHANNEL]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("drayton", DraytonTrigger, DraytonData) +def drayton_trigger(var, config): + pass + + +@register_dumper("drayton", DraytonDumper) +def drayton_dumper(var, config): + pass + + +@register_action("drayton", DraytonAction, DRAYTON_SCHEMA) +async def drayton_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) + cg.add(var.set_channel(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + + # RC5 RC5Data, RC5BinarySensor, RC5Trigger, RC5Action, RC5Dumper = declare_protocol("RC5") RC5_SCHEMA = cv.Schema( diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp new file mode 100644 index 0000000000..f5eae49058 --- /dev/null +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -0,0 +1,213 @@ +#include "drayton_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.drayton"; + +static const uint32_t BIT_TIME_US = 500; +static const uint8_t CARRIER_KHZ = 2; +static const uint8_t NBITS_PREAMBLE = 12; +static const uint8_t NBITS_SYNC = 4; +static const uint8_t NBITS_ADDRESS = 16; +static const uint8_t NBITS_CHANNEL = 5; +static const uint8_t NBITS_COMMAND = 7; +static const uint8_t NBITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; + +static const uint8_t CMD_ON = 0x41; +static const uint8_t CMD_OFF = 0x02; + +/* +Drayton Protocol +Using an oscilloscope to capture the data transmitted by the Digistat two +distinct packets for 'On' and 'Off' are transmitted. Each transmitted bit +has a period of 500us, a bit rate of 2000 baud. + +Each packet consists of an initial 1010 pattern to set up the receiver bias. +The number of these bits seen at the receiver varies depending on the state +of the bias when the packet transmission starts. The receiver algoritmn takes +account of this. + +The packet appears to be Manchester encoded, with a '10' tranmitted pair +representing a '1' bit and a '01' pair representing a '0' bit. Each packet is +begun with a '1100' syncronisation symbol which breaks this rule. Following +the sync are 28 '01' or '10' pairs. + +-------------------- + +Boiler On Command as received: +101010101010110001101001010101101001010101010101100101010101101001011001 +ppppppppppppSSSS-0-1-1-0-0-0-0-1-1-0-0-0-0-0-0-0-1-0-0-0-0-0-1-1-0-0-1-0 + +(Where pppp represents the preamble bits and SSSS represents the sync symbol) + +28 bits of data received 01100001100000001000001 10010 (bin) or 6180832 (hex) + +Boiler Off Command as received: +101010101010110001101001010101101001010101010101010101010110011001011001 +ppppppppppppSSSS-0-1-1-0-0-0-0-1-1-0-0-0-0-0-0-0-0-0-0-0-0-1-0-1-0-0-1-0 + +28 bits of data received 0110000110000000000001010010 (bin) or 6180052 (hex) + +-------------------- + +I have used 'RFLink' software (RLink Firmware Version: 1.1 Revision: 48) to +capture and retransmit the Digistat packets. RFLink splits each packet into an +ID, SWITCH, and CMD field. + +0;17;Drayton;ID=c300;SWITCH=12;CMD=ON; +20;18;Drayton;ID=c300;SWITCH=12;CMD=OFF; + +-------------------- + +Spliting my received data into three parts of 16, 7 and 5 bits gives address, +channel and Command values of: + +On 6180832 0110000110000000 1000001 10010 +address: '0x6180' channel: '0x12' command: '0x41' + +Off 6180052 0110000110000000 0000010 10010 +address: '0x6180' channel: '0x12' command: '0x02' + +These values are slightly different to those used by RFLink (the RFLink +ID/Adress value is rotated/manipulated), and I don't know who's interpretation +is correct. A larger data sample would help (I have only found five different +packet captures online) or definitive information from Drayton. + +Splitting each packet in this way works well for me with esphome. Any +corrections or additional data samples would be gratefully received. + +marshn + +*/ + +void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) { + uint16_t khz = CARRIER_KHZ; + dst->set_carrier_frequency(khz * 1000); + + // Preamble = 101010101010 + uint32_t out_data = 0x0AAA; + for (uint32_t mask = 1UL << (NBITS_PREAMBLE - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->mark(BIT_TIME_US); + } else { + dst->space(BIT_TIME_US); + } + } + + // Sync = 1100 + out_data = 0x000C; + for (uint32_t mask = 1UL << (NBITS_SYNC - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->mark(BIT_TIME_US); + } else { + dst->space(BIT_TIME_US); + } + } + + ESP_LOGD(TAG, "Send Drayton: address=%04x channel=%03x cmd=%02x", data.address, data.channel, data.command); + + out_data = data.address; + out_data <<= NBITS_COMMAND; + out_data |= data.command; + out_data <<= NBITS_CHANNEL; + out_data |= data.channel; + + ESP_LOGV(TAG, "Send Drayton: out_data %08x", out_data); + + for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->mark(BIT_TIME_US); + dst->space(BIT_TIME_US); + } else { + dst->space(BIT_TIME_US); + dst->mark(BIT_TIME_US); + } + } +} + +optional DraytonProtocol::decode(RemoteReceiveData src) { + DraytonData out{ + .address = 0, + .channel = 0, + .command = 0, + }; + + if (src.size() < 45) { + return {}; + } + + ESP_LOGVV(TAG, "Decode Drayton: %d, %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d", src.size(), + src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), src.peek(7), + src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), + src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); + + // If first preamble item is a space, skip it + if (src.peek_space_at_least(1)) { + src.advance(1); + } + + // Look for sync pulse, after. If sucessful index points to space of sync symbol + for (uint16_t preamble = 0; preamble <= NBITS_PREAMBLE * 2; preamble += 2) { + ESP_LOGVV(TAG, "Decode Drayton: preamble %d %d %d", preamble, src.peek(preamble), src.peek(preamble + 1)); + if (src.peek_mark(2 * BIT_TIME_US, preamble) && + (src.peek_space(2 * BIT_TIME_US, preamble + 1) || src.peek_space(3 * BIT_TIME_US, preamble + 1))) { + src.advance(preamble + 1); + break; + } + } + + // Read data. Index points to space of sync symbol + // Extract first bit + // Checks next bit to leave index pointing correctly + uint32_t out_data = 0; + uint8_t bit = NBITS_ADDRESS + NBITS_COMMAND + NBITS_CHANNEL - 1; + if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + out_data |= 0 << bit; + } else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; + } else { + ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %d", src.get_index()); + return {}; + } + + // Before/after each bit is read the index points to the transition at the start of the bit period or, + // if there is no transition at the start of the bit period, then the transition in the middle of + // the previous bit period. + while (--bit >= 1) { + ESP_LOGVV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data); + if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + out_data |= 0 << bit; + } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; + } else { + ESP_LOGVV(TAG, "Decode Drayton: Fail 2, %2d %08x", bit, out_data); + return {}; + } + } + if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { + out_data |= 0; + } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 1; + } + ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data); + + out.channel = (uint8_t) (out_data & 0x1F); + out_data >>= NBITS_CHANNEL; + out.command = (uint8_t) (out_data & 0x7F); + out_data >>= NBITS_COMMAND; + out.address = (uint16_t) (out_data & 0xFFFF); + + return out; +} +void DraytonProtocol::dump(const DraytonData &data) { + ESP_LOGD(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address, + ((data.address << 1) & 0xffff), data.channel, data.command); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/drayton_protocol.h b/esphome/components/remote_base/drayton_protocol.h new file mode 100644 index 0000000000..f468e7b57e --- /dev/null +++ b/esphome/components/remote_base/drayton_protocol.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct DraytonData { + uint16_t address; + uint8_t channel; + uint8_t command; + + bool operator==(const DraytonData &rhs) const { + return address == rhs.address && channel == rhs.channel && command == rhs.command; + } +}; + +class DraytonProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const DraytonData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const DraytonData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Drayton) + +template class DraytonAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, address) + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) override { + DraytonData data{}; + data.address = this->address_.value(x...); + data.channel = this->channel_.value(x...); + data.command = this->command_.value(x...); + DraytonProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome