HomeSpan/src/HomeSpan.cpp

1394 lines
46 KiB
C++

#include <ESPmDNS.h>
#include <nvs_flash.h>
#include <sodium.h>
#include <DNSServer.h>
#include "Utils.h"
#include "HAP.h"
using namespace Utils;
WiFiServer hapServer(80); // HTTP Server (i.e. this acccesory) running on usual port 80 (local-scoped variable to this file only)
HAPClient hap[MAX_CONNECTIONS]; // HAP Client structure containing HTTP client connections, parsing routines, and state variables (global-scoped variable)
Span homeSpan; // HAP Attributes database and all related control functions for this Accessory (global-scoped variable)
///////////////////////////////
// Span //
///////////////////////////////
void Span::begin(Category catID, char *displayName, char *hostNameBase, char *modelName){
this->displayName=displayName;
this->hostNameBase=hostNameBase;
this->modelName=modelName;
sprintf(this->category,"%d",catID);
pinMode(resetPin,INPUT_PULLUP);
delay(2000);
Serial.print("\n************************************************************\n"
"Welcome to HomeSpan!\n"
"Apple HomeKit for the Espressif ESP-32 WROOM and Arduino IDE\n"
"************************************************************\n\n"
"** Please ensure serial monitor is set to transmit <newlines>\n");
Serial.print("** Ground pin ");
Serial.print(resetPin);
Serial.print(" to delete all stored WiFi Network and HomeKit Pairing data (factory reset)\n\n");
Serial.print("HomeSpan Version: ");
Serial.print(HOMESPAN_VERSION);
Serial.print("\n");
Serial.print("ESP-IDF Version: ");
Serial.print(esp_get_idf_version());
Serial.print("\n");
Serial.print("Sketch Compiled: ");
Serial.print(__DATE__);
Serial.print(" ");
Serial.print(__TIME__);
Serial.print("\n\n");
if(!digitalRead(resetPin)){ // factory reset pin is low
nvs_flash_erase(); // erase NVS storage
Serial.print("** FACTORY RESET PIN LOW! ALL STORED DATA ERASED **\n** PROGRAM HALTED **\n");
while(1){
digitalWrite(LED_BUILTIN,HIGH);
delay(100);
digitalWrite(LED_BUILTIN,LOW);
delay(500);
}
}
} // begin
///////////////////////////////
void Span::poll() {
if(!strlen(category)){
Serial.print("\n** FATAL ERROR: Cannot run homeSpan.poll() without an initial call to homeSpan.begin()!\n** PROGRAM HALTED **\n\n");
while(1);
} else if(WiFi.status()!=WL_CONNECTED){
nvs_flash_init(); // initialize non-volatile-storage partition in flash
HAPClient::init(); // read NVS and load HAP settings
initWifi(); // initialize WiFi
if(!HAPClient::nAdminControllers()){
Serial.print("DEVICE NOT YET PAIRED -- PLEASE PAIR WITH HOMEKIT APP\n\n");
statusLED.start(500,0.5,2,1000);
} else {
statusLED.on();
}
Serial.print(displayName);
Serial.print(" is READY!\n\n");
}
char cBuf[8]="?";
if(Serial.available()){
readSerial(cBuf,1);
processSerialCommand(cBuf);
}
WiFiClient newClient;
if(newClient=hapServer.available()){ // found a new HTTP client
int freeSlot=getFreeSlot(); // get next free slot
if(freeSlot==-1){ // no available free slots
freeSlot=randombytes_uniform(MAX_CONNECTIONS);
LOG2("=======================================\n");
LOG1("** Freeing Client #");
LOG1(freeSlot);
LOG1(" (");
LOG1(millis()/1000);
LOG1(" sec) ");
LOG1(hap[freeSlot].client.remoteIP());
LOG1("\n");
hap[freeSlot].client.stop(); // disconnect client from first slot and re-use
}
hap[freeSlot].client=newClient; // copy new client handle into free slot
LOG2("=======================================\n");
LOG1("** Client #");
LOG1(freeSlot);
LOG1(" Connected: (");
LOG1(millis()/1000);
LOG1(" sec) ");
LOG1(hap[freeSlot].client.remoteIP());
LOG1("\n");
LOG2("\n");
hap[freeSlot].cPair=NULL; // reset pointer to verified ID
homeSpan.clearNotify(freeSlot); // clear all notification requests for this connection
HAPClient::pairStatus=pairState_M1; // reset starting PAIR STATE (which may be needed if Accessory failed in middle of pair-setup)
}
for(int i=0;i<MAX_CONNECTIONS;i++){ // loop over all HAP Connection slots
if(hap[i].client && hap[i].client.available()){ // if connection exists and data is available
HAPClient::conNum=i; // set connection number
hap[i].processRequest(); // process HAP request
if(!hap[i].client){ // client disconnected by server
LOG1("** Disconnecting Client #");
LOG1(i);
LOG1(" (");
LOG1(millis()/1000);
LOG1(" sec)\n");
}
LOG2("\n");
} // process HAP Client
} // for-loop over connection slots
HAPClient::callServiceLoops();
HAPClient::checkPushButtons();
HAPClient::checkNotifications();
HAPClient::checkTimedWrites();
switch(resetPressed){
case 0:
if(!digitalRead(resetPin)){
resetPressed=1;
resetTime=millis()+5000;
}
break;
case 1:
if(digitalRead(resetPin)){
resetPressed=0;
} else
if(millis()>resetTime){
resetPressed=2;
statusLED.start(200,0.5,4,800);
resetTime=millis()+6000;
}
break;
case 2:
if(digitalRead(resetPin)){
statusLED.off();
processSerialCommand("H");
} else
if(millis()>resetTime){
statusLED.off();
processSerialCommand("F");
}
break;
} // switch
} // poll
///////////////////////////////
int Span::getFreeSlot(){
for(int i=0;i<MAX_CONNECTIONS;i++){
if(!hap[i].client)
return(i);
}
return(-1);
}
//////////////////////////////////////
void Span::initWifi(){
const int MAX_SSID=32;
const int MAX_PWD=64;
struct {
char ssid[MAX_SSID+1];
char pwd[MAX_PWD+1];
} wifiData;
char id[18]; // create string version of Accessory ID for MDNS broadcast
memcpy(id,HAPClient::accessory.ID,17); // copy ID bytes
id[17]='\0'; // add terminating null
// create broadcaset name from server base name plus accessory ID (without ':')
int nChars=snprintf(NULL,0,"%s-%.2s%.2s%.2s%.2s%.2s%.2s",hostNameBase,id,id+3,id+6,id+9,id+12,id+15);
char hostName[nChars+1];
sprintf(hostName,"%s-%.2s%.2s%.2s%.2s%.2s%.2s",hostNameBase,id,id+3,id+6,id+9,id+12,id+15);
nvs_handle wifiHandle;
size_t len; // not used but required to read blobs from NVS
nvs_open("WIFI",NVS_READWRITE,&wifiHandle); // open WIFI data namespace in NVS
if(!nvs_get_blob(wifiHandle,"WIFIDATA",NULL,&len)){ // if found WiFi data in NVS
nvs_get_blob(wifiHandle,"WIFIDATA",&wifiData,&len); // retrieve data
} else {
statusLED.start(500,0.3,2,1000);
configure(hostName);
Serial.print("Please configure network...\n");
sprintf(wifiData.ssid,"MyNetwork");
sprintf(wifiData.pwd,"MyPassword");
Serial.print(">>> WiFi SSID (");
Serial.print(wifiData.ssid);
Serial.print("): ");
readSerial(wifiData.ssid,MAX_SSID);
Serial.print(wifiData.ssid);
Serial.print("\n>>> WiFi Password (");
Serial.print(wifiData.pwd);
Serial.print("): ");
readSerial(wifiData.pwd,MAX_PWD);
Serial.print(mask(wifiData.pwd,2));
Serial.print("\n\n");
nvs_set_blob(wifiHandle,"WIFIDATA",&wifiData,sizeof(wifiData)); // update data
nvs_commit(wifiHandle); // commit to NVS
}
int nTries=0;
statusLED.start(1000);
while(WiFi.status()!=WL_CONNECTED){
Serial.print("Connecting to: ");
Serial.print(wifiData.ssid);
Serial.print("... ");
nTries++;
if(WiFi.begin(wifiData.ssid,wifiData.pwd)!=WL_CONNECTED){
int delayTime=nTries%6?5000:60000;
char buf[8]="";
Serial.print("Can't connect. Re-trying in ");
Serial.print(delayTime/1000);
Serial.print(" seconds (or type 'W <return>' to reset WiFi data)...\n");
long sTime=millis();
while(millis()-sTime<delayTime){
if(Serial.available()){
readSerial(buf,1);
if(buf[0]=='W'){
nvs_handle wifiHandle;
nvs_open("WIFI",NVS_READWRITE,&wifiHandle); // open WIFI data namespace in NVS
nvs_erase_all(wifiHandle);
nvs_commit(wifiHandle);
Serial.print("\n** WIFI Network Data DELETED **\n** Restarting...\n\n");
delay(2000);
ESP.restart();
}
}
}
}
} // WiFi not yet connected
Serial.print("Success! IP: ");
Serial.print(WiFi.localIP());
Serial.print("\n");
Serial.print("\nStarting MDNS...\n");
Serial.print("Broadcasting as: ");
Serial.print(hostName);
Serial.print(".local (");
Serial.print(displayName);
Serial.print(" / ");
Serial.print(modelName);
Serial.print(")\n");
MDNS.begin(hostName); // set server host name (.local implied)
MDNS.setInstanceName(displayName); // set server display name
MDNS.addService("_hap","_tcp",80); // advertise HAP service on HTTP port (80)
// add MDNS (Bonjour) TXT records for configurable as well as fixed values (HAP Table 6-7)
char cNum[16];
sprintf(cNum,"%d",hapConfig.configNumber);
mdns_service_txt_item_set("_hap","_tcp","c#",cNum); // Accessory Current Configuration Number (updated whenever config of HAP Accessory Attribute Database is updated)
mdns_service_txt_item_set("_hap","_tcp","md",modelName); // Accessory Model Name
mdns_service_txt_item_set("_hap","_tcp","ci",category); // Accessory Category (HAP Section 13.1)
mdns_service_txt_item_set("_hap","_tcp","id",id); // string version of Accessory ID in form XX:XX:XX:XX:XX:XX (HAP Section 5.4)
mdns_service_txt_item_set("_hap","_tcp","ff","0"); // HAP Pairing Feature flags. MUST be "0" to specify Pair Setup method (HAP Table 5-3) without MiFi Authentification
mdns_service_txt_item_set("_hap","_tcp","pv","1.1"); // HAP version - MUST be set to "1.1" (HAP Section 6.6.3)
mdns_service_txt_item_set("_hap","_tcp","s#","1"); // HAP current state - MUST be set to "1"
if(!HAPClient::nAdminControllers()) // Accessory is not yet paired
mdns_service_txt_item_set("_hap","_tcp","sf","1"); // set Status Flag = 1 (Table 6-8)
else
mdns_service_txt_item_set("_hap","_tcp","sf","0"); // set Status Flag = 0
Serial.print("\nStarting Web (HTTP) Server supporting up to ");
Serial.print(MAX_CONNECTIONS);
Serial.print(" simultaneous connections...\n\n");
hapServer.begin();
statusLED.stop();
} // initWiFi
///////////////////////////////
void Span::configure(char *apName){
Serial.print("WiFi Configuration required. Please connect to Access Point: ");
Serial.print(apName);
Serial.print("\n");
const byte DNS_PORT = 53;
WiFiServer apServer(80);
DNSServer dnsServer;
IPAddress apIP(192, 168, 4, 1);
WiFi.mode(WIFI_AP);
WiFi.softAP(apName,"homespan");
dnsServer.start(DNS_PORT, "*", apIP);
apServer.begin();
needsConfiguration=true;
while(needsConfiguration){
dnsServer.processNextRequest();
if(hap[0].client=apServer.available()){ // found a new HTTP client
LOG2("=======================================\n");
LOG1("** Access Point Client Connected: (");
LOG1(millis()/1000);
LOG1(" sec) ");
LOG1(hap[0].client.remoteIP());
LOG1("\n");
LOG2("\n");
}
if(hap[0].client && hap[0].client.available()){ // if connection exists and data is available
HAPClient::conNum=0; // set connection number
hap[0].processRequest(); // process HAP request
if(!hap[0].client){ // client disconnected by server
LOG1("** Disconnecting AP Client (");
LOG1(millis()/1000);
LOG1(" sec)\n");
}
LOG2("\n");
} // process HAP Client
}
while(1);
while(!needsConfiguration){
dnsServer.processNextRequest();
WiFiClient apClient=apServer.available();
if(apClient){ // found a new HTTP client
Serial.print("Client Found: ");
Serial.println(apClient.remoteIP());
while(apClient.available()){ // data is available
int nBytes=apClient.read(HAPClient::httpBuf,HAPClient::MAX_HTTP); // read all available bytes up to maximum allowed
char *p=(char *)HAPClient::httpBuf;
p[nBytes]='\0';
Serial.print(p);
Serial.print("\n");
if(!strncmp(p,"GET /",5)){
apClient.println("HTTP/1.1 200 OK");
apClient.println("Content-type:text/html");
apClient.println();
apClient.print("<html><head><title>HomeSpan Configuration</title><style>p{font-size:300%; margin:25px}label{font-size:300%; margin:25px}input{font-size:250%; margin:25px}</style></head>");
apClient.print("<body style=\"background-color:lightyellow;\"><center><p><b>HomeSpan_12_54_DD_E4_23_F5</b></p></center>");
apClient.print("<p>Welcome to HomeSpan! This page allows you to configure the above HomeSpan device to connect to your WiFi network, and to create a Setup Code for pairing this device to HomeKit.</p>");
apClient.print("<p><em>WiFi Network</em> is a required field. If your network name is broadcast you can select it from the dropdown list. If you don't see your network name in the dropdown list you may type it into the text box.</p>");
apClient.print("<p><em>WiFi Password</em> is required only if your network is password-protected.</p>");
apClient.print("<p>Every HomeKit device requires an 8-digit <em>Setup Code</em>. If this device is being configured for the first time, or if the device has been factory reset, you must create a new <em>Setup Code</em>. ");
apClient.print("Otherwise, either leave this field blank to retain the current <em>Setup Code</em>, or type a new 8-digit <em>Setup Code</em> to replace the current one.</p>");
apClient.print("<p>Note that HomeSpan cannot display a previously saved <em>Setup Code</em>, so if you've forgotten what it is, this is the place to create a new one.</p>");
apClient.print("<p>The LED on this device should be <em>double-blinking</em> during this configuration.<p>");
apClient.print("<form method=\"post\">");
apClient.print("<label for=\"ssid\">WiFi Network:</label>");
apClient.print("<input list=\"network\" name=\"network\" placeholder=\"Type/Select\" required>");
apClient.print("<datalist id=\"network\">");
apClient.print("<option value=\"SEA-SHORE\">SEA-SHORE</option>");
apClient.print("<option value=\"NetworkA\">Network A</option>");
apClient.print("<option value=\"NetworkB\">Network B</option>");
apClient.print("<option value=\"MyNet\">My Network</option>");
apClient.print("</datalist><br><br>");
apClient.print("<label for=\"pwd\">WiFi Password:</label>");
apClient.print("<input type=\"password\" id=\"pwd\" name=\"pwd\">");
apClient.print("<br><br>");
apClient.print("<label for=\"code\">Setup Code:</label>");
apClient.print("<input type=\"tel\" id=\"code\" name=\"code\" placeholder=\"12345678\" pattern=\"[0-9]{8}\">");
apClient.print("<p><em>");
apClient.print("This device already has a Setup Code. You may leave Setup Code blank to retain the current one, or type a new Setup Code to change.");
apClient.print("This device does not yet have a Setup Code. You must create one above.");
apClient.print("</p>");
apClient.print("<center><input style=\"font-size:300%\" type=\"submit\" value=\"SUBMIT\"></center>");
apClient.print("</form></body></html>");
} // GET
if(!strncmp(p,"POST /",6)){
apClient.println("HTTP/1.1 200 OK");
apClient.println("Content-type:text/html");
apClient.println();
apClient.println("SUCCESS");
}
} // data available
apClient.stop();
}
}
}
///////////////////////////////
void Span::processSerialCommand(char *c){
switch(c[0]){
case 's': {
Serial.print("\n*** HomeSpan Status ***\n\n");
Serial.print("IP Address: ");
Serial.print(WiFi.localIP());
Serial.print("\n\n");
Serial.print("Accessory ID: ");
HAPClient::charPrintRow(HAPClient::accessory.ID,17);
Serial.print(" LTPK: ");
HAPClient::hexPrintRow(HAPClient::accessory.LTPK,32);
Serial.print("\n");
HAPClient::printControllers();
Serial.print("\n");
for(int i=0;i<MAX_CONNECTIONS;i++){
Serial.print("Connection #");
Serial.print(i);
Serial.print(" ");
if(hap[i].client){
Serial.print(hap[i].client.remoteIP());
Serial.print(" ");
if(hap[i].cPair){
Serial.print("ID=");
HAPClient::charPrintRow(hap[i].cPair->ID,36);
Serial.print(hap[i].cPair->admin?" (admin)":" (regular)");
} else {
Serial.print("(unverified)");
}
} else {
Serial.print("(unconnected)");
}
Serial.print("\n");
}
Serial.print("\n*** End Status ***\n");
}
break;
case 'd': {
TempBuffer <char> qBuf(sprintfAttributes(NULL)+1);
sprintfAttributes(qBuf.buf);
Serial.print("\n*** Attributes Database: size=");
Serial.print(qBuf.len()-1);
Serial.print(" configuration=");
Serial.print(hapConfig.configNumber);
Serial.print(" ***\n\n");
prettyPrint(qBuf.buf);
Serial.print("\n*** End Database ***\n\n");
}
break;
case 'W': {
nvs_handle wifiHandle;
nvs_open("WIFI",NVS_READWRITE,&wifiHandle); // open WIFI data namespace in NVS
nvs_erase_all(wifiHandle);
nvs_commit(wifiHandle);
Serial.print("\n** WIFI Network Data DELETED **\n** Restarting...\n\n");
delay(2000);
ESP.restart();
}
break;
case 'H': {
nvs_erase_all(HAPClient::nvsHandle);
nvs_commit(HAPClient::nvsHandle);
Serial.print("\n** HomeKit Pairing Data DELETED **\n** Restarting...\n\n");
delay(1000);
ESP.restart();
}
break;
case 'F': {
nvs_flash_erase();
Serial.print("\n** FACTORY RESET **\n** Restarting...\n\n");
delay(1000);
ESP.restart();
}
break;
case 'i':{
Serial.print("\n*** HomeSpan Info ***\n\n");
char cBuf[128];
for(int i=0;i<Accessories.size();i++){ // identify all services with over-ridden loop() methods
for(int j=0;j<Accessories[i]->Services.size();j++){
SpanService *s=Accessories[i]->Services[j];
sprintf(cBuf,"Service aid=%2d iid=%2d Update: %3s Loop: %3s Button: %3s\n",Accessories[i]->aid,s->iid,
(void(*)())(s->*(&SpanService::update))!=(void(*)())(&SpanService::update)?"YES":"NO",
(void(*)())(s->*(&SpanService::loop))!=(void(*)())(&SpanService::loop)?"YES":"NO",
(void(*)(int,boolean))(s->*(&SpanService::button))!=(void(*)(int,boolean))(&SpanService::button)?"YES":"NO"
);
Serial.print(cBuf);
}
}
Serial.print("\n*** End Status ***\n");
}
break;
case '?': {
Serial.print("\n*** HomeSpan Commands ***\n\n");
Serial.print(" s - print connection status\n");
Serial.print(" d - print attributes database\n");
Serial.print(" i - print detailed info about configuration\n");
Serial.print(" W - delete stored WiFi data and restart\n");
Serial.print(" H - delete stored HomeKit Pairing data and restart\n");
Serial.print(" F - delete all stored WiFi Network and HomeKit Pairing data and restart\n");
Serial.print(" ? - print this list of commands\n");
Serial.print("\n*** End Commands ***\n\n");
}
break;
default:
Serial.print("** Unknown command: '");
Serial.print(c);
Serial.print("' - type '?' for list of commands.\n");
break;
} // switch
}
///////////////////////////////
int Span::sprintfAttributes(char *cBuf){
int nBytes=0;
nBytes+=snprintf(cBuf,cBuf?64:0,"{\"accessories\":[");
for(int i=0;i<Accessories.size();i++){
nBytes+=Accessories[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL);
if(i+1<Accessories.size())
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",");
}
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"]}");
return(nBytes);
}
///////////////////////////////
void Span::prettyPrint(char *buf, int nsp){
int s=strlen(buf);
int indent=0;
for(int i=0;i<s;i++){
switch(buf[i]){
case '{':
case '[':
Serial.print(buf[i]);
Serial.print("\n");
indent+=nsp;
for(int j=0;j<indent;j++)
Serial.print(" ");
break;
case '}':
case ']':
Serial.print("\n");
indent-=nsp;
for(int j=0;j<indent;j++)
Serial.print(" ");
Serial.print(buf[i]);
break;
case ',':
Serial.print(buf[i]);
Serial.print("\n");
for(int j=0;j<indent;j++)
Serial.print(" ");
break;
default:
Serial.print(buf[i]);
} // switch
} // loop over all characters
Serial.print("\n");
} // prettyPrint
///////////////////////////////
SpanCharacteristic *Span::find(int aid, int iid){
if(aid<1 || aid>Accessories.size()) // aid out of range
return(NULL);
aid--; // convert from aid to array index number
for(int i=0;i<Accessories[aid]->Services.size();i++){ // loop over all Services in this Accessory
for(int j=0;j<Accessories[aid]->Services[i]->Characteristics.size();j++){ // loop over all Characteristics in this Service
if(iid == Accessories[aid]->Services[i]->Characteristics[j]->iid) // if matching iid
return(Accessories[aid]->Services[i]->Characteristics[j]); // return pointer to Characteristic
}
}
return(NULL);
}
///////////////////////////////
int Span::countCharacteristics(char *buf){
int nObj=0;
const char tag[]="\"aid\"";
while(buf=strstr(buf,tag)){ // count number of characteristic objects in PUT JSON request
nObj++;
buf+=strlen(tag);
}
return(nObj);
}
///////////////////////////////
int Span::updateCharacteristics(char *buf, SpanBuf *pObj){
int nObj=0;
char *p1;
int cFound=0;
boolean twFail=false;
while(char *t1=strtok_r(buf,"{",&p1)){ // parse 'buf' and extract objects into 'pObj' unless NULL
buf=NULL;
char *p2;
int okay=0;
while(char *t2=strtok_r(t1,"}[]:, \"\t\n\r",&p2)){
if(!cFound){ // first token found
if(strcmp(t2,"characteristics")){
Serial.print("\n*** ERROR: Problems parsing JSON - initial \"characteristics\" tag not found\n\n");
return(0);
}
cFound=1;
break;
}
t1=NULL;
char *t3;
if(!strcmp(t2,"aid") && (t3=strtok_r(t1,"}[]:, \"\t\n\r",&p2))){
pObj[nObj].aid=atoi(t3);
okay|=1;
} else
if(!strcmp(t2,"iid") && (t3=strtok_r(t1,"}[]:, \"\t\n\r",&p2))){
pObj[nObj].iid=atoi(t3);
okay|=2;
} else
if(!strcmp(t2,"value") && (t3=strtok_r(t1,"}[]:, \"\t\n\r",&p2))){
pObj[nObj].val=t3;
okay|=4;
} else
if(!strcmp(t2,"ev") && (t3=strtok_r(t1,"}[]:, \"\t\n\r",&p2))){
pObj[nObj].ev=t3;
okay|=8;
} else
if(!strcmp(t2,"pid") && (t3=strtok_r(t1,"}[]:, \"\t\n\r",&p2))){
uint64_t pid=strtoull(t3,NULL,0);
if(!TimedWrites.count(pid)){
Serial.print("\n*** ERROR: Timed Write PID not found\n\n");
twFail=true;
} else
if(millis()>TimedWrites[pid]){
Serial.print("\n*** ERROR: Timed Write Expired\n\n");
twFail=true;
}
} else {
Serial.print("\n*** ERROR: Problems parsing JSON characteristics object - unexpected property \"");
Serial.print(t2);
Serial.print("\"\n\n");
return(0);
}
} // parse property tokens
if(!t1){ // at least one token was found that was not initial "characteristics"
if(okay==7 || okay==11 || okay==15){ // all required properties found
nObj++; // increment number of characteristic objects found
} else {
Serial.print("\n*** ERROR: Problems parsing JSON characteristics object - missing required properties\n\n");
return(0);
}
}
} // parse objects
snapTime=millis(); // timestamp for this series of updates, assigned to each characteristic in loadUpdate()
for(int i=0;i<nObj;i++){ // PASS 1: loop over all objects, identify characteristics, and initialize update for those found
if(twFail){ // this is a timed-write request that has either expired or for which there was no PID
pObj[i].status=StatusCode::InvalidValue; // set error for all characteristics
} else {
pObj[i].characteristic = find(pObj[i].aid,pObj[i].iid); // find characteristic with matching aid/iid and store pointer
if(pObj[i].characteristic) // if found, initialize characterstic update with new val/ev
pObj[i].status=pObj[i].characteristic->loadUpdate(pObj[i].val,pObj[i].ev); // save status code, which is either an error, or TBD (in which case isUpdated for the characteristic has been set to true)
else
pObj[i].status=StatusCode::UnknownResource; // if not found, set HAP error
}
} // first pass
for(int i=0;i<nObj;i++){ // PASS 2: loop again over all objects
if(pObj[i].status==StatusCode::TBD){ // if object status still TBD
StatusCode status=pObj[i].characteristic->service->update(); // update service and save returned statusCode
for(int j=i;j<nObj;j++){ // loop over this object plus any remaining objects to update values and save status for any other characteristics in this service
if(pObj[j].characteristic->service==pObj[i].characteristic->service){ // if service of this characteristic matches service that was updated
pObj[j].status=status; // save statusCode for this object
LOG1("Updating aid=");
LOG1(pObj[j].characteristic->aid);
LOG1(" iid=");
LOG1(pObj[j].characteristic->iid);
if(status==StatusCode::OK){ // if status is okay
pObj[j].characteristic->value
=pObj[j].characteristic->newValue; // update characteristic value with new value
LOG1(" (okay)\n");
} else { // if status not okay
pObj[j].characteristic->newValue
=pObj[j].characteristic->value; // replace characteristic new value with original value
LOG1(" (failed)\n");
}
pObj[j].characteristic->isUpdated=false; // reset isUpdated flag for characteristic
}
}
} // object had TBD status
} // loop over all objects
return(1);
}
///////////////////////////////
void Span::clearNotify(int slotNum){
for(int i=0;i<Accessories.size();i++){
for(int j=0;j<Accessories[i]->Services.size();j++){
for(int k=0;k<Accessories[i]->Services[j]->Characteristics.size();k++){
Accessories[i]->Services[j]->Characteristics[k]->ev[slotNum]=false;
}
}
}
}
///////////////////////////////
int Span::sprintfNotify(SpanBuf *pObj, int nObj, char *cBuf, int conNum){
int nChars=0;
boolean notifyFlag=false;
nChars+=snprintf(cBuf,cBuf?64:0,"{\"characteristics\":[");
for(int i=0;i<nObj;i++){ // loop over all objects
if(pObj[i].status==StatusCode::OK && pObj[i].val){ // characteristic was successfully updated with a new value (i.e. not just an EV request)
if(pObj[i].characteristic->ev[conNum]){ // if notifications requested for this characteristic by specified connection number
if(notifyFlag) // already printed at least one other characteristic
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,","); // add preceeding comma before printing next characteristic
nChars+=pObj[i].characteristic->sprintfAttributes(cBuf?(cBuf+nChars):NULL,GET_AID); // get JSON attributes for characteristic
notifyFlag=true;
} // notification requested
} // characteristic updated
} // loop over all objects
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,"]}");
return(notifyFlag?nChars:0); // if notifyFlag is not set, return 0, else return number of characters printed to cBuf
}
///////////////////////////////
int Span::sprintfAttributes(SpanBuf *pObj, int nObj, char *cBuf){
int nChars=0;
nChars+=snprintf(cBuf,cBuf?64:0,"{\"characteristics\":[");
for(int i=0;i<nObj;i++){
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?128:0,"{\"aid\":%d,\"iid\":%d,\"status\":%d}",pObj[i].aid,pObj[i].iid,pObj[i].status);
if(i+1<nObj)
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,",");
}
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,"]}");
return(nChars);
}
///////////////////////////////
int Span::sprintfAttributes(char **ids, int numIDs, int flags, char *cBuf){
int nChars=0;
int aid, iid;
SpanCharacteristic *Characteristics[numIDs];
StatusCode status[numIDs];
boolean sFlag=false;
for(int i=0;i<numIDs;i++){ // PASS 1: loop over all ids requested to check status codes - only errors are if characteristic not found, or not readable
sscanf(ids[i],"%d.%d",&aid,&iid); // parse aid and iid
Characteristics[i]=find(aid,iid); // find matching chararacteristic
if(Characteristics[i]){ // if found
if(Characteristics[i]->perms&SpanCharacteristic::PR){ // if permissions allow reading
status[i]=StatusCode::OK; // always set status to OK (since no actual reading of device is needed)
} else {
Characteristics[i]=NULL;
status[i]=StatusCode::WriteOnly;
sFlag=true; // set flag indicating there was an error
}
} else {
status[i]=StatusCode::UnknownResource;
sFlag=true; // set flag indicating there was an error
}
}
nChars+=snprintf(cBuf,cBuf?64:0,"{\"characteristics\":[");
for(int i=0;i<numIDs;i++){ // PASS 2: loop over all ids requested and create JSON for each (with or without status code base on sFlag set above)
if(Characteristics[i]) // if found
nChars+=Characteristics[i]->sprintfAttributes(cBuf?(cBuf+nChars):NULL,flags); // get JSON attributes for characteristic
else{
sscanf(ids[i],"%d.%d",&aid,&iid); // parse aid and iid
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,"{\"iid\":%d,\"aid\":%d}",iid,aid); // else create JSON attributes based on requested aid/iid
}
if(sFlag){ // status flag is needed - overlay at end
nChars--;
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,",\"status\":%d}",status[i]);
}
if(i+1<numIDs)
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,",");
}
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,"]}");
return(nChars);
}
///////////////////////////////
// SpanAccessory //
///////////////////////////////
SpanAccessory::SpanAccessory(){
homeSpan.Accessories.push_back(this);
aid=homeSpan.Accessories.size();
}
///////////////////////////////
int SpanAccessory::sprintfAttributes(char *cBuf){
int nBytes=0;
nBytes+=snprintf(cBuf,cBuf?64:0,"{\"aid\":%d,\"services\":[",aid);
for(int i=0;i<Services.size();i++){
nBytes+=Services[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL);
if(i+1<Services.size())
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",");
}
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"]}");
return(nBytes);
}
///////////////////////////////
// SpanService //
///////////////////////////////
SpanService::SpanService(const char *type, ServiceType mod){
this->type=type;
hidden=(mod==ServiceType::Hidden);
primary=(mod==ServiceType::Primary);
if(homeSpan.Accessories.empty()){
Serial.print("*** FATAL ERROR: Can't create new Service without a defined Accessory. Program halted!\n\n");
while(1);
}
homeSpan.Accessories.back()->Services.push_back(this);
iid=++(homeSpan.Accessories.back()->iidCount);
}
///////////////////////////////
int SpanService::sprintfAttributes(char *cBuf){
int nBytes=0;
nBytes+=snprintf(cBuf,cBuf?64:0,"{\"iid\":%d,\"type\":\"%s\",",iid,type);
if(hidden)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"hidden\":true,");
if(primary)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"primary\":true,");
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"characteristics\":[");
for(int i=0;i<Characteristics.size();i++){
nBytes+=Characteristics[i]->sprintfAttributes(cBuf?(cBuf+nBytes):NULL,GET_META|GET_PERMS|GET_TYPE|GET_DESC);
if(i+1<Characteristics.size())
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",");
}
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"]}");
return(nBytes);
}
///////////////////////////////
// SpanCharacteristic //
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms){
this->type=type;
this->perms=perms;
if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){
Serial.print("*** FATAL ERROR: Can't create new Characteristic without a defined Service. Program halted!\n\n");
while(1);
}
homeSpan.Accessories.back()->Services.back()->Characteristics.push_back(this);
iid=++(homeSpan.Accessories.back()->iidCount);
service=homeSpan.Accessories.back()->Services.back();
aid=homeSpan.Accessories.back()->aid;
}
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, boolean value) : SpanCharacteristic(type, perms) {
this->format=BOOL;
this->value.BOOL=value;
}
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, int32_t value) : SpanCharacteristic(type, perms) {
this->format=INT;
this->value.INT=value;
}
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, uint8_t value) : SpanCharacteristic(type, perms) {
this->format=UINT8;
this->value.UINT8=value;
}
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, uint16_t value) : SpanCharacteristic(type, perms) {
this->format=UINT16;
this->value.UINT16=value;
}
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, uint32_t value) : SpanCharacteristic(type, perms) {
this->format=UINT32;
this->value.UINT32=value;
}
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, uint64_t value) : SpanCharacteristic(type, perms) {
this->format=UINT64;
this->value.UINT64=value;
}
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, double value) : SpanCharacteristic(type, perms) {
this->format=FLOAT;
this->value.FLOAT=value;
}
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(char *type, uint8_t perms, const char* value) : SpanCharacteristic(type, perms) {
this->format=STRING;
this->value.STRING=value;
}
///////////////////////////////
int SpanCharacteristic::sprintfAttributes(char *cBuf, int flags){
int nBytes=0;
const char permCodes[][7]={"pr","pw","ev","aa","tw","hd","wr"};
nBytes+=snprintf(cBuf,cBuf?64:0,"{\"iid\":%d",iid);
if(flags&GET_TYPE)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"type\":\"%s\"",type);
switch(format){
case BOOL:
if(perms&PR)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%s",value.BOOL?"true":"false");
if(flags&GET_META)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"bool\"");
break;
case INT:
if(perms&PR)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%d",value.INT);
if(flags&GET_META)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"int\"");
break;
case UINT8:
if(perms&PR)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%u",value.UINT8);
if(flags&GET_META)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"uint8\"");
break;
case UINT16:
if(perms&PR)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%u",value.UINT16);
if(flags&GET_META)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"uint16\"");
break;
case UINT32:
if(perms&PR)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%lu",value.UINT32);
if(flags&GET_META)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"uint32\"");
break;
case UINT64:
if(perms&PR)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%llu",value.UINT64);
if(flags&GET_META)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"uint64\"");
break;
case FLOAT:
if(perms&PR)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%lg",value.FLOAT);
if(flags&GET_META)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"float\"");
break;
case STRING:
if(perms&PR)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":\"%s\"",value.STRING);
if(flags&GET_META)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"string\"");
break;
} // switch
if(range && (flags&GET_META)){
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?128:0,",\"minValue\":%d,\"maxValue\":%d,\"minStep\":%d",range->min,range->max,range->step);
}
if(desc && (flags&GET_DESC)){
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?128:0,",\"description\":\"%s\"",desc);
}
if(flags&GET_PERMS){
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"perms\":[");
for(int i=0;i<7;i++){
if(perms&(1<<i)){
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"%s\"",permCodes[i]);
if(perms>=(1<<(i+1)))
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",");
}
}
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"]");
}
if(flags&GET_AID)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"aid\":%d",aid);
if(flags&GET_EV)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"ev\":%s",ev[HAPClient::conNum]?"true":"false");
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"}");
return(nBytes);
}
///////////////////////////////
StatusCode SpanCharacteristic::loadUpdate(char *val, char *ev){
if(ev){ // request for notification
boolean evFlag;
if(!strcmp(ev,"0") || !strcmp(ev,"false"))
evFlag=false;
else if(!strcmp(ev,"1") || !strcmp(ev,"true"))
evFlag=true;
else
return(StatusCode::InvalidValue);
if(evFlag && !(perms&EV)) // notification is not supported for characteristic
return(StatusCode::NotifyNotAllowed);
LOG1("Notification Request for aid=");
LOG1(aid);
LOG1(" iid=");
LOG1(iid);
LOG1(": ");
LOG1(evFlag?"true":"false");
LOG1("\n");
this->ev[HAPClient::conNum]=evFlag;
}
if(!val) // no request to update value
return(StatusCode::OK);
if(!(perms&PW)) // cannot write to read only characteristic
return(StatusCode::ReadOnly);
switch(format){
case BOOL:
if(!strcmp(val,"0") || !strcmp(val,"false"))
newValue.BOOL=false;
else if(!strcmp(val,"1") || !strcmp(val,"true"))
newValue.BOOL=true;
else
return(StatusCode::InvalidValue);
break;
case INT:
if(!sscanf(val,"%d",&newValue.INT))
return(StatusCode::InvalidValue);
break;
case UINT8:
if(!sscanf(val,"%u",&newValue.UINT8))
return(StatusCode::InvalidValue);
break;
case UINT16:
if(!sscanf(val,"%u",&newValue.UINT16))
return(StatusCode::InvalidValue);
break;
case UINT32:
if(!sscanf(val,"%llu",&newValue.UINT32))
return(StatusCode::InvalidValue);
break;
case UINT64:
if(!sscanf(val,"%llu",&newValue.UINT64))
return(StatusCode::InvalidValue);
break;
case FLOAT:
if(!sscanf(val,"%lg",&newValue.FLOAT))
return(StatusCode::InvalidValue);
break;
} // switch
isUpdated=true;
updateTime=homeSpan.snapTime;
return(StatusCode::TBD);
}
///////////////////////////////
void SpanCharacteristic::setVal(int val){
switch(format){
case BOOL:
value.BOOL=(boolean)val;
break;
case INT:
value.INT=(int)val;
break;
case UINT8:
value.UINT8=(uint8_t)val;
break;
case UINT16:
value.UINT16=(uint16_t)val;
break;
case UINT32:
value.UINT32=(uint32_t)val;
break;
case UINT64:
value.UINT64=(uint64_t)val;
break;
}
updateTime=homeSpan.snapTime;
SpanBuf sb; // create SpanBuf object
sb.characteristic=this; // set characteristic
sb.status=StatusCode::OK; // set status
sb.val=""; // set dummy "val" so that sprintfNotify knows to consider this "update"
homeSpan.Notifications.push_back(sb); // store SpanBuf in Notifications vector
}
///////////////////////////////
void SpanCharacteristic::setVal(double val){
value.FLOAT=(double)val;
updateTime=homeSpan.snapTime;
SpanBuf sb; // create SpanBuf object
sb.characteristic=this; // set characteristic
sb.status=StatusCode::OK; // set status
sb.val=""; // set dummy "val" so that sprintfNotify knows to consider this "update"
homeSpan.Notifications.push_back(sb); // store SpanBuf in Notifications vector
}
///////////////////////////////
int SpanCharacteristic::timeVal(){
return(homeSpan.snapTime-updateTime);
}
///////////////////////////////
// SpanRange //
///////////////////////////////
SpanRange::SpanRange(int min, int max, int step){
this->min=min;
this->max=max;
this->step=step;
if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty() || homeSpan.Accessories.back()->Services.back()->Characteristics.empty() ){
Serial.print("*** FATAL ERROR: Can't create new Range without a defined Characteristic. Program halted!\n\n");
while(1);
}
homeSpan.Accessories.back()->Services.back()->Characteristics.back()->range=this;
}
///////////////////////////////
// SpanButton //
///////////////////////////////
SpanButton::SpanButton(int pin, unsigned long longTime, unsigned long shortTime){
if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){
Serial.print("*** FATAL ERROR: Can't create new PushButton without a defined Service. Program halted!\n\n");
while(1);
}
Serial.print("Configuring PushButton: Pin="); // initialization message
Serial.print(pin);
Serial.print("\n");
this->pin=pin;
this->shortTime=shortTime;
this->longTime=longTime;
service=homeSpan.Accessories.back()->Services.back();
if((void(*)(int,boolean))(service->*(&SpanService::button))==(void(*)(int,boolean))(&SpanService::button))
Serial.print("*** WARNING: No button() method defined for this PushButton!\n\n");
homeSpan.PushButtons.push_back(this);
pinMode(pin,INPUT_PULLUP);
}
///////////////////////////////
void SpanButton::check(){
if(!isTriggered && !digitalRead(pin)){
isTriggered=true;
unsigned long cTime=millis();
shortAlarm=cTime+shortTime;
longAlarm=cTime+longTime;
} else
if(isTriggered && digitalRead(pin)){
unsigned long cTime=millis();
if(cTime>longAlarm)
service->button(pin, true);
else if(cTime>shortAlarm)
service->button(pin, false);
isTriggered=false;
}
}
///////////////////////////////