mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 04:02:54 +00:00
[remote_base] Send the 72-bit Jarolift TDEF group frame with transmit_keeloq (#20927)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
J. Nick Koston
parent
792d650fb2
commit
a576092f25
@@ -1044,6 +1044,21 @@ KEELOQ_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _keeloq_group_alone(config: ConfigType) -> ConfigType:
|
||||
# The group bits take the places of level and the suffix.
|
||||
if CONF_GROUP not in config:
|
||||
return config
|
||||
if CONF_SUFFIX in config or CONF_SUFFIX_BITS in config:
|
||||
raise cv.Invalid(
|
||||
"group cannot be combined with suffix or suffix_bits", [CONF_GROUP]
|
||||
)
|
||||
if config[CONF_LEVEL] is not False:
|
||||
raise cv.Invalid(
|
||||
"group sets the bit that level sends; remove level", [CONF_LEVEL]
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _keeloq_suffix_fits(config: ConfigType) -> ConfigType:
|
||||
# A lambda is only known on the device. A plain value has to fit the width.
|
||||
suffix = config.get(CONF_SUFFIX, 0)
|
||||
@@ -1060,6 +1075,7 @@ KEELOQ_ACTION_SCHEMA = KEELOQ_SCHEMA.extend(
|
||||
# No defaults: an absent key leaves the data at 0 and emits nothing.
|
||||
cv.Optional(CONF_SUFFIX): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFF)),
|
||||
cv.Optional(CONF_SUFFIX_BITS): cv.int_range(min=0, max=16),
|
||||
cv.Optional(CONF_GROUP): cv.hex_uint8_t,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1097,7 +1113,8 @@ register_protocol_action(
|
||||
(CONF_LEVEL, "vlow", cg.bool_),
|
||||
(CONF_SUFFIX, "suffix", cg.uint16),
|
||||
(CONF_SUFFIX_BITS, "suffix_bits", cg.uint8),
|
||||
validate=_keeloq_suffix_fits,
|
||||
automation.ApplyField(CONF_GROUP, "data.set_group", cg.uint8),
|
||||
validate=cv.All(_keeloq_group_alone, _keeloq_suffix_fits),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -93,9 +93,14 @@ void KeeloqProtocol::encode(RemoteTransmitData *dst, const KeeloqData &data) {
|
||||
dst->space(1 * BIT_TIME_US);
|
||||
}
|
||||
|
||||
// repeat flag - always sent as a '1'
|
||||
dst->mark(1 * BIT_TIME_US);
|
||||
dst->space(2 * BIT_TIME_US);
|
||||
// repeat flag
|
||||
if (data.repeat) {
|
||||
dst->mark(1 * BIT_TIME_US);
|
||||
dst->space(2 * BIT_TIME_US);
|
||||
} else {
|
||||
dst->mark(2 * BIT_TIME_US);
|
||||
dst->space(1 * BIT_TIME_US);
|
||||
}
|
||||
|
||||
const uint8_t extra = data.suffix_bits > NBITS_SUFFIX_MAX ? NBITS_SUFFIX_MAX : data.suffix_bits;
|
||||
for (uint8_t i = 0; i < extra; i++) {
|
||||
@@ -234,6 +239,12 @@ optional<KeeloqData> KeeloqProtocol::decode(RemoteReceiveData src) {
|
||||
}
|
||||
|
||||
void KeeloqProtocol::dump(const KeeloqData &data) {
|
||||
if (data.suffix_bits == 6) {
|
||||
// Jarolift TDEF frame: show the group as transmit_keeloq takes it.
|
||||
ESP_LOGD(TAG, "Received Keeloq: address=0x%08" PRIx32 ", command=0x%02x, encrypted=0x%08" PRIx32 ", group=0x%02x",
|
||||
data.address, data.command, data.encrypted, data.group());
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Received Keeloq: address=0x%08" PRIx32 ", command=0x%02x, encrypted=0x%08" PRIx32 ", suffix=0x%04x/%u",
|
||||
data.address, data.command, data.encrypted, data.suffix, data.suffix_bits);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,21 @@ struct KeeloqData {
|
||||
uint32_t encrypted; // 32 bit encrypted field
|
||||
uint32_t address; // 28 bit serial number
|
||||
uint8_t command; // Button Status S2-S1-S0-S3
|
||||
bool repeat; // Repeated command bit
|
||||
bool repeat{true}; // Repeated command bit
|
||||
bool vlow; // Battery status bit
|
||||
uint16_t suffix{0}; // Optional bits after the 66-bit HCS301 word
|
||||
uint8_t suffix_bits{0};
|
||||
|
||||
// Jarolift TDEF sends 8 group bits after the 64-bit word where the HCS301 has vlow and repeat.
|
||||
void set_group(uint8_t group) {
|
||||
this->vlow = group & 0x01;
|
||||
this->repeat = group & 0x02;
|
||||
this->suffix = group >> 2;
|
||||
this->suffix_bits = 6;
|
||||
}
|
||||
// Only a Jarolift TDEF frame (suffix_bits == 6) carries a group; check suffix_bits before using it.
|
||||
uint8_t group() const { return static_cast<uint8_t>((this->suffix << 2) | (this->repeat << 1) | this->vlow); }
|
||||
|
||||
bool operator==(const KeeloqData &rhs) const {
|
||||
// 0x10 matches any command. The suffix is not compared: the binary sensor does not take it.
|
||||
if (address != rhs.address) {
|
||||
|
||||
@@ -31,3 +31,11 @@ button:
|
||||
code: 0xd19ef0a9
|
||||
suffix: !lambda return 0x2A;
|
||||
suffix_bits: !lambda return 6;
|
||||
- platform: template
|
||||
name: Group frame
|
||||
on_press:
|
||||
- remote_transmitter.transmit_keeloq:
|
||||
address: 0x1234500
|
||||
command: 0x8
|
||||
code: 0xF94A09F2
|
||||
group: 0x0F
|
||||
|
||||
@@ -12,12 +12,18 @@ def test_keeloq_suffix_is_set_only_when_configured(
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("keeloq_suffix.yaml"))
|
||||
bodies = protocol_call_bodies(main_cpp)
|
||||
assert len(bodies) == 3
|
||||
plain, constant, lambda_ = bodies
|
||||
assert len(bodies) == 4
|
||||
plain, constant, lambda_, group = bodies
|
||||
# The plain word fills only the HCS301 fields.
|
||||
assert "data.suffix" not in plain
|
||||
assert "set_group" not in plain
|
||||
assert "apply_call.data.suffix = 0x2A;" in constant
|
||||
assert "apply_call.data.suffix_bits = 6;" in constant
|
||||
# A lambda is called inline.
|
||||
assert "apply_call.data.suffix = static_cast<uint16_t>(0x2A);" in lambda_
|
||||
assert "apply_call.data.suffix_bits = static_cast<uint8_t>(6);" in lambda_
|
||||
# group fills vlow, repeat and the suffix on the device.
|
||||
assert "apply_call.data.set_group(0x0F);" in group
|
||||
# It comes after the default level, so the group's bit 0 is what is sent.
|
||||
assert group.index("data.vlow") < group.index("set_group")
|
||||
assert "data.suffix" not in group
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <vector>
|
||||
#include "esphome/components/remote_base/keeloq_protocol.h"
|
||||
|
||||
namespace esphome::remote_base::testing {
|
||||
@@ -42,6 +43,40 @@ RawTimings as_received(RawTimings timings, bool drop_tail = false) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Jarolift TDEF group UP for the channel mask 0x0F0F, from a published test vector. The mask's low byte is
|
||||
// inside the hopping code, the high byte is the group sent after the 64-bit word.
|
||||
KeeloqData tdef_frame(uint8_t group) {
|
||||
KeeloqData data{};
|
||||
data.encrypted = 0xF94A09F2;
|
||||
data.address = 0x1234500;
|
||||
data.command = 0x8;
|
||||
data.set_group(group);
|
||||
return data;
|
||||
}
|
||||
|
||||
// The data bits after the 10-bit-time header: a 1-bit-time mark is a 1, a 2-bit-time mark a 0.
|
||||
std::vector<bool> sent_bits(const RawTimings &timings) {
|
||||
std::vector<bool> bits;
|
||||
size_t i = 0;
|
||||
while (i < timings.size() && timings[i] != -static_cast<int32_t>(10 * BIT_US)) {
|
||||
i++;
|
||||
}
|
||||
for (i++; i < timings.size(); i++) {
|
||||
if (timings[i] > 0) {
|
||||
bits.push_back(timings[i] == static_cast<int32_t>(BIT_US));
|
||||
}
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
uint32_t field(const std::vector<bool> &bits, size_t first, size_t count) {
|
||||
uint32_t value = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
value |= static_cast<uint32_t>(bits[first + i]) << i;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
optional<KeeloqData> decode(const RawTimings &timings) {
|
||||
return KeeloqProtocol().decode(RemoteReceiveData(timings, 25, TOLERANCE_MODE_PERCENTAGE));
|
||||
}
|
||||
@@ -146,7 +181,7 @@ TEST(KeeloqProtocolTest, ASpaceThatIsNeitherDataNorAGapDropsTheSuffix) {
|
||||
}
|
||||
|
||||
TEST(KeeloqProtocolTest, ASuffixFollowsARepeatBitOfZero) {
|
||||
// The encoder always sends the repeat bit as 1. Turn it into a 0 bit, with and without a suffix.
|
||||
// frame() sends the repeat bit as 1. Turn it into a 0 bit, with and without a suffix.
|
||||
for (uint8_t suffix_bits : {0, 8}) {
|
||||
RawTimings timings = as_received(encode(frame(suffix_bits > 0 ? 0xA5 : 0, suffix_bits)));
|
||||
const size_t repeat = timings.size() - 2 * suffix_bits - 2;
|
||||
@@ -174,4 +209,58 @@ TEST(KeeloqProtocolTest, ASuffixEndsAtAGap) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(KeeloqProtocolTest, HcsWordStillEndsWithRepeatOne) {
|
||||
// 32 code bits, 28 address bits, 4 command bits, vlow, repeat.
|
||||
std::vector<bool> bits = sent_bits(encode(frame(0, 0)));
|
||||
ASSERT_EQ(bits.size(), 66u);
|
||||
EXPECT_FALSE(bits[64]);
|
||||
EXPECT_TRUE(bits[65]);
|
||||
}
|
||||
|
||||
TEST(KeeloqProtocolTest, TdefGroupFrameSendsTheGroupAfterThe64BitWord) {
|
||||
std::vector<bool> bits = sent_bits(encode(tdef_frame(0x0F)));
|
||||
ASSERT_EQ(bits.size(), 72u);
|
||||
EXPECT_EQ(field(bits, 0, 32), 0xF94A09F2u);
|
||||
EXPECT_EQ(field(bits, 32, 28), 0x1234500u);
|
||||
EXPECT_EQ(field(bits, 60, 4), 0x8u);
|
||||
EXPECT_EQ(field(bits, 64, 8), 0x0Fu);
|
||||
}
|
||||
|
||||
TEST(KeeloqProtocolTest, TdefChannelOneSendsNoChannelTenBit) {
|
||||
// Channels 1 to 8 have no bit in the group. Bit 65 is the HCS301 repeat bit, which is always 1
|
||||
// without a group; a Jarolift receiver reads it as channel 10.
|
||||
std::vector<bool> bits = sent_bits(encode(tdef_frame(0x00)));
|
||||
ASSERT_EQ(bits.size(), 72u);
|
||||
EXPECT_EQ(field(bits, 64, 8), 0x00u);
|
||||
}
|
||||
|
||||
TEST(KeeloqProtocolTest, TdefGroupRoundTrips) {
|
||||
// 0x0F is channels 9 to 12, 0x80 is channel 16, the last bit of the frame.
|
||||
for (uint8_t group : {0x0F, 0x80}) {
|
||||
auto decoded = decode(as_received(encode(tdef_frame(group))));
|
||||
ASSERT_TRUE(decoded.has_value()) << int(group);
|
||||
if (decoded.has_value()) {
|
||||
EXPECT_EQ(decoded->address, 0x1234500u);
|
||||
EXPECT_EQ(decoded->command, 0x8);
|
||||
EXPECT_EQ(decoded->encrypted, 0xF94A09F2u);
|
||||
EXPECT_EQ(decoded->suffix_bits, 6);
|
||||
EXPECT_EQ(decoded->group(), group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(KeeloqProtocolTest, AReceivedRepeatBitOfZeroIsSentAgainAsZero) {
|
||||
KeeloqData received = frame(0, 0);
|
||||
received.repeat = false;
|
||||
std::vector<bool> bits = sent_bits(encode(received));
|
||||
ASSERT_EQ(bits.size(), 66u);
|
||||
EXPECT_FALSE(bits[65]);
|
||||
|
||||
auto decoded = decode(as_received(encode(received)));
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (decoded.has_value()) {
|
||||
EXPECT_FALSE(decoded->repeat);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_base::testing
|
||||
|
||||
@@ -71,8 +71,8 @@ on_jvc:
|
||||
on_keeloq:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "on_keeloq: %lu %lu %u %u %u"
|
||||
args: ["long(x.encrypted)", "long(x.address)", "x.command", "x.suffix", "x.suffix_bits"]
|
||||
format: "on_keeloq: %lu %lu %u %u %u %u"
|
||||
args: ["long(x.encrypted)", "long(x.address)", "x.command", "x.suffix", "x.suffix_bits", "x.suffix_bits == 6 ? x.group() : 0"]
|
||||
on_haier:
|
||||
then:
|
||||
- logger.log:
|
||||
|
||||
@@ -385,6 +385,17 @@ button:
|
||||
code: 0xd19ef0a9
|
||||
suffix: 0x00
|
||||
suffix_bits: 8
|
||||
- platform: template
|
||||
name: KeeLoq TDEF group
|
||||
on_press:
|
||||
remote_transmitter.transmit_keeloq:
|
||||
address: 0x1234500
|
||||
command: 0x8
|
||||
code: 0xF94A09F2
|
||||
group: 0x0F
|
||||
repeat:
|
||||
times: 2
|
||||
wait_time: 0ms
|
||||
- platform: template
|
||||
name: KeeLoq lambda with repeat
|
||||
on_press:
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.components.remote_base import (
|
||||
_keeloq_suffix_fits,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_GROUP
|
||||
from esphome.core import Lambda
|
||||
|
||||
|
||||
@@ -43,3 +44,21 @@ def test_transmit_action_checks_the_width() -> None:
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="0x100 does not fit in 8 suffix_bits"):
|
||||
schema({**base, CONF_SUFFIX: 0x100})
|
||||
|
||||
|
||||
def test_group_takes_the_place_of_level_and_suffix() -> None:
|
||||
schema = ACTION_REGISTRY["remote_transmitter.transmit_keeloq"].raw_schema
|
||||
base = {"address": 0x1234500, "code": 0xF94A09F2, CONF_GROUP: 0x0F}
|
||||
assert schema(base)[CONF_GROUP] == 0x0F
|
||||
with pytest.raises(cv.Invalid, match="group cannot be combined with suffix"):
|
||||
schema({**base, CONF_SUFFIX_BITS: 6})
|
||||
with pytest.raises(cv.Invalid, match="group cannot be combined with suffix"):
|
||||
schema({**base, CONF_SUFFIX: 0})
|
||||
with pytest.raises(cv.Invalid, match="remove level"):
|
||||
schema({**base, "level": True})
|
||||
|
||||
|
||||
def test_group_is_one_byte() -> None:
|
||||
schema = ACTION_REGISTRY["remote_transmitter.transmit_keeloq"].raw_schema
|
||||
with pytest.raises(cv.Invalid):
|
||||
schema({"address": 0x1, "code": 0x2, CONF_GROUP: 0x100})
|
||||
|
||||
Reference in New Issue
Block a user