Light, DimmableLight, ColorTemperatureLight, ColorLight, TemperatureSensor, ContactSensor, Blind, Thermostat, Lock, Scene, Doorbell and MultiDevice on the 2.0 API: addCapability(row, instance), one onDirective() handler, the report helpers, ChangeReports from loop(), a deferred answer for the lock. Wi-Fi is waited for 30 s, then the sketch says what to check and carries on. examples/README.md has what each sketch does, its limits and the measured sizes for d1_mini: Light takes 30,616 bytes of static RAM and 332,565 of flash; all 17 sketches build with 0 warnings. test/test_examples compares the discovery object of every new sketch: 147 host tests (were 135). The sketches were compiled, not run on a board. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
138 lines
4.5 KiB
C++
138 lines
4.5 KiB
C++
// Blind: a roller blind that Alexa opens, closes and sets to a position (Alexa.RangeController with semantics).
|
|
//
|
|
// The capability is one instance of the RangeController, "Blind.Lift": 0 is closed, 100 is open. Its semantics
|
|
// tell Alexa what "open", "close", "raise" and "lower" mean for it, and which values count as open and closed.
|
|
// SetRangeValue outside of 0 to 100 is answered with VALUE_OUT_OF_RANGE; AdjustRangeValue stops at the ends.
|
|
//
|
|
// The blind of this sketch is a number. moveTo() is the place for the motor.
|
|
#include <Arduino.h>
|
|
#include <ESP8266WiFi.h>
|
|
#include <Alex2ESP.h>
|
|
|
|
// Wi-Fi and the credentials of your Alex2MQTT account
|
|
const char *WIFI_SSID = "";
|
|
const char *WIFI_PASSWORD = "";
|
|
|
|
const char *ALEXA_USERNAME = "";
|
|
const char *ALEXA_PASSWORD = "";
|
|
const char *ALEXA_ROOT_TOPIC = "";
|
|
|
|
const char LIFT[] = "Blind.Lift";
|
|
|
|
Alex2ESP alexa;
|
|
int lift = 0; // percent open
|
|
|
|
// Joins the Wi-Fi network and returns after 30 s at the latest. Without a connection the sketch carries on: the
|
|
// ESP8266 keeps trying, and alexa.loop() opens the MQTT session once Wi-Fi is up.
|
|
void connectWiFi()
|
|
{
|
|
WiFi.mode(WIFI_STA);
|
|
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
|
Serial.printf("\n[WIFI] Connecting to \"%s\"\n", WIFI_SSID);
|
|
|
|
unsigned long started = millis();
|
|
while (WiFi.status() != WL_CONNECTED && millis() - started < 30000)
|
|
{
|
|
delay(100);
|
|
}
|
|
|
|
if (WiFi.status() == WL_CONNECTED)
|
|
{
|
|
Serial.printf("[WIFI] Connected, IP address %s\n", WiFi.localIP().toString().c_str());
|
|
}
|
|
else
|
|
{
|
|
Serial.printf("[WIFI] No connection after 30 s (status %d): check WIFI_SSID and WIFI_PASSWORD. Still trying.\n",
|
|
WiFi.status());
|
|
}
|
|
}
|
|
|
|
void moveTo(int position)
|
|
{
|
|
lift = position;
|
|
Serial.printf("[BLIND] %d percent open\n", lift);
|
|
}
|
|
|
|
// The "configuration" of the capability in discovery: the range of the value and its unit
|
|
void liftConfiguration(JsonObject configuration, void *)
|
|
{
|
|
JsonObject range = configuration["supportedRange"].to<JsonObject>();
|
|
range["minimumValue"] = 0;
|
|
range["maximumValue"] = 100;
|
|
range["precision"] = 1;
|
|
configuration["unitOfMeasure"] = FPSTR(AlexaUnits::Percent);
|
|
}
|
|
|
|
// The state of the blind: what the answer to a directive and the answer to ReportState carry
|
|
void sendState(AlexaStatusMessage message)
|
|
{
|
|
message.addHealthProp(EndpointHealth::OK).addRangeControllerProp(LIFT, lift).send();
|
|
}
|
|
|
|
void onDirective(AlexaDirective &directive)
|
|
{
|
|
if (directive.isReportState())
|
|
{
|
|
sendState(directive.stateReport());
|
|
return;
|
|
}
|
|
|
|
if (directive.is("SetRangeValue"))
|
|
{
|
|
int position = directive.payload["rangeValue"] | -1;
|
|
if (position < 0 || position > 100)
|
|
{
|
|
AlexaStatusMessage error = directive.error(AlexaErrorType::VALUE_OUT_OF_RANGE, "The blind takes 0 to 100");
|
|
error.payload()["validRange"]["minimumValue"] = 0;
|
|
error.payload()["validRange"]["maximumValue"] = 100;
|
|
error.send();
|
|
return;
|
|
}
|
|
moveTo(position);
|
|
}
|
|
else if (directive.is("AdjustRangeValue"))
|
|
{
|
|
moveTo(constrain(lift + (directive.payload["rangeValueDelta"] | 0), 0, 100));
|
|
}
|
|
else
|
|
{
|
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This blind takes a position").send();
|
|
return;
|
|
}
|
|
|
|
sendState(directive.response());
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
Serial.begin(74880);
|
|
|
|
connectWiFi();
|
|
|
|
// MQTT user name, MQTT password, root topic
|
|
alexa.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
|
|
|
|
// The name Alexa shows, and the id of the endpoint: every device of an account has its own
|
|
AlexaDevice *blind = alexa.getDevice("Bedroom Blind", "esp-blind");
|
|
blind->setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
|
|
|
|
blind->addCapability(AlexaInterfaces::RangeController, LIFT)
|
|
->addFriendlyAsset(AlexaAssets::Setting_Opening)
|
|
.setConfiguration(liftConfiguration)
|
|
.addActionMapping(ActionMapping({AlexaAction::Close}, "SetRangeValue", "{\"rangeValue\":0}"))
|
|
.addActionMapping(ActionMapping({AlexaAction::Open}, "SetRangeValue", "{\"rangeValue\":100}"))
|
|
.addActionMapping(ActionMapping({AlexaAction::Lower}, "AdjustRangeValue",
|
|
"{\"rangeValueDelta\":-10,\"rangeValueDeltaDefault\":false}"))
|
|
.addActionMapping(ActionMapping({AlexaAction::Raise}, "AdjustRangeValue",
|
|
"{\"rangeValueDelta\":10,\"rangeValueDeltaDefault\":false}"))
|
|
.addStateMapping({AlexaState::Closed}, 0)
|
|
.addStateMapping({AlexaState::Open}, 1, 100);
|
|
|
|
blind->addCapability(AlexaInterfaces::EndpointHealth);
|
|
blind->onDirective(onDirective);
|
|
}
|
|
|
|
void loop()
|
|
{
|
|
alexa.loop();
|
|
}
|