Report helpers for every tier-1 property, asset ids, AlexaCapability::isValid()
AlexaStatusMessage gets a helper for color, percentage, power level, range, mode, lock state, contact, motion, humidity and the thermostat (target, lower and upper setpoint, mode), and addProperty(row, place, value) for a property without one. Each is a function of its own on one shared builder; a value outside of the range of its property is reported as the nearest of the range with an error. AlexaResources.h holds the 103 asset ids of resources-and-assets.html (AlexaAssets, AlexaUnits); AlexaState has the seven states that were missing; addStateMapping() takes a number of any type. A setter that refuses its argument makes isValid() false, and the capability is left out of discovery with an error instead of being announced without what was refused. basicLight: static RAM 30,520 B (was 30,540), flash 333,389 B (was 334,981); 130 host tests (was 112). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
3215457f91
commit
77156ffbe5
10 changed files with 995 additions and 100 deletions
20
keywords.txt
20
keywords.txt
|
|
@ -14,6 +14,10 @@ AlexaInterfaceType KEYWORD1
|
||||||
AlexaInterfaces KEYWORD1
|
AlexaInterfaces KEYWORD1
|
||||||
AlexaInterfaceDesc KEYWORD1
|
AlexaInterfaceDesc KEYWORD1
|
||||||
AlexaStatusMessage KEYWORD1
|
AlexaStatusMessage KEYWORD1
|
||||||
|
AlexaAssets KEYWORD1
|
||||||
|
AlexaUnits KEYWORD1
|
||||||
|
AlexaLockState KEYWORD1
|
||||||
|
AlexaThermostatMode KEYWORD1
|
||||||
ActionMapping KEYWORD1
|
ActionMapping KEYWORD1
|
||||||
AlexaActions KEYWORD1
|
AlexaActions KEYWORD1
|
||||||
AlexaActionsUtils KEYWORD1
|
AlexaActionsUtils KEYWORD1
|
||||||
|
|
@ -80,6 +84,22 @@ AddTemperatureSensorProp KEYWORD2
|
||||||
AddBrightnessControllerProp KEYWORD2
|
AddBrightnessControllerProp KEYWORD2
|
||||||
AddColorTemperatureControllerProp KEYWORD2
|
AddColorTemperatureControllerProp KEYWORD2
|
||||||
AddToggleControllerProp KEYWORD2
|
AddToggleControllerProp KEYWORD2
|
||||||
|
addColorControllerProp KEYWORD2
|
||||||
|
addPowerLevelControllerProp KEYWORD2
|
||||||
|
addPercentageControllerProp KEYWORD2
|
||||||
|
addRangeControllerProp KEYWORD2
|
||||||
|
addModeControllerProp KEYWORD2
|
||||||
|
addLockControllerProp KEYWORD2
|
||||||
|
addContactSensorProp KEYWORD2
|
||||||
|
addMotionSensorProp KEYWORD2
|
||||||
|
addHumiditySensorProp KEYWORD2
|
||||||
|
addThermostatSetpointProp KEYWORD2
|
||||||
|
addThermostatLowerSetpointProp KEYWORD2
|
||||||
|
addThermostatUpperSetpointProp KEYWORD2
|
||||||
|
addThermostatDualSetpointProp KEYWORD2
|
||||||
|
addThermostatModeProp KEYWORD2
|
||||||
|
addProperty KEYWORD2
|
||||||
|
isValid KEYWORD2
|
||||||
AddContextProp KEYWORD2
|
AddContextProp KEYWORD2
|
||||||
send KEYWORD2
|
send KEYWORD2
|
||||||
printMemoryInfo KEYWORD2
|
printMemoryInfo KEYWORD2
|
||||||
|
|
|
||||||
58
readme.md
58
readme.md
|
|
@ -141,19 +141,23 @@ void fillLift(JsonObject configuration, void*) {
|
||||||
range["minimumValue"] = 0;
|
range["minimumValue"] = 0;
|
||||||
range["maximumValue"] = 100;
|
range["maximumValue"] = 100;
|
||||||
range["precision"] = 1;
|
range["precision"] = 1;
|
||||||
configuration["unitOfMeasure"] = "Alexa.Unit.Percent";
|
configuration["unitOfMeasure"] = FPSTR(AlexaUnits::Percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
|
device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
|
||||||
->addFriendlyAsset(PSTR("Alexa.Setting.Opening"))
|
->addFriendlyAsset(AlexaAssets::Setting_Opening)
|
||||||
.addFriendlyName("Lift", "en-US")
|
.addFriendlyName("Lift", "en-US")
|
||||||
.setConfiguration(fillLift)
|
.setConfiguration(fillLift)
|
||||||
.addActionMapping(ActionMapping({AlexaAction::Close, AlexaAction::Lower}, "SetRangeValue", "{\"rangeValue\":0}"))
|
.addActionMapping(ActionMapping({AlexaAction::Close, AlexaAction::Lower}, "SetRangeValue", "{\"rangeValue\":0}"))
|
||||||
.addActionMapping(ActionMapping({AlexaAction::Open, AlexaAction::Raise}, "SetRangeValue", "{\"rangeValue\":100}"))
|
.addActionMapping(ActionMapping({AlexaAction::Open, AlexaAction::Raise}, "SetRangeValue", "{\"rangeValue\":100}"))
|
||||||
.addStateMapping({AlexaState::Closed}, 0.0f)
|
.addStateMapping({AlexaState::Closed}, 0)
|
||||||
.addStateMapping({AlexaState::Open}, 1.0f, 100.0f);
|
.addStateMapping({AlexaState::Open}, 1, 100);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`AlexaAssets` and `AlexaUnits` (`src/AlexaResources.h`) hold the names and units of the catalog of Alexa, which Alexa translates: `AlexaAssets::Setting_Opening` is `Alexa.Setting.Opening`, `AlexaUnits::Temperature_Celsius` is `Alexa.Unit.Temperature.Celsius`. An id that the sketch does not name takes no flash.
|
||||||
|
|
||||||
|
A setter that refuses what it is given prints the reason and the chain goes on. `isValid()` of the capability is `false` from then on, and the device leaves the capability out of discovery with an error, so that Alexa does not learn half of it.
|
||||||
|
|
||||||
`addCapability()` returns `nullptr` for a generic controller without an instance: check the pointer when the instance is not a literal. A capability holds 3 names, 4 action mappings and 2 state mappings (`-DALEX2ESP_MAX_FRIENDLY_NAMES`, `-DALEX2ESP_MAX_ACTION_MAPPINGS`, `-DALEX2ESP_MAX_STATE_MAPPINGS` in `build_flags` change that). The instance and the text of a friendly name are copied. The locale, an asset id, the text value of a state mapping and the directive name and payload of an `ActionMapping` are kept as pointers: pass literals. `setNonControllable(true)` announces a capability that reports its state and takes no directives.
|
`addCapability()` returns `nullptr` for a generic controller without an instance: check the pointer when the instance is not a literal. A capability holds 3 names, 4 action mappings and 2 state mappings (`-DALEX2ESP_MAX_FRIENDLY_NAMES`, `-DALEX2ESP_MAX_ACTION_MAPPINGS`, `-DALEX2ESP_MAX_STATE_MAPPINGS` in `build_flags` change that). The instance and the text of a friendly name are copied. The locale, an asset id, the text value of a state mapping and the directive name and payload of an `ActionMapping` are kept as pointers: pass literals. `setNonControllable(true)` announces a capability that reports its state and takes no directives.
|
||||||
|
|
||||||
A device holds 8 capabilities (`-DALEX2ESP_MAX_CAPABILITIES`); `addCapability()` for one more prints an error and returns `nullptr`.
|
A device holds 8 capabilities (`-DALEX2ESP_MAX_CAPABILITIES`); `addCapability()` for one more prints an error and returns `nullptr`.
|
||||||
|
|
@ -240,24 +244,24 @@ The library describes these interfaces. `addCapability()` takes the row, `AlexaI
|
||||||
| `PowerController` | `POWER_CONTROLLER` | 3 | powerState | `addPowerControllerProp` |
|
| `PowerController` | `POWER_CONTROLLER` | 3 | powerState | `addPowerControllerProp` |
|
||||||
| `BrightnessController` | `BRIGHTNESS_CONTROLLER` | 3 | brightness | `addBrightnessControllerProp` |
|
| `BrightnessController` | `BRIGHTNESS_CONTROLLER` | 3 | brightness | `addBrightnessControllerProp` |
|
||||||
| `ColorTemperatureController` | `COLOR_TEMPERATURE_CONTROLLER` | 3 | colorTemperatureInKelvin | `addColorTemperatureControllerProp` |
|
| `ColorTemperatureController` | `COLOR_TEMPERATURE_CONTROLLER` | 3 | colorTemperatureInKelvin | `addColorTemperatureControllerProp` |
|
||||||
| `ToggleController` | `TOGGLE_CONTROLLER` | 3 | toggleState | `addToggleControllerProp` |
|
| `ColorController` | `COLOR_CONTROLLER` | 3 | color | `addColorControllerProp(hue, saturation, brightness)` |
|
||||||
| `TemperatureSensor` | `TEMPERATURE_SENSOR` | 3 | temperature | `addTemperatureSensorProp` |
|
| `PowerLevelController` | `POWER_LEVEL_CONTROLLER` | 3 | powerLevel | `addPowerLevelControllerProp` |
|
||||||
| `ColorController` | `COLOR_CONTROLLER` | 3 | color | `addContextProp` |
|
| `PercentageController` | `PERCENTAGE_CONTROLLER` | 3 | percentage | `addPercentageControllerProp` |
|
||||||
| `PowerLevelController` | `POWER_LEVEL_CONTROLLER` | 3 | powerLevel | `addContextProp` |
|
| `RangeController` | `RANGE_CONTROLLER` | 3 | rangeValue | `addRangeControllerProp(instance, value)` |
|
||||||
| `PercentageController` | `PERCENTAGE_CONTROLLER` | 3 | percentage | `addContextProp` |
|
| `ModeController` | `MODE_CONTROLLER` | 3 | mode | `addModeControllerProp(instance, mode)` |
|
||||||
| `RangeController` | `RANGE_CONTROLLER` | 3 | rangeValue | `addContextProp` |
|
| `ToggleController` | `TOGGLE_CONTROLLER` | 3 | toggleState | `addToggleControllerProp(instance, state)` |
|
||||||
| `ModeController` | `MODE_CONTROLLER` | 3 | mode | `addContextProp` |
|
| `ThermostatController` | `THERMOSTAT_CONTROLLER` | 3.2 | targetSetpoint, lowerSetpoint, upperSetpoint, thermostatMode | `addThermostatSetpointProp`, `addThermostatLowerSetpointProp`, `addThermostatUpperSetpointProp` (both: `addThermostatDualSetpointProp(lower, upper)`), `addThermostatModeProp` |
|
||||||
| `ThermostatController` | `THERMOSTAT_CONTROLLER` | 3.2 | targetSetpoint, lowerSetpoint, upperSetpoint, thermostatMode | `addContextProp` |
|
| `TemperatureSensor` | `TEMPERATURE_SENSOR` | 3 | temperature | `addTemperatureSensorProp(value, scale)` |
|
||||||
| `HumiditySensor` | `HUMIDITY_SENSOR` | 3 | relativeHumidity | `addContextProp` |
|
| `HumiditySensor` | `HUMIDITY_SENSOR` | 3 | relativeHumidity | `addHumiditySensorProp` |
|
||||||
| `LockController` | `LOCK_CONTROLLER` | 3 | lockState | `addContextProp` |
|
| `LockController` | `LOCK_CONTROLLER` | 3 | lockState | `addLockControllerProp` |
|
||||||
| `ContactSensor` | `CONTACT_SENSOR` | 3 | detectionState | `addContextProp` |
|
| `ContactSensor` | `CONTACT_SENSOR` | 3 | detectionState | `addContactSensorProp` |
|
||||||
| `MotionSensor` | `MOTION_SENSOR` | 3 | detectionState | `addContextProp` |
|
| `MotionSensor` | `MOTION_SENSOR` | 3 | detectionState | `addMotionSensorProp` |
|
||||||
| `TimeHoldController` | `TIME_HOLD_CONTROLLER` | 3 | holdStartTime, holdEndTime | `addContextProp` |
|
| `TimeHoldController` | `TIME_HOLD_CONTROLLER` | 3 | holdStartTime, holdEndTime | `addProperty` |
|
||||||
| `Speaker` | `SPEAKER` | 3 | volume, muted | `addContextProp` |
|
| `Speaker` | `SPEAKER` | 3 | volume, muted | `addProperty` |
|
||||||
| `PlaybackStateReporter` | `PLAYBACK_STATE_REPORTER` | 3 | playbackState | `addContextProp` |
|
| `PlaybackStateReporter` | `PLAYBACK_STATE_REPORTER` | 3 | playbackState | `addProperty` |
|
||||||
| `InputController` | `INPUT_CONTROLLER` | 3 | input | `addContextProp` |
|
| `InputController` | `INPUT_CONTROLLER` | 3 | input | `addProperty` |
|
||||||
| `ChannelController` | `CHANNEL_CONTROLLER` | 3 | channel | `addContextProp` |
|
| `ChannelController` | `CHANNEL_CONTROLLER` | 3 | channel | `addProperty` |
|
||||||
| `InventoryLevelSensor` | `INVENTORY_LEVEL_SENSOR` | 3 | level | `addContextProp` |
|
| `InventoryLevelSensor` | `INVENTORY_LEVEL_SENSOR` | 3 | level | `addProperty` |
|
||||||
| `PlaybackController` | `PLAYBACK_CONTROLLER` | 3 | `"properties": {}` | |
|
| `PlaybackController` | `PLAYBACK_CONTROLLER` | 3 | `"properties": {}` | |
|
||||||
| `WakeOnLANController` | `WAKE_ON_LAN_CONTROLLER` | 3 | `"properties": {}` | |
|
| `WakeOnLANController` | `WAKE_ON_LAN_CONTROLLER` | 3 | `"properties": {}` | |
|
||||||
| `SceneController` | `SCENE_CONTROLLER` | 3 | no `properties` object | |
|
| `SceneController` | `SCENE_CONTROLLER` | 3 | no `properties` object | |
|
||||||
|
|
@ -265,7 +269,7 @@ The library describes these interfaces. `addCapability()` takes the row, `AlexaI
|
||||||
| `StepSpeaker` | `STEP_SPEAKER` | 3 | no `properties` object | |
|
| `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) the sketch writes in the function it passes to `setConfiguration()`. Their events are not built by the library yet.
|
Every interface is announced with its version and properties; what else its discovery object needs (the configuration of a thermostat, the presets of a range) the sketch writes in the function it passes to `setConfiguration()`. A report helper is a function of its own, and a sketch links the ones it calls. A value outside of the range Alexa gives for its property (brightness, percentage, power level and humidity 0 to 100, color temperature 1000 to 10000, hue 0 to 360, saturation and brightness of a color 0 to 1) is reported as the nearest value of the range, with an error in the log. `addProperty(row, place, value)` adds a property that has no helper by its place in the row, 0 for the first: `addProperty(AlexaInterfaces::Speaker, 1, muted)` reports `muted`.
|
||||||
|
|
||||||
Every other value of `AlexaInterfaceType` (`COOKING`, `LAUNCHER`, `SECURITY_PANEL_CONTROLLER`, ...) names an interface the library has no row for. `addCapability()` adds nothing for it, prints `[Alex2ESP] error: <endpointId>: capability not added: ...` and returns `nullptr`.
|
Every other value of `AlexaInterfaceType` (`COOKING`, `LAUNCHER`, `SECURITY_PANEL_CONTROLLER`, ...) names an interface the library has no row for. `addCapability()` adds nothing for it, prints `[Alex2ESP] error: <endpointId>: capability not added: ...` and returns `nullptr`.
|
||||||
|
|
||||||
|
|
@ -320,6 +324,10 @@ Behaviour changes:
|
||||||
- New: `AlexaDevice::onDirective(handler)`. The handler, `void handler(AlexaDirective& d)`, gets every directive of its device with the device (`d.device`), the capability it is for (`d.capability`, `d.type`), its `ns`, `name`, `instance`, `correlationToken` and `payload`, and builds the answer with `d.response()`, `d.stateReport()`, `d.error(type, message)`, `d.deferred(seconds)` or, for a scene, `d.sceneStarted()` and `d.sceneStopped()`; one function can serve several devices. `registerEvent()` stays for the handlers of 1.x, which a device calls when it has no handler of `onDirective()`. `AlexaDevice::findCapability(namespace, instance)`.
|
- New: `AlexaDevice::onDirective(handler)`. The handler, `void handler(AlexaDirective& d)`, gets every directive of its device with the device (`d.device`), the capability it is for (`d.capability`, `d.type`), its `ns`, `name`, `instance`, `correlationToken` and `payload`, and builds the answer with `d.response()`, `d.stateReport()`, `d.error(type, message)`, `d.deferred(seconds)` or, for a scene, `d.sceneStarted()` and `d.sceneStopped()`; one function can serve several devices. `registerEvent()` stays for the handlers of 1.x, which a device calls when it has no handler of `onDirective()`. `AlexaDevice::findCapability(namespace, instance)`.
|
||||||
- New kinds of messages beside `Response` and `StateReport`: `ErrorResponse` (`AlexaErrorType` has the types of Alexa, `asErrorResponse(type, message)` turns an answer into one), `DeferredResponse` and the answer that follows it (`sendAsync()`, on `<root>/<endpointId>/deferredResponse`), `ChangeReport` (`AlexaDevice::changeReport(cause)`, on `<root>/changeReport`), the events of a scene, and `DoorbellPress` (`AlexaDevice::doorbellPress()`, or `event(row, name)` for another interface, on `<root>/event`). `AlexaDevice::response(token)` and `stateReport(token)` build an answer outside of the handler. `payload()` gives the payload of a message, `getKind()` its kind.
|
- New kinds of messages beside `Response` and `StateReport`: `ErrorResponse` (`AlexaErrorType` has the types of Alexa, `asErrorResponse(type, message)` turns an answer into one), `DeferredResponse` and the answer that follows it (`sendAsync()`, on `<root>/<endpointId>/deferredResponse`), `ChangeReport` (`AlexaDevice::changeReport(cause)`, on `<root>/changeReport`), the events of a scene, and `DoorbellPress` (`AlexaDevice::doorbellPress()`, or `event(row, name)` for another interface, on `<root>/event`). `AlexaDevice::response(token)` and `stateReport(token)` build an answer outside of the handler. `payload()` gives the payload of a message, `getKind()` its kind.
|
||||||
- The report helpers begin with a small letter: `addHealthProp`, `addPowerControllerProp`, `addBrightnessControllerProp`, `addColorTemperatureControllerProp`, `addToggleControllerProp(instance, state)`, `addTemperatureSensorProp(value, scale)`, `addContextProp`. The names of 1.x stay, with their order of arguments.
|
- The report helpers begin with a small letter: `addHealthProp`, `addPowerControllerProp`, `addBrightnessControllerProp`, `addColorTemperatureControllerProp`, `addToggleControllerProp(instance, state)`, `addTemperatureSensorProp(value, scale)`, `addContextProp`. The names of 1.x stay, with their order of arguments.
|
||||||
|
- New report helpers, one for every property of the interfaces a device on this board is likely to have: `addColorControllerProp`, `addPowerLevelControllerProp`, `addPercentageControllerProp`, `addRangeControllerProp`, `addModeControllerProp`, `addLockControllerProp` (`AlexaLockState`), `addContactSensorProp`, `addMotionSensorProp`, `addHumiditySensorProp`, `addThermostatSetpointProp`, `addThermostatLowerSetpointProp`, `addThermostatUpperSetpointProp`, `addThermostatDualSetpointProp` and `addThermostatModeProp` (`AlexaThermostatMode`); `addProperty(row, place, value)` for a property without one. See Interface Types.
|
||||||
|
- A helper reports a value outside of the range of its property as the nearest value of the range and prints an error; 1.1.0 sent the value as it was. The brightness and the color temperature are taken as `int` (1.1.0: `unsigned int`), so that a negative number is reported as the lowest value.
|
||||||
|
- New: `AlexaAssets` and `AlexaUnits` in `src/AlexaResources.h`, the 103 asset ids of the catalog of Alexa as strings in program memory. `AlexaState` has the states `EcoOn`, `EcoOff`, `Low`, `Empty`, `Full`, `Done` and `Stuck` beside `Open` and `Closed`. `addStateMapping()` takes a number of any type, `addStateMapping({AlexaState::Closed}, 0)`.
|
||||||
|
- New: `AlexaCapability::isValid()`, `false` after a setter has refused what it was given. A capability that is not valid is left out of discovery with an error; before, it was announced without what had been refused.
|
||||||
- A temperature is reported in the scale it is given in: `AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT, 69)` reports 69 `FAHRENHEIT`, where 1.1.0 reported 20.56 `CELSIUS`. `TemperatureSensorScale::KELVIN` is new.
|
- A temperature is reported in the scale it is given in: `AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT, 69)` reports 69 `FAHRENHEIT`, where 1.1.0 reported 20.56 `CELSIUS`. `TemperatureSensorScale::KELVIN` is new.
|
||||||
- `messageId` is a UUID of version 4 from the random source of the hardware. 1.1.0 sent 37 characters from `rand()`, seeded with the time in seconds: two messages of one second had the same id.
|
- `messageId` is a UUID of version 4 from the random source of the hardware. 1.1.0 sent 37 characters from `rand()`, seeded with the time in seconds: two messages of one second had the same id.
|
||||||
- A `ChangeReport` or an event that a handler sends is not taken for the answer to its directive.
|
- A `ChangeReport` or an event that a handler sends is not taken for the answer to its directive.
|
||||||
|
|
@ -330,9 +338,9 @@ Behaviour changes:
|
||||||
- `PlaybackController` and `WakeOnLANController` are announced with `"properties": {}`, as their pages show them.
|
- `PlaybackController` and `WakeOnLANController` are announced with `"properties": {}`, as their pages show them.
|
||||||
- Removed: `AlexaInterface::getJSON()` (`toJson()` adds the capability to the capabilities of its endpoint) and `getProps()` (the row has the properties), `ActionMapping::getJSON()` and the `String` and `std::vector` members of `ActionMapping`, the class `FriendlyName`.
|
- Removed: `AlexaInterface::getJSON()` (`toJson()` adds the capability to the capabilities of its endpoint) and `getProps()` (the row has the properties), `ActionMapping::getJSON()` and the `String` and `std::vector` members of `ActionMapping`, the class `FriendlyName`.
|
||||||
|
|
||||||
Memory: `examples/basicLight.cpp` for a D1 mini takes 30,540 bytes of static RAM (1.1.0: 52,768) and 334,981 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). 18,260 bytes of the static RAM were the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients; 3,720 were the names, versions and properties of all interfaces and the names of the display categories, which are in flash now. SNTP and the time stamp are 1.8 KB of the flash figure. The instance, names, configuration and mappings of a capability are 2.0 KB of it, the directive handler and the ErrorResponse 0.9 KB, the kinds of messages and the UUID 1.3 KB. A kind that a sketch does not send is not linked.
|
Memory: `examples/basicLight.cpp` for a D1 mini takes 30,520 bytes of static RAM (1.1.0: 52,768) and 333,389 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). 18,260 bytes of the static RAM were the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients; 3,720 were the names, versions and properties of all interfaces and the names of the display categories, which are in flash now. SNTP and the time stamp are 1.8 KB of the flash figure. The instance, names, configuration and mappings of a capability are 2.0 KB of it, the directive handler and the ErrorResponse 0.9 KB, the kinds of messages and the UUID 1.3 KB. A kind that a sketch does not send and a report helper that it does not call are not linked.
|
||||||
|
|
||||||
Tests: `pio test -e native` in the repository runs 112 host tests: 52 of the receive and publish logic (reassembly of fragments, the directives that wait for `loop()`, repeated directives, the size limits, a heap without room, the wait between reconnects, topics, time stamps, the message id), 30 of discovery (the discovery object of every example against what 1.1.0 announced, the 28 rows against the interface pages, a type without a row, the discovery objects of a range, a mode and a toggle controller, a scene and a doorbell, what a capability refuses), 11 of dispatch (the device and the capability a directive reaches, one handler for two devices, the handlers of 1.x, a capability more than a device holds, the answers to a directive for a capability the device lacks and to one without a handler) and 19 of the messages (every kind against the JSON and the topic it is published with, the names of 1.x, the scale of a temperature, what `send()` and `sendAsync()` refuse). No board is needed.
|
Tests: `pio test -e native` in the repository runs 130 host tests: 52 of the receive and publish logic (reassembly of fragments, the directives that wait for `loop()`, repeated directives, the size limits, a heap without room, the wait between reconnects, topics, time stamps, the message id), 35 of discovery (the discovery object of every example against what 1.1.0 announced, the 28 rows against the interface pages, a type without a row, the discovery objects of a range, a mode and a toggle controller, a scene and a doorbell, what a capability refuses and that it is left out afterwards, the states and the asset ids), 11 of dispatch (the device and the capability a directive reaches, one handler for two devices, the handlers of 1.x, a capability more than a device holds, the answers to a directive for a capability the device lacks and to one without a handler) and 32 of the messages (every kind against the JSON and the topic it is published with, every report helper against the JSON of its property, values outside of their range, the names of 1.x, the scale of a temperature, what `send()` and `sendAsync()` refuse). No board is needed.
|
||||||
|
|
||||||
Packaging: `library.json`, `library.properties` and the `softwareVersion` and `firmwareVersion` that a device reports in discovery say 1.2.0; the library has the number in one place, `ALEX2ESP_VERSION` in `src/AlexaVersion.h`. The `platformio.ini` of the repository is for the host tests; a sketch does not need it.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
#include "AlexaInterface.h"
|
#include "AlexaInterface.h"
|
||||||
#include "AlexaLimits.h"
|
#include "AlexaLimits.h"
|
||||||
#include "AlexaLog.h"
|
#include "AlexaLog.h"
|
||||||
|
#include "AlexaResources.h"
|
||||||
#include "AlexaTransport.h"
|
#include "AlexaTransport.h"
|
||||||
#include "AlexaUtils.h"
|
#include "AlexaUtils.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,21 @@ 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_OPEN[] PROGMEM = "Alexa.States.Open";
|
||||||
static const char STATE_CLOSED[] PROGMEM = "Alexa.States.Closed";
|
static const char STATE_CLOSED[] PROGMEM = "Alexa.States.Closed";
|
||||||
static const char *const STATE_NAMES[] PROGMEM = {STATE_OPEN, STATE_CLOSED};
|
static const char STATE_ECO_ON[] PROGMEM = "Alexa.States.EcoOn";
|
||||||
|
static const char STATE_ECO_OFF[] PROGMEM = "Alexa.States.EcoOff";
|
||||||
|
static const char STATE_LOW[] PROGMEM = "Alexa.States.Low";
|
||||||
|
static const char STATE_EMPTY[] PROGMEM = "Alexa.States.Empty";
|
||||||
|
static const char STATE_FULL[] PROGMEM = "Alexa.States.Full";
|
||||||
|
static const char STATE_DONE[] PROGMEM = "Alexa.States.Done";
|
||||||
|
static const char STATE_STUCK[] PROGMEM = "Alexa.States.Stuck";
|
||||||
|
static const char *const STATE_NAMES[] PROGMEM = {STATE_OPEN, STATE_CLOSED, STATE_ECO_ON, STATE_ECO_OFF, STATE_LOW,
|
||||||
|
STATE_EMPTY, STATE_FULL, STATE_DONE, STATE_STUCK};
|
||||||
static const uint8_t STATE_COUNT = sizeof(STATE_NAMES) / sizeof(STATE_NAMES[0]);
|
static const uint8_t STATE_COUNT = sizeof(STATE_NAMES) / sizeof(STATE_NAMES[0]);
|
||||||
|
|
||||||
|
static_assert(static_cast<uint8_t>(AlexaAction::SetEcoOff) + 1 == ACTION_COUNT, "an action has no name");
|
||||||
|
static_assert(static_cast<uint8_t>(AlexaState::Stuck) + 1 == STATE_COUNT, "a state has no name");
|
||||||
|
static_assert(ACTION_COUNT <= 8 && STATE_COUNT <= 16, "a mapping has no bit for every action or state");
|
||||||
|
|
||||||
static PGM_P nameAt(const char *const *names, uint8_t place)
|
static PGM_P nameAt(const char *const *names, uint8_t place)
|
||||||
{
|
{
|
||||||
return static_cast<PGM_P>(pgm_read_ptr(&names[place]));
|
return static_cast<PGM_P>(pgm_read_ptr(&names[place]));
|
||||||
|
|
@ -37,7 +49,7 @@ PGM_P alexaStateName(AlexaState state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The names of the bits that are set, in the order of the table
|
// 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)
|
static void addNames(JsonArray target, uint16_t bits, const char *const *names, uint8_t count)
|
||||||
{
|
{
|
||||||
for (uint8_t place = 0; place < count; place++)
|
for (uint8_t place = 0; place < count; place++)
|
||||||
{
|
{
|
||||||
|
|
@ -76,7 +88,7 @@ AlexaCapabilityName::AlexaCapabilityName(const AlexaInterfaceDesc &row, const ch
|
||||||
}
|
}
|
||||||
|
|
||||||
AlexaCapability::AlexaCapability(const AlexaInterfaceDesc &row, const char *instance)
|
AlexaCapability::AlexaCapability(const AlexaInterfaceDesc &row, const char *instance)
|
||||||
: row(&row), retrievable(true), proactivelyReported(false), nonControllable(false)
|
: row(&row), retrievable(true), proactivelyReported(false), nonControllable(false), valid(true)
|
||||||
{
|
{
|
||||||
setInstance(instance);
|
setInstance(instance);
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +105,12 @@ AlexaCapability::~AlexaCapability()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AlexaCapability &AlexaCapability::refused()
|
||||||
|
{
|
||||||
|
valid = false;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
AlexaCapability &AlexaCapability::setInstance(const char *newInstance)
|
AlexaCapability &AlexaCapability::setInstance(const char *newInstance)
|
||||||
{
|
{
|
||||||
free(instance);
|
free(instance);
|
||||||
|
|
@ -101,6 +119,7 @@ AlexaCapability &AlexaCapability::setInstance(const char *newInstance)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: instance not set: the heap has no room for it",
|
ALEX2ESP_LOGE("%s: instance not set: the heap has no room for it",
|
||||||
AlexaCapabilityName(*row, nullptr).text);
|
AlexaCapabilityName(*row, nullptr).text);
|
||||||
|
return refused();
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
@ -112,19 +131,19 @@ AlexaCapability &AlexaCapability::addFriendlyName(const char *text, const char *
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: friendly name not added: pass a text and a locale, addFriendlyName(\"Lift\", \"en-US\")",
|
ALEX2ESP_LOGE("%s: friendly name not added: pass a text and a locale, addFriendlyName(\"Lift\", \"en-US\")",
|
||||||
name.text);
|
name.text);
|
||||||
return *this;
|
return refused();
|
||||||
}
|
}
|
||||||
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
|
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: friendly name \"%s\" not added: it has %d already (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);
|
name.text, text, ALEX2ESP_MAX_FRIENDLY_NAMES);
|
||||||
return *this;
|
return refused();
|
||||||
}
|
}
|
||||||
const char *copy = copyText(text);
|
const char *copy = copyText(text);
|
||||||
if (copy == nullptr)
|
if (copy == nullptr)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: friendly name \"%s\" not added: the heap has no room for it", name.text, text);
|
ALEX2ESP_LOGE("%s: friendly name \"%s\" not added: the heap has no room for it", name.text, text);
|
||||||
return *this;
|
return refused();
|
||||||
}
|
}
|
||||||
names[nameCount++] = {copy, locale};
|
names[nameCount++] = {copy, locale};
|
||||||
return *this;
|
return *this;
|
||||||
|
|
@ -135,14 +154,14 @@ AlexaCapability &AlexaCapability::addFriendlyAsset(PGM_P assetId)
|
||||||
const AlexaCapabilityName name(*row, instance);
|
const AlexaCapabilityName name(*row, instance);
|
||||||
if (assetId == nullptr)
|
if (assetId == nullptr)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: asset not added: pass its id, addFriendlyAsset(PSTR(\"Alexa.Setting.Opening\"))", name.text);
|
ALEX2ESP_LOGE("%s: asset not added: pass its id, addFriendlyAsset(AlexaAssets::Setting_Opening)", name.text);
|
||||||
return *this;
|
return refused();
|
||||||
}
|
}
|
||||||
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
|
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: asset not added: it has %d names already (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);
|
name.text, ALEX2ESP_MAX_FRIENDLY_NAMES);
|
||||||
return *this;
|
return refused();
|
||||||
}
|
}
|
||||||
names[nameCount++] = {assetId, nullptr};
|
names[nameCount++] = {assetId, nullptr};
|
||||||
return *this;
|
return *this;
|
||||||
|
|
@ -180,19 +199,19 @@ AlexaCapability &AlexaCapability::addActionMapping(const ActionMapping &mapping)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: action mapping not added: only RangeController, ModeController and ToggleController take one",
|
ALEX2ESP_LOGE("%s: action mapping not added: only RangeController, ModeController and ToggleController take one",
|
||||||
name.text);
|
name.text);
|
||||||
return *this;
|
return refused();
|
||||||
}
|
}
|
||||||
if (mapping.actions == 0 || mapping.directiveName == nullptr || mapping.directiveName[0] == '\0')
|
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\")",
|
ALEX2ESP_LOGE("%s: action mapping not added: it needs an action and a directive, ActionMapping({AlexaAction::Open}, \"TurnOn\")",
|
||||||
name.text);
|
name.text);
|
||||||
return *this;
|
return refused();
|
||||||
}
|
}
|
||||||
if (actionCount >= ALEX2ESP_MAX_ACTION_MAPPINGS)
|
if (actionCount >= ALEX2ESP_MAX_ACTION_MAPPINGS)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: action mapping to %s not added: it has %d already (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);
|
name.text, mapping.directiveName, ALEX2ESP_MAX_ACTION_MAPPINGS);
|
||||||
return *this;
|
return refused();
|
||||||
}
|
}
|
||||||
actionMappings[actionCount++] = mapping;
|
actionMappings[actionCount++] = mapping;
|
||||||
return *this;
|
return *this;
|
||||||
|
|
@ -206,25 +225,28 @@ AlexaCapability::StateMapping *AlexaCapability::newStateMapping(std::initializer
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: state mapping not added: only RangeController, ModeController and ToggleController take one",
|
ALEX2ESP_LOGE("%s: state mapping not added: only RangeController, ModeController and ToggleController take one",
|
||||||
name.text);
|
name.text);
|
||||||
|
valid = false;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
if (states.size() == 0)
|
if (states.size() == 0)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: state mapping not added: it needs a state, addStateMapping({AlexaState::Closed}, 0.0f)",
|
ALEX2ESP_LOGE("%s: state mapping not added: it needs a state, addStateMapping({AlexaState::Closed}, 0)",
|
||||||
name.text);
|
name.text);
|
||||||
|
valid = false;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
if (stateCount >= ALEX2ESP_MAX_STATE_MAPPINGS)
|
if (stateCount >= ALEX2ESP_MAX_STATE_MAPPINGS)
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: state mapping not added: it has %d already (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);
|
name.text, ALEX2ESP_MAX_STATE_MAPPINGS);
|
||||||
|
valid = false;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
StateMapping *mapping = &stateMappings[stateCount++];
|
StateMapping *mapping = &stateMappings[stateCount++];
|
||||||
mapping->states = 0;
|
mapping->states = 0;
|
||||||
for (AlexaState state : states)
|
for (AlexaState state : states)
|
||||||
{
|
{
|
||||||
mapping->states |= static_cast<uint8_t>(1u << static_cast<uint8_t>(state));
|
mapping->states |= static_cast<uint16_t>(1u << static_cast<uint8_t>(state));
|
||||||
}
|
}
|
||||||
mapping->kind = kind;
|
mapping->kind = kind;
|
||||||
mapping->maximum = 0;
|
mapping->maximum = 0;
|
||||||
|
|
@ -258,7 +280,7 @@ AlexaCapability &AlexaCapability::addStateMapping(std::initializer_list<AlexaSta
|
||||||
{
|
{
|
||||||
ALEX2ESP_LOGE("%s: state mapping not added: it needs a value, addStateMapping({AlexaState::Closed}, \"OFF\")",
|
ALEX2ESP_LOGE("%s: state mapping not added: it needs a value, addStateMapping({AlexaState::Closed}, \"OFF\")",
|
||||||
AlexaCapabilityName(*row, instance).text);
|
AlexaCapabilityName(*row, instance).text);
|
||||||
return *this;
|
return refused();
|
||||||
}
|
}
|
||||||
StateMapping *mapping = newStateMapping(states, STATES_TO_TEXT);
|
StateMapping *mapping = newStateMapping(states, STATES_TO_TEXT);
|
||||||
if (mapping != nullptr)
|
if (mapping != nullptr)
|
||||||
|
|
@ -285,6 +307,12 @@ bool AlexaCapability::matches(const char *ns, const char *directiveInstance) con
|
||||||
// of a capability that has them. A plain literal takes static RAM in every sketch.
|
// of a capability that has them. A plain literal takes static RAM in every sketch.
|
||||||
bool AlexaCapability::toJson(JsonArray capabilities, const char *endpointId) const
|
bool AlexaCapability::toJson(JsonArray capabilities, const char *endpointId) const
|
||||||
{
|
{
|
||||||
|
if (!valid)
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: %s left out of discovery: a call that set it up was refused, correct what the error of that call names",
|
||||||
|
endpointId, AlexaCapabilityName(*row, instance).text);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (row->has(AIF_INSTANCED) && (instance == nullptr || nameCount == 0))
|
if (row->has(AIF_INSTANCED) && (instance == nullptr || nameCount == 0))
|
||||||
{
|
{
|
||||||
const AlexaCapabilityName name(*row, instance);
|
const AlexaCapabilityName name(*row, instance);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
#include <type_traits>
|
||||||
#include "AlexaInterfaces.h"
|
#include "AlexaInterfaces.h"
|
||||||
#include "AlexaLimits.h"
|
#include "AlexaLimits.h"
|
||||||
|
|
||||||
|
|
@ -23,11 +24,19 @@ enum class AlexaAction : uint8_t
|
||||||
SetEcoOff
|
SetEcoOff
|
||||||
};
|
};
|
||||||
|
|
||||||
// What Alexa says about the device: "the blind is open" (alexa-discovery-objects.html#state-mapping)
|
// What Alexa says about the device: "the blind is open" (alexa-discovery-objects.html#state-mapping). EcoOn and
|
||||||
|
// EcoOff are for a ToggleController; Low, Empty, Full, Done and Stuck are what Alexa announces a device for.
|
||||||
enum class AlexaState : uint8_t
|
enum class AlexaState : uint8_t
|
||||||
{
|
{
|
||||||
Open,
|
Open,
|
||||||
Closed
|
Closed,
|
||||||
|
EcoOn,
|
||||||
|
EcoOff,
|
||||||
|
Low,
|
||||||
|
Empty,
|
||||||
|
Full,
|
||||||
|
Done,
|
||||||
|
Stuck
|
||||||
};
|
};
|
||||||
|
|
||||||
// "Alexa.Actions.Open", in program memory
|
// "Alexa.Actions.Open", in program memory
|
||||||
|
|
@ -74,7 +83,9 @@ struct AlexaCapabilityName
|
||||||
// 116 bytes on the heap of an ESP8266, whatever the capability holds. Every setter returns the capability:
|
// 116 bytes on the heap of an ESP8266, whatever the capability holds. Every setter returns the capability:
|
||||||
// device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
|
// device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
|
||||||
// ->addFriendlyName("Lift", "en-US").setConfiguration(fillLift).addStateMapping({AlexaState::Closed}, 0.0f);
|
// ->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.
|
// What a capability refuses is printed at ERROR with what to change, and leaves the capability as it was. The
|
||||||
|
// chain goes on after a call that was refused; isValid() is false from then on, and the device leaves the
|
||||||
|
// capability out of discovery, where Alexa would otherwise learn half of it.
|
||||||
class AlexaCapability
|
class AlexaCapability
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
@ -97,6 +108,9 @@ public:
|
||||||
bool isProactivelyReported() const { return proactivelyReported; }
|
bool isProactivelyReported() const { return proactivelyReported; }
|
||||||
bool isNonControllable() const { return nonControllable; }
|
bool isNonControllable() const { return nonControllable; }
|
||||||
|
|
||||||
|
// false when a setter has refused what it was given. The reason was printed when it happened.
|
||||||
|
bool isValid() const { return valid; }
|
||||||
|
|
||||||
// The instance tells the capabilities of one interface on a device apart ("Blind.Lift", "Blind.Tilt"). It is
|
// 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.
|
// copied. addCapability(row, instance) sets it; this is for a capability that was added by its type.
|
||||||
AlexaCapability &setInstance(const char *instance);
|
AlexaCapability &setInstance(const char *instance);
|
||||||
|
|
@ -104,7 +118,8 @@ public:
|
||||||
// A name of the capability in words. The text is copied, the locale ("en-US") is not: pass a literal.
|
// 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);
|
AlexaCapability &addFriendlyName(const char *text, const char *locale);
|
||||||
|
|
||||||
// A name from the catalog of Alexa, which Alexa translates: PSTR("Alexa.Setting.Opening"). Not copied.
|
// A name from the catalog of Alexa, which Alexa translates: AlexaAssets::Setting_Opening of AlexaResources.h,
|
||||||
|
// or PSTR("Alexa.Setting.Opening"). Not copied.
|
||||||
AlexaCapability &addFriendlyAsset(PGM_P assetId);
|
AlexaCapability &addFriendlyAsset(PGM_P assetId);
|
||||||
|
|
||||||
AlexaCapability &setRetrievable(bool value);
|
AlexaCapability &setRetrievable(bool value);
|
||||||
|
|
@ -124,13 +139,21 @@ public:
|
||||||
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, float minimum, float maximum);
|
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, float minimum, float maximum);
|
||||||
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, const char *value);
|
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, const char *value);
|
||||||
|
|
||||||
|
// A number of another type, addStateMapping({AlexaState::Closed}, 0): without this a 0 is as much a text
|
||||||
|
// that is missing as it is a number, and the call does not compile
|
||||||
|
template <typename Number, typename = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
|
||||||
|
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, Number value)
|
||||||
|
{
|
||||||
|
return addStateMapping(states, static_cast<float>(value));
|
||||||
|
}
|
||||||
|
|
||||||
// Whether a directive with this namespace and instance is for this capability. The instance counts only
|
// Whether a directive with this namespace and instance is for this capability. The instance counts only
|
||||||
// for an interface that has instances.
|
// for an interface that has instances.
|
||||||
bool matches(const char *ns, const char *directiveInstance) const;
|
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
|
// 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,
|
// would reject the endpoint for, a generic controller without an instance or without a name, adds nothing,
|
||||||
// prints what is missing and returns false.
|
// prints what is missing and returns false. So does one that is not valid.
|
||||||
bool toJson(JsonArray capabilities, const char *endpointId) const;
|
bool toJson(JsonArray capabilities, const char *endpointId) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
@ -149,7 +172,7 @@ private:
|
||||||
|
|
||||||
struct StateMapping
|
struct StateMapping
|
||||||
{
|
{
|
||||||
uint8_t states; // one bit per AlexaState
|
uint16_t states; // one bit per AlexaState
|
||||||
uint8_t kind; // StateMappingKind
|
uint8_t kind; // StateMappingKind
|
||||||
union
|
union
|
||||||
{
|
{
|
||||||
|
|
@ -160,6 +183,7 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
StateMapping *newStateMapping(std::initializer_list<AlexaState> states, StateMappingKind kind);
|
StateMapping *newStateMapping(std::initializer_list<AlexaState> states, StateMappingKind kind);
|
||||||
|
AlexaCapability &refused();
|
||||||
void writeSemantics(JsonObject capability) const;
|
void writeSemantics(JsonObject capability) const;
|
||||||
void writeNames(JsonObject capability) const;
|
void writeNames(JsonObject capability) const;
|
||||||
|
|
||||||
|
|
@ -176,6 +200,7 @@ private:
|
||||||
bool retrievable : 1;
|
bool retrievable : 1;
|
||||||
bool proactivelyReported : 1;
|
bool proactivelyReported : 1;
|
||||||
bool nonControllable : 1;
|
bool nonControllable : 1;
|
||||||
|
bool valid : 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // ALEXA_CAPABILITY_H
|
#endif // ALEXA_CAPABILITY_H
|
||||||
|
|
|
||||||
141
src/AlexaResources.h
Normal file
141
src/AlexaResources.h
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
// The names Alexa has in its own catalog, which it translates into the language of the user: the assets of
|
||||||
|
// resources-and-assets.html under https://developer.amazon.com/en-US/docs/alexa/device-apis/
|
||||||
|
//
|
||||||
|
// capability->addFriendlyAsset(AlexaAssets::Setting_Opening);
|
||||||
|
// configuration["unitOfMeasure"] = FPSTR(AlexaUnits::Percent);
|
||||||
|
// labelOfAMode["value"]["assetId"] = FPSTR(AlexaAssets::Value_Open);
|
||||||
|
//
|
||||||
|
// Every id is a string in program memory that belongs to the file which includes this one: an id that the sketch
|
||||||
|
// does not name takes no room in it. The words a user says and the states Alexa names, "Alexa.Actions.Open" and
|
||||||
|
// "Alexa.States.Closed", are AlexaAction and AlexaState in AlexaCapability.h.
|
||||||
|
#ifndef ALEXA_RESOURCES_H
|
||||||
|
#define ALEXA_RESOURCES_H
|
||||||
|
|
||||||
|
#include "AlexaCompat.h"
|
||||||
|
|
||||||
|
#define ALEXA_RESOURCE(name, id) static const char name[] PROGMEM = id;
|
||||||
|
|
||||||
|
namespace AlexaAssets
|
||||||
|
{
|
||||||
|
// What a device does, as the name of a mode or a preset. Not the actions of an action mapping.
|
||||||
|
ALEXA_RESOURCE(Actions_Charge, "Alexa.Actions.Charge")
|
||||||
|
ALEXA_RESOURCE(Actions_Clean, "Alexa.Actions.Clean")
|
||||||
|
ALEXA_RESOURCE(Actions_Dispense, "Alexa.Actions.Dispense")
|
||||||
|
ALEXA_RESOURCE(Actions_Dock, "Alexa.Actions.Dock")
|
||||||
|
ALEXA_RESOURCE(Actions_Empty, "Alexa.Actions.Empty")
|
||||||
|
ALEXA_RESOURCE(Actions_Mop, "Alexa.Actions.Mop")
|
||||||
|
ALEXA_RESOURCE(Actions_Skip, "Alexa.Actions.Skip")
|
||||||
|
ALEXA_RESOURCE(Actions_Sweep, "Alexa.Actions.Sweep")
|
||||||
|
ALEXA_RESOURCE(Actions_Vacuum, "Alexa.Actions.Vacuum")
|
||||||
|
|
||||||
|
// The buttons of a remote and how they are pressed, for Alexa.SimpleEventSource
|
||||||
|
ALEXA_RESOURCE(Button_OffButton, "Alexa.Button.OffButton")
|
||||||
|
ALEXA_RESOURCE(Button_OnButton, "Alexa.Button.OnButton")
|
||||||
|
ALEXA_RESOURCE(Button_BrightenButton, "Alexa.Button.BrightenButton")
|
||||||
|
ALEXA_RESOURCE(Button_DimButton, "Alexa.Button.DimButton")
|
||||||
|
ALEXA_RESOURCE(Button_MainButton, "Alexa.Button.MainButton")
|
||||||
|
ALEXA_RESOURCE(Button_TopButton, "Alexa.Button.TopButton")
|
||||||
|
ALEXA_RESOURCE(Button_BottomButton, "Alexa.Button.BottomButton")
|
||||||
|
ALEXA_RESOURCE(Button_CenterButton, "Alexa.Button.CenterButton")
|
||||||
|
ALEXA_RESOURCE(Button_MiddleButton, "Alexa.Button.MiddleButton")
|
||||||
|
ALEXA_RESOURCE(Button_UpButton, "Alexa.Button.UpButton")
|
||||||
|
ALEXA_RESOURCE(Button_DownButton, "Alexa.Button.DownButton")
|
||||||
|
ALEXA_RESOURCE(Button_LeftButton, "Alexa.Button.LeftButton")
|
||||||
|
ALEXA_RESOURCE(Button_RightButton, "Alexa.Button.RightButton")
|
||||||
|
ALEXA_RESOURCE(Button_FirstButton, "Alexa.Button.FirstButton")
|
||||||
|
ALEXA_RESOURCE(Button_SecondButton, "Alexa.Button.SecondButton")
|
||||||
|
ALEXA_RESOURCE(Button_ThirdButton, "Alexa.Button.ThirdButton")
|
||||||
|
ALEXA_RESOURCE(Button_FourthButton, "Alexa.Button.FourthButton")
|
||||||
|
ALEXA_RESOURCE(Button_FifthButton, "Alexa.Button.FifthButton")
|
||||||
|
ALEXA_RESOURCE(Button_SixthButton, "Alexa.Button.SixthButton")
|
||||||
|
ALEXA_RESOURCE(Button_SeventhButton, "Alexa.Button.SeventhButton")
|
||||||
|
ALEXA_RESOURCE(Button_EighthButton, "Alexa.Button.EighthButton")
|
||||||
|
ALEXA_RESOURCE(Button_SinglePress, "Alexa.Button.SinglePress")
|
||||||
|
ALEXA_RESOURCE(Button_SinglePush, "Alexa.Button.SinglePush")
|
||||||
|
ALEXA_RESOURCE(Button_DoublePress, "Alexa.Button.DoublePress")
|
||||||
|
ALEXA_RESOURCE(Button_DoublePush, "Alexa.Button.DoublePush")
|
||||||
|
ALEXA_RESOURCE(Button_LongPress, "Alexa.Button.LongPress")
|
||||||
|
ALEXA_RESOURCE(Button_LongPush, "Alexa.Button.LongPush")
|
||||||
|
|
||||||
|
ALEXA_RESOURCE(DeviceName_AirPurifier, "Alexa.DeviceName.AirPurifier")
|
||||||
|
ALEXA_RESOURCE(DeviceName_Camera, "Alexa.DeviceName.Camera")
|
||||||
|
ALEXA_RESOURCE(DeviceName_Fan, "Alexa.DeviceName.Fan")
|
||||||
|
ALEXA_RESOURCE(DeviceName_Router, "Alexa.DeviceName.Router")
|
||||||
|
ALEXA_RESOURCE(DeviceName_Shade, "Alexa.DeviceName.Shade")
|
||||||
|
ALEXA_RESOURCE(DeviceName_Shower, "Alexa.DeviceName.Shower")
|
||||||
|
ALEXA_RESOURCE(DeviceName_SpaceHeater, "Alexa.DeviceName.SpaceHeater")
|
||||||
|
ALEXA_RESOURCE(DeviceName_Washer, "Alexa.DeviceName.Washer")
|
||||||
|
|
||||||
|
ALEXA_RESOURCE(Gesture_SingleClick, "Alexa.Gesture.SingleClick")
|
||||||
|
ALEXA_RESOURCE(Gesture_DoubleClick, "Alexa.Gesture.DoubleClick")
|
||||||
|
ALEXA_RESOURCE(Gesture_Tap, "Alexa.Gesture.Tap")
|
||||||
|
ALEXA_RESOURCE(Gesture_DoubleTap, "Alexa.Gestures.DoubleTap") // "Gestures": the id as the page has it
|
||||||
|
ALEXA_RESOURCE(Gesture_SwipeUp, "Alexa.Gesture.SwipeUp")
|
||||||
|
ALEXA_RESOURCE(Gesture_SwipeDown, "Alexa.Gesture.SwipeDown")
|
||||||
|
ALEXA_RESOURCE(Gesture_SwipeLeft, "Alexa.Gesture.SwipeLeft")
|
||||||
|
ALEXA_RESOURCE(Gesture_SwipeRight, "Alexa.Gesture.SwipeRight")
|
||||||
|
|
||||||
|
ALEXA_RESOURCE(Setting_2GGuestWiFi, "Alexa.Setting.2GGuestWiFi")
|
||||||
|
ALEXA_RESOURCE(Setting_5GGuestWiFi, "Alexa.Setting.5GGuestWiFi")
|
||||||
|
ALEXA_RESOURCE(Setting_Auto, "Alexa.Setting.Auto")
|
||||||
|
ALEXA_RESOURCE(Setting_Direction, "Alexa.Setting.Direction")
|
||||||
|
ALEXA_RESOURCE(Setting_DryCycle, "Alexa.Setting.DryCycle")
|
||||||
|
ALEXA_RESOURCE(Setting_FanSpeed, "Alexa.Setting.FanSpeed")
|
||||||
|
ALEXA_RESOURCE(Setting_GuestWiFi, "Alexa.Setting.GuestWiFi")
|
||||||
|
ALEXA_RESOURCE(Setting_Heat, "Alexa.Setting.Heat")
|
||||||
|
ALEXA_RESOURCE(Setting_Mode, "Alexa.Setting.Mode")
|
||||||
|
ALEXA_RESOURCE(Setting_Night, "Alexa.Setting.Night")
|
||||||
|
ALEXA_RESOURCE(Setting_Opening, "Alexa.Setting.Opening")
|
||||||
|
ALEXA_RESOURCE(Setting_Oscillate, "Alexa.Setting.Oscillate")
|
||||||
|
ALEXA_RESOURCE(Setting_Preset, "Alexa.Setting.Preset")
|
||||||
|
ALEXA_RESOURCE(Setting_Quiet, "Alexa.Setting.Quiet")
|
||||||
|
ALEXA_RESOURCE(Setting_Temperature, "Alexa.Setting.Temperature")
|
||||||
|
ALEXA_RESOURCE(Setting_WashCycle, "Alexa.Setting.WashCycle")
|
||||||
|
ALEXA_RESOURCE(Setting_WaterTemperature, "Alexa.Setting.WaterTemperature")
|
||||||
|
|
||||||
|
ALEXA_RESOURCE(Shower_HandHeld, "Alexa.Shower.HandHeld")
|
||||||
|
ALEXA_RESOURCE(Shower_RainHead, "Alexa.Shower.RainHead")
|
||||||
|
|
||||||
|
// The names of modes and presets
|
||||||
|
ALEXA_RESOURCE(Value_Open, "Alexa.Value.Open")
|
||||||
|
ALEXA_RESOURCE(Value_Close, "Alexa.Value.Close")
|
||||||
|
ALEXA_RESOURCE(Value_Minimum, "Alexa.Value.Minimum")
|
||||||
|
ALEXA_RESOURCE(Value_Low, "Alexa.Value.Low")
|
||||||
|
ALEXA_RESOURCE(Value_Medium, "Alexa.Value.Medium")
|
||||||
|
ALEXA_RESOURCE(Value_High, "Alexa.Value.High")
|
||||||
|
ALEXA_RESOURCE(Value_Maximum, "Alexa.Value.Maximum")
|
||||||
|
ALEXA_RESOURCE(Value_Delicate, "Alexa.Value.Delicate")
|
||||||
|
ALEXA_RESOURCE(Value_QuickWash, "Alexa.Value.QuickWash")
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the value of a range is measured in: "unitOfMeasure" in the configuration of Alexa.RangeController
|
||||||
|
namespace AlexaUnits
|
||||||
|
{
|
||||||
|
ALEXA_RESOURCE(Angle_Degrees, "Alexa.Unit.Angle.Degrees")
|
||||||
|
ALEXA_RESOURCE(Angle_Radians, "Alexa.Unit.Angle.Radians")
|
||||||
|
ALEXA_RESOURCE(Distance_Feet, "Alexa.Unit.Distance.Feet")
|
||||||
|
ALEXA_RESOURCE(Distance_Inches, "Alexa.Unit.Distance.Inches")
|
||||||
|
ALEXA_RESOURCE(Distance_Kilometers, "Alexa.Unit.Distance.Kilometers")
|
||||||
|
ALEXA_RESOURCE(Distance_Meters, "Alexa.Unit.Distance.Meters")
|
||||||
|
ALEXA_RESOURCE(Distance_Miles, "Alexa.Unit.Distance.Miles")
|
||||||
|
ALEXA_RESOURCE(Distance_Yards, "Alexa.Unit.Distance.Yards")
|
||||||
|
ALEXA_RESOURCE(Mass_Grams, "Alexa.Unit.Mass.Grams")
|
||||||
|
ALEXA_RESOURCE(Mass_Kilograms, "Alexa.Unit.Mass.Kilograms")
|
||||||
|
ALEXA_RESOURCE(Percent, "Alexa.Unit.Percent")
|
||||||
|
ALEXA_RESOURCE(Temperature_Celsius, "Alexa.Unit.Temperature.Celsius")
|
||||||
|
ALEXA_RESOURCE(Temperature_Degrees, "Alexa.Unit.Temperature.Degrees")
|
||||||
|
ALEXA_RESOURCE(Temperature_Fahrenheit, "Alexa.Unit.Temperature.Fahrenheit")
|
||||||
|
ALEXA_RESOURCE(Temperature_Kelvin, "Alexa.Unit.Temperature.Kelvin")
|
||||||
|
ALEXA_RESOURCE(Volume_CubicFeet, "Alexa.Unit.Volume.CubicFeet")
|
||||||
|
ALEXA_RESOURCE(Volume_CubicMeters, "Alexa.Unit.Volume.CubicMeters")
|
||||||
|
ALEXA_RESOURCE(Volume_Gallons, "Alexa.Unit.Volume.Gallons")
|
||||||
|
ALEXA_RESOURCE(Volume_Liters, "Alexa.Unit.Volume.Liters")
|
||||||
|
ALEXA_RESOURCE(Volume_Pints, "Alexa.Unit.Volume.Pints")
|
||||||
|
ALEXA_RESOURCE(Volume_Quarts, "Alexa.Unit.Volume.Quarts")
|
||||||
|
ALEXA_RESOURCE(Weight_Ounces, "Alexa.Unit.Weight.Ounces")
|
||||||
|
ALEXA_RESOURCE(Weight_Pounds, "Alexa.Unit.Weight.Pounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef ALEXA_RESOURCE
|
||||||
|
|
||||||
|
#endif // ALEXA_RESOURCES_H
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "AlexaStatusMessage.h"
|
#include "AlexaStatusMessage.h"
|
||||||
#include "AlexaBridgeLogic.h"
|
#include "AlexaBridgeLogic.h"
|
||||||
|
#include "AlexaCapability.h"
|
||||||
#include "AlexaLog.h"
|
#include "AlexaLog.h"
|
||||||
|
|
||||||
// What a 1.x sketch wrote where the time belongs when it built a property by hand: the backend's HTTP route
|
// What a 1.x sketch wrote where the time belongs when it built a property by hand: the backend's HTTP route
|
||||||
|
|
@ -82,9 +83,69 @@ JsonObject AlexaStatusMessage::payload()
|
||||||
return doc["event"]["payload"].as<JsonObject>();
|
return doc["event"]["payload"].as<JsonObject>();
|
||||||
}
|
}
|
||||||
|
|
||||||
AlexaStatusMessage &AlexaStatusMessage::addTemperatureSensorProp(float value, TemperatureSensorScale scale, unsigned int uncertaintyInMs)
|
JsonVariant AlexaStatusMessage::newProperty(const AlexaInterfaceDesc &row, uint8_t place, unsigned int uncertaintyInMs, const String &instance)
|
||||||
{
|
{
|
||||||
JsonDocument temperature;
|
JsonObject prop = properties().add<JsonObject>();
|
||||||
|
prop["namespace"] = FPSTR(row.ns);
|
||||||
|
prop["name"] = FPSTR(row.property(place));
|
||||||
|
if (!instance.isEmpty())
|
||||||
|
{
|
||||||
|
prop["instance"] = instance;
|
||||||
|
}
|
||||||
|
// Its place is between the name and the time, where the pages of Alexa show it
|
||||||
|
JsonVariant value = prop["value"].to<JsonVariant>();
|
||||||
|
setTimeOfSample(prop);
|
||||||
|
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AlexaStatusMessage::withinRange(int value, int lowest, int highest, const AlexaInterfaceDesc &row) const
|
||||||
|
{
|
||||||
|
if (value >= lowest && value <= highest)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return nearestOf(value < lowest, lowest, highest, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
float AlexaStatusMessage::withinRange(float value, int lowest, int highest, const AlexaInterfaceDesc &row) const
|
||||||
|
{
|
||||||
|
if (value >= lowest && value <= highest)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return nearestOf(value < lowest, lowest, highest, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
int AlexaStatusMessage::nearestOf(bool below, int lowest, int highest, const AlexaInterfaceDesc &row) const
|
||||||
|
{
|
||||||
|
const int nearest = below ? lowest : highest;
|
||||||
|
ALEX2ESP_LOGE("%s: %s reported as %d: the value was outside of %d to %d, the range Alexa gives for it",
|
||||||
|
endpointId.c_str(), AlexaCapabilityName(row, nullptr).text, nearest, lowest, highest);
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addSwitchProp(const AlexaInterfaceDesc &row, PowerController state, unsigned int uncertaintyInMs, const String &instance)
|
||||||
|
{
|
||||||
|
newProperty(row, 0, uncertaintyInMs, instance).set(state == PowerController::ON ? F("ON") : F("OFF"));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addNumberProp(const AlexaInterfaceDesc &row, int value, int lowest, int highest, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
newProperty(row, 0, uncertaintyInMs).set(withinRange(value, lowest, highest, row));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addDetectionProp(const AlexaInterfaceDesc &row, bool detected, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
newProperty(row, 0, uncertaintyInMs).set(detected ? F("DETECTED") : F("NOT_DETECTED"));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addTemperatureProp(const AlexaInterfaceDesc &row, uint8_t place, float value, TemperatureSensorScale scale, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
JsonObject temperature = newProperty(row, place, uncertaintyInMs).to<JsonObject>();
|
||||||
temperature["value"] = value;
|
temperature["value"] = value;
|
||||||
if (scale == TemperatureSensorScale::FAHRENHEIT)
|
if (scale == TemperatureSensorScale::FAHRENHEIT)
|
||||||
{
|
{
|
||||||
|
|
@ -98,7 +159,170 @@ AlexaStatusMessage &AlexaStatusMessage::addTemperatureSensorProp(float value, Te
|
||||||
{
|
{
|
||||||
temperature["scale"] = F("CELSIUS");
|
temperature["scale"] = F("CELSIUS");
|
||||||
}
|
}
|
||||||
return AddProperty(AlexaInterfaces::TemperatureSensor, temperature.as<JsonObject>(), uncertaintyInMs);
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
JsonObject connectivity = newProperty(AlexaInterfaces::EndpointHealth, 0, uncertaintyInMs).to<JsonObject>();
|
||||||
|
connectivity["value"] = endpointHealth == EndpointHealth::OK ? F("OK") : F("UNREACHABLE");
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addSwitchProp(AlexaInterfaces::PowerController, powerController, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addBrightnessControllerProp(int brightness, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addNumberProp(AlexaInterfaces::BrightnessController, brightness, 0, 100, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addPercentageControllerProp(int percentage, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addNumberProp(AlexaInterfaces::PercentageController, percentage, 0, 100, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addPowerLevelControllerProp(int powerLevel, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addNumberProp(AlexaInterfaces::PowerLevelController, powerLevel, 0, 100, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addColorTemperatureControllerProp(int colorTemperature, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addNumberProp(AlexaInterfaces::ColorTemperatureController, colorTemperature, 1000, 10000, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addColorControllerProp(float hue, float saturation, float brightness, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
const AlexaInterfaceDesc &row = AlexaInterfaces::ColorController;
|
||||||
|
JsonObject color = newProperty(row, 0, uncertaintyInMs).to<JsonObject>();
|
||||||
|
color[F("hue")] = withinRange(hue, 0, 360, row);
|
||||||
|
color[F("saturation")] = withinRange(saturation, 0, 1, row);
|
||||||
|
color[F("brightness")] = withinRange(brightness, 0, 1, row);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addRangeControllerProp(const String &instance, float value, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
newProperty(AlexaInterfaces::RangeController, 0, uncertaintyInMs, instance).set(value);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addModeControllerProp(const String &instance, const String &mode, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
newProperty(AlexaInterfaces::ModeController, 0, uncertaintyInMs, instance).set(mode);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addToggleControllerProp(const String &instance, PowerController toggleState, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addSwitchProp(AlexaInterfaces::ToggleController, toggleState, uncertaintyInMs, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addLockControllerProp(AlexaLockState lockState, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
JsonVariant value = newProperty(AlexaInterfaces::LockController, 0, uncertaintyInMs);
|
||||||
|
switch (lockState)
|
||||||
|
{
|
||||||
|
case AlexaLockState::LOCKED:
|
||||||
|
value.set(F("LOCKED"));
|
||||||
|
break;
|
||||||
|
case AlexaLockState::UNLOCKED:
|
||||||
|
value.set(F("UNLOCKED"));
|
||||||
|
break;
|
||||||
|
case AlexaLockState::JAMMED:
|
||||||
|
value.set(F("JAMMED"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addContactSensorProp(bool detected, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addDetectionProp(AlexaInterfaces::ContactSensor, detected, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addMotionSensorProp(bool detected, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addDetectionProp(AlexaInterfaces::MotionSensor, detected, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addTemperatureSensorProp(float value, TemperatureSensorScale scale, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addTemperatureProp(AlexaInterfaces::TemperatureSensor, 0, value, scale, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addHumiditySensorProp(float humidity, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
const AlexaInterfaceDesc &row = AlexaInterfaces::HumiditySensor;
|
||||||
|
JsonObject relativeHumidity = newProperty(row, 0, uncertaintyInMs).to<JsonObject>();
|
||||||
|
relativeHumidity["value"] = withinRange(humidity, 0, 100, row);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The places of the properties in the row of the thermostat (src/AlexaInterfaces.cpp)
|
||||||
|
enum ThermostatProperty : uint8_t
|
||||||
|
{
|
||||||
|
THERMOSTAT_TARGET_SETPOINT,
|
||||||
|
THERMOSTAT_LOWER_SETPOINT,
|
||||||
|
THERMOSTAT_UPPER_SETPOINT,
|
||||||
|
THERMOSTAT_MODE
|
||||||
|
};
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addThermostatSetpointProp(float value, TemperatureSensorScale scale, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addTemperatureProp(AlexaInterfaces::ThermostatController, THERMOSTAT_TARGET_SETPOINT, value, scale, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addThermostatLowerSetpointProp(float value, TemperatureSensorScale scale, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addTemperatureProp(AlexaInterfaces::ThermostatController, THERMOSTAT_LOWER_SETPOINT, value, scale, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addThermostatUpperSetpointProp(float value, TemperatureSensorScale scale, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
return addTemperatureProp(AlexaInterfaces::ThermostatController, THERMOSTAT_UPPER_SETPOINT, value, scale, uncertaintyInMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addThermostatModeProp(AlexaThermostatMode mode, unsigned int uncertaintyInMs)
|
||||||
|
{
|
||||||
|
JsonVariant value = newProperty(AlexaInterfaces::ThermostatController, THERMOSTAT_MODE, uncertaintyInMs);
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case AlexaThermostatMode::AUTO:
|
||||||
|
value.set(F("AUTO"));
|
||||||
|
break;
|
||||||
|
case AlexaThermostatMode::COOL:
|
||||||
|
value.set(F("COOL"));
|
||||||
|
break;
|
||||||
|
case AlexaThermostatMode::ECO:
|
||||||
|
value.set(F("ECO"));
|
||||||
|
break;
|
||||||
|
case AlexaThermostatMode::EM_HEAT:
|
||||||
|
value.set(F("EM_HEAT"));
|
||||||
|
break;
|
||||||
|
case AlexaThermostatMode::HEAT:
|
||||||
|
value.set(F("HEAT"));
|
||||||
|
break;
|
||||||
|
case AlexaThermostatMode::OFF:
|
||||||
|
value.set(F("OFF"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlexaStatusMessage &AlexaStatusMessage::addProperty(const AlexaInterfaceDesc &row, uint8_t place, JsonVariantConst value, unsigned int uncertaintyInMs, const String &instance)
|
||||||
|
{
|
||||||
|
if (place >= row.propertyCount())
|
||||||
|
{
|
||||||
|
ALEX2ESP_LOGE("%s: property %d of %s not added: the interface has %d, the first of them is 0",
|
||||||
|
endpointId.c_str(), place, AlexaCapabilityName(row, nullptr).text, row.propertyCount());
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
newProperty(row, place, uncertaintyInMs, instance).set(value);
|
||||||
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
AlexaStatusMessage &AlexaStatusMessage::addContextProp(JsonObjectConst property)
|
AlexaStatusMessage &AlexaStatusMessage::addContextProp(JsonObjectConst property)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,24 @@ enum class TemperatureSensorScale
|
||||||
KELVIN
|
KELVIN
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class AlexaLockState : uint8_t
|
||||||
|
{
|
||||||
|
LOCKED,
|
||||||
|
UNLOCKED,
|
||||||
|
JAMMED // the lock cannot reach either state
|
||||||
|
};
|
||||||
|
|
||||||
|
// The modes of alexa-thermostatcontroller.html. A thermostat announces the ones it has in its configuration.
|
||||||
|
enum class AlexaThermostatMode : uint8_t
|
||||||
|
{
|
||||||
|
AUTO,
|
||||||
|
COOL,
|
||||||
|
ECO,
|
||||||
|
EM_HEAT, // the emergency or auxiliary heat of a heat pump
|
||||||
|
HEAT,
|
||||||
|
OFF
|
||||||
|
};
|
||||||
|
|
||||||
enum class AlexaMessageKind : uint8_t
|
enum class AlexaMessageKind : uint8_t
|
||||||
{
|
{
|
||||||
RESPONSE, // the answer to a directive that was carried out
|
RESPONSE, // the answer to a directive that was carried out
|
||||||
|
|
@ -88,40 +106,63 @@ public:
|
||||||
|
|
||||||
AlexaMessageKind getKind() const { return kind; }
|
AlexaMessageKind getKind() const { return kind; }
|
||||||
|
|
||||||
// The properties of the device. In a ChangeReport the properties added before context() are those that
|
// The properties of the device, one function for each. In a ChangeReport the properties added before
|
||||||
// changed and the ones after it the others; every other kind has one list. An ErrorResponse, a
|
// context() are those that changed and the ones after it the others; every other kind has one list. An
|
||||||
// DeferredResponse and an event carry no properties: what is added to them is not sent.
|
// ErrorResponse, a DeferredResponse and an event carry no properties: what is added to them is not sent.
|
||||||
AlexaStatusMessage &addHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
|
//
|
||||||
{
|
// uncertaintyInMs is the time since the value was read from the hardware. A value outside of the range
|
||||||
JsonDocument healthValue;
|
// that Alexa gives for its property is reported as the nearest one of the range, and the log says so at
|
||||||
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
|
// ERROR. A sketch links the functions it calls and no others.
|
||||||
return AddProperty(AlexaInterfaces::EndpointHealth, healthValue.as<JsonObject>(), uncertaintyInMs);
|
AlexaStatusMessage &addHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0);
|
||||||
}
|
AlexaStatusMessage &addPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0);
|
||||||
|
|
||||||
AlexaStatusMessage &addPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
|
// 0 to 100
|
||||||
{
|
AlexaStatusMessage &addBrightnessControllerProp(int brightness, unsigned int uncertaintyInMs = 0);
|
||||||
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
|
AlexaStatusMessage &addPercentageControllerProp(int percentage, unsigned int uncertaintyInMs = 0);
|
||||||
return AddProperty(AlexaInterfaces::PowerController, value, uncertaintyInMs);
|
AlexaStatusMessage &addPowerLevelControllerProp(int powerLevel, unsigned int uncertaintyInMs = 0);
|
||||||
}
|
|
||||||
|
// 1000 to 10000 K
|
||||||
|
AlexaStatusMessage &addColorTemperatureControllerProp(int colorTemperature, unsigned int uncertaintyInMs = 0);
|
||||||
|
|
||||||
|
// Hue 0 to 360 degrees, saturation and brightness 0 to 1
|
||||||
|
AlexaStatusMessage &addColorControllerProp(float hue, float saturation, float brightness, unsigned int uncertaintyInMs = 0);
|
||||||
|
|
||||||
|
// The generic controllers, with the instance of the capability. The range of a value and the modes are what
|
||||||
|
// the configuration of the capability announced; the library does not check them.
|
||||||
|
AlexaStatusMessage &addRangeControllerProp(const String &instance, float value, unsigned int uncertaintyInMs = 0);
|
||||||
|
AlexaStatusMessage &addModeControllerProp(const String &instance, const String &mode, unsigned int uncertaintyInMs = 0);
|
||||||
|
AlexaStatusMessage &addToggleControllerProp(const String &instance, PowerController toggleState, unsigned int uncertaintyInMs = 0);
|
||||||
|
|
||||||
|
AlexaStatusMessage &addLockControllerProp(AlexaLockState lockState, unsigned int uncertaintyInMs = 0);
|
||||||
|
|
||||||
|
// true: the contact is open, there is motion
|
||||||
|
AlexaStatusMessage &addContactSensorProp(bool detected, unsigned int uncertaintyInMs = 0);
|
||||||
|
AlexaStatusMessage &addMotionSensorProp(bool detected, unsigned int uncertaintyInMs = 0);
|
||||||
|
|
||||||
// The temperature is reported in the scale it is given in
|
// The temperature is reported in the scale it is given in
|
||||||
AlexaStatusMessage &addTemperatureSensorProp(float value, TemperatureSensorScale scale = TemperatureSensorScale::CELSIUS, unsigned int uncertaintyInMs = 0);
|
AlexaStatusMessage &addTemperatureSensorProp(float value, TemperatureSensorScale scale = TemperatureSensorScale::CELSIUS, unsigned int uncertaintyInMs = 0);
|
||||||
|
|
||||||
AlexaStatusMessage &addBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
|
// Relative humidity, 0 to 100 percent
|
||||||
{
|
AlexaStatusMessage &addHumiditySensorProp(float humidity, unsigned int uncertaintyInMs = 0);
|
||||||
return AddProperty(AlexaInterfaces::BrightnessController, brightness, uncertaintyInMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
AlexaStatusMessage &addColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
|
// A thermostat with one setpoint reports targetSetpoint. One that heats below a temperature and cools above
|
||||||
|
// another reports lowerSetpoint and upperSetpoint, which addThermostatDualSetpointProp() adds together.
|
||||||
|
AlexaStatusMessage &addThermostatSetpointProp(float value, TemperatureSensorScale scale = TemperatureSensorScale::CELSIUS, unsigned int uncertaintyInMs = 0);
|
||||||
|
AlexaStatusMessage &addThermostatLowerSetpointProp(float value, TemperatureSensorScale scale = TemperatureSensorScale::CELSIUS, unsigned int uncertaintyInMs = 0);
|
||||||
|
AlexaStatusMessage &addThermostatUpperSetpointProp(float value, TemperatureSensorScale scale = TemperatureSensorScale::CELSIUS, unsigned int uncertaintyInMs = 0);
|
||||||
|
AlexaStatusMessage &addThermostatDualSetpointProp(float lower, float upper, TemperatureSensorScale scale = TemperatureSensorScale::CELSIUS, unsigned int uncertaintyInMs = 0)
|
||||||
{
|
{
|
||||||
return AddProperty(AlexaInterfaces::ColorTemperatureController, colorTemperature, uncertaintyInMs);
|
return addThermostatLowerSetpointProp(lower, scale, uncertaintyInMs).addThermostatUpperSetpointProp(upper, scale, uncertaintyInMs);
|
||||||
}
|
}
|
||||||
|
AlexaStatusMessage &addThermostatModeProp(AlexaThermostatMode mode, unsigned int uncertaintyInMs = 0);
|
||||||
|
|
||||||
AlexaStatusMessage &addToggleControllerProp(const String &instance, PowerController toggleState, unsigned int uncertaintyInMs = 0)
|
// A property without a function of its own: the row of its interface, the place of the property in the row
|
||||||
{
|
// (0 for the first) and its value, which is copied.
|
||||||
String value = (toggleState == PowerController::ON) ? "ON" : "OFF";
|
// JsonDocument volume;
|
||||||
return AddProperty(AlexaInterfaces::ToggleController, value, uncertaintyInMs, instance);
|
// volume.set(40);
|
||||||
}
|
// message.addProperty(AlexaInterfaces::Speaker, 0, volume.as<JsonVariantConst>());
|
||||||
|
// A place the row does not have adds nothing and prints an error.
|
||||||
|
AlexaStatusMessage &addProperty(const AlexaInterfaceDesc &row, uint8_t place, JsonVariantConst value, unsigned int uncertaintyInMs = 0, const String &instance = "");
|
||||||
|
|
||||||
// Adds a property the sketch built itself. Its timeOfSample is set to the current time when the object has
|
// Adds a property the sketch built itself. Its timeOfSample is set to the current time when the object has
|
||||||
// none or still carries the 1.x placeholder "{REPLACE_WITH_DATETIME}".
|
// none or still carries the 1.x placeholder "{REPLACE_WITH_DATETIME}".
|
||||||
|
|
@ -140,11 +181,11 @@ public:
|
||||||
{
|
{
|
||||||
return addTemperatureSensorProp(value, tempSensor, uncertaintyInMs);
|
return addTemperatureSensorProp(value, tempSensor, uncertaintyInMs);
|
||||||
}
|
}
|
||||||
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
|
AlexaStatusMessage &AddBrightnessControllerProp(int brightness, unsigned int uncertaintyInMs = 0)
|
||||||
{
|
{
|
||||||
return addBrightnessControllerProp(brightness, uncertaintyInMs);
|
return addBrightnessControllerProp(brightness, uncertaintyInMs);
|
||||||
}
|
}
|
||||||
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
|
AlexaStatusMessage &AddColorTemperatureControllerProp(int colorTemperature, unsigned int uncertaintyInMs = 0)
|
||||||
{
|
{
|
||||||
return addColorTemperatureControllerProp(colorTemperature, uncertaintyInMs);
|
return addColorTemperatureControllerProp(colorTemperature, uncertaintyInMs);
|
||||||
}
|
}
|
||||||
|
|
@ -207,22 +248,20 @@ private:
|
||||||
void setTimeOfSample(JsonObject property);
|
void setTimeOfSample(JsonObject property);
|
||||||
bool publishOn(PGM_P topicEnd, bool ofEndpoint);
|
bool publishOn(PGM_P topicEnd, bool ofEndpoint);
|
||||||
|
|
||||||
// Adds the first property of the row: every interface with a helper above reports one
|
// Adds a property to the list and returns its value, which is still empty, for the caller to set. The
|
||||||
template <typename T>
|
// place has to be one the row has.
|
||||||
AlexaStatusMessage &AddProperty(const AlexaInterfaceDesc &row, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
|
JsonVariant newProperty(const AlexaInterfaceDesc &row, uint8_t place, unsigned int uncertaintyInMs, const String &instance = "");
|
||||||
{
|
|
||||||
JsonObject prop = properties().add<JsonObject>();
|
|
||||||
prop["namespace"] = FPSTR(row.ns);
|
|
||||||
prop["name"] = FPSTR(row.property(0));
|
|
||||||
|
|
||||||
if (!instanceName.isEmpty()) {
|
// What several properties have in common
|
||||||
prop["instance"] = instanceName;
|
AlexaStatusMessage &addSwitchProp(const AlexaInterfaceDesc &row, PowerController state, unsigned int uncertaintyInMs, const String &instance = "");
|
||||||
}
|
AlexaStatusMessage &addNumberProp(const AlexaInterfaceDesc &row, int value, int lowest, int highest, unsigned int uncertaintyInMs);
|
||||||
prop["value"] = value;
|
AlexaStatusMessage &addDetectionProp(const AlexaInterfaceDesc &row, bool detected, unsigned int uncertaintyInMs);
|
||||||
|
AlexaStatusMessage &addTemperatureProp(const AlexaInterfaceDesc &row, uint8_t place, float value, TemperatureSensorScale scale, unsigned int uncertaintyInMs);
|
||||||
|
|
||||||
setTimeOfSample(prop);
|
// The value when it is within the range, the nearest of the range with a line in the log when it is not.
|
||||||
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
|
// Whole numbers have a function of their own: a lamp that reports its brightness links no floating point.
|
||||||
return *this;
|
int withinRange(int value, int lowest, int highest, const AlexaInterfaceDesc &row) const;
|
||||||
}
|
float withinRange(float value, int lowest, int highest, const AlexaInterfaceDesc &row) const;
|
||||||
|
int nearestOf(bool below, int lowest, int highest, const AlexaInterfaceDesc &row) const;
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "AlexaDevice.h"
|
#include "AlexaDevice.h"
|
||||||
#include "AlexaLog.h"
|
#include "AlexaLog.h"
|
||||||
|
#include "AlexaResources.h"
|
||||||
#include "AlexaVersion.h"
|
#include "AlexaVersion.h"
|
||||||
|
|
||||||
// The endpoint id and the defaults are the same in every example; its name, category and capabilities are not
|
// The endpoint id and the defaults are the same in every example; its name, category and capabilities are not
|
||||||
|
|
@ -552,15 +553,17 @@ void test_semantics_on_an_interface_that_takes_none_are_refused_with_an_error(vo
|
||||||
CapturedLog log;
|
CapturedLog log;
|
||||||
AlexaLog::setOutput(&log);
|
AlexaLog::setOutput(&log);
|
||||||
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
||||||
device.addCapability(AlexaInterfaces::PowerController)
|
AlexaCapability *power = device.addCapability(AlexaInterfaces::PowerController);
|
||||||
->addActionMapping(ActionMapping({AlexaAction::Open}, "TurnOn"))
|
power->addActionMapping(ActionMapping({AlexaAction::Open}, "TurnOn"));
|
||||||
.addStateMapping({AlexaState::Open}, "ON");
|
AlexaCapability *brightness = device.addCapability(AlexaInterfaces::BrightnessController);
|
||||||
|
brightness->addStateMapping({AlexaState::Open}, "ON");
|
||||||
|
|
||||||
assertLogged(log, "error: Alexa.PowerController: action mapping not added: "
|
assertLogged(log, "error: Alexa.PowerController: action mapping not added: "
|
||||||
"only RangeController, ModeController and ToggleController take one");
|
"only RangeController, ModeController and ToggleController take one");
|
||||||
assertLogged(log, "error: Alexa.PowerController: state mapping not added: "
|
assertLogged(log, "error: Alexa.BrightnessController: state mapping not added: "
|
||||||
"only RangeController, ModeController and ToggleController take one");
|
"only RangeController, ModeController and ToggleController take one");
|
||||||
assertDiscovery(endpoint("Test Lamp", "OTHER", POWER), device);
|
TEST_ASSERT_FALSE(power->isValid());
|
||||||
|
TEST_ASSERT_FALSE(brightness->isValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_error(void)
|
void test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_error(void)
|
||||||
|
|
@ -581,11 +584,12 @@ void test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_er
|
||||||
{
|
{
|
||||||
lift->addStateMapping({AlexaState::Open}, 100.0f);
|
lift->addStateMapping({AlexaState::Open}, 100.0f);
|
||||||
}
|
}
|
||||||
const std::string full = announcedBy(device);
|
TEST_ASSERT_NOT_NULL(strstr(announcedBy(device).c_str(), "Alexa.RangeController"));
|
||||||
|
TEST_ASSERT_TRUE(lift->isValid());
|
||||||
TEST_ASSERT_EQUAL_STRING("", log.text.c_str() + strlen("[Alex2ESP] ESP-01: capability Alexa.RangeController Blind.Lift added\r\n"));
|
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")
|
lift->addFriendlyName("Shade", "en-US")
|
||||||
.addFriendlyAsset(PSTR("Alexa.Setting.Opening"))
|
.addFriendlyAsset(AlexaAssets::Setting_Opening)
|
||||||
.addActionMapping(ActionMapping({AlexaAction::Close}, "SetRangeValue", "{\"rangeValue\":0}"))
|
.addActionMapping(ActionMapping({AlexaAction::Close}, "SetRangeValue", "{\"rangeValue\":0}"))
|
||||||
.addStateMapping({AlexaState::Closed}, 0.0f);
|
.addStateMapping({AlexaState::Closed}, 0.0f);
|
||||||
|
|
||||||
|
|
@ -596,7 +600,125 @@ void test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_er
|
||||||
"it has 4 already (ALEX2ESP_MAX_ACTION_MAPPINGS)");
|
"it has 4 already (ALEX2ESP_MAX_ACTION_MAPPINGS)");
|
||||||
assertLogged(log, "error: Alexa.RangeController Blind.Lift: state mapping not added: "
|
assertLogged(log, "error: Alexa.RangeController Blind.Lift: state mapping not added: "
|
||||||
"it has 2 already (ALEX2ESP_MAX_STATE_MAPPINGS)");
|
"it has 2 already (ALEX2ESP_MAX_STATE_MAPPINGS)");
|
||||||
TEST_ASSERT_EQUAL_STRING(full.c_str(), announcedBy(device).c_str());
|
TEST_ASSERT_FALSE(lift->isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
// One capability for every call that a setter refuses: each has to make its capability invalid
|
||||||
|
void test_capability_is_valid_until_a_setter_refuses_what_it_is_given(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
AlexaCapability *valid = device.addCapability(AlexaInterfaces::ToggleController, "Blind.Valid");
|
||||||
|
valid->addFriendlyName("Blind", "en-US")
|
||||||
|
.addFriendlyAsset(AlexaAssets::DeviceName_Shade)
|
||||||
|
.setNonControllable(true)
|
||||||
|
.addActionMapping(ActionMapping({AlexaAction::Close}, "TurnOn"))
|
||||||
|
.addStateMapping({AlexaState::Closed}, "ON");
|
||||||
|
TEST_ASSERT_TRUE(valid->isValid());
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.A")->addFriendlyName("", "en-US").isValid());
|
||||||
|
TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.B")->addFriendlyName("Blind", nullptr).isValid());
|
||||||
|
TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.C")->addFriendlyAsset(nullptr).isValid());
|
||||||
|
TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.D")->addActionMapping(ActionMapping({}, "TurnOn")).isValid());
|
||||||
|
TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.E")->addActionMapping(ActionMapping({AlexaAction::Open}, "")).isValid());
|
||||||
|
TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.F")->addStateMapping({}, "ON").isValid());
|
||||||
|
TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.G")
|
||||||
|
->addStateMapping({AlexaState::Open}, static_cast<const char *>(nullptr))
|
||||||
|
.isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_capability_that_is_not_valid_is_left_out_of_discovery_with_an_error(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
device.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
// The name that was refused is followed by one that is taken: the capability could be announced, as half
|
||||||
|
// of what the sketch asked for
|
||||||
|
device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
|
||||||
|
->addFriendlyName("Lift", nullptr)
|
||||||
|
.addFriendlyAsset(AlexaAssets::Setting_Opening);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("Bedroom Blinds", "OTHER", POWER), device);
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.RangeController Blind.Lift left out of discovery: "
|
||||||
|
"a call that set it up was refused, correct what the error of that call names");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_state_mapping_takes_a_number_of_any_type(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
||||||
|
AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
||||||
|
lift->addFriendlyAsset(AlexaAssets::Setting_Opening).addStateMapping({AlexaState::Closed}, 0).addStateMapping({AlexaState::Open}, 1, 100);
|
||||||
|
AlexaCapability *tilt = device.addCapability(AlexaInterfaces::RangeController, "Blind.Tilt");
|
||||||
|
tilt->addFriendlyAsset(AlexaAssets::Setting_Direction).addStateMapping({AlexaState::Closed}, 0.0).addStateMapping({AlexaState::Open}, 90u);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(lift->isValid());
|
||||||
|
TEST_ASSERT_TRUE(tilt->isValid());
|
||||||
|
assertDiscovery(
|
||||||
|
endpoint("Bedroom Blinds", "OTHER",
|
||||||
|
"{\"interface\":\"Alexa.RangeController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
||||||
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,"
|
||||||
|
"\"supported\":[{\"name\":\"rangeValue\"}]},"
|
||||||
|
"\"semantics\":{\"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\"}}]}},"
|
||||||
|
"{\"interface\":\"Alexa.RangeController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
||||||
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,"
|
||||||
|
"\"supported\":[{\"name\":\"rangeValue\"}]},"
|
||||||
|
"\"semantics\":{\"stateMappings\":["
|
||||||
|
"{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Closed\"],\"value\":0},"
|
||||||
|
"{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Open\"],\"value\":90}]},"
|
||||||
|
"\"instance\":\"Blind.Tilt\","
|
||||||
|
"\"capabilityResources\":{\"friendlyNames\":["
|
||||||
|
"{\"@type\":\"asset\",\"value\":{\"assetId\":\"Alexa.Setting.Direction\"}}]}}"),
|
||||||
|
device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_states_of_a_hunch_and_of_an_announcement_are_announced(void)
|
||||||
|
{
|
||||||
|
AlexaDevice device("Washer", "root", "ESP-01");
|
||||||
|
device.addCapability(AlexaInterfaces::ToggleController, "Washer.Eco")
|
||||||
|
->addFriendlyName("Eco", "en-US")
|
||||||
|
.addStateMapping({AlexaState::EcoOn}, "ON")
|
||||||
|
.addStateMapping({AlexaState::EcoOff}, "OFF");
|
||||||
|
device.addCapability(AlexaInterfaces::ModeController, "Washer.Cycle")
|
||||||
|
->addFriendlyAsset(AlexaAssets::Setting_WashCycle)
|
||||||
|
.addStateMapping({AlexaState::Stuck, AlexaState::Done}, "Cycle.End");
|
||||||
|
device.addCapability(AlexaInterfaces::RangeController, "Washer.Detergent")
|
||||||
|
->addFriendlyName("Detergent", "en-US")
|
||||||
|
.addStateMapping({AlexaState::Empty, AlexaState::Low}, 0, 10)
|
||||||
|
.addStateMapping({AlexaState::Full}, 100);
|
||||||
|
|
||||||
|
const std::string announced = announcedBy(device);
|
||||||
|
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(announced.c_str(),
|
||||||
|
"\"stateMappings\":[{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.EcoOn\"],\"value\":\"ON\"},"
|
||||||
|
"{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.EcoOff\"],\"value\":\"OFF\"}]"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(announced.c_str(),
|
||||||
|
"\"stateMappings\":[{\"@type\":\"StatesToValue\","
|
||||||
|
"\"states\":[\"Alexa.States.Done\",\"Alexa.States.Stuck\"],\"value\":\"Cycle.End\"}]"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(announced.c_str(),
|
||||||
|
"\"stateMappings\":[{\"@type\":\"StatesToRange\",\"states\":[\"Alexa.States.Low\",\"Alexa.States.Empty\"],"
|
||||||
|
"\"range\":{\"minimumValue\":0,\"maximumValue\":10}},"
|
||||||
|
"{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Full\"],\"value\":100}]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_assets_and_units_have_the_ids_of_alexa(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Setting.Opening", AlexaAssets::Setting_Opening);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Setting.2GGuestWiFi", AlexaAssets::Setting_2GGuestWiFi);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.DeviceName.SpaceHeater", AlexaAssets::DeviceName_SpaceHeater);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Value.QuickWash", AlexaAssets::Value_QuickWash);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Button.EighthButton", AlexaAssets::Button_EighthButton);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Gestures.DoubleTap", AlexaAssets::Gesture_DoubleTap);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Shower.RainHead", AlexaAssets::Shower_RainHead);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.Vacuum", AlexaAssets::Actions_Vacuum);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Unit.Percent", AlexaUnits::Percent);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Unit.Temperature.Celsius", AlexaUnits::Temperature_Celsius);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.Unit.Volume.CubicMeters", AlexaUnits::Volume_CubicMeters);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_action_mapping_payload_that_is_no_object_is_left_out_with_an_error(void)
|
void test_action_mapping_payload_that_is_no_object_is_left_out_with_an_error(void)
|
||||||
|
|
@ -620,6 +742,8 @@ 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.Open", alexaActionName(AlexaAction::Open));
|
||||||
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.SetEcoOff", alexaActionName(AlexaAction::SetEcoOff));
|
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.States.Closed", alexaStateName(AlexaState::Closed));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.States.EcoOn", alexaStateName(AlexaState::EcoOn));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Alexa.States.Stuck", alexaStateName(AlexaState::Stuck));
|
||||||
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.Lower", AlexaActionsUtils::toString(AlexaActions::Lower).c_str());
|
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.Lower", AlexaActionsUtils::toString(AlexaActions::Lower).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -658,5 +782,10 @@ int main(int, char **)
|
||||||
RUN_TEST(test_more_names_and_mappings_than_a_capability_holds_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_action_mapping_payload_that_is_no_object_is_left_out_with_an_error);
|
||||||
RUN_TEST(test_actions_and_states_have_the_names_of_alexa);
|
RUN_TEST(test_actions_and_states_have_the_names_of_alexa);
|
||||||
|
RUN_TEST(test_capability_is_valid_until_a_setter_refuses_what_it_is_given);
|
||||||
|
RUN_TEST(test_capability_that_is_not_valid_is_left_out_of_discovery_with_an_error);
|
||||||
|
RUN_TEST(test_state_mapping_takes_a_number_of_any_type);
|
||||||
|
RUN_TEST(test_states_of_a_hunch_and_of_an_announcement_are_announced);
|
||||||
|
RUN_TEST(test_assets_and_units_have_the_ids_of_alexa);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -493,6 +493,272 @@ void test_message_is_sent_once(void)
|
||||||
TEST_ASSERT_EQUAL_STRING("null", transport.sent[1].json.c_str()); // the bridge refuses it as empty
|
TEST_ASSERT_EQUAL_STRING("null", transport.sent[1].json.c_str()); // the bridge refuses it as empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The properties of the message as it was published, without what is around them
|
||||||
|
static std::string propertiesOf(const FakeTransport::Message &message)
|
||||||
|
{
|
||||||
|
JsonDocument doc;
|
||||||
|
TEST_ASSERT_TRUE(deserializeJson(doc, message.json) == DeserializationError::Ok);
|
||||||
|
std::string text;
|
||||||
|
serializeJson(doc["context"]["properties"], text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define PROPERTY(ns, name, value) "{\"namespace\":\"" ns "\",\"name\":\"" name "\",\"value\":" value SAMPLED
|
||||||
|
#define PROPERTY_OF(ns, name, instance, value) \
|
||||||
|
"{\"namespace\":\"" ns "\",\"name\":\"" name "\",\"instance\":\"" instance "\",\"value\":" value SAMPLED
|
||||||
|
|
||||||
|
void test_numbers_of_a_light_are_reported_as_they_are(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
lamp.response(TOKEN_OF_THE_DIRECTIVE)
|
||||||
|
.addBrightnessControllerProp(40)
|
||||||
|
.addPercentageControllerProp(75)
|
||||||
|
.addPowerLevelControllerProp(0)
|
||||||
|
.addColorTemperatureControllerProp(10000)
|
||||||
|
.send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY("Alexa.BrightnessController", "brightness", "40") ","
|
||||||
|
PROPERTY("Alexa.PercentageController", "percentage", "75") ","
|
||||||
|
PROPERTY("Alexa.PowerLevelController", "powerLevel", "0") ","
|
||||||
|
PROPERTY("Alexa.ColorTemperatureController", "colorTemperatureInKelvin", "10000") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_color_is_reported_as_hue_saturation_and_brightness(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
lamp.response(TOKEN_OF_THE_DIRECTIVE).addColorControllerProp(350.5f, 0.75f, 0.5f).send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY("Alexa.ColorController", "color", "{\"hue\":350.5,\"saturation\":0.75,\"brightness\":0.5}") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_generic_controllers_report_with_their_instance(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice fan("Fan", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
fan.response(TOKEN_OF_THE_DIRECTIVE)
|
||||||
|
.addRangeControllerProp("Fan.Speed", 7.5f)
|
||||||
|
.addRangeControllerProp("Fan.Angle", 100)
|
||||||
|
.addModeControllerProp("Fan.Direction", "Direction.Forward")
|
||||||
|
.addToggleControllerProp("Fan.Oscillate", PowerController::ON)
|
||||||
|
.send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY_OF("Alexa.RangeController", "rangeValue", "Fan.Speed", "7.5") ","
|
||||||
|
PROPERTY_OF("Alexa.RangeController", "rangeValue", "Fan.Angle", "100") ","
|
||||||
|
PROPERTY_OF("Alexa.ModeController", "mode", "Fan.Direction", "\"Direction.Forward\"") ","
|
||||||
|
PROPERTY_OF("Alexa.ToggleController", "toggleState", "Fan.Oscillate", "\"ON\"") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_lock_reports_each_of_its_states(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice lock("Lock", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
lock.response(TOKEN_OF_THE_DIRECTIVE)
|
||||||
|
.addLockControllerProp(AlexaLockState::LOCKED)
|
||||||
|
.addLockControllerProp(AlexaLockState::UNLOCKED)
|
||||||
|
.addLockControllerProp(AlexaLockState::JAMMED, 2000)
|
||||||
|
.send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY("Alexa.LockController", "lockState", "\"LOCKED\"") ","
|
||||||
|
PROPERTY("Alexa.LockController", "lockState", "\"UNLOCKED\"") ","
|
||||||
|
"{\"namespace\":\"Alexa.LockController\",\"name\":\"lockState\",\"value\":\"JAMMED\","
|
||||||
|
"\"timeOfSample\":\"2026-09-28T13:05:09Z\",\"uncertaintyInMilliseconds\":2000}]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_contact_and_motion_report_what_they_detect(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice door("Door", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
door.response(TOKEN_OF_THE_DIRECTIVE)
|
||||||
|
.addContactSensorProp(true)
|
||||||
|
.addContactSensorProp(false)
|
||||||
|
.addMotionSensorProp(true)
|
||||||
|
.addMotionSensorProp(false)
|
||||||
|
.send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY("Alexa.ContactSensor", "detectionState", "\"DETECTED\"") ","
|
||||||
|
PROPERTY("Alexa.ContactSensor", "detectionState", "\"NOT_DETECTED\"") ","
|
||||||
|
PROPERTY("Alexa.MotionSensor", "detectionState", "\"DETECTED\"") ","
|
||||||
|
PROPERTY("Alexa.MotionSensor", "detectionState", "\"NOT_DETECTED\"") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_humidity_is_reported_inside_an_object(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice sensor("Sensor", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
sensor.response(TOKEN_OF_THE_DIRECTIVE).addHumiditySensorProp(45.5f).send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY("Alexa.HumiditySensor", "relativeHumidity", "{\"value\":45.5}") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_health_reports_a_device_that_cannot_be_reached(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice sensor("Sensor", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
sensor.response(TOKEN_OF_THE_DIRECTIVE).addHealthProp(EndpointHealth::OK).addHealthProp(EndpointHealth::UNREACHABLE).send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" HEALTH_OK "," PROPERTY("Alexa.EndpointHealth", "connectivity", "{\"value\":\"UNREACHABLE\"}") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_thermostat_with_one_setpoint_reports_the_target_and_the_mode(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice thermostat("Thermostat", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
thermostat.response(TOKEN_OF_THE_DIRECTIVE)
|
||||||
|
.addThermostatSetpointProp(21.5f)
|
||||||
|
.addThermostatModeProp(AlexaThermostatMode::HEAT)
|
||||||
|
.send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY("Alexa.ThermostatController", "targetSetpoint", "{\"value\":21.5,\"scale\":\"CELSIUS\"}") ","
|
||||||
|
PROPERTY("Alexa.ThermostatController", "thermostatMode", "\"HEAT\"") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_thermostat_with_two_setpoints_reports_the_lower_and_the_upper(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice thermostat("Thermostat", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
thermostat.response(TOKEN_OF_THE_DIRECTIVE)
|
||||||
|
.addThermostatDualSetpointProp(68, 76, TemperatureSensorScale::FAHRENHEIT)
|
||||||
|
.addThermostatModeProp(AlexaThermostatMode::AUTO)
|
||||||
|
.send();
|
||||||
|
thermostat.response(TOKEN_OF_THE_DIRECTIVE)
|
||||||
|
.addThermostatLowerSetpointProp(68, TemperatureSensorScale::FAHRENHEIT)
|
||||||
|
.addThermostatUpperSetpointProp(76, TemperatureSensorScale::FAHRENHEIT)
|
||||||
|
.addThermostatModeProp(AlexaThermostatMode::AUTO)
|
||||||
|
.send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY("Alexa.ThermostatController", "lowerSetpoint", "{\"value\":68,\"scale\":\"FAHRENHEIT\"}") ","
|
||||||
|
PROPERTY("Alexa.ThermostatController", "upperSetpoint", "{\"value\":76,\"scale\":\"FAHRENHEIT\"}") ","
|
||||||
|
PROPERTY("Alexa.ThermostatController", "thermostatMode", "\"AUTO\"") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING(propertiesOf(transport.sent[0]).c_str(), propertiesOf(transport.sent[1]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_every_thermostat_mode_has_the_name_of_alexa(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice thermostat("Thermostat", "root", "ESP-01", &transport);
|
||||||
|
const AlexaThermostatMode modes[] = {AlexaThermostatMode::AUTO, AlexaThermostatMode::COOL, AlexaThermostatMode::ECO,
|
||||||
|
AlexaThermostatMode::EM_HEAT, AlexaThermostatMode::HEAT, AlexaThermostatMode::OFF};
|
||||||
|
const char *const names[] = {"AUTO", "COOL", "ECO", "EM_HEAT", "HEAT", "OFF"};
|
||||||
|
|
||||||
|
for (int i = 0; i < 6; i++)
|
||||||
|
{
|
||||||
|
thermostat.response(TOKEN_OF_THE_DIRECTIVE).addThermostatModeProp(modes[i]).send();
|
||||||
|
JsonDocument report;
|
||||||
|
TEST_ASSERT_TRUE(deserializeJson(report, transport.sent[i].json) == DeserializationError::Ok);
|
||||||
|
TEST_ASSERT_EQUAL_STRING(names[i], report["context"]["properties"][0]["value"] | "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_value_outside_of_its_range_is_reported_as_the_nearest_with_an_error(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
lamp.response(TOKEN_OF_THE_DIRECTIVE)
|
||||||
|
.addBrightnessControllerProp(120)
|
||||||
|
.addPercentageControllerProp(-5)
|
||||||
|
.addColorTemperatureControllerProp(500)
|
||||||
|
.addHumiditySensorProp(100.5f)
|
||||||
|
.addColorControllerProp(400, -0.25f, 0.5f)
|
||||||
|
.send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY("Alexa.BrightnessController", "brightness", "100") ","
|
||||||
|
PROPERTY("Alexa.PercentageController", "percentage", "0") ","
|
||||||
|
PROPERTY("Alexa.ColorTemperatureController", "colorTemperatureInKelvin", "1000") ","
|
||||||
|
PROPERTY("Alexa.HumiditySensor", "relativeHumidity", "{\"value\":100}") ","
|
||||||
|
PROPERTY("Alexa.ColorController", "color", "{\"hue\":360,\"saturation\":0,\"brightness\":0.5}") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.BrightnessController reported as 100: "
|
||||||
|
"the value was outside of 0 to 100, the range Alexa gives for it");
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.PercentageController reported as 0: the value was outside of 0 to 100");
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.ColorTemperatureController reported as 1000: the value was outside of 1000 to 10000");
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.HumiditySensor reported as 100: the value was outside of 0 to 100");
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.ColorController reported as 360: the value was outside of 0 to 360");
|
||||||
|
assertLogged(log, "error: ESP-01: Alexa.ColorController reported as 0: the value was outside of 0 to 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_property_without_a_function_is_added_by_its_place_in_the_row(void)
|
||||||
|
{
|
||||||
|
CapturedLog log;
|
||||||
|
AlexaLog::setOutput(&log);
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice speaker("Speaker", "root", "ESP-01", &transport);
|
||||||
|
JsonDocument values;
|
||||||
|
values["volume"] = 40;
|
||||||
|
values["muted"] = false;
|
||||||
|
values["level"]["@type"] = "Percentage";
|
||||||
|
values["level"]["value"] = 60;
|
||||||
|
|
||||||
|
speaker.response(TOKEN_OF_THE_DIRECTIVE)
|
||||||
|
.addProperty(AlexaInterfaces::Speaker, 0, values["volume"])
|
||||||
|
.addProperty(AlexaInterfaces::Speaker, 1, values["muted"], 250)
|
||||||
|
.addProperty(AlexaInterfaces::InventoryLevelSensor, 0, values["level"], 0, "Speaker.Battery")
|
||||||
|
.addProperty(AlexaInterfaces::Speaker, 2, values["volume"])
|
||||||
|
.addProperty(AlexaInterfaces::SceneController, 0, values["volume"])
|
||||||
|
.send();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"[" PROPERTY("Alexa.Speaker", "volume", "40") ","
|
||||||
|
"{\"namespace\":\"Alexa.Speaker\",\"name\":\"muted\",\"value\":false,"
|
||||||
|
"\"timeOfSample\":\"2026-09-28T13:05:09Z\",\"uncertaintyInMilliseconds\":250},"
|
||||||
|
PROPERTY_OF("Alexa.InventoryLevelSensor", "level", "Speaker.Battery", "{\"@type\":\"Percentage\",\"value\":60}") "]",
|
||||||
|
propertiesOf(transport.sent[0]).c_str());
|
||||||
|
assertLogged(log, "error: ESP-01: property 2 of Alexa.Speaker not added: the interface has 2, the first of them is 0");
|
||||||
|
assertLogged(log, "error: ESP-01: property 0 of Alexa.SceneController not added: the interface has 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_change_report_takes_the_properties_of_every_function(void)
|
||||||
|
{
|
||||||
|
FakeTransport transport;
|
||||||
|
AlexaDevice door("Door", "root", "ESP-01", &transport);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(door.changeReport(AlexaCause::PHYSICAL_INTERACTION)
|
||||||
|
.addContactSensorProp(true)
|
||||||
|
.context()
|
||||||
|
.addLockControllerProp(AlexaLockState::UNLOCKED)
|
||||||
|
.addHealthProp(EndpointHealth::OK)
|
||||||
|
.send());
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING(
|
||||||
|
"{\"event\":{" HEADER_OF("Alexa", "ChangeReport") "}" ENDPOINT
|
||||||
|
",\"payload\":{\"change\":{\"cause\":{\"type\":\"PHYSICAL_INTERACTION\"},\"properties\":["
|
||||||
|
PROPERTY("Alexa.ContactSensor", "detectionState", "\"DETECTED\"") "]}}},"
|
||||||
|
"\"context\":{\"properties\":[" PROPERTY("Alexa.LockController", "lockState", "\"UNLOCKED\"") "," HEALTH_OK "]}}",
|
||||||
|
published(transport.sent[0]).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
int main(int, char **)
|
int main(int, char **)
|
||||||
{
|
{
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
|
|
@ -515,5 +781,19 @@ int main(int, char **)
|
||||||
RUN_TEST(test_every_message_has_an_id_of_its_own);
|
RUN_TEST(test_every_message_has_an_id_of_its_own);
|
||||||
RUN_TEST(test_change_report_of_a_handler_is_not_its_answer);
|
RUN_TEST(test_change_report_of_a_handler_is_not_its_answer);
|
||||||
RUN_TEST(test_message_is_sent_once);
|
RUN_TEST(test_message_is_sent_once);
|
||||||
|
|
||||||
|
RUN_TEST(test_numbers_of_a_light_are_reported_as_they_are);
|
||||||
|
RUN_TEST(test_color_is_reported_as_hue_saturation_and_brightness);
|
||||||
|
RUN_TEST(test_generic_controllers_report_with_their_instance);
|
||||||
|
RUN_TEST(test_lock_reports_each_of_its_states);
|
||||||
|
RUN_TEST(test_contact_and_motion_report_what_they_detect);
|
||||||
|
RUN_TEST(test_humidity_is_reported_inside_an_object);
|
||||||
|
RUN_TEST(test_health_reports_a_device_that_cannot_be_reached);
|
||||||
|
RUN_TEST(test_thermostat_with_one_setpoint_reports_the_target_and_the_mode);
|
||||||
|
RUN_TEST(test_thermostat_with_two_setpoints_reports_the_lower_and_the_upper);
|
||||||
|
RUN_TEST(test_every_thermostat_mode_has_the_name_of_alexa);
|
||||||
|
RUN_TEST(test_value_outside_of_its_range_is_reported_as_the_nearest_with_an_error);
|
||||||
|
RUN_TEST(test_property_without_a_function_is_added_by_its_place_in_the_row);
|
||||||
|
RUN_TEST(test_change_report_takes_the_properties_of_every_function);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue