2095 lines
71 KiB
C++
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;
|
|
|
|
|