mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
431 lines
18 KiB
C++
431 lines
18 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <chrono>
|
|
#include <thread>
|
|
|
|
#include "esphome/components/hoermann_hcp/hoermann_hcp.h"
|
|
|
|
namespace esphome::hoermann_hcp {
|
|
|
|
using modbus::RegisterValues;
|
|
|
|
namespace {
|
|
|
|
// Register block addresses the Hoermann bus controller polls (see hoermann_hcp.cpp).
|
|
constexpr uint16_t COMMAND_REG = 0x9C41;
|
|
constexpr uint16_t STATE_REG = 0x9CB9;
|
|
constexpr uint16_t BROADCAST_REG = 0x9D31;
|
|
|
|
// The tests shorten the key-press delay to zero, so the release only needs the millis() clock to tick on.
|
|
constexpr auto KEY_PRESS_ELAPSED = std::chrono::milliseconds(2);
|
|
|
|
RegisterValues make_registers(std::initializer_list<uint16_t> values) {
|
|
RegisterValues registers;
|
|
for (uint16_t value : values)
|
|
registers.push_back(value);
|
|
return registers;
|
|
}
|
|
|
|
// The device only accepts commands once the bus controller has actually talked to it.
|
|
void connect(HoermannHcp &door) { door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000})); }
|
|
|
|
// Runs one command poll (write 2 / read 8) and returns the register carrying the key-press value.
|
|
uint16_t poll_command(HoermannHcp &door) {
|
|
door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000}));
|
|
RegisterValues response;
|
|
door.on_read_holding_registers(STATE_REG, 8, response);
|
|
EXPECT_EQ(response.size(), 8u);
|
|
return response.size() == 8u ? response[2] : 0xFFFF;
|
|
}
|
|
|
|
// Exposes the internal timings and the connection bookkeeping, so no test has to wait out a real delay.
|
|
class TestableHoermannHcp : public HoermannHcp {
|
|
public:
|
|
TestableHoermannHcp() { this->key_press_delay_ms_ = 0; }
|
|
|
|
using HoermannHcp::connection_timeout_ms_;
|
|
using HoermannHcp::set_valid_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// An empty poll (write 2 / read 2) answers with the fixed status word 0x0004.
|
|
TEST(HoermannHcpReadWrite, EmptyPollReturnsStatusWord) {
|
|
HoermannHcp door;
|
|
EXPECT_FALSE(door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000})).has_value());
|
|
RegisterValues response;
|
|
auto status = door.on_read_holding_registers(STATE_REG, 2, response);
|
|
EXPECT_FALSE(status.has_value());
|
|
ASSERT_EQ(response.size(), 2u);
|
|
EXPECT_EQ(response[0], 0x0004);
|
|
EXPECT_EQ(response[1], 0x0000);
|
|
}
|
|
|
|
// A bus scan (write 3 / read 5) answers with the fixed device identification block.
|
|
TEST(HoermannHcpReadWrite, BusScanReturnsIdentification) {
|
|
HoermannHcp door;
|
|
EXPECT_FALSE(door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000, 0x0000})).has_value());
|
|
RegisterValues response;
|
|
auto status = door.on_read_holding_registers(STATE_REG, 5, response);
|
|
EXPECT_FALSE(status.has_value());
|
|
ASSERT_EQ(response.size(), 5u);
|
|
EXPECT_EQ(response[1], 0x0005);
|
|
EXPECT_EQ(response[2], 0x0430);
|
|
EXPECT_EQ(response[3], 0x10ff);
|
|
EXPECT_EQ(response[4], 0xa845);
|
|
}
|
|
|
|
// Without a queued command, the command poll (write 2 / read 8) reports idle and no key press.
|
|
TEST(HoermannHcpReadWrite, IdleCommandPollHasNoCommand) {
|
|
HoermannHcp door;
|
|
EXPECT_FALSE(door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000})).has_value());
|
|
RegisterValues response;
|
|
auto status = door.on_read_holding_registers(STATE_REG, 8, response);
|
|
EXPECT_FALSE(status.has_value());
|
|
ASSERT_EQ(response.size(), 8u);
|
|
EXPECT_EQ(response[1], 0x0001);
|
|
EXPECT_EQ(response[2], 0x0000);
|
|
EXPECT_EQ(response[3], 0x0000);
|
|
}
|
|
|
|
// A queued control command is injected into the next command poll as a simulated key press.
|
|
TEST(HoermannHcpReadWrite, QueuedCommandIsInjectedIntoPoll) {
|
|
HoermannHcp door;
|
|
connect(door);
|
|
door.open_door();
|
|
EXPECT_FALSE(door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000})).has_value());
|
|
RegisterValues response;
|
|
auto status = door.on_read_holding_registers(STATE_REG, 8, response);
|
|
EXPECT_FALSE(status.has_value());
|
|
ASSERT_EQ(response.size(), 8u);
|
|
EXPECT_EQ(response[2], 0x0210); // COMMAND_OPEN "key pressed" value
|
|
EXPECT_EQ(response[3], 0x0000);
|
|
}
|
|
|
|
// A read of any other block is an addressing error rather than a successful all-zero reply.
|
|
TEST(HoermannHcpReadWrite, UnknownAddressIsRejected) {
|
|
HoermannHcp door;
|
|
RegisterValues response;
|
|
EXPECT_EQ(door.on_read_holding_registers(0x1234, 2, response), modbus::ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
|
EXPECT_EQ(door.on_write_registers(0x1234, make_registers({0x0000})), modbus::ExceptionCode::ILLEGAL_DATA_ADDRESS);
|
|
}
|
|
|
|
// A command is held for the key-press duration, then released, and only then can the next one be queued.
|
|
TEST(HoermannHcpReadWrite, CommandIsReleasedAfterTheKeyPressDelay) {
|
|
TestableHoermannHcp door;
|
|
connect(door);
|
|
door.open_door();
|
|
EXPECT_EQ(poll_command(door), 0x0210); // COMMAND_OPEN pressed
|
|
// Refused while one is pending: were it accepted, the release below would carry COMMAND_CLOSE's 0x0120.
|
|
door.close_door();
|
|
|
|
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
|
|
EXPECT_EQ(poll_command(door), 0x0110); // COMMAND_OPEN released
|
|
// With the command gone, the next one is accepted again.
|
|
door.close_door();
|
|
EXPECT_EQ(poll_command(door), 0x0220); // COMMAND_CLOSE pressed
|
|
}
|
|
|
|
// Commands issued while the bus controller is absent are dropped instead of firing when it returns.
|
|
TEST(HoermannHcpReadWrite, CommandIsDroppedWhileDisconnected) {
|
|
HoermannHcp door;
|
|
door.open_door();
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
}
|
|
|
|
// Losing the controller must drop a command it never fetched, otherwise it blocks every later command
|
|
// and fires unasked once the bus comes back.
|
|
TEST(HoermannHcpReadWrite, ConnectionLossDropsThePendingCommand) {
|
|
TestableHoermannHcp door;
|
|
connect(door);
|
|
door.open_door();
|
|
ASSERT_TRUE(door.is_valid());
|
|
|
|
door.set_valid_(false);
|
|
EXPECT_FALSE(door.is_valid());
|
|
|
|
// The reconnecting poll must not replay the dropped command.
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
// And the slot is free, so a new command is accepted.
|
|
door.close_door();
|
|
EXPECT_EQ(poll_command(door), 0x0220);
|
|
}
|
|
|
|
// The connection is dropped by update() once the controller stops polling, which is what releases a
|
|
// command it never fetched in the field.
|
|
TEST(HoermannHcpReadWrite, PollingTimeoutDropsTheConnection) {
|
|
TestableHoermannHcp door;
|
|
// Wide enough that a stall cannot expire the connection before the check below runs.
|
|
door.connection_timeout_ms_ = 10000;
|
|
connect(door);
|
|
door.open_door();
|
|
|
|
// Still inside the window: the controller counts as present.
|
|
door.update();
|
|
ASSERT_TRUE(door.is_valid());
|
|
|
|
// Shrink the window so the expiry needs only a short sleep; overshooting it only makes it surer.
|
|
door.connection_timeout_ms_ = 20;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
|
door.update();
|
|
EXPECT_FALSE(door.is_valid());
|
|
// The pending command went with the connection instead of firing on the reconnecting poll.
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
}
|
|
|
|
// Status broadcasts alone keep the connection alive, so a command the controller never fetches has to
|
|
// expire on its own; otherwise it blocks every later command until the bus goes quiet entirely.
|
|
TEST(HoermannHcpReadWrite, UnfetchedCommandExpiresWhileConnected) {
|
|
TestableHoermannHcp door;
|
|
door.connection_timeout_ms_ = 200;
|
|
connect(door);
|
|
door.open_door();
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(220));
|
|
// A status broadcast refreshes the connection without ever fetching the command.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0064, 0x0100}));
|
|
door.update();
|
|
ASSERT_TRUE(door.is_valid());
|
|
|
|
// With the stale command gone, the door accepts commands again.
|
|
door.close_door();
|
|
EXPECT_EQ(poll_command(door), 0x0220);
|
|
}
|
|
|
|
// The 0x17 read half echoes the message counter and command byte written to COMMAND_REG, packed
|
|
// differently per block length.
|
|
TEST(HoermannHcpReadWrite, CommandRegisterIsEchoedBack) {
|
|
HoermannHcp door;
|
|
// Counter 0x34 in the high byte, command 0x07 in the low byte.
|
|
door.on_write_registers(COMMAND_REG, make_registers({0x3407, 0x0000}));
|
|
|
|
RegisterValues command_poll;
|
|
door.on_read_holding_registers(STATE_REG, 8, command_poll);
|
|
ASSERT_EQ(command_poll.size(), 8u);
|
|
EXPECT_EQ(command_poll[0], 0x3400); // counter alone
|
|
EXPECT_EQ(command_poll[1], 0x0701); // command in the high byte, status 0x01 in the low
|
|
|
|
RegisterValues empty_poll;
|
|
door.on_read_holding_registers(STATE_REG, 2, empty_poll);
|
|
ASSERT_EQ(empty_poll.size(), 2u);
|
|
EXPECT_EQ(empty_poll[0], 0x3404); // status 0x04 shares the register with the counter here
|
|
EXPECT_EQ(empty_poll[1], 0x0700); // command alone
|
|
|
|
RegisterValues scan;
|
|
door.on_read_holding_registers(STATE_REG, 5, scan);
|
|
ASSERT_EQ(scan.size(), 5u);
|
|
EXPECT_EQ(scan[0], 0x3400);
|
|
EXPECT_EQ(scan[1], 0x0705);
|
|
}
|
|
|
|
// A status broadcast (function code 0x10 to 0x9D31) updates the decoded door state and position.
|
|
TEST(HoermannHcpWrite, BroadcastUpdatesStateAndPosition) {
|
|
HoermannHcp door;
|
|
// registers[1] low byte = position (value / 200), registers[2] high byte = state (0x01 -> opening).
|
|
auto status = door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0064, 0x0100}));
|
|
EXPECT_FALSE(status.has_value());
|
|
EXPECT_EQ(door.get_door_state(), DoorState::OPENING);
|
|
EXPECT_FLOAT_EQ(door.get_current_position(), 0.5f);
|
|
}
|
|
|
|
// The first broadcast has to be decoded even when it carries the register's initial value, otherwise a
|
|
// door parked mid-travel at boot keeps the CLOSED default and reports itself fully closed.
|
|
TEST(HoermannHcpWrite, FirstBroadcastReportingAStopIsDecoded) {
|
|
HoermannHcp door;
|
|
auto status = door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0064, 0x0000}));
|
|
EXPECT_FALSE(status.has_value());
|
|
EXPECT_EQ(door.get_door_state(), DoorState::STOPPED);
|
|
EXPECT_FLOAT_EQ(door.get_current_position(), 0.5f);
|
|
}
|
|
|
|
// The vent position is reported as state 0x00 with low byte 0x61, so a change confined to the low byte of
|
|
// the state register still has to be decoded.
|
|
TEST(HoermannHcpWrite, VentIsDecodedFromTheStateLowByte) {
|
|
HoermannHcp door;
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0000, 0x0100}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::OPENING);
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0000, 0x0000}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::STOPPED);
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0000, 0x0061}));
|
|
EXPECT_EQ(door.get_door_state(), DoorState::VENT);
|
|
}
|
|
|
|
// A door parking a count short of its end stop must still report exactly closed or open, because
|
|
// Cover::is_fully_closed() compares against 0.0 exactly.
|
|
TEST(HoermannHcpWrite, EndStopsReportExactPositions) {
|
|
HoermannHcp door;
|
|
// Position register 1 of 200 while the door reports itself closed.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0001, 0x4000}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::CLOSED);
|
|
EXPECT_FLOAT_EQ(door.get_current_position(), 0.0f);
|
|
|
|
// Position register 199 of 200 while the door reports itself open.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x00C7, 0x2000}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::OPEN);
|
|
EXPECT_FLOAT_EQ(door.get_current_position(), 1.0f);
|
|
|
|
// Away from the end stops the raw count is reported as-is.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0064, 0x0100}));
|
|
EXPECT_FLOAT_EQ(door.get_current_position(), 0.5f);
|
|
}
|
|
|
|
// A position request below the lower snap threshold becomes a plain close command.
|
|
TEST(HoermannHcpPosition, NearlyClosedTargetClosesTheDoor) {
|
|
HoermannHcp door;
|
|
connect(door);
|
|
door.set_position(0.02f);
|
|
RegisterValues response;
|
|
door.on_read_holding_registers(STATE_REG, 8, response);
|
|
ASSERT_EQ(response.size(), 8u);
|
|
EXPECT_EQ(response[2], 0x0220); // COMMAND_CLOSE "key pressed" value
|
|
}
|
|
|
|
// A half-open target starts the door moving towards the requested position.
|
|
TEST(HoermannHcpPosition, HalfOpenTargetOpensTheDoor) {
|
|
HoermannHcp door; // starts out fully closed
|
|
connect(door);
|
|
door.set_position(0.5f);
|
|
RegisterValues response;
|
|
door.on_read_holding_registers(STATE_REG, 8, response);
|
|
ASSERT_EQ(response.size(), 8u);
|
|
EXPECT_EQ(response[2], 0x0210); // COMMAND_OPEN "key pressed" value
|
|
}
|
|
|
|
// The door has no notion of a target, so it is stopped with an impulse once it travels past the request.
|
|
TEST(HoermannHcpPosition, TargetPositionStopsTheDoor) {
|
|
TestableHoermannHcp door;
|
|
connect(door);
|
|
door.set_position(0.5f);
|
|
EXPECT_EQ(poll_command(door), 0x0210); // COMMAND_OPEN pressed
|
|
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
|
|
EXPECT_EQ(poll_command(door), 0x0110); // COMMAND_OPEN released
|
|
|
|
// Position 20/200 = 0.1 while opening: short of the target, so the door keeps going.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0014, 0x0100}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::OPENING);
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
|
|
// Position 120/200 = 0.6 is past the target, so the door is stopped.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0078, 0x0100}));
|
|
EXPECT_EQ(poll_command(door), 0x0240); // COMMAND_IMPULSE pressed
|
|
}
|
|
|
|
// An impulse restarts a stopped door, so a frame reporting the stop and the target crossing at once
|
|
// must be read as "already stopped" rather than "still opening".
|
|
TEST(HoermannHcpPosition, StopReportedWithTheCrossingSendsNoImpulse) {
|
|
TestableHoermannHcp door;
|
|
connect(door);
|
|
door.set_position(0.5f);
|
|
EXPECT_EQ(poll_command(door), 0x0210);
|
|
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
|
|
EXPECT_EQ(poll_command(door), 0x0110);
|
|
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0014, 0x0100}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::OPENING);
|
|
|
|
// Same frame: position 0.6 (past the target) and state 0x20 -> the door has reached its open end stop.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0078, 0x2000}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::OPEN);
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
}
|
|
|
|
// A target the door never reaches is dropped once it comes to rest, so a later move is not cut short.
|
|
TEST(HoermannHcpPosition, TargetIsDroppedWhenTheDoorStopsShort) {
|
|
TestableHoermannHcp door;
|
|
connect(door);
|
|
door.set_position(0.5f);
|
|
EXPECT_EQ(poll_command(door), 0x0210);
|
|
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
|
|
EXPECT_EQ(poll_command(door), 0x0110);
|
|
|
|
// The door is stopped at 0.3 by a wall button, short of the requested 0.5.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0014, 0x0100}));
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x003C, 0x0000}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::STOPPED);
|
|
|
|
// A later manual open must run freely instead of being stopped at the abandoned target.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0050, 0x0100}));
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0078, 0x0100}));
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
}
|
|
|
|
// A target armed while the door is still travelling the other way must not be judged by that old direction,
|
|
// otherwise the very next position it reports counts as reached and stops the door where it stands.
|
|
TEST(HoermannHcpPosition, TargetArmedWhileMovingTheOtherWayWaitsForTheTurnaround) {
|
|
TestableHoermannHcp door;
|
|
connect(door);
|
|
// The door is closing, passing 60/200 = 0.3.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x003C, 0x0200}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::CLOSING);
|
|
|
|
door.set_position(0.5f);
|
|
EXPECT_EQ(poll_command(door), 0x0210); // COMMAND_OPEN pressed
|
|
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
|
|
EXPECT_EQ(poll_command(door), 0x0110); // COMMAND_OPEN released
|
|
|
|
// Still closing at 58/200 = 0.29: below the target, but not on the way to it.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x003A, 0x0200}));
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
|
|
// Now opening at 62/200 = 0.31, still short of the target.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x003E, 0x0100}));
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
|
|
// Past the target at 110/200 = 0.55, so the door is stopped.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x006E, 0x0100}));
|
|
EXPECT_EQ(poll_command(door), 0x0240); // COMMAND_IMPULSE pressed
|
|
}
|
|
|
|
// A motor turning around can report a momentary stop; dropping the target there would let the door run on
|
|
// to the end stop that the reversing command asked for.
|
|
TEST(HoermannHcpPosition, MomentaryStopWhileTurningAroundKeepsTheTarget) {
|
|
TestableHoermannHcp door;
|
|
connect(door);
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x003C, 0x0200}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::CLOSING);
|
|
|
|
door.set_position(0.5f);
|
|
EXPECT_EQ(poll_command(door), 0x0210);
|
|
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
|
|
EXPECT_EQ(poll_command(door), 0x0110);
|
|
|
|
// The stop reported on the way from closing to opening.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x003C, 0x0000}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::STOPPED);
|
|
|
|
// The door then opens and still has to be stopped at the requested position.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x003E, 0x0100}));
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x006E, 0x0100}));
|
|
EXPECT_EQ(poll_command(door), 0x0240);
|
|
}
|
|
|
|
// A door that never turns around has to lose the target as well, otherwise it would cut a later move short.
|
|
TEST(HoermannHcpPosition, TargetIsDroppedWhenTheDoorNeverTurnsAround) {
|
|
TestableHoermannHcp door;
|
|
door.connection_timeout_ms_ = 200;
|
|
connect(door);
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x003C, 0x0200}));
|
|
ASSERT_EQ(door.get_door_state(), DoorState::CLOSING);
|
|
|
|
door.set_position(0.5f);
|
|
EXPECT_EQ(poll_command(door), 0x0210);
|
|
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
|
|
EXPECT_EQ(poll_command(door), 0x0110);
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(220));
|
|
// The door ignored the command and closed all the way. Its broadcast keeps the connection alive, so the
|
|
// target is the only thing that may expire here.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0000, 0x4000}));
|
|
door.update();
|
|
ASSERT_TRUE(door.is_valid());
|
|
ASSERT_EQ(door.get_door_state(), DoorState::CLOSED);
|
|
|
|
// A later manual open must run freely instead of being stopped at the abandoned target.
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x003E, 0x0100}));
|
|
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x006E, 0x0100}));
|
|
EXPECT_EQ(poll_command(door), 0x0000);
|
|
}
|
|
|
|
} // namespace esphome::hoermann_hcp
|