SinricPro Library
SinricPro.h
1 /*
2  * Copyright (c) 2019 Sinric. All rights reserved.
3  * Licensed under Creative Commons Attribution-Share Alike (CC BY-SA)
4  *
5  * This file is part of the Sinric Pro (https://github.com/sinricpro/)
6  */
7 
8 #ifndef _SINRIC_H_
9 #define _SINRIC_H_
10 
11 #include "SinricProInterface.h"
12 #include "SinricProDeviceInterface.h"
13 #include "SinricProWebsocket.h"
14 #include "SinricProUDP.h"
15 #include "SinricProSignature.h"
16 #include "SinricProMessageid.h"
17 #include "SinricProQueue.h"
18 
23 class SinricProClass : public SinricProInterface {
24  public:
25  void begin(String socketAuthToken, String signingKey, String serverURL = SINRICPRO_SERVER_URL);
26  template <typename DeviceType>
27  DeviceType& add(const char* deviceId, unsigned long eventWaitTime = 1000);
28 
29  void add(SinricProDeviceInterface& newDevice);
30  void add(SinricProDeviceInterface* newDevice);
31  void handle();
32  void stop();
33  bool isConnected();
34 
42  typedef std::function<void(void)> ConnectedCallbackHandler;
50  typedef std::function<void(void)> DisconnectedCallbackHandler;
53 
54  void restoreDeviceStates(bool flag);
55 
56  DynamicJsonDocument prepareResponse(JsonDocument& requestMessage);
57  DynamicJsonDocument prepareEvent(const char* deviceId, const char* action, const char* cause) override;
58  void sendMessage(JsonDocument& jsonMessage) override;
59 
60  struct proxy {
61  proxy(SinricProClass* ptr, String deviceId) : ptr(ptr), deviceId(deviceId) {}
62  SinricProClass* ptr;
63  String deviceId;
64  template <typename DeviceType>
65  operator DeviceType&() { return as<DeviceType>(); }
66  template <typename DeviceType>
67  DeviceType& as() { return ptr->getDeviceInstance<DeviceType>(deviceId); }
68  };
85  proxy operator[](const String deviceId) { return proxy(this, deviceId); }
86 
87  // setResponseMessage is is just a workaround until verison 3.x.x will be released
88  void setResponseMessage(String &&message) { responseMessageStr = message; }
89 
95  unsigned long getTimestamp() override { return baseTimestamp + (millis()/1000); }
96  private:
97  void handleReceiveQueue();
98  void handleSendQueue();
99 
100  void handleRequest(DynamicJsonDocument& requestMessage, interface_t Interface);
101  void handleResponse(DynamicJsonDocument& responseMessage);
102 
103  DynamicJsonDocument prepareRequest(const char* deviceId, const char* action);
104 
105  void connect();
106  void disconnect();
107  void reconnect();
108 
109  void onConnect() { DEBUG_SINRIC("[SinricPro]: Connected to \"%s\"!]\r\n", serverURL.c_str()); }
110  void onDisconnect() { DEBUG_SINRIC("[SinricPro]: Disconnect\r\n"); }
111 
112  bool verifyDeviceId(const char* id);
113  bool verifyAppKey(const char* key);
114  bool verifyAppSecret(const char* secret);
115  void extractTimestamp(JsonDocument &message);
116 
117  SinricProDeviceInterface* getDevice(String deviceId);
118 
119  template <typename DeviceType>
120  DeviceType& getDeviceInstance(String deviceId);
121 
122  std::vector<SinricProDeviceInterface*> devices;
123  String socketAuthToken;
124  String signingKey;
125  String serverURL;
126 
127  websocketListener _websocketListener;
128  udpListener _udpListener;
129  SinricProQueue_t receiveQueue;
130  SinricProQueue_t sendQueue;
131 
132  unsigned long baseTimestamp = 0;
133 
134  bool _begin = false;
135  String responseMessageStr = "";
136 };
137 
138 SinricProDeviceInterface* SinricProClass::getDevice(String deviceId) {
139  for (auto& device : devices) {
140  if (deviceId == String(device->getDeviceId())) return device;
141  }
142  return nullptr;
143 }
144 
145 template <typename DeviceType>
146 DeviceType& SinricProClass::getDeviceInstance(String deviceId) {
147  DeviceType* tmp_device = (DeviceType*) getDevice(deviceId);
148  if (tmp_device) return *tmp_device;
149 
150  DEBUG_SINRIC("[SinricPro]: Device \"%s\" does not exist. Creating new device\r\n", deviceId.c_str());
151  DeviceType& tmp_deviceInstance = add<DeviceType>(deviceId.c_str());
152 
153  if (isConnected()) {
154  DEBUG_SINRIC("[SinricPro]: Reconnecting to server.\r\n");
155  reconnect();
156  }
157 
158  return tmp_deviceInstance;
159 }
160 
177 void SinricProClass::begin(String socketAuthToken, String signingKey, String serverURL) {
178  bool success = true;
179  if (!verifyAppKey(socketAuthToken.c_str())) {
180  DEBUG_SINRIC("[SinricPro:begin()]: App-Key \"%s\" is invalid!! Please check your app-key!! SinricPro will not work!\r\n", socketAuthToken.c_str());
181  success = false;
182  }
183  if (!verifyAppSecret(signingKey.c_str())) {
184  DEBUG_SINRIC("[SinricPro:begin()]: App-Secret \"%s\" is invalid!! Please check your app-secret!! SinricPro will not work!\r\n", signingKey.c_str());
185  success = false;
186  return;
187  }
188 
189  if(!success) {
190  _begin = false;
191  return;
192  }
193 
194  this->socketAuthToken = socketAuthToken;
195  this->signingKey = signingKey;
196  this->serverURL = serverURL;
197  _begin = true;
198  _udpListener.begin(&receiveQueue);
199 }
200 
201 template <typename DeviceType>
202 DeviceType& SinricProClass::add(const char* deviceId, unsigned long eventWaitTime) {
203  DeviceType* newDevice = new DeviceType(deviceId, eventWaitTime);
204  if (verifyDeviceId(deviceId)){
205  DEBUG_SINRIC("[SinricPro:add()]: Adding device with id \"%s\".\r\n", deviceId);
206  newDevice->begin(this);
207  if (verifyAppKey(socketAuthToken.c_str()) && verifyAppSecret(signingKey.c_str())) _begin = true;
208  } else {
209  DEBUG_SINRIC("[SinricPro:add()]: DeviceId \"%s\" is invalid!! Device will be ignored and will NOT WORK!\r\n", deviceId);
210  }
211  devices.push_back(newDevice);
212  return *newDevice;
213 }
214 
215 __attribute__ ((deprecated("Please use DeviceType& myDevice = SinricPro.add<DeviceType>(DeviceId);")))
216 void SinricProClass::add(SinricProDeviceInterface* newDevice) {
217  if (!verifyDeviceId(newDevice->getDeviceId())) return;
218  newDevice->begin(this);
219  devices.push_back(newDevice);
220 }
221 
222 __attribute__ ((deprecated("Please use DeviceType& myDevice = SinricPro.add<DeviceType>(DeviceId);")))
223 void SinricProClass::add(SinricProDeviceInterface& newDevice) {
224  if (!verifyDeviceId(newDevice.getDeviceId())) return;
225  newDevice.begin(this);
226  devices.push_back(&newDevice);
227 }
228 
245  static bool begin_error = false;
246  if (!_begin) {
247  if (!begin_error) { // print this only once!
248  DEBUG_SINRIC("[SinricPro:handle()]: ERROR! SinricPro.begin() failed or was not called prior to event handler\r\n");
249  DEBUG_SINRIC("[SinricPro:handle()]: -Reasons include an invalid app-key, invalid app-secret or no valid deviceIds)\r\n");
250  DEBUG_SINRIC("[SinricPro:handle()]: -SinricPro is disabled! Check earlier log messages for details.\r\n");
251  begin_error = true;
252  }
253  return;
254  }
255 
256 
257  if (!isConnected()) connect();
258  _websocketListener.handle();
259  _udpListener.handle();
260 
261  handleReceiveQueue();
262  handleSendQueue();
263 }
264 
265 DynamicJsonDocument SinricProClass::prepareRequest(const char* deviceId, const char* action) {
266  DynamicJsonDocument requestMessage(1024);
267  JsonObject header = requestMessage.createNestedObject("header");
268  header["payloadVersion"] = 2;
269  header["signatureVersion"] = 1;
270 
271  JsonObject payload = requestMessage.createNestedObject("payload");
272  payload["action"] = action;
273  payload["createdAt"] = 0;
274  payload["deviceId"] = deviceId;
275  payload["replyToken"] = MessageID().getID();
276  payload["type"] = "request";
277  payload.createNestedObject("value");
278  return requestMessage;
279 }
280 
281 void SinricProClass::handleResponse(DynamicJsonDocument& responseMessage) {
282  DEBUG_SINRIC("[SinricPro.handleResponse()]:\r\n");
283 
284  #ifndef NODEBUG_SINRIC
285  serializeJsonPretty(responseMessage, DEBUG_ESP_PORT);
286  Serial.println();
287  #endif
288 }
289 
290 void SinricProClass::handleRequest(DynamicJsonDocument& requestMessage, interface_t Interface) {
291  DEBUG_SINRIC("[SinricPro.handleRequest()]: handling request\r\n");
292  #ifndef NODEBUG_SINRIC
293  serializeJsonPretty(requestMessage, DEBUG_ESP_PORT);
294  #endif
295 
296  DynamicJsonDocument responseMessage = prepareResponse(requestMessage);
297 
298  // handle devices
299  bool success = false;
300  const char* deviceId = requestMessage["payload"]["deviceId"];
301  const char* action = requestMessage["payload"]["action"];
302  JsonObject request_value = requestMessage["payload"]["value"];
303  JsonObject response_value = responseMessage["payload"]["value"];
304 
305  for (auto& device : devices) {
306  if (strcmp(deviceId, device->getDeviceId()) == 0 && success == false) {
307  success = device->handleRequest(deviceId, action, request_value, response_value);
308  responseMessage["payload"]["success"] = success;
309  if (!success) {
310  if (responseMessageStr.length() > 0){
311  responseMessage["payload"]["message"] = responseMessageStr;
312  responseMessageStr = "";
313  } else {
314  responseMessage["payload"]["message"] = "Device returned an error while processing the request!";
315  }
316  }
317  }
318  }
319 
320  String responseString;
321  serializeJson(responseMessage, responseString);
322  sendQueue.push(new SinricProMessage(Interface, responseString.c_str()));
323 }
324 
325 void SinricProClass::handleReceiveQueue() {
326  if (receiveQueue.count() == 0) return;
327 
328  DEBUG_SINRIC("[SinricPro.handleReceiveQueue()]: %i message(s) in receiveQueue\r\n", receiveQueue.count());
329  while (receiveQueue.count() > 0) {
330  SinricProMessage* rawMessage = receiveQueue.pop();
331  DynamicJsonDocument jsonMessage(1024);
332  deserializeJson(jsonMessage, rawMessage->getMessage());
333 
334  bool sigMatch = false;
335 
336  if (strncmp(rawMessage->getMessage(), "{\"timestamp\":", 13) == 0 && strlen(rawMessage->getMessage()) <= 26) {
337  sigMatch=true; // timestamp message has no signature...ignore sigMatch for this!
338  } else {
339  sigMatch = verifyMessage(signingKey, jsonMessage);
340  }
341 
342  String messageType = jsonMessage["payload"]["type"];
343 
344  if (sigMatch) { // signature is valid process message
345  DEBUG_SINRIC("[SinricPro.handleReceiveQueue()]: Signature is valid. Processing message...\r\n");
346  extractTimestamp(jsonMessage);
347  if (messageType == "response") handleResponse(jsonMessage);
348  if (messageType == "request") handleRequest(jsonMessage, rawMessage->getInterface());
349  } else {
350  DEBUG_SINRIC("[SinricPro.handleReceiveQueue()]: Signature is invalid! Sending messsage to [dev/null] ;)\r\n");
351  }
352  delete rawMessage;
353  }
354 }
355 
356 void SinricProClass::handleSendQueue() {
357  if (!isConnected()) return;
358  if (!baseTimestamp) return;
359  while (sendQueue.count() > 0) {
360  DEBUG_SINRIC("[SinricPro:handleSendQueue()]: %i message(s) in sendQueue\r\n", sendQueue.count());
361  DEBUG_SINRIC("[SinricPro:handleSendQueue()]: Sending message...\r\n");
362 
363  SinricProMessage* rawMessage = sendQueue.pop();
364 
365  DynamicJsonDocument jsonMessage(1024);
366  deserializeJson(jsonMessage, rawMessage->getMessage());
367  jsonMessage["payload"]["createdAt"] = getTimestamp();
368  signMessage(signingKey, jsonMessage);
369 
370  String messageStr;
371 
372  serializeJson(jsonMessage, messageStr);
373  #ifndef NODEBUG_SINRIC
374  serializeJsonPretty(jsonMessage, DEBUG_ESP_PORT);
375  Serial.println();
376  #endif
377 
378  switch (rawMessage->getInterface()) {
379  case IF_WEBSOCKET: DEBUG_SINRIC("[SinricPro:handleSendQueue]: Sending to websocket\r\n"); _websocketListener.sendMessage(messageStr); break;
380  case IF_UDP: DEBUG_SINRIC("[SinricPro:handleSendQueue]: Sending to UDP\r\n");_udpListener.sendMessage(messageStr); break;
381  default: break;
382  }
383  delete rawMessage;
384  DEBUG_SINRIC("[SinricPro:handleSendQueue()]: message sent.\r\n");
385  }
386 }
387 
388 void SinricProClass::connect() {
389  String deviceList;
390  int i = 0;
391  for (auto& device : devices) {
392  const char* deviceId = device->getDeviceId();
393  if (verifyDeviceId(deviceId)) {
394  if (i>0) deviceList += ';';
395  deviceList += String(deviceId);
396  i++;
397  }
398  }
399  if (i==0) { // no device have been added! -> do not connect!
400  _begin = false;
401  DEBUG_SINRIC("[SinricPro]: ERROR! No valid devices available. Please add a valid device first!\r\n");
402  return;
403  }
404 
405  _websocketListener.begin(serverURL, socketAuthToken, deviceList, &receiveQueue);
406 }
407 
408 
409 void SinricProClass::stop() {
410  DEBUG_SINRIC("[SinricPro:stop()\r\n");
411  _websocketListener.stop();
412 }
413 
414 bool SinricProClass::isConnected() {
415  return _websocketListener.isConnected();
416 };
417 
428  _websocketListener.onConnected(cb);
429 }
430 
441  _websocketListener.onDisconnected(cb);
442 }
443 
444 
445 void SinricProClass::reconnect() {
446  DEBUG_SINRIC("SinricPro:reconnect(): disconnecting\r\n");
447  stop();
448  DEBUG_SINRIC("SinricPro:reconnect(): connecting\r\n");
449  connect();
450 }
451 
452 bool SinricProClass::verifyDeviceId(const char* id) {
453  if (strlen(id) != 24) return false;
454  int tmp; char tmp_c;
455  return sscanf(id, "%4x%4x%4x%4x%4x%4x%c",
456  &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp_c) == 6;
457 }
458 
459 bool SinricProClass::verifyAppKey(const char* key) {
460  if (strlen(key) != 36) return false;
461  int tmp; char tmp_c;
462  return sscanf(key, "%4x%4x-%4x-%4x-%4x-%4x%4x%4x%c",
463  &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp_c) == 8;
464 }
465 
466 bool SinricProClass::verifyAppSecret(const char* secret) {
467  if (strlen(secret) != 73) return false;
468  int tmp; char tmp_c;
469  return sscanf(secret, "%4x%4x-%4x-%4x-%4x-%4x%4x%4x-%4x%4x-%4x-%4x-%4x-%4x%4x%4x%c",
470  &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp_c) == 16;
471 }
472 
473 
474 void SinricProClass::extractTimestamp(JsonDocument &message) {
475  unsigned long tempTimestamp = 0;
476  // extract timestamp from timestamp message right after websocket connection is established
477  tempTimestamp = message["timestamp"] | 0;
478  if (tempTimestamp) {
479  baseTimestamp = tempTimestamp - (millis() / 1000);
480  DEBUG_SINRIC("[SinricPro:extractTimestamp(): Got Timestamp %lu\r\n", tempTimestamp);
481  return;
482  }
483 
484  // extract timestamp from request message
485  tempTimestamp = message["payload"]["createdAt"] | 0;
486  if (tempTimestamp) {
487  DEBUG_SINRIC("[SinricPro:extractTimestamp(): Got Timestamp %lu\r\n", tempTimestamp);
488  baseTimestamp = tempTimestamp - (millis() / 1000);
489  return;
490  }
491 }
492 
493 
494 void SinricProClass::sendMessage(JsonDocument& jsonMessage) {
495  if (!isConnected()) {
496  DEBUG_SINRIC("[SinricPro:sendMessage()]: device is offline, message has been dropped\r\n");
497  return;
498  }
499  DEBUG_SINRIC("[SinricPro:sendMessage()]: pushing message into sendQueue\r\n");
500  String messageString;
501  serializeJson(jsonMessage, messageString);
502  sendQueue.push(new SinricProMessage(IF_WEBSOCKET, messageString.c_str()));
503 }
504 
515  _websocketListener.setRestoreDeviceStates(flag);
516 }
517 
518 DynamicJsonDocument SinricProClass::prepareResponse(JsonDocument& requestMessage) {
519  DynamicJsonDocument responseMessage(1024);
520  JsonObject header = responseMessage.createNestedObject("header");
521  header["payloadVersion"] = 2;
522  header["signatureVersion"] = 1;
523 
524  JsonObject payload = responseMessage.createNestedObject("payload");
525  payload["action"] = requestMessage["payload"]["action"];
526  payload["clientId"] = requestMessage["payload"]["clientId"];
527  payload["createdAt"] = 0;
528  payload["deviceId"] = requestMessage["payload"]["deviceId"];
529  payload["message"] = "OK";
530  payload["replyToken"] = requestMessage["payload"]["replyToken"];
531  payload["success"] = false;
532  payload["type"] = "response";
533  payload.createNestedObject("value");
534  return responseMessage;
535 }
536 
537 
538 DynamicJsonDocument SinricProClass::prepareEvent(const char* deviceId, const char* action, const char* cause) {
539  DynamicJsonDocument eventMessage(1024);
540  JsonObject header = eventMessage.createNestedObject("header");
541  header["payloadVersion"] = 2;
542  header["signatureVersion"] = 1;
543 
544  JsonObject payload = eventMessage.createNestedObject("payload");
545  payload["action"] = action;
546  payload["cause"].createNestedObject("type");
547  payload["cause"]["type"] = cause;
548  payload["createdAt"] = 0;
549  payload["deviceId"] = deviceId;
550  payload["replyToken"] = MessageID().getID();
551  payload["type"] = "event";
552  payload.createNestedObject("value");
553  return eventMessage;
554 }
555 
556 #ifndef NOSINRIC_INSTANCE
557 
565 #endif
566 
567 #endif
SinricProClass::onConnected
void onConnected(ConnectedCallbackHandler cb)
Set callback function for websocket connected event.
Definition: SinricPro.h:427
SinricProClass::handle
void handle()
Handles communication between device and SinricPro Server.
Definition: SinricPro.h:244
SinricProClass::getTimestamp
unsigned long getTimestamp() override
Get the current timestamp.
Definition: SinricPro.h:95
SinricProClass::onDisconnected
void onDisconnected(DisconnectedCallbackHandler cb)
Set callback function for websocket disconnected event.
Definition: SinricPro.h:440
SinricProClass::restoreDeviceStates
void restoreDeviceStates(bool flag)
Enable / disable restore device states function.
Definition: SinricPro.h:514
SinricProClass::DisconnectedCallbackHandler
std::function< void(void)> DisconnectedCallbackHandler
Callback definition for onDisconnected function.
Definition: SinricPro.h:50
SinricProClass::operator[]
proxy operator[](const String deviceId)
operator[] is used tor create a new device instance or get an existing device instance
Definition: SinricPro.h:85
SinricProClass::begin
void begin(String socketAuthToken, String signingKey, String serverURL=SINRICPRO_SERVER_URL)
Initializing SinricProClass to be able to connect to SinricPro Server.
Definition: SinricPro.h:177
SinricProClass::ConnectedCallbackHandler
std::function< void(void)> ConnectedCallbackHandler
Callback definition for onConnected function.
Definition: SinricPro.h:42
SinricProClass
The main class of this library, handling communication between SinricPro Server and your devices.
Definition: SinricPro.h:23
SinricPro
The main instance of SinricProClass.