Enter Your Electronics & Design Project for a chance to win a $100 Shopping Cart! | Project14 Home | |
Monthly Themes | ||
Monthly Theme Poll |
PART 5 - THE HOUSE HUB:
From blog posts 1 through 4, we provided our fundamentals to home automation. To bring it all together, we'll now deep dive into the House Hub. For our main control hub, we are using a Raspberry Pi 3 A+ with a Matrix Creator Board. This gives us a full blown Linux computer, essential sensors, a security camera, and Voice Control through a small, wall mountable package (with a little 3D printing).
Raspberry Pi 3 A+https://www.element14.com/community/view-product.jspa?fsku=2946269&nsku=80AC9303&COM=noscriptProduct Link
Matrix Creator https://www.element14.com/community/view-product.jspa?fsku=2675819&nsku=05AC8548&COM=noscriptProduct Link
To have all of our home automation devices talk, we need to execute a C++ compiled program at startup on the Raspberry Pi. Within this program, it must be able to be a client for web sockets for standard and SSL connections. Also, it must be a server for web socket connections. This will allow for full communication to the devices within the house, including commercial devices such as the Nest Thermostat. In this blog, I'll first offer the code as standalone examples to serve future projects. I'll then combine the code to make our House Hub.
To get your Pi and Matrix ready, do the following:
- Install the Matrix Creator HAL package and Alexa App: https://www.hackster.io/matrix-labs/matrix-voice-and-matrix-creator-running-alexa-c-version-9b9d8d
- To ensure Alexa doesn't lock up when you are "headless", you need to modify the Alexa Sample App code. That tutorial is coming soon.
SSL Client Code:
The most lengthy of the web socket code is for client functionality for Secure Socket Layers. This is for HTTPS URLs. On the ESP8266 and Arduino, the code is pretty small as they use a "fingerprint" approach. However, the fingerprint can change from time-to-time causing you to update your code or firmware. Using the "openssl" library for C++ on the Raspberry Pi, we don't have to worry about that. To install the openssl library, do the following on the Raspberry Pi:
sudo apt-get install libssl-dev
Below is SSLClient code to perform a GET to a Nest Thermostat. This code can easily be modified to hit any SSL based API such as IFTTP webhooks. Just change the GET header data, address, and port passed in the GetHumidty() method.
//============================================================================ // Name : SSLClient.cpp // Compiling : //First install openssl: sudo apt-get install libssl-dev //g++ -c -o SSLClient.o humidity.cpp //g++ -o c SSLClient.o -lssl -lcrypto -L/usr/local/openssl/lib //============================================================================ #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <openssl/ssl.h> #include <openssl/err.h> #include <string.h> #include <string> #include <iostream> using namespace std; SSL *ssl; int sock; string RecvPacket() { int len=0; char buf[1000000]; len=SSL_read(ssl, buf, 1000); buf[len]=0; if (len < 0) { int err = SSL_get_error(ssl, len); if (err == SSL_ERROR_WANT_READ) return "0"; if (err == SSL_ERROR_WANT_WRITE) return "0"; if (err == SSL_ERROR_ZERO_RETURN || err == SSL_ERROR_SYSCALL || err == SSL_ERROR_SSL) return "-1"; } string output(buf); string answer=output.substr(output.find("content-length")+15); answer=answer.substr(answer.find("\n")+2); return answer; } int SendPacket(const char *buf) { int len = SSL_write(ssl, buf, strlen(buf)); if (len < 0) { int err = SSL_get_error(ssl, len); switch (err) { case SSL_ERROR_WANT_WRITE: return 0; case SSL_ERROR_WANT_READ: return 0; case SSL_ERROR_ZERO_RETURN: case SSL_ERROR_SYSCALL: case SSL_ERROR_SSL: default: return -1; } } } void log_ssl() { int err; while (err = ERR_get_error()) { char *str = ERR_error_string(err, 0); if (!str) return; printf(str); printf("\n"); fflush(stdout); } } string GetPacket(const char *the_request, const char *the_address, int the_port) { int s; s = socket(AF_INET, SOCK_STREAM, 0); if (!s) { printf("Error creating socket.\n"); return "-1"; } struct sockaddr_in sa; memset (&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_addr.s_addr = inet_addr(the_address); sa.sin_port = htons (the_port); socklen_t socklen = sizeof(sa); if (connect(s, (struct sockaddr *)&sa, socklen)) { printf("Error connecting to server.\n"); return "-1"; } SSL_library_init(); SSLeay_add_ssl_algorithms(); SSL_load_error_strings(); const SSL_METHOD *meth = TLSv1_2_client_method(); SSL_CTX *ctx = SSL_CTX_new (meth); ssl = SSL_new (ctx); if (!ssl) { printf("Error creating SSL.\n"); log_ssl(); return "-1"; } sock = SSL_get_fd(ssl); SSL_set_fd(ssl, s); int err = SSL_connect(ssl); if (err <= 0) { printf("Error creating SSL connection. err=%x\n", err); log_ssl(); fflush(stdout); return "-1"; } //printf ("SSL connection using %s\n", SSL_get_cipher (ssl)); SendPacket(the_request); return RecvPacket(); } string GetHumidity() { //To get your Nest Device ID and Bearer, go to https://codelabs.developers.google.com/codelabs/wwn-api-quickstart/#0 char request[]="GET /devices/thermostats/YOURDEVICE/humidity HTTP/1.1\nHost: firebase-apiserver17-tah01-iad01.dapi.production.nest.com\nConnection: close\nContent-Type: application/json\nAuthorization: Bearer YOURBEARER\n\n"; return GetPacket(&request[0],"52.4.203.41", 9553); } int main(int argc, char *argv[]) { std::cout<<GetHumidity(); printf("\n"); }
WEB SOCKET SERVER CODE
For awesome whole house device communication, the Raspberry Pi House Hub will also need to serve as a web socket server to receive commands from each device. Below is a standalone server that waits for commands on the port specified in the command line arguments:
/* A simple server in the internet domain using TCP The port number is passed as an argument This version runs forever, forking off a separate process for each connection */ //Compile with:g++ -oserver seanserver.cpp //usage server portnumber, ie server 5001 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <iostream> void dostuff(int); /* function prototype */ void error(const char *msg) { perror(msg); exit(1); } int main(int argc, char *argv[]) { int sockfd, newsockfd, portno, pid; socklen_t clilen; struct sockaddr_in serv_addr, cli_addr; if (argc < 2) { fprintf(stderr,"ERROR, no port provided\n"); exit(1); } sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) error("ERROR opening socket"); bzero((char *) &serv_addr, sizeof(serv_addr)); portno = atoi(argv[1]); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(portno); if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding"); listen(sockfd,5); clilen = sizeof(cli_addr); while (1) { newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen); if (newsockfd < 0) error("ERROR on accept"); pid = fork(); if (pid < 0) error("ERROR on fork"); if (pid == 0) { close(sockfd); dostuff(newsockfd); exit(0); } else close(newsockfd); } /* end of while */ close(sockfd); return 0; /* we never get here */ } /******** DOSTUFF() ********************* There is a separate instance of this function for each connection. It handles all communication once a connnection has been established. *****************************************/ void dostuff (int sock) { int n; char buffer[256]; bzero(buffer,256); n = read(sock,buffer,255); std::string str(buffer); std::string s=str.substr(0,str.find("\n")); if (n < 0) error("ERROR reading from socket"); printf("Here is the message: %s\n",buffer); if (s.compare("hello")==0) printf("\n\nhit\n\n"); n = write(sock,"I got your message",18); if (n < 0) error("ERROR writing to socket"); }
WEB SOCKET CLIENT CODE
The House Hub will not only need to listen to devices, but will also need to command some. For example, if the Nest Doorbell Camera sees motion, it will hit IFTTT, which will use a Webhook to hit our House Hub. The House Hub will then send a web socket command to the ESP8266 that controls our sprinkler system valve. In turn, we can chase off the deer eating our flower in the middle of the night by spraying them with water.
Below is standalone code that performs web socket client functionality.
//compile with: g++ -oclient seanclient.cpp //usage: client serverIP Port, ie client 192.168.1.60 5001 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> void error(const char *msg) { perror(msg); exit(0); } int main(int argc, char *argv[]) { int sockfd, portno, n; struct sockaddr_in serv_addr; struct hostent *server; char buffer[256]; if (argc < 3) { fprintf(stderr,"usage %s hostname port\n", argv[0]); exit(0); } portno = atoi(argv[2]); sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) error("ERROR opening socket"); server = gethostbyname(argv[1]); if (server == NULL) { fprintf(stderr,"ERROR, no such host\n"); exit(0); } bzero((char *) &serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length); serv_addr.sin_port = htons(portno); if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0) error("ERROR connecting"); printf("Please enter the message: "); bzero(buffer,256); fgets(buffer,255,stdin); n = write(sockfd,buffer,strlen(buffer)); if (n < 0) error("ERROR writing to socket"); bzero(buffer,256); n = read(sockfd,buffer,255); if (n < 0) error("ERROR reading from socket"); printf("%s\n",buffer); close(sockfd); return 0; }
ALEXA SKILL END POINT CODE
The following code is exceptional for handling Alexa Skills without using Amazon's Lamda service. Rather, it uses your Raspberry Pi as the end point. So, you have direct access to the Pi's hardware from Alexa without any middle-ware or companion scripts.
You will need additional JSON handling code from this repository: https://github.com/Tencent/rapidjson/tree/master/include/rapidjson, but you won't need to install any additional packages.
The function HandleAlexa is where you have the code check for your intent names and then execute accordingly.
//Compile with:g++ -oserver wally.cpp //usage server portnumber, ie server 5000 using namespace std; #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> //Obtain the followingrapidjson code from https://github.com/Tencent/rapidjson/tree/master/include/rapidjson #include "include/rapidjson/document.h" #include "include/rapidjson/error/en.h" #include <iostream> #include <sys/types.h> #include <sys/wait.h> using namespace rapidjson; //key definitions that must be changed for one's personal use of this code #define MONITORING_PORT "5000" #define ALEXA_SKILL_ID "amzn1.ask.skill.YOURSKILLID" //end of key definitions //Function Protocols string AlexaResponseJSON(string, string); void error(const char *); string GetHouseStatus(); string GetBackdoorStatus(); Document GetJSON(string); void HandleAlexa(string, int); void HandleCustom(string, int); bool LastByteReceived(string); void ListenForSocketConnection(); string ReadSocket(int); //End Function Protocols int main(int argc, char *argv[]) { ListenForSocketConnection(); return(0); //will never get here actually } void ListenForSocketConnection() { int sockfd, newsockfd, portno, pid; socklen_t clilen; struct sockaddr_in serv_addr, cli_addr; portno = atoi(MONITORING_PORT); sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) error("ERROR opening socket"); bzero((char *)&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(portno); if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding"); listen(sockfd, 5); clilen = sizeof(cli_addr); while (1) { newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen); if (newsockfd < 0) error("ERROR on accept"); pid = fork(); if (pid < 0) error("ERROR on fork"); if (pid == 0) { close(sockfd); string request = ReadSocket(newsockfd); //cout << request << endl; if (request.find(ALEXA_SKILL_ID) != string::npos) HandleAlexa(request, newsockfd); else HandleCustom(request, newsockfd); exit(0); } else { close(newsockfd); wait(&pid); } } /* end of while */ close(sockfd); } string GetHouseStatus() { return ("You still need to code the house status."); } string GetBackDoorStatus() { return ("You still need to code the backdoor status."); } void HandleAlexa(string str, int sock) { string request = str.substr(str.find("\r\n\r\n") + 4); //cout << endl<<endl<<"Entered HandleAlexa and about to GETJSON"<< endl; Document AlexaJSON = GetJSON(request); //cout << "Getting intent" << endl; string intent = AlexaJSON["request"]["intent"]["name"].GetString(); //cout << intent << endl; if (intent.compare("CheckStatus") == 0) { string str = AlexaResponseJSON("House Status", GetHouseStatus()); write(sock, str.c_str() , str.length()); } else if (intent.compare("CheckOnWally") == 0) { string str = AlexaResponseJSON("Wally Status", "Wally said he is doing great and thank you for asking!"); cout << endl << endl << str << endl; write(sock, str.c_str(), str.length()); } else if (intent.compare("DoorStatus") == 0) { string str = AlexaResponseJSON("Door Status", GetBackDoorStatus()); write(sock, str.c_str(), str.length()); } } Document GetJSON(string str) { Document d; //cout << endl<<endl<<str << endl; ParseResult result = d.Parse(str.c_str()); //cout << "parsed the result" << endl; if (!result) error("JSON parse error"); //cout << "Processed GetJSON" << endl; return(d); } void HandleCustom(string request, int sock) { string response = request.substr(request.find("\r\n\r\n") + 4); if (response.find("backdoor") != string::npos) { string str = "The backoor was opened."; //cout << str << endl; } } string AlexaResponseJSON(string title, string response) { string body="{\r\n" "\"version\": \"1.0\",\r\n" "\"response\" : {\r\n" "\"outputSpeech\": {\r\n" "\"type\": \"PlainText\",\r\n" "\"text\" : \"" + response + "\",\r\n" "\"playBehavior\" : \"REPLACE_ENQUEUED\"\r\n" "}, \r\n" "\"card\" : {\r\n" "\"type\": \"Standard\",\r\n" "\"title\" : \"" + title + "\",\r\n" "\"text\" : \"" + response + "\"\r\n" "}\r\n" "}, \r\n" "\"shouldEndSession\": true\r\n" "}\r\n" "}\r\n"; string header = "HTTP/1.1 200 OK\r\n" "Content-Type: application/json; charset = UTF-8\r\n" "Content-Length:" + to_string(body.length()) + "\r\n\r\n"; return (header + body); } bool LastByteReceived(string str) { //Check for the content-length in the header once it is received. Then, start counting bytes to see if we got them all. static int content_length = 0; //cout << "Entering LastByteREceived" << endl; if (content_length == 0) { //cout << "Checking for Content-length" << endl; if (str.find("Content-Length: ") != string::npos) { string temp = str.substr(str.find("Content-Length: ") + 16); if (temp.find("\r\n")) { //cout << "Setting Content-length" << endl; content_length = atoi((temp.substr(0, temp.find("\r\n")).c_str())); } else return(false); } else return(false); } if (content_length > 0) { if (str.find("\r\n\r\n") != string::npos) { string temp = str.substr(str.find("\r\n\r\n")); //cout << "Checking if content length has been reached" << endl; if (temp.length()-4 >= content_length) { //cout << "Content length reached" << endl; //imagine I need to clear this for future first //calls to LastByteReceived, but maybe not because //we exit in the main after handling the command. content_length = 0; return(true); } else return (false); } } else return (false); //cout << "Exiting LastByteREceived" << endl; } string ReadSocket(int sock) { int n; string all_bytes_received; char buffer[1000]; while (1) { bzero(buffer, 1000); n = read(sock, buffer, 999); if (n < 0) error("ERROR reading from socket"); cout << "reading in" << endl; std::string str(buffer); cout << "read buffer" << endl; all_bytes_received += str; cout << "added string" << endl; if (LastByteReceived(all_bytes_received)) break; } cout << "Got full buffer" << endl; return(all_bytes_received); //printf("Here is the message: %s\n", all_bytes_received); //n = write(sock, "I got your message", 18); } void error(const char *msg) { perror(msg); exit(0); }
PUTTING IT ALTOGETHER
Now that all fundamentals are understood, we can combine the web socket code, the Matrix Sensor Data code from Blog 3, and Matrix Alexa support to make an elegant voice commanded Smart Home Hub. Follow this post to see the final House Hub complete code once complete.