forked from HomeSpan/HomeSpan
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ProgrammableHub.ino
388 lines (282 loc) · 15.2 KB
/
ProgrammableHub.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
/*********************************************************************************
* MIT License
*
* Copyright (c) 2022 Gregg E. Berman
*
* https://github.com/HomeSpan/HomeSpan
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
********************************************************************************/
//////////////////////////////////////////////////////////////////
// //
// HomeSpan: A HomeKit implementation for the ESP32 //
// ------------------------------------------------ //
// //
// Demonstrates how to implement a Web Server alongside //
// of HomeSpan to create a Programmable Hub serving up to //
// 12 Configurable Lights. Allows for dynamic changes //
// to Accessories without needing to reboot //
// //
//////////////////////////////////////////////////////////////////
#include "HomeSpan.h"
#include <WebServer.h> // include WebServer library
WebServer webServer(80); // create WebServer on port 80
#define MAX_LIGHTS 12
#define MAX_NAME_LENGTH 32
#define HUB_NAME "lighthub"
enum colorType_t : uint8_t {
NO_COLOR,
TEMPERATURE_ONLY,
FULL_RGB
};
uint32_t aidStore=2; // keep track of unique AID numbers - start with AID=2
struct lightData_t {
char name[MAX_NAME_LENGTH+1]="";
uint32_t aid=0;
boolean isDimmable:1;
colorType_t colorType:2;
} lightData[MAX_LIGHTS];
nvs_handle savedData;
//////////////////////////////////////
void setup() {
Serial.begin(115200);
size_t len;
nvs_open("SAVED_DATA",NVS_READWRITE,&savedData); // open a new namespace called SAVED_DATA in the NVS
if(!nvs_get_blob(savedData,"LIGHTDATA",NULL,&len)) // if LIGHTDATA data found
nvs_get_blob(savedData,"LIGHTDATA",&lightData,&len); // retrieve data
nvs_get_u32(savedData,"AID",&aidStore); // get AID, if it exists
homeSpan.setLogLevel(1);
homeSpan.setHostNameSuffix(""); // use null string for suffix (rather than the HomeSpan device ID)
homeSpan.setPortNum(1201); // change port number for HomeSpan so we can use port 80 for the Web Server
homeSpan.setWifiCallback(setupWeb); // need to start Web Server after WiFi is established
homeSpan.begin(Category::Lighting,"HomeSpan Light Hub",HUB_NAME);
new SpanAccessory(1); // here we specified the AID=1 for clarity (it would default to 1 anyway if left blank)
new Service::AccessoryInformation();
new Characteristic::Identify();
new Characteristic::Model("HomeSpan Programmable Hub");
new Characteristic::AccessoryFlags();
for(int i=0;i<MAX_LIGHTS;i++){ // create Light Accessories based on saved data
if(lightData[i].aid)
addLight(i);
}
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('l'," - list all light accessories",listAccessories);
new SpanUserCommand('d',"<index> - delete a light accessory with index=<index>",[](const char *buf){deleteAccessory(atoi(buf+1));});
new SpanUserCommand('D'," - delete ALL light accessories",deleteAllAccessories);
new SpanUserCommand('u',"- update accessories database",updateAccessories);
} // end of setup()
///////////////////////////
void loop(){
homeSpan.poll();
webServer.handleClient(); // handle incoming web server traffic
}
///////////////////////////
void addLight(int index){
Serial.printf("Adding Light Accessory: Name='%s' Dimmable=%s Color=%s\n",
lightData[index].name,lightData[index].isDimmable?"YES":"NO",lightData[index].colorType==NO_COLOR?"NONE":(lightData[index].colorType==TEMPERATURE_ONLY?"TEMPERATURE_ONLY":"FULL_RGB"));
new SpanAccessory(lightData[index].aid);
new Service::AccessoryInformation();
new Characteristic::Identify();
new Characteristic::Name(lightData[index].name);
char sNum[32];
sprintf(sNum,"Light-%02d",index);
new Characteristic::SerialNumber(sNum);
new Service::LightBulb();
new Characteristic::On(0,true);
if(lightData[index].isDimmable)
new Characteristic::Brightness(100,true);
if(lightData[index].colorType==TEMPERATURE_ONLY)
new Characteristic::ColorTemperature(200,true);
if(lightData[index].colorType==FULL_RGB){
new Characteristic::Hue(0,true);
new Characteristic::Saturation(0,true);
}
}
///////////////////////////
int addLight(const char *name, boolean isDimmable, colorType_t colorType){
int index=0;
for(index=0;index<MAX_LIGHTS && lightData[index].aid;index++);
if(index==MAX_LIGHTS){
Serial.printf("Can't add Light Accessory - maximum number of %d are already defined.\n",MAX_LIGHTS);
return(-1);
}
int n=strncpy_trim(lightData[index].name,name,sizeof(lightData[index].name));
if(n==1){
Serial.printf("Can't add Light Accessory without a name specified.\n");
return(-1);
}
if(n>sizeof(lightData[index].name))
Serial.printf("Warning - name trimmed to max length of %d characters.\n",MAX_NAME_LENGTH);
lightData[index].isDimmable=isDimmable;
lightData[index].colorType=colorType;
lightData[index].aid=aidStore++;
nvs_set_blob(savedData,"LIGHTDATA",&lightData,sizeof(lightData)); // update data in the NVS
nvs_set_u32(savedData,"AID",aidStore);
nvs_commit(savedData);
addLight(index);
return(index);
}
///////////////////////////
size_t strncpy_trim(char *dest, const char *src, size_t dSize){
while(*src==' ') // skip over any leading spaces
src++;
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--;
size_t sSize=sLen+1; // add room for null terminator
if(dest!=NULL)
*stpncpy(dest,src,(dSize<sSize?dSize:sSize)-1)='\0';
return(sSize); // return total size needed for entire trimmed string, including null terminator
}
///////////////////////////
void deleteAccessory(int index){
if(index<0 || index>=MAX_LIGHTS){
Serial.printf("Invalid Light Accessory index - must be between 0 and %d.\n",MAX_LIGHTS-1);
return;
}
if(homeSpan.deleteAccessory(lightData[index].aid)){ // if deleteAccessory() is true, a match has been found
Serial.printf("Deleting Light Accessory: Name='%s'\n",lightData[index].name);
lightData[index].aid=0;
nvs_set_blob(savedData,"LIGHTDATA",&lightData,sizeof(lightData)); // update data in the NVS
nvs_commit(savedData);
} else {
Serial.printf("Nothing to delete - there is no Light Accessory at index=%d.\n",index);
}
}
///////////////////////////
void deleteAllAccessories(const char *buf){
for(int i=0;i<MAX_LIGHTS;i++){
homeSpan.deleteAccessory(lightData[i].aid);
lightData[i].aid=0;
}
nvs_set_blob(savedData,"LIGHTDATA",&lightData,sizeof(lightData)); // update data in the NVS
nvs_commit(savedData);
Serial.printf("All Light Accessories deleted!\n");
}
///////////////////////////
void updateAccessories(const char *buf){
if(homeSpan.updateDatabase())
Serial.printf("Accessories Database updated. New configuration number broadcasted...\n");
else
Serial.printf("Nothing to update - no changes were made!\n");
}
///////////////////////////
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].aid)
Serial.printf("%5d %8s %5s %-s\n",i,lightData[i].isDimmable?"YES":"NO",lightData[i].colorType==NO_COLOR?"NONE":(lightData[i].colorType==TEMPERATURE_ONLY?"TEMP":"RGB"),lightData[i].name);
}
Serial.printf("\n");
}
///////////////////////////
void setupWeb(){
Serial.printf("Starting Light Server Hub at %s.local\n\n",HUB_NAME);
webServer.begin();
webServer.on("/", []() {
String response = "<html><head><title>HomeSpan Programmable Light Hub</title>";
response += "<style>table, th, td {border: 1px solid black; border-collapse: collapse;} th, td { padding: 5px; text-align: center; } </style></head>\n";
response += "<body><h2>HomeSpan Lights</h2>";
response += "<form action='/addLight' method='get'>";
response += "<table><tr><th style='text-align:left;'>Accessory</th><th>Dim?</th><th>Color Control</th><th>Action</th></tr>";
int openSlots=MAX_LIGHTS;
for(int i=0;i<MAX_LIGHTS;i++){
if(lightData[i].aid){
response += "<tr><td style='text-align:left;'>" + String(lightData[i].name) + "</td>";
response += "<td><input type='checkbox' disabled " + String(lightData[i].isDimmable?"checked>":">") + "</td>";
response += "<td><input type='radio' disabled " + String(lightData[i].colorType==NO_COLOR?"checked>":">") + " NONE ";
response += "<input type='radio' disabled " + String(lightData[i].colorType==TEMPERATURE_ONLY?"checked>":">") + " TEMP ONLY ";
response += "<input type='radio' disabled " + String(lightData[i].colorType==FULL_RGB?"checked>":">") + " FULL COLOR </td>";
response += "<td><button type='button' onclick=\"document.location='/deleteLight?index=" + String(i) + "'\">Delete Light</button></td>";
response += "</tr>";
openSlots--;
}
}
response += "<tr><td style='text-align:left;'><input type='text' name='name' required placeholder='Type accessory name here...' size='"
+ String(MAX_NAME_LENGTH) + "' maxlength='" + String(MAX_NAME_LENGTH) + "'></td>";
response += "<td><input type='checkbox' name='isDimmable'></td>";
response += "<td><input type='radio' checked name='colorType' for='no_color' value='" + String(NO_COLOR) + "'><label for='no_color'> NONE </label>";
response += "<input type='radio' name='colorType' for='temp_only' value='" + String(TEMPERATURE_ONLY) + "'><label for='temp_only'> TEMP ONLY </label>";
response += "<input type='radio' name='colorType' for='full_rgb' value='" + String(FULL_RGB) + "'><label for='full_rgb'> FULL COLOR </label></td>";
response += "<td><input type='submit' value='Add Light'" + String(openSlots?"":" disabled") + "></td>";
response += "</tr>";
response += "</table>";
response += "</form>";
if(!openSlots)
response += "<p>Can't add any more Light Accessories. Max="+ String(MAX_LIGHTS) + "</p>";
response += "<p>Press here to delete ALL Light Accessories: <button type='button' onclick=\"document.location='/deleteAll'\">Delete All Lights</button></p>";
response += "<p>Press here to update the Home App when finished making changes: <button type='button' onclick=\"document.location='/update'\">Upddate HomeKit</button></p>";
response += "</body></html>";
webServer.send(200, "text/html", response);
});
webServer.on("/deleteLight", []() {
int index=atoi(webServer.arg(0).c_str());
String response = "<html><head><title>HomeSpan Programmable Light Hub</title><meta http-equiv='refresh' content = '3; url=/'/></head>";
response += "<body>Deleting Light Accessory '" + String(lightData[index].name) + "'...</body></html>";
deleteAccessory(index);
webServer.send(200, "text/html", response);
});
webServer.on("/deleteAll", []() {
String response = "<html><head><title>HomeSpan Programmable Light Hub</title><meta http-equiv='refresh' content = '3; url=/'/></head>";
response += "<body>Deleting All Light Accessories...</body></html>";
webServer.send(200, "text/html", response);
deleteAllAccessories("");
});
webServer.on("/update", []() {
String response = "<html><head><title>HomeSpan Programmable Light Hub</title><meta http-equiv='refresh' content = '3; url=/'/></head><body>";
if(homeSpan.updateDatabase())
response += "Accessories Database updated. New configuration number broadcasted...";
else
response += "Nothing to update - no changes were made...";
response += "...</body></html>";
webServer.send(200, "text/html", response);
});
webServer.on("/addLight", []() {
colorType_t colorType=NO_COLOR;
boolean isDimmable=false;
int iName=-1;
for(int i=0;i<webServer.args();i++){
if(!webServer.argName(i).compareTo(String("colorType")))
colorType=(colorType_t)webServer.arg(i).toInt();
else if(!webServer.argName(i).compareTo(String("isDimmable")))
isDimmable=true;
else if(!webServer.argName(i).compareTo(String("name")))
iName=i;
}
String response = "<html><head><title>HomeSpan Programmable Light Hub</title><meta http-equiv='refresh' content = '3; url=/'/></head><body>";
if(iName!=-1){
int index=addLight(webServer.arg(iName).c_str(),isDimmable,colorType);
response += "Adding Light Accessory '" + String(lightData[index].name) + "'";
} else
response += "Error - bad URL request";
response += "...</body></html>";
webServer.send(200, "text/html", response);
});
}
///////////////////////////