From c3d02f462ebc0b3e4a06f7f71e0ade9729da9b80 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 28 Sep 2026 22:20:34 +0000 Subject: [PATCH] Announce a doorbell with proactivelyReported and a scene with supportsDeactivation An interface without properties says beside its name what Alexa has to know of it; the library left both out. With a board and Alexa on 2026-09-28 the discovery of a doorbell was accepted (202), the endpoint was not listed, and its DoorbellPress was answered with 500 INTERNAL_SERVICE_EXCEPTION. DoorbellEventSource now always carries "proactivelyReported": true. SceneController carries "supportsDeactivation", false until the sketch calls setSupportsDeactivation(true); on another interface the call is refused and logged. The Scene example says that it can be undone. 148 host tests. Light: static RAM 30,616 B, flash 332,705 B (+140 B). Co-Authored-By: Claude Fable 5.1 --- examples/Doorbell/Doorbell.ino | 2 +- examples/README.md | 2 -- examples/Scene/Scene.ino | 6 +++--- keywords.txt | 1 + readme.md | 1 - src/AlexaCapability.cpp | 25 ++++++++++++++++++++++++- src/AlexaCapability.h | 5 +++++ src/AlexaInterfaces.cpp | 4 ++-- src/AlexaInterfaces.h | 4 +++- test/test_discovery/test_main.cpp | 25 ++++++++++++++++++++++--- test/test_examples/test_main.cpp | 8 ++++---- 11 files changed, 65 insertions(+), 18 deletions(-) diff --git a/examples/Doorbell/Doorbell.ino b/examples/Doorbell/Doorbell.ino index 42df252..a0ca189 100644 --- a/examples/Doorbell/Doorbell.ino +++ b/examples/Doorbell/Doorbell.ino @@ -76,7 +76,7 @@ void setup() // The name Alexa shows, and the id of the endpoint: every device of an account has its own doorbell = alexa.getDevice("Front Door Bell", "esp-doorbell"); doorbell->setDisplayCategory(DisplayCategory::DOORBELL); - doorbell->addCapability(AlexaInterfaces::DoorbellEventSource)->setProactivelyReported(true); + doorbell->addCapability(AlexaInterfaces::DoorbellEventSource); doorbell->addCapability(AlexaInterfaces::EndpointHealth); doorbell->onDirective(onDirective); } diff --git a/examples/README.md b/examples/README.md index 1844610..e036bb4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -105,8 +105,6 @@ not been tried. ## Limits -- Scene: the discovery object does not carry `supportsDeactivation`. The sketch answers `Deactivate`, but Alexa - may never send it. - Doorbell, ContactSensor: an event or a change while there is no session with the broker is not sent later. - Lock: the answer after the DeferredResponse is sent once. If the session is lost while the bolt moves, Alexa gets the state with its next ReportState. diff --git a/examples/Scene/Scene.ino b/examples/Scene/Scene.ino index 912bde3..e235322 100644 --- a/examples/Scene/Scene.ino +++ b/examples/Scene/Scene.ino @@ -4,8 +4,8 @@ // with DeactivationStarted. The scene of this sketch switches two outputs: the ceiling lamp off and the lamp // behind the screen on. // -// The discovery object of the library does not say that the scene can be deactivated (supportsDeactivation), so -// Alexa may never send Deactivate. The handler answers it all the same. +// setSupportsDeactivation(true) tells Alexa that the scene can be undone: "Alexa, turn off Movie Night" then +// arrives as Deactivate. #include #include #include @@ -93,7 +93,7 @@ void setup() // The name Alexa shows, and the id of the endpoint: every device of an account has its own AlexaDevice *scene = alexa.getDevice("Movie Night", "esp-scene"); scene->setDisplayCategory(DisplayCategory::SCENE_TRIGGER); - scene->addCapability(AlexaInterfaces::SceneController); + scene->addCapability(AlexaInterfaces::SceneController)->setSupportsDeactivation(true); scene->addCapability(AlexaInterfaces::EndpointHealth); scene->onDirective(onDirective); } diff --git a/keywords.txt b/keywords.txt index 6429578..27a23cd 100644 --- a/keywords.txt +++ b/keywords.txt @@ -104,6 +104,7 @@ addStateMapping KEYWORD2 addFriendlyAsset KEYWORD2 setConfiguration KEYWORD2 setNonControllable KEYWORD2 +setSupportsDeactivation KEYWORD2 isNonControllable KEYWORD2 getInstance KEYWORD2 getRow KEYWORD2 diff --git a/readme.md b/readme.md index 6ed488e..d416df1 100644 --- a/readme.md +++ b/readme.md @@ -685,7 +685,6 @@ What changes for a 1.x sketch that is compiled against 2.0: - Reports are published with QoS 0 and sent once. An event or a change while there is no session with the broker is not sent later; `send()` returns `false` and the sketch decides. - Tried with Alexa from this library: the six interfaces marked in [Interface Types](#interface-types). Not tried from this library: `ColorController`, `ThermostatController`, `LockController` and the deferred answer, `SceneController`, the ErrorResponse, the ChangeReports, `DoorbellPress` and every interface without an example. Voice commands have not been tried. - The examples are compiled, with 0 warnings, and what each announces in discovery is checked by the host tests. They have not run on a board and have not been built with the Arduino IDE. -- The discovery object of a scene does not carry `supportsDeactivation`, so Alexa may never send `Deactivate`. - The library keeps no state of the devices. What a device is set to is a variable of the sketch and starts from its initial value after a reset. - 28 of the interfaces of Alexa have a row. The others cannot be announced. - The Arduino IDE builds with the default limits. diff --git a/src/AlexaCapability.cpp b/src/AlexaCapability.cpp index 4d2c037..66a5c60 100644 --- a/src/AlexaCapability.cpp +++ b/src/AlexaCapability.cpp @@ -88,7 +88,7 @@ AlexaCapabilityName::AlexaCapabilityName(const AlexaInterfaceDesc &row, const ch } AlexaCapability::AlexaCapability(const AlexaInterfaceDesc &row, const char *instance) - : row(&row), retrievable(true), proactivelyReported(false), nonControllable(false), valid(true) + : row(&row), retrievable(true), proactivelyReported(false), nonControllable(false), deactivation(false), valid(true) { setInstance(instance); } @@ -185,6 +185,18 @@ AlexaCapability &AlexaCapability::setNonControllable(bool value) return *this; } +AlexaCapability &AlexaCapability::setSupportsDeactivation(bool value) +{ + if (!row->has(AIF_DEACTIVATION)) + { + const AlexaCapabilityName name(*row, instance); + ALEX2ESP_LOGE("%s: setSupportsDeactivation() ignored: only a SceneController can be deactivated", name.text); + return refused(); + } + deactivation = value; + return *this; +} + AlexaCapability &AlexaCapability::setConfiguration(AlexaConfigurationFiller fill, void *context) { fillConfiguration = fill; @@ -354,6 +366,17 @@ bool AlexaCapability::toJson(JsonArray capabilities, const char *endpointId) con properties[F("nonControllable")] = true; } } + // An interface without properties says beside its name what Alexa has to know of it. Without these Alexa + // accepts the discovery and leaves the endpoint out: a doorbell announced without proactivelyReported was not + // listed, and its DoorbellPress was answered with an error. + if (row->has(AIF_EVENTS)) + { + capability[F("proactivelyReported")] = true; + } + if (row->has(AIF_DEACTIVATION)) + { + capability[F("supportsDeactivation")] = static_cast(deactivation); + } writeSemantics(capability); if (instance != nullptr) diff --git a/src/AlexaCapability.h b/src/AlexaCapability.h index c2f3a16..26223c7 100644 --- a/src/AlexaCapability.h +++ b/src/AlexaCapability.h @@ -128,6 +128,10 @@ public: // true: Alexa reports the state and sends no directive that changes it AlexaCapability &setNonControllable(bool value); + // For a scene: true when it can be undone, and Alexa then sends Deactivate as well as Activate. A scene is + // announced with "supportsDeactivation": false until this is called. + AlexaCapability &setSupportsDeactivation(bool value); + AlexaCapability &setConfiguration(AlexaConfigurationFiller fill, void *context = nullptr); // Only for an interface that takes semantics: RangeController, ModeController, ToggleController @@ -200,6 +204,7 @@ private: bool retrievable : 1; bool proactivelyReported : 1; bool nonControllable : 1; + bool deactivation : 1; bool valid : 1; }; diff --git a/src/AlexaInterfaces.cpp b/src/AlexaInterfaces.cpp index dc60020..b03c315 100644 --- a/src/AlexaInterfaces.cpp +++ b/src/AlexaInterfaces.cpp @@ -103,10 +103,10 @@ ALEXA_ROW(ThermostatController, NS_THERMOSTAT, VERSION_3_2, PROPS_THERMOSTAT, 4, ALEXA_ROW(TemperatureSensor, NS_TEMPERATURE_SENSOR, VERSION_3, PROPS_TEMPERATURE_SENSOR, 1, AIF_NONE, TEMPERATURE_SENSOR); ALEXA_ROW(HumiditySensor, NS_HUMIDITY_SENSOR, VERSION_3, PROPS_HUMIDITY_SENSOR, 1, AIF_NONE, HUMIDITY_SENSOR); ALEXA_ROW(LockController, NS_LOCK, VERSION_3, PROPS_LOCK, 1, AIF_NONE, LOCK_CONTROLLER); -ALEXA_ROW(SceneController, NS_SCENE, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES, SCENE_CONTROLLER); +ALEXA_ROW(SceneController, NS_SCENE, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES | AIF_DEACTIVATION, SCENE_CONTROLLER); ALEXA_ROW(ContactSensor, NS_CONTACT_SENSOR, VERSION_3, PROPS_DETECTION, 1, AIF_NONE, CONTACT_SENSOR); ALEXA_ROW(MotionSensor, NS_MOTION_SENSOR, VERSION_3, PROPS_DETECTION, 1, AIF_NONE, MOTION_SENSOR); -ALEXA_ROW(DoorbellEventSource, NS_DOORBELL, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES, DOORBELL_EVENT_SOURCE); +ALEXA_ROW(DoorbellEventSource, NS_DOORBELL, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES | AIF_EVENTS, DOORBELL_EVENT_SOURCE); ALEXA_ROW(TimeHoldController, NS_TIME_HOLD, VERSION_3, PROPS_TIME_HOLD, 2, AIF_NONE, TIME_HOLD_CONTROLLER); ALEXA_ROW(Speaker, NS_SPEAKER, VERSION_3, PROPS_SPEAKER, 2, AIF_NONE, SPEAKER); ALEXA_ROW(StepSpeaker, NS_STEP_SPEAKER, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES, STEP_SPEAKER); diff --git a/src/AlexaInterfaces.h b/src/AlexaInterfaces.h index 80df67d..10d54ad 100644 --- a/src/AlexaInterfaces.h +++ b/src/AlexaInterfaces.h @@ -91,7 +91,9 @@ enum AlexaInterfaceFlags : uint8_t AIF_INSTANCED = 1, // a device may have several: each needs an instance and a friendly name AIF_NO_PROPERTIES = 2, // the discovery object has no "properties" (scenes, doorbells, step speakers) AIF_SEMANTICS = 4, // may carry action and state mappings - AIF_EMPTY_PROPERTIES = 8 // the discovery object has "properties": {}, as the page of the interface shows it + AIF_EMPTY_PROPERTIES = 8, // the discovery object has "properties": {}, as the page of the interface shows it + AIF_EVENTS = 16, // what it reports are events: "proactivelyReported": true beside the interface (doorbell) + AIF_DEACTIVATION = 32 // says whether it can be undone: "supportsDeactivation" beside the interface (scene) }; // 16 bytes in program memory. The ESP8266 reads flash in whole words only: the pointers can be read as they are, diff --git a/test/test_discovery/test_main.cpp b/test/test_discovery/test_main.cpp index 8e437cb..01edba5 100644 --- a/test/test_discovery/test_main.cpp +++ b/test/test_discovery/test_main.cpp @@ -247,7 +247,7 @@ void test_scene_is_announced_without_properties(void) device.addCapability(AlexaInterfaces::SceneController); assertDiscovery(endpoint("Movie Night", "SCENE_TRIGGER", - "{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\"}"), + "{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"supportsDeactivation\":false}"), device); } @@ -421,14 +421,32 @@ void test_toggle_controller_that_only_reports_is_announced_as_non_controllable(v device); } +// alexa-scenecontroller.html: supportsDeactivation says whether Alexa may send Deactivate +void test_scene_that_can_be_undone_says_so_and_no_other_interface_can(void) +{ + AlexaDevice device("Movie Night", "root", "ESP-01"); + device.setDisplayCategory(DisplayCategory::SCENE_TRIGGER); + device.addCapability(AlexaInterfaces::SceneController)->setSupportsDeactivation(true); + + assertDiscovery(endpoint("Movie Night", "SCENE_TRIGGER", + "{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"supportsDeactivation\":true}"), + device); + + AlexaDevice lamp("Lamp", "root", "ESP-02"); + AlexaCapability *power = lamp.addCapability(AlexaInterfaces::PowerController); + TEST_ASSERT_TRUE(power->isValid()); + power->setSupportsDeactivation(true); + TEST_ASSERT_FALSE_MESSAGE(power->isValid(), "a call the interface cannot take is a refused call"); +} + void test_doorbell_is_announced_without_properties(void) { AlexaDevice device("Front Door", "root", "ESP-01"); device.setDisplayCategory(DisplayCategory::DOORBELL); - device.addCapability(AlexaInterfaces::DoorbellEventSource)->setProactivelyReported(true); + device.addCapability(AlexaInterfaces::DoorbellEventSource); assertDiscovery(endpoint("Front Door", "DOORBELL", - "{\"interface\":\"Alexa.DoorbellEventSource\",\"version\":\"3\",\"type\":\"AlexaInterface\"}"), + "{\"interface\":\"Alexa.DoorbellEventSource\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"proactivelyReported\":true}"), device); } @@ -769,6 +787,7 @@ int main(int, char **) RUN_TEST(test_range_controller_blind_is_announced_with_names_configuration_and_semantics); RUN_TEST(test_mode_controller_is_announced_with_its_modes_and_text_states); RUN_TEST(test_toggle_controller_that_only_reports_is_announced_as_non_controllable); + RUN_TEST(test_scene_that_can_be_undone_says_so_and_no_other_interface_can); RUN_TEST(test_doorbell_is_announced_without_properties); RUN_TEST(test_playback_and_wake_on_lan_are_announced_with_empty_properties); RUN_TEST(test_instance_and_friendly_name_are_copied); diff --git a/test/test_examples/test_main.cpp b/test/test_examples/test_main.cpp index 001dff0..72b5595 100644 --- a/test/test_examples/test_main.cpp +++ b/test/test_examples/test_main.cpp @@ -239,11 +239,11 @@ void test_scene_discovery(void) { AlexaDevice scene("Movie Night", "root", "esp-scene"); scene.setDisplayCategory(DisplayCategory::SCENE_TRIGGER); - scene.addCapability(AlexaInterfaces::SceneController); + scene.addCapability(AlexaInterfaces::SceneController)->setSupportsDeactivation(true); scene.addCapability(AlexaInterfaces::EndpointHealth); assertDiscovery(endpoint("esp-scene", "Movie Night", "SCENE_TRIGGER", - list({"{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\"}", + list({"{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"supportsDeactivation\":true}", HEALTH})), scene); } @@ -252,12 +252,12 @@ void test_doorbell_discovery(void) { AlexaDevice doorbell("Front Door Bell", "root", "esp-doorbell"); doorbell.setDisplayCategory(DisplayCategory::DOORBELL); - doorbell.addCapability(AlexaInterfaces::DoorbellEventSource)->setProactivelyReported(true); + doorbell.addCapability(AlexaInterfaces::DoorbellEventSource); doorbell.addCapability(AlexaInterfaces::EndpointHealth); assertDiscovery( endpoint("esp-doorbell", "Front Door Bell", "DOORBELL", - list({"{\"interface\":\"Alexa.DoorbellEventSource\",\"version\":\"3\",\"type\":\"AlexaInterface\"}", + list({"{\"interface\":\"Alexa.DoorbellEventSource\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"proactivelyReported\":true}", HEALTH})), doorbell); }