diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index a664a6bdaf..b0dc93e3b7 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1501,22 +1501,34 @@ async def raw_action(var, config, args): ) = 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)), + cv.Required(CONF_ADDRESS): cv.hex_uint16_t, + cv.Optional(CONF_DATA): cv.hex_int_range(min=0, max=0xF), + cv.Optional(CONF_CHANNEL): cv.hex_int_range(min=0, max=0x1F), + cv.Optional(CONF_COMMAND): cv.hex_int_range(min=0, max=0x7F), } ) +# The legacy command is the 4 data bits followed by this many checksum bits +DRAYTON_NBITS_CHECKSUM_IN_COMMAND = 3 +# Either `data`, or the legacy `channel` + `command` pair (raw packet bits) +_validate_drayton_form = cv.All( + cv.has_none_or_all_keys(CONF_CHANNEL, CONF_COMMAND), + cv.has_exactly_one_key(CONF_DATA, CONF_COMMAND), +) -@register_binary_sensor("drayton", DraytonBinarySensor, DRAYTON_SCHEMA) +@register_binary_sensor( + "drayton", DraytonBinarySensor, cv.All(DRAYTON_SCHEMA, _validate_drayton_form) +) def drayton_binary_sensor(var, config): + # Matching compares address and data only + if (command := config.get(CONF_COMMAND)) is not None: + data = command >> DRAYTON_NBITS_CHECKSUM_IN_COMMAND + else: + data = config[CONF_DATA] cg.add( var.set_data( cg.StructInitializer( - DraytonData, - ("address", config[CONF_ADDRESS]), - ("channel", config[CONF_CHANNEL]), - ("command", config[CONF_COMMAND]), + DraytonData, ("address", config[CONF_ADDRESS]), ("data", data) ) ) ) @@ -1537,8 +1549,10 @@ register_protocol_action( "Drayton", DRAYTON_SCHEMA, (CONF_ADDRESS, "address", cg.uint16), + (CONF_DATA, "data", cg.uint8), (CONF_CHANNEL, "channel", cg.uint8), (CONF_COMMAND, "command", cg.uint8), + validate=_validate_drayton_form, ) diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp index 598f1a476e..1bb2a68417 100644 --- a/esphome/components/remote_base/drayton_protocol.cpp +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -12,13 +12,12 @@ static constexpr uint8_t CARRIER_KHZ = 2; static constexpr uint8_t NBITS_PREAMBLE = 12; static constexpr uint8_t NBITS_SYNC = 4; static constexpr uint8_t NBITS_ADDRESS = 16; -static constexpr uint8_t NBITS_CHANNEL = 5; +static constexpr uint8_t NBITS_DATA = 4; +static constexpr uint8_t NBITS_CHECKSUM = 8; static constexpr uint8_t NBITS_COMMAND = 7; -static constexpr uint8_t NDATABITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; -static constexpr uint8_t MIN_RX_SRC = (NDATABITS + NBITS_SYNC / 2); - -static constexpr uint8_t CMD_ON = 0x41; -static constexpr uint8_t CMD_OFF = 0x02; +static constexpr uint8_t NBITS_CHANNEL = 5; +static constexpr uint8_t NBITS_PKT = NBITS_ADDRESS + NBITS_DATA + NBITS_CHECKSUM; +static constexpr uint8_t MIN_RX_SRC = (NBITS_PKT + NBITS_SYNC / 2); /* Drayton Protocol @@ -31,59 +30,72 @@ 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 packet is Manchester coded, 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: +Sample 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) +28 bits received 01100001100000001000001 10010 (bin) or 6180832 (hex) -Boiler Off Command as received: +Sample 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) +28 bits 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. +The 28 bits consist of 20 bits of payload and 8 bits of checksum. -0;17;Drayton;ID=c300;SWITCH=12;CMD=ON; -20;18;Drayton;ID=c300;SWITCH=12;CMD=OFF; +In order of transmission the payload consists of the following fields: --------------------- +Bits Description +12 ID of the thermostat. A Digistat receiver will only respond to a packet if its ID + matches the ID that the receiver has previously learnt. +4 Pairing field, normally 0000. The first packets sent by the thermostat after its + batteries are inserted have this field set to 0001, which lets a Digistat receiver + in pairing mode learn the ID of the thermostat. +1 Thermostat on (1) or off (0). +1 Always 0. +1 Low battery (1) or battery OK (0). +1 Always 0. -Spliting my received data into three parts of 16, 7 and 5 bits gives address, -channel and Command values of: +The YAML/lambda 'address' is the first 16 bits as sent (the 12 bit ID followed by the +pairing field, so the pairing flag is the address LSB), and 'data' is the next 4 bits +(0x8 = on, 0x2 = low battery). -On 6180832 0110000110000000 1000001 10010 -address: '0x6180' channel: '0x12' command: '0x41' +Checksum: see calc_cs(); the +1 when the sum is above 0x7F is empirical. The captured +packets above give 0x32 (On) and 0x52 (Off). Packets that fail it are logged at verbose level. -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 +Legacy split (from RFLink): address (16), command (7: data + top 3 checksum bits), channel (5: low checksum bits). +When channel and command are set they are sent as is, with no checksum calculated. */ +static uint8_t calc_cs(uint32_t out_data) { + uint16_t cs = 0; + uint32_t rev = reverse_bits(out_data); + + for (uint8_t i = 4; i <= 20; i += 4) { + cs <<= 1; + cs += (rev >> i) & 0x0F; + } + + if (cs > 0x7F) { + cs += 1; + } + // Sent LSB first + return reverse_bits(uint8_t(cs << 1)); +} + void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) { uint16_t khz = CARRIER_KHZ; dst->set_carrier_frequency(khz * 1000); @@ -108,17 +120,21 @@ void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) { } } - ESP_LOGD(TAG, "Send Drayton: address=%04x channel=%03x cmd=%02x", data.address, data.channel, data.command); + if (data.channel || data.command) { + out_data = data.address; + out_data <<= NBITS_COMMAND; + out_data |= data.command; + out_data <<= NBITS_CHANNEL; + out_data |= data.channel; + } else { + out_data = data.address << (NBITS_DATA + NBITS_CHECKSUM); + out_data |= (data.data & ((1u << NBITS_DATA) - 1)) << NBITS_CHECKSUM; // a lambda may exceed 4 bits + out_data |= calc_cs(out_data); + } + ESP_LOGD(TAG, "addr=%04x data=%x channel=%02x command=%02x", data.address, data.data, data.channel, data.command); + ESP_LOGV(TAG, "out_data %07" PRIx32, out_data); - 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 %08" PRIx32, out_data); - - for (uint32_t mask = 1UL << (NDATABITS - 1); mask != 0; mask >>= 1) { + for (uint32_t mask = 1UL << (NBITS_PKT - 1); mask != 0; mask >>= 1) { if (out_data & mask) { dst->mark(BIT_TIME_US); dst->space(BIT_TIME_US); @@ -130,21 +146,9 @@ void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) { } optional DraytonProtocol::decode(RemoteReceiveData src) { - DraytonData out{ - .address = 0, - .channel = 0, - .command = 0, - }; + DraytonData out{}; while (src.size() - src.get_index() >= MIN_RX_SRC) { - ESP_LOGVV(TAG, - "Decode Drayton: %" PRId32 ", %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 - " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 - " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 "", - src.size() - src.get_index(), 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); @@ -152,12 +156,12 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { // Look for sync pulse, after. If sucessful index points to space of sync symbol while (src.size() - src.get_index() >= MIN_RX_SRC) { - ESP_LOGVV(TAG, "Decode Drayton: sync search %" PRIu32 ", %" PRId32 " %" PRId32, src.size() - src.get_index(), - src.peek(), src.peek(1)); + ESP_LOGVV(TAG, "sync search %" PRIu32 ", %" PRId32 " %" PRId32, src.size() - src.get_index(), src.peek(), + src.peek(1)); if (src.peek_mark(2 * BIT_TIME_US) && (src.peek_space(2 * BIT_TIME_US, 1) || src.peek_space(3 * BIT_TIME_US, 1))) { src.advance(1); - ESP_LOGVV(TAG, "Decode Drayton: Found SYNC, - %" PRIu32, src.get_index()); + ESP_LOGVV(TAG, "Found SYNC %" PRIu32, src.get_index()); break; } else { src.advance(2); @@ -165,8 +169,8 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { } // No point continuing if not enough samples remaining to complete a packet - if (src.size() - src.get_index() < NDATABITS) { - ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %" PRIu32, src.get_index()); + if (src.size() - src.get_index() < NBITS_PKT) { + ESP_LOGV(TAG, "Fail1 %" PRIu32, src.get_index()); break; } @@ -174,17 +178,14 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { // Extract first bit // Checks next bit to leave index pointing correctly uint32_t out_data = 0; - uint8_t bit = NDATABITS - 1; - ESP_LOGVV(TAG, "Decode Drayton: first bit %" PRId32 " %" PRId32 ", %" PRId32, src.peek(0), src.peek(1), - src.peek(2)); + uint8_t bit = NBITS_PKT - 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 2, - %" PRId32 " %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), - src.peek(1)); + ESP_LOGVV(TAG, "Fail2 %" PRId32 " %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), src.peek(1)); continue; } @@ -192,7 +193,7 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { // 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 %08" PRIx32, bit, out_data); + ESP_LOGVV(TAG, "Data %2d %08" PRIx32, 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; @@ -205,8 +206,7 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { } if (bit > 0) { - ESP_LOGVV(TAG, "Decode Drayton: Fail 3, %" PRId32 " %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), - src.peek(1)); + ESP_LOGVV(TAG, "Fail3 %" PRId32 " %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), src.peek(1)); continue; } @@ -218,21 +218,28 @@ optional DraytonProtocol::decode(RemoteReceiveData src) { continue; } - ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08" PRIx32, bit, out_data); + uint8_t cs = out_data & 0xFF; + uint8_t expected_cs = calc_cs(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); + if (cs != expected_cs) { + ESP_LOGV(TAG, "Dropped %07" PRIx32 ": checksum 0x%02x, expected 0x%02x", out_data, cs, expected_cs); + continue; + } + + ESP_LOGV(TAG, "Data %07" PRIx32, out_data); + + out.data = (out_data >> NBITS_CHECKSUM) & ((1u << NBITS_DATA) - 1); + out.address = out_data >> (NBITS_CHECKSUM + NBITS_DATA); + out.channel = out_data & ((1u << NBITS_CHANNEL) - 1); + out.command = (out_data >> NBITS_CHANNEL) & ((1u << NBITS_COMMAND) - 1); return out; } return {}; } + void DraytonProtocol::dump(const DraytonData &data) { - ESP_LOGI(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address, - ((data.address << 1) & 0xffff), data.channel, data.command); + ESP_LOGI(TAG, "Received Drayton: address=0x%04X, data=0x%X", data.address, data.data); } } // namespace esphome::remote_base diff --git a/esphome/components/remote_base/drayton_protocol.h b/esphome/components/remote_base/drayton_protocol.h index ef9dc39fb3..83988fbaa6 100644 --- a/esphome/components/remote_base/drayton_protocol.h +++ b/esphome/components/remote_base/drayton_protocol.h @@ -11,10 +11,10 @@ struct DraytonData { uint16_t address; uint8_t channel; uint8_t command; + uint8_t data; // last, so positional initializers written for the legacy layout keep their meaning - bool operator==(const DraytonData &rhs) const { - return address == rhs.address && channel == rhs.channel && command == rhs.command; - } + // channel/command: legacy split of the same bits, filled on decode, ignored by operator== + bool operator==(const DraytonData &rhs) const { return address == rhs.address && data == rhs.data; } }; class DraytonProtocol : public RemoteProtocol { diff --git a/tests/components/remote_base/__init__.py b/tests/components/remote_base/__init__.py index cddee51195..36060d079c 100644 --- a/tests/components/remote_base/__init__.py +++ b/tests/components/remote_base/__init__.py @@ -17,6 +17,7 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: "nec", "magiquest", "rc5x", + "drayton", "panasonic", ): request_protocol(protocol) diff --git a/tests/components/remote_base/drayton_test.cpp b/tests/components/remote_base/drayton_test.cpp new file mode 100644 index 0000000000..455235cd99 --- /dev/null +++ b/tests/components/remote_base/drayton_test.cpp @@ -0,0 +1,141 @@ +#include +#include +#include "esphome/components/remote_base/drayton_protocol.h" + +namespace esphome::remote_base::testing { + +namespace { + +constexpr int32_t BIT_US = 500; +constexpr size_t PACKET_BITS = 28; + +// Captured packets from the header comment of drayton_protocol.cpp +constexpr uint32_t ON_PACKET = 0x6180832; +constexpr uint32_t OFF_PACKET = 0x6180052; +const char *const ON_CAPTURE = "101010101010110001101001010101101001010101010101100101010101101001011001"; +const char *const OFF_CAPTURE = "101010101010110001101001010101101001010101010101010101010110011001011001"; + +DraytonData with_data(uint16_t address, uint8_t data) { + DraytonData out{}; + out.address = address; + out.data = data; + return out; +} + +DraytonData with_legacy(uint16_t address, uint8_t channel, uint8_t command) { + DraytonData out{}; + out.address = address; + out.channel = channel; + out.command = command; + return out; +} + +RawTimings encode(const DraytonData &data) { + RemoteTransmitData tx; + DraytonProtocol().encode(&tx, data); + return tx.get_data(); +} + +// The packet bits sent after the preamble and sync, one Manchester pair per bit ('10' = 1) +uint32_t sent_packet(const RawTimings &timings) { + uint32_t word = 0; + for (size_t i = timings.size() - 2 * PACKET_BITS; i < timings.size(); i += 2) { + word = (word << 1) | (timings[i] > 0 ? 1 : 0); + } + return word; +} + +// A receiver reports runs of the same level as one item. +RawTimings as_received(const RawTimings &timings) { + RawTimings merged; + for (int32_t item : timings) { + if (!merged.empty() && (merged.back() < 0) == (item < 0)) { + merged.back() += item; + } else { + merged.push_back(item); + } + } + return merged; +} + +RawTimings from_capture(const std::string &bits) { + RawTimings timings; + for (char bit : bits) { + timings.push_back(bit == '1' ? BIT_US : -BIT_US); + } + return as_received(timings); +} + +optional decode(const RawTimings &timings) { + return DraytonProtocol().decode(RemoteReceiveData(timings, 25, TOLERANCE_MODE_PERCENTAGE)); +} + +} // namespace + +TEST(DraytonProtocolTest, EncodeAppendsChecksum) { + EXPECT_EQ(sent_packet(encode(with_data(0x6180, 0x8))), ON_PACKET); + EXPECT_EQ(sent_packet(encode(with_data(0x6180, 0x0))), OFF_PACKET); + // A lambda value above 4 bits is masked instead of spilling into the address + EXPECT_EQ(sent_packet(encode(with_data(0x6180, 0x18))), ON_PACKET); +} + +TEST(DraytonProtocolTest, LegacyEncodeSendsChannelAndCommandAsIs) { + EXPECT_EQ(sent_packet(encode(with_legacy(0x6180, 0x12, 0x41))), ON_PACKET); + EXPECT_EQ(sent_packet(encode(with_legacy(0x1234, 0x01, 0x05))), 0x12340A1u); +} + +TEST(DraytonProtocolTest, DecodesCapturedPackets) { + auto on = decode(from_capture(ON_CAPTURE)); + ASSERT_TRUE(on.has_value()); + if (!on.has_value()) { + return; + } + EXPECT_EQ(on->address, 0x6180); + EXPECT_EQ(on->data, 0x8); + EXPECT_EQ(on->channel, 0x12); + EXPECT_EQ(on->command, 0x41); + + auto off = decode(from_capture(OFF_CAPTURE)); + ASSERT_TRUE(off.has_value()); + if (!off.has_value()) { + return; + } + EXPECT_EQ(off->address, 0x6180); + EXPECT_EQ(off->data, 0x0); + EXPECT_EQ(off->channel, 0x12); + EXPECT_EQ(off->command, 0x02); +} + +TEST(DraytonProtocolTest, RoundTrips) { + for (uint16_t address : {0x0000, 0x6180, 0x1231, 0xFFFF}) { + for (uint8_t data : {0x0, 0x2, 0x8, 0xA, 0xF}) { + auto decoded = decode(as_received(encode(with_data(address, data)))); + ASSERT_TRUE(decoded.has_value()) << std::hex << address << " " << int(data); + if (!decoded.has_value()) { + return; + } + EXPECT_EQ(decoded->address, address); + EXPECT_EQ(decoded->data, data); + } + } +} + +TEST(DraytonProtocolTest, RejectsBadChecksum) { + // On packet with the low checksum bits changed from 0x12 to 0x13 + EXPECT_FALSE(decode(as_received(encode(with_legacy(0x6180, 0x13, 0x41)))).has_value()); + // On packet with the data bits of the Off packet + EXPECT_FALSE(decode(as_received(encode(with_legacy(0x6180, 0x12, 0x01)))).has_value()); +} + +TEST(DraytonProtocolTest, BinarySensorFormsMatchDecodedPacket) { + auto on = decode(from_capture(ON_CAPTURE)); + ASSERT_TRUE(on.has_value()); + if (!on.has_value()) { + return; + } + // The decoded channel and command are ignored + EXPECT_TRUE(*on == with_data(0x6180, 0x8)); + EXPECT_FALSE(*on == with_data(0x6181, 0x8)); +} + +} // namespace esphome::remote_base::testing diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index fca5b0c374..cd01690344 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -51,8 +51,8 @@ on_dooya: on_drayton: then: - logger.log: - format: "on_drayton: %u %u %u" - args: ["x.address", "x.channel", "x.command"] + format: "on_drayton: %u %u %u %u" + args: ["x.address", "x.channel", "x.command", "x.data"] on_dyson: then: - logger.log: diff --git a/tests/components/remote_receiver/esp32-common.yaml b/tests/components/remote_receiver/esp32-common.yaml index f66c2ed3b0..d2b135f034 100644 --- a/tests/components/remote_receiver/esp32-common.yaml +++ b/tests/components/remote_receiver/esp32-common.yaml @@ -21,6 +21,17 @@ binary_sensor: address: 0x00 command: 0x0D extension: 0x03 + - platform: remote_receiver + name: Drayton Thermostat On + drayton: + address: 0x6180 + data: 0x8 + - platform: remote_receiver + name: Drayton Thermostat On Legacy + drayton: + address: 0x6180 + channel: 0x12 + command: 0x41 - platform: remote_receiver name: Panasonic 56 bit Remote Input panasonic: diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index 2ef158456e..52a43f41b9 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -27,6 +27,13 @@ button: on_press: remote_transmitter.transmit_brennenstuhl: code: 0xBD2E2C + - platform: template + name: Drayton Thermostat On + id: drayton_thermostat_on + on_press: + remote_transmitter.transmit_drayton: + address: 0x1230 + data: 0x8 - platform: template name: Dyson fan up id: dyson_fan_up