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