mirror of
https://github.com/esphome/esphome.git
synced 2026-10-19 04:32:50 +00:00
[remote_base] Add a protocol for 433MHz A-OK remotes (#20991)
This commit is contained in:
@@ -13,6 +13,7 @@ from esphome.const import (
|
||||
CONF_BUTTON,
|
||||
CONF_CARRIER_FREQUENCY,
|
||||
CONF_CHANNEL,
|
||||
CONF_CHANNELS,
|
||||
CONF_CHECK,
|
||||
CONF_CODE,
|
||||
CONF_COMMAND,
|
||||
@@ -71,6 +72,7 @@ CONF_CONTROL = "control"
|
||||
CONF_CONTROL_IN_KEY = "control_in_key"
|
||||
CONF_ROLLING_CODE = "rolling_code"
|
||||
CONF_EXTENSION = "extension"
|
||||
CONF_REMOTE_ID = "remote_id"
|
||||
|
||||
ns = remote_base_ns = cg.esphome_ns.namespace("remote_base")
|
||||
RemoteProtocol = ns.class_("RemoteProtocol")
|
||||
@@ -2911,3 +2913,55 @@ register_protocol_action(
|
||||
(CONF_MODE, "mode", IRA211ModeEnum),
|
||||
(CONF_FAN_MODE, "fan", IRA211FanEnum),
|
||||
)
|
||||
|
||||
|
||||
# A-OK
|
||||
(
|
||||
AOKData,
|
||||
AOKBinarySensor,
|
||||
AOKTrigger,
|
||||
_,
|
||||
AOKDumper,
|
||||
) = declare_protocol("AOK")
|
||||
|
||||
AOK_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_REMOTE_ID): cv.hex_int_range(0, 0xFFFFFF),
|
||||
cv.Required(CONF_CHANNELS): cv.hex_uint16_t,
|
||||
cv.Required(CONF_COMMAND): cv.hex_uint8_t,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_binary_sensor("aok", AOKBinarySensor, AOK_SCHEMA)
|
||||
def aok_binary_sensor(var: MockObj, config: ConfigType) -> None:
|
||||
cg.add(
|
||||
var.set_data(
|
||||
cg.StructInitializer(
|
||||
AOKData,
|
||||
("remote_id", config[CONF_REMOTE_ID]),
|
||||
("channels", config[CONF_CHANNELS]),
|
||||
("command", config[CONF_COMMAND]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@register_trigger("aok", AOKTrigger, AOKData)
|
||||
def aok_trigger(var: MockObj, config: ConfigType) -> None:
|
||||
"""The trigger takes no options beyond the automation."""
|
||||
|
||||
|
||||
@register_dumper("aok", AOKDumper)
|
||||
def aok_dumper(var: MockObj, config: ConfigType) -> None:
|
||||
"""The dumper takes no options."""
|
||||
|
||||
|
||||
register_protocol_action(
|
||||
"aok",
|
||||
"AOK",
|
||||
AOK_SCHEMA,
|
||||
(CONF_REMOTE_ID, "remote_id", cg.uint32),
|
||||
(CONF_CHANNELS, "channels", cg.uint16),
|
||||
(CONF_COMMAND, "command", cg.uint8),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "aok_protocol.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::remote_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "remote.aok");
|
||||
|
||||
// A frame is a long start mark, then 64 bits, then a long gap. A one bit is a long mark and a short space,
|
||||
// a zero bit is a short mark and a long space. The bits are: header (8), remote id (24), channels (16),
|
||||
// command (8) and checksum (8), most significant bit first.
|
||||
static constexpr uint8_t NBITS = 64;
|
||||
static constexpr uint32_t START_HIGH_US = 5000;
|
||||
static constexpr uint32_t LONG_US = 600;
|
||||
static constexpr uint32_t SHORT_US = 285;
|
||||
static constexpr uint8_t HEADER = 0xA3;
|
||||
static constexpr uint32_t REMOTE_ID_MASK = 0xFFFFFF;
|
||||
|
||||
// The sum of the six bytes between the header and the checksum
|
||||
static uint8_t checksum(uint64_t frame) {
|
||||
uint8_t sum = 0;
|
||||
for (uint8_t shift = 8; shift < 56; shift += 8) {
|
||||
sum += frame >> shift;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void AOKProtocol::encode(RemoteTransmitData *dst, const AOKData &data) {
|
||||
uint64_t frame = (uint64_t(HEADER) << 56) | (uint64_t(data.remote_id & REMOTE_ID_MASK) << 32) |
|
||||
(uint64_t(data.channels) << 16) | (uint64_t(data.command) << 8);
|
||||
frame |= checksum(frame);
|
||||
|
||||
dst->reserve((NBITS + 2) * 2);
|
||||
dst->item(START_HIGH_US, LONG_US);
|
||||
for (uint64_t mask = 1ULL << (NBITS - 1); mask; mask >>= 1) {
|
||||
if (frame & mask) {
|
||||
dst->item(LONG_US, SHORT_US);
|
||||
} else {
|
||||
dst->item(SHORT_US, LONG_US);
|
||||
}
|
||||
}
|
||||
dst->item(LONG_US, START_HIGH_US);
|
||||
}
|
||||
|
||||
optional<AOKData> AOKProtocol::decode(RemoteReceiveData src) {
|
||||
// A buffer can hold noise and several repeats of a frame, so look for the first good one. After any
|
||||
// failure the loop restarts at the next timing, or where the failed frame stopped.
|
||||
while (src.is_valid()) {
|
||||
if (!src.expect_pulse_with_gap(START_HIGH_US, LONG_US)) {
|
||||
src.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t frame = 0;
|
||||
uint8_t bits = 0;
|
||||
for (; bits < NBITS; bits++) {
|
||||
frame <<= 1;
|
||||
if (src.expect_item(LONG_US, SHORT_US)) {
|
||||
frame |= 1;
|
||||
} else if (!src.expect_item(SHORT_US, LONG_US)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bits == NBITS && uint8_t(frame >> 56) == HEADER && uint8_t(frame) == checksum(frame)) {
|
||||
return AOKData{
|
||||
.remote_id = uint32_t(frame >> 32) & REMOTE_ID_MASK,
|
||||
.channels = uint16_t(frame >> 16),
|
||||
.command = uint8_t(frame >> 8),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void AOKProtocol::dump(const AOKData &data) {
|
||||
ESP_LOGI(TAG, "Received A-OK: remote_id=0x%06" PRIX32 ", channels=0x%04X, command=0x%02X", data.remote_id,
|
||||
data.channels, data.command);
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "remote_base.h"
|
||||
|
||||
namespace esphome::remote_base {
|
||||
|
||||
struct AOKData {
|
||||
uint32_t remote_id; // 24 bits
|
||||
uint16_t channels;
|
||||
uint8_t command;
|
||||
|
||||
bool operator==(const AOKData &rhs) const {
|
||||
return this->remote_id == rhs.remote_id && this->channels == rhs.channels && this->command == rhs.command;
|
||||
}
|
||||
};
|
||||
|
||||
class AOKProtocol : public RemoteProtocol<AOKData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const AOKData &data);
|
||||
optional<AOKData> decode(RemoteReceiveData src);
|
||||
void dump(const AOKData &data);
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(AOK)
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
@@ -186,6 +186,7 @@
|
||||
#define REMOTE_BASE_LISTENER_COUNT 1
|
||||
#define USE_REMOTE_PROTOCOL_ABBWELCOME
|
||||
#define USE_REMOTE_PROTOCOL_AEHA
|
||||
#define USE_REMOTE_PROTOCOL_AOK
|
||||
#define USE_REMOTE_PROTOCOL_BEO4
|
||||
#define USE_REMOTE_PROTOCOL_BRENNENSTUHL
|
||||
#define USE_REMOTE_PROTOCOL_BYRONSX
|
||||
|
||||
@@ -12,6 +12,7 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
"haier",
|
||||
"mirage",
|
||||
"aeha",
|
||||
"aok",
|
||||
"keeloq",
|
||||
"nexus",
|
||||
"ira211",
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/remote_base/aok_protocol.h"
|
||||
|
||||
namespace esphome::remote_base::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t FRAME = 0xA312345600A10B48ULL;
|
||||
// The start mark and space, then 64 bits of a mark and a space each, then the closing mark and space
|
||||
constexpr size_t TIMINGS_SIZE = (1 + 64 + 1) * 2;
|
||||
|
||||
RawTimings encode(const AOKData &data) {
|
||||
RemoteTransmitData tx;
|
||||
AOKProtocol().encode(&tx, data);
|
||||
return tx.get_data();
|
||||
}
|
||||
|
||||
optional<AOKData> decode(const RawTimings &timings) {
|
||||
return AOKProtocol().decode(RemoteReceiveData(timings, 25, TOLERANCE_MODE_PERCENTAGE));
|
||||
}
|
||||
|
||||
// Bit `n` counted from the first one sent starts at timing 2 + 2 * n
|
||||
RawTimings frame_timings(uint64_t frame) {
|
||||
RawTimings timings{5000, -600};
|
||||
for (int i = 63; i >= 0; i--) {
|
||||
if ((frame >> i) & 1) {
|
||||
timings.insert(timings.end(), {600, -285});
|
||||
} else {
|
||||
timings.insert(timings.end(), {285, -600});
|
||||
}
|
||||
}
|
||||
timings.insert(timings.end(), {600, -5000});
|
||||
return timings;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(AOKProtocolTest, EncodesKnownFrame) {
|
||||
EXPECT_EQ(encode(AOKData{.remote_id = 0x123456, .channels = 0x00A1, .command = 0x0B}), frame_timings(FRAME));
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, DecodesKnownFrame) {
|
||||
auto decoded = decode(frame_timings(FRAME));
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (!decoded.has_value()) {
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(decoded->remote_id, 0x123456u);
|
||||
EXPECT_EQ(decoded->channels, 0x00A1);
|
||||
EXPECT_EQ(decoded->command, 0x0B);
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, RoundTrips) {
|
||||
for (const AOKData &sent :
|
||||
{AOKData{0x000000, 0x0000, 0x00}, AOKData{0xFFFFFF, 0xFFFF, 0xFF}, AOKData{0xABCDEF, 0x8001, 0x7E}}) {
|
||||
const RawTimings timings = encode(sent);
|
||||
ASSERT_EQ(timings.size(), TIMINGS_SIZE);
|
||||
auto decoded = decode(timings);
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (!decoded.has_value()) {
|
||||
return;
|
||||
}
|
||||
EXPECT_TRUE(*decoded == sent);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, EncodingKeepsRemoteIdToItsFieldWidth) {
|
||||
auto decoded = decode(encode(AOKData{.remote_id = 0xFF123456, .channels = 0x00A1, .command = 0x0B}));
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (!decoded.has_value()) {
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(decoded->remote_id, 0x123456u);
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, RejectsBadChecksum) { EXPECT_FALSE(decode(frame_timings(FRAME ^ 1)).has_value()); }
|
||||
|
||||
TEST(AOKProtocolTest, RejectsBadHeader) {
|
||||
// The header is not part of the checksum, so only the header check can reject this
|
||||
EXPECT_FALSE(decode(frame_timings(FRAME ^ (1ULL << 56))).has_value());
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, SkipsLeadingNoise) {
|
||||
RawTimings timings{300, -1200, 150, -90};
|
||||
const RawTimings frame = frame_timings(FRAME);
|
||||
timings.insert(timings.end(), frame.begin(), frame.end());
|
||||
auto decoded = decode(timings);
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (!decoded.has_value()) {
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(decoded->remote_id, 0x123456u);
|
||||
EXPECT_EQ(decoded->command, 0x0B);
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, UsesRepeatWhenFirstFrameIsCorrupt) {
|
||||
RawTimings timings = frame_timings(FRAME ^ 1);
|
||||
const RawTimings good = frame_timings(FRAME);
|
||||
timings.insert(timings.end(), good.begin(), good.end());
|
||||
auto decoded = decode(timings);
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (!decoded.has_value()) {
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(decoded->command, 0x0B);
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, UsesRepeatWhenFirstFrameIsCutShort) {
|
||||
RawTimings timings = frame_timings(FRAME);
|
||||
timings.resize(timings.size() - 40);
|
||||
const RawTimings good = frame_timings(FRAME);
|
||||
timings.insert(timings.end(), good.begin(), good.end());
|
||||
EXPECT_TRUE(decode(timings).has_value());
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, RejectsBadStart) {
|
||||
RawTimings timings = frame_timings(FRAME);
|
||||
timings[0] = 2000;
|
||||
EXPECT_FALSE(decode(timings).has_value());
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, RejectsTruncatedFrame) {
|
||||
RawTimings timings = frame_timings(FRAME);
|
||||
timings.resize(timings.size() - 6);
|
||||
EXPECT_FALSE(decode(timings).has_value());
|
||||
}
|
||||
|
||||
TEST(AOKProtocolTest, RejectsBadBitTiming) {
|
||||
RawTimings timings = frame_timings(FRAME);
|
||||
timings[2 + 2 * 20] = 1200;
|
||||
EXPECT_FALSE(decode(timings).has_value());
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_base::testing
|
||||
@@ -187,6 +187,11 @@ on_toto:
|
||||
- logger.log:
|
||||
format: "on_toto: %u %u %u"
|
||||
args: ["x.rc_code_1", "x.rc_code_2", "x.command"]
|
||||
on_aok:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "on_aok: %u %u %u"
|
||||
args: ["x.remote_id", "x.channels", "x.command"]
|
||||
on_hob2hood:
|
||||
then:
|
||||
- logger.log:
|
||||
|
||||
@@ -395,6 +395,23 @@ button:
|
||||
repeat:
|
||||
times: 3
|
||||
wait_time: 10ms
|
||||
- platform: template
|
||||
name: A-OK
|
||||
on_press:
|
||||
remote_transmitter.transmit_aok:
|
||||
remote_id: 0x123456
|
||||
channels: 0x00A1
|
||||
command: 0x0B
|
||||
- platform: template
|
||||
name: A-OK lambda with repeat
|
||||
on_press:
|
||||
remote_transmitter.transmit_aok:
|
||||
remote_id: 0x123456
|
||||
channels: !lambda return 0x1000;
|
||||
command: 0x43
|
||||
repeat:
|
||||
times: 8
|
||||
wait_time: 5ms
|
||||
- platform: template
|
||||
name: Toto with repeat
|
||||
on_press:
|
||||
|
||||
Reference in New Issue
Block a user