Updating ProgrammableHub2 Example

This commit is contained in:
Gregg 2022-07-28 17:13:04 -05:00
parent be3af3b14e
commit 689ea86991
2 changed files with 102 additions and 169 deletions

View File

@ -30,27 +30,13 @@
// HomeSpan: A HomeKit implementation for the ESP32 // // HomeSpan: A HomeKit implementation for the ESP32 //
// ------------------------------------------------ // // ------------------------------------------------ //
// // // //
// Example 20: Demonstrates various advance HomeSpan functions //
// by implementing a Bridge in which one or more //
// Lightbulb Accessories can be added and deleted //
// *dynamically* without needing to restart the //
// device //
// // // //
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
#include "HomeSpan.h" #include "HomeSpan.h"
// In Example 20 we will implement a bridge device supporting up to 10 Lightbulb Accessories. However, rather than pre-specifying the number of Lights, we #define MAX_LIGHTS 10
// will allow Light Accessories to be added and deleted dynamically by the user via the CLI. Changes are reflected in the Home App without the need to restart #define MAX_NAME_LENGTH 10
// the device! Note this example uses a variety of advanced HomeSpan functions, as well as some detailed features of both the ESP32-IDF and C++ that have not been used
// in any of the previous examples.
// We will use a C++ array with 10 elements containing integers representing the Light "ID" of each Lightbulb Accessory implemented. An ID of zero means there is no
// Light defined in that element.
#include <array> // include the C++ standard library array container
#define MAX_LIGHTS 10
enum colorType_t : uint8_t{ enum colorType_t : uint8_t{
NO_COLOR, NO_COLOR,
@ -59,26 +45,17 @@ enum colorType_t : uint8_t{
}; };
struct lightData_t { struct lightData_t {
char name[33]=""; char name[MAX_NAME_LENGTH+1]="";
union { union {
struct { struct {
boolean isConfigured:1; boolean isConfigured:1;
boolean isdimmable:1; boolean isDimmable:1;
colorType_t colorType:2; colorType_t colorType:2;
}; };
uint8_t val=0; uint8_t val=0;
}; };
}; } lightData[MAX_LIGHTS];
std::array<lightData_t,MAX_LIGHTS> lightData;
std::array<int,10> lights; // declare "lights" to be an array of 10 integers
using std::fill; // place the std library function fill, remove, and find, into the global namespace so we can use them below without prefacing with "std::"
using std::remove;
using std::find;
// We will use non-volatile storage (NVS) to store the lights array so that the device can restore the current configuration upon rebooting
nvs_handle savedData; // declare savdData as a handle to be used with the NVS (see the ESP32-IDF for details on how to use NVS storage) nvs_handle savedData; // declare savdData as a handle to be used with the NVS (see the ESP32-IDF for details on how to use NVS storage)
@ -88,170 +65,130 @@ void setup() {
Serial.begin(115200); Serial.begin(115200);
fill(lights.begin(),lights.end(),0); // initialize lights array with zeros in each of the 10 elements (no Light Accessories defined)
size_t len; size_t len;
nvs_open("SAVED_DATA",NVS_READWRITE,&savedData); // open a new namespace called SAVED_DATA in the NVS nvs_open("SAVED_DATA",NVS_READWRITE,&savedData); // open a new namespace called SAVED_DATA in the NVS
if(!nvs_get_blob(savedData,"LIGHTS",NULL,&len)) // if LIGHTS data found
nvs_get_blob(savedData,"LIGHTS",&lights,&len); // retrieve data if(!nvs_get_blob(savedData,"LIGHTDATA",NULL,&len)) // if LIGHTDATA data found
nvs_get_blob(savedData,"LIGHTDATA",&lightData,&len); // retrieve data
homeSpan.setLogLevel(1); homeSpan.setLogLevel(1);
homeSpan.begin(Category::Lighting,"HomeSpan Lights"); homeSpan.begin(Category::Lighting,"HomeSpan Light Hub");
// We begin by creating the Bridge Accessory
new SpanAccessory(1); // here we specified the AID=1 for clarity (it would default to 1 anyway if left blank) new SpanAccessory(1); // here we specified the AID=1 for clarity (it would default to 1 anyway if left blank)
new Service::AccessoryInformation(); new Service::AccessoryInformation();
new Characteristic::Identify(); new Characteristic::Identify();
new Characteristic::Model("HomeSpan Dynamic Bridge"); // defining the Model is optional new Characteristic::Model("HomeSpan Programmable Hub");
// Now we create Light Accessories based on what is recorded in the lights array for(int i=0;i<MAX_LIGHTS;i++){ // create Light Accessories based on saved data
// We'll use C++ iterators to loop over all elements until we reach the end of the array, or find an element with a value of zero if(lightData[i].isConfigured)
addLight(i,lightData[i].name,lightData[i].isDimmable,lightData[i].colorType);
for(auto it=lights.begin(); it!=lights.end() && *it!=0; it++) // loop over all elements (stopping when we get to the end, or hit an element with a value of zero)
addLight(*it); // call addLight (defined further below) with an argument equal to the integer stored in that element
for(auto it=lightData.begin(); it!=lightData.end(); it++){ // loop over all elements in lightData array
if((*it).isConfigured);
} }
new SpanUserCommand('a',"<name> - add non-dimmable light accessory using name=<name>",[](const char *c){addLight(-1,c+1,false,NO_COLOR);});
new SpanUserCommand('A',"<name> - add dimmable light accessory using name=<name>",[](const char *c){addLight(-1,c+1,true,NO_COLOR);});
new SpanUserCommand('t',"<name> - add non-dimmable light accessory with color-temperature control using name=<name>",[](const char *c){addLight(-1,c+1,false,TEMPERATURE_ONLY);});
new SpanUserCommand('T',"<name> - add dimmable light accessory with color-temperature control using name=<name>",[](const char *c){addLight(-1,c+1,true,TEMPERATURE_ONLY);});
new SpanUserCommand('r',"<name> - add non-dimmable light accessory with full RGB color control using name=<name>",[](const char *c){addLight(-1,c+1,false,FULL_RGB);});
new SpanUserCommand('R',"<name> - add dimmable light accessory with full RGB color control using name=<name>",[](const char *c){addLight(-1,c+1,true,FULL_RGB);});
// Next we create user-defined CLI commands so we can add Light Accessories of different types from the CLI. new SpanUserCommand('l'," - list all light accessories",listAccessories);
// We'll use inline lambda functions each calling addLight() with different parameters. new SpanUserCommand('d',"<index> - delete a light accessory with index=<index>",deleteAccessory);
new SpanUserCommand('a',"<name> - add non-dimmable light accessory using name=<name>",[](const char *c){addLight(c+1,false,NO_COLOR);});
new SpanUserCommand('A',"<name> - add dimmable light accessory using name=<name>",[](const char *c){addLight(c+1,true,NO_COLOR);});
new SpanUserCommand('t',"<name> - add non-dimmable light accessory with color-temperature control using name=<name>",[](const char *c){addLight(c+1,false,TEMPERATURE_ONLY);});
new SpanUserCommand('T',"<name> - add dimmable light accessory with color-temperature control using name=<name>",[](const char *c){addLight(c+1,true,TEMPERATURE_ONLY);});
new SpanUserCommand('r',"<name> - add non-dimmable light accessory with full RGB color control using name=<name>",[](const char *c){addLight(c+1,false,FULL_RGB);});
new SpanUserCommand('R',"<name> - add dimmable light accessory with full RGB color control using name=<name>",[](const char *c){addLight(c+1,true,FULL_RGB);});
new SpanUserCommand('d',"<num> - delete a light accessory with id=<num>",deleteAccessory);
new SpanUserCommand('D'," - delete ALL light accessories",deleteAllAccessories); new SpanUserCommand('D'," - delete ALL light accessories",deleteAllAccessories);
new SpanUserCommand('u',"- update accessories database",updateAccessories); new SpanUserCommand('u',"- update accessories database",updateAccessories);
// Finally we call autoPoll to start polling the background. Note this is purely optional and only used here to illustrate how to
// use autoPoll - you could instead have called the usual homeSpan.poll() function by including it inside the Arduino loop() function
homeSpan.autoPoll(); homeSpan.autoPoll();
} // end of setup() } // end of setup()
// Usually the Arduino loop() function would be defined somewhere here. But since we used autoPoll in the setup() function,
// we don't have to define the loop() function at all in this sketch! Why don't we get an error? Because HomeSpan includes
// a default loop() function, which prevents the compiler from complaining about loop() being undefined.
/////////////////////////// ///////////////////////////
// This function creates a new Light Accessory. It is called initially in setup() above to create Light Accessories based void addLight(int index, const char *name, boolean isDimmable, colorType_t colorType){
// on what was stored in the lights array. It is also called in response to adding lights via the CLI, which dynamically
// adds a new Light Accessory while the device is running.
void addLight(const char *name, boolean isDimmable, colorType_t colorType){ if(index<0){
for(index=0;index<MAX_LIGHTS && lightData[index].isConfigured;index++);
if(index==MAX_LIGHTS){
Serial.printf("Can't add Light Accessory - maximum number of %d are already defined.\n",MAX_LIGHTS);
return;
}
while(*name==' ') // strip any leading spaces int n=strncpy_trim(lightData[index].name,name,sizeof(lightData[index].name));
name++;
Serial.printf("Adding '%s' Light Accessory: %s with %s control\n",name,isDimmable?"dimmable":"non-dimmable",colorType==NO_COLOR?"no color":(colorType==TEMPERATURE_ONLY?"color-temperature":"full RGB color")); if(n==1){
// Serial.printf("Can't add Light Accessory without a name specified.\n");
// new SpanAccessory(n+1); // IMPORTANT: add 1, since first Accessory with AID=1 is already used by the Bridge Accessory return;
// new Service::AccessoryInformation(); }
// new Characteristic::Identify();
// new Characteristic::Name(name);
// new Characteristic::SerialNumber(sNum);
// new Service::LightBulb();
// new Characteristic::On(0,true);
}
/////////////////////////// if(n>sizeof(lightData[index].name))
Serial.printf("Warning - name trimmed to max length of %d characters.\n",MAX_NAME_LENGTH);
// This function creates a new Light Accessory with n as the "ID". lightData[index].isDimmable=isDimmable;
// It is called initially in setup() above to create Light Accessories based lightData[index].colorType=colorType;
// on what was stored in the lights array. It is also called in response to lightData[index].isConfigured=true;
// typing 'a' into the CLI (see below), which dynamically adds a new Light Accessory name=lightData[index].name;
// while the device is running.
void addLight(int n){ nvs_set_blob(savedData,"LIGHTDATA",&lightData,sizeof(lightData)); // update data in the NVS
nvs_commit(savedData);
}
char name[32]; Serial.printf("Adding Light Accessory: Name='%s' Dimmable=%s Color=%s\n",name,isDimmable?"YES":"NO",colorType==NO_COLOR?"NONE":(colorType==TEMPERATURE_ONLY?"TEMPERATURE_ONLY":"FULL_RGB"));
sprintf(name,"Light-%d",n); // create the name of the device using the specified "ID"
char sNum[32];
sprintf(sNum,"%0.10d",n); // create serial number from the ID - this is helpful in case we rename the Light to something else using the Home App
Serial.printf("Adding Accessory: %s\n",name); new SpanAccessory(index+2); // IMPORTANT: add 2, since first Accessory with AID=1 is already used by the Bridge Accessory
new SpanAccessory(n+1); // IMPORTANT: add 1, since first Accessory with AID=1 is already used by the Bridge Accessory
new Service::AccessoryInformation(); new Service::AccessoryInformation();
new Characteristic::Identify(); new Characteristic::Identify();
new Characteristic::Name(name); new Characteristic::Name(name);
new Characteristic::SerialNumber(sNum);
new Service::LightBulb(); new Service::LightBulb();
new Characteristic::On(0,true); new Characteristic::On(0,true);
if(isDimmable)
new Characteristic::Brightness(100,true);
if(colorType==TEMPERATURE_ONLY)
new Characteristic::ColorTemperature(200,true);
if(colorType==FULL_RGB){
new Characteristic::Hue(0,true);
new Characteristic::Saturation(0,true);
}
} }
/////////////////////////// ///////////////////////////
// This function is called in response to typing '@a <num>' into the CLI. size_t strncpy_trim(char *dest, const char *src, size_t dSize){
// It adds a new Light Accessory with ID=num, by calling addLight(num) above.
void addAccessory(const char *buf){ while(*src==' ') // skip over any leading spaces
src++;
int n=atoi(buf+1); // read the value of <num> specified size_t sLen=strlen(src); // string length of src after skipping over leading spaces
while(sLen>0 && src[sLen-1]==' ') // shorten length to remove trailing spaces
sLen--;
if(n<1){ // ensure <num> is greater than 0 size_t sSize=sLen+1; // add room for null terminator
Serial.printf("Invalid Accessory number!\n");
return;
}
if(find(lights.begin(),lights.end(),n)!=lights.end()){ // search for this ID in the existing lights array - if found, report an error and return if(dest!=NULL)
Serial.printf("Accessory Light-%d already implemented!\n",n); *stpncpy(dest,src,(dSize<sSize?dSize:sSize)-1)='\0';
return;
}
auto it=find(lights.begin(),lights.end(),0); // find the next "free" element in the light array (the first element with a value of zero) return(sSize); // return total size needed for entire trimmed string, including null terminator
if(it==lights.end()){ // if there were no elements with a zero, the array is full and no new Lights can be added
Serial.printf("Can't add any more lights - max is %d!\n",lights.size());
return;
}
*it=n; // save light number
nvs_set_blob(savedData,"LIGHTS",&lights,sizeof(lights)); // update data in the NVS
nvs_commit(savedData);
addLight(n); // add light accessory by calling the function above!
} }
/////////////////////////// ///////////////////////////
// This function deletes an existing Light Accessory and is called
// in response to typing '@d <num>' into the CLI.
void deleteAccessory(const char *buf){ void deleteAccessory(const char *buf){
int n=atoi(buf+1); // same as above, we read the specified <num> and check that it is valid (i.e. greater than 0) int n=atoi(buf+1);
if(n<1){ if(n<1 || n>MAX_LIGHTS){
Serial.printf("Invalid Accessory number!\n"); Serial.printf("Invalid Light Accessory index - must be between 1 and %d.\n",MAX_LIGHTS);
return; return;
} }
// Below we use the homeSpan method deleteAccessory(aid) to completely delete the Accessory with AID=n+1.
// We add 1 because the AID of the first Light Accessory is 2, since the Bridge Accessory has an AID of 1.
// The deleteAccessory() method returns true if an Accessory with matching AID is found, otherwise it returns false.
// When deleting an Accessory, HomeSpan will print a delete message for every Service, Characteristic, loop() method,
// button() method, and SpanButton, associated with that Accessory. These are Level-1 Log messages, so you'll need
// to have the Log Level in the sketch set to 1 or 2 to receive the output.
if(homeSpan.deleteAccessory(n+1)){ // if deleteAccessory() is true, a match has been found if(homeSpan.deleteAccessory(n+1)){ // if deleteAccessory() is true, a match has been found
Serial.printf("Deleting Accessory: Light-%d\n",n); Serial.printf("Deleting Accessory: Light-%d\n",n);
fill(remove(lights.begin(),lights.end(),n),lights.end(),0); // remove entry from lights array and fill any undefined elements with zero lightData[n].isConfigured=false;
nvs_set_blob(savedData,"LIGHTS",&lights,sizeof(lights)); // update data in the NVS nvs_set_blob(savedData,"LIGHTDATA",&lightData,sizeof(lightData)); // update data in the NVS
nvs_commit(savedData); nvs_commit(savedData);
} else { } else {
Serial.printf("No such Accessory: Light-%d\n",n); Serial.printf("Nothing to delete - there is no Light Accessory at index=%d.\n",n);
} }
} }
@ -259,19 +196,13 @@ void deleteAccessory(const char *buf){
void deleteAllAccessories(const char *buf){ void deleteAllAccessories(const char *buf){
// This function is called in response to typing '@D' into the CLI. for(int i=0;i<MAX_LIGHTS;i++){
// It deletes all Light Accessories if(lightData[i].isConfigured){
lightData[i].isConfigured=false;
if(lights[0]==0){ // first check that there is at least one Light Accessory by checking for a non-zero ID in lights[0] }
Serial.printf("There are no Light Accessories to delete!\n");
return;
} }
for(auto it=lights.begin(); it!=lights.end() && *it!=0; it++) // use an iterator to loop over all non-zero elements in the lights array... nvs_set_blob(savedData,"LIGHTDATA",&lightData,sizeof(lightData)); // update data in the NVS
homeSpan.deleteAccessory(*it+1); // ... and delete the matching Light Accessory (don't forgot to add 1 to the Light ID to form the AID)
fill(lights.begin(),lights.end(),0); // zero out all the elements in the lights array, since all Light Accessories have been deleted
nvs_set_blob(savedData,"LIGHTS",&lights,sizeof(lights)); // update data in the NVS
nvs_commit(savedData); nvs_commit(savedData);
Serial.printf("All Light Accessories deleted!\n"); Serial.printf("All Light Accessories deleted!\n");
@ -279,23 +210,8 @@ void deleteAllAccessories(const char *buf){
/////////////////////////// ///////////////////////////
// Lastly we have the all-important updateAccessories function.
// This is called in response to typing '@u' into the CLI.
// Though the above functions can be used to add and delete Light Accessories
// dyammically, Controllers such as the Home App that are already connected to
// the device don't yet know additional Light Accessories have been added to (or
// deleted from) the overall Accessories datase. To let them know, HomeSpan needs
// to increment the HAP Configuration Number and re-broadcast it via MDNS so all
// connected Controllers are aware that they need to request a refresh from the device.
// When you type '@u' into the CLI, you should see a lot of activity between the device
// and any connected Controllers as they request a refresh. Be patient - it can take up to a
// minute for changes to be properly reflected in the Home App on your iPhone or Mac.
void updateAccessories(const char *buf){ void updateAccessories(const char *buf){
// note the updateDatabase() method returns true if the database has indeed changed (e.g. one or more new Light Accessories were added), or false if nothing has changed
if(homeSpan.updateDatabase()) if(homeSpan.updateDatabase())
Serial.printf("Accessories Database updated. New configuration number broadcasted...\n"); Serial.printf("Accessories Database updated. New configuration number broadcasted...\n");
else else
@ -303,3 +219,20 @@ void updateAccessories(const char *buf){
} }
/////////////////////////// ///////////////////////////
void listAccessories(const char *buf){
Serial.printf("\nIndex Dimmable Color Name\n");
Serial.printf("----- -------- ----- ");
for(int i=0;i<MAX_NAME_LENGTH;i++)
Serial.printf("-");
Serial.printf("\n");
for(int i=0;i<MAX_LIGHTS;i++){
if(lightData[i].isConfigured)
Serial.printf("%5d %8s %5s %-s\n",i+1,lightData[i].isDimmable?"YES":"NO",lightData[i].colorType==NO_COLOR?"NONE":(lightData[i].colorType==TEMPERATURE_ONLY?"TEMP":"RGB"),lightData[i].name);
}
}
///////////////////////////

View File

@ -208,10 +208,10 @@ void Span::pollTask() {
checkConnect(); checkConnect();
} }
char cBuf[17]="?"; char cBuf[65]="?";
if(Serial.available()){ if(Serial.available()){
readSerial(cBuf,16); readSerial(cBuf,64);
processSerialCommand(cBuf); processSerialCommand(cBuf);
} }