AlexaDevice: capability array, linked list, a directive handler with the device in hand
onDirective(handler) registers void handler(AlexaDirective&): the directive comes with its device, the capability it is for, namespace, name, instance, correlationToken and payload, and response() and stateReport() build the answer, so one function serves several devices. registerEvent() is kept; the five examples compile unchanged and announce the same discovery objects. Devices are a linked list on the bridge and a device holds ALEX2ESP_MAX_CAPABILITIES (8) capability pointers in place of two std::deque; one capability more is refused with an error. A device without a handler, and a handler that sends nothing for a capability the device lacks, are answered with the ErrorResponse INVALID_DIRECTIVE instead of a timeout. A directive whose endpointId is not a device of the sketch is ignored at DEBUG. basicLight on a D1 mini: static RAM 30,672 -> 30,600 B, flash 332,541 -> 333,437 B, 0 warnings in the five examples; 88 host tests (11 new, test/test_dispatch). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
7afc4346e8
commit
d3e7ee7f2a
10 changed files with 767 additions and 67 deletions
|
|
@ -1,8 +1,9 @@
|
||||||
; Development project of the library itself. A sketch does not need this file: it gets the library through
|
; Development project of the library itself. A sketch does not need this file: it gets the library through
|
||||||
; lib_deps (readme.md, Quick Start).
|
; lib_deps (readme.md, Quick Start).
|
||||||
;
|
;
|
||||||
; pio test -e native host tests of the bridge logic (src/AlexaBridgeLogic.cpp) and of discovery
|
; pio test -e native host tests of the bridge logic (src/AlexaBridgeLogic.cpp), of discovery and of dispatch
|
||||||
; (src/AlexaDevice.cpp, src/AlexaCapability.cpp, src/AlexaInterfaces.cpp); no board, no broker
|
; (src/AlexaDevice.cpp, src/AlexaCapability.cpp, src/AlexaInterfaces.cpp,
|
||||||
|
; src/AlexaStatusMessage.cpp); no board, no broker
|
||||||
|
|
||||||
[platformio]
|
[platformio]
|
||||||
default_envs = native
|
default_envs = native
|
||||||
|
|
@ -12,7 +13,7 @@ platform = native
|
||||||
test_framework = unity
|
test_framework = unity
|
||||||
; Only the sources without a hardware dependency are compiled with the tests
|
; Only the sources without a hardware dependency are compiled with the tests
|
||||||
test_build_src = yes
|
test_build_src = yes
|
||||||
build_src_filter = -<*> +<AlexaBridgeLogic.cpp> +<AlexaCapability.cpp> +<AlexaDevice.cpp> +<AlexaInterfaces.cpp> +<AlexaLog.cpp>
|
build_src_filter = -<*> +<AlexaBridgeLogic.cpp> +<AlexaCapability.cpp> +<AlexaDevice.cpp> +<AlexaInterfaces.cpp> +<AlexaLog.cpp> +<AlexaStatusMessage.cpp>
|
||||||
lib_deps =
|
lib_deps =
|
||||||
bblanchon/ArduinoJson@^7
|
bblanchon/ArduinoJson@^7
|
||||||
; test/host_arduino holds the Arduino.h of the host: String, Print and Serial
|
; test/host_arduino holds the Arduino.h of the host: String, Print and Serial
|
||||||
|
|
|
||||||
43
readme.md
43
readme.md
|
|
@ -156,6 +156,40 @@ device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
|
||||||
|
|
||||||
`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.
|
`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`.
|
||||||
|
|
||||||
|
### Example: one handler for several devices
|
||||||
|
`onDirective()` registers a function that gets every directive of its device, `ReportState` included, together with the device and the capability the directive is for. The same function can serve any number of devices:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
AlexaDevice* lamps[2];
|
||||||
|
bool on[2];
|
||||||
|
|
||||||
|
void onLamp(AlexaDirective& d) {
|
||||||
|
bool& state = on[d.device == lamps[0] ? 0 : 1];
|
||||||
|
if (d.isReportState()) {
|
||||||
|
d.stateReport().AddHealthProp(EndpointHealth::OK).AddPowerControllerProp(state ? PowerController::ON : PowerController::OFF).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.type != AlexaInterfaceType::POWER_CONTROLLER) {
|
||||||
|
return; // not for a capability of the lamp: the library answers INVALID_DIRECTIVE
|
||||||
|
}
|
||||||
|
state = d.is("TurnOn");
|
||||||
|
d.response().AddHealthProp(EndpointHealth::OK).AddPowerControllerProp(state ? PowerController::ON : PowerController::OFF).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// in setup(), after begin()
|
||||||
|
lamps[0] = alexClient.getDevice("Desk Lamp", "ESP-01");
|
||||||
|
lamps[1] = alexClient.getDevice("Floor Lamp", "ESP-02");
|
||||||
|
for (AlexaDevice* lamp : lamps) {
|
||||||
|
lamp->setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamp->addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp->onDirective(onLamp);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`d.ns`, `d.name`, `d.instance`, `d.correlationToken` and `d.payload` are those of the directive and valid until the handler returns. `d.capability` is the capability of the device with the namespace and the instance of the directive, `nullptr` when the device has none and for `ReportState`. A device with such a handler does not call the `ReportState` and `Event` handlers of `registerEvent()`, which keep working for a device without one.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How it talks to Alex2MQTT
|
## How it talks to Alex2MQTT
|
||||||
|
|
@ -164,7 +198,7 @@ Everything goes over MQTT (port 1883 of `alex2mqtt.stormysdream.club`); the libr
|
||||||
|
|
||||||
- **Session.** `begin()` starts SNTP (`pool.ntp.org`, `time.nist.gov`) and returns; `loop()` opens the MQTT session once Wi-Fi is up and the clock is set, or after 5 s of Wi-Fi without an answer, and subscribes to `<root>/discover` and `<root>/+/alexaDirective`. `getState()` is `CONNECTED` when the broker has acknowledged both subscriptions; `[Alex2ESP] error: the broker refused the subscription to <root>/discover` means that the root topic is not the one of the account. A session that ends or cannot be opened prints `[Alex2ESP] error: disconnected: <reason>; next attempt in N s` and is opened again after 1 s, then 2 s, 4 s and so on up to once a minute, for as long as Wi-Fi is up; the wait starts at 1 s again once a session is open. `the broker refused the username or the password` is the reason to look for when a board never shows up in Alexa. A sketch that sets the clock itself (its own `configTime()` with a time zone, an RTC) calls `alexClient.setTimeSource(false)` before `begin()`. The board has to reach an NTP server: DNS for the two names and outbound UDP port 123. Without the time of day the session still opens, the reports carry a `timeOfSample` in 1970, and the library prints `[Alex2ESP] error: the clock is not set, ...` when it connects and then at most once a minute while reports are sent.
|
- **Session.** `begin()` starts SNTP (`pool.ntp.org`, `time.nist.gov`) and returns; `loop()` opens the MQTT session once Wi-Fi is up and the clock is set, or after 5 s of Wi-Fi without an answer, and subscribes to `<root>/discover` and `<root>/+/alexaDirective`. `getState()` is `CONNECTED` when the broker has acknowledged both subscriptions; `[Alex2ESP] error: the broker refused the subscription to <root>/discover` means that the root topic is not the one of the account. A session that ends or cannot be opened prints `[Alex2ESP] error: disconnected: <reason>; next attempt in N s` and is opened again after 1 s, then 2 s, 4 s and so on up to once a minute, for as long as Wi-Fi is up; the wait starts at 1 s again once a session is open. `the broker refused the username or the password` is the reason to look for when a board never shows up in Alexa. A sketch that sets the clock itself (its own `configTime()` with a time zone, an RTC) calls `alexClient.setTimeSource(false)` before `begin()`. The board has to reach an NTP server: DNS for the two names and outbound UDP port 123. Without the time of day the session still opens, the reports carry a `timeOfSample` in 1970, and the library prints `[Alex2ESP] error: the clock is not set, ...` when it connects and then at most once a minute while reports are sent.
|
||||||
- **Discovery.** On `<root>/discover` the library answers with one discovery object per device on `<root>/discover_r`. The backend accepts one endpoint object per message and collects everything that arrives within 1 s for Alexa's discovery answer (up to 5 s for its proactive AddOrUpdate push), so all devices are published back to back from the next `loop()`. Each object sits on the heap (about 1 KB) until the MQTT client has sent it; when the client cannot take another one (free heap under 4 KB), the library prints `[Alex2ESP] discovery deferred at <endpointId>` and sends the rest from `loop()` as the queue drains, for up to 5 s after the request. `[Alex2ESP] error: discovery gave up: N device(s) not announced` means those devices missed this answer - on the backend's proactive discovery that can remove them from Alexa until the next one.
|
- **Discovery.** On `<root>/discover` the library answers with one discovery object per device on `<root>/discover_r`. The backend accepts one endpoint object per message and collects everything that arrives within 1 s for Alexa's discovery answer (up to 5 s for its proactive AddOrUpdate push), so all devices are published back to back from the next `loop()`. Each object sits on the heap (about 1 KB) until the MQTT client has sent it; when the client cannot take another one (free heap under 4 KB), the library prints `[Alex2ESP] discovery deferred at <endpointId>` and sends the rest from `loop()` as the queue drains, for up to 5 s after the request. `[Alex2ESP] error: discovery gave up: N device(s) not announced` means those devices missed this answer - on the backend's proactive discovery that can remove them from Alexa until the next one.
|
||||||
- **Directives.** The directive arrives as JSON on `<root>/<endpointId>/alexaDirective`. A directive larger than one TCP segment arrives in fragments, which are put together in one heap block that exists only until `loop()` has parsed it. `loop()` then fires `ReportState` or `Event` (and `DirectiveReceived`, if registered) with the directive: `directive["header"]`, `directive["endpoint"]`, `directive["payload"]`. Every call of `loop()` handles one directive, in the order of arrival. Up to eight directives wait for it: Alexa sends a group command ("turn off the kitchen") as one directive per endpoint, and they arrive faster than a busy sketch calls `loop()`. A directive that arrives twice (a broker that mirrors its topics delivers every message twice) is handled once; the repeat is recognised while it arrives and takes no place among the waiting ones.
|
- **Directives.** The directive arrives as JSON on `<root>/<endpointId>/alexaDirective`. A directive larger than one TCP segment arrives in fragments, which are put together in one heap block that exists only until `loop()` has parsed it. `loop()` then calls the handler of the device: the one of `onDirective()`, or else `ReportState` or `Event` of `registerEvent()` (and before it `DirectiveReceived`, if registered) with the directive: `directive["header"]`, `directive["endpoint"]`, `directive["payload"]`. A device without a handler answers with the ErrorResponse `INVALID_DIRECTIVE`, and so does a device whose handler sent nothing for a directive that names a capability the device does not have; both print an error. A handler that sends nothing for a capability of its device leaves the directive unanswered, which prints `[Alex2ESP] error: <endpointId>: the handler sent no answer to ...`. Every call of `loop()` handles one directive, in the order of arrival. Up to eight directives wait for it: Alexa sends a group command ("turn off the kitchen") as one directive per endpoint, and they arrive faster than a busy sketch calls `loop()`. A directive that arrives twice (a broker that mirrors its topics delivers every message twice) is handled once; the repeat is recognised while it arrives and takes no place among the waiting ones.
|
||||||
- **Reports.** `send()` publishes the report on `<root>/<endpointId>/alexaResponce` at once. The backend waits 7 s for it, so answer from the event handler. Every property carries the board's UTC time as `timeOfSample`.
|
- **Reports.** `send()` publishes the report on `<root>/<endpointId>/alexaResponce` at once. The backend waits 7 s for it, so answer from the event handler. Every property carries the board's UTC time as `timeOfSample`.
|
||||||
- **Limits.** A directive may be 2047 bytes (`ALEX2ESP_MAX_DIRECTIVE`), a report or the discovery object of one device 3071 (`ALEX2ESP_MAX_MESSAGE`). The second limit is the first plus 1024 unless it is set: an answer repeats the `correlationToken` of its directive, which is most of a large directive, and adds 140 to 170 bytes per property, so the largest directive can be answered with six properties. Eight directives (`ALEX2ESP_MAX_QUEUED_DIRECTIVES`) of 8188 bytes together (`ALEX2ESP_MAX_QUEUED_BYTES`, four times the largest directive) may wait for `loop()`; one that finds no place is dropped with `[Alex2ESP] error: directive of N bytes on <topic> dropped: ...`. `-D<name>=<value>` in `build_flags` changes a limit. `send()` returns `false` when the report was not sent: no session with the broker, the MQTT client or the heap cannot take it, or it is too large. Nothing is ever sent truncated, and nothing is dropped without a line on Serial.
|
- **Limits.** A directive may be 2047 bytes (`ALEX2ESP_MAX_DIRECTIVE`), a report or the discovery object of one device 3071 (`ALEX2ESP_MAX_MESSAGE`). The second limit is the first plus 1024 unless it is set: an answer repeats the `correlationToken` of its directive, which is most of a large directive, and adds 140 to 170 bytes per property, so the largest directive can be answered with six properties. Eight directives (`ALEX2ESP_MAX_QUEUED_DIRECTIVES`) of 8188 bytes together (`ALEX2ESP_MAX_QUEUED_BYTES`, four times the largest directive) may wait for `loop()`; one that finds no place is dropped with `[Alex2ESP] error: directive of N bytes on <topic> dropped: ...`. `-D<name>=<value>` in `build_flags` changes a limit. `send()` returns `false` when the report was not sent: no session with the broker, the MQTT client or the heap cannot take it, or it is too large. Nothing is ever sent truncated, and nothing is dropped without a line on Serial.
|
||||||
- **Serial output.** Every line of the library starts with `[Alex2ESP]`, a problem with `[Alex2ESP] error:`. `alexClient.setLogLevel(AlexaLogLevel::ERROR)` leaves only the problems, `AlexaLogLevel::NONE` nothing; the default, `AlexaLogLevel::INFO`, adds the session, discovery and one line per directive (`[Alex2ESP] ESP-01 <- Alexa.PowerController.TurnOn`). `AlexaLogLevel::DEBUG` (sizes and free heap per message) has to be compiled in with `-DALEX2ESP_LOG_MAX=3`; `-DALEX2ESP_LOG_MAX=0` compiles every line out. The username, the password, the root topic and correlation tokens are never printed, at any level, so a log can be posted as it is: a topic appears without its root (`ESP-01/alexaResponce`), a directive under its endpoint id. A sketch that defines a macro named `DEBUG`, `ERROR` or `INFO` cannot write the level of that name; it passes the number instead, for example `alexClient.setLogLevel(static_cast<AlexaLogLevel>(3))` for `DEBUG`.
|
- **Serial output.** Every line of the library starts with `[Alex2ESP]`, a problem with `[Alex2ESP] error:`. `alexClient.setLogLevel(AlexaLogLevel::ERROR)` leaves only the problems, `AlexaLogLevel::NONE` nothing; the default, `AlexaLogLevel::INFO`, adds the session, discovery and one line per directive (`[Alex2ESP] ESP-01 <- Alexa.PowerController.TurnOn`). `AlexaLogLevel::DEBUG` (sizes and free heap per message) has to be compiled in with `-DALEX2ESP_LOG_MAX=3`; `-DALEX2ESP_LOG_MAX=0` compiles every line out. The username, the password, the root topic and correlation tokens are never printed, at any level, so a log can be posted as it is: a topic appears without its root (`ESP-01/alexaResponce`), a directive under its endpoint id. A sketch that defines a macro named `DEBUG`, `ERROR` or `INFO` cannot write the level of that name; it passes the number instead, for example `alexClient.setLogLevel(static_cast<AlexaLogLevel>(3))` for `DEBUG`.
|
||||||
|
|
@ -260,14 +294,17 @@ Behaviour changes:
|
||||||
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
- 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()` or `d.stateReport()`; 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)`, `AlexaStatusMessage::asErrorResponse(type, message)`.
|
||||||
|
- A directive that nothing answers is answered by the library with the ErrorResponse `INVALID_DIRECTIVE`: when the device has no handler for it, and when it names a capability that the device does not have and the handler sent nothing. 1.1.0 left both to the timeout, after which Alexa says that the device does not respond. A handler that sends nothing for a capability of its device prints an error; nothing is sent for it.
|
||||||
|
- Devices are a linked list, and a device holds its capabilities in an array of 8 pointers (`ALEX2ESP_MAX_CAPABILITIES`); both were a `std::deque`. A ninth capability is refused with an error and `addCapability()` returns `nullptr`. `getDevice()` returns `nullptr` with an error when the heap has no room for the device. An `AlexaDevice` cannot be copied. A directive whose topic names a device of the board and whose `endpointId` does not is ignored with a line at `DEBUG` (it was an error).
|
||||||
- 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`.
|
- 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.
|
- 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.
|
- `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`.
|
- 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,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.
|
Memory: `examples/basicLight.cpp` for a D1 mini takes 30,600 bytes of static RAM (1.1.0: 52,768) and 333,437 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.
|
||||||
|
|
||||||
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.
|
Tests: `pio test -e native` in the repository runs 88 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), 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) and 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). 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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ Alex2ESP::Alex2ESP()
|
||||||
discoveryRequested(false),
|
discoveryRequested(false),
|
||||||
discoveryActive(false),
|
discoveryActive(false),
|
||||||
discoveryDeferred(false),
|
discoveryDeferred(false),
|
||||||
discoveryNext(0),
|
discoveryNext(nullptr),
|
||||||
discoveryAnnounced(0),
|
discoveryAnnounced(0),
|
||||||
discoveryStarted(0),
|
discoveryStarted(0),
|
||||||
discoveryLastAttempt(0),
|
discoveryLastAttempt(0),
|
||||||
|
|
@ -153,7 +153,7 @@ AsyncMqttClientDisconnectReason Alex2ESP::getDisconnectReason() const
|
||||||
AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
|
AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
|
||||||
{
|
{
|
||||||
// Check if a device with the given endpointId already exists
|
// Check if a device with the given endpointId already exists
|
||||||
AlexaDevice *existing = findDevice(endpointId.c_str(), endpointId.length());
|
AlexaDevice *existing = devices.find(endpointId.c_str(), endpointId.length());
|
||||||
if (existing != nullptr)
|
if (existing != nullptr)
|
||||||
{
|
{
|
||||||
return existing;
|
return existing;
|
||||||
|
|
@ -164,25 +164,15 @@ AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
|
||||||
ALEX2ESP_LOGE("device %s created before begin(): it has no root topic, its reports will not arrive", endpointId.c_str());
|
ALEX2ESP_LOGE("device %s created before begin(): it has no root topic, its reports will not arrive", endpointId.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the device doesn't exist, create a new one
|
AlexaDevice *device = devices.add(name, rootTopic, endpointId, this);
|
||||||
devices.emplace_back(name, rootTopic, endpointId, this);
|
if (device == nullptr)
|
||||||
ALEX2ESP_LOGI("device %s (%s) created", endpointId.c_str(), name.c_str());
|
|
||||||
|
|
||||||
// Return a pointer to the newly created device
|
|
||||||
return &devices.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
AlexaDevice *Alex2ESP::findDevice(const char *endpointId, size_t length)
|
|
||||||
{
|
{
|
||||||
for (auto &device : devices)
|
ALEX2ESP_LOGE("device %s not created: no memory (%u bytes of heap free)", endpointId.c_str(), (unsigned)ESP.getFreeHeap());
|
||||||
{
|
|
||||||
if (device.hasEndpointId(endpointId, length))
|
|
||||||
{
|
|
||||||
return &device;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
ALEX2ESP_LOGI("device %s (%s) created", endpointId.c_str(), name.c_str());
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
// The first connect waits for Wi-Fi and then until the clock is set, for CLOCK_WAIT_MS at most: a report sent before
|
// The first connect waits for Wi-Fi and then until the clock is set, for CLOCK_WAIT_MS at most: a report sent before
|
||||||
// SNTP has answered would carry a time of sample in 1970.
|
// SNTP has answered would carry a time of sample in 1970.
|
||||||
|
|
@ -332,7 +322,7 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (findDevice(endpointId, endpointLength) == nullptr)
|
if (devices.find(endpointId, endpointLength) == nullptr)
|
||||||
{
|
{
|
||||||
// <root>/+/alexaDirective delivers the directives of every endpoint of the account, also those of other
|
// <root>/+/alexaDirective delivers the directives of every endpoint of the account, also those of other
|
||||||
// boards: not an error, and not worth a buffer
|
// boards: not an error, and not worth a buffer
|
||||||
|
|
@ -405,10 +395,11 @@ void Alex2ESP::processDirective()
|
||||||
const char *name = received["header"]["name"] | "";
|
const char *name = received["header"]["name"] | "";
|
||||||
const char *messageId = received["header"]["messageId"] | "";
|
const char *messageId = received["header"]["messageId"] | "";
|
||||||
|
|
||||||
AlexaDevice *device = findDevice(endpointId, strlen(endpointId));
|
AlexaDevice *device = devices.find(endpointId, strlen(endpointId));
|
||||||
if (device == nullptr)
|
if (device == nullptr)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("directive %s.%s dropped: it names the endpoint '%s', which this board does not have", interfaceName, name, endpointId);
|
// The topic named a device of this board, the directive itself names another endpoint
|
||||||
|
ALEX2ESP_LOGD("directive %s.%s ignored: the sketch has no device '%s'", interfaceName, name, endpointId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (recentIds.seenBefore(messageId))
|
if (recentIds.seenBefore(messageId))
|
||||||
|
|
@ -420,15 +411,7 @@ void Alex2ESP::processDirective()
|
||||||
ALEX2ESP_LOGI("%s <- %s.%s", endpointId, interfaceName, name);
|
ALEX2ESP_LOGI("%s <- %s.%s", endpointId, interfaceName, name);
|
||||||
ALEX2ESP_LOGD("%u bytes, %u bytes of heap free", (unsigned)length, (unsigned)ESP.getFreeHeap());
|
ALEX2ESP_LOGD("%u bytes, %u bytes of heap free", (unsigned)length, (unsigned)ESP.getFreeHeap());
|
||||||
|
|
||||||
device->triggerEvent("DirectiveReceived", message, AlexaInterfaceType::UNKNOWN, false);
|
device->handleDirective(message);
|
||||||
if (strcmp(interfaceName, "Alexa") == 0 && strcmp(name, "ReportState") == 0)
|
|
||||||
{
|
|
||||||
device->triggerEvent("ReportState", message, AlexaInterfaceType::UNKNOWN);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
device->triggerEvent("Event", message, device->getInterfaceType(interfaceName));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Answer a Discover: one discovery object per device, published straight to <root>/discover_r. The backend accepts
|
// Answer a Discover: one discovery object per device, published straight to <root>/discover_r. The backend accepts
|
||||||
|
|
@ -442,7 +425,7 @@ void Alex2ESP::continueDiscovery()
|
||||||
discoveryRequested = false;
|
discoveryRequested = false;
|
||||||
discoveryActive = true;
|
discoveryActive = true;
|
||||||
discoveryDeferred = false;
|
discoveryDeferred = false;
|
||||||
discoveryNext = 0;
|
discoveryNext = devices.first();
|
||||||
discoveryAnnounced = 0;
|
discoveryAnnounced = 0;
|
||||||
}
|
}
|
||||||
if (!discoveryActive)
|
if (!discoveryActive)
|
||||||
|
|
@ -469,9 +452,9 @@ void Alex2ESP::continueDiscovery()
|
||||||
// device we stop, remember where we got to and let loop() carry on once the queue has drained.
|
// device we stop, remember where we got to and let loop() carry on once the queue has drained.
|
||||||
void Alex2ESP::publishDiscovery()
|
void Alex2ESP::publishDiscovery()
|
||||||
{
|
{
|
||||||
while (discoveryNext < devices.size())
|
while (discoveryNext != nullptr)
|
||||||
{
|
{
|
||||||
const AlexaDevice &device = devices[discoveryNext];
|
const AlexaDevice &device = *discoveryNext;
|
||||||
JsonDocument json = device.getDeviceJSON();
|
JsonDocument json = device.getDeviceJSON();
|
||||||
size_t length = 0;
|
size_t length = 0;
|
||||||
AlexaSendResult result = trySend(discoverTopicSend.c_str(), json, &length);
|
AlexaSendResult result = trySend(discoverTopicSend.c_str(), json, &length);
|
||||||
|
|
@ -497,7 +480,7 @@ void Alex2ESP::publishDiscovery()
|
||||||
logRefusal(discoverTopicSend.c_str(), result, length);
|
logRefusal(discoverTopicSend.c_str(), result, length);
|
||||||
ALEX2ESP_LOGE("device %s is not announced", device.getEndpointId().c_str());
|
ALEX2ESP_LOGE("device %s is not announced", device.getEndpointId().c_str());
|
||||||
}
|
}
|
||||||
discoveryNext++;
|
discoveryNext = device.nextDevice();
|
||||||
}
|
}
|
||||||
|
|
||||||
discoveryActive = false;
|
discoveryActive = false;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
* endpoints through the MQTT broker at alex2mqtt.stormysdream.club.
|
* endpoints through the MQTT broker at alex2mqtt.stormysdream.club.
|
||||||
*
|
*
|
||||||
* <root>/discover in answered with one discovery object per device on <root>/discover_r
|
* <root>/discover in answered with one discovery object per device on <root>/discover_r
|
||||||
* <root>/<endpointId>/alexaDirective in the directive, handed to the device's ReportState / Event handler
|
* <root>/<endpointId>/alexaDirective in the directive, handed to the handler of the device (onDirective(), or
|
||||||
|
* the ReportState / Event handlers of 1.x)
|
||||||
* <root>/<endpointId>/alexaResponce out the report the handler built with buildStatusMessage()
|
* <root>/<endpointId>/alexaResponce out the report the handler built with buildStatusMessage()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -25,7 +26,6 @@
|
||||||
#include "AlexaLog.h"
|
#include "AlexaLog.h"
|
||||||
#include "AlexaTransport.h"
|
#include "AlexaTransport.h"
|
||||||
#include "AlexaUtils.h"
|
#include "AlexaUtils.h"
|
||||||
#include <deque>
|
|
||||||
|
|
||||||
enum class Alex2ESPState
|
enum class Alex2ESPState
|
||||||
{
|
{
|
||||||
|
|
@ -58,7 +58,7 @@ public:
|
||||||
AsyncMqttClientDisconnectReason getDisconnectReason() const;
|
AsyncMqttClientDisconnectReason getDisconnectReason() const;
|
||||||
|
|
||||||
// Returns the device with this endpointId, creating it on first use. The pointer stays valid for the lifetime
|
// Returns the device with this endpointId, creating it on first use. The pointer stays valid for the lifetime
|
||||||
// of the client: devices live in a std::deque, which never relocates its elements when another one is added.
|
// of the client. nullptr when the heap has no room for another device; the reason is printed.
|
||||||
// Call it after begin(): a device takes the root topic of its reports when it is created.
|
// Call it after begin(): a device takes the root topic of its reports when it is created.
|
||||||
AlexaDevice *getDevice(const String &name, const String &endpointId);
|
AlexaDevice *getDevice(const String &name, const String &endpointId);
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ private:
|
||||||
String discoverTopicSend; // The topic we send discovery messages
|
String discoverTopicSend; // The topic we send discovery messages
|
||||||
String directiveFilter; // The subscription that delivers the directives of every endpoint
|
String directiveFilter; // The subscription that delivers the directives of every endpoint
|
||||||
|
|
||||||
std::deque<AlexaDevice> devices; // Collection of devices (deque: pointers handed out by getDevice stay valid)
|
AlexaDeviceList devices; // On the heap, one by one: pointers handed out by getDevice stay valid
|
||||||
|
|
||||||
Alex2ESPState state;
|
Alex2ESPState state;
|
||||||
AsyncMqttClientDisconnectReason disconnectReason;
|
AsyncMqttClientDisconnectReason disconnectReason;
|
||||||
|
|
@ -104,7 +104,7 @@ private:
|
||||||
bool discoveryRequested; // Set by the MQTT callback, taken by loop()
|
bool discoveryRequested; // Set by the MQTT callback, taken by loop()
|
||||||
bool discoveryActive; // An answer is going out
|
bool discoveryActive; // An answer is going out
|
||||||
bool discoveryDeferred; // The answer had to pause: the MQTT client or the heap was full
|
bool discoveryDeferred; // The answer had to pause: the MQTT client or the heap was full
|
||||||
size_t discoveryNext; // Next device to announce
|
AlexaDevice *discoveryNext; // Next device to announce
|
||||||
size_t discoveryAnnounced; // Devices announced in this answer
|
size_t discoveryAnnounced; // Devices announced in this answer
|
||||||
unsigned long discoveryStarted; // millis() when the Discover arrived
|
unsigned long discoveryStarted; // millis() when the Discover arrived
|
||||||
unsigned long discoveryLastAttempt; // millis() of the publish that was refused
|
unsigned long discoveryLastAttempt; // millis() of the publish that was refused
|
||||||
|
|
@ -127,7 +127,6 @@ private:
|
||||||
void publishDiscovery();
|
void publishDiscovery();
|
||||||
void processDirective();
|
void processDirective();
|
||||||
|
|
||||||
AlexaDevice *findDevice(const char *endpointId, size_t length);
|
|
||||||
AlexaSendResult trySend(const char *topic, JsonDocument &doc, size_t *length);
|
AlexaSendResult trySend(const char *topic, JsonDocument &doc, size_t *length);
|
||||||
void logRefusal(const char *topic, AlexaSendResult result, size_t length);
|
void logRefusal(const char *topic, AlexaSendResult result, size_t length);
|
||||||
const char *logName(const char *topic) const; // The topic as the log may print it: without the root topic
|
const char *logName(const char *topic) const; // The topic as the log may print it: without the root topic
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
#include "AlexaDevice.h"
|
#include "AlexaDevice.h"
|
||||||
#include "AlexaLog.h"
|
#include "AlexaLog.h"
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
// The texts of the ErrorResponse the device sends itself; they reach the log of the skill, not the user
|
||||||
|
static const char ERROR_INVALID_DIRECTIVE[] PROGMEM = "INVALID_DIRECTIVE";
|
||||||
|
static const char ERROR_NO_HANDLER[] PROGMEM = "The endpoint has no handler for directives";
|
||||||
|
static const char ERROR_NO_CAPABILITY[] PROGMEM = "The endpoint does not have this capability";
|
||||||
|
|
||||||
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport)
|
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport)
|
||||||
: name(name), endpointId(endpointId), rootTopic(rootTopic), transport(transport) {
|
: name(name), endpointId(endpointId), rootTopic(rootTopic), transport(transport) {
|
||||||
|
|
@ -10,6 +16,12 @@ AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const Stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AlexaDevice::~AlexaDevice() {
|
||||||
|
for (uint8_t i = 0; i < capabilityCount; ++i) {
|
||||||
|
delete capabilities[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Setters and Getters
|
// Setters and Getters
|
||||||
void AlexaDevice::setName(const String& name) {
|
void AlexaDevice::setName(const String& name) {
|
||||||
this->name = name;
|
this->name = name;
|
||||||
|
|
@ -88,15 +100,27 @@ AlexaCapability* AlexaDevice::addCapability(const AlexaInterfaceDesc& row, const
|
||||||
}
|
}
|
||||||
|
|
||||||
AlexaCapability* AlexaDevice::capabilityOf(const AlexaInterfaceDesc& row, const char* instance) {
|
AlexaCapability* AlexaDevice::capabilityOf(const AlexaInterfaceDesc& row, const char* instance) {
|
||||||
for (AlexaCapability& capability : capabilities) {
|
for (uint8_t i = 0; i < capabilityCount; ++i) {
|
||||||
if (&capability.getRow() == &row && (instance == nullptr || strcmp(capability.getInstance(), instance) == 0)) {
|
AlexaCapability* capability = capabilities[i];
|
||||||
return &capability;
|
if (&capability->getRow() == &row && (instance == nullptr || strcmp(capability->getInstance(), instance) == 0)) {
|
||||||
|
return capability;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
capabilities.emplace_back(row, instance);
|
if (capabilityCount == ALEX2ESP_MAX_CAPABILITIES) {
|
||||||
|
ALEX2ESP_LOGE("%s: capability %s not added: the device has %d already (ALEX2ESP_MAX_CAPABILITIES)",
|
||||||
|
endpointId.c_str(), AlexaCapabilityName(row, instance).text, ALEX2ESP_MAX_CAPABILITIES);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
AlexaCapability* capability = new (std::nothrow) AlexaCapability(row, instance);
|
||||||
|
if (capability == nullptr) {
|
||||||
|
ALEX2ESP_LOGE("%s: capability %s not added: no memory", endpointId.c_str(),
|
||||||
|
AlexaCapabilityName(row, instance).text);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
capabilities[capabilityCount++] = capability;
|
||||||
ALEX2ESP_LOGI("%s: capability %s added", endpointId.c_str(), AlexaCapabilityName(row, instance).text);
|
ALEX2ESP_LOGI("%s: capability %s added", endpointId.c_str(), AlexaCapabilityName(row, instance).text);
|
||||||
return &capabilities.back();
|
return capability;
|
||||||
}
|
}
|
||||||
|
|
||||||
AlexaCapability* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
|
AlexaCapability* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
|
||||||
|
|
@ -107,14 +131,23 @@ AlexaCapability* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
AlexaInterfaceType AlexaDevice::getInterfaceType(const char* interfaceName) const {
|
AlexaInterfaceType AlexaDevice::getInterfaceType(const char* interfaceName) const {
|
||||||
for (const AlexaCapability& capability : capabilities) {
|
for (uint8_t i = 0; i < capabilityCount; ++i) {
|
||||||
if (strcmp_P(interfaceName, capability.getRow().ns) == 0) {
|
if (strcmp_P(interfaceName, capabilities[i]->getRow().ns) == 0) {
|
||||||
return capability.getType();
|
return capabilities[i]->getType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return AlexaInterfaceType::UNKNOWN;
|
return AlexaInterfaceType::UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AlexaCapability* AlexaDevice::findCapability(const char* ns, const char* instance) const {
|
||||||
|
for (uint8_t i = 0; i < capabilityCount; ++i) {
|
||||||
|
if (capabilities[i]->matches(ns, instance)) {
|
||||||
|
return capabilities[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
JsonDocument AlexaDevice::getDeviceJSON() const {
|
JsonDocument AlexaDevice::getDeviceJSON() const {
|
||||||
|
|
@ -143,8 +176,8 @@ JsonDocument AlexaDevice::getDeviceJSON() const {
|
||||||
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
|
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
|
||||||
|
|
||||||
// A capability that cannot be announced says so itself; the device is announced with the others
|
// A capability that cannot be announced says so itself; the device is announced with the others
|
||||||
for (const AlexaCapability& capability : capabilities) {
|
for (uint8_t i = 0; i < capabilityCount; ++i) {
|
||||||
capability.toJson(capabilitiesArray, endpointId.c_str());
|
capabilities[i]->toJson(capabilitiesArray, endpointId.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
return json;
|
return json;
|
||||||
|
|
@ -163,16 +196,107 @@ void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const Js
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger the event and invoke the corresponding callback
|
// Trigger the event and invoke the corresponding callback
|
||||||
void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing) const {
|
bool AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing) const {
|
||||||
for (int i = 0; i < MAX_EVENTS; ++i) {
|
for (int i = 0; i < MAX_EVENTS; ++i) {
|
||||||
if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) {
|
if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) {
|
||||||
// Found the event name, call the corresponding callback function
|
// Found the event name, call the corresponding callback function
|
||||||
eventCallbacks[i](directive, type);
|
eventCallbacks[i](directive, type);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// No handler: the directive goes unanswered, which Alexa reports as a device that does not respond
|
|
||||||
if (warnIfMissing) {
|
if (warnIfMissing) {
|
||||||
ALEX2ESP_LOGE("%s: no handler registered for %s, directive not handled", endpointId.c_str(), eventName);
|
ALEX2ESP_LOGE("%s: no handler registered for %s, directive not handled", endpointId.c_str(), eventName);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AlexaDevice::onDirective(AlexaDirectiveHandler handler) {
|
||||||
|
directiveHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AlexaDevice::handleDirective(const JsonDocument& message) {
|
||||||
|
JsonObjectConst header = message["header"];
|
||||||
|
AlexaDirective directive;
|
||||||
|
directive.device = this;
|
||||||
|
directive.ns = header["namespace"] | "";
|
||||||
|
directive.name = header["name"] | "";
|
||||||
|
directive.instance = header["instance"] | "";
|
||||||
|
directive.correlationToken = header["correlationToken"] | "";
|
||||||
|
directive.payload = message["payload"];
|
||||||
|
directive.capability = findCapability(directive.ns, directive.instance);
|
||||||
|
directive.type = directive.capability != nullptr ? directive.capability->getType() : AlexaInterfaceType::UNKNOWN;
|
||||||
|
|
||||||
|
answered = false;
|
||||||
|
triggerEvent("DirectiveReceived", message, AlexaInterfaceType::UNKNOWN, false);
|
||||||
|
bool handled = true;
|
||||||
|
if (directiveHandler != nullptr) {
|
||||||
|
directiveHandler(directive);
|
||||||
|
} else if (directive.isReportState()) {
|
||||||
|
handled = triggerEvent("ReportState", message, AlexaInterfaceType::UNKNOWN, false);
|
||||||
|
} else {
|
||||||
|
// As in 1.x, the type is that of the namespace, whatever the instance
|
||||||
|
handled = triggerEvent("Event", message, getInterfaceType(directive.ns), false);
|
||||||
|
}
|
||||||
|
if (answered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without an answer Alexa waits for the timeout of the backend and then says that the device does not
|
||||||
|
// respond; the ErrorResponse ends the wait
|
||||||
|
PGM_P reason;
|
||||||
|
if (!handled) {
|
||||||
|
ALEX2ESP_LOGE("%s: %s.%s refused as INVALID_DIRECTIVE: the device has no handler, give it one with onDirective()",
|
||||||
|
endpointId.c_str(), directive.ns, directive.name);
|
||||||
|
reason = ERROR_NO_HANDLER;
|
||||||
|
} else if (directive.capability == nullptr && !directive.isReportState()) {
|
||||||
|
ALEX2ESP_LOGE("%s: %s.%s refused as INVALID_DIRECTIVE: the device has no such capability, and its handler sent no answer",
|
||||||
|
endpointId.c_str(), directive.ns, directive.name);
|
||||||
|
reason = ERROR_NO_CAPABILITY;
|
||||||
|
} else {
|
||||||
|
ALEX2ESP_LOGE("%s: the handler sent no answer to %s.%s: Alexa will say that the device does not respond",
|
||||||
|
endpointId.c_str(), directive.ns, directive.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buildStatusMessage(directive.correlationToken, true).asErrorResponse(ERROR_INVALID_DIRECTIVE, reason).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaSendResult AlexaDevice::publish(const char* topic, JsonDocument& doc) {
|
||||||
|
answered = true;
|
||||||
|
return transport->publish(topic, doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AlexaDevice::timestamp(char* buffer, size_t size) {
|
||||||
|
transport->timestamp(buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaDeviceList::~AlexaDeviceList() {
|
||||||
|
while (head != nullptr) {
|
||||||
|
AlexaDevice* device = head;
|
||||||
|
head = device->next;
|
||||||
|
delete device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaDevice* AlexaDeviceList::add(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport) {
|
||||||
|
AlexaDevice* device = new (std::nothrow) AlexaDevice(name, rootTopic, endpointId, transport);
|
||||||
|
if (device == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (tail == nullptr) {
|
||||||
|
head = device;
|
||||||
|
} else {
|
||||||
|
tail->next = device;
|
||||||
|
}
|
||||||
|
tail = device;
|
||||||
|
count++;
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaDevice* AlexaDeviceList::find(const char* id, size_t length) const {
|
||||||
|
for (AlexaDevice* device = head; device != nullptr; device = device->next) {
|
||||||
|
if (device->hasEndpointId(id, length)) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <deque>
|
#include "AlexaLimits.h"
|
||||||
#include "AlexaStatusMessage.h"
|
#include "AlexaStatusMessage.h"
|
||||||
#include "AlexaTransport.h"
|
#include "AlexaTransport.h"
|
||||||
#include "AlexaVersion.h"
|
#include "AlexaVersion.h"
|
||||||
|
|
@ -22,20 +22,53 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class AlexaDevice;
|
||||||
|
|
||||||
|
// A directive as the handler of its device gets it. The texts belong to the directive: they are valid until the
|
||||||
|
// handler returns, so a handler that answers later keeps a copy of the correlationToken.
|
||||||
|
struct AlexaDirective {
|
||||||
|
AlexaDevice* device;
|
||||||
|
AlexaCapability* capability; // nullptr for ReportState, and when the device has no capability for the directive
|
||||||
|
AlexaInterfaceType type; // of the capability, UNKNOWN without one
|
||||||
|
const char* ns; // "Alexa.PowerController"
|
||||||
|
const char* name; // "TurnOn"
|
||||||
|
const char* instance; // "Blind.Lift", "" for an interface without instances
|
||||||
|
const char* correlationToken;
|
||||||
|
JsonObjectConst payload;
|
||||||
|
|
||||||
|
bool is(const char* directiveName) const { return strcmp(name, directiveName) == 0; }
|
||||||
|
bool isReportState() const { return strcmp(ns, "Alexa") == 0 && is("ReportState"); }
|
||||||
|
|
||||||
class AlexaDevice {
|
// The answer to a directive that changed something, and the answer to ReportState: add the properties of the
|
||||||
|
// device and send()
|
||||||
|
AlexaStatusMessage response() const;
|
||||||
|
AlexaStatusMessage stateReport() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One function can serve several devices: the directive says which one it is for
|
||||||
|
typedef void (*AlexaDirectiveHandler)(AlexaDirective& directive);
|
||||||
|
|
||||||
|
// A device is the transport of its own messages: it passes them on to the bridge and so knows whether a handler
|
||||||
|
// answered the directive it was given.
|
||||||
|
class AlexaDevice final : private AlexaTransport {
|
||||||
public:
|
public:
|
||||||
// Devices are created by Alex2ESP::getDevice(), which passes the bridge as the transport that publishes the
|
// Devices are created by Alex2ESP::getDevice(), which passes the bridge as the transport that publishes the
|
||||||
// device's reports. A device built without one can describe itself, but its reports cannot be sent.
|
// device's reports. A device built without one can describe itself, but its reports cannot be sent.
|
||||||
AlexaDevice(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport = nullptr);
|
AlexaDevice(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport = nullptr);
|
||||||
|
~AlexaDevice();
|
||||||
|
|
||||||
|
// It owns its capabilities, and the pointers to it that the sketch holds have to stay valid
|
||||||
|
AlexaDevice(const AlexaDevice&) = delete;
|
||||||
|
AlexaDevice& operator=(const AlexaDevice&) = delete;
|
||||||
|
|
||||||
void setName(const String& name);
|
void setName(const String& name);
|
||||||
String getName() const;
|
String getName() const;
|
||||||
|
|
||||||
String getEndpointId() const;
|
String getEndpointId() const;
|
||||||
|
|
||||||
|
// The device that was created after this one, nullptr for the last
|
||||||
|
AlexaDevice* nextDevice() const { return next; }
|
||||||
|
|
||||||
// Compares the endpoint id with `length` characters at `id` (not NUL-terminated: a part of an MQTT topic)
|
// Compares the endpoint id with `length` characters at `id` (not NUL-terminated: a part of an MQTT topic)
|
||||||
bool hasEndpointId(const char* id, size_t length) const;
|
bool hasEndpointId(const char* id, size_t length) const;
|
||||||
|
|
||||||
|
|
@ -61,8 +94,8 @@ public:
|
||||||
// Returns the capability of this device for the interface, creating it on first use. An interface that a
|
// Returns the capability of this device for the interface, creating it on first use. An interface that a
|
||||||
// device may have several of (RangeController, ModeController, ToggleController) takes the instance that tells
|
// device may have several of (RangeController, ModeController, ToggleController) takes the instance that tells
|
||||||
// them apart, "Blind.Lift"; without one, and with an instance for any other interface, nothing is added, the
|
// them apart, "Blind.Lift"; without one, and with an instance for any other interface, nothing is added, the
|
||||||
// reason is printed and nullptr returned. The pointer stays valid for the lifetime of the device:
|
// reason is printed and nullptr returned. A device holds ALEX2ESP_MAX_CAPABILITIES capabilities; one more is
|
||||||
// capabilities live in a std::deque, which never relocates its elements when another one is added.
|
// refused in the same way. The pointer stays valid for the lifetime of the device.
|
||||||
AlexaCapability* addCapability(const AlexaInterfaceDesc& row, const char* instance = nullptr);
|
AlexaCapability* addCapability(const AlexaInterfaceDesc& row, const char* instance = nullptr);
|
||||||
|
|
||||||
// The same by the type of the interface, as 1.x sketches write it: they call setInstance() on what they get,
|
// The same by the type of the interface, as 1.x sketches write it: they call setInstance() on what they get,
|
||||||
|
|
@ -78,17 +111,38 @@ public:
|
||||||
// UNKNOWN when the device has none
|
// UNKNOWN when the device has none
|
||||||
AlexaInterfaceType getInterfaceType(const char* interfaceName) const;
|
AlexaInterfaceType getInterfaceType(const char* interfaceName) const;
|
||||||
|
|
||||||
|
// The capability a directive with this namespace and instance is for, nullptr when the device has none
|
||||||
|
AlexaCapability* findCapability(const char* ns, const char* instance) const;
|
||||||
|
|
||||||
|
// The handler of every directive for this device, ReportState included. With one, the "ReportState" and
|
||||||
|
// "Event" handlers of registerEvent() are not called.
|
||||||
|
void onDirective(AlexaDirectiveHandler handler);
|
||||||
|
|
||||||
|
// The handlers of 1.x: "ReportState", "Event" for every other directive, and "DirectiveReceived", which is
|
||||||
|
// called before either
|
||||||
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
|
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
|
||||||
|
|
||||||
// warnIfMissing=false keeps optional events (DirectiveReceived) quiet when the sketch did not register them
|
// Returns false when no handler has this name. warnIfMissing=false keeps that quiet.
|
||||||
void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing = true) const;
|
bool triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing = true) const;
|
||||||
|
|
||||||
|
// Hands a directive, {"header": ..., "endpoint": ..., "payload": ...}, to the handler of the device. The
|
||||||
|
// device answers with the ErrorResponse INVALID_DIRECTIVE itself when it has no handler, and when the
|
||||||
|
// directive is for a capability it does not have and the handler sent nothing. A handler that sends nothing
|
||||||
|
// for a capability of the device leaves the directive unanswered; that is printed as an error.
|
||||||
|
void handleDirective(const JsonDocument& message);
|
||||||
|
|
||||||
|
// A device without a transport gets none for its messages: send() says what is wrong
|
||||||
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) {
|
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) {
|
||||||
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse,transport);
|
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse,transport != nullptr ? this : nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class AlexaDeviceList;
|
||||||
|
|
||||||
|
// AlexaTransport, for the messages of this device: notes the answer and passes it on to the bridge
|
||||||
|
AlexaSendResult publish(const char* topic, JsonDocument& doc) override;
|
||||||
|
void timestamp(char* buffer, size_t size) override;
|
||||||
|
|
||||||
// The capability with this row and, when one is given, this instance; added when the device has none
|
// The capability with this row and, when one is given, this instance; added when the device has none
|
||||||
AlexaCapability* capabilityOf(const AlexaInterfaceDesc& row, const char* instance);
|
AlexaCapability* capabilityOf(const AlexaInterfaceDesc& row, const char* instance);
|
||||||
AlexaCapability* refuseCapability(AlexaInterfaceType type);
|
AlexaCapability* refuseCapability(AlexaInterfaceType type);
|
||||||
|
|
@ -97,6 +151,9 @@ private:
|
||||||
String endpointId;
|
String endpointId;
|
||||||
String rootTopic;
|
String rootTopic;
|
||||||
AlexaTransport* transport;
|
AlexaTransport* transport;
|
||||||
|
AlexaDevice* next = nullptr; // in the list of the bridge
|
||||||
|
AlexaDirectiveHandler directiveHandler = nullptr;
|
||||||
|
bool answered = false; // a message has been sent since the handler of the directive was called
|
||||||
const char* eventNames[MAX_EVENTS];
|
const char* eventNames[MAX_EVENTS];
|
||||||
void (*eventCallbacks[MAX_EVENTS])(const JsonDocument&, const AlexaInterfaceType&);
|
void (*eventCallbacks[MAX_EVENTS])(const JsonDocument&, const AlexaInterfaceType&);
|
||||||
|
|
||||||
|
|
@ -107,8 +164,41 @@ private:
|
||||||
String model = "Alexa2MQTT";
|
String model = "Alexa2MQTT";
|
||||||
const String softwareVersion = ALEX2ESP_VERSION;
|
const String softwareVersion = ALEX2ESP_VERSION;
|
||||||
|
|
||||||
std::deque<AlexaCapability> capabilities; // deque: pointers handed out by addCapability stay valid
|
AlexaCapability* capabilities[ALEX2ESP_MAX_CAPABILITIES]; // on the heap, owned
|
||||||
|
uint8_t capabilityCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline AlexaStatusMessage AlexaDirective::response() const {
|
||||||
|
return device->buildStatusMessage(correlationToken, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline AlexaStatusMessage AlexaDirective::stateReport() const {
|
||||||
|
return device->buildStatusMessage(correlationToken, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The devices of a bridge in the order they were created, as a linked list: a device is on the heap from the
|
||||||
|
// moment the sketch asks for it, and a sketch without devices pays for none.
|
||||||
|
class AlexaDeviceList {
|
||||||
|
public:
|
||||||
|
AlexaDeviceList() {}
|
||||||
|
~AlexaDeviceList();
|
||||||
|
|
||||||
|
AlexaDeviceList(const AlexaDeviceList&) = delete;
|
||||||
|
AlexaDeviceList& operator=(const AlexaDeviceList&) = delete;
|
||||||
|
|
||||||
|
// Creates a device at the end of the list. nullptr when the heap has no room for it.
|
||||||
|
AlexaDevice* add(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport);
|
||||||
|
|
||||||
|
// The device with this endpoint id, `length` characters at `id`; nullptr when the list has none
|
||||||
|
AlexaDevice* find(const char* id, size_t length) const;
|
||||||
|
|
||||||
|
AlexaDevice* first() const { return head; }
|
||||||
|
size_t size() const { return count; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
AlexaDevice* head = nullptr;
|
||||||
|
AlexaDevice* tail = nullptr;
|
||||||
|
size_t count = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,12 @@
|
||||||
#define ALEX2ESP_MAX_QUEUED_BYTES (4 * ALEX2ESP_MAX_DIRECTIVE)
|
#define ALEX2ESP_MAX_QUEUED_BYTES (4 * ALEX2ESP_MAX_DIRECTIVE)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// How many capabilities one device holds. Each place takes 4 bytes in every device of the sketch, used or not.
|
||||||
|
// One more than fits is refused with an error. Alexa accepts 100 capabilities for an endpoint.
|
||||||
|
#ifndef ALEX2ESP_MAX_CAPABILITIES
|
||||||
|
#define ALEX2ESP_MAX_CAPABILITIES 8
|
||||||
|
#endif
|
||||||
|
|
||||||
// What one capability holds: the names Alexa calls it by, the actions and the states it maps. Each place takes 8
|
// What one capability holds: the names Alexa calls it by, the actions and the states it maps. Each place takes 8
|
||||||
// or 12 bytes in every capability of the sketch, used or not. One more than fits is refused with an error.
|
// or 12 bytes in every capability of the sketch, used or not. One more than fits is refused with an error.
|
||||||
#ifndef ALEX2ESP_MAX_FRIENDLY_NAMES
|
#ifndef ALEX2ESP_MAX_FRIENDLY_NAMES
|
||||||
|
|
@ -54,6 +60,10 @@
|
||||||
#error "ALEX2ESP_MAX_QUEUED_BYTES has to be ALEX2ESP_MAX_DIRECTIVE or more: the largest directive has to fit the empty queue"
|
#error "ALEX2ESP_MAX_QUEUED_BYTES has to be ALEX2ESP_MAX_DIRECTIVE or more: the largest directive has to fit the empty queue"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if ALEX2ESP_MAX_CAPABILITIES < 1 || ALEX2ESP_MAX_CAPABILITIES > 100
|
||||||
|
#error "ALEX2ESP_MAX_CAPABILITIES has to be between 1 and 100"
|
||||||
|
#endif
|
||||||
|
|
||||||
#if ALEX2ESP_MAX_FRIENDLY_NAMES < 1 || ALEX2ESP_MAX_FRIENDLY_NAMES > 255 || ALEX2ESP_MAX_ACTION_MAPPINGS < 1 || \
|
#if ALEX2ESP_MAX_FRIENDLY_NAMES < 1 || ALEX2ESP_MAX_FRIENDLY_NAMES > 255 || ALEX2ESP_MAX_ACTION_MAPPINGS < 1 || \
|
||||||
ALEX2ESP_MAX_ACTION_MAPPINGS > 255 || ALEX2ESP_MAX_STATE_MAPPINGS < 1 || ALEX2ESP_MAX_STATE_MAPPINGS > 255
|
ALEX2ESP_MAX_ACTION_MAPPINGS > 255 || ALEX2ESP_MAX_STATE_MAPPINGS < 1 || ALEX2ESP_MAX_STATE_MAPPINGS > 255
|
||||||
#error "ALEX2ESP_MAX_FRIENDLY_NAMES, ALEX2ESP_MAX_ACTION_MAPPINGS and ALEX2ESP_MAX_STATE_MAPPINGS have to be between 1 and 255"
|
#error "ALEX2ESP_MAX_FRIENDLY_NAMES, ALEX2ESP_MAX_ACTION_MAPPINGS and ALEX2ESP_MAX_STATE_MAPPINGS have to be between 1 and 255"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "AlexaStatusMessage.h"
|
#include "AlexaStatusMessage.h"
|
||||||
#include "AlexaLog.h"
|
#include "AlexaLog.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
// What a 1.x sketch wrote where the time belongs when it built a property by hand: the backend's HTTP route
|
// What a 1.x sketch wrote where the time belongs when it built a property by hand: the backend's HTTP route
|
||||||
// replaced it. Reports leave over MQTT now, where nothing rewrites them, so the library fills the time in.
|
// replaced it. Reports leave over MQTT now, where nothing rewrites them, so the library fills the time in.
|
||||||
|
|
@ -44,6 +46,17 @@ AlexaStatusMessage &AlexaStatusMessage::AddContextProp(const JsonObject &propert
|
||||||
return *this; // a property that did not fit leaves the document marked as overflowed: send() refuses it
|
return *this; // a property that did not fit leaves the document marked as overflowed: send() refuses it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::asErrorResponse(PGM_P type, PGM_P message)
|
||||||
|
{
|
||||||
|
JsonObject event = doc["event"];
|
||||||
|
event["header"]["name"] = F("ErrorResponse");
|
||||||
|
event["payload"][F("type")] = FPSTR(type);
|
||||||
|
event["payload"][F("message")] = FPSTR(message);
|
||||||
|
doc.remove("context");
|
||||||
|
contextProperties = JsonArray(); // the array went with the context: what is added now goes nowhere
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
bool AlexaStatusMessage::send()
|
bool AlexaStatusMessage::send()
|
||||||
{
|
{
|
||||||
if (transport == nullptr)
|
if (transport == nullptr)
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,11 @@ public:
|
||||||
// none or still carries the 1.x placeholder "{REPLACE_WITH_DATETIME}".
|
// none or still carries the 1.x placeholder "{REPLACE_WITH_DATETIME}".
|
||||||
AlexaStatusMessage &AddContextProp(const JsonObject &property);
|
AlexaStatusMessage &AddContextProp(const JsonObject &property);
|
||||||
|
|
||||||
|
// Turns the message into the ErrorResponse of alexa-errorresponse.html, which has no context: properties
|
||||||
|
// added before or after are not sent. The type is one of that page, PSTR("INVALID_DIRECTIVE"); the message
|
||||||
|
// is for the log of the skill, Alexa does not read it to the user. Both may be in program memory.
|
||||||
|
AlexaStatusMessage &asErrorResponse(PGM_P type, PGM_P message);
|
||||||
|
|
||||||
// Publishes the report on <root>/<endpointId>/alexaResponce now. Returns false when nothing was sent: no
|
// Publishes the report on <root>/<endpointId>/alexaResponce now. Returns false when nothing was sent: no
|
||||||
// session with the broker, the MQTT client or the heap cannot take the report, or it is over
|
// session with the broker, the MQTT client or the heap cannot take the report, or it is over
|
||||||
// ALEX2ESP_MAX_MESSAGE bytes. The reason is on Serial; a report is never sent truncated.
|
// ALEX2ESP_MAX_MESSAGE bytes. The reason is on Serial; a report is never sent truncated.
|
||||||
|
|
|
||||||
438
test/test_dispatch/test_main.cpp
Normal file
438
test/test_dispatch/test_main.cpp
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
// Host tests of dispatch: which device and which capability a directive reaches, what the handler is given, what
|
||||||
|
// a device holds, and what the device answers itself (src/AlexaDevice.cpp). The transport of the devices is a
|
||||||
|
// fake that keeps what would have been published.
|
||||||
|
// pio test -e native
|
||||||
|
#include <unity.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "AlexaDevice.h"
|
||||||
|
#include "AlexaLog.h"
|
||||||
|
|
||||||
|
class FakeTransport : public AlexaTransport
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Message
|
||||||
|
{
|
||||||
|
std::string topic;
|
||||||
|
std::string json;
|
||||||
|
};
|
||||||
|
std::vector<Message> sent;
|
||||||
|
|
||||||
|
AlexaSendResult publish(const char *topic, JsonDocument &doc) override
|
||||||
|
{
|
||||||
|
Message message;
|
||||||
|
message.topic = topic;
|
||||||
|
serializeJson(doc, message.json);
|
||||||
|
sent.push_back(message);
|
||||||
|
return AlexaSendResult::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void timestamp(char *buffer, size_t size) override { snprintf(buffer, size, "2026-09-28T13:05:09Z"); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class CapturedLog : public Print
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::string text;
|
||||||
|
|
||||||
|
size_t write(const uint8_t *buffer, size_t size) override
|
||||||
|
{
|
||||||
|
text.append(reinterpret_cast<const char *>(buffer), size);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void assertLogged(const CapturedLog &log, const char *expected)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(log.text.c_str(), expected), log.text.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the handler of the tests was given, copied: the texts of a directive end with the handler
|
||||||
|
struct Seen
|
||||||
|
{
|
||||||
|
int calls;
|
||||||
|
AlexaDevice *device;
|
||||||
|
AlexaCapability *capability;
|
||||||
|
AlexaInterfaceType type;
|
||||||
|
std::string ns;
|
||||||
|
std::string name;
|
||||||
|
std::string instance;
|
||||||
|
std::string correlationToken;
|
||||||
|
int rangeValue;
|
||||||
|
bool reportState;
|
||||||
|
};
|
||||||
|
|
||||||
|
static Seen seen;
|
||||||
|
static bool handlerAnswers;
|
||||||
|
|
||||||
|
static void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
seen.calls++;
|
||||||
|
seen.device = directive.device;
|
||||||
|
seen.capability = directive.capability;
|
||||||
|
seen.type = directive.type;
|
||||||
|
seen.ns = directive.ns;
|
||||||
|
seen.name = directive.name;
|
||||||
|
seen.instance = directive.instance;
|
||||||
|
seen.correlationToken = directive.correlationToken;
|
||||||
|
seen.rangeValue = directive.payload["rangeValue"] | -1;
|
||||||
|
seen.reportState = directive.isReportState();
|
||||||
|
if (!handlerAnswers)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
directive.stateReport().AddPowerControllerProp(PowerController::OFF).send();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
directive.response().AddPowerControllerProp(directive.is("TurnOn") ? PowerController::ON : PowerController::OFF).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int eventCalls;
|
||||||
|
static int reportStateCalls;
|
||||||
|
static AlexaInterfaceType eventType;
|
||||||
|
|
||||||
|
static void onEvent(const JsonDocument &, const AlexaInterfaceType &type)
|
||||||
|
{
|
||||||
|
eventCalls++;
|
||||||
|
eventType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onReportState(const JsonDocument &, const AlexaInterfaceType &)
|
||||||
|
{
|
||||||
|
reportStateCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The directive as Alex2MQTT publishes it on <root>/<endpointId>/alexaDirective
|
||||||
|
static void directiveFor(JsonDocument &message, const char *endpointId, const char *ns, const char *name,
|
||||||
|
const char *instance = nullptr)
|
||||||
|
{
|
||||||
|
message.clear();
|
||||||
|
message["header"]["namespace"] = ns;
|
||||||
|
message["header"]["name"] = name;
|
||||||
|
message["header"]["payloadVersion"] = "3";
|
||||||
|
message["header"]["messageId"] = "5f8a426e-01e4-4cc9-8b79-65f8bd0fd8a4";
|
||||||
|
message["header"]["correlationToken"] = "token-of-the-directive";
|
||||||
|
if (instance != nullptr)
|
||||||
|
{
|
||||||
|
message["header"]["instance"] = instance;
|
||||||
|
}
|
||||||
|
message["endpoint"]["endpointId"] = endpointId;
|
||||||
|
message["payload"].to<JsonObject>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assertErrorResponse(const FakeTransport::Message &message, const char *topic)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_STRING(topic, message.topic.c_str());
|
||||||
|
JsonDocument answer;
|
||||||
|
TEST_ASSERT_TRUE(deserializeJson(answer, message.json) == DeserializationError::Ok);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa", answer["event"]["header"]["namespace"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ErrorResponse", answer["event"]["header"]["name"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("token-of-the-directive", answer["event"]["header"]["correlationToken"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("INVALID_DIRECTIVE", answer["event"]["payload"]["type"] | "");
|
||||||
|
TEST_ASSERT_TRUE(strlen(answer["event"]["payload"]["message"] | "") > 0);
|
||||||
|
TEST_ASSERT_TRUE(answer["context"].isNull()); // alexa-errorresponse.html: an ErrorResponse has no context
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
AlexaLog::setOutput(nullptr);
|
||||||
|
seen = Seen();
|
||||||
|
handlerAnswers = true;
|
||||||
|
eventCalls = 0;
|
||||||
|
reportStateCalls = 0;
|
||||||
|
eventType = AlexaInterfaceType::UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
void test_devices_are_found_by_their_endpoint_id_in_the_order_they_were_created(void)
|
||||||
|
{
|
||||||
|
AlexaDeviceList devices;
|
||||||
|
TEST_ASSERT_NULL(devices.first());
|
||||||
|
AlexaDevice *lamp = devices.add("Lamp", "root", "ESP-01", nullptr);
|
||||||
|
AlexaDevice *fan = devices.add("Fan", "root", "ESP-02", nullptr);
|
||||||
|
AlexaDevice *blind = devices.add("Blind", "root", "ESP-03", nullptr);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(3, devices.size());
|
||||||
|
TEST_ASSERT_EQUAL_PTR(lamp, devices.first());
|
||||||
|
TEST_ASSERT_EQUAL_PTR(fan, lamp->nextDevice());
|
||||||
|
TEST_ASSERT_EQUAL_PTR(blind, fan->nextDevice());
|
||||||
|
TEST_ASSERT_NULL(blind->nextDevice());
|
||||||
|
TEST_ASSERT_EQUAL_PTR(fan, devices.find("ESP-02", 6));
|
||||||
|
// The id is a part of a topic: "ESP-02/alexaDirective"
|
||||||
|
TEST_ASSERT_EQUAL_PTR(fan, devices.find("ESP-02/alexaDirective", 6));
|
||||||
|
// An endpoint of another board
|
||||||
|
TEST_ASSERT_NULL(devices.find("ESP-04", 6));
|
||||||
|
TEST_ASSERT_NULL(devices.find("ESP-0", 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_one_handler_serves_two_devices_and_is_told_which(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDeviceList devices;
|
||||||
|
AlexaDevice *lamp = devices.add("Lamp", "root", "ESP-01", &transport);
|
||||||
|
AlexaDevice *fan = devices.add("Fan", "root", "ESP-02", &transport);
|
||||||
|
lamp->addCapability(AlexaInterfaces::PowerController);
|
||||||
|
AlexaCapability *fanPower = fan->addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp->onDirective(onDirective);
|
||||||
|
fan->onDirective(onDirective);
|
||||||
|
|
||||||
|
JsonDocument message;
|
||||||
|
directiveFor(message, "ESP-02", "Alexa.PowerController", "TurnOn");
|
||||||
|
devices.find("ESP-02", 6)->handleDirective(message);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(1, seen.calls);
|
||||||
|
TEST_ASSERT_EQUAL_PTR(fan, seen.device);
|
||||||
|
TEST_ASSERT_EQUAL_PTR(fanPower, seen.capability);
|
||||||
|
TEST_ASSERT_TRUE(AlexaInterfaceType::POWER_CONTROLLER == seen.type);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.PowerController", seen.ns.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("TurnOn", seen.name.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", seen.instance.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("token-of-the-directive", seen.correlationToken.c_str());
|
||||||
|
TEST_ASSERT_FALSE(seen.reportState);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(1, transport.sent.size());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("root/ESP-02/alexaResponce", transport.sent[0].topic.c_str());
|
||||||
|
JsonDocument answer;
|
||||||
|
TEST_ASSERT_TRUE(deserializeJson(answer, transport.sent[0].json) == DeserializationError::Ok);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Response", answer["event"]["header"]["name"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("token-of-the-directive", answer["event"]["header"]["correlationToken"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ESP-02", answer["event"]["endpoint"]["endpointId"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ON", answer["context"]["properties"][0]["value"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("2026-09-28T13:05:09Z", answer["context"]["properties"][0]["timeOfSample"] | "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_directive_with_an_instance_reaches_the_capability_with_that_instance(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice blind("Blind", "root", "ESP-01", &transport);
|
||||||
|
AlexaCapability *lift = blind.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
||||||
|
AlexaCapability *tilt = blind.addCapability(AlexaInterfaces::RangeController, "Blind.Tilt");
|
||||||
|
blind.onDirective(onDirective);
|
||||||
|
|
||||||
|
JsonDocument message;
|
||||||
|
directiveFor(message, "ESP-01", "Alexa.RangeController", "SetRangeValue", "Blind.Tilt");
|
||||||
|
message["payload"]["rangeValue"] = 40;
|
||||||
|
blind.handleDirective(message);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_PTR(tilt, seen.capability);
|
||||||
|
TEST_ASSERT_TRUE(AlexaInterfaceType::RANGE_CONTROLLER == seen.type);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Blind.Tilt", seen.instance.c_str());
|
||||||
|
TEST_ASSERT_EQUAL(40, seen.rangeValue);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_PTR(lift, blind.findCapability("Alexa.RangeController", "Blind.Lift"));
|
||||||
|
TEST_ASSERT_NULL(blind.findCapability("Alexa.RangeController", "Blind.Slat"));
|
||||||
|
TEST_ASSERT_NULL(blind.findCapability("Alexa.RangeController", ""));
|
||||||
|
TEST_ASSERT_NULL(blind.findCapability("Alexa.ModeController", "Blind.Lift"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_report_state_is_handed_over_without_a_capability_and_answered_with_a_state_report(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp.onDirective(onDirective);
|
||||||
|
|
||||||
|
JsonDocument message;
|
||||||
|
directiveFor(message, "ESP-01", "Alexa", "ReportState");
|
||||||
|
lamp.handleDirective(message);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(seen.reportState);
|
||||||
|
TEST_ASSERT_NULL(seen.capability);
|
||||||
|
TEST_ASSERT_EQUAL(1, transport.sent.size());
|
||||||
|
JsonDocument answer;
|
||||||
|
TEST_ASSERT_TRUE(deserializeJson(answer, transport.sent[0].json) == DeserializationError::Ok);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("StateReport", answer["event"]["header"]["name"] | "");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("OFF", answer["context"]["properties"][0]["value"] | "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_one_capability_more_than_a_device_holds_is_refused_with_an_error(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaDevice device("Panel", "root", "ESP-01");
|
||||||
|
AlexaCapability *first = nullptr;
|
||||||
|
for (int i = 0; i < ALEX2ESP_MAX_CAPABILITIES; i++)
|
||||||
|
{
|
||||||
|
char instance[16];
|
||||||
|
snprintf(instance, sizeof(instance), "Panel.%d", i);
|
||||||
|
AlexaCapability *added = device.addCapability(AlexaInterfaces::ToggleController, instance);
|
||||||
|
TEST_ASSERT_NOT_NULL(added);
|
||||||
|
added->addFriendlyName("Switch", "en-US");
|
||||||
|
if (i == 0)
|
||||||
|
{
|
||||||
|
first = added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string full;
|
||||||
|
serializeJson(device.getDeviceJSON(), full);
|
||||||
|
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaces::PowerController));
|
||||||
|
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER));
|
||||||
|
|
||||||
|
char expected[160];
|
||||||
|
snprintf(expected, sizeof(expected),
|
||||||
|
"error: ESP-01: capability Alexa.PowerController not added: the device has %d already "
|
||||||
|
"(ALEX2ESP_MAX_CAPABILITIES)",
|
||||||
|
ALEX2ESP_MAX_CAPABILITIES);
|
||||||
|
assertLogged(log, expected);
|
||||||
|
assertLogged(log, "error: ESP-01: capability Alexa.BrightnessController not added");
|
||||||
|
|
||||||
|
// What it has is still found and announced
|
||||||
|
TEST_ASSERT_EQUAL_PTR(first, device.addCapability(AlexaInterfaces::ToggleController, "Panel.0"));
|
||||||
|
TEST_ASSERT_NULL(device.findCapability("Alexa.PowerController", ""));
|
||||||
|
std::string announced;
|
||||||
|
serializeJson(device.getDeviceJSON(), announced);
|
||||||
|
TEST_ASSERT_EQUAL_STRING(full.c_str(), announced.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_directive_for_an_interface_the_device_lacks_is_answered_with_invalid_directive(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp.onDirective(onDirective);
|
||||||
|
handlerAnswers = false; // as a handler that knows its own interfaces and leaves the others alone
|
||||||
|
|
||||||
|
JsonDocument message;
|
||||||
|
directiveFor(message, "ESP-01", "Alexa.LockController", "Lock");
|
||||||
|
lamp.handleDirective(message);
|
||||||
|
|
||||||
|
// The handler is asked first: it sees that the device has no such capability
|
||||||
|
TEST_ASSERT_EQUAL(1, seen.calls);
|
||||||
|
TEST_ASSERT_NULL(seen.capability);
|
||||||
|
TEST_ASSERT_TRUE(AlexaInterfaceType::UNKNOWN == seen.type);
|
||||||
|
TEST_ASSERT_EQUAL(1, transport.sent.size());
|
||||||
|
assertErrorResponse(transport.sent[0], "root/ESP-01/alexaResponce");
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.LockController.Lock refused as INVALID_DIRECTIVE: "
|
||||||
|
"the device has no such capability");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_instance_the_device_lacks_is_answered_with_invalid_directive(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice blind("Blind", "root", "ESP-01", &transport);
|
||||||
|
blind.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
||||||
|
blind.onDirective(onDirective);
|
||||||
|
handlerAnswers = false;
|
||||||
|
|
||||||
|
JsonDocument message;
|
||||||
|
directiveFor(message, "ESP-01", "Alexa.RangeController", "SetRangeValue", "Blind.Tilt");
|
||||||
|
blind.handleDirective(message);
|
||||||
|
|
||||||
|
TEST_ASSERT_NULL(seen.capability);
|
||||||
|
TEST_ASSERT_EQUAL(1, transport.sent.size());
|
||||||
|
assertErrorResponse(transport.sent[0], "root/ESP-01/alexaResponce");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_answer_of_the_handler_to_an_interface_the_device_lacks_is_the_only_answer(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp.onDirective(onDirective);
|
||||||
|
|
||||||
|
JsonDocument message;
|
||||||
|
directiveFor(message, "ESP-01", "Alexa.LockController", "Lock");
|
||||||
|
lamp.handleDirective(message);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(1, transport.sent.size());
|
||||||
|
JsonDocument answer;
|
||||||
|
TEST_ASSERT_TRUE(deserializeJson(answer, transport.sent[0].json) == DeserializationError::Ok);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Response", answer["event"]["header"]["name"] | "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_device_without_a_handler_answers_with_invalid_directive(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
|
||||||
|
JsonDocument message;
|
||||||
|
directiveFor(message, "ESP-01", "Alexa.PowerController", "TurnOn");
|
||||||
|
lamp.handleDirective(message);
|
||||||
|
directiveFor(message, "ESP-01", "Alexa", "ReportState");
|
||||||
|
lamp.handleDirective(message);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(2, transport.sent.size());
|
||||||
|
assertErrorResponse(transport.sent[0], "root/ESP-01/alexaResponce");
|
||||||
|
assertErrorResponse(transport.sent[1], "root/ESP-01/alexaResponce");
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.PowerController.TurnOn refused as INVALID_DIRECTIVE: "
|
||||||
|
"the device has no handler, give it one with onDirective()");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_handler_that_sends_nothing_for_a_capability_of_the_device_is_reported(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp.onDirective(onDirective);
|
||||||
|
handlerAnswers = false;
|
||||||
|
|
||||||
|
JsonDocument message;
|
||||||
|
directiveFor(message, "ESP-01", "Alexa.PowerController", "TurnOn");
|
||||||
|
lamp.handleDirective(message);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(1, seen.calls);
|
||||||
|
TEST_ASSERT_EQUAL(0, transport.sent.size());
|
||||||
|
assertLogged(log, "error: ESP-01: the handler sent no answer to Alexa.PowerController.TurnOn");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_handlers_of_1x_get_report_state_and_the_type_of_every_other_directive(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice blind("Blind", "root", "ESP-01", &transport);
|
||||||
|
blind.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
|
||||||
|
blind.addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER)->setInstance("ESP-01.Toggle");
|
||||||
|
blind.registerEvent("ReportState", onReportState);
|
||||||
|
blind.registerEvent("Event", onEvent);
|
||||||
|
|
||||||
|
JsonDocument message;
|
||||||
|
directiveFor(message, "ESP-01", "Alexa.ToggleController", "TurnOn", "ESP-01.Toggle");
|
||||||
|
blind.handleDirective(message);
|
||||||
|
TEST_ASSERT_EQUAL(1, eventCalls);
|
||||||
|
TEST_ASSERT_EQUAL(0, reportStateCalls);
|
||||||
|
TEST_ASSERT_TRUE(AlexaInterfaceType::TOGGLE_CONTROLLER == eventType);
|
||||||
|
|
||||||
|
directiveFor(message, "ESP-01", "Alexa", "ReportState");
|
||||||
|
blind.handleDirective(message);
|
||||||
|
TEST_ASSERT_EQUAL(1, eventCalls);
|
||||||
|
TEST_ASSERT_EQUAL(1, reportStateCalls);
|
||||||
|
|
||||||
|
// With a handler of 2.0 the device calls that one
|
||||||
|
blind.onDirective(onDirective);
|
||||||
|
directiveFor(message, "ESP-01", "Alexa.PowerController", "TurnOff");
|
||||||
|
blind.handleDirective(message);
|
||||||
|
TEST_ASSERT_EQUAL(1, eventCalls);
|
||||||
|
TEST_ASSERT_EQUAL(1, seen.calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int, char **)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_devices_are_found_by_their_endpoint_id_in_the_order_they_were_created);
|
||||||
|
RUN_TEST(test_one_handler_serves_two_devices_and_is_told_which);
|
||||||
|
RUN_TEST(test_directive_with_an_instance_reaches_the_capability_with_that_instance);
|
||||||
|
RUN_TEST(test_report_state_is_handed_over_without_a_capability_and_answered_with_a_state_report);
|
||||||
|
RUN_TEST(test_one_capability_more_than_a_device_holds_is_refused_with_an_error);
|
||||||
|
RUN_TEST(test_directive_for_an_interface_the_device_lacks_is_answered_with_invalid_directive);
|
||||||
|
RUN_TEST(test_instance_the_device_lacks_is_answered_with_invalid_directive);
|
||||||
|
RUN_TEST(test_answer_of_the_handler_to_an_interface_the_device_lacks_is_the_only_answer);
|
||||||
|
RUN_TEST(test_device_without_a_handler_answers_with_invalid_directive);
|
||||||
|
RUN_TEST(test_handler_that_sends_nothing_for_a_capability_of_the_device_is_reported);
|
||||||
|
RUN_TEST(test_handlers_of_1x_get_report_state_and_the_type_of_every_other_directive);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue