Report helpers for every tier-1 property, asset ids, AlexaCapability::isValid()

AlexaStatusMessage gets a helper for color, percentage, power level, range, mode, lock state, contact,
motion, humidity and the thermostat (target, lower and upper setpoint, mode), and addProperty(row, place,
value) for a property without one. Each is a function of its own on one shared builder; a value outside of
the range of its property is reported as the nearest of the range with an error.
AlexaResources.h holds the 103 asset ids of resources-and-assets.html (AlexaAssets, AlexaUnits); AlexaState
has the seven states that were missing; addStateMapping() takes a number of any type.
A setter that refuses its argument makes isValid() false, and the capability is left out of discovery with
an error instead of being announced without what was refused.
basicLight: static RAM 30,520 B (was 30,540), flash 333,389 B (was 334,981); 130 host tests (was 112).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 21:00:01 +00:00
parent 3215457f91
commit 77156ffbe5
10 changed files with 995 additions and 100 deletions

View file

@ -141,19 +141,23 @@ void fillLift(JsonObject configuration, void*) {
range["minimumValue"] = 0;
range["maximumValue"] = 100;
range["precision"] = 1;
configuration["unitOfMeasure"] = "Alexa.Unit.Percent";
configuration["unitOfMeasure"] = FPSTR(AlexaUnits::Percent);
}
device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
->addFriendlyAsset(PSTR("Alexa.Setting.Opening"))
->addFriendlyAsset(AlexaAssets::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);
.addStateMapping({AlexaState::Closed}, 0)
.addStateMapping({AlexaState::Open}, 1, 100);
```
`AlexaAssets` and `AlexaUnits` (`src/AlexaResources.h`) hold the names and units of the catalog of Alexa, which Alexa translates: `AlexaAssets::Setting_Opening` is `Alexa.Setting.Opening`, `AlexaUnits::Temperature_Celsius` is `Alexa.Unit.Temperature.Celsius`. An id that the sketch does not name takes no flash.
A setter that refuses what it is given prints the reason and the chain goes on. `isValid()` of the capability is `false` from then on, and the device leaves the capability out of discovery with an error, so that Alexa does not learn half of it.
`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.
A device holds 8 capabilities (`-DALEX2ESP_MAX_CAPABILITIES`); `addCapability()` for one more prints an error and returns `nullptr`.
@ -240,24 +244,24 @@ The library describes these interfaces. `addCapability()` takes the row, `AlexaI
| `PowerController` | `POWER_CONTROLLER` | 3 | powerState | `addPowerControllerProp` |
| `BrightnessController` | `BRIGHTNESS_CONTROLLER` | 3 | brightness | `addBrightnessControllerProp` |
| `ColorTemperatureController` | `COLOR_TEMPERATURE_CONTROLLER` | 3 | colorTemperatureInKelvin | `addColorTemperatureControllerProp` |
| `ToggleController` | `TOGGLE_CONTROLLER` | 3 | toggleState | `addToggleControllerProp` |
| `TemperatureSensor` | `TEMPERATURE_SENSOR` | 3 | temperature | `addTemperatureSensorProp` |
| `ColorController` | `COLOR_CONTROLLER` | 3 | color | `addContextProp` |
| `PowerLevelController` | `POWER_LEVEL_CONTROLLER` | 3 | powerLevel | `addContextProp` |
| `PercentageController` | `PERCENTAGE_CONTROLLER` | 3 | percentage | `addContextProp` |
| `RangeController` | `RANGE_CONTROLLER` | 3 | rangeValue | `addContextProp` |
| `ModeController` | `MODE_CONTROLLER` | 3 | mode | `addContextProp` |
| `ThermostatController` | `THERMOSTAT_CONTROLLER` | 3.2 | targetSetpoint, lowerSetpoint, upperSetpoint, thermostatMode | `addContextProp` |
| `HumiditySensor` | `HUMIDITY_SENSOR` | 3 | relativeHumidity | `addContextProp` |
| `LockController` | `LOCK_CONTROLLER` | 3 | lockState | `addContextProp` |
| `ContactSensor` | `CONTACT_SENSOR` | 3 | detectionState | `addContextProp` |
| `MotionSensor` | `MOTION_SENSOR` | 3 | detectionState | `addContextProp` |
| `TimeHoldController` | `TIME_HOLD_CONTROLLER` | 3 | holdStartTime, holdEndTime | `addContextProp` |
| `Speaker` | `SPEAKER` | 3 | volume, muted | `addContextProp` |
| `PlaybackStateReporter` | `PLAYBACK_STATE_REPORTER` | 3 | playbackState | `addContextProp` |
| `InputController` | `INPUT_CONTROLLER` | 3 | input | `addContextProp` |
| `ChannelController` | `CHANNEL_CONTROLLER` | 3 | channel | `addContextProp` |
| `InventoryLevelSensor` | `INVENTORY_LEVEL_SENSOR` | 3 | level | `addContextProp` |
| `ColorController` | `COLOR_CONTROLLER` | 3 | color | `addColorControllerProp(hue, saturation, brightness)` |
| `PowerLevelController` | `POWER_LEVEL_CONTROLLER` | 3 | powerLevel | `addPowerLevelControllerProp` |
| `PercentageController` | `PERCENTAGE_CONTROLLER` | 3 | percentage | `addPercentageControllerProp` |
| `RangeController` | `RANGE_CONTROLLER` | 3 | rangeValue | `addRangeControllerProp(instance, value)` |
| `ModeController` | `MODE_CONTROLLER` | 3 | mode | `addModeControllerProp(instance, mode)` |
| `ToggleController` | `TOGGLE_CONTROLLER` | 3 | toggleState | `addToggleControllerProp(instance, state)` |
| `ThermostatController` | `THERMOSTAT_CONTROLLER` | 3.2 | targetSetpoint, lowerSetpoint, upperSetpoint, thermostatMode | `addThermostatSetpointProp`, `addThermostatLowerSetpointProp`, `addThermostatUpperSetpointProp` (both: `addThermostatDualSetpointProp(lower, upper)`), `addThermostatModeProp` |
| `TemperatureSensor` | `TEMPERATURE_SENSOR` | 3 | temperature | `addTemperatureSensorProp(value, scale)` |
| `HumiditySensor` | `HUMIDITY_SENSOR` | 3 | relativeHumidity | `addHumiditySensorProp` |
| `LockController` | `LOCK_CONTROLLER` | 3 | lockState | `addLockControllerProp` |
| `ContactSensor` | `CONTACT_SENSOR` | 3 | detectionState | `addContactSensorProp` |
| `MotionSensor` | `MOTION_SENSOR` | 3 | detectionState | `addMotionSensorProp` |
| `TimeHoldController` | `TIME_HOLD_CONTROLLER` | 3 | holdStartTime, holdEndTime | `addProperty` |
| `Speaker` | `SPEAKER` | 3 | volume, muted | `addProperty` |
| `PlaybackStateReporter` | `PLAYBACK_STATE_REPORTER` | 3 | playbackState | `addProperty` |
| `InputController` | `INPUT_CONTROLLER` | 3 | input | `addProperty` |
| `ChannelController` | `CHANNEL_CONTROLLER` | 3 | channel | `addProperty` |
| `InventoryLevelSensor` | `INVENTORY_LEVEL_SENSOR` | 3 | level | `addProperty` |
| `PlaybackController` | `PLAYBACK_CONTROLLER` | 3 | `"properties": {}` | |
| `WakeOnLANController` | `WAKE_ON_LAN_CONTROLLER` | 3 | `"properties": {}` | |
| `SceneController` | `SCENE_CONTROLLER` | 3 | no `properties` object | |
@ -265,7 +269,7 @@ The library describes these interfaces. `addCapability()` takes the row, `AlexaI
| `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) the sketch writes in the function it passes to `setConfiguration()`. Their events are not built by the library yet.
Every interface is announced with its version and properties; what else its discovery object needs (the configuration of a thermostat, the presets of a range) the sketch writes in the function it passes to `setConfiguration()`. A report helper is a function of its own, and a sketch links the ones it calls. A value outside of the range Alexa gives for its property (brightness, percentage, power level and humidity 0 to 100, color temperature 1000 to 10000, hue 0 to 360, saturation and brightness of a color 0 to 1) is reported as the nearest value of the range, with an error in the log. `addProperty(row, place, value)` adds a property that has no helper by its place in the row, 0 for the first: `addProperty(AlexaInterfaces::Speaker, 1, muted)` reports `muted`.
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`.
@ -320,6 +324,10 @@ Behaviour changes:
- New: `AlexaDevice::onDirective(handler)`. The handler, `void handler(AlexaDirective& d)`, gets every directive of its device with the device (`d.device`), the capability it is for (`d.capability`, `d.type`), its `ns`, `name`, `instance`, `correlationToken` and `payload`, and builds the answer with `d.response()`, `d.stateReport()`, `d.error(type, message)`, `d.deferred(seconds)` or, for a scene, `d.sceneStarted()` and `d.sceneStopped()`; one function can serve several devices. `registerEvent()` stays for the handlers of 1.x, which a device calls when it has no handler of `onDirective()`. `AlexaDevice::findCapability(namespace, instance)`.
- New kinds of messages beside `Response` and `StateReport`: `ErrorResponse` (`AlexaErrorType` has the types of Alexa, `asErrorResponse(type, message)` turns an answer into one), `DeferredResponse` and the answer that follows it (`sendAsync()`, on `<root>/<endpointId>/deferredResponse`), `ChangeReport` (`AlexaDevice::changeReport(cause)`, on `<root>/changeReport`), the events of a scene, and `DoorbellPress` (`AlexaDevice::doorbellPress()`, or `event(row, name)` for another interface, on `<root>/event`). `AlexaDevice::response(token)` and `stateReport(token)` build an answer outside of the handler. `payload()` gives the payload of a message, `getKind()` its kind.
- The report helpers begin with a small letter: `addHealthProp`, `addPowerControllerProp`, `addBrightnessControllerProp`, `addColorTemperatureControllerProp`, `addToggleControllerProp(instance, state)`, `addTemperatureSensorProp(value, scale)`, `addContextProp`. The names of 1.x stay, with their order of arguments.
- New report helpers, one for every property of the interfaces a device on this board is likely to have: `addColorControllerProp`, `addPowerLevelControllerProp`, `addPercentageControllerProp`, `addRangeControllerProp`, `addModeControllerProp`, `addLockControllerProp` (`AlexaLockState`), `addContactSensorProp`, `addMotionSensorProp`, `addHumiditySensorProp`, `addThermostatSetpointProp`, `addThermostatLowerSetpointProp`, `addThermostatUpperSetpointProp`, `addThermostatDualSetpointProp` and `addThermostatModeProp` (`AlexaThermostatMode`); `addProperty(row, place, value)` for a property without one. See Interface Types.
- A helper reports a value outside of the range of its property as the nearest value of the range and prints an error; 1.1.0 sent the value as it was. The brightness and the color temperature are taken as `int` (1.1.0: `unsigned int`), so that a negative number is reported as the lowest value.
- New: `AlexaAssets` and `AlexaUnits` in `src/AlexaResources.h`, the 103 asset ids of the catalog of Alexa as strings in program memory. `AlexaState` has the states `EcoOn`, `EcoOff`, `Low`, `Empty`, `Full`, `Done` and `Stuck` beside `Open` and `Closed`. `addStateMapping()` takes a number of any type, `addStateMapping({AlexaState::Closed}, 0)`.
- New: `AlexaCapability::isValid()`, `false` after a setter has refused what it was given. A capability that is not valid is left out of discovery with an error; before, it was announced without what had been refused.
- A temperature is reported in the scale it is given in: `AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT, 69)` reports 69 `FAHRENHEIT`, where 1.1.0 reported 20.56 `CELSIUS`. `TemperatureSensorScale::KELVIN` is new.
- `messageId` is a UUID of version 4 from the random source of the hardware. 1.1.0 sent 37 characters from `rand()`, seeded with the time in seconds: two messages of one second had the same id.
- A `ChangeReport` or an event that a handler sends is not taken for the answer to its directive.
@ -330,9 +338,9 @@ Behaviour changes:
- `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,540 bytes of static RAM (1.1.0: 52,768) and 334,981 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, the directive handler and the ErrorResponse 0.9 KB, the kinds of messages and the UUID 1.3 KB. A kind that a sketch does not send is not linked.
Memory: `examples/basicLight.cpp` for a D1 mini takes 30,520 bytes of static RAM (1.1.0: 52,768) and 333,389 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, the directive handler and the ErrorResponse 0.9 KB, the kinds of messages and the UUID 1.3 KB. A kind that a sketch does not send and a report helper that it does not call are not linked.
Tests: `pio test -e native` in the repository runs 112 host tests: 52 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, the message id), 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), 11 of dispatch (the device and the capability a directive reaches, one handler for two devices, the handlers of 1.x, a capability more than a device holds, the answers to a directive for a capability the device lacks and to one without a handler) and 19 of the messages (every kind against the JSON and the topic it is published with, the names of 1.x, the scale of a temperature, what `send()` and `sendAsync()` refuse). No board is needed.
Tests: `pio test -e native` in the repository runs 130 host tests: 52 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, the message id), 35 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 and that it is left out afterwards, the states and the asset ids), 11 of dispatch (the device and the capability a directive reaches, one handler for two devices, the handlers of 1.x, a capability more than a device holds, the answers to a directive for a capability the device lacks and to one without a handler) and 32 of the messages (every kind against the JSON and the topic it is published with, every report helper against the JSON of its property, values outside of their range, the names of 1.x, the scale of a temperature, what `send()` and `sendAsync()` refuse). 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.