HomeSpan/src/HomeSpan.cpp

1322 lines
42 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);
}
///////////////////////////////
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;
}
}
///////////////////////////////