/********************************************************************************* * MIT License * * Copyright (c) 2020-2021 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. * ********************************************************************************/ #pragma once #ifndef ARDUINO_ARCH_ESP32 #error ERROR: HOMESPAN IS ONLY AVAILABLE FOR ESP32 MICROCONTROLLERS! #endif #pragma GCC diagnostic ignored "-Wpmf-conversions" // eliminates warning messages from use of pointers to member functions to detect whether update() and loop() are overridden by user #include #include #include #include #include "Settings.h" #include "Utils.h" #include "Network.h" #include "HAPConstants.h" #include "HapQR.h" #include "Characteristics.h" using std::vector; using std::unordered_map; enum { GET_AID=1, GET_META=2, GET_PERMS=4, GET_TYPE=8, GET_EV=16, GET_DESC=32, GET_NV=64, GET_ALL=255 }; /////////////////////////////// // Forward-Declarations struct Span; struct SpanAccessory; struct SpanService; struct SpanCharacteristic; struct SpanRange; struct SpanBuf; struct SpanButton; struct SpanUserCommand; extern Span homeSpan; /////////////////////////////// struct SpanConfig { int configNumber=0; // configuration number - broadcast as Bonjour "c#" (computed automatically) uint8_t hashCode[48]={0}; // SHA-384 hash of Span Database stored as a form of unique "signature" to know when to update the config number upon changes }; /////////////////////////////// struct SpanBuf{ // temporary storage buffer for use with putCharacteristicsURL() and checkTimedResets() uint32_t aid=0; // updated aid int iid=0; // updated iid char *val=NULL; // updated value (optional, though either at least 'val' or 'ev' must be specified) char *ev=NULL; // updated event notification flag (optional, though either at least 'val' or 'ev' must be specified) StatusCode status; // return status (HAP Table 6-11) SpanCharacteristic *characteristic=NULL; // Characteristic to update (NULL if not found) }; /////////////////////////////// struct Span{ const char *displayName; // display name for this device - broadcast as part of Bonjour MDNS const char *hostNameBase; // base of hostName of this device - full host name broadcast by Bonjour MDNS will have 6-byte accessoryID as well as '.local' automatically appended const char *hostNameSuffix=NULL; // optional "suffix" of hostName of this device. If specified, will be used as the hostName suffix instead of the 6-byte accessoryID char *hostName; // full host name of this device - constructed from hostNameBase and 6-byte AccessoryID const char *modelName; // model name of this device - broadcast as Bonjour field "md" char category[3]=""; // category ID of primary accessory - broadcast as Bonjour field "ci" (HAP Section 13) unsigned long snapTime; // current time (in millis) snapped before entering Service loops() or updates() boolean isInitialized=false; // flag indicating HomeSpan has been initialized int nFatalErrors=0; // number of fatal errors in user-defined configuration int nWarnings=0; // number of warnings errors in user-defined configuration String configLog; // log of configuration process, including any errors boolean isBridge=true; // flag indicating whether device is configured as a bridge (i.e. first Accessory contains nothing but AccessoryInformation and HAPProtocolInformation) HapQR qrCode; // optional QR Code to use for pairing const char *sketchVersion="n/a"; // version of the sketch nvs_handle charNVS; // handle for non-volatile-storage of Characteristics data nvs_handle wifiNVS=0; // handle for non-volatile-storage of WiFi data char pairingCodeCommand[12]=""; // user-specified Pairing Code - only needed if Pairing Setup Code is specified in sketch using setPairingCode() boolean connected=false; // WiFi connection status unsigned long waitTime=60000; // time to wait (in milliseconds) between WiFi connection attempts unsigned long alarmConnect=0; // time after which WiFi connection attempt should be tried again const char *defaultSetupCode=DEFAULT_SETUP_CODE; // Setup Code used for pairing int statusPin=DEFAULT_STATUS_PIN; // pin for Status LED uint16_t autoOffLED=0; // automatic turn-off duration (in seconds) for Status LED int controlPin=DEFAULT_CONTROL_PIN; // pin for Control Pushbutton uint8_t logLevel=DEFAULT_LOG_LEVEL; // level for writing out log messages to serial monitor uint8_t maxConnections=CONFIG_LWIP_MAX_SOCKETS-2; // maximum number of allowed simultaneous HAP connections uint8_t requestedMaxCon=CONFIG_LWIP_MAX_SOCKETS-2; // requested maximum number of simultaneous HAP connections unsigned long comModeLife=DEFAULT_COMMAND_TIMEOUT*1000; // length of time (in milliseconds) to keep Command Mode alive before resuming normal operations uint16_t tcpPortNum=DEFAULT_TCP_PORT; // port for TCP communications between HomeKit and HomeSpan char qrID[5]=""; // Setup ID used for pairing with QR Code boolean otaEnabled=false; // enables Over-the-Air ("OTA") updates char otaPwd[33]; // MD5 Hash of OTA password, represented as a string of hexidecimal characters boolean otaAuth; // OTA requires password when set to true void (*wifiCallback)()=NULL; // optional callback function to invoke once WiFi connectivity is established void (*pairCallback)(boolean isPaired)=NULL; // optional callback function to invoke when pairing is established (true) or lost (false) boolean autoStartAPEnabled=false; // enables auto start-up of Access Point when WiFi Credentials not found void (*apFunction)()=NULL; // optional function to invoke when starting Access Point WiFiServer *hapServer; // pointer to the HAP Server connection Blinker statusLED; // indicates HomeSpan status PushButton controlButton; // controls HomeSpan configuration and resets Network network; // configures WiFi and Setup Code via either serial monitor or temporary Access Point SpanConfig hapConfig; // track configuration changes to the HAP Accessory database; used to increment the configuration number (c#) when changes found vector Accessories; // vector of pointers to all Accessories vector Loops; // vector of pointer to all Services that have over-ridden loop() methods vector Notifications; // vector of SpanBuf objects that store info for Characteristics that are updated with setVal() and require a Notification Event vector PushButtons; // vector of pointer to all PushButtons unordered_map TimedWrites; // map of timed-write PIDs and Alarm Times (based on TTLs) unordered_map UserCommands; // map of pointers to all UserCommands void begin(Category catID=DEFAULT_CATEGORY, const char *displayName=DEFAULT_DISPLAY_NAME, const char *hostNameBase=DEFAULT_HOST_NAME, const char *modelName=DEFAULT_MODEL_NAME); void poll(); // poll HAP Clients and process any new HAP requests int getFreeSlot(); // returns free HAPClient slot number. HAPClients slot keep track of each active HAPClient connection void checkConnect(); // check WiFi connection; connect if needed void commandMode(); // allows user to control and reset HomeSpan settings with the control button void processSerialCommand(const char *c); // process command 'c' (typically from readSerial, though can be called with any 'c') void checkRanges(); // checks values of all Characteristics to ensure they are each within range int sprintfAttributes(char *cBuf); // prints Attributes JSON database into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL void prettyPrint(char *buf, int nsp=2); // print arbitrary JSON from buf to serial monitor, formatted with indentions of 'nsp' spaces SpanCharacteristic *find(uint32_t aid, int iid); // return Characteristic with matching aid and iid (else NULL if not found) int countCharacteristics(char *buf); // return number of characteristic objects referenced in PUT /characteristics JSON request int updateCharacteristics(char *buf, SpanBuf *pObj); // parses PUT /characteristics JSON request 'buf into 'pObj' and updates referenced characteristics; returns 1 on success, 0 on fail int sprintfAttributes(SpanBuf *pObj, int nObj, char *cBuf); // prints SpanBuf object into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL int sprintfAttributes(char **ids, int numIDs, int flags, char *cBuf); // prints accessory.characteristic ids into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL void clearNotify(int slotNum); // set ev notification flags for connection 'slotNum' to false across all characteristics int sprintfNotify(SpanBuf *pObj, int nObj, char *cBuf, int conNum); // prints notification JSON into buf based on SpanBuf objects and specified connection number void setControlPin(uint8_t pin){controlPin=pin;} // sets Control Pin void setStatusPin(uint8_t pin){statusPin=pin;} // sets Status Pin void setStatusAutoOff(uint16_t duration){autoOffLED=duration;} // sets Status LED auto off (seconds) int getStatusPin(){return(statusPin);} // get Status Pin void setApSSID(const char *ssid){network.apSSID=ssid;} // sets Access Point SSID void setApPassword(const char *pwd){network.apPassword=pwd;} // sets Access Point Password void setApTimeout(uint16_t nSec){network.lifetime=nSec*1000;} // sets Access Point Timeout (seconds) void setCommandTimeout(uint16_t nSec){comModeLife=nSec*1000;} // sets Command Mode Timeout (seconds) void setLogLevel(uint8_t level){logLevel=level;} // sets Log Level for log messages (0=baseline, 1=intermediate, 2=all) void reserveSocketConnections(uint8_t n){maxConnections-=n;} // reserves n socket connections *not* to be used for HAP void setHostNameSuffix(const char *suffix){hostNameSuffix=suffix;} // sets the hostName suffix to be used instead of the 6-byte AccessoryID void setPortNum(uint16_t port){tcpPortNum=port;} // sets the TCP port number to use for communications between HomeKit and HomeSpan void setQRID(const char *id); // sets the Setup ID for optional pairing with a QR Code void setSketchVersion(const char *sVer){sketchVersion=sVer;} // set optional sketch version number const char *getSketchVersion(){return sketchVersion;} // get sketch version number void setWifiCallback(void (*f)()){wifiCallback=f;} // sets an optional user-defined function to call once WiFi connectivity is established void setPairCallback(void (*f)(boolean isPaired)){pairCallback=f;} // sets an optional user-defined function to call when Pairing is established (true) or lost (false) void setApFunction(void (*f)()){apFunction=f;} // sets an optional user-defined function to call when activating the WiFi Access Point void enableAutoStartAP(){autoStartAPEnabled=true;} // enables auto start-up of Access Point when WiFi Credentials not found void setWifiCredentials(const char *ssid, const char *pwd); // sets WiFi Credentials void setPairingCode(const char *s){sprintf(pairingCodeCommand,"S %9s",s);} // sets the Pairing Code - use is NOT recommended. Use 'S' from CLI instead void deleteStoredValues(){processSerialCommand("V");} // deletes stored Characteristic values from NVS void enableOTA(boolean auth=true){otaEnabled=true;otaAuth=auth;reserveSocketConnections(1);} // enables Over-the-Air updates, with (auth=true) or without (auth=false) authorization password [[deprecated("Please use reserveSocketConnections(n) method instead.")]] void setMaxConnections(uint8_t n){requestedMaxCon=n;} // sets maximum number of simultaneous HAP connections }; /////////////////////////////// struct SpanAccessory{ uint32_t aid=0; // Accessory Instance ID (HAP Table 6-1) int iidCount=0; // running count of iid to use for Services and Characteristics associated with this Accessory vector Services; // vector of pointers to all Services in this Accessory SpanAccessory(uint32_t aid=0); int sprintfAttributes(char *cBuf); // prints Accessory JSON database into buf, unless buf=NULL; return number of characters printed, excluding null terminator, even if buf=NULL void validate(); // error-checks Accessory }; /////////////////////////////// struct SpanService{ int iid=0; // Instance ID (HAP Table 6-2) const char *type; // Service Type const char *hapName; // HAP Name boolean hidden=false; // optional property indicating service is hidden boolean primary=false; // optional property indicating service is primary vector Characteristics; // vector of pointers to all Characteristics in this Service vector req; // vector of pointers to all required HAP Characteristic Types for this Service vector opt; // vector of pointers to all optional HAP Characteristic Types for this Service vector linkedServices; // vector of pointers to any optional linked Services SpanService(const char *type, const char *hapName); SpanService *setPrimary(); // sets the Service Type to be primary and returns pointer to self SpanService *setHidden(); // sets the Service Type to be hidden and returns pointer to self SpanService *addLink(SpanService *svc); // adds svc as a Linked Service and returns pointer to self vector getLinks(){return(linkedServices);} // returns linkedServices vector for use as range in "for-each" loops int sprintfAttributes(char *cBuf); // prints Service JSON records into buf; return number of characters printed, excluding null terminator void validate(); // error-checks Service virtual boolean update() {return(true);} // placeholder for code that is called when a Service is updated via a Controller. Must return true/false depending on success of update virtual void loop(){} // loops for each Service - called every cycle and can be over-ridden with user-defined code virtual void button(int pin, int pressType){} // method called for a Service when a button attached to "pin" has a Single, Double, or Long Press, according to pressType }; /////////////////////////////// struct SpanCharacteristic{ union UVal { BOOL_t BOOL; UINT8_t UINT8; UINT16_t UINT16; UINT32_t UINT32; UINT64_t UINT64; INT_t INT; FLOAT_t FLOAT; STRING_t STRING = NULL; }; int iid=0; // Instance ID (HAP Table 6-3) const char *type; // Characteristic Type const char *hapName; // HAP Name UVal value; // Characteristic Value uint8_t perms; // Characteristic Permissions FORMAT format; // Characteristic Format char *desc=NULL; // Characteristic Description (optional) char *unit=NULL; // Characteristic Unit (optional) UVal minValue; // Characteristic minimum (not applicable for STRING) UVal maxValue; // Characteristic maximum (not applicable for STRING) UVal stepValue; // Characteristic step size (not applicable for STRING) boolean staticRange; // Flag that indicates whether Range is static and cannot be changed with setRange() boolean customRange=false; // Flag for custom ranges const char *validValues=NULL; // Optional JSON array of valid values. Applicable only to uint8 Characteristics boolean *ev; // Characteristic Event Notify Enable (per-connection) char *nvsKey=NULL; // key for NVS storage of Characteristic value uint32_t aid=0; // Accessory ID - passed through from Service containing this Characteristic boolean isUpdated=false; // set to true when new value has been requested by PUT /characteristic unsigned long updateTime=0; // last time value was updated (in millis) either by PUT /characteristic OR by setVal() UVal newValue; // the updated value requested by PUT /characteristic SpanService *service=NULL; // pointer to Service containing this Characteristic SpanCharacteristic(HapChar *hapChar); // contructor int sprintfAttributes(char *cBuf, int flags); // prints Characteristic JSON records into buf, according to flags mask; return number of characters printed, excluding null terminator StatusCode loadUpdate(char *val, char *ev); // load updated val/ev from PUT /characteristic JSON request. Return intiial HAP status code (checks to see if characteristic is found, is writable, etc.) boolean updated(){return(isUpdated);} // returns isUpdated unsigned long timeVal(); // returns time elapsed (in millis) since value was last updated SpanCharacteristic *setValidValues(int n, ...); // sets a list of 'n' valid values allowed for a Characteristic and returns pointer to self. Only applicable if format=uint8 String uvPrint(UVal &u){ char c[64]; switch(format){ case FORMAT::BOOL: return(String(u.BOOL)); case FORMAT::INT: return(String(u.INT)); case FORMAT::UINT8: return(String(u.UINT8)); case FORMAT::UINT16: return(String(u.UINT16)); case FORMAT::UINT32: return(String(u.UINT32)); case FORMAT::UINT64: sprintf(c,"%llu",u.UINT64); return(String(c)); case FORMAT::FLOAT: sprintf(c,"%llg",u.FLOAT); return(String(c)); case FORMAT::STRING: sprintf(c,"\"%s\"",u.STRING); return(String(c)); } // switch return(String()); // included to prevent compiler warnings } void uvSet(UVal &dest, UVal &src){ if(format==FORMAT::STRING) uvSet(dest,(const char *)src.STRING); else dest=src; } void uvSet(UVal &u, const char *val){ u.STRING = (char *)realloc(u.STRING, strlen(val) + 1); strcpy(u.STRING, val); } template void uvSet(UVal &u, T val){ switch(format){ case FORMAT::BOOL: u.BOOL=(boolean)val; break; case FORMAT::INT: u.INT=(int)val; break; case FORMAT::UINT8: u.UINT8=(uint8_t)val; break; case FORMAT::UINT16: u.UINT16=(uint16_t)val; break; case FORMAT::UINT32: u.UINT32=(uint32_t)val; break; case FORMAT::UINT64: u.UINT64=(uint64_t)val; break; case FORMAT::FLOAT: u.FLOAT=(double)val; break; } // switch } template T uvGet(UVal &u){ switch(format){ case FORMAT::BOOL: return((T) u.BOOL); case FORMAT::INT: return((T) u.INT); case FORMAT::UINT8: return((T) u.UINT8); case FORMAT::UINT16: return((T) u.UINT16); case FORMAT::UINT32: return((T) u.UINT32); case FORMAT::UINT64: return((T) u.UINT64); case FORMAT::FLOAT: return((T) u.FLOAT); } return(0); // included to prevent compiler warnings } template SpanCharacteristic *setRange(A min, B max, S step=0){ char c[256]; homeSpan.configLog+=String(" \u2b0c Set Range for ") + String(hapName) + " with AID=" + String(aid) + ", IID=" + String(iid); if(customRange){ sprintf(c," *** ERROR! Range already set for this Characteristic! ***\n"); homeSpan.nFatalErrors++; } else if(staticRange){ sprintf(c," *** ERROR! Can't change range for this Characteristic! ***\n"); homeSpan.nFatalErrors++; } else { uvSet(minValue,min); uvSet(maxValue,max); uvSet(stepValue,step); customRange=true; if(uvGet(stepValue)>0) sprintf(c,": Min=%s, Max=%s, Step=%s\n",uvPrint(minValue),uvPrint(maxValue),uvPrint(stepValue)); else sprintf(c,": Min=%s, Max=%s\n",uvPrint(minValue),uvPrint(maxValue)); } homeSpan.configLog+=c; return(this); } // setRange() template void init(T val, boolean nvsStore, A min=0, B max=1){ int nvsFlag=0; uvSet(value,val); if(nvsStore){ nvsKey=(char *)malloc(16); uint16_t t; sscanf(type,"%x",&t); sprintf(nvsKey,"%04X%08X%03X",t,aid,iid&0xFFF); size_t len; if(format != FORMAT::STRING){ if(!nvs_get_blob(homeSpan.charNVS,nvsKey,NULL,&len)){ nvs_get_blob(homeSpan.charNVS,nvsKey,&value,&len); nvsFlag=2; } else { nvs_set_blob(homeSpan.charNVS,nvsKey,&value,sizeof(UVal)); // store data nvs_commit(homeSpan.charNVS); // commit to NVS nvsFlag=1; } } else { if(!nvs_get_str(homeSpan.charNVS,nvsKey,NULL,&len)){ char c[len]; nvs_get_str(homeSpan.charNVS,nvsKey,c,&len); uvSet(value,(const char *)c); nvsFlag=2; } else { nvs_set_str(homeSpan.charNVS,nvsKey,value.STRING); // store string data nvs_commit(homeSpan.charNVS); // commit to NVS nvsFlag=1; } } } uvSet(newValue,value); if(format != FORMAT::STRING) { uvSet(minValue,min); uvSet(maxValue,max); uvSet(stepValue,0); } int x=0; sscanf(type,"%*8[0-9a-fA-F]-%*4[0-9a-fA-F]-%*4[0-9a-fA-F]-%*4[0-9a-fA-F]-%*12[0-9a-fA-F]%n",&x); boolean isCustom=(strlen(type)==36 && x==36); homeSpan.configLog+="(" + uvPrint(value) + ")" + ": IID=" + String(iid) + ", " + (isCustom?"Custom-":"") + "UUID=\"" + String(type) + "\""; if(format!=FORMAT::STRING && format!=FORMAT::BOOL) homeSpan.configLog+= ", Range=[" + String(uvPrint(minValue)) + "," + String(uvPrint(maxValue)) + "]"; if(nvsFlag==2) homeSpan.configLog+=" (restored)"; else if(nvsFlag==1) homeSpan.configLog+=" (storing)"; boolean valid=isCustom; for(int i=0; !valid && iServices.back()->req.size(); i++) valid=!strcmp(type,homeSpan.Accessories.back()->Services.back()->req[i]->type); for(int i=0; !valid && iServices.back()->opt.size(); i++) valid=!strcmp(type,homeSpan.Accessories.back()->Services.back()->opt[i]->type); if(!valid){ homeSpan.configLog+=" *** WARNING! Service does not support this Characteristic. ***"; homeSpan.nWarnings++; } boolean repeated=false; for(int i=0; !repeated && iServices.back()->Characteristics.size(); i++) repeated=!strcmp(type,homeSpan.Accessories.back()->Services.back()->Characteristics[i]->type); if(valid && repeated){ homeSpan.configLog+=" *** ERROR! Characteristic already defined for this Service. ***"; homeSpan.nFatalErrors++; } homeSpan.Accessories.back()->Services.back()->Characteristics.push_back(this); homeSpan.configLog+="\n"; } // init() template T getVal(){ return(uvGet(value)); } template T getNewVal(){ return(uvGet(newValue)); } char *getString(){ if(format == FORMAT::STRING) return value.STRING; return NULL; } char *getNewString(){ if(format == FORMAT::STRING) return newValue.STRING; return NULL; } void setString(const char *val){ if((perms & EV) == 0){ Serial.printf("\n*** WARNING: Attempt to update Characteristic::%s with setVal() ignored. No NOTIFICATION permission on this characteristic\n\n",hapName); return; } uvSet(value,val); uvSet(newValue,value); updateTime=homeSpan.snapTime; SpanBuf sb; // create SpanBuf object sb.characteristic=this; // set characteristic sb.status=StatusCode::OK; // set status char dummy[]=""; sb.val=dummy; // set dummy "val" so that sprintfNotify knows to consider this "update" homeSpan.Notifications.push_back(sb); // store SpanBuf in Notifications vector if(nvsKey){ nvs_set_str(homeSpan.charNVS,nvsKey,value.STRING); // store data nvs_commit(homeSpan.charNVS); } } // setString() template void setVal(T val, boolean notify=true){ if((perms & EV) == 0){ Serial.printf("\n*** WARNING: Attempt to update Characteristic::%s with setVal() ignored. No NOTIFICATION permission on this characteristic\n\n",hapName); return; } if(val < uvGet(minValue) || val > uvGet(maxValue)){ Serial.printf("\n*** WARNING: Attempt to update Characteristic::%s with setVal(%llg) is out of range [%llg,%llg]. This may cause device to become non-reponsive!\n\n", hapName,(double)val,uvGet(minValue),uvGet(maxValue)); } uvSet(value,val); uvSet(newValue,value); updateTime=homeSpan.snapTime; if(notify){ SpanBuf sb; // create SpanBuf object sb.characteristic=this; // set characteristic sb.status=StatusCode::OK; // set status char dummy[]=""; sb.val=dummy; // set dummy "val" so that sprintfNotify knows to consider this "update" homeSpan.Notifications.push_back(sb); // store SpanBuf in Notifications vector if(nvsKey){ nvs_set_blob(homeSpan.charNVS,nvsKey,&value,sizeof(UVal)); // store data nvs_commit(homeSpan.charNVS); } } } // setVal() SpanCharacteristic *setPerms(uint8_t perms){ this->perms=perms; homeSpan.configLog+=String(" \u2b0c Change Permissions for ") + String(hapName) + " with AID=" + String(aid) + ", IID=" + String(iid) + ":"; char pNames[][7]={"PR","PW","EV","AA","TW","HD","WR"}; char sep=' '; for(uint8_t i=0;i<7;i++){ if(perms & (1<