[remote_base] Add AEG Hob2Hood Infrared Protocol (#4687)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Mat931
2026-09-26 00:42:21 +01:00
committed by GitHub
co-authored by pre-commit-ci-lite[bot] J. Nick Koston
parent bfe1f7bd17
commit cfb7d8089e
8 changed files with 289 additions and 1 deletions
+14
View File
@@ -0,0 +1,14 @@
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
from esphome.components.remote_base import request_protocol
async def to_code_testing(config: ConfigType) -> None:
# Protocol sources are compiled only behind their define; keep the ones under test.
request_protocol("hob2hood")
manifest.to_code = to_code_testing
# AUTO_LOAD is not resolved by the unit test build.
manifest.dependencies = manifest.dependencies + ["binary_sensor"]
@@ -0,0 +1,83 @@
#include <gtest/gtest.h>
#include "esphome/components/remote_base/hob2hood_protocol.h"
namespace esphome::remote_base::testing {
namespace {
constexpr std::array<Hob2HoodCommand, 7> ALL_COMMANDS = {
HOB2HOOD_COMMAND_LIGHT_OFF, HOB2HOOD_COMMAND_LIGHT_ON, HOB2HOOD_COMMAND_FAN_OFF, HOB2HOOD_COMMAND_FAN_LOW,
HOB2HOOD_COMMAND_FAN_MEDIUM, HOB2HOOD_COMMAND_FAN_HIGH, HOB2HOOD_COMMAND_FAN_MAX,
};
RawTimings encode(Hob2HoodCommand command) {
RemoteTransmitData data;
Hob2HoodProtocol().encode(&data, Hob2HoodData{command});
return data.get_data();
}
optional<Hob2HoodData> decode(const RawTimings &timings, uint32_t tolerance = 25,
ToleranceMode mode = TOLERANCE_MODE_PERCENTAGE) {
return Hob2HoodProtocol().decode(RemoteReceiveData(timings, tolerance, mode));
}
void expect_decodes_to(const RawTimings &timings, Hob2HoodCommand command, uint32_t tolerance = 25,
ToleranceMode mode = TOLERANCE_MODE_PERCENTAGE) {
auto decoded = decode(timings, tolerance, mode);
ASSERT_TRUE(decoded.has_value()) << "command 0x" << std::hex << int(command);
// clang-tidy's unchecked-optional-access models neither gtest's ASSERT_TRUE nor value() as a check
if (decoded.has_value()) {
EXPECT_EQ(decoded->command, command);
}
}
} // namespace
// light_on is 0xd2: the bits 0 11010010 11010011 11010100 form 17 runs. A run of n zero bits is a mark of
// n * 700 + 300 us; a run of n one bits is a space of n * 700 - 200 us.
TEST(Hob2HoodProtocolTest, EncodesTheDocumentedTimings) {
const RawTimings expected = {1000, -1200, 1000, -500, 1700, -500, 1000, -1200, 1000,
-500, 1700, -2600, 1000, -500, 1000, -500, 1700};
EXPECT_EQ(encode(HOB2HOOD_COMMAND_LIGHT_ON), expected);
}
TEST(Hob2HoodProtocolTest, LongestFrameFitsTheReservedLength) {
for (auto command : ALL_COMMANDS) {
EXPECT_LE(encode(command).size(), 18u) << "command 0x" << std::hex << int(command);
}
EXPECT_EQ(encode(HOB2HOOD_COMMAND_LIGHT_OFF).size(), 18u);
}
TEST(Hob2HoodProtocolTest, RoundTripsEveryCommand) {
for (auto command : ALL_COMMANDS) {
expect_decodes_to(encode(command), command);
}
}
// A receiver never captures the trailing space, so a frame that ends in one must still decode without it.
TEST(Hob2HoodProtocolTest, DecodesWithoutTheTrailingSpace) {
for (auto command : ALL_COMMANDS) {
auto timings = encode(command);
if (timings.back() < 0)
timings.pop_back();
expect_decodes_to(timings, command);
}
}
// Real receivers shorten marks and lengthen spaces by a couple of hundred microseconds, which is why the
// documentation recommends a 350us tolerance.
TEST(Hob2HoodProtocolTest, DecodesSkewedTimingsWithinTheRecommendedTolerance) {
auto timings = encode(HOB2HOOD_COMMAND_FAN_HIGH);
for (auto &t : timings) {
t += t > 0 ? -240 : -230;
}
expect_decodes_to(timings, HOB2HOOD_COMMAND_FAN_HIGH, 350, TOLERANCE_MODE_TIME);
}
TEST(Hob2HoodProtocolTest, RejectsAForeignFrame) {
const RawTimings nec_like = {9000, -4500, 560, -560, 560, -1690, 560, -560, 560};
EXPECT_FALSE(decode(nec_like).has_value());
EXPECT_FALSE(decode({}).has_value());
}
} // namespace esphome::remote_base::testing
@@ -172,3 +172,8 @@ on_toto:
- logger.log:
format: "on_toto: %u %u %u"
args: ["x.rc_code_1", "x.rc_code_2", "x.command"]
on_hob2hood:
then:
- logger.log:
format: "on_hob2hood: %u"
args: ["x.command"]
@@ -15,3 +15,7 @@ binary_sensor:
panasonic:
address: 0x4004
command: 0x100BCBD
- platform: remote_receiver
name: Hob2Hood Light On
hob2hood:
command: light_on
@@ -278,3 +278,8 @@ button:
- remote_transmitter.digital_write:
transmitter_id: xmitr
value: !lambda return id(remote_transmitter_test_number).state > 127;
- platform: template
name: Hob2Hood
on_press:
- remote_transmitter.transmit_hob2hood:
command: light_on