AlexaCapability: instance, asset and text names, configuration, action and state mappings, nonControllable

AlexaCapability replaces the header-only AlexaInterface as what a device holds: 116 bytes with fixed places for
3 names, 4 action mappings and 2 state mappings, no std::vector or std::string. AlexaInterface and AlexaActions
stay as 1.x names; the five examples compile unchanged and announce the same bytes.
addCapability(row, instance) adds several capabilities of one generic controller. An instanced interface without
an instance or a name, an instance or semantics on an interface that takes none, and a full store are refused
with an ERROR line that says what to change.
PlaybackController and WakeOnLANController announce "properties": {} (AIF_EMPTY_PROPERTIES).
basicLight on a D1 mini: static RAM 30,788 -> 30,672 B, flash 330,509 -> 332,541 B; host tests 61 -> 77.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 19:33:57 +00:00
parent 8b11cc2394
commit 7afc4346e8
12 changed files with 1084 additions and 253 deletions

View file

@ -131,6 +131,31 @@ toggleController->addActionMapping(openMapping);
An `ActionMapping` takes an optional third argument, the directive payload as JSON text (for example `"{\"rangeValue\": 0}"` for a RangeController). It is emitted as an object in the discovery response and omitted when empty.
### Example: a blind with a position
`RangeController`, `ModeController` and `ToggleController` are generic controllers: a device may have several of one interface, each with an instance and at least one name. `addCapability()` takes the row and the instance and returns an `AlexaCapability`; its setters return the capability, so they can be chained.
```cpp
void fillLift(JsonObject configuration, void*) {
JsonObject range = configuration["supportedRange"].to<JsonObject>();
range["minimumValue"] = 0;
range["maximumValue"] = 100;
range["precision"] = 1;
configuration["unitOfMeasure"] = "Alexa.Unit.Percent";
}
device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
->addFriendlyAsset(PSTR("Alexa.Setting.Opening"))
.addFriendlyName("Lift", "en-US")
.setConfiguration(fillLift)
.addActionMapping(ActionMapping({AlexaAction::Close, AlexaAction::Lower}, "SetRangeValue", "{\"rangeValue\":0}"))
.addActionMapping(ActionMapping({AlexaAction::Open, AlexaAction::Raise}, "SetRangeValue", "{\"rangeValue\":100}"))
.addStateMapping({AlexaState::Closed}, 0.0f)
.addStateMapping({AlexaState::Open}, 1.0f, 100.0f);
```
`addCapability()` returns `nullptr` for a generic controller without an instance: check the pointer when the instance is not a literal. A capability holds 3 names, 4 action mappings and 2 state mappings (`-DALEX2ESP_MAX_FRIENDLY_NAMES`, `-DALEX2ESP_MAX_ACTION_MAPPINGS`, `-DALEX2ESP_MAX_STATE_MAPPINGS` in `build_flags` change that). The instance and the text of a friendly name are copied. The locale, an asset id, the text value of a state mapping and the directive name and payload of an `ActionMapping` are kept as pointers: pass literals. `setNonControllable(true)` announces a capability that reports its state and takes no directives.
---
## How it talks to Alex2MQTT
@ -176,14 +201,14 @@ The library describes these interfaces. `addCapability()` takes the row, `AlexaI
| `InputController` | `INPUT_CONTROLLER` | 3 | input | `AddContextProp` |
| `ChannelController` | `CHANNEL_CONTROLLER` | 3 | channel | `AddContextProp` |
| `InventoryLevelSensor` | `INVENTORY_LEVEL_SENSOR` | 3 | level | `AddContextProp` |
| `PlaybackController` | `PLAYBACK_CONTROLLER` | 3 | none | |
| `WakeOnLANController` | `WAKE_ON_LAN_CONTROLLER` | 3 | none | |
| `PlaybackController` | `PLAYBACK_CONTROLLER` | 3 | `"properties": {}` | |
| `WakeOnLANController` | `WAKE_ON_LAN_CONTROLLER` | 3 | `"properties": {}` | |
| `SceneController` | `SCENE_CONTROLLER` | 3 | no `properties` object | |
| `DoorbellEventSource` | `DOORBELL_EVENT_SOURCE` | 3 | no `properties` object | |
| `StepSpeaker` | `STEP_SPEAKER` | 3 | no `properties` object | |
| `SimpleEventSource` | `SIMPLE_EVENT_SOURCE` | 1.0 | no `properties` object | |
The interfaces below the first six are announced with their version and properties; what else their discovery object needs (the configuration of a thermostat, the presets of a range) and their events are not built by the library yet.
The interfaces below the first six are announced with their version and properties; what else their discovery object needs (the configuration of a thermostat, the presets of a range) the sketch writes in the function it passes to `setConfiguration()`. Their events are not built by the library yet.
Every other value of `AlexaInterfaceType` (`COOKING`, `LAUNCHER`, `SECURITY_PANEL_CONTROLLER`, ...) names an interface the library has no row for. `addCapability()` adds nothing for it, prints `[Alex2ESP] error: <endpointId>: capability not added: ...` and returns `nullptr`.
@ -234,10 +259,15 @@ Behaviour changes:
- `addCapability()` with a type that has no row adds nothing, prints an error and returns `nullptr`. 1.1.0 announced such an interface as version 1 without properties.
- The handler of `Event` gets the type of the capability of its device that the directive names, and `AlexaInterfaceType::UNKNOWN` for a namespace the device has no capability for. 1.1.0 looked the namespace up among all interfaces.
- Removed: `AlexaInterfaceType::AUTHORIZATION_CONTROLLER` and `AUTOMOTIVE_VEHICLE_DATA`, which Alexa has withdrawn. `AlexaInterfaceUtils` (`toString`, `fromString`, `getVersion`, `getProps`): a row has the namespace, the version and the properties, `alexaInterfaceRow(type)` gives the row of a type. The constructor of `AlexaInterface` takes a row. `DisplayCategory` and `AlexaInterfaceType` are one byte wide.
- New: `AlexaCapability`, which `addCapability()` returns. `addCapability(AlexaInterfaces::RangeController, "Blind.Lift")` takes the instance of an interface that a device may have several of, and a device can have several capabilities of one such interface. `addFriendlyAsset()`, `setConfiguration()`, `addStateMapping()` and `setNonControllable()` are new; every setter returns the capability. `matches(namespace, instance)` tells whether a directive is for the capability. `AlexaInterface` and `AlexaActions` stay as names of `AlexaCapability` and `AlexaAction`, so a 1.x sketch compiles as it is.
- A capability takes 116 bytes of heap and holds 3 friendly names, 4 action mappings and 2 state mappings (`ALEX2ESP_MAX_FRIENDLY_NAMES`, `ALEX2ESP_MAX_ACTION_MAPPINGS`, `ALEX2ESP_MAX_STATE_MAPPINGS`); one more is refused with an error. The instance and the text of a friendly name are copied, as before. The locale and the directive name and payload of an `ActionMapping` are kept as pointers, where 1.1.0 copied them: pass literals. An `ActionMapping` announces its actions in the order of `AlexaAction`.
- What Alexa does not accept is refused with an error that says what to change. `addCapability(row)` for an interface with instances and no instance, or with an instance for an interface without, adds nothing and returns `nullptr`. A capability with instances that has no instance or no friendly name when the device is announced (a 1.x sketch sets both after `addCapability(type)`) is left out of discovery. Action and state mappings are taken by `RangeController`, `ModeController` and `ToggleController` only.
- `PlaybackController` and `WakeOnLANController` are announced with `"properties": {}`, as their pages show them.
- Removed: `AlexaInterface::getJSON()` (`toJson()` adds the capability to the capabilities of its endpoint) and `getProps()` (the row has the properties), `ActionMapping::getJSON()` and the `String` and `std::vector` members of `ActionMapping`, the class `FriendlyName`.
Memory: `examples/basicLight.cpp` for a D1 mini takes 30,788 bytes of static RAM (1.1.0: 52,768) and 330,509 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). 18,260 bytes of the static RAM were the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients; 3,720 were the names, versions and properties of all interfaces and the names of the display categories, which are in flash now. SNTP and the time stamp are 1.8 KB of the flash figure.
Memory: `examples/basicLight.cpp` for a D1 mini takes 30,672 bytes of static RAM (1.1.0: 52,768) and 332,541 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). 18,260 bytes of the static RAM were the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients; 3,720 were the names, versions and properties of all interfaces and the names of the display categories, which are in flash now. SNTP and the time stamp are 1.8 KB of the flash figure. The instance, names, configuration and mappings of a capability are 2.0 KB of it.
Tests: `pio test -e native` in the repository runs 61 host tests: 47 of the receive and publish logic (reassembly of fragments, the directives that wait for `loop()`, repeated directives, the size limits, a heap without room, the wait between reconnects, topics, time stamps) and 14 of discovery (the discovery object of every example against what 1.1.0 announced, the 28 rows against the interface pages, a type without a row). No board is needed.
Tests: `pio test -e native` in the repository runs 77 host tests: 47 of the receive and publish logic (reassembly of fragments, the directives that wait for `loop()`, repeated directives, the size limits, a heap without room, the wait between reconnects, topics, time stamps) and 30 of discovery (the discovery object of every example against what 1.1.0 announced, the 28 rows against the interface pages, a type without a row, the discovery objects of a range, a mode and a toggle controller, a scene and a doorbell, what a capability refuses). No board is needed.
Packaging: `library.json`, `library.properties` and the `softwareVersion` and `firmwareVersion` that a device reports in discovery say 1.2.0; the library has the number in one place, `ALEX2ESP_VERSION` in `src/AlexaVersion.h`. The `platformio.ini` of the repository is for the host tests; a sketch does not need it.