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