// HaEntity/HaSensor (docs/DESIGN.md, D23): discovery component, topics, // throttling (SEN-14), availability without a value (SEN-15). #include #include #include #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 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{"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"