HA entity, sensor and device classes (iteration 2, step 3)
HaEntity describes an entity for device-based discovery, throttles its state (SEN-14) and reports availability (SEN-15); HaSensor is the `sensor` type. HaDevice publishes discovery, availability and states after each connect and after HA's birth message, provides the last will and removes itself from HA. Unit tests with a fake MQTT client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// HaEntity/HaSensor (docs/DESIGN.md, D23): discovery component, topics,
|
||||
// throttling (SEN-14), availability without a value (SEN-15).
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "ha/HaSensor.h"
|
||||
|
||||
namespace {
|
||||
|
||||
HaEntity::Description uptime(int throttleMs = 1000) {
|
||||
return {.id = QStringLiteral("uptime"),
|
||||
.name = QStringLiteral("Uptime"),
|
||||
.deviceClass = QStringLiteral("duration"),
|
||||
.unit = QStringLiteral("s"),
|
||||
.category = QStringLiteral("diagnostic"),
|
||||
.throttleMs = throttleMs};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class TestHaEntity : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void discoveryComponent() {
|
||||
const HaSensor sensor(uptime());
|
||||
const QJsonObject c = sensor.discoveryComponent(QStringLiteral("fa_dev"));
|
||||
QCOMPARE(c.value(QStringLiteral("p")).toString(), QStringLiteral("sensor"));
|
||||
QCOMPARE(c.value(QStringLiteral("name")).toString(),
|
||||
QStringLiteral("Uptime"));
|
||||
QCOMPARE(c.value(QStringLiteral("uniq_id")).toString(),
|
||||
QStringLiteral("fa_dev_uptime"));
|
||||
QCOMPARE(c.value(QStringLiteral("dev_cla")).toString(),
|
||||
QStringLiteral("duration"));
|
||||
QCOMPARE(c.value(QStringLiteral("unit_of_meas")).toString(),
|
||||
QStringLiteral("s"));
|
||||
QCOMPARE(c.value(QStringLiteral("ent_cat")).toString(),
|
||||
QStringLiteral("diagnostic"));
|
||||
QCOMPARE(c.value(QStringLiteral("stat_t")).toString(),
|
||||
QStringLiteral("~/uptime/state"));
|
||||
const QJsonArray availability = c.value(QStringLiteral("avty")).toArray();
|
||||
QCOMPARE(availability.size(), 2);
|
||||
QCOMPARE(availability[0].toObject().value(QStringLiteral("t")).toString(),
|
||||
QStringLiteral("~/availability"));
|
||||
QCOMPARE(availability[1].toObject().value(QStringLiteral("t")).toString(),
|
||||
QStringLiteral("~/uptime/availability"));
|
||||
QCOMPARE(c.value(QStringLiteral("avty_mode")).toString(),
|
||||
QStringLiteral("all"));
|
||||
}
|
||||
|
||||
void optionalKeysOnlyWhenSet() {
|
||||
const HaSensor sensor(
|
||||
{.id = QStringLiteral("x"), .name = QStringLiteral("X")});
|
||||
const QJsonObject c = sensor.discoveryComponent(QStringLiteral("fa_dev"));
|
||||
QVERIFY(!c.contains(QStringLiteral("dev_cla")));
|
||||
QVERIFY(!c.contains(QStringLiteral("unit_of_meas")));
|
||||
QVERIFY(!c.contains(QStringLiteral("ent_cat")));
|
||||
}
|
||||
|
||||
void topics() {
|
||||
const HaSensor sensor(uptime());
|
||||
QCOMPARE(sensor.stateTopic(), QStringLiteral("uptime/state"));
|
||||
QCOMPARE(sensor.availabilityTopic(), QStringLiteral("uptime/availability"));
|
||||
}
|
||||
|
||||
// SEN-15: no value → unavailable; the first value → state, then available.
|
||||
void firstValueMakesAvailable() {
|
||||
HaSensor sensor(uptime());
|
||||
QVERIFY(!sensor.hasValue());
|
||||
QVERIFY(sensor.state().isEmpty());
|
||||
QList<QString> events;
|
||||
connect(&sensor, &HaEntity::stateChanged, this,
|
||||
[&](const QByteArray& s) { events.append("state " + s); });
|
||||
connect(&sensor, &HaEntity::availabilityChanged, this,
|
||||
[&](bool a) { events.append(a ? "available" : "unavailable"); });
|
||||
sensor.setValue(42);
|
||||
QCOMPARE(events, (QList<QString>{"state 42", "available"}));
|
||||
QVERIFY(sensor.hasValue());
|
||||
QCOMPARE(sensor.state(), QByteArray("42"));
|
||||
}
|
||||
|
||||
void clearValueMakesUnavailable() {
|
||||
HaSensor sensor(uptime());
|
||||
sensor.setValue(1);
|
||||
QSignalSpy availability(&sensor, &HaEntity::availabilityChanged);
|
||||
QSignalSpy states(&sensor, &HaEntity::stateChanged);
|
||||
sensor.clearValue();
|
||||
QCOMPARE(availability.size(), 1);
|
||||
QCOMPARE(availability.first().first().toBool(), false);
|
||||
QVERIFY(!sensor.hasValue());
|
||||
sensor.clearValue(); // no change, no signal
|
||||
QCOMPARE(availability.size(), 1);
|
||||
// The same value as before the gap is a new first value.
|
||||
sensor.setValue(1);
|
||||
QCOMPARE(states.size(), 1);
|
||||
QCOMPARE(availability.size(), 2);
|
||||
QCOMPARE(availability.last().first().toBool(), true);
|
||||
}
|
||||
|
||||
void unchangedValueNotEmitted() {
|
||||
HaSensor sensor(uptime(0));
|
||||
QSignalSpy states(&sensor, &HaEntity::stateChanged);
|
||||
sensor.setValue(5);
|
||||
sensor.setValue(5);
|
||||
QCOMPARE(states.size(), 1);
|
||||
}
|
||||
|
||||
// SEN-14: changes within the interval → one state, the newest, at its end.
|
||||
void throttlesChanges() {
|
||||
HaSensor sensor(uptime(300));
|
||||
QSignalSpy states(&sensor, &HaEntity::stateChanged);
|
||||
sensor.setValue(1);
|
||||
QCOMPARE(states.size(), 1);
|
||||
sensor.setValue(2);
|
||||
sensor.setValue(3);
|
||||
QCOMPARE(states.size(), 1);
|
||||
QTest::qWait(100);
|
||||
QCOMPARE(states.size(), 1);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(states.size(), 2, 1000);
|
||||
QCOMPARE(states.last().first().toByteArray(), QByteArray("3"));
|
||||
// Nothing more pending.
|
||||
QTest::qWait(400);
|
||||
QCOMPARE(states.size(), 2);
|
||||
}
|
||||
|
||||
void changeAfterIntervalEmittedAtOnce() {
|
||||
HaSensor sensor(uptime(100));
|
||||
QSignalSpy states(&sensor, &HaEntity::stateChanged);
|
||||
sensor.setValue(1);
|
||||
QTest::qWait(150);
|
||||
sensor.setValue(2);
|
||||
QCOMPARE(states.size(), 2);
|
||||
}
|
||||
|
||||
// Back to the emitted value within the interval: nothing to publish.
|
||||
void revertWithinIntervalNotEmitted() {
|
||||
HaSensor sensor(uptime(200));
|
||||
QSignalSpy states(&sensor, &HaEntity::stateChanged);
|
||||
sensor.setValue(1);
|
||||
sensor.setValue(2);
|
||||
sensor.setValue(1);
|
||||
QTest::qWait(400);
|
||||
QCOMPARE(states.size(), 1);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestHaEntity)
|
||||
#include "tst_haentity.moc"
|
||||
Reference in New Issue
Block a user