[hoermann_hcp] Add connectivity binary sensor (#18189)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Josef Zweck
2026-08-11 08:09:33 -05:00
committed by GitHub
co-authored by J. Nick Koston
parent 04dd6b3a55
commit 069f40f653
5 changed files with 151 additions and 0 deletions
@@ -0,0 +1,36 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_CONNECTIVITY, ENTITY_CATEGORY_DIAGNOSTIC
from esphome.types import ConfigType
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp, hoermann_hcp_ns
DEPENDENCIES = ["hoermann_hcp"]
CONF_IS_CONNECTED = "is_connected"
HoermannHcpConnectedBinarySensor = hoermann_hcp_ns.class_(
"HoermannHcpConnectedBinarySensor", binary_sensor.BinarySensor, cg.Component
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_HOERMANN_HCP_ID): cv.use_id(HoermannHcp),
cv.Optional(CONF_IS_CONNECTED): binary_sensor.binary_sensor_schema(
HoermannHcpConnectedBinarySensor,
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.COMPONENT_SCHEMA),
}
),
cv.has_at_least_one_key(CONF_IS_CONNECTED),
)
async def to_code(config: ConfigType) -> None:
if (conf := config.get(CONF_IS_CONNECTED)) is not None:
parent = await cg.get_variable(config[CONF_HOERMANN_HCP_ID])
var = await binary_sensor.new_binary_sensor(conf, parent)
await cg.register_component(var, conf)
@@ -0,0 +1,18 @@
#include "hoermann_hcp_binary_sensor.h"
#include "esphome/core/log.h"
namespace esphome::hoermann_hcp {
static const char *const TAG = "hoermann_hcp.binary_sensor";
void HoermannHcpConnectedBinarySensor::setup() {
// Publishing unconditionally is deliberate: the base class dedupes, and filters need every input to drive
// their timers.
this->parent_->add_on_state_callback([this]() { this->publish_state(this->parent_->is_valid()); });
this->publish_initial_state(this->parent_->is_valid());
}
void HoermannHcpConnectedBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Hoermann HCP Connected", this); }
} // namespace esphome::hoermann_hcp
@@ -0,0 +1,20 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/component.h"
#include "../hoermann_hcp.h"
namespace esphome::hoermann_hcp {
class HoermannHcpConnectedBinarySensor : public binary_sensor::BinarySensor, public Component {
public:
explicit HoermannHcpConnectedBinarySensor(HoermannHcp *parent) : parent_(parent) {}
void setup() override;
void dump_config() override;
protected:
HoermannHcp *const parent_;
};
} // namespace esphome::hoermann_hcp
@@ -0,0 +1,72 @@
#include <gtest/gtest.h>
#include "esphome/components/hoermann_hcp/binary_sensor/hoermann_hcp_binary_sensor.h"
namespace esphome::hoermann_hcp {
using modbus::RegisterValues;
namespace {
constexpr uint16_t COMMAND_REG = 0x9C41;
constexpr uint16_t BROADCAST_REG = 0x9D31;
RegisterValues make_registers(std::initializer_list<uint16_t> values) {
RegisterValues registers;
for (uint16_t value : values)
registers.push_back(value);
return registers;
}
// Exposes the connection bookkeeping so a drop can be driven without waiting one out.
class TestableHoermannHcp : public HoermannHcp {
public:
using HoermannHcp::set_valid_;
};
} // namespace
// Nothing has been heard from the bus controller yet, so the sensor starts out seeded as disconnected.
TEST(HoermannHcpBinarySensorTest, StartsDisconnected) {
HoermannHcp door;
HoermannHcpConnectedBinarySensor sensor(&door);
sensor.setup();
EXPECT_TRUE(sensor.has_state());
EXPECT_FALSE(sensor.state);
}
// The connection flag follows the bus controller in both directions.
TEST(HoermannHcpBinarySensorTest, FollowsTheConnectionState) {
TestableHoermannHcp door;
HoermannHcpConnectedBinarySensor sensor(&door);
sensor.setup();
ASSERT_FALSE(sensor.state);
door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000}));
door.update();
EXPECT_TRUE(sensor.state);
door.set_valid_(false);
door.update();
EXPECT_FALSE(sensor.state);
}
// Any hub change re-runs the publish path, so an unchanged connection must not be reported twice.
TEST(HoermannHcpBinarySensorTest, UnchangedConnectionIsPublishedOnce) {
HoermannHcp door;
HoermannHcpConnectedBinarySensor sensor(&door);
sensor.setup();
int publishes = 0;
sensor.add_on_state_callback([&publishes](bool /*state*/) { publishes++; });
door.on_write_registers(COMMAND_REG, make_registers({0x0000, 0x0000}));
door.update();
ASSERT_EQ(publishes, 1);
// A status broadcast changes the door state without touching the connection.
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0064, 0x0100}));
door.update();
EXPECT_EQ(publishes, 1);
}
} // namespace esphome::hoermann_hcp
@@ -6,3 +6,8 @@ cover:
- platform: hoermann_hcp
name: Garage Door
device_class: garage
binary_sensor:
- platform: hoermann_hcp
is_connected:
name: Garage Connected