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 
35  typedef std::function<void(void)> ConnectCallbackHandler;
36  void onConnected(ConnectCallbackHandler cb);
37  void onDisconnected(ConnectCallbackHandler cb);
38 
39  void restoreDeviceStates(bool flag);
40 
41  DynamicJsonDocument prepareResponse(JsonDocument& requestMessage);
42  DynamicJsonDocument prepareEvent(const char* deviceId, const char* action, const char* cause) override;
43  void sendMessage(JsonDocument& jsonMessage) override;
44 
45  struct proxy {
46  proxy(SinricProClass* ptr, String deviceId) : ptr(ptr), deviceId(deviceId) {}
47  SinricProClass* ptr;
48  String deviceId;
49  template <typename DeviceType>
50  operator DeviceType&() { return as<DeviceType>(); }
51  template <typename DeviceType>
52  DeviceType& as() { return ptr->getDeviceInstance<DeviceType>(deviceId); }
53  };
70  proxy operator[](const String deviceId) { return proxy(this, deviceId); }
71 
72  // setResponseMessage is is just a workaround until verison 3.x.x will be released
73  void setResponseMessage(String &&message) { responseMessageStr = message; }
74 
75  private:
76  void handleReceiveQueue();
77  void handleSendQueue();
78 
79  void handleRequest(DynamicJsonDocument& requestMessage, interface_t Interface);
80  void handleResponse(DynamicJsonDocument& responseMessage);
81 
82  DynamicJsonDocument prepareRequest(const char* deviceId, const char* action);
83 
84  void connect();
85  void disconnect();
86  void reconnect();
87 
88  void onConnect() { DEBUG_SINRIC("[SinricPro]: Connected to \"%s\"!]\r\n", serverURL.c_str()); }
89  void onDisconnect() { DEBUG_SINRIC("[SinricPro]: Disconnect\r\n"); }
90 
91  bool verifyDeviceId(const char* id);
92  bool verifyAppKey(const char* key);
93  bool verifyAppSecret(const char* secret);
94  void extractTimestamp(JsonDocument &message);
95 
96  SinricProDeviceInterface* getDevice(String deviceId);
97 
98  template <typename DeviceType>
99  DeviceType& getDeviceInstance(String deviceId);
100 
101  std::vector<SinricProDeviceInterface*> devices;
102  String socketAuthToken;
103  String signingKey;
104  String serverURL;
105 
106  websocketListener _websocketListener;
107  udpListener _udpListener;
108  SinricProQueue_t receiveQueue;
109  SinricProQueue_t sendQueue;
110 
111  unsigned long getTimestamp() { return baseTimestamp + (millis()/1000); }
112  unsigned long baseTimestamp = 0;
113 
114  bool _begin = false;
115  String responseMessageStr = "";
116 };
117 
118 SinricProDeviceInterface* SinricProClass::getDevice(String deviceId) {
119  for (auto& device : devices) {
120  if (deviceId == String(device->getDeviceId())) return device;
121  }
122  return nullptr;
123 }
124 
125 template <typename DeviceType>
126 DeviceType& SinricProClass::getDeviceInstance(String deviceId) {
127  DeviceType* tmp_device = (DeviceType*) getDevice(deviceId);
128  if (tmp_device) return *tmp_device;
129 
130  DEBUG_SINRIC("[SinricPro]: Device \"%s\" does not exist. Creating new device\r\n", deviceId.c_str());
131  DeviceType& tmp_deviceInstance = add<DeviceType>(deviceId.c_str());
132 
133  if (isConnected()) {
134  DEBUG_SINRIC("[SinricPro]: Reconnecting to server.\r\n");
135  reconnect();
136  }
137 
138  return tmp_deviceInstance;
139 }
140 
157 void SinricProClass::begin(String socketAuthToken, String signingKey, String serverURL) {
158  bool success = true;
159  if (!verifyAppKey(socketAuthToken.c_str())) {
160  DEBUG_SINRIC("[SinricPro:begin()]: App-Key \"%s\" is invalid!! Please check your app-key!! SinricPro will not work!\r\n", socketAuthToken.c_str());
161  success = false;
162  }
163  if (!verifyAppSecret(signingKey.c_str())) {
164  DEBUG_SINRIC("[SinricPro:begin()]: App-Secret \"%s\" is invalid!! Please check your app-secret!! SinricPro will not work!\r\n", signingKey.c_str());
165  success = false;
166  return;
167  }
168 
169  if(!success) {
170  _begin = false;
171  return;
172  }
173 
174  this->socketAuthToken = socketAuthToken;
175  this->signingKey = signingKey;
176  this->serverURL = serverURL;
177  _begin = true;
178  _udpListener.begin(&receiveQueue);
179 }
180 
181 template <typename DeviceType>
182 DeviceType& SinricProClass::add(const char* deviceId, unsigned long eventWaitTime) {
183  DeviceType* newDevice = new DeviceType(deviceId, eventWaitTime);
184  if (verifyDeviceId(deviceId)){
185  DEBUG_SINRIC("[SinricPro:add()]: Adding device with id \"%s\".\r\n", deviceId);
186  newDevice->begin(this);
187  if (verifyAppKey(socketAuthToken.c_str()) && verifyAppSecret(signingKey.c_str())) _begin = true;
188  } else {
189  DEBUG_SINRIC("[SinricPro:add()]: DeviceId \"%s\" is invalid!! Device will be ignored and will NOT WORK!\r\n", deviceId);
190  }
191  devices.push_back(newDevice);
192  return *newDevice;
193 }
194 
195 __attribute__ ((deprecated("Please use DeviceType& myDevice = SinricPro.add<DeviceType>(DeviceId);")))
196 void SinricProClass::add(SinricProDeviceInterface* newDevice) {
197  if (!verifyDeviceId(newDevice->getDeviceId())) return;
198  newDevice->begin(this);
199  devices.push_back(newDevice);
200 }
201 
202 __attribute__ ((deprecated("Please use DeviceType& myDevice = SinricPro.add<DeviceType>(DeviceId);")))
203 void SinricProClass::add(SinricProDeviceInterface& newDevice) {
204  if (!verifyDeviceId(newDevice.getDeviceId())) return;
205  newDevice.begin(this);
206  devices.push_back(&newDevice);
207 }
208 
225  static bool begin_error = false;
226  if (!_begin) {
227  if (!begin_error) { // print this only once!
228  DEBUG_SINRIC("[SinricPro:handle()]: ERROR! SinricPro.begin() failed or was not called prior to event handler\r\n");
229  DEBUG_SINRIC("[SinricPro:handle()]: -Reasons include an invalid app-key, invalid app-secret or no valid deviceIds)\r\n");
230  DEBUG_SINRIC("[SinricPro:handle()]: -SinricPro is disabled! Check earlier log messages for details.\r\n");
231  begin_error = true;
232  }
233  return;
234  }
235 
236 
237  if (!isConnected()) connect();
238  _websocketListener.handle();
239  _udpListener.handle();
240 
241  handleReceiveQueue();
242  handleSendQueue();
243 }
244 
245 DynamicJsonDocument SinricProClass::prepareRequest(const char* deviceId, const char* action) {
246  DynamicJsonDocument requestMessage(1024);
247  JsonObject header = requestMessage.createNestedObject("header");
248  header["payloadVersion"] = 2;
249  header["signatureVersion"] = 1;
250 
251  JsonObject payload = requestMessage.createNestedObject("payload");
252  payload["action"] = action;
253  payload["createdAt"] = 0;
254  payload["deviceId"] = deviceId;
255  payload["replyToken"] = MessageID().getID();
256  payload["type"] = "request";
257  payload.createNestedObject("value");
258  return requestMessage;
259 }
260 
261 void SinricProClass::handleResponse(DynamicJsonDocument& responseMessage) {
262  DEBUG_SINRIC("[SinricPro.handleResponse()]:\r\n");
263 
264  #ifndef NODEBUG_SINRIC
265  serializeJsonPretty(responseMessage, DEBUG_ESP_PORT);
266  Serial.println();
267  #endif
268 }
269 
270 void SinricProClass::handleRequest(DynamicJsonDocument& requestMessage, interface_t Interface) {
271  DEBUG_SINRIC("[SinricPro.handleRequest()]: handling request\r\n");
272  #ifndef NODEBUG_SINRIC
273  serializeJsonPretty(requestMessage, DEBUG_ESP_PORT);
274  #endif
275 
276  DynamicJsonDocument responseMessage = prepareResponse(requestMessage);
277 
278  // handle devices
279  bool success = false;
280  const char* deviceId = requestMessage["payload"]["deviceId"];
281  const char* action = requestMessage["payload"]["action"];
282  JsonObject request_value = requestMessage["payload"]["value"];
283  JsonObject response_value = responseMessage["payload"]["value"];
284 
285  for (auto& device : devices) {
286  if (strcmp(deviceId, device->getDeviceId()) == 0 && success == false) {
287  success = device->handleRequest(deviceId, action, request_value, response_value);
288  responseMessage["payload"]["success"] = success;
289  if (!success) {
290  if (responseMessageStr.length() > 0){
291  responseMessage["payload"]["message"] = responseMessageStr;
292  responseMessageStr = "";
293  } else {
294  responseMessage["payload"]["message"] = "Device returned an error while processing the request!";
295  }
296  }
297  }
298  }
299 
300  String responseString;
301  serializeJson(responseMessage, responseString);
302  sendQueue.push(new SinricProMessage(Interface, responseString.c_str()));
303 }
304 
305 void SinricProClass::handleReceiveQueue() {
306  if (receiveQueue.count() == 0) return;
307 
308  DEBUG_SINRIC("[SinricPro.handleReceiveQueue()]: %i message(s) in receiveQueue\r\n", receiveQueue.count());
309  while (receiveQueue.count() > 0) {
310  SinricProMessage* rawMessage = receiveQueue.pop();
311  DynamicJsonDocument jsonMessage(1024);
312  deserializeJson(jsonMessage, rawMessage->getMessage());
313 
314  bool sigMatch = false;
315 
316  if (strncmp(rawMessage->getMessage(), "{\"timestamp\":", 13) == 0 && strlen(rawMessage->getMessage()) <= 26) {
317  sigMatch=true; // timestamp message has no signature...ignore sigMatch for this!
318  } else {
319  sigMatch = verifyMessage(signingKey, jsonMessage);
320  }
321 
322  String messageType = jsonMessage["payload"]["type"];
323 
324  if (sigMatch) { // signature is valid process message
325  DEBUG_SINRIC("[SinricPro.handleReceiveQueue()]: Signature is valid. Processing message...\r\n");
326  extractTimestamp(jsonMessage);
327  if (messageType == "response") handleResponse(jsonMessage);
328  if (messageType == "request") handleRequest(jsonMessage, rawMessage->getInterface());
329  } else {
330  DEBUG_SINRIC("[SinricPro.handleReceiveQueue()]: Signature is invalid! Sending messsage to [dev/null] ;)\r\n");
331  }
332  delete rawMessage;
333  }
334 }
335 
336 void SinricProClass::handleSendQueue() {
337  if (!isConnected()) return;
338  if (!baseTimestamp) return;
339  while (sendQueue.count() > 0) {
340  DEBUG_SINRIC("[SinricPro:handleSendQueue()]: %i message(s) in sendQueue\r\n", sendQueue.count());
341  DEBUG_SINRIC("[SinricPro:handleSendQueue()]: Sending message...\r\n");
342 
343  SinricProMessage* rawMessage = sendQueue.pop();
344 
345  DynamicJsonDocument jsonMessage(1024);
346  deserializeJson(jsonMessage, rawMessage->getMessage());
347  jsonMessage["payload"]["createdAt"] = getTimestamp();
348  signMessage(signingKey, jsonMessage);
349 
350  String messageStr;
351 
352  serializeJson(jsonMessage, messageStr);
353  #ifndef NODEBUG_SINRIC
354  serializeJsonPretty(jsonMessage, DEBUG_ESP_PORT);
355  Serial.println();
356  #endif
357 
358  switch (rawMessage->getInterface()) {
359  case IF_WEBSOCKET: DEBUG_SINRIC("[SinricPro:handleSendQueue]: Sending to websocket\r\n"); _websocketListener.sendMessage(messageStr); break;
360  case IF_UDP: DEBUG_SINRIC("[SinricPro:handleSendQueue]: Sending to UDP\r\n");_udpListener.sendMessage(messageStr); break;
361  default: break;
362  }
363  delete rawMessage;
364  DEBUG_SINRIC("[SinricPro:handleSendQueue()]: message sent.\r\n");
365  }
366 }
367 
368 void SinricProClass::connect() {
369  String deviceList;
370  int i = 0;
371  for (auto& device : devices) {
372  const char* deviceId = device->getDeviceId();
373  if (verifyDeviceId(deviceId)) {
374  if (i>0) deviceList += ';';
375  deviceList += String(deviceId);
376  i++;
377  }
378  }
379  if (i==0) { // no device have been added! -> do not connect!
380  _begin = false;
381  DEBUG_SINRIC("[SinricPro]: ERROR! No valid devices available. Please add a valid device first!\r\n");
382  return;
383  }
384 
385  _websocketListener.begin(serverURL, socketAuthToken, deviceList, &receiveQueue);
386 }
387 
388 
389 void SinricProClass::stop() {
390  DEBUG_SINRIC("[SinricPro:stop()\r\n");
391  _websocketListener.stop();
392 }
393 
394 bool SinricProClass::isConnected() {
395  return _websocketListener.isConnected();
396 };
397 
398 
399 void SinricProClass::onConnected(ConnectCallbackHandler cb) {
400  _websocketListener.onConnected(cb);
401 }
402 
403 void SinricProClass::onDisconnected(ConnectCallbackHandler cb) {
404  _websocketListener.onDisconnected(cb);
405 }
406 
407 
408 void SinricProClass::reconnect() {
409  DEBUG_SINRIC("SinricPro:reconnect(): disconnecting\r\n");
410  stop();
411  DEBUG_SINRIC("SinricPro:reconnect(): connecting\r\n");
412  connect();
413 }
414 
415 bool SinricProClass::verifyDeviceId(const char* id) {
416  if (strlen(id) != 24) return false;
417  int tmp; char tmp_c;
418  return sscanf(id, "%4x%4x%4x%4x%4x%4x%c",
419  &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp_c) == 6;
420 }
421 
422 bool SinricProClass::verifyAppKey(const char* key) {
423  if (strlen(key) != 36) return false;
424  int tmp; char tmp_c;
425  return sscanf(key, "%4x%4x-%4x-%4x-%4x-%4x%4x%4x%c",
426  &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp_c) == 8;
427 }
428 
429 bool SinricProClass::verifyAppSecret(const char* secret) {
430  if (strlen(secret) != 73) return false;
431  int tmp; char tmp_c;
432  return sscanf(secret, "%4x%4x-%4x-%4x-%4x-%4x%4x%4x-%4x%4x-%4x-%4x-%4x-%4x%4x%4x%c",
433  &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp_c) == 16;
434 }
435 
436 
437 void SinricProClass::extractTimestamp(JsonDocument &message) {
438  unsigned long tempTimestamp = 0;
439  // extract timestamp from timestamp message right after websocket connection is established
440  tempTimestamp = message["timestamp"] | 0;
441  if (tempTimestamp) {
442  baseTimestamp = tempTimestamp - (millis() / 1000);
443  DEBUG_SINRIC("[SinricPro:extractTimestamp(): Got Timestamp %lu\r\n", tempTimestamp);
444  return;
445  }
446 
447  // extract timestamp from request message
448  tempTimestamp = message["payload"]["createdAt"] | 0;
449  if (tempTimestamp) {
450  DEBUG_SINRIC("[SinricPro:extractTimestamp(): Got Timestamp %lu\r\n", tempTimestamp);
451  baseTimestamp = tempTimestamp - (millis() / 1000);
452  return;
453  }
454 }
455 
456 
457 void SinricProClass::sendMessage(JsonDocument& jsonMessage) {
458  DEBUG_SINRIC("[SinricPro:sendMessage()]: pushing message into sendQueue\r\n");
459  String messageString;
460  serializeJson(jsonMessage, messageString);
461  sendQueue.push(new SinricProMessage(IF_WEBSOCKET, messageString.c_str()));
462 }
463 
474  _websocketListener.setRestoreDeviceStates(flag);
475 }
476 
477 DynamicJsonDocument SinricProClass::prepareResponse(JsonDocument& requestMessage) {
478  DynamicJsonDocument responseMessage(1024);
479  JsonObject header = responseMessage.createNestedObject("header");
480  header["payloadVersion"] = 2;
481  header["signatureVersion"] = 1;
482 
483  JsonObject payload = responseMessage.createNestedObject("payload");
484  payload["action"] = requestMessage["payload"]["action"];
485  payload["clientId"] = requestMessage["payload"]["clientId"];
486  payload["createdAt"] = 0;
487  payload["deviceId"] = requestMessage["payload"]["deviceId"];
488  payload["message"] = "OK";
489  payload["replyToken"] = requestMessage["payload"]["replyToken"];
490  payload["success"] = false;
491  payload["type"] = "response";
492  payload.createNestedObject("value");
493  return responseMessage;
494 }
495 
496 
497 DynamicJsonDocument SinricProClass::prepareEvent(const char* deviceId, const char* action, const char* cause) {
498  DynamicJsonDocument eventMessage(1024);
499  JsonObject header = eventMessage.createNestedObject("header");
500  header["payloadVersion"] = 2;
501  header["signatureVersion"] = 1;
502 
503  JsonObject payload = eventMessage.createNestedObject("payload");
504  payload["action"] = action;
505  payload["cause"].createNestedObject("type");
506  payload["cause"]["type"] = cause;
507  payload["createdAt"] = 0;
508  payload["deviceId"] = deviceId;
509  payload["replyToken"] = MessageID().getID();
510  payload["type"] = "event";
511  payload.createNestedObject("value");
512  return eventMessage;
513 }
514 
515 #ifndef NOSINRIC_INSTANCE
516 
524 #endif
525 
526 #endif
SinricProClass::handle
void handle()
Handles communication between device and SinricPro Server.
Definition: SinricPro.h:224
SinricProClass::restoreDeviceStates
void restoreDeviceStates(bool flag)
Enable / disable restore device states function.
Definition: SinricPro.h:473
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:70
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:157
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.