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

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

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

View file

@ -28,6 +28,7 @@
#include "AlexaInterface.h"
#include "AlexaLimits.h"
#include "AlexaLog.h"
#include "AlexaResources.h"
#include "AlexaTransport.h"
#include "AlexaUtils.h"

View file

@ -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_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_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)
{
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
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++)
{
@ -76,7 +88,7 @@ AlexaCapabilityName::AlexaCapabilityName(const AlexaInterfaceDesc &row, const ch
}
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);
}
@ -93,6 +105,12 @@ AlexaCapability::~AlexaCapability()
}
}
AlexaCapability &AlexaCapability::refused()
{
valid = false;
return *this;
}
AlexaCapability &AlexaCapability::setInstance(const char *newInstance)
{
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",
AlexaCapabilityName(*row, nullptr).text);
return refused();
}
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\")",
name.text);
return *this;
return refused();
}
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
{
ALEX2ESP_LOGE("%s: friendly name \"%s\" not added: it has %d already (ALEX2ESP_MAX_FRIENDLY_NAMES)",
name.text, text, ALEX2ESP_MAX_FRIENDLY_NAMES);
return *this;
return refused();
}
const char *copy = copyText(text);
if (copy == nullptr)
{
ALEX2ESP_LOGE("%s: friendly name \"%s\" not added: the heap has no room for it", name.text, text);
return *this;
return refused();
}
names[nameCount++] = {copy, locale};
return *this;
@ -135,14 +154,14 @@ AlexaCapability &AlexaCapability::addFriendlyAsset(PGM_P assetId)
const AlexaCapabilityName name(*row, instance);
if (assetId == nullptr)
{
ALEX2ESP_LOGE("%s: asset not added: pass its id, addFriendlyAsset(PSTR(\"Alexa.Setting.Opening\"))", name.text);
return *this;
ALEX2ESP_LOGE("%s: asset not added: pass its id, addFriendlyAsset(AlexaAssets::Setting_Opening)", name.text);
return refused();
}
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
{
ALEX2ESP_LOGE("%s: asset not added: it has %d names already (ALEX2ESP_MAX_FRIENDLY_NAMES)",
name.text, ALEX2ESP_MAX_FRIENDLY_NAMES);
return *this;
return refused();
}
names[nameCount++] = {assetId, nullptr};
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",
name.text);
return *this;
return refused();
}
if (mapping.actions == 0 || mapping.directiveName == nullptr || mapping.directiveName[0] == '\0')
{
ALEX2ESP_LOGE("%s: action mapping not added: it needs an action and a directive, ActionMapping({AlexaAction::Open}, \"TurnOn\")",
name.text);
return *this;
return refused();
}
if (actionCount >= ALEX2ESP_MAX_ACTION_MAPPINGS)
{
ALEX2ESP_LOGE("%s: action mapping to %s not added: it has %d already (ALEX2ESP_MAX_ACTION_MAPPINGS)",
name.text, mapping.directiveName, ALEX2ESP_MAX_ACTION_MAPPINGS);
return *this;
return refused();
}
actionMappings[actionCount++] = mapping;
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",
name.text);
valid = false;
return nullptr;
}
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);
valid = false;
return nullptr;
}
if (stateCount >= ALEX2ESP_MAX_STATE_MAPPINGS)
{
ALEX2ESP_LOGE("%s: state mapping not added: it has %d already (ALEX2ESP_MAX_STATE_MAPPINGS)",
name.text, ALEX2ESP_MAX_STATE_MAPPINGS);
valid = false;
return nullptr;
}
StateMapping *mapping = &stateMappings[stateCount++];
mapping->states = 0;
for (AlexaState state : states)
{
mapping->states |= static_cast<uint8_t>(1u << static_cast<uint8_t>(state));
mapping->states |= static_cast<uint16_t>(1u << static_cast<uint8_t>(state));
}
mapping->kind = kind;
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\")",
AlexaCapabilityName(*row, instance).text);
return *this;
return refused();
}
StateMapping *mapping = newStateMapping(states, STATES_TO_TEXT);
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.
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))
{
const AlexaCapabilityName name(*row, instance);

View file

@ -9,6 +9,7 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <initializer_list>
#include <type_traits>
#include "AlexaInterfaces.h"
#include "AlexaLimits.h"
@ -23,11 +24,19 @@ enum class AlexaAction : uint8_t
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
{
Open,
Closed
Closed,
EcoOn,
EcoOff,
Low,
Empty,
Full,
Done,
Stuck
};
// "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:
// device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
// ->addFriendlyName("Lift", "en-US").setConfiguration(fillLift).addStateMapping({AlexaState::Closed}, 0.0f);
// What a capability refuses is printed at ERROR with what to change, and leaves the capability as it was.
// 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
{
public:
@ -97,6 +108,9 @@ public:
bool isProactivelyReported() const { return proactivelyReported; }
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
// copied. addCapability(row, instance) sets it; this is for a capability that was added by its type.
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.
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 &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, 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
// for an interface that has instances.
bool matches(const char *ns, const char *directiveInstance) const;
// Adds the discovery object of the capability to the capabilities of its endpoint. A capability that Alexa
// would reject the endpoint for, a generic controller without an instance or without a name, adds nothing,
// prints what is missing and returns false.
// prints what is missing and returns false. So does one that is not valid.
bool toJson(JsonArray capabilities, const char *endpointId) const;
private:
@ -149,8 +172,8 @@ private:
struct StateMapping
{
uint8_t states; // one bit per AlexaState
uint8_t kind; // StateMappingKind
uint16_t states; // one bit per AlexaState
uint8_t kind; // StateMappingKind
union
{
float value; // the value, or the lowest of a range
@ -160,6 +183,7 @@ private:
};
StateMapping *newStateMapping(std::initializer_list<AlexaState> states, StateMappingKind kind);
AlexaCapability &refused();
void writeSemantics(JsonObject capability) const;
void writeNames(JsonObject capability) const;
@ -176,6 +200,7 @@ private:
bool retrievable : 1;
bool proactivelyReported : 1;
bool nonControllable : 1;
bool valid : 1;
};
#endif // ALEXA_CAPABILITY_H

141
src/AlexaResources.h Normal file
View 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

View file

@ -1,5 +1,6 @@
#include "AlexaStatusMessage.h"
#include "AlexaBridgeLogic.h"
#include "AlexaCapability.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
@ -82,9 +83,69 @@ JsonObject AlexaStatusMessage::payload()
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;
if (scale == TemperatureSensorScale::FAHRENHEIT)
{
@ -98,7 +159,170 @@ AlexaStatusMessage &AlexaStatusMessage::addTemperatureSensorProp(float value, Te
{
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)

View file

@ -36,6 +36,24 @@ enum class TemperatureSensorScale
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
{
RESPONSE, // the answer to a directive that was carried out
@ -88,40 +106,63 @@ public:
AlexaMessageKind getKind() const { return kind; }
// The properties of the device. In a ChangeReport the properties added before context() are those that
// changed and the ones after it the others; every other kind has one list. An 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)
{
JsonDocument healthValue;
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
return AddProperty(AlexaInterfaces::EndpointHealth, healthValue.as<JsonObject>(), uncertaintyInMs);
}
// The properties of the device, one function for each. In a ChangeReport the properties added before
// context() are those that changed and the ones after it the others; every other kind has one list. An
// ErrorResponse, a DeferredResponse and an event carry no properties: what is added to them is not sent.
//
// uncertaintyInMs is the time since the value was read from the hardware. A value outside of the range
// that Alexa gives for its property is reported as the nearest one of the range, and the log says so at
// ERROR. A sketch links the functions it calls and no others.
AlexaStatusMessage &addHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0);
AlexaStatusMessage &addPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0);
AlexaStatusMessage &addPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaces::PowerController, value, uncertaintyInMs);
}
// 0 to 100
AlexaStatusMessage &addBrightnessControllerProp(int brightness, unsigned int uncertaintyInMs = 0);
AlexaStatusMessage &addPercentageControllerProp(int percentage, unsigned int uncertaintyInMs = 0);
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
AlexaStatusMessage &addTemperatureSensorProp(float value, TemperatureSensorScale scale = TemperatureSensorScale::CELSIUS, unsigned int uncertaintyInMs = 0);
AlexaStatusMessage &addBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaces::BrightnessController, brightness, uncertaintyInMs);
}
// Relative humidity, 0 to 100 percent
AlexaStatusMessage &addHumiditySensorProp(float humidity, unsigned int uncertaintyInMs = 0);
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)
{
String value = (toggleState == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaces::ToggleController, value, uncertaintyInMs, instance);
}
// 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.
// JsonDocument volume;
// 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
// none or still carries the 1.x placeholder "{REPLACE_WITH_DATETIME}".
@ -140,11 +181,11 @@ public:
{
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);
}
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
AlexaStatusMessage &AddColorTemperatureControllerProp(int colorTemperature, unsigned int uncertaintyInMs = 0)
{
return addColorTemperatureControllerProp(colorTemperature, uncertaintyInMs);
}
@ -207,22 +248,20 @@ private:
void setTimeOfSample(JsonObject property);
bool publishOn(PGM_P topicEnd, bool ofEndpoint);
// Adds the first property of the row: every interface with a helper above reports one
template <typename T>
AlexaStatusMessage &AddProperty(const AlexaInterfaceDesc &row, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
{
JsonObject prop = properties().add<JsonObject>();
prop["namespace"] = FPSTR(row.ns);
prop["name"] = FPSTR(row.property(0));
// Adds a property to the list and returns its value, which is still empty, for the caller to set. The
// place has to be one the row has.
JsonVariant newProperty(const AlexaInterfaceDesc &row, uint8_t place, unsigned int uncertaintyInMs, const String &instance = "");
if (!instanceName.isEmpty()) {
prop["instance"] = instanceName;
}
prop["value"] = value;
// What several properties have in common
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);
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);
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
return *this;
}
// The value when it is within the range, the nearest of the range with a line in the log when it is not.
// Whole numbers have a function of their own: a lamp that reports its brightness links no floating point.
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