AlexaCapability: instance, asset and text names, configuration, action and state mappings, nonControllable
AlexaCapability replaces the header-only AlexaInterface as what a device holds: 116 bytes with fixed places for
3 names, 4 action mappings and 2 state mappings, no std::vector or std::string. AlexaInterface and AlexaActions
stay as 1.x names; the five examples compile unchanged and announce the same bytes.
addCapability(row, instance) adds several capabilities of one generic controller. An instanced interface without
an instance or a name, an instance or semantics on an interface that takes none, and a full store are refused
with an ERROR line that says what to change.
PlaybackController and WakeOnLANController announce "properties": {} (AIF_EMPTY_PROPERTIES).
basicLight on a D1 mini: static RAM 30,788 -> 30,672 B, flash 330,509 -> 332,541 B; host tests 61 -> 77.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
8b11cc2394
commit
7afc4346e8
12 changed files with 1084 additions and 253 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
; 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) and of discovery
|
||||||
; (src/AlexaDevice.cpp, src/AlexaInterfaces.cpp); no board, no broker
|
; (src/AlexaDevice.cpp, src/AlexaCapability.cpp, src/AlexaInterfaces.cpp); no board, no broker
|
||||||
|
|
||||||
[platformio]
|
[platformio]
|
||||||
default_envs = native
|
default_envs = native
|
||||||
|
|
@ -12,7 +12,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> +<AlexaDevice.cpp> +<AlexaInterfaces.cpp> +<AlexaLog.cpp>
|
build_src_filter = -<*> +<AlexaBridgeLogic.cpp> +<AlexaCapability.cpp> +<AlexaDevice.cpp> +<AlexaInterfaces.cpp> +<AlexaLog.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
|
||||||
|
|
|
||||||
40
readme.md
40
readme.md
|
|
@ -131,6 +131,31 @@ toggleController->addActionMapping(openMapping);
|
||||||
|
|
||||||
An `ActionMapping` takes an optional third argument, the directive payload as JSON text (for example `"{\"rangeValue\": 0}"` for a RangeController). It is emitted as an object in the discovery response and omitted when empty.
|
An `ActionMapping` takes an optional third argument, the directive payload as JSON text (for example `"{\"rangeValue\": 0}"` for a RangeController). It is emitted as an object in the discovery response and omitted when empty.
|
||||||
|
|
||||||
|
### Example: a blind with a position
|
||||||
|
|
||||||
|
`RangeController`, `ModeController` and `ToggleController` are generic controllers: a device may have several of one interface, each with an instance and at least one name. `addCapability()` takes the row and the instance and returns an `AlexaCapability`; its setters return the capability, so they can be chained.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void fillLift(JsonObject configuration, void*) {
|
||||||
|
JsonObject range = configuration["supportedRange"].to<JsonObject>();
|
||||||
|
range["minimumValue"] = 0;
|
||||||
|
range["maximumValue"] = 100;
|
||||||
|
range["precision"] = 1;
|
||||||
|
configuration["unitOfMeasure"] = "Alexa.Unit.Percent";
|
||||||
|
}
|
||||||
|
|
||||||
|
device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
|
||||||
|
->addFriendlyAsset(PSTR("Alexa.Setting.Opening"))
|
||||||
|
.addFriendlyName("Lift", "en-US")
|
||||||
|
.setConfiguration(fillLift)
|
||||||
|
.addActionMapping(ActionMapping({AlexaAction::Close, AlexaAction::Lower}, "SetRangeValue", "{\"rangeValue\":0}"))
|
||||||
|
.addActionMapping(ActionMapping({AlexaAction::Open, AlexaAction::Raise}, "SetRangeValue", "{\"rangeValue\":100}"))
|
||||||
|
.addStateMapping({AlexaState::Closed}, 0.0f)
|
||||||
|
.addStateMapping({AlexaState::Open}, 1.0f, 100.0f);
|
||||||
|
```
|
||||||
|
|
||||||
|
`addCapability()` returns `nullptr` for a generic controller without an instance: check the pointer when the instance is not a literal. A capability holds 3 names, 4 action mappings and 2 state mappings (`-DALEX2ESP_MAX_FRIENDLY_NAMES`, `-DALEX2ESP_MAX_ACTION_MAPPINGS`, `-DALEX2ESP_MAX_STATE_MAPPINGS` in `build_flags` change that). The instance and the text of a friendly name are copied. The locale, an asset id, the text value of a state mapping and the directive name and payload of an `ActionMapping` are kept as pointers: pass literals. `setNonControllable(true)` announces a capability that reports its state and takes no directives.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How it talks to Alex2MQTT
|
## How it talks to Alex2MQTT
|
||||||
|
|
@ -176,14 +201,14 @@ The library describes these interfaces. `addCapability()` takes the row, `AlexaI
|
||||||
| `InputController` | `INPUT_CONTROLLER` | 3 | input | `AddContextProp` |
|
| `InputController` | `INPUT_CONTROLLER` | 3 | input | `AddContextProp` |
|
||||||
| `ChannelController` | `CHANNEL_CONTROLLER` | 3 | channel | `AddContextProp` |
|
| `ChannelController` | `CHANNEL_CONTROLLER` | 3 | channel | `AddContextProp` |
|
||||||
| `InventoryLevelSensor` | `INVENTORY_LEVEL_SENSOR` | 3 | level | `AddContextProp` |
|
| `InventoryLevelSensor` | `INVENTORY_LEVEL_SENSOR` | 3 | level | `AddContextProp` |
|
||||||
| `PlaybackController` | `PLAYBACK_CONTROLLER` | 3 | none | |
|
| `PlaybackController` | `PLAYBACK_CONTROLLER` | 3 | `"properties": {}` | |
|
||||||
| `WakeOnLANController` | `WAKE_ON_LAN_CONTROLLER` | 3 | none | |
|
| `WakeOnLANController` | `WAKE_ON_LAN_CONTROLLER` | 3 | `"properties": {}` | |
|
||||||
| `SceneController` | `SCENE_CONTROLLER` | 3 | no `properties` object | |
|
| `SceneController` | `SCENE_CONTROLLER` | 3 | no `properties` object | |
|
||||||
| `DoorbellEventSource` | `DOORBELL_EVENT_SOURCE` | 3 | no `properties` object | |
|
| `DoorbellEventSource` | `DOORBELL_EVENT_SOURCE` | 3 | no `properties` object | |
|
||||||
| `StepSpeaker` | `STEP_SPEAKER` | 3 | no `properties` object | |
|
| `StepSpeaker` | `STEP_SPEAKER` | 3 | no `properties` object | |
|
||||||
| `SimpleEventSource` | `SIMPLE_EVENT_SOURCE` | 1.0 | no `properties` object | |
|
| `SimpleEventSource` | `SIMPLE_EVENT_SOURCE` | 1.0 | no `properties` object | |
|
||||||
|
|
||||||
The interfaces below the first six are announced with their version and properties; what else their discovery object needs (the configuration of a thermostat, the presets of a range) and their events are not built by the library yet.
|
The interfaces below the first six are announced with their version and properties; what else their discovery object needs (the configuration of a thermostat, the presets of a range) the sketch writes in the function it passes to `setConfiguration()`. Their events are not built by the library yet.
|
||||||
|
|
||||||
Every other value of `AlexaInterfaceType` (`COOKING`, `LAUNCHER`, `SECURITY_PANEL_CONTROLLER`, ...) names an interface the library has no row for. `addCapability()` adds nothing for it, prints `[Alex2ESP] error: <endpointId>: capability not added: ...` and returns `nullptr`.
|
Every other value of `AlexaInterfaceType` (`COOKING`, `LAUNCHER`, `SECURITY_PANEL_CONTROLLER`, ...) names an interface the library has no row for. `addCapability()` adds nothing for it, prints `[Alex2ESP] error: <endpointId>: capability not added: ...` and returns `nullptr`.
|
||||||
|
|
||||||
|
|
@ -234,10 +259,15 @@ Behaviour changes:
|
||||||
- `addCapability()` with a type that has no row adds nothing, prints an error and returns `nullptr`. 1.1.0 announced such an interface as version 1 without properties.
|
- `addCapability()` with a type that has no row adds nothing, prints an error and returns `nullptr`. 1.1.0 announced such an interface as version 1 without properties.
|
||||||
- The handler of `Event` gets the type of the capability of its device that the directive names, and `AlexaInterfaceType::UNKNOWN` for a namespace the device has no capability for. 1.1.0 looked the namespace up among all interfaces.
|
- 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.
|
||||||
|
- A capability takes 116 bytes of heap and holds 3 friendly names, 4 action mappings and 2 state mappings (`ALEX2ESP_MAX_FRIENDLY_NAMES`, `ALEX2ESP_MAX_ACTION_MAPPINGS`, `ALEX2ESP_MAX_STATE_MAPPINGS`); one more is refused with an error. The instance and the text of a friendly name are copied, as before. The locale and the directive name and payload of an `ActionMapping` are kept as pointers, where 1.1.0 copied them: pass literals. An `ActionMapping` announces its actions in the order of `AlexaAction`.
|
||||||
|
- What Alexa does not accept is refused with an error that says what to change. `addCapability(row)` for an interface with instances and no instance, or with an instance for an interface without, adds nothing and returns `nullptr`. A capability with instances that has no instance or no friendly name when the device is announced (a 1.x sketch sets both after `addCapability(type)`) is left out of discovery. Action and state mappings are taken by `RangeController`, `ModeController` and `ToggleController` only.
|
||||||
|
- `PlaybackController` and `WakeOnLANController` are announced with `"properties": {}`, as their pages show them.
|
||||||
|
- Removed: `AlexaInterface::getJSON()` (`toJson()` adds the capability to the capabilities of its endpoint) and `getProps()` (the row has the properties), `ActionMapping::getJSON()` and the `String` and `std::vector` members of `ActionMapping`, the class `FriendlyName`.
|
||||||
|
|
||||||
Memory: `examples/basicLight.cpp` for a D1 mini takes 30,788 bytes of static RAM (1.1.0: 52,768) and 330,509 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). 18,260 bytes of the static RAM were the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients; 3,720 were the names, versions and properties of all interfaces and the names of the display categories, which are in flash now. SNTP and the time stamp are 1.8 KB of the flash figure.
|
Memory: `examples/basicLight.cpp` for a D1 mini takes 30,672 bytes of static RAM (1.1.0: 52,768) and 332,541 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). 18,260 bytes of the static RAM were the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients; 3,720 were the names, versions and properties of all interfaces and the names of the display categories, which are in flash now. SNTP and the time stamp are 1.8 KB of the flash figure. The instance, names, configuration and mappings of a capability are 2.0 KB of it.
|
||||||
|
|
||||||
Tests: `pio test -e native` in the repository runs 61 host tests: 47 of the receive and publish logic (reassembly of fragments, the directives that wait for `loop()`, repeated directives, the size limits, a heap without room, the wait between reconnects, topics, time stamps) and 14 of discovery (the discovery object of every example against what 1.1.0 announced, the 28 rows against the interface pages, a type without a row). No board is needed.
|
Tests: `pio test -e native` in the repository runs 77 host tests: 47 of the receive and publish logic (reassembly of fragments, the directives that wait for `loop()`, repeated directives, the size limits, a heap without room, the wait between reconnects, topics, time stamps) and 30 of discovery (the discovery object of every example against what 1.1.0 announced, the 28 rows against the interface pages, a type without a row, the discovery objects of a range, a mode and a toggle controller, a scene and a doorbell, what a capability refuses). No board is needed.
|
||||||
|
|
||||||
Packaging: `library.json`, `library.properties` and the `softwareVersion` and `firmwareVersion` that a device reports in discovery say 1.2.0; the library has the number in one place, `ALEX2ESP_VERSION` in `src/AlexaVersion.h`. The `platformio.ini` of the repository is for the host tests; a sketch does not need it.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
436
src/AlexaCapability.cpp
Normal file
436
src/AlexaCapability.cpp
Normal file
|
|
@ -0,0 +1,436 @@
|
||||||
|
#include "AlexaCapability.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "AlexaLog.h"
|
||||||
|
|
||||||
|
// The names are looked up by a value that is only known when the sketch runs, so both tables are always linked
|
||||||
|
static const char ACTION_OPEN[] PROGMEM = "Alexa.Actions.Open";
|
||||||
|
static const char ACTION_CLOSE[] PROGMEM = "Alexa.Actions.Close";
|
||||||
|
static const char ACTION_RAISE[] PROGMEM = "Alexa.Actions.Raise";
|
||||||
|
static const char ACTION_LOWER[] PROGMEM = "Alexa.Actions.Lower";
|
||||||
|
static const char ACTION_SET_ECO_ON[] PROGMEM = "Alexa.Actions.SetEcoOn";
|
||||||
|
static const char ACTION_SET_ECO_OFF[] PROGMEM = "Alexa.Actions.SetEcoOff";
|
||||||
|
static const char *const ACTION_NAMES[] PROGMEM = {ACTION_OPEN, ACTION_CLOSE, ACTION_RAISE,
|
||||||
|
ACTION_LOWER, ACTION_SET_ECO_ON, ACTION_SET_ECO_OFF};
|
||||||
|
static const uint8_t ACTION_COUNT = sizeof(ACTION_NAMES) / sizeof(ACTION_NAMES[0]);
|
||||||
|
|
||||||
|
static const char STATE_OPEN[] PROGMEM = "Alexa.States.Open";
|
||||||
|
static const char STATE_CLOSED[] PROGMEM = "Alexa.States.Closed";
|
||||||
|
static const char *const STATE_NAMES[] PROGMEM = {STATE_OPEN, STATE_CLOSED};
|
||||||
|
static const uint8_t STATE_COUNT = sizeof(STATE_NAMES) / sizeof(STATE_NAMES[0]);
|
||||||
|
|
||||||
|
static PGM_P nameAt(const char *const *names, uint8_t place)
|
||||||
|
{
|
||||||
|
return static_cast<PGM_P>(pgm_read_ptr(&names[place]));
|
||||||
|
}
|
||||||
|
|
||||||
|
PGM_P alexaActionName(AlexaAction action)
|
||||||
|
{
|
||||||
|
const uint8_t place = static_cast<uint8_t>(action);
|
||||||
|
return place < ACTION_COUNT ? nameAt(ACTION_NAMES, place) : PSTR("Unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
PGM_P alexaStateName(AlexaState state)
|
||||||
|
{
|
||||||
|
const uint8_t place = static_cast<uint8_t>(state);
|
||||||
|
return place < STATE_COUNT ? nameAt(STATE_NAMES, place) : PSTR("Unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The names of the bits that are set, in the order of the table
|
||||||
|
static void addNames(JsonArray target, uint8_t bits, const char *const *names, uint8_t count)
|
||||||
|
{
|
||||||
|
for (uint8_t place = 0; place < count; place++)
|
||||||
|
{
|
||||||
|
if ((bits & (1u << place)) != 0)
|
||||||
|
{
|
||||||
|
target.add(FPSTR(nameAt(names, place)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A copy on the heap; nullptr for no text, an empty text and a heap without room
|
||||||
|
static char *copyText(const char *text)
|
||||||
|
{
|
||||||
|
if (text == nullptr || text[0] == '\0')
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const size_t size = strlen(text) + 1;
|
||||||
|
char *copy = static_cast<char *>(malloc(size));
|
||||||
|
if (copy != nullptr)
|
||||||
|
{
|
||||||
|
memcpy(copy, text, size);
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapabilityName::AlexaCapabilityName(const AlexaInterfaceDesc &row, const char *instance)
|
||||||
|
{
|
||||||
|
strncpy_P(text, row.ns, sizeof(text) - 1);
|
||||||
|
text[sizeof(text) - 1] = '\0';
|
||||||
|
if (instance != nullptr && instance[0] != '\0')
|
||||||
|
{
|
||||||
|
strncat(text, " ", sizeof(text) - strlen(text) - 1);
|
||||||
|
strncat(text, instance, sizeof(text) - strlen(text) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability::AlexaCapability(const AlexaInterfaceDesc &row, const char *instance)
|
||||||
|
: row(&row), retrievable(true), proactivelyReported(false), nonControllable(false)
|
||||||
|
{
|
||||||
|
setInstance(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability::~AlexaCapability()
|
||||||
|
{
|
||||||
|
free(instance);
|
||||||
|
for (uint8_t i = 0; i < nameCount; i++)
|
||||||
|
{
|
||||||
|
if (names[i].locale != nullptr)
|
||||||
|
{
|
||||||
|
free(const_cast<char *>(names[i].value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::setInstance(const char *newInstance)
|
||||||
|
{
|
||||||
|
free(instance);
|
||||||
|
instance = copyText(newInstance);
|
||||||
|
if (instance == nullptr && newInstance != nullptr && newInstance[0] != '\0')
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: instance not set: the heap has no room for it",
|
||||||
|
AlexaCapabilityName(*row, nullptr).text);
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::addFriendlyName(const char *text, const char *locale)
|
||||||
|
{
|
||||||
|
const AlexaCapabilityName name(*row, instance);
|
||||||
|
if (text == nullptr || text[0] == '\0' || locale == nullptr || locale[0] == '\0')
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: friendly name not added: pass a text and a locale, addFriendlyName(\"Lift\", \"en-US\")",
|
||||||
|
name.text);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: friendly name \"%s\" not added: it has %d already (ALEX2ESP_MAX_FRIENDLY_NAMES)",
|
||||||
|
name.text, text, ALEX2ESP_MAX_FRIENDLY_NAMES);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
const char *copy = copyText(text);
|
||||||
|
if (copy == nullptr)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: friendly name \"%s\" not added: the heap has no room for it", name.text, text);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
names[nameCount++] = {copy, locale};
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::addFriendlyAsset(PGM_P assetId)
|
||||||
|
{
|
||||||
|
const AlexaCapabilityName name(*row, instance);
|
||||||
|
if (assetId == nullptr)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: asset not added: pass its id, addFriendlyAsset(PSTR(\"Alexa.Setting.Opening\"))", name.text);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: asset not added: it has %d names already (ALEX2ESP_MAX_FRIENDLY_NAMES)",
|
||||||
|
name.text, ALEX2ESP_MAX_FRIENDLY_NAMES);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
names[nameCount++] = {assetId, nullptr};
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::setRetrievable(bool value)
|
||||||
|
{
|
||||||
|
retrievable = value;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::setProactivelyReported(bool value)
|
||||||
|
{
|
||||||
|
proactivelyReported = value;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::setNonControllable(bool value)
|
||||||
|
{
|
||||||
|
nonControllable = value;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::setConfiguration(AlexaConfigurationFiller fill, void *context)
|
||||||
|
{
|
||||||
|
fillConfiguration = fill;
|
||||||
|
configurationContext = context;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::addActionMapping(const ActionMapping &mapping)
|
||||||
|
{
|
||||||
|
const AlexaCapabilityName name(*row, instance);
|
||||||
|
if (!row->has(AIF_SEMANTICS))
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: action mapping not added: only RangeController, ModeController and ToggleController take one",
|
||||||
|
name.text);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
if (mapping.actions == 0 || mapping.directiveName == nullptr || mapping.directiveName[0] == '\0')
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: action mapping not added: it needs an action and a directive, ActionMapping({AlexaAction::Open}, \"TurnOn\")",
|
||||||
|
name.text);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
if (actionCount >= ALEX2ESP_MAX_ACTION_MAPPINGS)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: action mapping to %s not added: it has %d already (ALEX2ESP_MAX_ACTION_MAPPINGS)",
|
||||||
|
name.text, mapping.directiveName, ALEX2ESP_MAX_ACTION_MAPPINGS);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
actionMappings[actionCount++] = mapping;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability::StateMapping *AlexaCapability::newStateMapping(std::initializer_list<AlexaState> states,
|
||||||
|
StateMappingKind kind)
|
||||||
|
{
|
||||||
|
const AlexaCapabilityName name(*row, instance);
|
||||||
|
if (!row->has(AIF_SEMANTICS))
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: state mapping not added: only RangeController, ModeController and ToggleController take one",
|
||||||
|
name.text);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (states.size() == 0)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: state mapping not added: it needs a state, addStateMapping({AlexaState::Closed}, 0.0f)",
|
||||||
|
name.text);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (stateCount >= ALEX2ESP_MAX_STATE_MAPPINGS)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: state mapping not added: it has %d already (ALEX2ESP_MAX_STATE_MAPPINGS)",
|
||||||
|
name.text, ALEX2ESP_MAX_STATE_MAPPINGS);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
StateMapping *mapping = &stateMappings[stateCount++];
|
||||||
|
mapping->states = 0;
|
||||||
|
for (AlexaState state : states)
|
||||||
|
{
|
||||||
|
mapping->states |= static_cast<uint8_t>(1u << static_cast<uint8_t>(state));
|
||||||
|
}
|
||||||
|
mapping->kind = kind;
|
||||||
|
mapping->maximum = 0;
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::addStateMapping(std::initializer_list<AlexaState> states, float value)
|
||||||
|
{
|
||||||
|
StateMapping *mapping = newStateMapping(states, STATES_TO_VALUE);
|
||||||
|
if (mapping != nullptr)
|
||||||
|
{
|
||||||
|
mapping->value = value;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::addStateMapping(std::initializer_list<AlexaState> states, float minimum, float maximum)
|
||||||
|
{
|
||||||
|
StateMapping *mapping = newStateMapping(states, STATES_TO_RANGE);
|
||||||
|
if (mapping != nullptr)
|
||||||
|
{
|
||||||
|
mapping->value = minimum;
|
||||||
|
mapping->maximum = maximum;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::addStateMapping(std::initializer_list<AlexaState> states, const char *value)
|
||||||
|
{
|
||||||
|
if (value == nullptr)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: state mapping not added: it needs a value, addStateMapping({AlexaState::Closed}, \"OFF\")",
|
||||||
|
AlexaCapabilityName(*row, instance).text);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
StateMapping *mapping = newStateMapping(states, STATES_TO_TEXT);
|
||||||
|
if (mapping != nullptr)
|
||||||
|
{
|
||||||
|
mapping->text = value;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AlexaCapability::matches(const char *ns, const char *directiveInstance) const
|
||||||
|
{
|
||||||
|
if (ns == nullptr || strcmp_P(ns, row->ns) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!row->has(AIF_INSTANCED))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return directiveInstance != nullptr && instance != nullptr && strcmp(directiveInstance, instance) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The keys that only some capabilities have are F() strings: they stay in flash and are copied into the document
|
||||||
|
// of a capability that has them. A plain literal takes static RAM in every sketch.
|
||||||
|
bool AlexaCapability::toJson(JsonArray capabilities, const char *endpointId) const
|
||||||
|
{
|
||||||
|
if (row->has(AIF_INSTANCED) && (instance == nullptr || nameCount == 0))
|
||||||
|
{
|
||||||
|
const AlexaCapabilityName name(*row, instance);
|
||||||
|
if (instance == nullptr)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: %s left out of discovery: it has no instance, call setInstance(\"Blind.Lift\") on it",
|
||||||
|
endpointId, name.text);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: %s left out of discovery: it has no name, call addFriendlyName(\"Lift\", \"en-US\") on it",
|
||||||
|
endpointId, name.text);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject capability = capabilities.add<JsonObject>();
|
||||||
|
capability["interface"] = FPSTR(row->ns);
|
||||||
|
capability["version"] = FPSTR(row->version);
|
||||||
|
capability["type"] = "AlexaInterface";
|
||||||
|
|
||||||
|
if (row->has(AIF_EMPTY_PROPERTIES))
|
||||||
|
{
|
||||||
|
capability["properties"].to<JsonObject>();
|
||||||
|
}
|
||||||
|
else if (!row->has(AIF_NO_PROPERTIES))
|
||||||
|
{
|
||||||
|
JsonObject properties = capability["properties"].to<JsonObject>();
|
||||||
|
properties["retrievable"] = static_cast<bool>(retrievable);
|
||||||
|
properties["proactivelyReported"] = static_cast<bool>(proactivelyReported);
|
||||||
|
|
||||||
|
JsonArray supported = properties["supported"].to<JsonArray>();
|
||||||
|
for (uint8_t i = 0; i < row->propertyCount(); i++)
|
||||||
|
{
|
||||||
|
supported.add<JsonObject>()["name"] = FPSTR(row->property(i));
|
||||||
|
}
|
||||||
|
if (nonControllable)
|
||||||
|
{
|
||||||
|
properties[F("nonControllable")] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSemantics(capability);
|
||||||
|
if (instance != nullptr)
|
||||||
|
{
|
||||||
|
capability["instance"] = static_cast<const char *>(instance);
|
||||||
|
}
|
||||||
|
writeNames(capability);
|
||||||
|
if (fillConfiguration != nullptr)
|
||||||
|
{
|
||||||
|
fillConfiguration(capability[F("configuration")].to<JsonObject>(), configurationContext);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantics exist only when there is something to map, as alex2node announces them
|
||||||
|
void AlexaCapability::writeSemantics(JsonObject capability) const
|
||||||
|
{
|
||||||
|
if (actionCount == 0 && stateCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonObject semantics = capability["semantics"].to<JsonObject>();
|
||||||
|
|
||||||
|
if (actionCount > 0)
|
||||||
|
{
|
||||||
|
JsonArray mappings = semantics["actionMappings"].to<JsonArray>();
|
||||||
|
for (uint8_t i = 0; i < actionCount; i++)
|
||||||
|
{
|
||||||
|
const ActionMapping &action = actionMappings[i];
|
||||||
|
JsonObject mapping = mappings.add<JsonObject>();
|
||||||
|
mapping["@type"] = "ActionsToDirective";
|
||||||
|
addNames(mapping["actions"].to<JsonArray>(), action.actions, ACTION_NAMES, ACTION_COUNT);
|
||||||
|
|
||||||
|
JsonObject directive = mapping["directive"].to<JsonObject>();
|
||||||
|
directive["name"] = action.directiveName;
|
||||||
|
if (action.directivePayload == nullptr || action.directivePayload[0] == '\0')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Alexa expects the payload as an object, not as a string that holds JSON. Parsed with its length,
|
||||||
|
// as the bridge parses a directive: the sketch links one parser and not two.
|
||||||
|
JsonDocument payload;
|
||||||
|
const DeserializationError error =
|
||||||
|
deserializeJson(payload, action.directivePayload, strlen(action.directivePayload));
|
||||||
|
if (error == DeserializationError::Ok && payload.is<JsonObject>())
|
||||||
|
{
|
||||||
|
directive["payload"] = payload.as<JsonObject>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: payload of the action mapping to %s left out: it is not the JSON text of an object",
|
||||||
|
AlexaCapabilityName(*row, instance).text, action.directiveName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateCount > 0)
|
||||||
|
{
|
||||||
|
JsonArray mappings = semantics[F("stateMappings")].to<JsonArray>();
|
||||||
|
for (uint8_t i = 0; i < stateCount; i++)
|
||||||
|
{
|
||||||
|
const StateMapping &state = stateMappings[i];
|
||||||
|
JsonObject mapping = mappings.add<JsonObject>();
|
||||||
|
mapping["@type"] = state.kind == STATES_TO_RANGE ? F("StatesToRange") : F("StatesToValue");
|
||||||
|
addNames(mapping[F("states")].to<JsonArray>(), state.states, STATE_NAMES, STATE_COUNT);
|
||||||
|
|
||||||
|
if (state.kind == STATES_TO_RANGE)
|
||||||
|
{
|
||||||
|
JsonObject range = mapping[F("range")].to<JsonObject>();
|
||||||
|
range[F("minimumValue")] = state.value;
|
||||||
|
range[F("maximumValue")] = state.maximum;
|
||||||
|
}
|
||||||
|
else if (state.kind == STATES_TO_TEXT)
|
||||||
|
{
|
||||||
|
mapping["value"] = state.text;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mapping["value"] = state.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AlexaCapability::writeNames(JsonObject capability) const
|
||||||
|
{
|
||||||
|
if (nameCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonArray friendlyNames = capability["capabilityResources"]["friendlyNames"].to<JsonArray>();
|
||||||
|
for (uint8_t i = 0; i < nameCount; i++)
|
||||||
|
{
|
||||||
|
JsonObject friendlyName = friendlyNames.add<JsonObject>();
|
||||||
|
if (names[i].locale == nullptr)
|
||||||
|
{
|
||||||
|
friendlyName["@type"] = F("asset");
|
||||||
|
friendlyName["value"][F("assetId")] = FPSTR(names[i].value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
friendlyName["@type"] = "text";
|
||||||
|
JsonObject value = friendlyName["value"].to<JsonObject>();
|
||||||
|
value["text"] = names[i].value;
|
||||||
|
value["locale"] = names[i].locale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
181
src/AlexaCapability.h
Normal file
181
src/AlexaCapability.h
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
// One capability of one device: a row of AlexaInterfaces and what this device adds to it. For most interfaces that
|
||||||
|
// is nothing. A generic controller (RangeController, ModeController, ToggleController) has an instance and the
|
||||||
|
// names Alexa calls it by, a configuration, and the words Alexa maps to its directives and states.
|
||||||
|
// alexa-discovery-objects.html, generic-controllers.html under
|
||||||
|
// https://developer.amazon.com/en-US/docs/alexa/device-apis/
|
||||||
|
#ifndef ALEXA_CAPABILITY_H
|
||||||
|
#define ALEXA_CAPABILITY_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include "AlexaInterfaces.h"
|
||||||
|
#include "AlexaLimits.h"
|
||||||
|
|
||||||
|
// What a user says: "open", "close", "raise", "lower" (alexa-discovery-objects.html#action-mapping)
|
||||||
|
enum class AlexaAction : uint8_t
|
||||||
|
{
|
||||||
|
Open,
|
||||||
|
Close,
|
||||||
|
Raise,
|
||||||
|
Lower,
|
||||||
|
SetEcoOn,
|
||||||
|
SetEcoOff
|
||||||
|
};
|
||||||
|
|
||||||
|
// What Alexa says about the device: "the blind is open" (alexa-discovery-objects.html#state-mapping)
|
||||||
|
enum class AlexaState : uint8_t
|
||||||
|
{
|
||||||
|
Open,
|
||||||
|
Closed
|
||||||
|
};
|
||||||
|
|
||||||
|
// "Alexa.Actions.Open", in program memory
|
||||||
|
PGM_P alexaActionName(AlexaAction action);
|
||||||
|
|
||||||
|
// "Alexa.States.Open", in program memory
|
||||||
|
PGM_P alexaStateName(AlexaState state);
|
||||||
|
|
||||||
|
// The directive Alexa sends for one or more actions. The name of the directive and its payload, the JSON text of
|
||||||
|
// an object, are kept as pointers and not copied: pass literals. Actions are announced in the order of AlexaAction.
|
||||||
|
class ActionMapping
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ActionMapping() {}
|
||||||
|
|
||||||
|
ActionMapping(std::initializer_list<AlexaAction> actions, const char *directiveName,
|
||||||
|
const char *directivePayload = nullptr)
|
||||||
|
: directiveName(directiveName), directivePayload(directivePayload)
|
||||||
|
{
|
||||||
|
for (AlexaAction action : actions)
|
||||||
|
{
|
||||||
|
this->actions |= static_cast<uint8_t>(1u << static_cast<uint8_t>(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t actions = 0; // one bit per AlexaAction
|
||||||
|
const char *directiveName = nullptr;
|
||||||
|
const char *directivePayload = nullptr; // nullptr or "" for a directive without payload
|
||||||
|
};
|
||||||
|
|
||||||
|
// Writes the "configuration" object of a capability when the device is announced; context is what the sketch
|
||||||
|
// passed to setConfiguration()
|
||||||
|
typedef void (*AlexaConfigurationFiller)(JsonObject configuration, void *context);
|
||||||
|
|
||||||
|
// "Alexa.RangeController Blind.Lift" for a log line. The namespace is in program memory, where the %s of the log
|
||||||
|
// cannot read it.
|
||||||
|
struct AlexaCapabilityName
|
||||||
|
{
|
||||||
|
AlexaCapabilityName(const AlexaInterfaceDesc &row, const char *instance);
|
||||||
|
|
||||||
|
char text[72];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 116 bytes on the heap of an ESP8266, whatever the capability holds. Every setter returns the capability:
|
||||||
|
// device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
|
||||||
|
// ->addFriendlyName("Lift", "en-US").setConfiguration(fillLift).addStateMapping({AlexaState::Closed}, 0.0f);
|
||||||
|
// What a capability refuses is printed at ERROR with what to change, and leaves the capability as it was.
|
||||||
|
class AlexaCapability
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit AlexaCapability(const AlexaInterfaceDesc &row, const char *instance = nullptr);
|
||||||
|
~AlexaCapability();
|
||||||
|
|
||||||
|
// It owns copies of its instance and its names
|
||||||
|
AlexaCapability(const AlexaCapability &) = delete;
|
||||||
|
AlexaCapability &operator=(const AlexaCapability &) = delete;
|
||||||
|
|
||||||
|
const AlexaInterfaceDesc &getRow() const { return *row; }
|
||||||
|
AlexaInterfaceType getType() const { return row->interfaceType(); }
|
||||||
|
String getTypeString() const { return String(FPSTR(row->ns)); }
|
||||||
|
String getVersion() const { return String(FPSTR(row->version)); }
|
||||||
|
|
||||||
|
// "" when the capability has none
|
||||||
|
const char *getInstance() const { return instance != nullptr ? instance : ""; }
|
||||||
|
|
||||||
|
bool isRetrievable() const { return retrievable; }
|
||||||
|
bool isProactivelyReported() const { return proactivelyReported; }
|
||||||
|
bool isNonControllable() const { return nonControllable; }
|
||||||
|
|
||||||
|
// The instance tells the capabilities of one interface on a device apart ("Blind.Lift", "Blind.Tilt"). It is
|
||||||
|
// copied. addCapability(row, instance) sets it; this is for a capability that was added by its type.
|
||||||
|
AlexaCapability &setInstance(const char *instance);
|
||||||
|
|
||||||
|
// A name of the capability in words. The text is copied, the locale ("en-US") is not: pass a literal.
|
||||||
|
AlexaCapability &addFriendlyName(const char *text, const char *locale);
|
||||||
|
|
||||||
|
// A name from the catalog of Alexa, which Alexa translates: PSTR("Alexa.Setting.Opening"). Not copied.
|
||||||
|
AlexaCapability &addFriendlyAsset(PGM_P assetId);
|
||||||
|
|
||||||
|
AlexaCapability &setRetrievable(bool value);
|
||||||
|
AlexaCapability &setProactivelyReported(bool value);
|
||||||
|
|
||||||
|
// true: Alexa reports the state and sends no directive that changes it
|
||||||
|
AlexaCapability &setNonControllable(bool value);
|
||||||
|
|
||||||
|
AlexaCapability &setConfiguration(AlexaConfigurationFiller fill, void *context = nullptr);
|
||||||
|
|
||||||
|
// Only for an interface that takes semantics: RangeController, ModeController, ToggleController
|
||||||
|
AlexaCapability &addActionMapping(const ActionMapping &mapping);
|
||||||
|
|
||||||
|
// The states a value stands for: a number or a range of a RangeController, the text of a mode or "ON"/"OFF"
|
||||||
|
// of a ToggleController. A text is not copied: pass a literal.
|
||||||
|
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, float value);
|
||||||
|
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, float minimum, float maximum);
|
||||||
|
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, const char *value);
|
||||||
|
|
||||||
|
// Whether a directive with this namespace and instance is for this capability. The instance counts only
|
||||||
|
// for an interface that has instances.
|
||||||
|
bool matches(const char *ns, const char *directiveInstance) const;
|
||||||
|
|
||||||
|
// Adds the discovery object of the capability to the capabilities of its endpoint. A capability that Alexa
|
||||||
|
// would reject the endpoint for, a generic controller without an instance or without a name, adds nothing,
|
||||||
|
// prints what is missing and returns false.
|
||||||
|
bool toJson(JsonArray capabilities, const char *endpointId) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Name
|
||||||
|
{
|
||||||
|
const char *value; // the text, a copy on the heap, or the asset id in program memory
|
||||||
|
const char *locale; // nullptr for an asset
|
||||||
|
};
|
||||||
|
|
||||||
|
enum StateMappingKind : uint8_t
|
||||||
|
{
|
||||||
|
STATES_TO_VALUE,
|
||||||
|
STATES_TO_RANGE,
|
||||||
|
STATES_TO_TEXT
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StateMapping
|
||||||
|
{
|
||||||
|
uint8_t states; // one bit per AlexaState
|
||||||
|
uint8_t kind; // StateMappingKind
|
||||||
|
union
|
||||||
|
{
|
||||||
|
float value; // the value, or the lowest of a range
|
||||||
|
const char *text;
|
||||||
|
};
|
||||||
|
float maximum;
|
||||||
|
};
|
||||||
|
|
||||||
|
StateMapping *newStateMapping(std::initializer_list<AlexaState> states, StateMappingKind kind);
|
||||||
|
void writeSemantics(JsonObject capability) const;
|
||||||
|
void writeNames(JsonObject capability) const;
|
||||||
|
|
||||||
|
const AlexaInterfaceDesc *row; // in program memory, never nullptr
|
||||||
|
char *instance = nullptr; // a copy on the heap
|
||||||
|
AlexaConfigurationFiller fillConfiguration = nullptr;
|
||||||
|
void *configurationContext = nullptr;
|
||||||
|
Name names[ALEX2ESP_MAX_FRIENDLY_NAMES];
|
||||||
|
ActionMapping actionMappings[ALEX2ESP_MAX_ACTION_MAPPINGS];
|
||||||
|
StateMapping stateMappings[ALEX2ESP_MAX_STATE_MAPPINGS];
|
||||||
|
uint8_t nameCount = 0;
|
||||||
|
uint8_t actionCount = 0;
|
||||||
|
uint8_t stateCount = 0;
|
||||||
|
bool retrievable : 1;
|
||||||
|
bool proactivelyReported : 1;
|
||||||
|
bool nonControllable : 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // ALEXA_CAPABILITY_H
|
||||||
|
|
@ -72,28 +72,34 @@ String AlexaDevice::getSoftwareVersion() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
AlexaInterface* AlexaDevice::addCapability(const AlexaInterfaceDesc& row) {
|
AlexaCapability* AlexaDevice::addCapability(const AlexaInterfaceDesc& row, const char* instance) {
|
||||||
// Check if a interface with the given row already exists
|
const bool hasInstance = instance != nullptr && instance[0] != '\0';
|
||||||
for (auto& iface : capabilities) {
|
if (row.has(AIF_INSTANCED) && !hasInstance) {
|
||||||
if (&iface.getRow() == &row) {
|
ALEX2ESP_LOGE("%s: capability %s not added: it needs an instance, addCapability(row, \"Blind.Lift\")",
|
||||||
return &iface; // Return the existing interface
|
endpointId.c_str(), AlexaCapabilityName(row, nullptr).text);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!row.has(AIF_INSTANCED) && hasInstance) {
|
||||||
|
ALEX2ESP_LOGE("%s: capability %s not added: the interface takes no instance, leave it out",
|
||||||
|
endpointId.c_str(), AlexaCapabilityName(row, instance).text);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return capabilityOf(row, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaCapability* AlexaDevice::capabilityOf(const AlexaInterfaceDesc& row, const char* instance) {
|
||||||
|
for (AlexaCapability& capability : capabilities) {
|
||||||
|
if (&capability.getRow() == &row && (instance == nullptr || strcmp(capability.getInstance(), instance) == 0)) {
|
||||||
|
return &capability;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the interface doesn't exist, create a new one
|
capabilities.emplace_back(row, instance);
|
||||||
capabilities.emplace_back(row);
|
ALEX2ESP_LOGI("%s: capability %s added", endpointId.c_str(), AlexaCapabilityName(row, instance).text);
|
||||||
|
|
||||||
// The namespace is in program memory, where the %s of the log cannot read it
|
|
||||||
char interfaceName[40];
|
|
||||||
strncpy_P(interfaceName, row.ns, sizeof(interfaceName) - 1);
|
|
||||||
interfaceName[sizeof(interfaceName) - 1] = '\0';
|
|
||||||
ALEX2ESP_LOGI("%s: capability %s added", endpointId.c_str(), interfaceName);
|
|
||||||
|
|
||||||
// Return a pointer to the newly created device
|
|
||||||
return &capabilities.back();
|
return &capabilities.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
AlexaInterface* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
|
AlexaCapability* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
|
||||||
ALEX2ESP_LOGE("%s: capability not added: the library has no description of interface type %d; "
|
ALEX2ESP_LOGE("%s: capability not added: the library has no description of interface type %d; "
|
||||||
"readme.md lists the interfaces it has (Interface Types)",
|
"readme.md lists the interfaces it has (Interface Types)",
|
||||||
endpointId.c_str(), static_cast<int>(type));
|
endpointId.c_str(), static_cast<int>(type));
|
||||||
|
|
@ -101,7 +107,7 @@ AlexaInterface* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
AlexaInterfaceType AlexaDevice::getInterfaceType(const char* interfaceName) const {
|
AlexaInterfaceType AlexaDevice::getInterfaceType(const char* interfaceName) const {
|
||||||
for (const AlexaInterface& capability : capabilities) {
|
for (const AlexaCapability& capability : capabilities) {
|
||||||
if (strcmp_P(interfaceName, capability.getRow().ns) == 0) {
|
if (strcmp_P(interfaceName, capability.getRow().ns) == 0) {
|
||||||
return capability.getType();
|
return capability.getType();
|
||||||
}
|
}
|
||||||
|
|
@ -136,31 +142,14 @@ JsonDocument AlexaDevice::getDeviceJSON() const {
|
||||||
|
|
||||||
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
|
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
|
||||||
|
|
||||||
for (const AlexaInterface& capability : capabilities) {
|
// A capability that cannot be announced says so itself; the device is announced with the others
|
||||||
capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
for (const AlexaCapability& capability : capabilities) {
|
||||||
|
capability.toJson(capabilitiesArray, endpointId.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
|
||||||
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
// void AlexaDevice::registerEvent(const String& eventName, std::function<void(const JsonDocument&,const AlexaInterfaceType&)> callback) {
|
|
||||||
// eventCallbacks[eventName] = callback;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// void AlexaDevice::triggerEvent(const String& eventName, const JsonDocument& directive,const AlexaInterfaceType& type) const {
|
|
||||||
// auto it = eventCallbacks.find(eventName);
|
|
||||||
// if (it != eventCallbacks.end()) {
|
|
||||||
// // Call the registered callback function
|
|
||||||
// it->second(directive,type);
|
|
||||||
// } else {
|
|
||||||
// Serial.print("No event registered for: ");
|
|
||||||
// Serial.println(eventName);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// Register an event callback function
|
// Register an event callback function
|
||||||
void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) {
|
void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) {
|
||||||
for (int i = 0; i < MAX_EVENTS; ++i) {
|
for (int i = 0; i < MAX_EVENTS; ++i) {
|
||||||
|
|
|
||||||
|
|
@ -58,17 +58,20 @@ public:
|
||||||
|
|
||||||
JsonDocument getDeviceJSON() const;
|
JsonDocument getDeviceJSON() const;
|
||||||
|
|
||||||
// Returns the capability that describes this interface, creating it on first use. The pointer stays valid for
|
// Returns the capability of this device for the interface, creating it on first use. An interface that a
|
||||||
// the lifetime of the device: capabilities live in a std::deque, which never relocates its elements when
|
// device may have several of (RangeController, ModeController, ToggleController) takes the instance that tells
|
||||||
// another one is added.
|
// them apart, "Blind.Lift"; without one, and with an instance for any other interface, nothing is added, the
|
||||||
AlexaInterface* addCapability(const AlexaInterfaceDesc& row);
|
// reason is printed and nullptr returned. The pointer stays valid for the lifetime of the device:
|
||||||
|
// capabilities live in a std::deque, which never relocates its elements when another one is added.
|
||||||
|
AlexaCapability* addCapability(const AlexaInterfaceDesc& row, const char* instance = nullptr);
|
||||||
|
|
||||||
// The same by the type of the interface, as 1.x sketches write it. A type without a row in AlexaInterfaces
|
// The same by the type of the interface, as 1.x sketches write it: they call setInstance() on what they get,
|
||||||
// cannot be described: nothing is added, the reason is printed and nullptr returned. Inlined, so that a type
|
// so the instance is not asked for here but when the device is announced. A type without a row in
|
||||||
// written out in the sketch links its row only (alexaInterfaceRow).
|
// AlexaInterfaces cannot be described: nothing is added, the reason is printed and nullptr returned. Inlined,
|
||||||
__attribute__((always_inline)) AlexaInterface* addCapability(AlexaInterfaceType type) {
|
// so that a type written out in the sketch links its row only (alexaInterfaceRow).
|
||||||
|
__attribute__((always_inline)) AlexaCapability* addCapability(AlexaInterfaceType type) {
|
||||||
const AlexaInterfaceDesc* row = alexaInterfaceRow(type);
|
const AlexaInterfaceDesc* row = alexaInterfaceRow(type);
|
||||||
return row != nullptr ? addCapability(*row) : refuseCapability(type);
|
return row != nullptr ? capabilityOf(*row, nullptr) : refuseCapability(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The type of the capability of this device with the namespace of a directive ("Alexa.PowerController"),
|
// The type of the capability of this device with the namespace of a directive ("Alexa.PowerController"),
|
||||||
|
|
@ -86,7 +89,9 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AlexaInterface* refuseCapability(AlexaInterfaceType type);
|
// 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* refuseCapability(AlexaInterfaceType type);
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
String endpointId;
|
String endpointId;
|
||||||
|
|
@ -102,7 +107,7 @@ private:
|
||||||
String model = "Alexa2MQTT";
|
String model = "Alexa2MQTT";
|
||||||
const String softwareVersion = ALEX2ESP_VERSION;
|
const String softwareVersion = ALEX2ESP_VERSION;
|
||||||
|
|
||||||
std::deque<AlexaInterface> capabilities; // deque: pointers handed out by addCapability stay valid
|
std::deque<AlexaCapability> capabilities; // deque: pointers handed out by addCapability stay valid
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,202 +1,17 @@
|
||||||
|
// The names of library 1.x for what 2.0 calls AlexaCapability and AlexaAction. They stay through 2.x, so that a
|
||||||
|
// sketch written for 1.x compiles as it is; new sketches use the 2.0 names.
|
||||||
#ifndef ALEXA_INTERFACE_H
|
#ifndef ALEXA_INTERFACE_H
|
||||||
#define ALEXA_INTERFACE_H
|
#define ALEXA_INTERFACE_H
|
||||||
|
|
||||||
#include <string>
|
#include "AlexaCapability.h"
|
||||||
#include <vector>
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <ArduinoJson.h>
|
|
||||||
#include "AlexaInterfaces.h"
|
|
||||||
#include "AlexaLog.h"
|
|
||||||
|
|
||||||
enum class AlexaActions {
|
|
||||||
Open,
|
|
||||||
Close,
|
|
||||||
Raise,
|
|
||||||
Lower,
|
|
||||||
SetEcoOn,
|
|
||||||
SetEcoOff
|
|
||||||
};
|
|
||||||
|
|
||||||
|
typedef AlexaCapability AlexaInterface;
|
||||||
|
typedef AlexaAction AlexaActions;
|
||||||
|
|
||||||
class AlexaActionsUtils
|
class AlexaActionsUtils
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Helper function to convert AlexaActions enum to string
|
static String toString(AlexaActions action) { return String(FPSTR(alexaActionName(action))); }
|
||||||
static String toString(AlexaActions action) {
|
|
||||||
switch (action) {
|
|
||||||
case AlexaActions::Open: return "Alexa.Actions.Open";
|
|
||||||
case AlexaActions::Close: return "Alexa.Actions.Close";
|
|
||||||
case AlexaActions::Raise: return "Alexa.Actions.Raise";
|
|
||||||
case AlexaActions::Lower: return "Alexa.Actions.Lower";
|
|
||||||
case AlexaActions::SetEcoOn: return "Alexa.Actions.SetEcoOn";
|
|
||||||
case AlexaActions::SetEcoOff: return "Alexa.Actions.SetEcoOff";
|
|
||||||
default: return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
class FriendlyName {
|
|
||||||
public:
|
|
||||||
FriendlyName(const std::string& t, const std::string& l)
|
|
||||||
: text(t), locale(l) {}
|
|
||||||
|
|
||||||
std::string text;
|
|
||||||
std::string locale;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Updated ActionMapping class definition
|
|
||||||
class ActionMapping {
|
|
||||||
public:
|
|
||||||
String type = "ActionsToDirective"; // Type of action mapping (always "ActionsToDirective")
|
|
||||||
std::vector<AlexaActions> actions; // Array of AlexaActions
|
|
||||||
struct Directive {
|
|
||||||
String name; // Name of the directive
|
|
||||||
String payload; // JSON text of the directive's payload object, empty when there is none
|
|
||||||
} directive; // Directive object containing name and payload
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
ActionMapping(std::vector<AlexaActions> actions, String directiveName, String directivePayload="")
|
|
||||||
: actions(actions) {
|
|
||||||
directive.name = directiveName;
|
|
||||||
directive.payload = directivePayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Get the JSON representation of ActionMapping
|
|
||||||
JsonDocument getJSON() const {
|
|
||||||
JsonDocument doc;
|
|
||||||
doc["@type"] = type;
|
|
||||||
|
|
||||||
// Convert actions array to strings
|
|
||||||
JsonArray actionArray = doc["actions"].to<JsonArray>();
|
|
||||||
for (const auto& action : actions) {
|
|
||||||
actionArray.add(AlexaActionsUtils::toString(action));
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject directiveObj = doc["directive"].to<JsonObject>();
|
|
||||||
directiveObj["name"] = directive.name;
|
|
||||||
if (directive.payload.length() > 0) {
|
|
||||||
// Alexa expects the payload as an object, not as a string holding JSON: parse the text we were given
|
|
||||||
JsonDocument payloadDoc;
|
|
||||||
if (deserializeJson(payloadDoc, directive.payload) == DeserializationError::Ok && payloadDoc.is<JsonObject>()) {
|
|
||||||
directiveObj["payload"] = payloadDoc.as<JsonObject>();
|
|
||||||
} else {
|
|
||||||
ALEX2ESP_LOGE("action mapping payload is not a JSON object, omitted: %s", directive.payload.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// A capability of a device: a row of AlexaInterfaces and what this device adds to it
|
|
||||||
class AlexaInterface
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
const AlexaInterfaceDesc *row; // in program memory, never nullptr
|
|
||||||
bool retrievable;
|
|
||||||
bool proactivelyReported;
|
|
||||||
std::vector<ActionMapping> actionMappings; // Array of ActionMappings
|
|
||||||
std::vector<FriendlyName> friendlyNames; // Array of FriendlyNames
|
|
||||||
|
|
||||||
|
|
||||||
String instance;
|
|
||||||
bool nameSet = false;
|
|
||||||
public:
|
|
||||||
// Constructor
|
|
||||||
AlexaInterface(const AlexaInterfaceDesc &row, bool retrievable = true, bool proactivelyReported = false)
|
|
||||||
: row(&row), retrievable(retrievable), proactivelyReported(proactivelyReported) {}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
const AlexaInterfaceDesc &getRow() const { return *row; }
|
|
||||||
AlexaInterfaceType getType() const { return row->interfaceType(); }
|
|
||||||
String getTypeString() const { return String(FPSTR(row->ns)); }
|
|
||||||
String getVersion() const { return String(FPSTR(row->version)); }
|
|
||||||
std::vector<String> getProps() const
|
|
||||||
{
|
|
||||||
std::vector<String> props;
|
|
||||||
for (uint8_t i = 0; i < row->propertyCount(); i++)
|
|
||||||
{
|
|
||||||
props.push_back(String(FPSTR(row->property(i))));
|
|
||||||
}
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setInstance(const char* name){
|
|
||||||
instance=name;
|
|
||||||
nameSet=true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addFriendlyName(const char* name,const char* locale){
|
|
||||||
friendlyNames.emplace_back(name,locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isRetrievable() const { return retrievable; }
|
|
||||||
bool isProactivelyReported() const { return proactivelyReported; }
|
|
||||||
|
|
||||||
// Setters
|
|
||||||
void setRetrievable(bool value) { retrievable = value; }
|
|
||||||
void setProactivelyReported(bool value) { proactivelyReported = value; }
|
|
||||||
|
|
||||||
void addActionMapping(const ActionMapping& actionMapping) {
|
|
||||||
actionMappings.push_back(actionMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonDocument getJSON() const
|
|
||||||
{
|
|
||||||
JsonDocument doc;
|
|
||||||
|
|
||||||
// Set base properties
|
|
||||||
doc["interface"] = FPSTR(row->ns);
|
|
||||||
doc["version"] = FPSTR(row->version);
|
|
||||||
doc["type"] = "AlexaInterface";
|
|
||||||
|
|
||||||
// Add properties, unless the interface has none to report (a scene, a doorbell)
|
|
||||||
if (!row->has(AIF_NO_PROPERTIES)) {
|
|
||||||
JsonObject properties = doc["properties"].to<JsonObject>();
|
|
||||||
properties["retrievable"] = retrievable;
|
|
||||||
properties["proactivelyReported"] = proactivelyReported;
|
|
||||||
|
|
||||||
JsonArray supported = properties["supported"].to<JsonArray>();
|
|
||||||
for (uint8_t i = 0; i < row->propertyCount(); i++)
|
|
||||||
{
|
|
||||||
supported.add<JsonObject>()["name"] = FPSTR(row->property(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Add action mappings - semantics only exist when there is something to map (as alex2node does)
|
|
||||||
if (!actionMappings.empty()) {
|
|
||||||
JsonObject semantics = doc["semantics"].to<JsonObject>();
|
|
||||||
JsonArray actionMappingArray = semantics["actionMappings"].to<JsonArray>();
|
|
||||||
for (const ActionMapping& actionMapping : actionMappings) {
|
|
||||||
actionMappingArray.add(actionMapping.getJSON().as<JsonObject>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(nameSet){
|
|
||||||
doc["instance"]=instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Add capabilityResources if friendlyNames is not empty
|
|
||||||
if (!friendlyNames.empty()) {
|
|
||||||
JsonObject capabilityResources = doc["capabilityResources"].to<JsonObject>();
|
|
||||||
JsonArray friendlyNamesArray = capabilityResources["friendlyNames"].to<JsonArray>();
|
|
||||||
|
|
||||||
for (const FriendlyName& fn : friendlyNames) {
|
|
||||||
JsonObject friendlyNameObj = friendlyNamesArray.add<JsonObject>();
|
|
||||||
friendlyNameObj["@type"] = "text";
|
|
||||||
|
|
||||||
JsonObject valueObj = friendlyNameObj["value"].to<JsonObject>();
|
|
||||||
valueObj["text"] = fn.text;
|
|
||||||
valueObj["locale"] = fn.locale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.shrinkToFit();
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // ALEXA_INTERFACE_H
|
#endif // ALEXA_INTERFACE_H
|
||||||
|
|
|
||||||
|
|
@ -110,13 +110,13 @@ ALEXA_ROW(DoorbellEventSource, NS_DOORBELL, VERSION_3, nullptr, 0, AIF_NO_PROPER
|
||||||
ALEXA_ROW(TimeHoldController, NS_TIME_HOLD, VERSION_3, PROPS_TIME_HOLD, 2, AIF_NONE, TIME_HOLD_CONTROLLER);
|
ALEXA_ROW(TimeHoldController, NS_TIME_HOLD, VERSION_3, PROPS_TIME_HOLD, 2, AIF_NONE, TIME_HOLD_CONTROLLER);
|
||||||
ALEXA_ROW(Speaker, NS_SPEAKER, VERSION_3, PROPS_SPEAKER, 2, AIF_NONE, SPEAKER);
|
ALEXA_ROW(Speaker, NS_SPEAKER, VERSION_3, PROPS_SPEAKER, 2, AIF_NONE, SPEAKER);
|
||||||
ALEXA_ROW(StepSpeaker, NS_STEP_SPEAKER, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES, STEP_SPEAKER);
|
ALEXA_ROW(StepSpeaker, NS_STEP_SPEAKER, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES, STEP_SPEAKER);
|
||||||
ALEXA_ROW(PlaybackController, NS_PLAYBACK, VERSION_3, nullptr, 0, AIF_NONE, PLAYBACK_CONTROLLER);
|
ALEXA_ROW(PlaybackController, NS_PLAYBACK, VERSION_3, nullptr, 0, AIF_EMPTY_PROPERTIES, PLAYBACK_CONTROLLER);
|
||||||
ALEXA_ROW(PlaybackStateReporter, NS_PLAYBACK_STATE, VERSION_3, PROPS_PLAYBACK_STATE, 1, AIF_NONE, PLAYBACK_STATE_REPORTER);
|
ALEXA_ROW(PlaybackStateReporter, NS_PLAYBACK_STATE, VERSION_3, PROPS_PLAYBACK_STATE, 1, AIF_NONE, PLAYBACK_STATE_REPORTER);
|
||||||
ALEXA_ROW(InputController, NS_INPUT, VERSION_3, PROPS_INPUT, 1, AIF_NONE, INPUT_CONTROLLER);
|
ALEXA_ROW(InputController, NS_INPUT, VERSION_3, PROPS_INPUT, 1, AIF_NONE, INPUT_CONTROLLER);
|
||||||
ALEXA_ROW(ChannelController, NS_CHANNEL, VERSION_3, PROPS_CHANNEL, 1, AIF_NONE, CHANNEL_CONTROLLER);
|
ALEXA_ROW(ChannelController, NS_CHANNEL, VERSION_3, PROPS_CHANNEL, 1, AIF_NONE, CHANNEL_CONTROLLER);
|
||||||
ALEXA_ROW(InventoryLevelSensor, NS_INVENTORY_LEVEL, VERSION_3, PROPS_INVENTORY_LEVEL, 1, AIF_INSTANCED, INVENTORY_LEVEL_SENSOR);
|
ALEXA_ROW(InventoryLevelSensor, NS_INVENTORY_LEVEL, VERSION_3, PROPS_INVENTORY_LEVEL, 1, AIF_INSTANCED, INVENTORY_LEVEL_SENSOR);
|
||||||
ALEXA_ROW(SimpleEventSource, NS_SIMPLE_EVENT, VERSION_1_0, nullptr, 0, AIF_INSTANCED | AIF_NO_PROPERTIES, SIMPLE_EVENT_SOURCE);
|
ALEXA_ROW(SimpleEventSource, NS_SIMPLE_EVENT, VERSION_1_0, nullptr, 0, AIF_INSTANCED | AIF_NO_PROPERTIES, SIMPLE_EVENT_SOURCE);
|
||||||
ALEXA_ROW(WakeOnLANController, NS_WAKE_ON_LAN, VERSION_3, nullptr, 0, AIF_NONE, WAKE_ON_LAN_CONTROLLER);
|
ALEXA_ROW(WakeOnLANController, NS_WAKE_ON_LAN, VERSION_3, nullptr, 0, AIF_EMPTY_PROPERTIES, WAKE_ON_LAN_CONTROLLER);
|
||||||
|
|
||||||
// The display categories in the order of the enum. Their names are looked up by a value that is only known when
|
// The display categories in the order of the enum. Their names are looked up by a value that is only known when
|
||||||
// the sketch runs, so this table is always linked (about 1 KB of flash).
|
// the sketch runs, so this table is always linked (about 1 KB of flash).
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,10 @@ enum class AlexaInterfaceType : uint8_t
|
||||||
enum AlexaInterfaceFlags : uint8_t
|
enum AlexaInterfaceFlags : uint8_t
|
||||||
{
|
{
|
||||||
AIF_NONE = 0,
|
AIF_NONE = 0,
|
||||||
AIF_INSTANCED = 1, // a generic controller: needs an instance and friendly names
|
AIF_INSTANCED = 1, // a device may have several: each needs an instance and a friendly name
|
||||||
AIF_NO_PROPERTIES = 2, // the discovery object has no "properties" (scenes, doorbells, step speakers)
|
AIF_NO_PROPERTIES = 2, // the discovery object has no "properties" (scenes, doorbells, step speakers)
|
||||||
AIF_SEMANTICS = 4 // may carry action and state mappings
|
AIF_SEMANTICS = 4, // may carry action and state mappings
|
||||||
|
AIF_EMPTY_PROPERTIES = 8 // the discovery object has "properties": {}, as the page of the interface shows it
|
||||||
};
|
};
|
||||||
|
|
||||||
// 16 bytes in program memory. The ESP8266 reads flash in whole words only: the pointers can be read as they are,
|
// 16 bytes in program memory. The ESP8266 reads flash in whole words only: the pointers can be read as they are,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,20 @@
|
||||||
#define ALEX2ESP_MAX_QUEUED_BYTES (4 * ALEX2ESP_MAX_DIRECTIVE)
|
#define ALEX2ESP_MAX_QUEUED_BYTES (4 * ALEX2ESP_MAX_DIRECTIVE)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
#ifndef ALEX2ESP_MAX_FRIENDLY_NAMES
|
||||||
|
#define ALEX2ESP_MAX_FRIENDLY_NAMES 3
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ALEX2ESP_MAX_ACTION_MAPPINGS
|
||||||
|
#define ALEX2ESP_MAX_ACTION_MAPPINGS 4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ALEX2ESP_MAX_STATE_MAPPINGS
|
||||||
|
#define ALEX2ESP_MAX_STATE_MAPPINGS 2
|
||||||
|
#endif
|
||||||
|
|
||||||
#if ALEX2ESP_MAX_QUEUED_DIRECTIVES < 1 || ALEX2ESP_MAX_QUEUED_DIRECTIVES > 64
|
#if ALEX2ESP_MAX_QUEUED_DIRECTIVES < 1 || ALEX2ESP_MAX_QUEUED_DIRECTIVES > 64
|
||||||
#error "ALEX2ESP_MAX_QUEUED_DIRECTIVES has to be between 1 and 64"
|
#error "ALEX2ESP_MAX_QUEUED_DIRECTIVES has to be between 1 and 64"
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -40,4 +54,9 @@
|
||||||
#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_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
|
||||||
|
#error "ALEX2ESP_MAX_FRIENDLY_NAMES, ALEX2ESP_MAX_ACTION_MAPPINGS and ALEX2ESP_MAX_STATE_MAPPINGS have to be between 1 and 255"
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif // ALEXA_LIMITS_H
|
#endif // ALEXA_LIMITS_H
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// The part of Arduino.h that AlexaDevice, AlexaInterface and AlexaLog use, for the host tests (pio test -e native):
|
// The part of Arduino.h that AlexaDevice, AlexaCapability and AlexaLog use, for the host tests (pio test -e native):
|
||||||
// String, Print and Serial. The program-memory helpers come from src/AlexaCompat.h. A board never sees this file.
|
// String, Print and Serial. The program-memory helpers come from src/AlexaCompat.h. A board never sees this file.
|
||||||
#ifndef ALEX2ESP_HOST_ARDUINO_H
|
#ifndef ALEX2ESP_HOST_ARDUINO_H
|
||||||
#define ALEX2ESP_HOST_ARDUINO_H
|
#define ALEX2ESP_HOST_ARDUINO_H
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// Host tests of discovery: the object that each example announces on <root>/discover_r, compared byte by byte
|
// Host tests of discovery: the object that each example announces on <root>/discover_r, compared byte by byte
|
||||||
// with what library 1.1.0 sent for the same sketch, and what the descriptor rows of src/AlexaInterfaces.cpp hold.
|
// with what library 1.1.0 sent for the same sketch, what the descriptor rows of src/AlexaInterfaces.cpp hold, and
|
||||||
|
// what a capability announces and refuses (src/AlexaCapability.cpp).
|
||||||
// The devices are built with the calls of the example's setup().
|
// The devices are built with the calls of the example's setup().
|
||||||
// pio test -e native
|
// pio test -e native
|
||||||
#include <unity.h>
|
#include <unity.h>
|
||||||
|
|
@ -285,6 +286,343 @@ void test_display_categories_have_the_name_of_their_enumerator(void)
|
||||||
TEST_ASSERT_EQUAL_STRING("OTHER", alexaDisplayCategoryName(static_cast<DisplayCategory>(200)));
|
TEST_ASSERT_EQUAL_STRING("OTHER", alexaDisplayCategoryName(static_cast<DisplayCategory>(200)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::string announcedBy(const AlexaDevice &device)
|
||||||
|
{
|
||||||
|
std::string announced;
|
||||||
|
serializeJson(device.getDeviceJSON(), announced);
|
||||||
|
return announced;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assertLogged(const CapturedLog &log, const char *expected)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(log.text.c_str(), expected), log.text.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiftRange
|
||||||
|
{
|
||||||
|
int minimum;
|
||||||
|
int maximum;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void fillLiftConfiguration(JsonObject configuration, void *context)
|
||||||
|
{
|
||||||
|
const LiftRange *range = static_cast<const LiftRange *>(context);
|
||||||
|
JsonObject supportedRange = configuration["supportedRange"].to<JsonObject>();
|
||||||
|
supportedRange["minimumValue"] = range->minimum;
|
||||||
|
supportedRange["maximumValue"] = range->maximum;
|
||||||
|
supportedRange["precision"] = 1;
|
||||||
|
configuration["unitOfMeasure"] = "Alexa.Unit.Percent";
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_range_controller_blind_is_announced_with_names_configuration_and_semantics(void)
|
||||||
|
{
|
||||||
|
static LiftRange range = {0, 100};
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
device.setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
|
||||||
|
AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
||||||
|
TEST_ASSERT_NOT_NULL(lift);
|
||||||
|
lift->addFriendlyAsset(PSTR("Alexa.Setting.Opening"))
|
||||||
|
.addFriendlyName("Lift", "en-US")
|
||||||
|
.setProactivelyReported(true)
|
||||||
|
.setConfiguration(fillLiftConfiguration, &range)
|
||||||
|
.addActionMapping(ActionMapping({AlexaAction::Close, AlexaAction::Lower}, "SetRangeValue", "{\"rangeValue\":0}"))
|
||||||
|
.addActionMapping(ActionMapping({AlexaAction::Raise, AlexaAction::Open}, "SetRangeValue", "{\"rangeValue\":100}"))
|
||||||
|
.addStateMapping({AlexaState::Closed}, 0.0f)
|
||||||
|
.addStateMapping({AlexaState::Open}, 1.0f, 100.0f);
|
||||||
|
|
||||||
|
assertDiscovery(
|
||||||
|
endpoint("Bedroom Blinds", "INTERIOR_BLIND",
|
||||||
|
"{\"interface\":\"Alexa.RangeController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
||||||
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":true,"
|
||||||
|
"\"supported\":[{\"name\":\"rangeValue\"}]},"
|
||||||
|
"\"semantics\":{\"actionMappings\":["
|
||||||
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Close\",\"Alexa.Actions.Lower\"],"
|
||||||
|
"\"directive\":{\"name\":\"SetRangeValue\",\"payload\":{\"rangeValue\":0}}},"
|
||||||
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Open\",\"Alexa.Actions.Raise\"],"
|
||||||
|
"\"directive\":{\"name\":\"SetRangeValue\",\"payload\":{\"rangeValue\":100}}}],"
|
||||||
|
"\"stateMappings\":["
|
||||||
|
"{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Closed\"],\"value\":0},"
|
||||||
|
"{\"@type\":\"StatesToRange\",\"states\":[\"Alexa.States.Open\"],"
|
||||||
|
"\"range\":{\"minimumValue\":1,\"maximumValue\":100}}]},"
|
||||||
|
"\"instance\":\"Blind.Lift\","
|
||||||
|
"\"capabilityResources\":{\"friendlyNames\":["
|
||||||
|
"{\"@type\":\"asset\",\"value\":{\"assetId\":\"Alexa.Setting.Opening\"}},"
|
||||||
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Lift\",\"locale\":\"en-US\"}}]},"
|
||||||
|
"\"configuration\":{\"supportedRange\":{\"minimumValue\":0,\"maximumValue\":100,\"precision\":1},"
|
||||||
|
"\"unitOfMeasure\":\"Alexa.Unit.Percent\"}}"),
|
||||||
|
device);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fillPositionConfiguration(JsonObject configuration, void *)
|
||||||
|
{
|
||||||
|
configuration["ordered"] = false;
|
||||||
|
JsonArray supportedModes = configuration["supportedModes"].to<JsonArray>();
|
||||||
|
for (const char *position : {"Up", "Down"})
|
||||||
|
{
|
||||||
|
JsonObject mode = supportedModes.add<JsonObject>();
|
||||||
|
mode["value"] = std::string("Position.") + position;
|
||||||
|
JsonObject name = mode["modeResources"]["friendlyNames"].add<JsonObject>();
|
||||||
|
name["@type"] = "text";
|
||||||
|
name["value"]["text"] = position;
|
||||||
|
name["value"]["locale"] = "en-US";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_mode_controller_is_announced_with_its_modes_and_text_states(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Garage Door", "root", "ESP-01");
|
||||||
|
device.setDisplayCategory(DisplayCategory::GARAGE_DOOR);
|
||||||
|
device.addCapability(AlexaInterfaces::ModeController, "Door.Position")
|
||||||
|
->addFriendlyAsset(PSTR("Alexa.Setting.Mode"))
|
||||||
|
.setConfiguration(fillPositionConfiguration)
|
||||||
|
.addActionMapping(ActionMapping({AlexaAction::Open}, "SetMode", "{\"mode\":\"Position.Up\"}"))
|
||||||
|
.addStateMapping({AlexaState::Closed}, "Position.Down");
|
||||||
|
|
||||||
|
assertDiscovery(
|
||||||
|
endpoint("Garage Door", "GARAGE_DOOR",
|
||||||
|
"{\"interface\":\"Alexa.ModeController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
||||||
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"mode\"}]},"
|
||||||
|
"\"semantics\":{\"actionMappings\":["
|
||||||
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Open\"],"
|
||||||
|
"\"directive\":{\"name\":\"SetMode\",\"payload\":{\"mode\":\"Position.Up\"}}}],"
|
||||||
|
"\"stateMappings\":["
|
||||||
|
"{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Closed\"],\"value\":\"Position.Down\"}]},"
|
||||||
|
"\"instance\":\"Door.Position\","
|
||||||
|
"\"capabilityResources\":{\"friendlyNames\":["
|
||||||
|
"{\"@type\":\"asset\",\"value\":{\"assetId\":\"Alexa.Setting.Mode\"}}]},"
|
||||||
|
"\"configuration\":{\"ordered\":false,\"supportedModes\":["
|
||||||
|
"{\"value\":\"Position.Up\",\"modeResources\":{\"friendlyNames\":["
|
||||||
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Up\",\"locale\":\"en-US\"}}]}},"
|
||||||
|
"{\"value\":\"Position.Down\",\"modeResources\":{\"friendlyNames\":["
|
||||||
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Down\",\"locale\":\"en-US\"}}]}}]}}"),
|
||||||
|
device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_toggle_controller_that_only_reports_is_announced_as_non_controllable(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Fan", "root", "ESP-01");
|
||||||
|
device.setDisplayCategory(DisplayCategory::FAN);
|
||||||
|
device.addCapability(AlexaInterfaces::ToggleController, "Fan.Oscillate")
|
||||||
|
->addFriendlyName("Oscillate", "en-US")
|
||||||
|
.addFriendlyName("Schwenken", "de-DE")
|
||||||
|
.setRetrievable(false)
|
||||||
|
.setNonControllable(true);
|
||||||
|
|
||||||
|
assertDiscovery(
|
||||||
|
endpoint("Fan", "FAN",
|
||||||
|
"{\"interface\":\"Alexa.ToggleController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
||||||
|
"\"properties\":{\"retrievable\":false,\"proactivelyReported\":false,"
|
||||||
|
"\"supported\":[{\"name\":\"toggleState\"}],\"nonControllable\":true},"
|
||||||
|
"\"instance\":\"Fan.Oscillate\","
|
||||||
|
"\"capabilityResources\":{\"friendlyNames\":["
|
||||||
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Oscillate\",\"locale\":\"en-US\"}},"
|
||||||
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Schwenken\",\"locale\":\"de-DE\"}}]}}"),
|
||||||
|
device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_doorbell_is_announced_without_properties(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Front Door", "root", "ESP-01");
|
||||||
|
device.setDisplayCategory(DisplayCategory::DOORBELL);
|
||||||
|
device.addCapability(AlexaInterfaces::DoorbellEventSource)->setProactivelyReported(true);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("Front Door", "DOORBELL",
|
||||||
|
"{\"interface\":\"Alexa.DoorbellEventSource\",\"version\":\"3\",\"type\":\"AlexaInterface\"}"),
|
||||||
|
device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_playback_and_wake_on_lan_are_announced_with_empty_properties(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Media PC", "root", "ESP-01");
|
||||||
|
device.setDisplayCategory(DisplayCategory::COMPUTER);
|
||||||
|
device.addCapability(AlexaInterfaces::PlaybackController);
|
||||||
|
device.addCapability(AlexaInterfaces::WakeOnLANController);
|
||||||
|
|
||||||
|
assertDiscovery(
|
||||||
|
endpoint("Media PC", "COMPUTER",
|
||||||
|
"{\"interface\":\"Alexa.PlaybackController\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"properties\":{}},"
|
||||||
|
"{\"interface\":\"Alexa.WakeOnLANController\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"properties\":{}}"),
|
||||||
|
device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_instance_and_friendly_name_are_copied(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
{
|
||||||
|
std::string instance = "Blind.Tilt";
|
||||||
|
std::string name = "Tilt";
|
||||||
|
device.addCapability(AlexaInterfaces::RangeController, instance.c_str())->addFriendlyName(name.c_str(), "en-US");
|
||||||
|
instance.assign("overwritten");
|
||||||
|
name.assign("gone");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string announced = announcedBy(device);
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(announced.c_str(), "\"instance\":\"Blind.Tilt\""));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(announced.c_str(), "\"text\":\"Tilt\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_instances_of_one_interface_are_capabilities_of_their_own(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
||||||
|
AlexaCapability *tilt = device.addCapability(AlexaInterfaces::RangeController, "Blind.Tilt");
|
||||||
|
|
||||||
|
TEST_ASSERT_NOT_NULL(lift);
|
||||||
|
TEST_ASSERT_NOT_NULL(tilt);
|
||||||
|
TEST_ASSERT_TRUE(lift != tilt);
|
||||||
|
TEST_ASSERT_EQUAL_PTR(tilt, device.addCapability(AlexaInterfaces::RangeController, "Blind.Tilt"));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Blind.Lift", lift->getInstance());
|
||||||
|
// As a 1.x sketch asks for the capability it added by its type
|
||||||
|
TEST_ASSERT_EQUAL_PTR(lift, device.addCapability(AlexaInterfaceType::RANGE_CONTROLLER));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_directive_matches_by_namespace_and_for_instanced_interfaces_by_instance(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
AlexaCapability *power = device.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(power->matches("Alexa.PowerController", nullptr));
|
||||||
|
TEST_ASSERT_TRUE(power->matches("Alexa.PowerController", "Blind.Lift"));
|
||||||
|
TEST_ASSERT_FALSE(power->matches("Alexa.RangeController", nullptr));
|
||||||
|
TEST_ASSERT_FALSE(power->matches(nullptr, nullptr));
|
||||||
|
TEST_ASSERT_TRUE(lift->matches("Alexa.RangeController", "Blind.Lift"));
|
||||||
|
TEST_ASSERT_FALSE(lift->matches("Alexa.RangeController", "Blind.Tilt"));
|
||||||
|
TEST_ASSERT_FALSE(lift->matches("Alexa.RangeController", nullptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_instanced_row_without_an_instance_is_refused_with_an_error(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
|
||||||
|
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaces::RangeController));
|
||||||
|
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaces::ModeController, ""));
|
||||||
|
|
||||||
|
assertLogged(log, "error: ESP-01: capability Alexa.RangeController not added: it needs an instance, "
|
||||||
|
"addCapability(row, \"Blind.Lift\")");
|
||||||
|
assertLogged(log, "error: ESP-01: capability Alexa.ModeController not added: it needs an instance");
|
||||||
|
assertDiscovery(endpoint("Bedroom Blinds", "OTHER", ""), device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_instance_for_an_interface_without_instances_is_refused_with_an_error(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
||||||
|
|
||||||
|
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaces::PowerController, "Lamp.Power"));
|
||||||
|
|
||||||
|
assertLogged(log, "error: ESP-01: capability Alexa.PowerController Lamp.Power not added: "
|
||||||
|
"the interface takes no instance, leave it out");
|
||||||
|
assertDiscovery(endpoint("Test Lamp", "OTHER", ""), device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_capability_added_by_type_without_an_instance_is_left_out_of_discovery(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
|
||||||
|
AlexaInterface *toggle = device.addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
|
||||||
|
TEST_ASSERT_NOT_NULL(toggle);
|
||||||
|
toggle->addFriendlyName("Bedroom Blinds", "en-US");
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("Bedroom Blinds", "OTHER", POWER), device);
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.ToggleController left out of discovery: it has no instance, "
|
||||||
|
"call setInstance(\"Blind.Lift\") on it");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_instanced_capability_without_a_friendly_name_is_left_out_of_discovery(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
device.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
device.addCapability(AlexaInterfaces::ModeController, "Blind.Mode");
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("Bedroom Blinds", "OTHER", POWER), device);
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.ModeController Blind.Mode left out of discovery: it has no name, "
|
||||||
|
"call addFriendlyName(\"Lift\", \"en-US\") on it");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_semantics_on_an_interface_that_takes_none_are_refused_with_an_error(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
||||||
|
device.addCapability(AlexaInterfaces::PowerController)
|
||||||
|
->addActionMapping(ActionMapping({AlexaAction::Open}, "TurnOn"))
|
||||||
|
.addStateMapping({AlexaState::Open}, "ON");
|
||||||
|
|
||||||
|
assertLogged(log, "error: Alexa.PowerController: action mapping not added: "
|
||||||
|
"only RangeController, ModeController and ToggleController take one");
|
||||||
|
assertLogged(log, "error: Alexa.PowerController: state mapping not added: "
|
||||||
|
"only RangeController, ModeController and ToggleController take one");
|
||||||
|
assertDiscovery(endpoint("Test Lamp", "OTHER", POWER), device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_error(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
||||||
|
for (int i = 0; i < ALEX2ESP_MAX_FRIENDLY_NAMES; i++)
|
||||||
|
{
|
||||||
|
lift->addFriendlyName("Lift", "en-US");
|
||||||
|
}
|
||||||
|
for (int i = 0; i < ALEX2ESP_MAX_ACTION_MAPPINGS; i++)
|
||||||
|
{
|
||||||
|
lift->addActionMapping(ActionMapping({AlexaAction::Open}, "SetRangeValue", "{\"rangeValue\":100}"));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < ALEX2ESP_MAX_STATE_MAPPINGS; i++)
|
||||||
|
{
|
||||||
|
lift->addStateMapping({AlexaState::Open}, 100.0f);
|
||||||
|
}
|
||||||
|
const std::string full = announcedBy(device);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", log.text.c_str() + strlen("[Alex2ESP] ESP-01: capability Alexa.RangeController Blind.Lift added\r\n"));
|
||||||
|
|
||||||
|
lift->addFriendlyName("Shade", "en-US")
|
||||||
|
.addFriendlyAsset(PSTR("Alexa.Setting.Opening"))
|
||||||
|
.addActionMapping(ActionMapping({AlexaAction::Close}, "SetRangeValue", "{\"rangeValue\":0}"))
|
||||||
|
.addStateMapping({AlexaState::Closed}, 0.0f);
|
||||||
|
|
||||||
|
assertLogged(log, "error: Alexa.RangeController Blind.Lift: friendly name \"Shade\" not added: "
|
||||||
|
"it has 3 already (ALEX2ESP_MAX_FRIENDLY_NAMES)");
|
||||||
|
assertLogged(log, "error: Alexa.RangeController Blind.Lift: asset not added: it has 3 names already");
|
||||||
|
assertLogged(log, "error: Alexa.RangeController Blind.Lift: action mapping to SetRangeValue not added: "
|
||||||
|
"it has 4 already (ALEX2ESP_MAX_ACTION_MAPPINGS)");
|
||||||
|
assertLogged(log, "error: Alexa.RangeController Blind.Lift: state mapping not added: "
|
||||||
|
"it has 2 already (ALEX2ESP_MAX_STATE_MAPPINGS)");
|
||||||
|
TEST_ASSERT_EQUAL_STRING(full.c_str(), announcedBy(device).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_action_mapping_payload_that_is_no_object_is_left_out_with_an_error(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
device.addCapability(AlexaInterfaces::ToggleController, "Blind.Toggle")
|
||||||
|
->addFriendlyName("Blind", "en-US")
|
||||||
|
.addActionMapping(ActionMapping({AlexaAction::Close}, "TurnOn", "rangeValue: 0"));
|
||||||
|
|
||||||
|
const std::string announced = announcedBy(device);
|
||||||
|
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(announced.c_str(), "\"directive\":{\"name\":\"TurnOn\"}"));
|
||||||
|
assertLogged(log, "error: Alexa.ToggleController Blind.Toggle: payload of the action mapping to TurnOn left out: "
|
||||||
|
"it is not the JSON text of an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_actions_and_states_have_the_names_of_alexa(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.Open", alexaActionName(AlexaAction::Open));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.SetEcoOff", alexaActionName(AlexaAction::SetEcoOff));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.States.Closed", alexaStateName(AlexaState::Closed));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.Lower", AlexaActionsUtils::toString(AlexaActions::Lower).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
int main(int, char **)
|
int main(int, char **)
|
||||||
{
|
{
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
|
|
@ -303,5 +641,22 @@ int main(int, char **)
|
||||||
RUN_TEST(test_thermostat_announces_its_four_properties);
|
RUN_TEST(test_thermostat_announces_its_four_properties);
|
||||||
RUN_TEST(test_directive_gets_the_type_of_the_capability_it_names);
|
RUN_TEST(test_directive_gets_the_type_of_the_capability_it_names);
|
||||||
RUN_TEST(test_display_categories_have_the_name_of_their_enumerator);
|
RUN_TEST(test_display_categories_have_the_name_of_their_enumerator);
|
||||||
|
|
||||||
|
RUN_TEST(test_range_controller_blind_is_announced_with_names_configuration_and_semantics);
|
||||||
|
RUN_TEST(test_mode_controller_is_announced_with_its_modes_and_text_states);
|
||||||
|
RUN_TEST(test_toggle_controller_that_only_reports_is_announced_as_non_controllable);
|
||||||
|
RUN_TEST(test_doorbell_is_announced_without_properties);
|
||||||
|
RUN_TEST(test_playback_and_wake_on_lan_are_announced_with_empty_properties);
|
||||||
|
RUN_TEST(test_instance_and_friendly_name_are_copied);
|
||||||
|
RUN_TEST(test_instances_of_one_interface_are_capabilities_of_their_own);
|
||||||
|
RUN_TEST(test_directive_matches_by_namespace_and_for_instanced_interfaces_by_instance);
|
||||||
|
RUN_TEST(test_instanced_row_without_an_instance_is_refused_with_an_error);
|
||||||
|
RUN_TEST(test_instance_for_an_interface_without_instances_is_refused_with_an_error);
|
||||||
|
RUN_TEST(test_capability_added_by_type_without_an_instance_is_left_out_of_discovery);
|
||||||
|
RUN_TEST(test_instanced_capability_without_a_friendly_name_is_left_out_of_discovery);
|
||||||
|
RUN_TEST(test_semantics_on_an_interface_that_takes_none_are_refused_with_an_error);
|
||||||
|
RUN_TEST(test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_error);
|
||||||
|
RUN_TEST(test_action_mapping_payload_that_is_no_object_is_left_out_with_an_error);
|
||||||
|
RUN_TEST(test_actions_and_states_have_the_names_of_alexa);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue