Describe interfaces with PROGMEM descriptor rows; drop the switch tables

AlexaInterfaces.h/.cpp hold one row per interface in program memory (28: namespace,
version, property names, flags, type), each row and string an object of its own, so
the linker keeps only the rows a sketch names. addCapability(AlexaInterfaceType) is
an always-inline switch that folds to one row; a type without a row is refused with
an error. The Event handler gets its type from the capabilities of the device.
Versions follow the interface pages (EndpointHealth 3.1, ThermostatController 3.2),
HumiditySensor is new, the two withdrawn interfaces and AlexaInterfaceUtils are gone.
Display category names are in flash. The discovery objects of the five examples are
unchanged: 14 new host tests compare them with what the previous commit announced.
basicLight on a D1 mini: static RAM 34,508 -> 30,788, flash 338,533 -> 330,509;
it links two rows, EndpointHealth and PowerController. Host tests: 47 -> 61.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 19:04:14 +00:00
parent d534f23f27
commit 8b11cc2394
13 changed files with 909 additions and 777 deletions

View file

@ -11,7 +11,8 @@ Alex2ESPState KEYWORD1
AlexaDevice KEYWORD1
AlexaInterface KEYWORD1
AlexaInterfaceType KEYWORD1
AlexaInterfaceUtils KEYWORD1
AlexaInterfaces KEYWORD1
AlexaInterfaceDesc KEYWORD1
AlexaStatusMessage KEYWORD1
ActionMapping KEYWORD1
AlexaActions KEYWORD1

View file

@ -1,7 +1,8 @@
; Development project of the library itself. A sketch does not need this file: it gets the library through
; lib_deps (readme.md, Quick Start).
;
; pio test -e native host tests of the bridge logic in src/AlexaBridgeLogic.cpp; no board, no broker
; 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
[platformio]
default_envs = native
@ -11,7 +12,8 @@ platform = native
test_framework = unity
; Only the sources without a hardware dependency are compiled with the tests
test_build_src = yes
build_src_filter = -<*> +<AlexaBridgeLogic.cpp>
build_src_filter = -<*> +<AlexaBridgeLogic.cpp> +<AlexaDevice.cpp> +<AlexaInterfaces.cpp> +<AlexaLog.cpp>
lib_deps =
bblanchon/ArduinoJson@^7
build_flags = -std=gnu++17 -Wall -Wextra
; test/host_arduino holds the Arduino.h of the host: String, Print and Serial
build_flags = -std=gnu++17 -Wall -Wextra -Itest/host_arduino

108
readme.md
View file

@ -149,71 +149,43 @@ Boards that run 1.1.0 or older keep working: the backend still publishes the tok
---
## Interface Types
| Alexa Interface Type | Status |
|-----------------------------------------------------|-------------|
| AlexaInterfaceType::ENDPOINT_HEALTH | Fully Supported |
| AlexaInterfaceType::POWER_CONTROLLER | Fully Supported |
| AlexaInterfaceType::BRIGHTNESS_CONTROLLER | Fully Supported |
| AlexaInterfaceType::TOGGLE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::TEMPERATURE_SENSOR | Fully Supported |
| AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::AUTOMATION_MANAGEMENT | Supported* |
| AlexaInterfaceType::CHANNEL_CONTROLLER | Supported* |
| AlexaInterfaceType::COLOR_CONTROLLER | Supported* |
| AlexaInterfaceType::CONTACT_SENSOR | Supported* |
| AlexaInterfaceType::APPLICATION_STATE_REPORTER | Supported* |
| AlexaInterfaceType::AUDIO_PLAY_QUEUE | Supported* |
| AlexaInterfaceType::AUTHORIZATION_CONTROLLER | Supported* |
| AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA | Supported* |
| AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER | Supported* |
| AlexaInterfaceType::CAMERA_STREAM_CONTROLLER | Supported* |
| AlexaInterfaceType::COMMISSIONABLE | Supported* |
| AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER | Supported* |
| AlexaInterfaceType::COOKING | Supported* |
| AlexaInterfaceType::DATA_CONTROLLER | Supported* |
| AlexaInterfaceType::DEVICE_USAGE_ESTIMATION | Supported* |
| AlexaInterfaceType::DEVICE_USAGE_METER | Supported* |
| AlexaInterfaceType::DOORBELL_EVENT_SOURCE | Supported* |
| AlexaInterfaceType::EQUALIZER_CONTROLLER | Supported* |
| AlexaInterfaceType::INPUT_CONTROLLER | Supported* |
| AlexaInterfaceType::INVENTORY_LEVEL_SENSOR | Supported* |
| AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR | Supported* |
| AlexaInterfaceType::INVENTORY_USAGE_SENSOR | Supported* |
| AlexaInterfaceType::KEYPAD_CONTROLLER | Supported* |
| AlexaInterfaceType::LAUNCHER | Supported* |
| AlexaInterfaceType::LOCK_CONTROLLER | Supported* |
| AlexaInterfaceType::MEDIA_PLAYBACK | Supported* |
| AlexaInterfaceType::MEDIA_SEARCH | Supported* |
| AlexaInterfaceType::MODE_CONTROLLER | Supported* |
| AlexaInterfaceType::MOTION_SENSOR | Supported* |
| AlexaInterfaceType::PERCENTAGE_CONTROLLER | Supported* |
| AlexaInterfaceType::PLAYBACK_CONTROLLER | Supported* |
| AlexaInterfaceType::PLAYBACK_STATE_REPORTER | Supported* |
| AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE | Supported* |
| AlexaInterfaceType::RANGE_CONTROLLER | Supported* |
| AlexaInterfaceType::RECORD_CONTROLLER | Supported* |
| AlexaInterfaceType::REMOTE_VIDEO_PLAYER | Supported* |
| AlexaInterfaceType::RTC_SESSION_CONTROLLER | Supported* |
| AlexaInterfaceType::SCENE_CONTROLLER | Supported* |
| AlexaInterfaceType::SECURITY_PANEL_CONTROLLER | Supported* |
| AlexaInterfaceType::SEEK_CONTROLLER | Supported* |
| AlexaInterfaceType::SIMPLE_EVENT_SOURCE | Supported* |
| AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR | Supported* |
| AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER | Supported* |
| AlexaInterfaceType::SPEAKER | Supported* |
| AlexaInterfaceType::STEP_SPEAKER | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE | Supported* |
| AlexaInterfaceType::TIME_HOLD_CONTROLLER | Supported* |
| AlexaInterfaceType::UI_CONTROLLER | Supported* |
| AlexaInterfaceType::USER_PREFERENCE | Supported* |
| AlexaInterfaceType::VIDEO_RECORDER | Supported* |
| AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER | Supported* |
The library describes these interfaces. `addCapability()` takes the row, `AlexaInterfaces::PowerController`, or the type as 1.x sketches write it, `AlexaInterfaceType::POWER_CONTROLLER`. A sketch links the rows it names and no others.
(*Partial support or limited implementation advanced configuration is required)
| Row in `AlexaInterfaces` | `AlexaInterfaceType::` | Version | Properties | Report helper |
|---|---|---|---|---|
| `EndpointHealth` | `ENDPOINT_HEALTH` | 3.1 | connectivity | `AddHealthProp` |
| `PowerController` | `POWER_CONTROLLER` | 3 | powerState | `AddPowerControllerProp` |
| `BrightnessController` | `BRIGHTNESS_CONTROLLER` | 3 | brightness | `AddBrightnessControllerProp` |
| `ColorTemperatureController` | `COLOR_TEMPERATURE_CONTROLLER` | 3 | colorTemperatureInKelvin | `AddColorTemperatureControllerProp` |
| `ToggleController` | `TOGGLE_CONTROLLER` | 3 | toggleState | `AddToggleControllerProp` |
| `TemperatureSensor` | `TEMPERATURE_SENSOR` | 3 | temperature | `AddTemperatureSensorProp` |
| `ColorController` | `COLOR_CONTROLLER` | 3 | color | `AddContextProp` |
| `PowerLevelController` | `POWER_LEVEL_CONTROLLER` | 3 | powerLevel | `AddContextProp` |
| `PercentageController` | `PERCENTAGE_CONTROLLER` | 3 | percentage | `AddContextProp` |
| `RangeController` | `RANGE_CONTROLLER` | 3 | rangeValue | `AddContextProp` |
| `ModeController` | `MODE_CONTROLLER` | 3 | mode | `AddContextProp` |
| `ThermostatController` | `THERMOSTAT_CONTROLLER` | 3.2 | targetSetpoint, lowerSetpoint, upperSetpoint, thermostatMode | `AddContextProp` |
| `HumiditySensor` | `HUMIDITY_SENSOR` | 3 | relativeHumidity | `AddContextProp` |
| `LockController` | `LOCK_CONTROLLER` | 3 | lockState | `AddContextProp` |
| `ContactSensor` | `CONTACT_SENSOR` | 3 | detectionState | `AddContextProp` |
| `MotionSensor` | `MOTION_SENSOR` | 3 | detectionState | `AddContextProp` |
| `TimeHoldController` | `TIME_HOLD_CONTROLLER` | 3 | holdStartTime, holdEndTime | `AddContextProp` |
| `Speaker` | `SPEAKER` | 3 | volume, muted | `AddContextProp` |
| `PlaybackStateReporter` | `PLAYBACK_STATE_REPORTER` | 3 | playbackState | `AddContextProp` |
| `InputController` | `INPUT_CONTROLLER` | 3 | input | `AddContextProp` |
| `ChannelController` | `CHANNEL_CONTROLLER` | 3 | channel | `AddContextProp` |
| `InventoryLevelSensor` | `INVENTORY_LEVEL_SENSOR` | 3 | level | `AddContextProp` |
| `PlaybackController` | `PLAYBACK_CONTROLLER` | 3 | none | |
| `WakeOnLANController` | `WAKE_ON_LAN_CONTROLLER` | 3 | none | |
| `SceneController` | `SCENE_CONTROLLER` | 3 | no `properties` object | |
| `DoorbellEventSource` | `DOORBELL_EVENT_SOURCE` | 3 | no `properties` object | |
| `StepSpeaker` | `STEP_SPEAKER` | 3 | no `properties` object | |
| `SimpleEventSource` | `SIMPLE_EVENT_SOURCE` | 1.0 | no `properties` object | |
The interfaces below the first six are announced with their version and properties; what else their discovery object needs (the configuration of a thermostat, the presets of a range) and their events are not built by the library yet.
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`.
---
@ -256,10 +228,16 @@ Behaviour changes:
- A report is sent through the client that created its device. An `AlexaDevice` or an `AlexaStatusMessage` that a sketch constructs itself has no client: `send()` returns `false` and prints `report for <endpointId> not sent: its device was not created by getDevice()`, where 1.1.0 queued the report. Use `getDevice()` and `buildStatusMessage()`. Both constructors take the client as an optional last argument, an `AlexaTransport*`.
- New: `Alex2ESP::setLogLevel()`, `Alex2ESP::setTimeSource()`, `AlexaDevice::hasEndpointId()`, `AlexaLog`, `AlexaSendResult`, and `AlexaTransport`, the interface a device sends and stamps its reports through. `Alex2ESP` implements it with `publish(topic, document)`, which sends a `JsonDocument` on the session of the library under the same checks as a report and returns an `AlexaSendResult`, and `timestamp(buffer, size)`, which writes the current time as `timeOfSample` has it.
- Removed: the queues and buffers of `AlexaUtils` (`enqueue`, `dequeue`, `dequeueVals`, `enqueueReceive`, `dequeueReceive`, `isQueueEmpty`, `isQueueFull`, `isReceiveQueueEmpty`, `isReceiveQueueFull`, `receivePayload`, `nextMessageId`) and its `log`/`logln`, which printed nothing unless the library was edited; `AlexaUtils::printMemoryInfo()` stays. `MAX_STATUS_REPORT_SIZE` (the limit is `ALEX2ESP_MAX_MESSAGE`). The library no longer includes `ESP8266HTTPClient`.
- Interfaces are described by rows in program memory, `AlexaInterfaces::PowerController` and 27 more (see Interface Types), in place of the `switch` tables of `AlexaInterfaceUtils`, whose strings took RAM in every sketch. `addCapability()` takes a row or, as before, an `AlexaInterfaceType`; a sketch links only the rows it names. The discovery objects of the five examples are the same byte for byte.
- Versions and properties follow the interface pages of Alexa: `EndpointHealth` is announced as 3.1 (1.1.0: 3.3) and `ThermostatController` as 3.2 with its four properties (1.1.0: 3, none). `Speaker`, `StepSpeaker`, `PlaybackStateReporter`, `InventoryLevelSensor` and `WakeOnLANController` are announced as 3 (1.1.0: 1), `SimpleEventSource` as 1.0. The interfaces that 1.1.0 announced with an empty list of properties have their properties (`lockState`, `detectionState`, `mode`, `rangeValue`, `percentage`, ...). `SceneController`, `DoorbellEventSource`, `StepSpeaker` and `SimpleEventSource` are announced without a `properties` object.
- New: `AlexaInterfaceType::HUMIDITY_SENSOR` with its row, `AlexaInterfaces::HumiditySensor`. `AlexaDevice::getInterfaceType(namespace)`.
- `addCapability()` with a type that has no row adds nothing, prints an error and returns `nullptr`. 1.1.0 announced such an interface as version 1 without properties.
- The handler of `Event` gets the type of the capability of its device that the directive names, and `AlexaInterfaceType::UNKNOWN` for a namespace the device has no capability for. 1.1.0 looked the namespace up among all interfaces.
- Removed: `AlexaInterfaceType::AUTHORIZATION_CONTROLLER` and `AUTOMOTIVE_VEHICLE_DATA`, which Alexa has withdrawn. `AlexaInterfaceUtils` (`toString`, `fromString`, `getVersion`, `getProps`): a row has the namespace, the version and the properties, `alexaInterfaceRow(type)` gives the row of a type. The constructor of `AlexaInterface` takes a row. `DisplayCategory` and `AlexaInterfaceType` are one byte wide.
Memory: `examples/basicLight.cpp` for a D1 mini takes 34,508 bytes of static RAM (1.1.0: 52,768) and 338,533 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). The static RAM was the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients. SNTP and the time stamp are 1.8 KB of the flash figure.
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.
Tests: `pio test -e native` in the repository runs 47 host tests 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). No board is needed.
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.
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.

View file

@ -427,7 +427,7 @@ void Alex2ESP::processDirective()
}
else
{
device->triggerEvent("Event", message, AlexaInterfaceUtils::fromString(interfaceName));
device->triggerEvent("Event", message, device->getInterfaceType(interfaceName));
}
}

View file

@ -1,14 +1,23 @@
// Lets the sources that have no hardware dependency compile on a host (pio test -e native), where the
// program-memory helpers of the Arduino cores do not exist. On a board this is Arduino.h and nothing else.
// program-memory helpers of the Arduino cores do not exist: there a PROGMEM object is an ordinary constant and a
// pointer to one an ordinary pointer. On a board this is Arduino.h and nothing else.
#ifndef ALEXA_COMPAT_H
#define ALEXA_COMPAT_H
#if defined(ARDUINO)
#include <Arduino.h>
#else
#ifndef PSTR
#include <stdint.h>
#include <string.h>
#define PROGMEM
#define PGM_P const char *
#define PSTR(text) (text)
#endif
#define F(text) (text)
#define FPSTR(text) (text)
#define pgm_read_byte(address) (*reinterpret_cast<const uint8_t *>(address))
#define pgm_read_ptr(address) (*reinterpret_cast<const void *const *>(address))
#define strcmp_P strcmp
#define strncpy_P strncpy
#endif
#endif // ALEXA_COMPAT_H

View file

@ -72,23 +72,43 @@ String AlexaDevice::getSoftwareVersion() const {
}
AlexaInterface* AlexaDevice::addCapability(AlexaInterfaceType type) {
// Check if a interface with the given type already exists
AlexaInterface* AlexaDevice::addCapability(const AlexaInterfaceDesc& row) {
// Check if a interface with the given row already exists
for (auto& iface : capabilities) {
if (iface.getType() == type) {
if (&iface.getRow() == &row) {
return &iface; // Return the existing interface
}
}
// If the interface doesn't exist, create a new one
capabilities.emplace_back(type);
capabilities.emplace_back(row);
ALEX2ESP_LOGI("%s: capability %s added", endpointId.c_str(), capabilities.back().getTypeString().c_str());
// 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();
}
AlexaInterface* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
ALEX2ESP_LOGE("%s: capability not added: the library has no description of interface type %d; "
"readme.md lists the interfaces it has (Interface Types)",
endpointId.c_str(), static_cast<int>(type));
return nullptr;
}
AlexaInterfaceType AlexaDevice::getInterfaceType(const char* interfaceName) const {
for (const AlexaInterface& capability : capabilities) {
if (strcmp_P(interfaceName, capability.getRow().ns) == 0) {
return capability.getType();
}
}
return AlexaInterfaceType::UNKNOWN;
}
JsonDocument AlexaDevice::getDeviceJSON() const {
@ -104,7 +124,7 @@ JsonDocument AlexaDevice::getDeviceJSON() const {
JsonArray displayCategories = json["displayCategories"].to<JsonArray>();
displayCategories.add(DisplayCategoryUtils::toString(displayCategory));
displayCategories.add(FPSTR(alexaDisplayCategoryName(displayCategory)));
JsonObject additionalAttributes = json["additionalAttributes"].to<JsonObject>();
additionalAttributes["manufacturer"] = manufacturer;

View file

@ -6,7 +6,6 @@
#include "AlexaInterface.h"
#include <functional>
#include <map>
#include <unordered_map>
#include <string>
#include <deque>
#include "AlexaStatusMessage.h"
@ -16,134 +15,13 @@
#define MAX_EVENTS 10
enum class DisplayCategory {
ACTIVITY_TRIGGER,
AIR_CONDITIONER,
AIR_FRESHENER,
AIR_PURIFIER,
AIR_QUALITY_MONITOR,
ALEXA_VOICE_ENABLED,
AUTO_ACCESSORY,
BLUETOOTH_SPEAKER,
CAMERA,
CHRISTMAS_TREE,
COFFEE_MAKER,
COMPUTER,
CONTACT_SENSOR,
DISHWASHER,
DOOR,
DOORBELL,
DRYER,
EXTERIOR_BLIND,
FAN,
GAME_CONSOLE,
GARAGE_DOOR,
HEADPHONES,
HUB,
INTERIOR_BLIND,
LAPTOP,
LIGHT,
MICROWAVE,
MOBILE_PHONE,
MOTION_SENSOR,
MUSIC_SYSTEM,
NETWORK_HARDWARE,
OTHER,
OVEN,
PHONE,
PRINTER,
REMOTE,
ROUTER,
SCENE_TRIGGER,
SCREEN,
SECURITY_PANEL,
SECURITY_SYSTEM,
SLOW_COOKER,
SMARTLOCK,
SMARTPLUG,
SPEAKER,
STREAMING_DEVICE,
SWITCH,
TABLET,
TEMPERATURE_SENSOR,
THERMOSTAT,
TV,
VACUUM_CLEANER,
VEHICLE,
WASHER,
WATER_HEATER,
WEARABLE
};
class DisplayCategoryUtils {
public:
static String toString(DisplayCategory category) {
static const std::unordered_map<DisplayCategory, String> categoryMap = {
{DisplayCategory::ACTIVITY_TRIGGER, "ACTIVITY_TRIGGER"},
{DisplayCategory::AIR_CONDITIONER, "AIR_CONDITIONER"},
{DisplayCategory::AIR_FRESHENER, "AIR_FRESHENER"},
{DisplayCategory::AIR_PURIFIER, "AIR_PURIFIER"},
{DisplayCategory::AIR_QUALITY_MONITOR, "AIR_QUALITY_MONITOR"},
{DisplayCategory::ALEXA_VOICE_ENABLED, "ALEXA_VOICE_ENABLED"},
{DisplayCategory::AUTO_ACCESSORY, "AUTO_ACCESSORY"},
{DisplayCategory::BLUETOOTH_SPEAKER, "BLUETOOTH_SPEAKER"},
{DisplayCategory::CAMERA, "CAMERA"},
{DisplayCategory::CHRISTMAS_TREE, "CHRISTMAS_TREE"},
{DisplayCategory::COFFEE_MAKER, "COFFEE_MAKER"},
{DisplayCategory::COMPUTER, "COMPUTER"},
{DisplayCategory::CONTACT_SENSOR, "CONTACT_SENSOR"},
{DisplayCategory::DISHWASHER, "DISHWASHER"},
{DisplayCategory::DOOR, "DOOR"},
{DisplayCategory::DOORBELL, "DOORBELL"},
{DisplayCategory::DRYER, "DRYER"},
{DisplayCategory::EXTERIOR_BLIND, "EXTERIOR_BLIND"},
{DisplayCategory::FAN, "FAN"},
{DisplayCategory::GAME_CONSOLE, "GAME_CONSOLE"},
{DisplayCategory::GARAGE_DOOR, "GARAGE_DOOR"},
{DisplayCategory::HEADPHONES, "HEADPHONES"},
{DisplayCategory::HUB, "HUB"},
{DisplayCategory::INTERIOR_BLIND, "INTERIOR_BLIND"},
{DisplayCategory::LAPTOP, "LAPTOP"},
{DisplayCategory::LIGHT, "LIGHT"},
{DisplayCategory::MICROWAVE, "MICROWAVE"},
{DisplayCategory::MOBILE_PHONE, "MOBILE_PHONE"},
{DisplayCategory::MOTION_SENSOR, "MOTION_SENSOR"},
{DisplayCategory::MUSIC_SYSTEM, "MUSIC_SYSTEM"},
{DisplayCategory::NETWORK_HARDWARE, "NETWORK_HARDWARE"},
{DisplayCategory::OTHER, "OTHER"},
{DisplayCategory::OVEN, "OVEN"},
{DisplayCategory::PHONE, "PHONE"},
{DisplayCategory::PRINTER, "PRINTER"},
{DisplayCategory::REMOTE, "REMOTE"},
{DisplayCategory::ROUTER, "ROUTER"},
{DisplayCategory::SCENE_TRIGGER, "SCENE_TRIGGER"},
{DisplayCategory::SCREEN, "SCREEN"},
{DisplayCategory::SECURITY_PANEL, "SECURITY_PANEL"},
{DisplayCategory::SECURITY_SYSTEM, "SECURITY_SYSTEM"},
{DisplayCategory::SLOW_COOKER, "SLOW_COOKER"},
{DisplayCategory::SMARTLOCK, "SMARTLOCK"},
{DisplayCategory::SMARTPLUG, "SMARTPLUG"},
{DisplayCategory::SPEAKER, "SPEAKER"},
{DisplayCategory::STREAMING_DEVICE, "STREAMING_DEVICE"},
{DisplayCategory::SWITCH, "SWITCH"},
{DisplayCategory::TABLET, "TABLET"},
{DisplayCategory::TEMPERATURE_SENSOR, "TEMPERATURE_SENSOR"},
{DisplayCategory::THERMOSTAT, "THERMOSTAT"},
{DisplayCategory::TV, "TV"},
{DisplayCategory::VACUUM_CLEANER, "VACUUM_CLEANER"},
{DisplayCategory::VEHICLE, "VEHICLE"},
{DisplayCategory::WASHER, "WASHER"},
{DisplayCategory::WATER_HEATER, "WATER_HEATER"},
{DisplayCategory::WEARABLE, "WEARABLE"}
return String(FPSTR(alexaDisplayCategoryName(category)));
}
};
auto it = categoryMap.find(category);
if (it != categoryMap.end()) {
return it->second;
}
return "UNKNOWN";
}
};
@ -180,9 +58,23 @@ public:
JsonDocument getDeviceJSON() const;
// Returns the capability of this type, creating it on first use. 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.
AlexaInterface* addCapability(AlexaInterfaceType type);
// Returns the capability that describes this interface, creating it on first use. 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.
AlexaInterface* addCapability(const AlexaInterfaceDesc& row);
// The same by the type of the interface, as 1.x sketches write it. A type without a row in AlexaInterfaces
// cannot be described: nothing is added, the reason is printed and nullptr returned. Inlined, so that a type
// written out in the sketch links its row only (alexaInterfaceRow).
__attribute__((always_inline)) AlexaInterface* addCapability(AlexaInterfaceType type) {
const AlexaInterfaceDesc* row = alexaInterfaceRow(type);
return row != nullptr ? addCapability(*row) : refuseCapability(type);
}
// The type of the capability of this device with the namespace of a directive ("Alexa.PowerController"),
// UNKNOWN when the device has none
AlexaInterfaceType getInterfaceType(const char* interfaceName) const;
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
@ -194,6 +86,8 @@ public:
}
private:
AlexaInterface* refuseCapability(AlexaInterfaceType type);
String name;
String endpointId;
String rootTopic;

View file

@ -3,548 +3,11 @@
#include <string>
#include <vector>
#include <unordered_map>
#include <Arduino.h>
#include <ArduinoJson.h>
#include "AlexaInterfaces.h"
#include "AlexaLog.h"
// Enum for Alexa Interface Types
enum class AlexaInterfaceType
{
APPLICATION_STATE_REPORTER,
AUDIO_PLAY_QUEUE,
AUTHORIZATION_CONTROLLER,
AUTOMATION_MANAGEMENT,
AUTOMOTIVE_VEHICLE_DATA,
BRIGHTNESS_CONTROLLER,
CAMERA_LIVE_VIEW_CONTROLLER,
CAMERA_STREAM_CONTROLLER,
CHANNEL_CONTROLLER,
COLOR_CONTROLLER,
COLOR_TEMPERATURE_CONTROLLER,
COMMISSIONABLE,
CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER,
CONTACT_SENSOR,
COOKING,
COOKING_FOOD_TEMPERATURE_CONTROLLER,
COOKING_FOOD_TEMPERATURE_SENSOR,
COOKING_PRESET_CONTROLLER,
COOKING_TEMPERATURE_CONTROLLER,
COOKING_TEMPERATURE_SENSOR,
COOKING_TIME_CONTROLLER,
DATA_CONTROLLER,
DEVICE_USAGE_ESTIMATION,
DEVICE_USAGE_METER,
DOORBELL_EVENT_SOURCE,
ENDPOINT_HEALTH,
EQUALIZER_CONTROLLER,
INPUT_CONTROLLER,
INVENTORY_LEVEL_SENSOR,
INVENTORY_LEVEL_USAGE_SENSOR,
INVENTORY_USAGE_SENSOR,
KEYPAD_CONTROLLER,
LAUNCHER,
LOCK_CONTROLLER,
MEDIA_PLAYBACK,
MEDIA_PLAY_QUEUE,
MEDIA_SEARCH,
MODE_CONTROLLER,
MOTION_SENSOR,
PERCENTAGE_CONTROLLER,
PLAYBACK_CONTROLLER,
PLAYBACK_STATE_REPORTER,
POWER_CONTROLLER,
POWER_LEVEL_CONTROLLER,
PROACTIVE_NOTIFICATION_SOURCE,
RANGE_CONTROLLER,
RECORD_CONTROLLER,
REMOTE_VIDEO_PLAYER,
RTC_SESSION_CONTROLLER,
SCENE_CONTROLLER,
SECURITY_PANEL_CONTROLLER,
SECURITY_PANEL_CONTROLLER_ALERT,
SEEK_CONTROLLER,
SIMPLE_EVENT_SOURCE,
SMART_VISION_OBJECT_DETECTION_SENSOR,
SMART_VISION_SNAPSHOT_PROVIDER,
SPEAKER,
STEP_SPEAKER,
TEMPERATURE_SENSOR,
THERMOSTAT_CONTROLLER,
THERMOSTAT_CONTROLLER_CONFIGURATION,
THERMOSTAT_CONTROLLER_HVAC_COMPONENTS,
THERMOSTAT_CONTROLLER_SCHEDULE,
TIME_HOLD_CONTROLLER,
TOGGLE_CONTROLLER,
UI_CONTROLLER,
USER_PREFERENCE,
VIDEO_RECORDER,
WAKE_ON_LAN_CONTROLLER,
UNKNOWN
};
// Utility class to handle Alexa Interface type details
class AlexaInterfaceUtils
{
public:
static AlexaInterfaceType fromString(const String& str)
{
if (str == "Alexa.ApplicationStateReporter")
return AlexaInterfaceType::APPLICATION_STATE_REPORTER;
if (str == "Alexa.Audio.PlayQueue")
return AlexaInterfaceType::AUDIO_PLAY_QUEUE;
if (str == "Alexa.AuthorizationController")
return AlexaInterfaceType::AUTHORIZATION_CONTROLLER;
if (str == "Alexa.AutomationManagement")
return AlexaInterfaceType::AUTOMATION_MANAGEMENT;
if (str == "Alexa.Automotive.VehicleData")
return AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA;
if (str == "Alexa.BrightnessController")
return AlexaInterfaceType::BRIGHTNESS_CONTROLLER;
if (str == "Alexa.Camera.LiveViewController")
return AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER;
if (str == "Alexa.CameraStreamController")
return AlexaInterfaceType::CAMERA_STREAM_CONTROLLER;
if (str == "Alexa.ChannelController")
return AlexaInterfaceType::CHANNEL_CONTROLLER;
if (str == "Alexa.ColorController")
return AlexaInterfaceType::COLOR_CONTROLLER;
if (str == "Alexa.ColorTemperatureController")
return AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER;
if (str == "Alexa.Commissionable")
return AlexaInterfaceType::COMMISSIONABLE;
if (str == "Alexa.ConsentManagement.ConsentRequiredReporter")
return AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER;
if (str == "Alexa.ContactSensor")
return AlexaInterfaceType::CONTACT_SENSOR;
if (str == "Alexa.Cooking")
return AlexaInterfaceType::COOKING;
if (str == "Alexa.Cooking.FoodTemperatureController")
return AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_CONTROLLER;
if (str == "Alexa.Cooking.FoodTemperatureSensor")
return AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_SENSOR;
if (str == "Alexa.Cooking.PresetController")
return AlexaInterfaceType::COOKING_PRESET_CONTROLLER;
if (str == "Alexa.Cooking.TemperatureController")
return AlexaInterfaceType::COOKING_TEMPERATURE_CONTROLLER;
if (str == "Alexa.Cooking.TemperatureSensor")
return AlexaInterfaceType::COOKING_TEMPERATURE_SENSOR;
if (str == "Alexa.Cooking.TimeController")
return AlexaInterfaceType::COOKING_TIME_CONTROLLER;
if (str == "Alexa.DataController")
return AlexaInterfaceType::DATA_CONTROLLER;
if (str == "Alexa.DeviceUsage.Estimation")
return AlexaInterfaceType::DEVICE_USAGE_ESTIMATION;
if (str == "Alexa.DeviceUsage.Meter")
return AlexaInterfaceType::DEVICE_USAGE_METER;
if (str == "Alexa.DoorbellEventSource")
return AlexaInterfaceType::DOORBELL_EVENT_SOURCE;
if (str == "Alexa.EndpointHealth")
return AlexaInterfaceType::ENDPOINT_HEALTH;
if (str == "Alexa.EqualizerController")
return AlexaInterfaceType::EQUALIZER_CONTROLLER;
if (str == "Alexa.InputController")
return AlexaInterfaceType::INPUT_CONTROLLER;
if (str == "Alexa.InventoryLevelSensor")
return AlexaInterfaceType::INVENTORY_LEVEL_SENSOR;
if (str == "Alexa.InventoryLevelUsageSensor")
return AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR;
if (str == "Alexa.InventoryUsageSensor")
return AlexaInterfaceType::INVENTORY_USAGE_SENSOR;
if (str == "Alexa.KeypadController")
return AlexaInterfaceType::KEYPAD_CONTROLLER;
if (str == "Alexa.Launcher")
return AlexaInterfaceType::LAUNCHER;
if (str == "Alexa.LockController")
return AlexaInterfaceType::LOCK_CONTROLLER;
if (str == "Alexa.Media.Playback")
return AlexaInterfaceType::MEDIA_PLAYBACK;
if (str == "Alexa.Media.PlayQueue")
return AlexaInterfaceType::MEDIA_PLAY_QUEUE;
if (str == "Alexa.Media.Search")
return AlexaInterfaceType::MEDIA_SEARCH;
if (str == "Alexa.ModeController")
return AlexaInterfaceType::MODE_CONTROLLER;
if (str == "Alexa.MotionSensor")
return AlexaInterfaceType::MOTION_SENSOR;
if (str == "Alexa.PercentageController")
return AlexaInterfaceType::PERCENTAGE_CONTROLLER;
if (str == "Alexa.PlaybackController")
return AlexaInterfaceType::PLAYBACK_CONTROLLER;
if (str == "Alexa.PlaybackStateReporter")
return AlexaInterfaceType::PLAYBACK_STATE_REPORTER;
if (str == "Alexa.PowerController")
return AlexaInterfaceType::POWER_CONTROLLER;
if (str == "Alexa.PowerLevelController")
return AlexaInterfaceType::POWER_LEVEL_CONTROLLER;
if (str == "Alexa.ProactiveNotificationSource")
return AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE;
if (str == "Alexa.RangeController")
return AlexaInterfaceType::RANGE_CONTROLLER;
if (str == "Alexa.RecordController")
return AlexaInterfaceType::RECORD_CONTROLLER;
if (str == "Alexa.RemoteVideoPlayer")
return AlexaInterfaceType::REMOTE_VIDEO_PLAYER;
if (str == "Alexa.RTCSessionController")
return AlexaInterfaceType::RTC_SESSION_CONTROLLER;
if (str == "Alexa.SceneController")
return AlexaInterfaceType::SCENE_CONTROLLER;
if (str == "Alexa.SecurityPanelController")
return AlexaInterfaceType::SECURITY_PANEL_CONTROLLER;
if (str == "Alexa.SecurityPanelController.Alert")
return AlexaInterfaceType::SECURITY_PANEL_CONTROLLER_ALERT;
if (str == "Alexa.SeekController")
return AlexaInterfaceType::SEEK_CONTROLLER;
if (str == "Alexa.SimpleEventSource")
return AlexaInterfaceType::SIMPLE_EVENT_SOURCE;
if (str == "Alexa.SmartVision.ObjectDetectionSensor")
return AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR;
if (str == "Alexa.SmartVision.SnapshotProvider")
return AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER;
if (str == "Alexa.Speaker")
return AlexaInterfaceType::SPEAKER;
if (str == "Alexa.StepSpeaker")
return AlexaInterfaceType::STEP_SPEAKER;
if (str == "Alexa.TemperatureSensor")
return AlexaInterfaceType::TEMPERATURE_SENSOR;
if (str == "Alexa.ThermostatController")
return AlexaInterfaceType::THERMOSTAT_CONTROLLER;
if (str == "Alexa.ThermostatController.Configuration")
return AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION;
if (str == "Alexa.ThermostatController.HVAC.Components")
return AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS;
if (str == "Alexa.ThermostatController.Schedule")
return AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE;
if (str == "Alexa.TimeHoldController")
return AlexaInterfaceType::TIME_HOLD_CONTROLLER;
if (str == "Alexa.ToggleController")
return AlexaInterfaceType::TOGGLE_CONTROLLER;
if (str == "Alexa.UIController")
return AlexaInterfaceType::UI_CONTROLLER;
if (str == "Alexa.UserPreference")
return AlexaInterfaceType::USER_PREFERENCE;
if (str == "Alexa.VideoRecorder")
return AlexaInterfaceType::VIDEO_RECORDER;
if (str == "Alexa.WakeOnLANController")
return AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER;
return AlexaInterfaceType::UNKNOWN;
}
static String toString(AlexaInterfaceType type)
{
switch (type)
{
case AlexaInterfaceType::APPLICATION_STATE_REPORTER:
return "Alexa.ApplicationStateReporter";
case AlexaInterfaceType::AUDIO_PLAY_QUEUE:
return "Alexa.Audio.PlayQueue";
case AlexaInterfaceType::AUTHORIZATION_CONTROLLER:
return "Alexa.AuthorizationController";
case AlexaInterfaceType::AUTOMATION_MANAGEMENT:
return "Alexa.AutomationManagement";
case AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA:
return "Alexa.Automotive.VehicleData";
case AlexaInterfaceType::BRIGHTNESS_CONTROLLER:
return "Alexa.BrightnessController";
case AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER:
return "Alexa.Camera.LiveViewController";
case AlexaInterfaceType::CAMERA_STREAM_CONTROLLER:
return "Alexa.CameraStreamController";
case AlexaInterfaceType::CHANNEL_CONTROLLER:
return "Alexa.ChannelController";
case AlexaInterfaceType::COLOR_CONTROLLER:
return "Alexa.ColorController";
case AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER:
return "Alexa.ColorTemperatureController";
case AlexaInterfaceType::COMMISSIONABLE:
return "Alexa.Commissionable";
case AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER:
return "Alexa.ConsentManagement.ConsentRequiredReporter";
case AlexaInterfaceType::CONTACT_SENSOR:
return "Alexa.ContactSensor";
case AlexaInterfaceType::COOKING:
return "Alexa.Cooking";
case AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_CONTROLLER:
return "Alexa.Cooking.FoodTemperatureController";
case AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_SENSOR:
return "Alexa.Cooking.FoodTemperatureSensor";
case AlexaInterfaceType::COOKING_PRESET_CONTROLLER:
return "Alexa.Cooking.PresetController";
case AlexaInterfaceType::COOKING_TEMPERATURE_CONTROLLER:
return "Alexa.Cooking.TemperatureController";
case AlexaInterfaceType::COOKING_TEMPERATURE_SENSOR:
return "Alexa.Cooking.TemperatureSensor";
case AlexaInterfaceType::COOKING_TIME_CONTROLLER:
return "Alexa.Cooking.TimeController";
case AlexaInterfaceType::DATA_CONTROLLER:
return "Alexa.DataController";
case AlexaInterfaceType::DEVICE_USAGE_ESTIMATION:
return "Alexa.DeviceUsage.Estimation";
case AlexaInterfaceType::DEVICE_USAGE_METER:
return "Alexa.DeviceUsage.Meter";
case AlexaInterfaceType::DOORBELL_EVENT_SOURCE:
return "Alexa.DoorbellEventSource";
case AlexaInterfaceType::ENDPOINT_HEALTH:
return "Alexa.EndpointHealth";
case AlexaInterfaceType::EQUALIZER_CONTROLLER:
return "Alexa.EqualizerController";
case AlexaInterfaceType::INPUT_CONTROLLER:
return "Alexa.InputController";
case AlexaInterfaceType::INVENTORY_LEVEL_SENSOR:
return "Alexa.InventoryLevelSensor";
case AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR:
return "Alexa.InventoryLevelUsageSensor";
case AlexaInterfaceType::INVENTORY_USAGE_SENSOR:
return "Alexa.InventoryUsageSensor";
case AlexaInterfaceType::KEYPAD_CONTROLLER:
return "Alexa.KeypadController";
case AlexaInterfaceType::LAUNCHER:
return "Alexa.Launcher";
case AlexaInterfaceType::LOCK_CONTROLLER:
return "Alexa.LockController";
case AlexaInterfaceType::MEDIA_PLAYBACK:
return "Alexa.Media.Playback";
case AlexaInterfaceType::MEDIA_PLAY_QUEUE:
return "Alexa.Media.PlayQueue";
case AlexaInterfaceType::MEDIA_SEARCH:
return "Alexa.Media.Search";
case AlexaInterfaceType::MODE_CONTROLLER:
return "Alexa.ModeController";
case AlexaInterfaceType::MOTION_SENSOR:
return "Alexa.MotionSensor";
case AlexaInterfaceType::PERCENTAGE_CONTROLLER:
return "Alexa.PercentageController";
case AlexaInterfaceType::PLAYBACK_CONTROLLER:
return "Alexa.PlaybackController";
case AlexaInterfaceType::PLAYBACK_STATE_REPORTER:
return "Alexa.PlaybackStateReporter";
case AlexaInterfaceType::POWER_CONTROLLER:
return "Alexa.PowerController";
case AlexaInterfaceType::POWER_LEVEL_CONTROLLER:
return "Alexa.PowerLevelController";
case AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE:
return "Alexa.ProactiveNotificationSource";
case AlexaInterfaceType::RANGE_CONTROLLER:
return "Alexa.RangeController";
case AlexaInterfaceType::RECORD_CONTROLLER:
return "Alexa.RecordController";
case AlexaInterfaceType::REMOTE_VIDEO_PLAYER:
return "Alexa.RemoteVideoPlayer";
case AlexaInterfaceType::RTC_SESSION_CONTROLLER:
return "Alexa.RTCSessionController";
case AlexaInterfaceType::SCENE_CONTROLLER:
return "Alexa.SceneController";
case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER:
return "Alexa.SecurityPanelController";
case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER_ALERT:
return "Alexa.SecurityPanelController.Alert";
case AlexaInterfaceType::SEEK_CONTROLLER:
return "Alexa.SeekController";
case AlexaInterfaceType::SIMPLE_EVENT_SOURCE:
return "Alexa.SimpleEventSource";
case AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR:
return "Alexa.SmartVision.ObjectDetectionSensor";
case AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER:
return "Alexa.SmartVision.SnapshotProvider";
case AlexaInterfaceType::SPEAKER:
return "Alexa.Speaker";
case AlexaInterfaceType::STEP_SPEAKER:
return "Alexa.StepSpeaker";
case AlexaInterfaceType::TEMPERATURE_SENSOR:
return "Alexa.TemperatureSensor";
case AlexaInterfaceType::THERMOSTAT_CONTROLLER:
return "Alexa.ThermostatController";
case AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION:
return "Alexa.ThermostatController.Configuration";
case AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS:
return "Alexa.ThermostatController.HVAC.Components";
case AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE:
return "Alexa.ThermostatController.Schedule";
case AlexaInterfaceType::TIME_HOLD_CONTROLLER:
return "Alexa.TimeHoldController";
case AlexaInterfaceType::TOGGLE_CONTROLLER:
return "Alexa.ToggleController";
case AlexaInterfaceType::UI_CONTROLLER:
return "Alexa.UIController";
case AlexaInterfaceType::USER_PREFERENCE:
return "Alexa.UserPreference";
case AlexaInterfaceType::VIDEO_RECORDER:
return "Alexa.VideoRecorder";
case AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER:
return "Alexa.WakeOnLANController";
default:
return "UNKNOWN";
}
}
static String getVersion(AlexaInterfaceType type)
{
switch (type)
{
case AlexaInterfaceType::APPLICATION_STATE_REPORTER:
case AlexaInterfaceType::AUDIO_PLAY_QUEUE:
case AlexaInterfaceType::AUTHORIZATION_CONTROLLER:
case AlexaInterfaceType::AUTOMATION_MANAGEMENT:
case AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA:
case AlexaInterfaceType::COMMISSIONABLE:
case AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER:
case AlexaInterfaceType::COOKING:
case AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType::COOKING_FOOD_TEMPERATURE_SENSOR:
case AlexaInterfaceType::COOKING_PRESET_CONTROLLER:
case AlexaInterfaceType::COOKING_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType::COOKING_TEMPERATURE_SENSOR:
case AlexaInterfaceType::COOKING_TIME_CONTROLLER:
case AlexaInterfaceType::DATA_CONTROLLER:
case AlexaInterfaceType::DEVICE_USAGE_ESTIMATION:
case AlexaInterfaceType::DEVICE_USAGE_METER:
case AlexaInterfaceType::EQUALIZER_CONTROLLER:
case AlexaInterfaceType::INVENTORY_LEVEL_SENSOR:
case AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR:
case AlexaInterfaceType::INVENTORY_USAGE_SENSOR:
case AlexaInterfaceType::KEYPAD_CONTROLLER:
case AlexaInterfaceType::MEDIA_PLAYBACK:
case AlexaInterfaceType::MEDIA_PLAY_QUEUE:
case AlexaInterfaceType::MEDIA_SEARCH:
case AlexaInterfaceType::PLAYBACK_STATE_REPORTER:
case AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE:
case AlexaInterfaceType::REMOTE_VIDEO_PLAYER:
case AlexaInterfaceType::RTC_SESSION_CONTROLLER:
case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER:
case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER_ALERT:
case AlexaInterfaceType::SIMPLE_EVENT_SOURCE:
case AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR:
case AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER:
case AlexaInterfaceType::SPEAKER:
case AlexaInterfaceType::STEP_SPEAKER:
case AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION:
case AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS:
case AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE:
case AlexaInterfaceType::UI_CONTROLLER:
case AlexaInterfaceType::USER_PREFERENCE:
case AlexaInterfaceType::VIDEO_RECORDER:
case AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER:
return "1";
case AlexaInterfaceType::BRIGHTNESS_CONTROLLER:
case AlexaInterfaceType::CAMERA_STREAM_CONTROLLER:
case AlexaInterfaceType::CHANNEL_CONTROLLER:
case AlexaInterfaceType::COLOR_CONTROLLER:
case AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER:
case AlexaInterfaceType::CONTACT_SENSOR:
case AlexaInterfaceType::DOORBELL_EVENT_SOURCE:
case AlexaInterfaceType::INPUT_CONTROLLER:
case AlexaInterfaceType::LOCK_CONTROLLER:
case AlexaInterfaceType::MODE_CONTROLLER:
case AlexaInterfaceType::MOTION_SENSOR:
case AlexaInterfaceType::PERCENTAGE_CONTROLLER:
case AlexaInterfaceType::PLAYBACK_CONTROLLER:
case AlexaInterfaceType::POWER_CONTROLLER:
case AlexaInterfaceType::POWER_LEVEL_CONTROLLER:
case AlexaInterfaceType::RANGE_CONTROLLER:
case AlexaInterfaceType::RECORD_CONTROLLER:
case AlexaInterfaceType::SCENE_CONTROLLER:
case AlexaInterfaceType::SEEK_CONTROLLER:
case AlexaInterfaceType::TEMPERATURE_SENSOR:
case AlexaInterfaceType::THERMOSTAT_CONTROLLER:
case AlexaInterfaceType::TOGGLE_CONTROLLER:
return "3";
case AlexaInterfaceType::LAUNCHER:
return "1.1";
case AlexaInterfaceType::ENDPOINT_HEALTH:
return "3.3";
default:
return "UNKNOWN";
}
}
static std::vector<String> getProps(AlexaInterfaceType type)
{
switch (type)
{
case AlexaInterfaceType::AUTOMATION_MANAGEMENT:
return {"automationStatuses"};
case AlexaInterfaceType::BRIGHTNESS_CONTROLLER:
return {"brightness"};
case AlexaInterfaceType::CHANNEL_CONTROLLER:
return {"channel"};
case AlexaInterfaceType::COLOR_CONTROLLER:
return {"color"};
case AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER:
return {"colorTemperatureInKelvin"};
case AlexaInterfaceType::CONTACT_SENSOR:
return {"detectionState"};
case AlexaInterfaceType::POWER_CONTROLLER:
return {"powerState"};
case AlexaInterfaceType::ENDPOINT_HEALTH:
return {"connectivity"};
case AlexaInterfaceType::TOGGLE_CONTROLLER:
return {"toggleState"};
case AlexaInterfaceType::TEMPERATURE_SENSOR:
return {"temperature"};
case AlexaInterfaceType::APPLICATION_STATE_REPORTER:
case AlexaInterfaceType::AUDIO_PLAY_QUEUE:
case AlexaInterfaceType::AUTHORIZATION_CONTROLLER:
case AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA:
case AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER:
case AlexaInterfaceType::CAMERA_STREAM_CONTROLLER:
case AlexaInterfaceType::COMMISSIONABLE:
case AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER:
case AlexaInterfaceType::COOKING:
case AlexaInterfaceType::DATA_CONTROLLER:
case AlexaInterfaceType::DEVICE_USAGE_ESTIMATION:
case AlexaInterfaceType::DEVICE_USAGE_METER:
case AlexaInterfaceType::DOORBELL_EVENT_SOURCE:
case AlexaInterfaceType::EQUALIZER_CONTROLLER:
case AlexaInterfaceType::INPUT_CONTROLLER:
case AlexaInterfaceType::INVENTORY_LEVEL_SENSOR:
case AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR:
case AlexaInterfaceType::INVENTORY_USAGE_SENSOR:
case AlexaInterfaceType::KEYPAD_CONTROLLER:
case AlexaInterfaceType::LAUNCHER:
case AlexaInterfaceType::LOCK_CONTROLLER:
case AlexaInterfaceType::MEDIA_PLAYBACK:
case AlexaInterfaceType::MEDIA_SEARCH:
case AlexaInterfaceType::MODE_CONTROLLER:
case AlexaInterfaceType::MOTION_SENSOR:
case AlexaInterfaceType::PERCENTAGE_CONTROLLER:
case AlexaInterfaceType::PLAYBACK_CONTROLLER:
case AlexaInterfaceType::PLAYBACK_STATE_REPORTER:
case AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE:
case AlexaInterfaceType::RANGE_CONTROLLER:
case AlexaInterfaceType::RECORD_CONTROLLER:
case AlexaInterfaceType::REMOTE_VIDEO_PLAYER:
case AlexaInterfaceType::RTC_SESSION_CONTROLLER:
case AlexaInterfaceType::SCENE_CONTROLLER:
case AlexaInterfaceType::SECURITY_PANEL_CONTROLLER:
case AlexaInterfaceType::SEEK_CONTROLLER:
case AlexaInterfaceType::SIMPLE_EVENT_SOURCE:
case AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR:
case AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER:
case AlexaInterfaceType::SPEAKER:
case AlexaInterfaceType::STEP_SPEAKER:
case AlexaInterfaceType::THERMOSTAT_CONTROLLER:
case AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION:
case AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS:
case AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE:
case AlexaInterfaceType::TIME_HOLD_CONTROLLER:
case AlexaInterfaceType::UI_CONTROLLER:
case AlexaInterfaceType::USER_PREFERENCE:
case AlexaInterfaceType::VIDEO_RECORDER:
case AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER:
return {};
default:
return {"UNKNOWN"};
}
}
};
enum class AlexaActions {
Open,
Close,
@ -627,11 +90,11 @@ public:
// AlexaInterface class definition
// A capability of a device: a row of AlexaInterfaces and what this device adds to it
class AlexaInterface
{
private:
AlexaInterfaceType type;
const AlexaInterfaceDesc *row; // in program memory, never nullptr
bool retrievable;
bool proactivelyReported;
std::vector<ActionMapping> actionMappings; // Array of ActionMappings
@ -642,14 +105,23 @@ private:
bool nameSet = false;
public:
// Constructor
AlexaInterface(AlexaInterfaceType type, bool retrievable = true, bool proactivelyReported = false)
: type(type), retrievable(retrievable), proactivelyReported(proactivelyReported) {}
AlexaInterface(const AlexaInterfaceDesc &row, bool retrievable = true, bool proactivelyReported = false)
: row(&row), retrievable(retrievable), proactivelyReported(proactivelyReported) {}
// Getters
AlexaInterfaceType getType() const { return type; }
String getTypeString() const { return AlexaInterfaceUtils::toString(type); }
String getVersion() const { return AlexaInterfaceUtils::getVersion(type); }
std::vector<String> getProps() const { return AlexaInterfaceUtils::getProps(type); }
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;
@ -676,23 +148,21 @@ public:
JsonDocument doc;
// Set base properties
doc["interface"] = getTypeString();
doc["version"] = getVersion();
doc["interface"] = FPSTR(row->ns);
doc["version"] = FPSTR(row->version);
doc["type"] = "AlexaInterface";
// Add properties
// 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;
std::vector<String> props = getProps();
JsonArray supported = properties["supported"].to<JsonArray>();
int index = 0;
for (String prop : props)
for (uint8_t i = 0; i < row->propertyCount(); i++)
{
supported[index]["name"] = prop;
index++;
supported.add<JsonObject>()["name"] = FPSTR(row->property(i));
}
}

159
src/AlexaInterfaces.cpp Normal file
View file

@ -0,0 +1,159 @@
#include "AlexaInterfaces.h"
// Every string and every row is a PROGMEM object of its own: what no capability of the sketch refers to is not
// linked. A new interface is its strings and one ALEXA_ROW here, an extern and a case in AlexaInterfaces.h.
#define ALEXA_TEXT(name, text) static const char name[] PROGMEM = text
#define ALEXA_ROW(row, ns, version, props, propCount, flags, type) \
const AlexaInterfaceDesc AlexaInterfaces::row PROGMEM = {ns, version, props, propCount, flags, AlexaInterfaceType::type}
ALEXA_TEXT(VERSION_1_0, "1.0");
ALEXA_TEXT(VERSION_3, "3");
ALEXA_TEXT(VERSION_3_1, "3.1");
ALEXA_TEXT(VERSION_3_2, "3.2");
ALEXA_TEXT(NS_ENDPOINT_HEALTH, "Alexa.EndpointHealth");
ALEXA_TEXT(NS_POWER, "Alexa.PowerController");
ALEXA_TEXT(NS_BRIGHTNESS, "Alexa.BrightnessController");
ALEXA_TEXT(NS_COLOR, "Alexa.ColorController");
ALEXA_TEXT(NS_COLOR_TEMPERATURE, "Alexa.ColorTemperatureController");
ALEXA_TEXT(NS_POWER_LEVEL, "Alexa.PowerLevelController");
ALEXA_TEXT(NS_PERCENTAGE, "Alexa.PercentageController");
ALEXA_TEXT(NS_RANGE, "Alexa.RangeController");
ALEXA_TEXT(NS_MODE, "Alexa.ModeController");
ALEXA_TEXT(NS_TOGGLE, "Alexa.ToggleController");
ALEXA_TEXT(NS_THERMOSTAT, "Alexa.ThermostatController");
ALEXA_TEXT(NS_TEMPERATURE_SENSOR, "Alexa.TemperatureSensor");
ALEXA_TEXT(NS_HUMIDITY_SENSOR, "Alexa.HumiditySensor");
ALEXA_TEXT(NS_LOCK, "Alexa.LockController");
ALEXA_TEXT(NS_SCENE, "Alexa.SceneController");
ALEXA_TEXT(NS_CONTACT_SENSOR, "Alexa.ContactSensor");
ALEXA_TEXT(NS_MOTION_SENSOR, "Alexa.MotionSensor");
ALEXA_TEXT(NS_DOORBELL, "Alexa.DoorbellEventSource");
ALEXA_TEXT(NS_TIME_HOLD, "Alexa.TimeHoldController");
ALEXA_TEXT(NS_SPEAKER, "Alexa.Speaker");
ALEXA_TEXT(NS_STEP_SPEAKER, "Alexa.StepSpeaker");
ALEXA_TEXT(NS_PLAYBACK, "Alexa.PlaybackController");
ALEXA_TEXT(NS_PLAYBACK_STATE, "Alexa.PlaybackStateReporter");
ALEXA_TEXT(NS_INPUT, "Alexa.InputController");
ALEXA_TEXT(NS_CHANNEL, "Alexa.ChannelController");
ALEXA_TEXT(NS_INVENTORY_LEVEL, "Alexa.InventoryLevelSensor");
ALEXA_TEXT(NS_SIMPLE_EVENT, "Alexa.SimpleEventSource");
ALEXA_TEXT(NS_WAKE_ON_LAN, "Alexa.WakeOnLANController");
ALEXA_TEXT(PROPERTY_CONNECTIVITY, "connectivity");
ALEXA_TEXT(PROPERTY_POWER_STATE, "powerState");
ALEXA_TEXT(PROPERTY_BRIGHTNESS, "brightness");
ALEXA_TEXT(PROPERTY_COLOR, "color");
ALEXA_TEXT(PROPERTY_COLOR_TEMPERATURE_IN_KELVIN, "colorTemperatureInKelvin");
ALEXA_TEXT(PROPERTY_POWER_LEVEL, "powerLevel");
ALEXA_TEXT(PROPERTY_PERCENTAGE, "percentage");
ALEXA_TEXT(PROPERTY_RANGE_VALUE, "rangeValue");
ALEXA_TEXT(PROPERTY_MODE, "mode");
ALEXA_TEXT(PROPERTY_TOGGLE_STATE, "toggleState");
ALEXA_TEXT(PROPERTY_TARGET_SETPOINT, "targetSetpoint");
ALEXA_TEXT(PROPERTY_LOWER_SETPOINT, "lowerSetpoint");
ALEXA_TEXT(PROPERTY_UPPER_SETPOINT, "upperSetpoint");
ALEXA_TEXT(PROPERTY_THERMOSTAT_MODE, "thermostatMode");
ALEXA_TEXT(PROPERTY_TEMPERATURE, "temperature");
ALEXA_TEXT(PROPERTY_RELATIVE_HUMIDITY, "relativeHumidity");
ALEXA_TEXT(PROPERTY_LOCK_STATE, "lockState");
ALEXA_TEXT(PROPERTY_DETECTION_STATE, "detectionState");
ALEXA_TEXT(PROPERTY_HOLD_START_TIME, "holdStartTime");
ALEXA_TEXT(PROPERTY_HOLD_END_TIME, "holdEndTime");
ALEXA_TEXT(PROPERTY_VOLUME, "volume");
ALEXA_TEXT(PROPERTY_MUTED, "muted");
ALEXA_TEXT(PROPERTY_PLAYBACK_STATE, "playbackState");
ALEXA_TEXT(PROPERTY_INPUT, "input");
ALEXA_TEXT(PROPERTY_CHANNEL, "channel");
ALEXA_TEXT(PROPERTY_LEVEL, "level");
static const char *const PROPS_ENDPOINT_HEALTH[] PROGMEM = {PROPERTY_CONNECTIVITY};
static const char *const PROPS_POWER[] PROGMEM = {PROPERTY_POWER_STATE};
static const char *const PROPS_BRIGHTNESS[] PROGMEM = {PROPERTY_BRIGHTNESS};
static const char *const PROPS_COLOR[] PROGMEM = {PROPERTY_COLOR};
static const char *const PROPS_COLOR_TEMPERATURE[] PROGMEM = {PROPERTY_COLOR_TEMPERATURE_IN_KELVIN};
static const char *const PROPS_POWER_LEVEL[] PROGMEM = {PROPERTY_POWER_LEVEL};
static const char *const PROPS_PERCENTAGE[] PROGMEM = {PROPERTY_PERCENTAGE};
static const char *const PROPS_RANGE[] PROGMEM = {PROPERTY_RANGE_VALUE};
static const char *const PROPS_MODE[] PROGMEM = {PROPERTY_MODE};
static const char *const PROPS_TOGGLE[] PROGMEM = {PROPERTY_TOGGLE_STATE};
static const char *const PROPS_THERMOSTAT[] PROGMEM = {PROPERTY_TARGET_SETPOINT, PROPERTY_LOWER_SETPOINT, PROPERTY_UPPER_SETPOINT, PROPERTY_THERMOSTAT_MODE};
static const char *const PROPS_TEMPERATURE_SENSOR[] PROGMEM = {PROPERTY_TEMPERATURE};
static const char *const PROPS_HUMIDITY_SENSOR[] PROGMEM = {PROPERTY_RELATIVE_HUMIDITY};
static const char *const PROPS_LOCK[] PROGMEM = {PROPERTY_LOCK_STATE};
static const char *const PROPS_DETECTION[] PROGMEM = {PROPERTY_DETECTION_STATE};
static const char *const PROPS_TIME_HOLD[] PROGMEM = {PROPERTY_HOLD_START_TIME, PROPERTY_HOLD_END_TIME};
static const char *const PROPS_SPEAKER[] PROGMEM = {PROPERTY_VOLUME, PROPERTY_MUTED};
static const char *const PROPS_PLAYBACK_STATE[] PROGMEM = {PROPERTY_PLAYBACK_STATE};
static const char *const PROPS_INPUT[] PROGMEM = {PROPERTY_INPUT};
static const char *const PROPS_CHANNEL[] PROGMEM = {PROPERTY_CHANNEL};
static const char *const PROPS_INVENTORY_LEVEL[] PROGMEM = {PROPERTY_LEVEL};
ALEXA_ROW(EndpointHealth, NS_ENDPOINT_HEALTH, VERSION_3_1, PROPS_ENDPOINT_HEALTH, 1, AIF_NONE, ENDPOINT_HEALTH);
ALEXA_ROW(PowerController, NS_POWER, VERSION_3, PROPS_POWER, 1, AIF_NONE, POWER_CONTROLLER);
ALEXA_ROW(BrightnessController, NS_BRIGHTNESS, VERSION_3, PROPS_BRIGHTNESS, 1, AIF_NONE, BRIGHTNESS_CONTROLLER);
ALEXA_ROW(ColorController, NS_COLOR, VERSION_3, PROPS_COLOR, 1, AIF_NONE, COLOR_CONTROLLER);
ALEXA_ROW(ColorTemperatureController, NS_COLOR_TEMPERATURE, VERSION_3, PROPS_COLOR_TEMPERATURE, 1, AIF_NONE, COLOR_TEMPERATURE_CONTROLLER);
ALEXA_ROW(PowerLevelController, NS_POWER_LEVEL, VERSION_3, PROPS_POWER_LEVEL, 1, AIF_NONE, POWER_LEVEL_CONTROLLER);
ALEXA_ROW(PercentageController, NS_PERCENTAGE, VERSION_3, PROPS_PERCENTAGE, 1, AIF_NONE, PERCENTAGE_CONTROLLER);
ALEXA_ROW(RangeController, NS_RANGE, VERSION_3, PROPS_RANGE, 1, AIF_INSTANCED | AIF_SEMANTICS, RANGE_CONTROLLER);
ALEXA_ROW(ModeController, NS_MODE, VERSION_3, PROPS_MODE, 1, AIF_INSTANCED | AIF_SEMANTICS, MODE_CONTROLLER);
ALEXA_ROW(ToggleController, NS_TOGGLE, VERSION_3, PROPS_TOGGLE, 1, AIF_INSTANCED | AIF_SEMANTICS, TOGGLE_CONTROLLER);
ALEXA_ROW(ThermostatController, NS_THERMOSTAT, VERSION_3_2, PROPS_THERMOSTAT, 4, AIF_NONE, THERMOSTAT_CONTROLLER);
ALEXA_ROW(TemperatureSensor, NS_TEMPERATURE_SENSOR, VERSION_3, PROPS_TEMPERATURE_SENSOR, 1, AIF_NONE, TEMPERATURE_SENSOR);
ALEXA_ROW(HumiditySensor, NS_HUMIDITY_SENSOR, VERSION_3, PROPS_HUMIDITY_SENSOR, 1, AIF_NONE, HUMIDITY_SENSOR);
ALEXA_ROW(LockController, NS_LOCK, VERSION_3, PROPS_LOCK, 1, AIF_NONE, LOCK_CONTROLLER);
ALEXA_ROW(SceneController, NS_SCENE, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES, SCENE_CONTROLLER);
ALEXA_ROW(ContactSensor, NS_CONTACT_SENSOR, VERSION_3, PROPS_DETECTION, 1, AIF_NONE, CONTACT_SENSOR);
ALEXA_ROW(MotionSensor, NS_MOTION_SENSOR, VERSION_3, PROPS_DETECTION, 1, AIF_NONE, MOTION_SENSOR);
ALEXA_ROW(DoorbellEventSource, NS_DOORBELL, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES, DOORBELL_EVENT_SOURCE);
ALEXA_ROW(TimeHoldController, NS_TIME_HOLD, VERSION_3, PROPS_TIME_HOLD, 2, AIF_NONE, TIME_HOLD_CONTROLLER);
ALEXA_ROW(Speaker, NS_SPEAKER, VERSION_3, PROPS_SPEAKER, 2, AIF_NONE, SPEAKER);
ALEXA_ROW(StepSpeaker, NS_STEP_SPEAKER, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES, STEP_SPEAKER);
ALEXA_ROW(PlaybackController, NS_PLAYBACK, VERSION_3, nullptr, 0, AIF_NONE, PLAYBACK_CONTROLLER);
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(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(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);
// 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).
#define ALEXA_DISPLAY_CATEGORIES(X) \
X(ACTIVITY_TRIGGER) X(AIR_CONDITIONER) X(AIR_FRESHENER) X(AIR_PURIFIER) X(AIR_QUALITY_MONITOR) \
X(ALEXA_VOICE_ENABLED) X(AUTO_ACCESSORY) X(BLUETOOTH_SPEAKER) X(CAMERA) X(CHRISTMAS_TREE) X(COFFEE_MAKER) \
X(COMPUTER) X(CONTACT_SENSOR) X(DISHWASHER) X(DOOR) X(DOORBELL) X(DRYER) X(EXTERIOR_BLIND) X(FAN) \
X(GAME_CONSOLE) X(GARAGE_DOOR) X(HEADPHONES) X(HUB) X(INTERIOR_BLIND) X(LAPTOP) X(LIGHT) X(MICROWAVE) \
X(MOBILE_PHONE) X(MOTION_SENSOR) X(MUSIC_SYSTEM) X(NETWORK_HARDWARE) X(OTHER) X(OVEN) X(PHONE) X(PRINTER) \
X(REMOTE) X(ROUTER) X(SCENE_TRIGGER) X(SCREEN) X(SECURITY_PANEL) X(SECURITY_SYSTEM) X(SLOW_COOKER) \
X(SMARTLOCK) X(SMARTPLUG) X(SPEAKER) X(STREAMING_DEVICE) X(SWITCH) X(TABLET) X(TEMPERATURE_SENSOR) \
X(THERMOSTAT) X(TV) X(VACUUM_CLEANER) X(VEHICLE) X(WASHER) X(WATER_HEATER) X(WEARABLE)
#define ALEXA_CATEGORY_TEXT(name) static const char CATEGORY_##name[] PROGMEM = #name;
ALEXA_DISPLAY_CATEGORIES(ALEXA_CATEGORY_TEXT)
#define ALEXA_CATEGORY_ENTRY(name) CATEGORY_##name,
static const char *const CATEGORY_NAMES[] PROGMEM = {ALEXA_DISPLAY_CATEGORIES(ALEXA_CATEGORY_ENTRY)};
// A category that is added to the enum and not to the list above, or at another place, does not compile
enum CategoryPlace : size_t
{
#define ALEXA_CATEGORY_PLACE(name) PLACE_##name,
ALEXA_DISPLAY_CATEGORIES(ALEXA_CATEGORY_PLACE)
PLACE_COUNT
};
#define ALEXA_CATEGORY_CHECK(name) \
static_assert(static_cast<size_t>(DisplayCategory::name) == PLACE_##name, "display category " #name " is out of place");
ALEXA_DISPLAY_CATEGORIES(ALEXA_CATEGORY_CHECK)
static_assert(static_cast<size_t>(DisplayCategory::WEARABLE) + 1 == PLACE_COUNT, "a display category has no name");
PGM_P alexaDisplayCategoryName(DisplayCategory category)
{
size_t place = static_cast<size_t>(category);
if (place >= PLACE_COUNT)
{
place = PLACE_OTHER;
}
return static_cast<PGM_P>(pgm_read_ptr(&CATEGORY_NAMES[place]));
}

251
src/AlexaInterfaces.h Normal file
View file

@ -0,0 +1,251 @@
// The Alexa interfaces the library can describe. Every interface is one row in program memory: its namespace, its
// version, the names of the properties it reports and what its discovery object carries. A sketch names the rows
// it uses, device->addCapability(AlexaInterfaces::PowerController), and the linker leaves out every other row and
// its strings: each is an object of its own, and the ESP8266 core links with --gc-sections. That only works while
// nothing refers to all rows at once, so there is no table that leads from an AlexaInterfaceType to its row.
#ifndef ALEXA_INTERFACES_H
#define ALEXA_INTERFACES_H
#include <stddef.h>
#include <stdint.h>
#include "AlexaCompat.h"
// What the handler of "Event" is told about the directive. The values without a row in AlexaInterfaces are
// interfaces the library cannot describe yet: addCapability() refuses them.
enum class AlexaInterfaceType : uint8_t
{
APPLICATION_STATE_REPORTER,
AUDIO_PLAY_QUEUE,
AUTOMATION_MANAGEMENT,
BRIGHTNESS_CONTROLLER,
CAMERA_LIVE_VIEW_CONTROLLER,
CAMERA_STREAM_CONTROLLER,
CHANNEL_CONTROLLER,
COLOR_CONTROLLER,
COLOR_TEMPERATURE_CONTROLLER,
COMMISSIONABLE,
CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER,
CONTACT_SENSOR,
COOKING,
COOKING_FOOD_TEMPERATURE_CONTROLLER,
COOKING_FOOD_TEMPERATURE_SENSOR,
COOKING_PRESET_CONTROLLER,
COOKING_TEMPERATURE_CONTROLLER,
COOKING_TEMPERATURE_SENSOR,
COOKING_TIME_CONTROLLER,
DATA_CONTROLLER,
DEVICE_USAGE_ESTIMATION,
DEVICE_USAGE_METER,
DOORBELL_EVENT_SOURCE,
ENDPOINT_HEALTH,
EQUALIZER_CONTROLLER,
HUMIDITY_SENSOR,
INPUT_CONTROLLER,
INVENTORY_LEVEL_SENSOR,
INVENTORY_LEVEL_USAGE_SENSOR,
INVENTORY_USAGE_SENSOR,
KEYPAD_CONTROLLER,
LAUNCHER,
LOCK_CONTROLLER,
MEDIA_PLAYBACK,
MEDIA_PLAY_QUEUE,
MEDIA_SEARCH,
MODE_CONTROLLER,
MOTION_SENSOR,
PERCENTAGE_CONTROLLER,
PLAYBACK_CONTROLLER,
PLAYBACK_STATE_REPORTER,
POWER_CONTROLLER,
POWER_LEVEL_CONTROLLER,
PROACTIVE_NOTIFICATION_SOURCE,
RANGE_CONTROLLER,
RECORD_CONTROLLER,
REMOTE_VIDEO_PLAYER,
RTC_SESSION_CONTROLLER,
SCENE_CONTROLLER,
SECURITY_PANEL_CONTROLLER,
SECURITY_PANEL_CONTROLLER_ALERT,
SEEK_CONTROLLER,
SIMPLE_EVENT_SOURCE,
SMART_VISION_OBJECT_DETECTION_SENSOR,
SMART_VISION_SNAPSHOT_PROVIDER,
SPEAKER,
STEP_SPEAKER,
TEMPERATURE_SENSOR,
THERMOSTAT_CONTROLLER,
THERMOSTAT_CONTROLLER_CONFIGURATION,
THERMOSTAT_CONTROLLER_HVAC_COMPONENTS,
THERMOSTAT_CONTROLLER_SCHEDULE,
TIME_HOLD_CONTROLLER,
TOGGLE_CONTROLLER,
UI_CONTROLLER,
USER_PREFERENCE,
VIDEO_RECORDER,
WAKE_ON_LAN_CONTROLLER,
UNKNOWN
};
enum AlexaInterfaceFlags : uint8_t
{
AIF_NONE = 0,
AIF_INSTANCED = 1, // a generic controller: needs an instance and friendly names
AIF_NO_PROPERTIES = 2, // the discovery object has no "properties" (scenes, doorbells, step speakers)
AIF_SEMANTICS = 4 // may carry action and state mappings
};
// 16 bytes in program memory. The ESP8266 reads flash in whole words only: the pointers can be read as they are,
// the three single bytes through the functions below.
struct AlexaInterfaceDesc
{
PGM_P ns; // "Alexa.PowerController"
PGM_P version; // "3"
const char *const *props; // the names of the properties, propCount of them; nullptr when there are none
uint8_t propCount;
uint8_t flags; // AlexaInterfaceFlags
AlexaInterfaceType type;
uint8_t propertyCount() const { return pgm_read_byte(&propCount); }
bool has(AlexaInterfaceFlags flag) const { return (pgm_read_byte(&flags) & flag) != 0; }
AlexaInterfaceType interfaceType() const { return static_cast<AlexaInterfaceType>(pgm_read_byte(&type)); }
// The name of a property, index below propertyCount()
PGM_P property(uint8_t index) const { return static_cast<PGM_P>(pgm_read_ptr(&props[index])); }
};
// Versions and property names are those of the interface pages under
// https://developer.amazon.com/en-US/docs/alexa/device-apis/
namespace AlexaInterfaces
{
extern const AlexaInterfaceDesc EndpointHealth;
extern const AlexaInterfaceDesc PowerController;
extern const AlexaInterfaceDesc BrightnessController;
extern const AlexaInterfaceDesc ColorController;
extern const AlexaInterfaceDesc ColorTemperatureController;
extern const AlexaInterfaceDesc PowerLevelController;
extern const AlexaInterfaceDesc PercentageController;
extern const AlexaInterfaceDesc RangeController;
extern const AlexaInterfaceDesc ModeController;
extern const AlexaInterfaceDesc ToggleController;
extern const AlexaInterfaceDesc ThermostatController;
extern const AlexaInterfaceDesc TemperatureSensor;
extern const AlexaInterfaceDesc HumiditySensor;
extern const AlexaInterfaceDesc LockController;
extern const AlexaInterfaceDesc SceneController;
extern const AlexaInterfaceDesc ContactSensor;
extern const AlexaInterfaceDesc MotionSensor;
extern const AlexaInterfaceDesc DoorbellEventSource;
extern const AlexaInterfaceDesc TimeHoldController;
extern const AlexaInterfaceDesc Speaker;
extern const AlexaInterfaceDesc StepSpeaker;
extern const AlexaInterfaceDesc PlaybackController;
extern const AlexaInterfaceDesc PlaybackStateReporter;
extern const AlexaInterfaceDesc InputController;
extern const AlexaInterfaceDesc ChannelController;
extern const AlexaInterfaceDesc InventoryLevelSensor;
extern const AlexaInterfaceDesc SimpleEventSource;
extern const AlexaInterfaceDesc WakeOnLANController;
}
// The row of a type, nullptr for a type without one. Always inlined, so that a type the compiler knows, as in
// addCapability(AlexaInterfaceType::POWER_CONTROLLER), leaves one row behind and not the switch. With a type that
// is only known when the sketch runs, the sketch links every row.
__attribute__((always_inline)) inline const AlexaInterfaceDesc *alexaInterfaceRow(AlexaInterfaceType type)
{
switch (type)
{
case AlexaInterfaceType::ENDPOINT_HEALTH: return &AlexaInterfaces::EndpointHealth;
case AlexaInterfaceType::POWER_CONTROLLER: return &AlexaInterfaces::PowerController;
case AlexaInterfaceType::BRIGHTNESS_CONTROLLER: return &AlexaInterfaces::BrightnessController;
case AlexaInterfaceType::COLOR_CONTROLLER: return &AlexaInterfaces::ColorController;
case AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER: return &AlexaInterfaces::ColorTemperatureController;
case AlexaInterfaceType::POWER_LEVEL_CONTROLLER: return &AlexaInterfaces::PowerLevelController;
case AlexaInterfaceType::PERCENTAGE_CONTROLLER: return &AlexaInterfaces::PercentageController;
case AlexaInterfaceType::RANGE_CONTROLLER: return &AlexaInterfaces::RangeController;
case AlexaInterfaceType::MODE_CONTROLLER: return &AlexaInterfaces::ModeController;
case AlexaInterfaceType::TOGGLE_CONTROLLER: return &AlexaInterfaces::ToggleController;
case AlexaInterfaceType::THERMOSTAT_CONTROLLER: return &AlexaInterfaces::ThermostatController;
case AlexaInterfaceType::TEMPERATURE_SENSOR: return &AlexaInterfaces::TemperatureSensor;
case AlexaInterfaceType::HUMIDITY_SENSOR: return &AlexaInterfaces::HumiditySensor;
case AlexaInterfaceType::LOCK_CONTROLLER: return &AlexaInterfaces::LockController;
case AlexaInterfaceType::SCENE_CONTROLLER: return &AlexaInterfaces::SceneController;
case AlexaInterfaceType::CONTACT_SENSOR: return &AlexaInterfaces::ContactSensor;
case AlexaInterfaceType::MOTION_SENSOR: return &AlexaInterfaces::MotionSensor;
case AlexaInterfaceType::DOORBELL_EVENT_SOURCE: return &AlexaInterfaces::DoorbellEventSource;
case AlexaInterfaceType::TIME_HOLD_CONTROLLER: return &AlexaInterfaces::TimeHoldController;
case AlexaInterfaceType::SPEAKER: return &AlexaInterfaces::Speaker;
case AlexaInterfaceType::STEP_SPEAKER: return &AlexaInterfaces::StepSpeaker;
case AlexaInterfaceType::PLAYBACK_CONTROLLER: return &AlexaInterfaces::PlaybackController;
case AlexaInterfaceType::PLAYBACK_STATE_REPORTER: return &AlexaInterfaces::PlaybackStateReporter;
case AlexaInterfaceType::INPUT_CONTROLLER: return &AlexaInterfaces::InputController;
case AlexaInterfaceType::CHANNEL_CONTROLLER: return &AlexaInterfaces::ChannelController;
case AlexaInterfaceType::INVENTORY_LEVEL_SENSOR: return &AlexaInterfaces::InventoryLevelSensor;
case AlexaInterfaceType::SIMPLE_EVENT_SOURCE: return &AlexaInterfaces::SimpleEventSource;
case AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER: return &AlexaInterfaces::WakeOnLANController;
default: return nullptr;
}
}
enum class DisplayCategory : uint8_t
{
ACTIVITY_TRIGGER,
AIR_CONDITIONER,
AIR_FRESHENER,
AIR_PURIFIER,
AIR_QUALITY_MONITOR,
ALEXA_VOICE_ENABLED,
AUTO_ACCESSORY,
BLUETOOTH_SPEAKER,
CAMERA,
CHRISTMAS_TREE,
COFFEE_MAKER,
COMPUTER,
CONTACT_SENSOR,
DISHWASHER,
DOOR,
DOORBELL,
DRYER,
EXTERIOR_BLIND,
FAN,
GAME_CONSOLE,
GARAGE_DOOR,
HEADPHONES,
HUB,
INTERIOR_BLIND,
LAPTOP,
LIGHT,
MICROWAVE,
MOBILE_PHONE,
MOTION_SENSOR,
MUSIC_SYSTEM,
NETWORK_HARDWARE,
OTHER,
OVEN,
PHONE,
PRINTER,
REMOTE,
ROUTER,
SCENE_TRIGGER,
SCREEN,
SECURITY_PANEL,
SECURITY_SYSTEM,
SLOW_COOKER,
SMARTLOCK,
SMARTPLUG,
SPEAKER,
STREAMING_DEVICE,
SWITCH,
TABLET,
TEMPERATURE_SENSOR,
THERMOSTAT,
TV,
VACUUM_CLEANER,
VEHICLE,
WASHER,
WATER_HEATER,
WEARABLE
};
// The name Alexa knows the category by, in program memory. A value that is not a category gives "OTHER".
PGM_P alexaDisplayCategoryName(DisplayCategory category);
#endif // ALEXA_INTERFACES_H

View file

@ -3,9 +3,8 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include "AlexaInterface.h"
#include "AlexaInterfaces.h"
#include "AlexaTransport.h"
#include "AlexaUtils.h"
enum class EndpointHealth
{
@ -35,13 +34,13 @@ public:
{
JsonDocument healthValue;
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as<JsonObject>(), uncertaintyInMs);
return AddProperty(AlexaInterfaces::EndpointHealth, healthValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs);
return AddProperty(AlexaInterfaces::PowerController, value, uncertaintyInMs);
}
AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor,float value, unsigned int uncertaintyInMs = 0)
@ -52,21 +51,21 @@ public:
if((tempSensor == TemperatureSensorScale::FAHRENHEIT)){
tempValue["value"] = (value - 32) * 5.0 / 9.0;
}
return AddProperty(AlexaInterfaceType::TEMPERATURE_SENSOR, "temperature", tempValue.as<JsonObject>(), uncertaintyInMs);
return AddProperty(AlexaInterfaces::TemperatureSensor, tempValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "brightness", brightness, uncertaintyInMs);
return AddProperty(AlexaInterfaces::BrightnessController, brightness, uncertaintyInMs);
}
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "colorTemperatureInKelvin", colorTemperature, uncertaintyInMs);
return AddProperty(AlexaInterfaces::ColorTemperatureController, colorTemperature, uncertaintyInMs);
}
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName);
return AddProperty(AlexaInterfaces::ToggleController, value, uncertaintyInMs,instanceName);
}
// Adds a property the sketch built itself. Its timeOfSample is set to the current time when the object has
@ -88,12 +87,13 @@ private:
String generateMessageId();
void setTimeOfSample(JsonObject property);
// Adds the first property of the row: every interface with a helper above reports one
template <typename T>
AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
AlexaStatusMessage &AddProperty(const AlexaInterfaceDesc &row, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
{
JsonObject prop = contextProperties.add<JsonObject>();
prop["namespace"] = AlexaInterfaceUtils::toString(type);
prop["name"] = propertyName;
prop["namespace"] = FPSTR(row.ns);
prop["name"] = FPSTR(row.property(0));
if (!instanceName.isEmpty()) {
prop["instance"] = instanceName;

View file

@ -0,0 +1,41 @@
// The part of Arduino.h that AlexaDevice, AlexaInterface 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.
#ifndef ALEX2ESP_HOST_ARDUINO_H
#define ALEX2ESP_HOST_ARDUINO_H
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include "AlexaCompat.h"
class String : public std::string
{
public:
String() {}
String(const char *text) : std::string(text != nullptr ? text : "") {}
String(const std::string &text) : std::string(text) {}
bool isEmpty() const { return empty(); }
};
class Print
{
public:
virtual ~Print() {}
virtual size_t write(const uint8_t *buffer, size_t size) = 0;
size_t print(const char *text) { return write(reinterpret_cast<const uint8_t *>(text), strlen(text)); }
size_t println(const char *text) { return print(text) + print("\r\n"); }
};
class HostSerial : public Print
{
public:
size_t write(const uint8_t *buffer, size_t size) override { return fwrite(buffer, 1, size, stdout); }
};
inline HostSerial Serial;
#endif // ALEX2ESP_HOST_ARDUINO_H

View file

@ -0,0 +1,307 @@
// 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.
// The devices are built with the calls of the example's setup().
// pio test -e native
#include <unity.h>
#include <ArduinoJson.h>
#include <string.h>
#include <string>
#include "AlexaDevice.h"
#include "AlexaLog.h"
#include "AlexaVersion.h"
// The endpoint id and the defaults are the same in every example; its name, category and capabilities are not
static std::string endpoint(const char *friendlyName, const char *displayCategory, const std::string &capabilities)
{
return std::string("{\"endpointId\":\"ESP-01\",\"friendlyName\":\"") + friendlyName +
"\",\"description\":\"Alexa2MQTT Default Device\",\"manufacturerName\":\"Alexa2MQTT\","
"\"displayCategories\":[\"" + displayCategory + "\"],"
"\"additionalAttributes\":{\"manufacturer\":\"Alexa2MQTT\",\"model\":\"Alexa2MQTT\","
"\"serialNumber\":\"ESP2Alex\",\"firmwareVersion\":\"" ALEX2ESP_VERSION "\","
"\"softwareVersion\":\"" ALEX2ESP_VERSION "\",\"customIdentifier\":\"ESP2Alex\"},"
"\"capabilities\":[" + capabilities + "]}";
}
static const char POWER[] =
"{\"interface\":\"Alexa.PowerController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"powerState\"}]}}";
static const char BRIGHTNESS[] =
"{\"interface\":\"Alexa.BrightnessController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"brightness\"}]}}";
static const char COLOR_TEMPERATURE[] =
"{\"interface\":\"Alexa.ColorTemperatureController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,"
"\"supported\":[{\"name\":\"colorTemperatureInKelvin\"}]}}";
static const char TEMPERATURE[] =
"{\"interface\":\"Alexa.TemperatureSensor\",\"version\":\"3\",\"type\":\"AlexaInterface\","
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"temperature\"}]}}";
static const char BLIND_TOGGLE[] =
"{\"interface\":\"Alexa.ToggleController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"toggleState\"}]},"
"\"semantics\":{\"actionMappings\":["
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Close\"],\"directive\":{\"name\":\"TurnOn\"}},"
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Open\"],\"directive\":{\"name\":\"TurnOff\"}}]},"
"\"instance\":\"ESP-01.Toggle\","
"\"capabilityResources\":{\"friendlyNames\":["
"{\"@type\":\"text\",\"value\":{\"text\":\"Bedroom Blinds\",\"locale\":\"en-US\"}}]}}";
static void assertDiscovery(const std::string &expected, const AlexaDevice &device)
{
std::string announced;
serializeJson(device.getDeviceJSON(), announced);
TEST_ASSERT_EQUAL_STRING(expected.c_str(), announced.c_str());
}
void setUp(void)
{
AlexaLog::setOutput(nullptr); // the lines about added capabilities are not what is tested
}
void tearDown(void) {}
void test_basic_light_discovery(void)
{
AlexaDevice device("Test Lamp", "root", "ESP-01");
device.setDisplayCategory(DisplayCategory::LIGHT);
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
assertDiscovery(endpoint("Test Lamp", "LIGHT", POWER), device);
}
void test_light_with_brightness_discovery(void)
{
AlexaDevice device("Test Lamp", "root", "ESP-01");
device.setDisplayCategory(DisplayCategory::LIGHT);
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
device.addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
assertDiscovery(endpoint("Test Lamp", "LIGHT", std::string(POWER) + "," + BRIGHTNESS), device);
}
void test_light_with_color_temperature_discovery(void)
{
AlexaDevice device("Test Lamp ESP", "root", "ESP-01");
device.setDisplayCategory(DisplayCategory::LIGHT);
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
device.addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
device.addCapability(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER);
assertDiscovery(endpoint("Test Lamp ESP", "LIGHT", std::string(POWER) + "," + BRIGHTNESS + "," + COLOR_TEMPERATURE),
device);
}
void test_temperature_sensor_discovery(void)
{
AlexaDevice device("ESP Temp", "root", "ESP-01");
device.setDisplayCategory(DisplayCategory::TEMPERATURE_SENSOR);
device.addCapability(AlexaInterfaceType::TEMPERATURE_SENSOR);
assertDiscovery(endpoint("ESP Temp", "TEMPERATURE_SENSOR", TEMPERATURE), device);
}
void test_blind_control_discovery(void)
{
String deviceName = "Bedroom Blinds";
AlexaDevice device(deviceName, "root", "ESP-01");
device.setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
AlexaInterface *toggleController = device.addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
toggleController->setInstance("ESP-01.Toggle");
toggleController->addFriendlyName(deviceName.c_str(), "en-US");
ActionMapping closeMapping({AlexaActions::Close}, "TurnOn");
ActionMapping openMapping({AlexaActions::Open}, "TurnOff");
toggleController->addActionMapping(closeMapping);
toggleController->addActionMapping(openMapping);
assertDiscovery(endpoint("Bedroom Blinds", "INTERIOR_BLIND", std::string(POWER) + "," + BLIND_TOGGLE), device);
}
// What the library prints, for the tests that expect an error
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;
}
};
struct ExpectedRow
{
const AlexaInterfaceDesc *row;
AlexaInterfaceType type;
const char *ns;
const char *version;
const char *properties; // separated by commas
};
// The interface pages of the Alexa documentation, as the design lists them
static const ExpectedRow EXPECTED_ROWS[] = {
{&AlexaInterfaces::EndpointHealth, AlexaInterfaceType::ENDPOINT_HEALTH, "Alexa.EndpointHealth", "3.1", "connectivity"},
{&AlexaInterfaces::PowerController, AlexaInterfaceType::POWER_CONTROLLER, "Alexa.PowerController", "3", "powerState"},
{&AlexaInterfaces::BrightnessController, AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "Alexa.BrightnessController", "3", "brightness"},
{&AlexaInterfaces::ColorController, AlexaInterfaceType::COLOR_CONTROLLER, "Alexa.ColorController", "3", "color"},
{&AlexaInterfaces::ColorTemperatureController, AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "Alexa.ColorTemperatureController", "3", "colorTemperatureInKelvin"},
{&AlexaInterfaces::PowerLevelController, AlexaInterfaceType::POWER_LEVEL_CONTROLLER, "Alexa.PowerLevelController", "3", "powerLevel"},
{&AlexaInterfaces::PercentageController, AlexaInterfaceType::PERCENTAGE_CONTROLLER, "Alexa.PercentageController", "3", "percentage"},
{&AlexaInterfaces::RangeController, AlexaInterfaceType::RANGE_CONTROLLER, "Alexa.RangeController", "3", "rangeValue"},
{&AlexaInterfaces::ModeController, AlexaInterfaceType::MODE_CONTROLLER, "Alexa.ModeController", "3", "mode"},
{&AlexaInterfaces::ToggleController, AlexaInterfaceType::TOGGLE_CONTROLLER, "Alexa.ToggleController", "3", "toggleState"},
{&AlexaInterfaces::ThermostatController, AlexaInterfaceType::THERMOSTAT_CONTROLLER, "Alexa.ThermostatController", "3.2", "targetSetpoint,lowerSetpoint,upperSetpoint,thermostatMode"},
{&AlexaInterfaces::TemperatureSensor, AlexaInterfaceType::TEMPERATURE_SENSOR, "Alexa.TemperatureSensor", "3", "temperature"},
{&AlexaInterfaces::HumiditySensor, AlexaInterfaceType::HUMIDITY_SENSOR, "Alexa.HumiditySensor", "3", "relativeHumidity"},
{&AlexaInterfaces::LockController, AlexaInterfaceType::LOCK_CONTROLLER, "Alexa.LockController", "3", "lockState"},
{&AlexaInterfaces::SceneController, AlexaInterfaceType::SCENE_CONTROLLER, "Alexa.SceneController", "3", ""},
{&AlexaInterfaces::ContactSensor, AlexaInterfaceType::CONTACT_SENSOR, "Alexa.ContactSensor", "3", "detectionState"},
{&AlexaInterfaces::MotionSensor, AlexaInterfaceType::MOTION_SENSOR, "Alexa.MotionSensor", "3", "detectionState"},
{&AlexaInterfaces::DoorbellEventSource, AlexaInterfaceType::DOORBELL_EVENT_SOURCE, "Alexa.DoorbellEventSource", "3", ""},
{&AlexaInterfaces::TimeHoldController, AlexaInterfaceType::TIME_HOLD_CONTROLLER, "Alexa.TimeHoldController", "3", "holdStartTime,holdEndTime"},
{&AlexaInterfaces::Speaker, AlexaInterfaceType::SPEAKER, "Alexa.Speaker", "3", "volume,muted"},
{&AlexaInterfaces::StepSpeaker, AlexaInterfaceType::STEP_SPEAKER, "Alexa.StepSpeaker", "3", ""},
{&AlexaInterfaces::PlaybackController, AlexaInterfaceType::PLAYBACK_CONTROLLER, "Alexa.PlaybackController", "3", ""},
{&AlexaInterfaces::PlaybackStateReporter, AlexaInterfaceType::PLAYBACK_STATE_REPORTER, "Alexa.PlaybackStateReporter", "3", "playbackState"},
{&AlexaInterfaces::InputController, AlexaInterfaceType::INPUT_CONTROLLER, "Alexa.InputController", "3", "input"},
{&AlexaInterfaces::ChannelController, AlexaInterfaceType::CHANNEL_CONTROLLER, "Alexa.ChannelController", "3", "channel"},
{&AlexaInterfaces::InventoryLevelSensor, AlexaInterfaceType::INVENTORY_LEVEL_SENSOR, "Alexa.InventoryLevelSensor", "3", "level"},
{&AlexaInterfaces::SimpleEventSource, AlexaInterfaceType::SIMPLE_EVENT_SOURCE, "Alexa.SimpleEventSource", "1.0", ""},
{&AlexaInterfaces::WakeOnLANController, AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER, "Alexa.WakeOnLANController", "3", ""},
};
void test_rows_hold_the_namespace_version_and_properties_of_their_interface(void)
{
TEST_ASSERT_EQUAL_size_t(28, sizeof(EXPECTED_ROWS) / sizeof(EXPECTED_ROWS[0]));
for (const ExpectedRow &expected : EXPECTED_ROWS)
{
const AlexaInterfaceDesc &row = *expected.row;
TEST_ASSERT_EQUAL_STRING(expected.ns, row.ns);
TEST_ASSERT_EQUAL_STRING_MESSAGE(expected.version, row.version, expected.ns);
TEST_ASSERT_TRUE_MESSAGE(expected.type == row.interfaceType(), expected.ns);
std::string properties;
for (uint8_t i = 0; i < row.propertyCount(); i++)
{
properties += (i == 0 ? "" : ",");
properties += row.property(i);
}
TEST_ASSERT_EQUAL_STRING_MESSAGE(expected.properties, properties.c_str(), expected.ns);
}
}
void test_every_row_is_found_by_its_type(void)
{
for (const ExpectedRow &expected : EXPECTED_ROWS)
{
TEST_ASSERT_EQUAL_PTR_MESSAGE(expected.row, alexaInterfaceRow(expected.type), expected.ns);
}
TEST_ASSERT_NULL(alexaInterfaceRow(AlexaInterfaceType::COOKING));
TEST_ASSERT_NULL(alexaInterfaceRow(AlexaInterfaceType::UNKNOWN));
}
void test_generic_controllers_and_interfaces_without_properties_are_flagged(void)
{
TEST_ASSERT_TRUE(AlexaInterfaces::ToggleController.has(AIF_INSTANCED));
TEST_ASSERT_TRUE(AlexaInterfaces::RangeController.has(AIF_SEMANTICS));
TEST_ASSERT_TRUE(AlexaInterfaces::InventoryLevelSensor.has(AIF_INSTANCED));
TEST_ASSERT_FALSE(AlexaInterfaces::InventoryLevelSensor.has(AIF_SEMANTICS));
TEST_ASSERT_TRUE(AlexaInterfaces::SimpleEventSource.has(AIF_NO_PROPERTIES));
TEST_ASSERT_FALSE(AlexaInterfaces::PowerController.has(AIF_INSTANCED));
TEST_ASSERT_FALSE(AlexaInterfaces::PlaybackController.has(AIF_NO_PROPERTIES));
}
void test_capability_by_row_and_by_type_is_the_same_capability(void)
{
AlexaDevice device("Test Lamp", "root", "ESP-01");
AlexaInterface *byType = device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
AlexaInterface *byRow = device.addCapability(AlexaInterfaces::PowerController);
TEST_ASSERT_NOT_NULL(byType);
TEST_ASSERT_EQUAL_PTR(byType, byRow);
assertDiscovery(endpoint("Test Lamp", "OTHER", POWER), device);
}
void test_type_without_a_row_is_refused_with_an_error(void)
{
CapturedLog log;
AlexaLog::setOutput(&log);
AlexaDevice device("Oven", "root", "ESP-01");
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaceType::COOKING));
TEST_ASSERT_NOT_NULL(strstr(log.text.c_str(), "[Alex2ESP] error: ESP-01: capability not added"));
assertDiscovery(endpoint("Oven", "OTHER", ""), device);
}
void test_scene_is_announced_without_properties(void)
{
AlexaDevice device("Movie Night", "root", "ESP-01");
device.setDisplayCategory(DisplayCategory::SCENE_TRIGGER);
device.addCapability(AlexaInterfaces::SceneController);
assertDiscovery(endpoint("Movie Night", "SCENE_TRIGGER",
"{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\"}"),
device);
}
void test_thermostat_announces_its_four_properties(void)
{
AlexaDevice device("Hallway", "root", "ESP-01");
device.setDisplayCategory(DisplayCategory::THERMOSTAT);
device.addCapability(AlexaInterfaceType::THERMOSTAT_CONTROLLER);
assertDiscovery(endpoint("Hallway", "THERMOSTAT",
"{\"interface\":\"Alexa.ThermostatController\",\"version\":\"3.2\",\"type\":\"AlexaInterface\","
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":["
"{\"name\":\"targetSetpoint\"},{\"name\":\"lowerSetpoint\"},{\"name\":\"upperSetpoint\"},"
"{\"name\":\"thermostatMode\"}]}}"),
device);
}
void test_directive_gets_the_type_of_the_capability_it_names(void)
{
AlexaDevice device("Test Lamp", "root", "ESP-01");
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
device.addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
TEST_ASSERT_TRUE(AlexaInterfaceType::BRIGHTNESS_CONTROLLER == device.getInterfaceType("Alexa.BrightnessController"));
TEST_ASSERT_TRUE(AlexaInterfaceType::POWER_CONTROLLER == device.getInterfaceType("Alexa.PowerController"));
// An interface of the library that this device does not have, and no interface at all
TEST_ASSERT_TRUE(AlexaInterfaceType::UNKNOWN == device.getInterfaceType("Alexa.LockController"));
TEST_ASSERT_TRUE(AlexaInterfaceType::UNKNOWN == device.getInterfaceType("Alexa"));
}
void test_display_categories_have_the_name_of_their_enumerator(void)
{
TEST_ASSERT_EQUAL_STRING("ACTIVITY_TRIGGER", alexaDisplayCategoryName(DisplayCategory::ACTIVITY_TRIGGER));
TEST_ASSERT_EQUAL_STRING("OTHER", alexaDisplayCategoryName(DisplayCategory::OTHER));
TEST_ASSERT_EQUAL_STRING("WEARABLE", alexaDisplayCategoryName(DisplayCategory::WEARABLE));
TEST_ASSERT_EQUAL_STRING("TV", DisplayCategoryUtils::toString(DisplayCategory::TV).c_str());
TEST_ASSERT_EQUAL_STRING("OTHER", alexaDisplayCategoryName(static_cast<DisplayCategory>(200)));
}
int main(int, char **)
{
UNITY_BEGIN();
RUN_TEST(test_basic_light_discovery);
RUN_TEST(test_light_with_brightness_discovery);
RUN_TEST(test_light_with_color_temperature_discovery);
RUN_TEST(test_temperature_sensor_discovery);
RUN_TEST(test_blind_control_discovery);
RUN_TEST(test_rows_hold_the_namespace_version_and_properties_of_their_interface);
RUN_TEST(test_every_row_is_found_by_its_type);
RUN_TEST(test_generic_controllers_and_interfaces_without_properties_are_flagged);
RUN_TEST(test_capability_by_row_and_by_type_is_the_same_capability);
RUN_TEST(test_type_without_a_row_is_refused_with_an_error);
RUN_TEST(test_scene_is_announced_without_properties);
RUN_TEST(test_thermostat_announces_its_four_properties);
RUN_TEST(test_directive_gets_the_type_of_the_capability_it_names);
RUN_TEST(test_display_categories_have_the_name_of_their_enumerator);
return UNITY_END();
}