[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:
Michael Smith
2026-10-12 07:37:31 -10:00
committed by GitHub
co-authored by Copilot Autofix powered by AI Claude Fable 5.1 J. Nick Koston
parent db9138c550
commit 99b61760c3
10 changed files with 245 additions and 1 deletions
@@ -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
+1
View File
@@ -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();")
+1
View File
@@ -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({})