Examples as Arduino sketches: one folder per device type, each its own endpointId, Wi-Fi with a timeout
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>
This commit is contained in:
parent
7369883479
commit
69b4d59896
15 changed files with 2061 additions and 1 deletions
319
examples/Thermostat/Thermostat.ino
Normal file
319
examples/Thermostat/Thermostat.ino
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
// Thermostat: heating and cooling with a setpoint (Alexa.ThermostatController, Alexa.TemperatureSensor).
|
||||
//
|
||||
// Modes HEAT, COOL, AUTO and OFF. HEAT and COOL have one setpoint; AUTO has two, it heats below the lower and
|
||||
// cools above the upper one. Setpoints are 10 to 30 degrees Celsius and, in AUTO, at least 2 degrees apart.
|
||||
// A directive in Fahrenheit or Kelvin is converted; the thermostat reports in Celsius.
|
||||
//
|
||||
// What is refused, and with which error:
|
||||
// a setpoint outside of 10 to 30 degrees TEMPERATURE_VALUE_OUT_OF_RANGE, with the range
|
||||
// two setpoints closer than 2 degrees REQUESTED_SETPOINTS_TOO_CLOSE, with the distance
|
||||
// two setpoints in HEAT or COOL DUAL_SETPOINTS_UNSUPPORTED
|
||||
// a setpoint while the thermostat is OFF THERMOSTAT_IS_OFF
|
||||
// a mode other than the four UNSUPPORTED_THERMOSTAT_MODE
|
||||
//
|
||||
// The sensor is a TMP36 on A0 of a Wemos D1 mini (A0 takes 0 to 3.2 V); two outputs switch the heating and the
|
||||
// cooling.
|
||||
#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 uint8_t HEAT_PIN = 5; // D1 on a Wemos D1 mini, high = on
|
||||
const uint8_t COOL_PIN = 4; // D2
|
||||
|
||||
const float LOWEST = 10; // degrees Celsius
|
||||
const float HIGHEST = 30;
|
||||
const float DISTANCE = 2; // between the two setpoints of AUTO
|
||||
const float HYSTERESIS = 0.5f;
|
||||
|
||||
Alex2ESP alexa;
|
||||
AlexaThermostatMode mode = AlexaThermostatMode::HEAT;
|
||||
float target = 21; // HEAT and COOL
|
||||
float lower = 19; // AUTO
|
||||
float upper = 24;
|
||||
float temperature = 0;
|
||||
unsigned long lastRead = 0;
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
// TMP36: 0.5 V at 0 degrees Celsius, 10 mV per degree
|
||||
float readTemperature()
|
||||
{
|
||||
float volts = analogRead(A0) * 3.2f / 1023;
|
||||
return (volts - 0.5f) * 100;
|
||||
}
|
||||
|
||||
// A temperature of a directive, {"value": 70, "scale": "FAHRENHEIT"}, in degrees Celsius
|
||||
float toCelsius(JsonVariantConst temperature)
|
||||
{
|
||||
float value = temperature["value"] | 0.0f;
|
||||
const char *scale = temperature["scale"] | "CELSIUS";
|
||||
if (strcmp(scale, "FAHRENHEIT") == 0)
|
||||
{
|
||||
return (value - 32) * 5 / 9;
|
||||
}
|
||||
return strcmp(scale, "KELVIN") == 0 ? value - 273.15f : value;
|
||||
}
|
||||
|
||||
// A difference between two temperatures: one degree Fahrenheit is 5/9 of a degree Celsius
|
||||
float differenceInCelsius(JsonVariantConst difference)
|
||||
{
|
||||
float value = difference["value"] | 0.0f;
|
||||
return strcmp(difference["scale"] | "CELSIUS", "FAHRENHEIT") == 0 ? value * 5 / 9 : value;
|
||||
}
|
||||
|
||||
void setCelsius(JsonObject temperature, float value)
|
||||
{
|
||||
temperature["value"] = value;
|
||||
temperature["scale"] = "CELSIUS";
|
||||
}
|
||||
|
||||
bool withinRange(float setpoint)
|
||||
{
|
||||
return setpoint >= LOWEST && setpoint <= HIGHEST;
|
||||
}
|
||||
|
||||
void sendOutOfRange(AlexaDirective &directive)
|
||||
{
|
||||
AlexaStatusMessage error = directive.error(AlexaErrorType::TEMPERATURE_VALUE_OUT_OF_RANGE, "10 to 30 degrees Celsius");
|
||||
JsonObject range = error.payload()["validRange"].to<JsonObject>();
|
||||
setCelsius(range["minimumValue"].to<JsonObject>(), LOWEST);
|
||||
setCelsius(range["maximumValue"].to<JsonObject>(), HIGHEST);
|
||||
error.send();
|
||||
}
|
||||
|
||||
// The state of the thermostat: what the answer to a directive and the answer to ReportState carry
|
||||
void sendState(AlexaStatusMessage message)
|
||||
{
|
||||
message.addHealthProp(EndpointHealth::OK)
|
||||
.addTemperatureSensorProp(temperature, TemperatureSensorScale::CELSIUS, millis() - lastRead)
|
||||
.addThermostatModeProp(mode);
|
||||
if (mode == AlexaThermostatMode::AUTO)
|
||||
{
|
||||
message.addThermostatDualSetpointProp(lower, upper);
|
||||
}
|
||||
else
|
||||
{
|
||||
message.addThermostatSetpointProp(target);
|
||||
}
|
||||
message.send();
|
||||
}
|
||||
|
||||
// Takes the setpoints when the thermostat can use them and returns true; sends the error and returns false when not.
|
||||
// One setpoint in AUTO moves both and keeps their distance.
|
||||
bool setSetpoints(AlexaDirective &directive, bool two, float first, float second)
|
||||
{
|
||||
if (mode == AlexaThermostatMode::OFF)
|
||||
{
|
||||
directive.error(AlexaErrorType::THERMOSTAT_IS_OFF, "Set a mode first").send();
|
||||
return false;
|
||||
}
|
||||
if (two && mode != AlexaThermostatMode::AUTO)
|
||||
{
|
||||
directive.error(AlexaErrorType::DUAL_SETPOINTS_UNSUPPORTED, "Two setpoints in AUTO only").send();
|
||||
return false;
|
||||
}
|
||||
if (!two && mode == AlexaThermostatMode::AUTO)
|
||||
{
|
||||
float half = (upper - lower) / 2;
|
||||
second = first + half;
|
||||
first = first - half;
|
||||
}
|
||||
|
||||
if (mode != AlexaThermostatMode::AUTO)
|
||||
{
|
||||
if (!withinRange(first))
|
||||
{
|
||||
sendOutOfRange(directive);
|
||||
return false;
|
||||
}
|
||||
target = first;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!withinRange(first) || !withinRange(second))
|
||||
{
|
||||
sendOutOfRange(directive);
|
||||
return false;
|
||||
}
|
||||
if (second - first < DISTANCE)
|
||||
{
|
||||
AlexaStatusMessage error = directive.error(AlexaErrorType::REQUESTED_SETPOINTS_TOO_CLOSE, "At least 2 degrees apart");
|
||||
setCelsius(error.payload()["minimumTemperatureDelta"].to<JsonObject>(), DISTANCE);
|
||||
error.send();
|
||||
return false;
|
||||
}
|
||||
lower = first;
|
||||
upper = second;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setMode(const char *name)
|
||||
{
|
||||
if (strcmp(name, "HEAT") == 0)
|
||||
{
|
||||
mode = AlexaThermostatMode::HEAT;
|
||||
}
|
||||
else if (strcmp(name, "COOL") == 0)
|
||||
{
|
||||
mode = AlexaThermostatMode::COOL;
|
||||
}
|
||||
else if (strcmp(name, "AUTO") == 0)
|
||||
{
|
||||
mode = AlexaThermostatMode::AUTO;
|
||||
}
|
||||
else if (strcmp(name, "OFF") == 0)
|
||||
{
|
||||
mode = AlexaThermostatMode::OFF;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void onDirective(AlexaDirective &directive)
|
||||
{
|
||||
if (directive.isReportState())
|
||||
{
|
||||
sendState(directive.stateReport());
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObjectConst payload = directive.payload;
|
||||
if (directive.is("SetTargetTemperature"))
|
||||
{
|
||||
bool two = !payload["lowerSetpoint"].isNull() && !payload["upperSetpoint"].isNull();
|
||||
bool taken = two ? setSetpoints(directive, true, toCelsius(payload["lowerSetpoint"]), toCelsius(payload["upperSetpoint"]))
|
||||
: setSetpoints(directive, false, toCelsius(payload["targetSetpoint"]), 0);
|
||||
if (!taken)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (directive.is("AdjustTargetTemperature"))
|
||||
{
|
||||
float centre = mode == AlexaThermostatMode::AUTO ? (lower + upper) / 2 : target;
|
||||
if (!setSetpoints(directive, false, centre + differenceInCelsius(payload["targetSetpointDelta"]), 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (directive.is("SetThermostatMode"))
|
||||
{
|
||||
if (!setMode(payload["thermostatMode"]["value"] | ""))
|
||||
{
|
||||
directive.error(AlexaErrorType::UNSUPPORTED_THERMOSTAT_MODE, "HEAT, COOL, AUTO or OFF").send();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This thermostat takes setpoints and modes").send();
|
||||
return;
|
||||
}
|
||||
|
||||
sendState(directive.response());
|
||||
}
|
||||
|
||||
// The "configuration" of the capability in discovery
|
||||
void thermostatConfiguration(JsonObject configuration, void *)
|
||||
{
|
||||
JsonArray modes = configuration["supportedModes"].to<JsonArray>();
|
||||
modes.add("HEAT");
|
||||
modes.add("COOL");
|
||||
modes.add("AUTO");
|
||||
modes.add("OFF");
|
||||
configuration["supportsScheduling"] = false;
|
||||
}
|
||||
|
||||
// Switches on below the setpoint and off above it, with half a degree between the two
|
||||
void control()
|
||||
{
|
||||
bool heats = mode == AlexaThermostatMode::HEAT || mode == AlexaThermostatMode::AUTO;
|
||||
bool cools = mode == AlexaThermostatMode::COOL || mode == AlexaThermostatMode::AUTO;
|
||||
float heatBelow = mode == AlexaThermostatMode::AUTO ? lower : target;
|
||||
float coolAbove = mode == AlexaThermostatMode::AUTO ? upper : target;
|
||||
|
||||
if (!heats || temperature >= heatBelow + HYSTERESIS)
|
||||
{
|
||||
digitalWrite(HEAT_PIN, LOW);
|
||||
}
|
||||
else if (temperature <= heatBelow - HYSTERESIS)
|
||||
{
|
||||
digitalWrite(HEAT_PIN, HIGH);
|
||||
}
|
||||
|
||||
if (!cools || temperature <= coolAbove - HYSTERESIS)
|
||||
{
|
||||
digitalWrite(COOL_PIN, LOW);
|
||||
}
|
||||
else if (temperature >= coolAbove + HYSTERESIS)
|
||||
{
|
||||
digitalWrite(COOL_PIN, HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(74880);
|
||||
pinMode(HEAT_PIN, OUTPUT);
|
||||
pinMode(COOL_PIN, OUTPUT);
|
||||
temperature = readTemperature();
|
||||
|
||||
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 *thermostat = alexa.getDevice("Hallway Thermostat", "esp-thermostat");
|
||||
thermostat->setDisplayCategory(DisplayCategory::THERMOSTAT);
|
||||
thermostat->addCapability(AlexaInterfaces::ThermostatController)->setConfiguration(thermostatConfiguration);
|
||||
thermostat->addCapability(AlexaInterfaces::TemperatureSensor);
|
||||
thermostat->addCapability(AlexaInterfaces::EndpointHealth);
|
||||
thermostat->onDirective(onDirective);
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
alexa.loop();
|
||||
|
||||
if (millis() - lastRead >= 2000)
|
||||
{
|
||||
lastRead = millis();
|
||||
temperature = readTemperature();
|
||||
control();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue