mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[remote_base] updates to support 56 bit panasonic codes (#20260)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
@@ -59,6 +59,7 @@ AUTO_LOAD = ["binary_sensor"]
|
||||
DOMAIN = "remote_base"
|
||||
|
||||
|
||||
CONF_ADDRESS_2 = "address_2"
|
||||
CONF_RECEIVER_ID = "receiver_id"
|
||||
CONF_TRANSMITTER_ID = "transmitter_id"
|
||||
CONF_FIRST = "first"
|
||||
@@ -2122,41 +2123,73 @@ register_protocol_action(
|
||||
) = declare_protocol("Panasonic")
|
||||
PANASONIC_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_NBITS, default=48): cv.one_of(48, 56, int=True),
|
||||
cv.Required(CONF_ADDRESS): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_ADDRESS_2, default=0x00): cv.hex_uint8_t,
|
||||
cv.Required(CONF_COMMAND): cv.hex_uint32_t,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_binary_sensor("panasonic", PanasonicBinarySensor, PANASONIC_SCHEMA)
|
||||
def panasonic_binary_sensor(var, config):
|
||||
def _panasonic_address_2_needs_56_bits(config: ConfigType) -> ConfigType:
|
||||
# A 48-bit code has no second address byte; a lambda is only known on the device
|
||||
address_2 = config[CONF_ADDRESS_2]
|
||||
if config[CONF_NBITS] == 48 and isinstance(address_2, int) and address_2 != 0:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_ADDRESS_2} is only sent with {CONF_NBITS}: 56", [CONF_ADDRESS_2]
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
# Transmit only
|
||||
PANASONIC_ACTION_SCHEMA = PANASONIC_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_CARRIER_FREQUENCY, default="35000Hz"): cv.All(
|
||||
cv.frequency, cv.uint32_t
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_binary_sensor(
|
||||
"panasonic",
|
||||
PanasonicBinarySensor,
|
||||
cv.All(PANASONIC_SCHEMA, _panasonic_address_2_needs_56_bits),
|
||||
)
|
||||
def panasonic_binary_sensor(var: MockObj, config: ConfigType) -> None:
|
||||
cg.add(
|
||||
var.set_data(
|
||||
cg.StructInitializer(
|
||||
PanasonicData,
|
||||
("address", config[CONF_ADDRESS]),
|
||||
("command", config[CONF_COMMAND]),
|
||||
("nbits", config[CONF_NBITS]),
|
||||
("address_2", config[CONF_ADDRESS_2]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@register_trigger("panasonic", PanasonicTrigger, PanasonicData)
|
||||
def panasonic_trigger(var, config):
|
||||
pass
|
||||
def panasonic_trigger(var: MockObj, config: ConfigType) -> None:
|
||||
"""The trigger takes no options beyond the automation."""
|
||||
|
||||
|
||||
@register_dumper("panasonic", PanasonicDumper)
|
||||
def panasonic_dumper(var, config):
|
||||
pass
|
||||
def panasonic_dumper(var: MockObj, config: ConfigType) -> None:
|
||||
"""The dumper takes no options."""
|
||||
|
||||
|
||||
register_protocol_action(
|
||||
"panasonic",
|
||||
"Panasonic",
|
||||
PANASONIC_SCHEMA,
|
||||
PANASONIC_ACTION_SCHEMA,
|
||||
(CONF_NBITS, "nbits", cg.uint8),
|
||||
(CONF_ADDRESS, "address", cg.uint16),
|
||||
(CONF_ADDRESS_2, "address_2", cg.uint8),
|
||||
(CONF_COMMAND, "command", cg.uint32),
|
||||
(CONF_CARRIER_FREQUENCY, "carrier_frequency", cg.uint32),
|
||||
validate=_panasonic_address_2_needs_56_bits,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,62 +11,72 @@ static constexpr uint32_t BIT_HIGH_US = 502;
|
||||
static constexpr uint32_t BIT_ZERO_LOW_US = 400;
|
||||
static constexpr uint32_t BIT_ONE_LOW_US = 1244;
|
||||
|
||||
static constexpr uint8_t NBITS_ADDRESS = 16;
|
||||
static constexpr uint8_t NBITS_ADDRESS_2 = 8;
|
||||
static constexpr uint8_t NBITS_COMMAND = 32;
|
||||
static constexpr uint8_t NBITS_SHORT = NBITS_ADDRESS + NBITS_COMMAND;
|
||||
static constexpr uint8_t NBITS_LONG = NBITS_SHORT + NBITS_ADDRESS_2;
|
||||
|
||||
static void encode_bits(RemoteTransmitData *dst, uint32_t value, uint8_t nbits) {
|
||||
for (uint32_t mask = 1UL << (nbits - 1); mask != 0; mask >>= 1) {
|
||||
dst->item(BIT_HIGH_US, (value & mask) ? BIT_ONE_LOW_US : BIT_ZERO_LOW_US);
|
||||
}
|
||||
}
|
||||
|
||||
static bool decode_bits(RemoteReceiveData &src, uint32_t &value, uint8_t nbits) {
|
||||
value = 0;
|
||||
for (uint32_t mask = 1UL << (nbits - 1); mask != 0; mask >>= 1) {
|
||||
if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) {
|
||||
value |= mask;
|
||||
} else if (!src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PanasonicProtocol::encode(RemoteTransmitData *dst, const PanasonicData &data) {
|
||||
dst->reserve(100);
|
||||
if (data.nbits != NBITS_SHORT && data.nbits != NBITS_LONG) {
|
||||
ESP_LOGW(TAG, "Unsupported nbits %u, expected 48 or 56", data.nbits);
|
||||
return;
|
||||
}
|
||||
// Header, one item per bit, trailing mark
|
||||
dst->reserve(3 + 2 * data.nbits);
|
||||
dst->set_carrier_frequency(data.carrier_frequency);
|
||||
dst->item(HEADER_HIGH_US, HEADER_LOW_US);
|
||||
dst->set_carrier_frequency(35000);
|
||||
|
||||
uint32_t mask;
|
||||
for (mask = 1UL << 15; mask != 0; mask >>= 1) {
|
||||
if (data.address & mask) {
|
||||
dst->item(BIT_HIGH_US, BIT_ONE_LOW_US);
|
||||
} else {
|
||||
dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US);
|
||||
}
|
||||
}
|
||||
|
||||
for (mask = 1UL << 31; mask != 0; mask >>= 1) {
|
||||
if (data.command & mask) {
|
||||
dst->item(BIT_HIGH_US, BIT_ONE_LOW_US);
|
||||
} else {
|
||||
dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US);
|
||||
}
|
||||
encode_bits(dst, data.address, NBITS_ADDRESS);
|
||||
if (data.nbits == NBITS_LONG) {
|
||||
encode_bits(dst, data.address_2, NBITS_ADDRESS_2);
|
||||
}
|
||||
encode_bits(dst, data.command, NBITS_COMMAND);
|
||||
dst->mark(BIT_HIGH_US);
|
||||
}
|
||||
|
||||
optional<PanasonicData> PanasonicProtocol::decode(RemoteReceiveData src) {
|
||||
PanasonicData out{
|
||||
.address = 0,
|
||||
.command = 0,
|
||||
};
|
||||
if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US))
|
||||
return {};
|
||||
|
||||
uint32_t mask;
|
||||
for (mask = 1UL << 15; mask != 0; mask >>= 1) {
|
||||
if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) {
|
||||
out.address |= mask;
|
||||
} else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) {
|
||||
out.address &= ~mask;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
uint32_t address;
|
||||
uint32_t word;
|
||||
if (!decode_bits(src, address, NBITS_ADDRESS) || !decode_bits(src, word, NBITS_COMMAND))
|
||||
return {};
|
||||
|
||||
for (mask = 1UL << 31; mask != 0; mask >>= 1) {
|
||||
if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) {
|
||||
out.command |= mask;
|
||||
} else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) {
|
||||
out.command &= ~mask;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
PanasonicData out{};
|
||||
out.address = address;
|
||||
uint32_t tail;
|
||||
if (decode_bits(src, tail, NBITS_ADDRESS_2)) {
|
||||
out.nbits = NBITS_LONG;
|
||||
out.address_2 = word >> 24;
|
||||
out.command = (word << 8) | tail;
|
||||
} else {
|
||||
out.command = word;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void PanasonicProtocol::dump(const PanasonicData &data) {
|
||||
ESP_LOGI(TAG, "Received Panasonic: address=0x%04X, command=0x%08" PRIX32, data.address, data.command);
|
||||
ESP_LOGI(TAG, "Received Panasonic: nbits=%u, address=0x%04X, address_2=0x%02X, command=0x%08" PRIX32, data.nbits,
|
||||
data.address, data.address_2, data.command);
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_base
|
||||
|
||||
@@ -8,10 +8,17 @@
|
||||
namespace esphome::remote_base {
|
||||
|
||||
struct PanasonicData {
|
||||
// address and command stay first so positional initializers written for the 48-bit struct keep working.
|
||||
uint16_t address;
|
||||
uint32_t command;
|
||||
uint8_t nbits{48}; // 48 or 56
|
||||
uint8_t address_2{0};
|
||||
// Transmit only: a receiver cannot measure the carrier, so it is not compared.
|
||||
uint32_t carrier_frequency{35000};
|
||||
|
||||
bool operator==(const PanasonicData &rhs) const { return address == rhs.address && command == rhs.command; }
|
||||
bool operator==(const PanasonicData &rhs) const {
|
||||
return address == rhs.address && address_2 == rhs.address_2 && command == rhs.command && nbits == rhs.nbits;
|
||||
}
|
||||
};
|
||||
|
||||
class PanasonicProtocol : public RemoteProtocol<PanasonicData> {
|
||||
|
||||
@@ -15,6 +15,7 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
"keeloq",
|
||||
"nexus",
|
||||
"rc5x",
|
||||
"panasonic",
|
||||
):
|
||||
request_protocol(protocol)
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "esphome/components/remote_base/panasonic_protocol.h"
|
||||
|
||||
namespace esphome::remote_base::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
const PanasonicData FRAME_48{0x4004, 0x0100BCBD};
|
||||
const PanasonicData FRAME_56{0x4004, 0x0100BCBD, 56, 0x12};
|
||||
|
||||
RawTimings encode(const PanasonicData &data) {
|
||||
RemoteTransmitData tx;
|
||||
PanasonicProtocol().encode(&tx, data);
|
||||
return tx.get_data();
|
||||
}
|
||||
|
||||
optional<PanasonicData> decode(const RawTimings &timings) {
|
||||
return PanasonicProtocol().decode(RemoteReceiveData(timings, 25, TOLERANCE_MODE_PERCENTAGE));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(PanasonicProtocolTest, ForwardsTheConfiguredCarrier) {
|
||||
PanasonicData data = FRAME_56;
|
||||
data.carrier_frequency = 36000;
|
||||
RemoteTransmitData tx;
|
||||
PanasonicProtocol().encode(&tx, data);
|
||||
EXPECT_EQ(tx.get_carrier_frequency(), 36000u);
|
||||
}
|
||||
|
||||
TEST(PanasonicProtocolTest, RoundTrips48BitFrame) {
|
||||
RemoteTransmitData tx;
|
||||
PanasonicProtocol().encode(&tx, FRAME_48);
|
||||
EXPECT_EQ(tx.get_data().size(), 2u + 2u * 48u + 1u);
|
||||
EXPECT_EQ(tx.get_carrier_frequency(), 35000u);
|
||||
auto decoded = decode(tx.get_data());
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (!decoded.has_value()) {
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(*decoded, FRAME_48);
|
||||
}
|
||||
|
||||
TEST(PanasonicProtocolTest, RoundTrips56BitFrame) {
|
||||
RawTimings timings = encode(FRAME_56);
|
||||
EXPECT_EQ(timings.size(), 2u + 2u * 56u + 1u);
|
||||
auto decoded = decode(timings);
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (!decoded.has_value()) {
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(*decoded, FRAME_56);
|
||||
}
|
||||
|
||||
TEST(PanasonicProtocolTest, Decodes56BitFrameWithTrailingItems) {
|
||||
RawTimings timings = encode(FRAME_56);
|
||||
timings.insert(timings.end(), {-20000, 3502, -1750, 502, -400});
|
||||
auto decoded = decode(timings);
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (!decoded.has_value()) {
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(*decoded, FRAME_56);
|
||||
}
|
||||
|
||||
TEST(PanasonicProtocolTest, Decodes48BitFrameWithTrailingItems) {
|
||||
RawTimings timings = encode(FRAME_48);
|
||||
// A repeat frame arriving inside the idle window.
|
||||
timings.insert(timings.end(), {-20000, 3502, -1750, 502, -400});
|
||||
auto decoded = decode(timings);
|
||||
ASSERT_TRUE(decoded.has_value());
|
||||
if (!decoded.has_value()) {
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(*decoded, FRAME_48);
|
||||
}
|
||||
|
||||
TEST(PanasonicProtocolTest, RejectsTruncatedFrame) {
|
||||
RawTimings timings = encode(FRAME_48);
|
||||
timings.resize(timings.size() - 4);
|
||||
EXPECT_FALSE(decode(timings).has_value());
|
||||
}
|
||||
|
||||
TEST(PanasonicProtocolTest, InvalidNbitsSendsNothing) {
|
||||
PanasonicData data = FRAME_48;
|
||||
data.nbits = 40;
|
||||
EXPECT_TRUE(encode(data).empty());
|
||||
}
|
||||
|
||||
TEST(PanasonicProtocolTest, EqualityIgnoresCarrier) {
|
||||
PanasonicData other_carrier = FRAME_56;
|
||||
other_carrier.carrier_frequency = 36000;
|
||||
EXPECT_EQ(FRAME_56, other_carrier);
|
||||
PanasonicData other_address_2 = FRAME_56;
|
||||
other_address_2.address_2 = 0x13;
|
||||
EXPECT_NE(FRAME_56, other_address_2);
|
||||
EXPECT_NE(FRAME_48, FRAME_56);
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_base::testing
|
||||
@@ -116,8 +116,8 @@ on_onkyori:
|
||||
on_panasonic:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "on_panasonic: %u %lu"
|
||||
args: ["x.address", "long(x.command)"]
|
||||
format: "on_panasonic: %u %u %u %lu"
|
||||
args: ["x.nbits", "x.address", "x.address_2", "long(x.command)"]
|
||||
on_pioneer:
|
||||
then:
|
||||
- logger.log:
|
||||
|
||||
@@ -21,6 +21,13 @@ binary_sensor:
|
||||
address: 0x00
|
||||
command: 0x0D
|
||||
extension: 0x03
|
||||
- platform: remote_receiver
|
||||
name: Panasonic 56 bit Remote Input
|
||||
panasonic:
|
||||
nbits: 56
|
||||
address: 0x4004
|
||||
address_2: 0x12
|
||||
command: 0x100BCBD
|
||||
- platform: remote_receiver
|
||||
name: Hob2Hood Light On
|
||||
hob2hood:
|
||||
|
||||
@@ -103,6 +103,15 @@ button:
|
||||
remote_transmitter.transmit_panasonic:
|
||||
address: 0x4004
|
||||
command: 0x1000BCD
|
||||
- platform: template
|
||||
name: Panasonic 56 bit
|
||||
on_press:
|
||||
remote_transmitter.transmit_panasonic:
|
||||
nbits: 56
|
||||
address: 0x4004
|
||||
address_2: 0x12
|
||||
command: !lambda return 0x1000BCD;
|
||||
carrier_frequency: 36kHz
|
||||
- platform: template
|
||||
name: Pioneer
|
||||
on_press:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""A Panasonic address_2 is only sent with a 56-bit code."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
from esphome.components.remote_base import (
|
||||
CONF_ADDRESS_2,
|
||||
PANASONIC_SCHEMA,
|
||||
_panasonic_address_2_needs_56_bits,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_NBITS
|
||||
from esphome.core import Lambda
|
||||
|
||||
BASE = {"address": 0x4004, "command": 0x0100BCBD}
|
||||
|
||||
|
||||
def test_address_2_needs_56_bits() -> None:
|
||||
config = PANASONIC_SCHEMA({**BASE, CONF_NBITS: 56, CONF_ADDRESS_2: 0x12})
|
||||
assert _panasonic_address_2_needs_56_bits(config)[CONF_ADDRESS_2] == 0x12
|
||||
with pytest.raises(cv.Invalid, match="only sent with nbits: 56"):
|
||||
_panasonic_address_2_needs_56_bits(
|
||||
PANASONIC_SCHEMA({**BASE, CONF_NBITS: 48, CONF_ADDRESS_2: 0x12})
|
||||
)
|
||||
|
||||
|
||||
def test_zero_address_2_is_fine_with_48_bits() -> None:
|
||||
config = PANASONIC_SCHEMA({**BASE, CONF_ADDRESS_2: 0})
|
||||
assert _panasonic_address_2_needs_56_bits(config)[CONF_NBITS] == 48
|
||||
|
||||
|
||||
def test_lambda_address_2_is_not_checked_here() -> None:
|
||||
config = {**BASE, CONF_NBITS: 48, CONF_ADDRESS_2: Lambda("return 0x12;")}
|
||||
assert isinstance(
|
||||
_panasonic_address_2_needs_56_bits(config)[CONF_ADDRESS_2], Lambda
|
||||
)
|
||||
|
||||
|
||||
def test_transmit_action_checks_address_2() -> None:
|
||||
schema = ACTION_REGISTRY["remote_transmitter.transmit_panasonic"].raw_schema
|
||||
assert schema({**BASE, CONF_NBITS: 56, CONF_ADDRESS_2: 0x12})[CONF_NBITS] == 56
|
||||
assert isinstance(
|
||||
schema({**BASE, CONF_ADDRESS_2: Lambda("return 0x12;")})[CONF_ADDRESS_2],
|
||||
Lambda,
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="only sent with nbits: 56"):
|
||||
schema({**BASE, CONF_ADDRESS_2: 0x12})
|
||||
Reference in New Issue
Block a user