HomeSpan/src/HomeSpan.cpp

2095 lines
71 KiB
C++

/*********************************************************************************
* MIT License
*
* Copyright (c) 2020-2022 Gregg E. Berman
*
* https://github.com/HomeSpan/HomeSpan
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
********************************************************************************/
#include <ESPmDNS.h>
#include <nvs_flash.h>
#include <sodium.h>
#include <WiFi.h>
#include <driver/ledc.h>
#include <mbedtls/version.h>
#include <esp_task_wdt.h>
#include <esp_sntp.h>
#include <esp_ota_ops.h>
#include "HomeSpan.h"
#include "HAP.h"
const __attribute__((section(".rodata_custom_desc"))) SpanPartition spanPartition = {HOMESPAN_MAGIC_COOKIE};
using namespace Utils;
HAPClient **hap; // 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)
HapCharacteristics hapChars; // Instantiation of all HAP Characteristics (used to create SpanCharacteristics)
///////////////////////////////
// Span //
///////////////////////////////
void Span::begin(Category catID, const char *displayName, const char *hostNameBase, const char *modelName){
this->displayName=displayName;
this->hostNameBase=hostNameBase;
this->modelName=modelName;
sprintf(this->category,"%d",(int)catID);
esp_task_wdt_delete(xTaskGetIdleTaskHandleForCPU(0)); // required to avoid watchdog timeout messages from ESP32-C3
controlButton.init(controlPin);
statusLED.init(statusPin,0,autoOffLED);
if(requestedMaxCon<maxConnections) // if specific request for max connections is less than computed max connections
maxConnections=requestedMaxCon; // over-ride max connections with requested value
hap=(HAPClient **)calloc(maxConnections,sizeof(HAPClient *));
for(int i=0;i<maxConnections;i++)
hap[i]=new HAPClient;
hapServer=new WiFiServer(tcpPortNum);
nvs_flash_init(); // initialize non-volatile-storage partition in flash
nvs_open("CHAR",NVS_READWRITE,&charNVS); // open Characteristic data namespace in NVS
nvs_open("WIFI",NVS_READWRITE,&wifiNVS); // open WIFI data namespace in NVS
nvs_open("OTA",NVS_READWRITE,&otaNVS); // open OTA data namespace in NVS
size_t len;
if(strlen(network.wifiData.ssid)){ // if setWifiCredentials was already called
nvs_set_blob(wifiNVS,"WIFIDATA",&network.wifiData,sizeof(network.wifiData)); // update data
nvs_commit(wifiNVS); // commit to NVS
} else
if(!nvs_get_blob(wifiNVS,"WIFIDATA",NULL,&len)) // else if found WiFi data in NVS
nvs_get_blob(wifiNVS,"WIFIDATA",&homeSpan.network.wifiData,&len); // retrieve data
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\n");
Serial.print("Message Logs: Level ");
Serial.print(logLevel);
Serial.print("\nStatus LED: Pin ");
if(statusPin>=0){
Serial.print(statusPin);
if(autoOffLED>0)
Serial.printf(" (Auto Off=%d sec)",autoOffLED);
}
else
Serial.print("- *** WARNING: Status LED Pin is UNDEFINED");
Serial.print("\nDevice Control: Pin ");
if(controlPin>=0)
Serial.print(controlPin);
else
Serial.print("- *** WARNING: Device Control Pin is UNDEFINED");
Serial.print("\nSketch Version: ");
Serial.print(getSketchVersion());
Serial.print("\nHomeSpan Version: ");
Serial.print(HOMESPAN_VERSION);
Serial.print("\nArduino-ESP Ver.: ");
Serial.print(ARDUINO_ESP_VERSION);
Serial.printf("\nESP-IDF Version: %d.%d.%d",ESP_IDF_VERSION_MAJOR,ESP_IDF_VERSION_MINOR,ESP_IDF_VERSION_PATCH);
Serial.printf("\nESP32 Chip: %s Rev %d %s-core %dMB Flash", ESP.getChipModel(),ESP.getChipRevision(),
ESP.getChipCores()==1?"single":"dual",ESP.getFlashChipSize()/1024/1024);
#ifdef ARDUINO_VARIANT
Serial.print("\nESP32 Board: ");
Serial.print(ARDUINO_VARIANT);
#endif
Serial.printf("\nPWM Resources: %d channels, %d timers, max %d-bit duty resolution",
LEDC_SPEED_MODE_MAX*LEDC_CHANNEL_MAX,LEDC_SPEED_MODE_MAX*LEDC_TIMER_MAX,LEDC_TIMER_BIT_MAX-1);
Serial.printf("\nSodium Version: %s Lib %d.%d",sodium_version_string(),sodium_library_version_major(),sodium_library_version_minor());
char mbtlsv[64];
mbedtls_version_get_string_full(mbtlsv);
Serial.printf("\nMbedTLS Version: %s",mbtlsv);
Serial.print("\nSketch Compiled: ");
Serial.print(__DATE__);
Serial.print(" ");
Serial.print(__TIME__);
uint8_t prevSHA[32]={0};
uint8_t sha256[32];
if(!nvs_get_blob(otaNVS,"SHA256",NULL,&len)) // get previous app SHA256 (if it exists)
nvs_get_blob(otaNVS,"SHA256",prevSHA,&len);
esp_partition_get_sha256(esp_ota_get_running_partition(),sha256); // get current app SHA256
newCode=(memcmp(prevSHA,sha256,32)!=0); // set newCode flag based on comparison of previous and current SHA256 values
nvs_set_blob(otaNVS,"SHA256",sha256,sizeof(sha256)); // save current SHA256
nvs_commit(otaNVS);
esp_ota_img_states_t otaState;
esp_ota_get_state_partition(esp_ota_get_running_partition(),&otaState);
Serial.printf("\nPartition: %s (%s-0x%0X)",esp_ota_get_running_partition()->label,newCode?"NEW":"REBOOTED",otaState);
Serial.print("\n\nDevice Name: ");
Serial.print(displayName);
Serial.print("\n\n");
uint8_t otaRequired=0;
nvs_get_u8(otaNVS,"OTA_REQUIRED",&otaRequired);
nvs_set_u8(otaNVS,"OTA_REQUIRED",0);
nvs_commit(otaNVS);
if(otaRequired && !spanOTA.enabled){
Serial.printf("\n\n*** OTA SAFE MODE ALERT: OTA REQUIRED BUT NOT ENABLED. ROLLING BACK TO PREVIOUS APPLICATION ***\n\n");
delay(100);
esp_ota_mark_app_invalid_rollback_and_reboot();
}
} // 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);
}
if(!isInitialized){
if(!homeSpan.Accessories.empty()){
if(!homeSpan.Accessories.back()->Services.empty())
homeSpan.Accessories.back()->Services.back()->validate();
homeSpan.Accessories.back()->validate();
}
checkRanges();
if(nWarnings>0){
configLog+="\n*** CAUTION: There " + String((nWarnings>1?"are ":"is ")) + String(nWarnings) + " WARNING" + (nWarnings>1?"S":"") + " associated with this configuration that may lead to the device becoming non-responsive, or operating in an unexpected manner. ***\n";
}
processSerialCommand("i"); // print homeSpan configuration info
if(nFatalErrors>0){
Serial.print("\n*** PROGRAM HALTED DUE TO ");
Serial.print(nFatalErrors);
Serial.print(" FATAL ERROR");
if(nFatalErrors>1)
Serial.print("S");
Serial.print(" IN CONFIGURATION! ***\n\n");
while(1);
}
Serial.print("\n");
HAPClient::init(); // read NVS and load HAP settings
if(!strlen(network.wifiData.ssid)){
Serial.print("*** WIFI CREDENTIALS DATA NOT FOUND. ");
if(autoStartAPEnabled){
Serial.print("AUTO-START OF ACCESS POINT ENABLED...\n\n");
processSerialCommand("A");
} else {
Serial.print("YOU MAY CONFIGURE BY TYPING 'W <RETURN>'.\n\n");
statusLED.start(LED_WIFI_NEEDED);
}
} else {
homeSpan.statusLED.start(LED_WIFI_CONNECTING);
}
controlButton.reset();
Serial.print(displayName);
Serial.print(" is READY!\n\n");
isInitialized=true;
} // isInitialized
if(strlen(network.wifiData.ssid)>0){
checkConnect();
}
char cBuf[17]="?";
if(Serial.available()){
readSerial(cBuf,16);
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(maxConnections);
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(" on Socket ");
LOG1(hap[freeSlot]->client.fd()-LWIP_SOCKET_OFFSET+1);
LOG1("/");
LOG1(CONFIG_LWIP_MAX_SOCKETS);
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<maxConnections;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
homeSpan.lastClientIP=hap[i]->client.remoteIP().toString(); // store IP Address for web logging
hap[i]->processRequest(); // process HAP request
homeSpan.lastClientIP="0.0.0.0"; // reset stored IP address to show "0.0.0.0" if homeSpan.getClientIP() is used in any other context
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();
if(spanOTA.enabled)
ArduinoOTA.handle();
if(controlButton.primed()){
statusLED.start(LED_ALERT);
}
if(controlButton.triggered(3000,10000)){
statusLED.off();
if(controlButton.type()==PushButton::LONG){
controlButton.wait();
processSerialCommand("F"); // FACTORY RESET
} else {
commandMode(); // COMMAND MODE
}
}
statusLED.check();
} // poll
///////////////////////////////
int Span::getFreeSlot(){
for(int i=0;i<maxConnections;i++){
if(!hap[i]->client)
return(i);
}
return(-1);
}
//////////////////////////////////////
void Span::commandMode(){
Serial.print("*** ENTERING COMMAND MODE ***\n\n");
int mode=1;
boolean done=false;
statusLED.start(500,0.3,mode,1000);
unsigned long alarmTime=millis()+comModeLife;
while(!done){
if(millis()>alarmTime){
Serial.print("*** Command Mode: Timed Out (");
Serial.print(comModeLife/1000);
Serial.print(" seconds).\n\n");
mode=1;
done=true;
statusLED.start(LED_ALERT);
delay(2000);
} else
if(controlButton.triggered(10,3000)){
if(controlButton.type()==PushButton::SINGLE){
mode++;
if(mode==6)
mode=1;
statusLED.start(500,0.3,mode,1000);
} else {
done=true;
}
} // button press
} // while
statusLED.start(LED_ALERT);
controlButton.wait();
switch(mode){
case 1:
Serial.print("*** NO ACTION\n\n");
if(strlen(network.wifiData.ssid)==0)
statusLED.start(LED_WIFI_NEEDED);
else
if(!HAPClient::nAdminControllers())
statusLED.start(LED_PAIRING_NEEDED);
else
statusLED.on();
break;
case 2:
processSerialCommand("R");
break;
case 3:
processSerialCommand("A");
break;
case 4:
processSerialCommand("U");
break;
case 5:
processSerialCommand("X");
break;
} // switch
Serial.print("*** EXITING COMMAND MODE ***\n\n");
}
//////////////////////////////////////
void Span::checkConnect(){
if(connected){
if(WiFi.status()==WL_CONNECTED)
return;
Serial.print("\n\n*** WiFi Connection Lost!\n"); // losing and re-establishing connection has not been tested
connected=false;
waitTime=60000;
alarmConnect=0;
homeSpan.statusLED.start(LED_WIFI_CONNECTING);
}
if(WiFi.status()!=WL_CONNECTED){
if(millis()<alarmConnect) // not yet time to try to try connecting
return;
if(waitTime==60000)
waitTime=1000;
else
waitTime*=2;
if(waitTime==32000){
Serial.print("\n*** Can't connect to ");
Serial.print(network.wifiData.ssid);
Serial.print(". You may type 'W <return>' to re-configure WiFi, or 'X <return>' to erase WiFi credentials. Will try connecting again in 60 seconds.\n\n");
waitTime=60000;
} else {
Serial.print("Trying to connect to ");
Serial.print(network.wifiData.ssid);
Serial.print(". Waiting ");
Serial.print(waitTime/1000);
Serial.print(" second(s) for response...\n");
WiFi.begin(network.wifiData.ssid,network.wifiData.pwd);
}
alarmConnect=millis()+waitTime;
return;
}
connected=true;
Serial.print("Successfully connected to ");
Serial.print(network.wifiData.ssid);
Serial.print("! IP Address: ");
Serial.print(WiFi.localIP());
Serial.print("\n");
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;
if(!hostNameSuffix)
nChars=snprintf(NULL,0,"%s-%.2s%.2s%.2s%.2s%.2s%.2s",hostNameBase,id,id+3,id+6,id+9,id+12,id+15);
else
nChars=snprintf(NULL,0,"%s%s",hostNameBase,hostNameSuffix);
char hostName[nChars+1];
if(!hostNameSuffix)
sprintf(hostName,"%s-%.2s%.2s%.2s%.2s%.2s%.2s",hostNameBase,id,id+3,id+6,id+9,id+12,id+15);
else
sprintf(hostName,"%s%s",hostNameBase,hostNameSuffix);
char d[strlen(hostName)+1];
sscanf(hostName,"%[A-Za-z0-9-]",d);
if(strlen(hostName)>255|| hostName[0]=='-' || hostName[strlen(hostName)-1]=='-' || strlen(hostName)!=strlen(d)){
Serial.printf("\n*** Error: Can't start MDNS due to invalid hostname '%s'.\n",hostName);
Serial.print("*** Hostname must consist of 255 or less alphanumeric characters or a hyphen, except that the hyphen cannot be the first or last character.\n");
Serial.print("*** PROGRAM HALTED!\n\n");
while(1);
}
Serial.print("\nStarting MDNS...\n\n");
Serial.print("HostName: ");
Serial.print(hostName);
Serial.print(".local:");
Serial.print(tcpPortNum);
Serial.print("\nDisplay Name: ");
Serial.print(displayName);
Serial.print("\nModel Name: ");
Serial.print(modelName);
Serial.print("\nSetup ID: ");
Serial.print(qrID);
Serial.print("\n\n");
MDNS.begin(hostName); // set server host name (.local implied)
MDNS.setInstanceName(displayName); // set server display name
MDNS.addService("_hap","_tcp",tcpPortNum); // advertise HAP service on specified port
// 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
mdns_service_txt_item_set("_hap","_tcp","hspn",HOMESPAN_VERSION); // HomeSpan Version Number (info only - NOT used by HAP)
mdns_service_txt_item_set("_hap","_tcp","ard-esp32",ARDUINO_ESP_VERSION); // Arduino-ESP32 Version Number (info only - NOT used by HAP)
mdns_service_txt_item_set("_hap","_tcp","board",ARDUINO_VARIANT); // Board Name (info only - NOT used by HAP)
mdns_service_txt_item_set("_hap","_tcp","sketch",sketchVersion); // Sketch Version (info only - NOT used by HAP)
uint8_t hashInput[22];
uint8_t hashOutput[64];
char setupHash[9];
size_t len;
memcpy(hashInput,qrID,4); // Create the Seup ID for use with optional QR Codes. This is an undocumented feature of HAP R2!
memcpy(hashInput+4,id,17); // Step 1: Concatenate 4-character Setup ID and 17-character Accessory ID into hashInput
mbedtls_sha512_ret(hashInput,21,hashOutput,0); // Step 2: Perform SHA-512 hash on combined 21-byte hashInput to create 64-byte hashOutput
mbedtls_base64_encode((uint8_t *)setupHash,9,&len,hashOutput,4); // Step 3: Encode the first 4 bytes of hashOutput in base64, which results in an 8-character, null-terminated, setupHash
mdns_service_txt_item_set("_hap","_tcp","sh",setupHash); // Step 4: broadcast the resulting Setup Hash
// boolean autoEnable=false;
// uint32_t otaStatus=0;
// nvs_get_u32(otaNVS,"OTASTATUS",&otaStatus);
//
// Serial.printf("*** OTA STATUS: %d ***\n\r",otaStatus);
//
// if(otaStatus&SpanOTA::OTA_DOWNLOADED ){ // if OTA was used for last download
// otaStatus^=SpanOTA::OTA_DOWNLOADED; // turn off OTA_DOWNLOADED flag
// if(!spanOTA.enabled && (otaStatus&SpanOTA::OTA_SAFEMODE)) // if OTA is not enabled, but it was last enabled in safe mode
// autoEnable=true; // activate auto-enable
//
// } else if(otaStatus&SpanOTA::OTA_BOOTED ){ // if OTA was present in last boot, but not used for download
// if(!newCode){ // if code has NOT changed
// if(!spanOTA.enabled && (otaStatus&SpanOTA::OTA_SAFEMODE)) // if OTA is not enabled, but it was last enabled in safe mode
// autoEnable=true; // activate auto-enable
// } else { // code has changed - do not activate auto-enable
// otaStatus^=SpanOTA::OTA_BOOTED; // turn off OTA_DOWNLOADED flag
// }
// }
//
// nvs_set_u32(otaNVS,"OTASTATUS",otaStatus);
// nvs_commit(otaNVS);
//
// if(autoEnable){
// spanOTA.enabled=true;
// spanOTA.auth=otaStatus&SpanOTA::OTA_AUTHORIZED;
// spanOTA.safeLoad=true;
// Serial.printf("OTA Safe Mode: OTA Auto-Enabled\n");
// }
if(spanOTA.enabled){
if(esp_ota_get_running_partition()!=esp_ota_get_next_update_partition(NULL)){
ArduinoOTA.setHostname(hostName);
if(spanOTA.auth)
ArduinoOTA.setPasswordHash(spanOTA.otaPwd);
ArduinoOTA.onStart(spanOTA.start).onEnd(spanOTA.end).onProgress(spanOTA.progress).onError(spanOTA.error);
ArduinoOTA.begin();
reserveSocketConnections(1);
Serial.print("Starting OTA Server: ");
Serial.print(displayName);
Serial.print(" at ");
Serial.print(WiFi.localIP());
Serial.print("\nAuthorization Password: ");
Serial.print(spanOTA.auth?"Enabled\n\n":"DISABLED!\n\n");
} else {
Serial.print("\n*** WARNING: Can't start OTA Server - Partition table used to compile this sketch is not configured for OTA.\n\n");
spanOTA.enabled=false;
}
}
mdns_service_txt_item_set("_hap","_tcp","ota",spanOTA.enabled?"yes":"no"); // OTA status (info only - NOT used by HAP)
if(webLog.isEnabled){
mdns_service_txt_item_set("_hap","_tcp","logURL",webLog.statusURL.c_str()+4); // Web Log status (info only - NOT used by HAP)
Serial.printf("Web Logging enabled at http://%s.local:%d%swith max number of entries=%d\n\n",hostName,tcpPortNum,webLog.statusURL.c_str()+4,webLog.maxEntries);
webLog.initTime();
}
Serial.printf("Starting HAP Server on port %d supporting %d simultaneous HomeKit Controller Connections...\n",tcpPortNum,maxConnections);
hapServer->begin();
Serial.print("\n");
if(!HAPClient::nAdminControllers()){
Serial.print("DEVICE NOT YET PAIRED -- PLEASE PAIR WITH HOMEKIT APP\n\n");
statusLED.start(LED_PAIRING_NEEDED);
} else {
statusLED.on();
}
if(wifiCallback)
wifiCallback();
} // initWiFi
///////////////////////////////
void Span::setQRID(const char *id){
char tBuf[5];
sscanf(id,"%4[0-9A-Za-z]",tBuf);
if(strlen(id)==4 && strlen(tBuf)==4){
sprintf(qrID,"%s",id);
}
} // setQRID
///////////////////////////////
void Span::processSerialCommand(const 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<maxConnections;i++){
Serial.print("Connection #");
Serial.print(i);
Serial.print(" ");
if(hap[i]->client){
Serial.print(hap[i]->client.remoteIP());
Serial.print(" on Socket ");
Serial.print(hap[i]->client.fd()-LWIP_SOCKET_OFFSET+1);
Serial.print("/");
Serial.print(CONFIG_LWIP_MAX_SOCKETS);
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\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 'Q': {
char tBuf[5];
const char *s=c+1+strspn(c+1," ");
sscanf(s," %4[0-9A-Za-z]",tBuf);
if(strlen(s)==4 && strlen(tBuf)==4){
sprintf(qrID,"%s",tBuf);
Serial.print("\nChanging default Setup ID for QR Code to: '");
Serial.print(qrID);
Serial.print("'. Will take effect after next restart.\n\n");
nvs_set_str(HAPClient::hapNVS,"SETUPID",qrID); // update data
nvs_commit(HAPClient::hapNVS);
} else {
Serial.print("\n*** Invalid request to change Setup ID for QR Code to: '");
Serial.print(s);
Serial.print("'. Setup ID must be exactly 4 alphanumeric characters (0-9, A-Z, and a-z).\n\n");
}
}
break;
case 'O': {
char textPwd[34]="\0";
Serial.print("\n>>> New OTA Password, or <return> to cancel request: ");
readSerial(textPwd,33);
if(strlen(textPwd)==0){
Serial.print("(cancelled)\n\n");
return;
}
if(strlen(textPwd)==33){
Serial.print("\n*** Sorry, 32 character limit - request cancelled\n\n");
return;
}
Serial.print(mask(textPwd,2));
Serial.print("\n");
MD5Builder otaPwdHash;
otaPwdHash.begin();
otaPwdHash.add(textPwd);
otaPwdHash.calculate();
otaPwdHash.getChars(spanOTA.otaPwd);
nvs_set_str(otaNVS,"OTADATA",spanOTA.otaPwd); // update data
nvs_commit(otaNVS);
Serial.print("... Accepted! Password change will take effect after next restart.\n");
if(!spanOTA.enabled)
Serial.print("... Note: OTA has not been enabled in this sketch.\n");
Serial.print("\n");
}
break;
case 'S': {
char buf[128];
char setupCode[10];
struct { // temporary structure to hold SRP verification code and salt stored in NVS
uint8_t salt[16];
uint8_t verifyCode[384];
} verifyData;
sscanf(c+1," %9[0-9]",setupCode);
if(strlen(setupCode)!=8){
Serial.print("\n*** Invalid request to change Setup Code. Code must be exactly 8 digits.\n\n");
} else
if(!network.allowedCode(setupCode)){
Serial.print("\n*** Invalid request to change Setup Code. Code too simple.\n\n");
} else {
sprintf(buf,"\n\nGenerating SRP verification data for new Setup Code: %.3s-%.2s-%.3s ... ",setupCode,setupCode+3,setupCode+5);
Serial.print(buf);
HAPClient::srp.createVerifyCode(setupCode,verifyData.verifyCode,verifyData.salt); // create verification code from default Setup Code and random salt
nvs_set_blob(HAPClient::srpNVS,"VERIFYDATA",&verifyData,sizeof(verifyData)); // update data
nvs_commit(HAPClient::srpNVS); // commit to NVS
Serial.print("New Code Saved!\n");
Serial.print("Setup Payload for Optional QR Code: ");
Serial.print(qrCode.get(atoi(setupCode),qrID,atoi(category)));
Serial.print("\n\n");
}
}
break;
case 'U': {
HAPClient::removeControllers(); // clear all Controller data
nvs_set_blob(HAPClient::hapNVS,"CONTROLLERS",HAPClient::controllers,sizeof(HAPClient::controllers)); // update data
nvs_commit(HAPClient::hapNVS); // commit to NVS
Serial.print("\n*** HomeSpan Pairing Data DELETED ***\n\n");
for(int i=0;i<maxConnections;i++){ // loop over all connection slots
if(hap[i]->client){ // if slot is connected
LOG1("*** Terminating Client #");
LOG1(i);
LOG1("\n");
hap[i]->client.stop();
}
}
Serial.print("\nDEVICE NOT YET PAIRED -- PLEASE PAIR WITH HOMEKIT APP\n\n");
mdns_service_txt_item_set("_hap","_tcp","sf","1"); // set Status Flag = 1 (Table 6-8)
if(strlen(network.wifiData.ssid)==0)
statusLED.start(LED_WIFI_NEEDED);
else
statusLED.start(LED_PAIRING_NEEDED);
}
break;
case 'W': {
if(strlen(network.wifiData.ssid)>0){
Serial.print("*** Stopping all current WiFi services...\n\n");
hapServer->end();
MDNS.end();
WiFi.disconnect();
}
network.serialConfigure();
nvs_set_blob(wifiNVS,"WIFIDATA",&network.wifiData,sizeof(network.wifiData)); // update data
nvs_commit(wifiNVS); // commit to NVS
Serial.print("\n*** WiFi Credentials SAVED! Re-starting ***\n\n");
statusLED.off();
delay(1000);
ESP.restart();
}
break;
case 'A': {
if(strlen(network.wifiData.ssid)>0){
Serial.print("*** Stopping all current WiFi services...\n\n");
hapServer->end();
MDNS.end();
WiFi.disconnect();
}
if(apFunction){
apFunction();
return;
}
network.apConfigure();
nvs_set_blob(wifiNVS,"WIFIDATA",&network.wifiData,sizeof(network.wifiData)); // update data
nvs_commit(wifiNVS); // commit to NVS
Serial.print("\n*** Credentials saved!\n\n");
if(strlen(network.setupCode)){
char s[10];
sprintf(s,"S%s",network.setupCode);
processSerialCommand(s);
} else {
Serial.print("*** Setup Code Unchanged\n");
}
Serial.print("\n*** Re-starting ***\n\n");
statusLED.off();
delay(1000);
ESP.restart(); // re-start device
}
break;
case 'X': {
statusLED.off();
nvs_erase_all(wifiNVS);
nvs_commit(wifiNVS);
Serial.print("\n*** WiFi Credentials ERASED! Re-starting...\n\n");
delay(1000);
ESP.restart(); // re-start device
}
break;
case 'V': {
nvs_erase_all(charNVS);
nvs_commit(charNVS);
Serial.print("\n*** Values for all saved Characteristics erased!\n\n");
}
break;
case 'H': {
statusLED.off();
nvs_erase_all(HAPClient::hapNVS);
nvs_commit(HAPClient::hapNVS);
Serial.print("\n*** HomeSpan Device ID and Pairing Data DELETED! Restarting...\n\n");
delay(1000);
ESP.restart();
}
break;
case 'R': {
statusLED.off();
Serial.print("\n*** Restarting...\n\n");
delay(1000);
ESP.restart();
}
break;
case 'F': {
statusLED.off();
nvs_erase_all(HAPClient::hapNVS);
nvs_commit(HAPClient::hapNVS);
nvs_erase_all(wifiNVS);
nvs_commit(wifiNVS);
nvs_erase_all(charNVS);
nvs_commit(charNVS);
nvs_erase_all(otaNVS);
nvs_commit(otaNVS);
Serial.print("\n*** FACTORY RESET! Restarting...\n\n");
delay(1000);
ESP.restart();
}
break;
case 'E': {
statusLED.off();
nvs_flash_erase();
Serial.print("\n*** ALL DATA ERASED! Restarting...\n\n");
delay(1000);
ESP.restart();
}
break;
case 'L': {
int level=0;
sscanf(c+1,"%d",&level);
if(level<0)
level=0;
if(level>2)
level=2;
Serial.print("\n*** Log Level set to ");
Serial.print(level);
Serial.print("\n\n");
delay(1000);
setLogLevel(level);
}
break;
case 'i':{
Serial.print("\n*** HomeSpan Info ***\n\n");
Serial.print(configLog);
Serial.print("\nConfigured as Bridge: ");
Serial.print(homeSpan.isBridge?"YES":"NO");
Serial.print("\n\n");
char d[]="------------------------------";
Serial.printf("%-30s %8s %10s %s %s %s %s %s\n","Service","UUID","AID","IID","Update","Loop","Button","Linked Services");
Serial.printf("%.30s %.8s %.10s %.3s %.6s %.4s %.6s %.15s\n",d,d,d,d,d,d,d,d);
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];
Serial.printf("%-30s %8.8s %10u %3d %6s %4s %6s ",s->hapName,s->type,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"
);
if(s->linkedServices.empty())
Serial.print("-");
for(int k=0;k<s->linkedServices.size();k++){
Serial.print(s->linkedServices[k]->iid);
if(k<s->linkedServices.size()-1)
Serial.print(",");
}
Serial.print("\n");
}
}
Serial.print("\n*** End Info ***\n");
}
break;
case '?': {
Serial.print("\n*** HomeSpan Commands ***\n\n");
Serial.print(" s - print connection status\n");
Serial.print(" i - print summary information about the HAP Database\n");
Serial.print(" d - print the full HAP Accessory Attributes Database in JSON format\n");
Serial.print("\n");
Serial.print(" W - configure WiFi Credentials and restart\n");
Serial.print(" X - delete WiFi Credentials and restart\n");
Serial.print(" S <code> - change the HomeKit Pairing Setup Code to <code>\n");
Serial.print(" Q <id> - change the HomeKit Setup ID for QR Codes to <id>\n");
Serial.print(" O - change the OTA password\n");
Serial.print(" A - start the HomeSpan Setup Access Point\n");
Serial.print("\n");
Serial.print(" V - delete value settings for all saved Characteristics\n");
Serial.print(" U - unpair device by deleting all Controller data\n");
Serial.print(" H - delete HomeKit Device ID as well as all Controller data and restart\n");
Serial.print("\n");
Serial.print(" R - restart device\n");
Serial.print(" F - factory reset and restart\n");
Serial.print(" E - erase ALL stored data and restart\n");
Serial.print("\n");
Serial.print(" L <level> - change the Log Level setting to <level>\n");
Serial.print("\n");
for(auto uCom=homeSpan.UserCommands.begin(); uCom!=homeSpan.UserCommands.end(); uCom++) // loop over all UserCommands using an iterator
Serial.printf(" @%c %s\n",uCom->first,uCom->second->s);
if(!homeSpan.UserCommands.empty())
Serial.print("\n");
Serial.print(" ? - print this list of commands\n\n");
Serial.print("*** End Commands ***\n\n");
}
break;
case '@':{
auto uCom=UserCommands.find(c[1]);
if(uCom!=UserCommands.end()){
if(uCom->second->userFunction1)
uCom->second->userFunction1(c+1);
else
uCom->second->userFunction2(c+1,uCom->second->userArg);
break;
}
}
default:
Serial.print("*** Unknown command: '");
Serial.print(c);
Serial.print("'. Type '?' for list of commands.\n");
break;
} // switch
}
///////////////////////////////
void Span::setWifiCredentials(const char *ssid, const char *pwd){
sprintf(network.wifiData.ssid,"%.*s",MAX_SSID,ssid);
sprintf(network.wifiData.pwd,"%.*s",MAX_PWD,pwd);
if(wifiNVS){ // is begin() already called and wifiNVS is open
nvs_set_blob(wifiNVS,"WIFIDATA",&network.wifiData,sizeof(network.wifiData)); // update data
nvs_commit(wifiNVS); // commit to NVS
}
}
///////////////////////////////
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(uint32_t aid, int iid){
int index=-1;
for(int i=0;i<Accessories.size();i++){ // loop over all Accessories to find aid
if(Accessories[i]->aid==aid){ // if match, save index into Accessories array
index=i;
break;
}
}
if(index<0) // fail if no match on aid
return(NULL);
for(int i=0;i<Accessories[index]->Services.size();i++){ // loop over all Services in this Accessory
for(int j=0;j<Accessories[index]->Services[i]->Characteristics.size();j++){ // loop over all Characteristics in this Service
if(iid == Accessories[index]->Services[i]->Characteristics[j]->iid) // if matching iid
return(Accessories[index]->Services[i]->Characteristics[j]); // return pointer to Characteristic
}
}
return(NULL); // fail if no match on iid
}
///////////////////////////////
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))){
sscanf(t3,"%u",&pObj[nObj].aid);
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,"}[]:,\"",&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()?StatusCode::OK:StatusCode::Unable; // update service and save statusCode as OK or Unable depending on whether return is true or false
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->uvSet(pObj[j].characteristic->value,pObj[j].characteristic->newValue); // update characteristic value with new value
if(pObj[j].characteristic->nvsKey){ // if storage key found
if(pObj[j].characteristic->format != FORMAT::STRING)
nvs_set_blob(charNVS,pObj[j].characteristic->nvsKey,&(pObj[j].characteristic->value),sizeof(pObj[j].characteristic->value)); // store data
else
nvs_set_str(charNVS,pObj[j].characteristic->nvsKey,pObj[j].characteristic->value.STRING); // store data
nvs_commit(charNVS);
}
LOG1(" (okay)\n");
} else { // if status not okay
pObj[j].characteristic->uvSet(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_NV); // 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\":%u,\"iid\":%d,\"status\":%d}",pObj[i].aid,pObj[i].iid,(int)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;
uint32_t aid;
int 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],"%u.%d",&aid,&iid); // parse aid and iid
Characteristics[i]=find(aid,iid); // find matching chararacteristic
if(Characteristics[i]){ // if found
if(Characteristics[i]->perms&PERMS::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],"%u.%d",&aid,&iid); // parse aid and iid
nChars+=snprintf(cBuf?(cBuf+nChars):NULL,cBuf?64:0,"{\"iid\":%d,\"aid\":%u}",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}",(int)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);
}
///////////////////////////////
void Span::checkRanges(){
boolean okay=true;
homeSpan.configLog+="\nRange Check:";
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++){
SpanCharacteristic *chr=Accessories[i]->Services[j]->Characteristics[k];
if(chr->format!=STRING && (chr->uvGet<double>(chr->value) < chr->uvGet<double>(chr->minValue) || chr->uvGet<double>(chr->value) > chr->uvGet<double>(chr->maxValue))){
char c[256];
sprintf(c,"\n \u2718 Characteristic %s with AID=%d, IID=%d: Initial value of %lg is out of range [%llg,%llg]",
chr->hapName,chr->aid,chr->iid,chr->uvGet<double>(chr->value),chr->uvGet<double>(chr->minValue),chr->uvGet<double>(chr->maxValue));
if(okay)
homeSpan.configLog+="\n";
homeSpan.configLog+=c;
homeSpan.nWarnings++;
okay=false;
}
}
}
}
if(okay)
homeSpan.configLog+=" No Warnings";
homeSpan.configLog+="\n\n";
}
///////////////////////////////
// SpanAccessory //
///////////////////////////////
SpanAccessory::SpanAccessory(uint32_t aid){
if(!homeSpan.Accessories.empty()){
if(homeSpan.Accessories.size()==HAPClient::MAX_ACCESSORIES){
Serial.print("\n\n*** FATAL ERROR: Can't create more than ");
Serial.print(HAPClient::MAX_ACCESSORIES);
Serial.print(" Accessories. Program Halting.\n\n");
while(1);
}
this->aid=homeSpan.Accessories.back()->aid+1;
if(!homeSpan.Accessories.back()->Services.empty())
homeSpan.Accessories.back()->Services.back()->validate();
homeSpan.Accessories.back()->validate();
} else {
this->aid=1;
}
homeSpan.Accessories.push_back(this);
if(aid>0){ // override with user-specified aid
this->aid=aid;
}
homeSpan.configLog+="\u27a4 Accessory: AID=" + String(this->aid);
for(int i=0;i<homeSpan.Accessories.size()-1;i++){
if(this->aid==homeSpan.Accessories[i]->aid){
homeSpan.configLog+=" *** ERROR! ID already in use for another Accessory. ***";
homeSpan.nFatalErrors++;
break;
}
}
if(homeSpan.Accessories.size()==1 && this->aid!=1){
homeSpan.configLog+=" *** ERROR! ID of first Accessory must always be 1. ***";
homeSpan.nFatalErrors++;
}
homeSpan.configLog+="\n";
}
///////////////////////////////
void SpanAccessory::validate(){
boolean foundInfo=false;
boolean foundProtocol=false;
for(int i=0;i<Services.size();i++){
if(!strcmp(Services[i]->type,"3E"))
foundInfo=true;
else if(!strcmp(Services[i]->type,"A2"))
foundProtocol=true;
else if(aid==1) // this is an Accessory with aid=1, but it has more than just AccessoryInfo and HAPProtocolInformation. So...
homeSpan.isBridge=false; // ...this is not a bridge device
}
if(!foundInfo){
homeSpan.configLog+=" \u2718 Service AccessoryInformation";
homeSpan.configLog+=" *** ERROR! Required Service for this Accessory not found. ***\n";
homeSpan.nFatalErrors++;
}
if(!foundProtocol && (aid==1 || !homeSpan.isBridge)){ // HAPProtocolInformation must always be present in Accessory if aid=1, and any other Accessory if the device is not a bridge)
homeSpan.configLog+=" \u2718 Service HAPProtocolInformation";
homeSpan.configLog+=" *** ERROR! Required Service for this Accessory not found. ***\n";
homeSpan.nFatalErrors++;
}
}
///////////////////////////////
int SpanAccessory::sprintfAttributes(char *cBuf){
int nBytes=0;
nBytes+=snprintf(cBuf,cBuf?64:0,"{\"aid\":%u,\"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, const char *hapName, boolean isCustom){
if(!homeSpan.Accessories.empty() && !homeSpan.Accessories.back()->Services.empty()) // this is not the first Service to be defined for this Accessory
homeSpan.Accessories.back()->Services.back()->validate();
this->type=type;
this->hapName=hapName;
this->isCustom=isCustom;
homeSpan.configLog+=" \u279f Service " + String(hapName);
if(homeSpan.Accessories.empty()){
homeSpan.configLog+=" *** ERROR! Can't create new Service without a defined Accessory! ***\n";
homeSpan.nFatalErrors++;
return;
}
homeSpan.Accessories.back()->Services.push_back(this);
iid=++(homeSpan.Accessories.back()->iidCount);
homeSpan.configLog+=": IID=" + String(iid) + ", " + (isCustom?"Custom-":"") + "UUID=\"" + String(type) + "\"";
if(Span::invalidUUID(type,isCustom)){
homeSpan.configLog+=" *** ERROR! Format of UUID is invalid. ***";
homeSpan.nFatalErrors++;
}
if(!strcmp(this->type,"3E") && iid!=1){
homeSpan.configLog+=" *** ERROR! The AccessoryInformation Service must be defined before any other Services in an Accessory. ***";
homeSpan.nFatalErrors++;
}
homeSpan.configLog+="\n";
}
///////////////////////////////
SpanService *SpanService::setPrimary(){
primary=true;
return(this);
}
///////////////////////////////
SpanService *SpanService::setHidden(){
hidden=true;
return(this);
}
///////////////////////////////
SpanService *SpanService::addLink(SpanService *svc){
linkedServices.push_back(svc);
return(this);
}
///////////////////////////////
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,");
if(!linkedServices.empty()){
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"\"linked\":[");
for(int i=0;i<linkedServices.size();i++){
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"%d",linkedServices[i]->iid);
if(i+1<linkedServices.size())
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",");
}
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,"],");
}
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);
}
///////////////////////////////
void SpanService::validate(){
for(int i=0;i<req.size();i++){
boolean valid=false;
for(int j=0;!valid && j<Characteristics.size();j++)
valid=!strcmp(req[i]->type,Characteristics[j]->type);
if(!valid){
homeSpan.configLog+=" \u2718 Characteristic " + String(req[i]->hapName);
homeSpan.configLog+=" *** WARNING! Required Characteristic for this Service not found. ***\n";
homeSpan.nWarnings++;
}
}
vector<HapChar *>().swap(opt);
vector<HapChar *>().swap(req);
}
///////////////////////////////
// SpanCharacteristic //
///////////////////////////////
SpanCharacteristic::SpanCharacteristic(HapChar *hapChar, boolean isCustom){
type=hapChar->type;
perms=hapChar->perms;
hapName=hapChar->hapName;
format=hapChar->format;
staticRange=hapChar->staticRange;
this->isCustom=isCustom;
homeSpan.configLog+=" \u21e8 Characteristic " + String(hapName);
if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){
homeSpan.configLog+=" *** ERROR! Can't create new Characteristic without a defined Service! ***\n";
homeSpan.nFatalErrors++;
return;
}
iid=++(homeSpan.Accessories.back()->iidCount);
service=homeSpan.Accessories.back()->Services.back();
aid=homeSpan.Accessories.back()->aid;
ev=(boolean *)calloc(homeSpan.maxConnections,sizeof(boolean));
}
///////////////////////////////
int SpanCharacteristic::sprintfAttributes(char *cBuf, int flags){
int nBytes=0;
const char permCodes[][7]={"pr","pw","ev","aa","tw","hd","wr"};
const char formatCodes[][8]={"bool","uint8","uint16","uint32","uint64","int","float","string"};
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);
if(perms&PR){
if(perms&NV && !(flags&GET_NV))
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":null");
else
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"value\":%s",uvPrint(value).c_str());
}
if(flags&GET_META){
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?64:0,",\"format\":\"%s\"",formatCodes[format]);
if(customRange && (flags&GET_META)){
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?128:0,",\"minValue\":%s,\"maxValue\":%s",uvPrint(minValue).c_str(),uvPrint(maxValue).c_str());
if(uvGet<float>(stepValue)>0)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?128:0,",\"minStep\":%s",uvPrint(stepValue).c_str());
}
if(unit){
if(strlen(unit)>0)
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?128:0,",\"unit\":\"%s\"",unit);
else
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?128:0,",\"unit\":null");
}
if(validValues){
nBytes+=snprintf(cBuf?(cBuf+nBytes):NULL,cBuf?128:0,",\"valid-values\":%s",validValues);
}
}
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\":%u",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(!strcmp(val,"false"))
newValue.INT=0;
else if(!strcmp(val,"true"))
newValue.INT=1;
else if(!sscanf(val,"%d",&newValue.INT))
return(StatusCode::InvalidValue);
break;
case UINT8:
if(!strcmp(val,"false"))
newValue.UINT8=0;
else if(!strcmp(val,"true"))
newValue.UINT8=1;
else if(!sscanf(val,"%hhu",&newValue.UINT8))
return(StatusCode::InvalidValue);
break;
case UINT16:
if(!strcmp(val,"false"))
newValue.UINT16=0;
else if(!strcmp(val,"true"))
newValue.UINT16=1;
else if(!sscanf(val,"%hu",&newValue.UINT16))
return(StatusCode::InvalidValue);
break;
case UINT32:
if(!strcmp(val,"false"))
newValue.UINT32=0;
else if(!strcmp(val,"true"))
newValue.UINT32=1;
else if(!sscanf(val,"%u",&newValue.UINT32))
return(StatusCode::InvalidValue);
break;
case UINT64:
if(!strcmp(val,"false"))
newValue.UINT64=0;
else if(!strcmp(val,"true"))
newValue.UINT64=1;
else if(!sscanf(val,"%llu",&newValue.UINT64))
return(StatusCode::InvalidValue);
break;
case FLOAT:
if(!sscanf(val,"%lg",&newValue.FLOAT))
return(StatusCode::InvalidValue);
break;
case STRING:
uvSet(newValue,(const char *)val);
break;
default:
break;
} // switch
isUpdated=true;
updateTime=homeSpan.snapTime;
return(StatusCode::TBD);
}
///////////////////////////////
unsigned long SpanCharacteristic::timeVal(){
return(homeSpan.snapTime-updateTime);
}
///////////////////////////////
SpanCharacteristic *SpanCharacteristic::setValidValues(int n, ...){
char c[256];
String *s = new String("[");
va_list vl;
va_start(vl,n);
for(int i=0;i<n;i++){
*s+=va_arg(vl,int);
if(i!=n-1)
*s+=",";
}
va_end(vl);
*s+="]";
homeSpan.configLog+=String(" \u2b0c Set Valid Values for ") + String(hapName) + " with IID=" + String(iid);
if(validValues){
sprintf(c," *** ERROR! Valid Values already set for this Characteristic! ***\n");
homeSpan.nFatalErrors++;
} else
if(format!=UINT8){
sprintf(c," *** ERROR! Can't set Valid Values for this Characteristic! ***\n");
homeSpan.nFatalErrors++;
} else {
validValues=s->c_str();
sprintf(c,": ValidValues=%s\n",validValues);
}
homeSpan.configLog+=c;
return(this);
}
///////////////////////////////
// SpanRange //
///////////////////////////////
SpanRange::SpanRange(int min, int max, int step){
if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty() || homeSpan.Accessories.back()->Services.back()->Characteristics.empty() ){
homeSpan.configLog+=" \u2718 SpanRange: *** ERROR! Can't create new Range without a defined Characteristic! ***\n";
homeSpan.nFatalErrors++;
} else {
homeSpan.Accessories.back()->Services.back()->Characteristics.back()->setRange(min,max,step);
}
}
///////////////////////////////
// SpanButton //
///////////////////////////////
SpanButton::SpanButton(int pin, uint16_t longTime, uint16_t singleTime, uint16_t doubleTime){
homeSpan.configLog+=" \u25bc SpanButton: Pin=" + String(pin) + ", Single=" + String(singleTime) + "ms, Double=" + String(doubleTime) + "ms, Long=" + String(longTime) + "ms";
if(homeSpan.Accessories.empty() || homeSpan.Accessories.back()->Services.empty()){
homeSpan.configLog+=" *** ERROR! Can't create new PushButton without a defined Service! ***\n";
homeSpan.nFatalErrors++;
return;
}
Serial.print("Configuring PushButton: Pin="); // initialization message
Serial.print(pin);
Serial.print("\n");
this->pin=pin;
this->longTime=longTime;
this->singleTime=singleTime;
this->doubleTime=doubleTime;
service=homeSpan.Accessories.back()->Services.back();
if((void(*)(int,int))(service->*(&SpanService::button))==(void(*)(int,int))(&SpanService::button)){
homeSpan.configLog+=" *** WARNING: No button() method defined for this PushButton! ***";
homeSpan.nWarnings++;
}
pushButton=new PushButton(pin); // create underlying PushButton
homeSpan.configLog+="\n";
homeSpan.PushButtons.push_back(this);
}
///////////////////////////////
// SpanUserCommand //
///////////////////////////////
SpanUserCommand::SpanUserCommand(char c, const char *s, void (*f)(const char *)){
this->s=s;
userFunction1=f;
homeSpan.UserCommands[c]=this;
}
///////////////////////////////
SpanUserCommand::SpanUserCommand(char c, const char *s, void (*f)(const char *, void *), void *arg){
this->s=s;
userFunction2=f;
userArg=arg;
homeSpan.UserCommands[c]=this;
}
///////////////////////////////
// SpanWebLog //
///////////////////////////////
void SpanWebLog::init(uint16_t maxEntries, const char *serv, const char *tz, const char *url){
isEnabled=true;
this->maxEntries=maxEntries;
timeServer=serv;
timeZone=tz;
statusURL="GET /" + String(url) + " ";
log = (log_t *)calloc(maxEntries,sizeof(log_t));
}
///////////////////////////////
void SpanWebLog::initTime(){
if(!timeServer)
return;
Serial.printf("Acquiring Time from %s (%s). Waiting %d second(s) for response... ",timeServer,timeZone,waitTime/1000);
configTzTime(timeZone,timeServer);
struct tm timeinfo;
if(getLocalTime(&timeinfo,waitTime)){
strftime(bootTime,sizeof(bootTime),"%c",&timeinfo);
Serial.printf("%s\n\n",bootTime);
homeSpan.reserveSocketConnections(1);
timeInit=true;
} else {
Serial.printf("Can't access Time Server - time-keeping not initialized!\n\n");
}
}
///////////////////////////////
void SpanWebLog::addLog(const char *fmt, ...){
if(maxEntries==0)
return;
int index=nEntries%maxEntries;
log[index].upTime=esp_timer_get_time();
if(timeInit)
getLocalTime(&log[index].clockTime,10);
else
log[index].clockTime.tm_year=0;
free(log[index].message);
va_list ap;
va_start(ap,fmt);
vasprintf(&log[index].message,fmt,ap);
va_end(ap);
log[index].clientIP=homeSpan.lastClientIP;
nEntries++;
if(homeSpan.logLevel>0)
Serial.printf("WEBLOG: %s\n",log[index].message);
}
///////////////////////////////
// SpanOTA //
///////////////////////////////
void SpanOTA::init(boolean _auth, boolean _safeLoad){
enabled=true;
safeLoad=_safeLoad;
auth=_auth;
}
///////////////////////////////
void SpanOTA::start(){
Serial.printf("\n*** Current Partition: %s\n*** New Partition: %s\n*** OTA Starting..",
esp_ota_get_running_partition()->label,esp_ota_get_next_update_partition(NULL)->label);
otaPercent=0;
homeSpan.statusLED.start(LED_OTA_STARTED);
}
///////////////////////////////
void SpanOTA::end(){
// nvs_set_u32(homeSpan.otaNVS, "OTASTATUS", OTA_DOWNLOADED | OTA_BOOTED | (auth?OTA_AUTHORIZED:0) | (safeLoad?OTA_SAFEMODE:0));
nvs_set_u8(homeSpan.otaNVS,"OTA_REQUIRED",safeLoad);
nvs_commit(homeSpan.otaNVS);
Serial.printf(" DONE! Rebooting...\n");
homeSpan.statusLED.off();
delay(100); // make sure commit it finished before reboot
}
///////////////////////////////
void SpanOTA::progress(uint32_t progress, uint32_t total){
int percent=progress*100/total;
if(percent/10 != otaPercent/10){
otaPercent=percent;
Serial.printf("%d%%..",progress*100/total);
}
if(safeLoad && progress==total){
SpanPartition newSpanPartition;
esp_partition_read(esp_ota_get_next_update_partition(NULL), sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t), &newSpanPartition, sizeof(newSpanPartition));
Serial.printf("Checking for HomeSpan Magic Cookie: %s..",newSpanPartition.magicCookie);
if(strcmp(newSpanPartition.magicCookie,spanPartition.magicCookie))
Update.abort();
}
}
///////////////////////////////
void SpanOTA::error(ota_error_t err){
Serial.printf("*** OTA Error[%u]: ", err);
if (err == OTA_AUTH_ERROR) Serial.println("Auth Failed\n");
else if (err == OTA_BEGIN_ERROR) Serial.println("Begin Failed\n");
else if (err == OTA_CONNECT_ERROR) Serial.println("Connect Failed\n");
else if (err == OTA_RECEIVE_ERROR) Serial.println("Receive Failed\n");
else if (err == OTA_END_ERROR) Serial.println("End Failed\n");
}
///////////////////////////////
int SpanOTA::otaPercent;
boolean SpanOTA::safeLoad;
boolean SpanOTA::enabled=false;
boolean SpanOTA::auth;