mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[remote_base] Add the Pioneer WYT transmit protocol (#17175)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
Claude Fable 5.1
J. Nick Koston
parent
db9138c550
commit
99b61760c3
@@ -1228,6 +1228,24 @@ register_protocol_action(
|
||||
)
|
||||
|
||||
|
||||
# PioneerWyt
|
||||
PioneerWytData = ns.class_("PioneerWytData")
|
||||
PIONEER_WYT_SCHEMA = cv.Schema(
|
||||
{
|
||||
# 13 bytes get the checksum appended; a 14th byte is replaced by the recomputed checksum
|
||||
cv.Required(CONF_CODE): cv.All([cv.hex_uint8_t], cv.Length(min=13, max=14)),
|
||||
}
|
||||
)
|
||||
|
||||
register_protocol_action(
|
||||
"pioneer_wyt",
|
||||
"PioneerWyt",
|
||||
PIONEER_WYT_SCHEMA,
|
||||
_bytes_field(CONF_CODE, f"data = {PioneerWytData}({{}})"),
|
||||
automation.ApplyCall("data.finalize()"),
|
||||
)
|
||||
|
||||
|
||||
# Pronto
|
||||
(
|
||||
ProntoData,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "pioneer_wyt_protocol.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::remote_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "remote.pioneer_wyt");
|
||||
|
||||
// The timings and frame header are shared with tcl112
|
||||
static constexpr uint32_t HEADER_MARK_US = 3100;
|
||||
static constexpr uint32_t HEADER_SPACE_US = 1650;
|
||||
static constexpr uint32_t BIT_MARK_US = 500;
|
||||
static constexpr uint32_t BIT_ONE_SPACE_US = 1100;
|
||||
static constexpr uint32_t BIT_ZERO_SPACE_US = 350;
|
||||
|
||||
void PioneerWytProtocol::encode(RemoteTransmitData *dst, const PioneerWytData &data) {
|
||||
char hex_buf[format_hex_pretty_size(PioneerWytData::SIZE)];
|
||||
ESP_LOGD(TAG, "Transmit PioneerWyt: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
dst->set_carrier_frequency(38000);
|
||||
// header mark and space, a mark and space per bit, then the trailing mark
|
||||
dst->reserve(dst->get_data().size() + 3 + PioneerWytData::SIZE * 8 * 2);
|
||||
dst->item(HEADER_MARK_US, HEADER_SPACE_US);
|
||||
for (uint8_t i = 0; i < data.size(); i++) {
|
||||
for (uint8_t b = 0; b < 8; b++)
|
||||
dst->item(BIT_MARK_US, (data[i] & (1 << b)) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US);
|
||||
}
|
||||
dst->mark(BIT_MARK_US);
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "remote_base.h"
|
||||
|
||||
namespace esphome::remote_base {
|
||||
|
||||
class PioneerWytData {
|
||||
public:
|
||||
static constexpr uint8_t SIZE = 14;
|
||||
enum PioneerWytDataType : uint8_t {
|
||||
PIONEER_WYT_TYPE_GENERAL = 0x01,
|
||||
PIONEER_WYT_TYPE_FAN = 0x02,
|
||||
};
|
||||
|
||||
// Make default
|
||||
PioneerWytData() {}
|
||||
// Make from initializer_list
|
||||
PioneerWytData(std::initializer_list<uint8_t> data) {
|
||||
std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin());
|
||||
}
|
||||
// Make from vector
|
||||
PioneerWytData(const std::vector<uint8_t> &data) {
|
||||
std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin());
|
||||
}
|
||||
PioneerWytData(const uint8_t *data, size_t len) {
|
||||
progmem_memcpy(this->data_.data(), data, std::min(len, this->data_.size()));
|
||||
}
|
||||
|
||||
uint8_t *data() { return this->data_.data(); }
|
||||
const uint8_t *data() const { return this->data_.data(); }
|
||||
uint8_t size() const { return this->data_.size(); }
|
||||
// FAN frames add 0x0F to the byte sum
|
||||
void finalize() {
|
||||
this->data_[OFFSET_CS] =
|
||||
sum_bytes(this->data_.data(), OFFSET_CS, this->type() == PIONEER_WYT_TYPE_FAN ? 0x0F : 0x00);
|
||||
}
|
||||
PioneerWytDataType type() const { return static_cast<PioneerWytDataType>(this->data_[3]); }
|
||||
uint8_t &operator[](size_t idx) { return this->data_[idx]; }
|
||||
const uint8_t &operator[](size_t idx) const { return this->data_[idx]; }
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t OFFSET_CS = SIZE - 1;
|
||||
std::array<uint8_t, SIZE> data_{};
|
||||
};
|
||||
|
||||
class PioneerWytProtocol : public RemoteProtocol<PioneerWytData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const PioneerWytData &data);
|
||||
};
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
@@ -182,6 +182,7 @@
|
||||
#define USE_REMOTE_PROTOCOL_ONKYORI
|
||||
#define USE_REMOTE_PROTOCOL_PANASONIC
|
||||
#define USE_REMOTE_PROTOCOL_PIONEER
|
||||
#define USE_REMOTE_PROTOCOL_PIONEER_WYT
|
||||
#define USE_REMOTE_PROTOCOL_PRONTO
|
||||
#define USE_REMOTE_PROTOCOL_RAW
|
||||
#define USE_REMOTE_PROTOCOL_RC5
|
||||
|
||||
@@ -31,3 +31,8 @@ button:
|
||||
on_press:
|
||||
- remote_transmitter.transmit_midea:
|
||||
code: !lambda return {0xA2, 0x08, 0xFF, 0xFF, 0xFF};
|
||||
- platform: template
|
||||
name: Pioneer WYT lambda
|
||||
on_press:
|
||||
- remote_transmitter.transmit_pioneer_wyt:
|
||||
code: !lambda return {0x23, 0xCB, 0x26, 0x01, 0x00, 0x24, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x80};
|
||||
|
||||
@@ -11,7 +11,7 @@ def test_byte_payloads(
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("byte_payloads.yaml"))
|
||||
abb, aeha, midea = protocol_call_bodies(main_cpp)
|
||||
abb, aeha, midea, pioneer_wyt = protocol_call_bodies(main_cpp)
|
||||
# The address length goes first, a configured id turns the automatic one off
|
||||
assert abb.index("set_three_byte_address(true);") < abb.index("set_source_address(")
|
||||
assert abb.index("auto_message_id = true;") < abb.index(
|
||||
@@ -31,3 +31,9 @@ def test_byte_payloads(
|
||||
in midea
|
||||
)
|
||||
assert midea.rstrip().endswith("apply_call.data.finalize();")
|
||||
# A 13-byte code is copied into the frame and the checksum is appended by finalize()
|
||||
assert (
|
||||
"apply_call.data = remote_base::PioneerWytData([]() -> std::vector<uint8_t> {"
|
||||
in pioneer_wyt
|
||||
)
|
||||
assert pioneer_wyt.rstrip().endswith("apply_call.data.finalize();")
|
||||
|
||||
@@ -17,6 +17,7 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
"nec",
|
||||
"magiquest",
|
||||
"rc5x",
|
||||
"pioneer_wyt",
|
||||
"drayton",
|
||||
"panasonic",
|
||||
):
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include "esphome/components/remote_base/pioneer_wyt_protocol.h"
|
||||
|
||||
namespace esphome::remote_base::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
using Frame = std::array<uint8_t, PioneerWytData::SIZE>;
|
||||
|
||||
// Factory remote capture: cool mode, 26°C, beeper on, display on; the last byte is the checksum
|
||||
constexpr Frame COOL_26_CAPTURE = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x24, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC1};
|
||||
|
||||
Frame bytes(const PioneerWytData &data) {
|
||||
Frame out{};
|
||||
std::copy_n(data.data(), out.size(), out.begin());
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(PioneerWytProtocolTest, RawCodeGetsTheCaptureChecksum) {
|
||||
PioneerWytData data(std::vector<uint8_t>(COOL_26_CAPTURE.begin(), COOL_26_CAPTURE.end() - 1));
|
||||
data.finalize();
|
||||
EXPECT_EQ(bytes(data), COOL_26_CAPTURE);
|
||||
}
|
||||
|
||||
TEST(PioneerWytProtocolTest, GivenChecksumIsRecomputed) {
|
||||
PioneerWytData data(COOL_26_CAPTURE.data(), COOL_26_CAPTURE.size());
|
||||
data[PioneerWytData::SIZE - 1] = 0x00;
|
||||
data.finalize();
|
||||
EXPECT_EQ(bytes(data), COOL_26_CAPTURE);
|
||||
}
|
||||
|
||||
// FAN frames add 0x0F to the byte sum
|
||||
TEST(PioneerWytProtocolTest, FanFrameChecksumHasOffset) {
|
||||
PioneerWytData data{0x23, 0xCB, 0x26, 0x02, 0x00, 0x40, 0xC0, 0x10, 0xC0, 0x00, 0x00, 0x00, 0x00};
|
||||
EXPECT_EQ(data.type(), PioneerWytData::PIONEER_WYT_TYPE_FAN);
|
||||
data.finalize();
|
||||
EXPECT_EQ(data[PioneerWytData::SIZE - 1], 0xF5);
|
||||
}
|
||||
|
||||
TEST(PioneerWytProtocolTest, EncodesHeaderAndAllBits) {
|
||||
RemoteTransmitData dst;
|
||||
PioneerWytProtocol().encode(&dst, PioneerWytData(COOL_26_CAPTURE.data(), COOL_26_CAPTURE.size()));
|
||||
// header mark and space, a mark and space per bit, then the trailing mark
|
||||
const auto &raw = dst.get_data();
|
||||
ASSERT_EQ(raw.size(), 3u + PioneerWytData::SIZE * 8 * 2);
|
||||
EXPECT_EQ(dst.get_carrier_frequency(), 38000u);
|
||||
EXPECT_EQ(raw[0], 3100);
|
||||
EXPECT_EQ(raw[1], -1650);
|
||||
// 0x23 LSB first: bit 0 is one, bit 2 is zero
|
||||
EXPECT_EQ(raw[3], -1100);
|
||||
EXPECT_EQ(raw[7], -350);
|
||||
EXPECT_EQ(raw.back(), 500);
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_base::testing
|
||||
@@ -127,6 +127,18 @@ button:
|
||||
rc_code_2: 0xA506
|
||||
repeat:
|
||||
times: 2
|
||||
- platform: template
|
||||
name: Pioneer WYT
|
||||
on_press:
|
||||
remote_transmitter.transmit_pioneer_wyt:
|
||||
# Captured from the factory remote: cool mode, 26°C. The encoder appends the checksum.
|
||||
code: [0x23, 0xCB, 0x26, 0x01, 0x00, 0x24, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x80]
|
||||
- platform: template
|
||||
name: Pioneer WYT With Checksum
|
||||
on_press:
|
||||
remote_transmitter.transmit_pioneer_wyt:
|
||||
# Same capture with its checksum byte included
|
||||
code: [0x23, 0xCB, 0x26, 0x01, 0x00, 0x24, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC1]
|
||||
- platform: template
|
||||
name: RC Switch Raw
|
||||
on_press:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Schema checks for the pioneer_wyt transmit action."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import Lambda
|
||||
|
||||
# Factory remote capture: cool mode, 26°C, without the trailing checksum byte
|
||||
RAW_CODE = [
|
||||
0x23,
|
||||
0xCB,
|
||||
0x26,
|
||||
0x01,
|
||||
0x00,
|
||||
0x24,
|
||||
0x03,
|
||||
0x05,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x80,
|
||||
]
|
||||
RAW_CODE_CHECKSUM = 0xC1
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def schema() -> cv.Schema:
|
||||
return ACTION_REGISTRY["remote_transmitter.transmit_pioneer_wyt"].raw_schema
|
||||
|
||||
|
||||
def test_code_checksum_matches_capture() -> None:
|
||||
assert sum(RAW_CODE) & 0xFF == RAW_CODE_CHECKSUM
|
||||
|
||||
|
||||
def test_code_accepts_with_and_without_checksum_byte(schema: cv.Schema) -> None:
|
||||
with_checksum = [*RAW_CODE, RAW_CODE_CHECKSUM]
|
||||
assert schema({"code": RAW_CODE})["code"] == RAW_CODE
|
||||
assert schema({"code": with_checksum})["code"] == with_checksum
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [RAW_CODE[:-1], [*RAW_CODE, RAW_CODE_CHECKSUM, 0x00]])
|
||||
def test_code_rejects_other_lengths(schema: cv.Schema, code: list[int]) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
schema({"code": code})
|
||||
|
||||
|
||||
def test_code_accepts_lambda(schema: cv.Schema) -> None:
|
||||
code = Lambda("return {0x23, 0xCB};")
|
||||
assert isinstance(schema({"code": code})["code"], Lambda)
|
||||
|
||||
|
||||
def test_code_is_required(schema: cv.Schema) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
schema({})
|
||||
Reference in New Issue
Block a user