1 /******************************************************************************
2 Permission is hereby granted, free of charge, to any person obtaining a copy
3 of this software and associated documentation files (the "Software"), to deal
4 in the Software without restriction, including without limitation the rights
5 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
6 copies of the Software, and to permit persons to whom the Software is
7 furnished to do so, subject to the following conditions:
8
9 The above copyright notice and this permission notice shall be included in all
10 copies or substantial portions of the Software.
11
12 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18 SOFTWARE.
19
20 ===========
21 Original source code from: http://sourceforge.net/projects/harmonyhubcontrol/
22 Integration in Domoticz done by: Jan ten Hove
23 Cleanup and changes: GizMoCuz
24 History:
25 16 August 2016:
26 - Fixed: Logitech Harmony, Ping request now working as well for firmware 4.10.30 (Herman)
27 19 November 2016:
28 - Removed: Need to login remotely with username/password
29 11 April 2018:
30 - Refactored: address several issues with working with firmware 4.14.123 (gordonb3)
31 19 June 2018:
32 - Complete overhaul: Abandon original methods from harmonyhubcontrol (gordonb3)
33 => use a centralized (polled) socket reader function rather than wait for a direct
34 response to a socket write operation.
35 25 June 2018:
36 - Make debug lines available to the logging system
37 */
38
39 //#define _DEBUG
40
41
42 #include "stdafx.h"
43 #include "HarmonyHub.h"
44 #include "../main/Helper.h"
45 #include "../main/Logger.h"
46 #include "hardwaretypes.h"
47 #include "../main/localtime_r.h"
48 #include "../main/RFXtrx.h"
49 #include "../main/SQLHelper.h"
50 #include "../main/mainworker.h"
51 #include "../webserver/Base64.h"
52 #include "csocket.h"
53 #include "../main/json_helper.h"
54
55
56 #define CONNECTION_ID "446f6d6f-7469-637a-2d48-61726d6f6e79" // `Domoticz-Harmony` in ASCII
57 #define GET_CONFIG_COMMAND "get_config"
58 #define START_ACTIVITY_COMMAND "start_activity"
59 #define GET_CURRENT_ACTIVITY_COMMAND "get_current_activity_id"
60 #define HARMONY_PING_INTERVAL_SECONDS 30 // must be smaller than 40 seconds or Hub will silently hang up on us
61 #define HARMONY_SEND_ACK_SECONDS 10 // must be smaller than 20 seconds...
62 #define HARMONY_PING_RETRY_SECONDS 5
63 #define HARMONY_RETRY_LOGIN_SECONDS 60
64 #define HEARTBEAT_SECONDS 12
65
66
67 // Note:
68 // HarmonyHub is on Wifi and can thus send frames with a maximum payload length of 2324 bytes
69 // Normal implementations will however obey the 1500 bytes MTU from the wired networks that
70 // they are attached to and this may be limited even further if the router uses mechanisms like
71 // PPTP for connecting the (Wireless) LAN to the internet.
72 #define SOCKET_BUFFER_SIZE 1500
73
74
75 #ifdef _DEBUG
76 #include <iostream>
77 #endif
78
CHarmonyHub(const int ID,const std::string & IPAddress,const unsigned int port)79 CHarmonyHub::CHarmonyHub(const int ID, const std::string &IPAddress, const unsigned int port):
80 m_szHarmonyAddress(IPAddress)
81 {
82 m_usHarmonyPort = (unsigned short)port;
83 m_HwdID=ID;
84 Init();
85 }
86
87
~CHarmonyHub(void)88 CHarmonyHub::~CHarmonyHub(void)
89 {
90 StopHardware();
91 }
92
93
Init()94 void CHarmonyHub::Init()
95 {
96 m_connection=NULL;
97 m_connectionstatus = DISCONNECTED;
98 m_bNeedMoreData = false;
99 m_bWantAnswer = false;
100 m_bNeedEcho = false;
101 m_bReceivedMessage = false;
102 m_bIsChangingActivity = false;
103 m_bShowConnectError = true;
104 m_szCurActivityID = "";
105 m_szHubSwVersion = "";
106 m_szAuthorizationToken = "";
107 m_mapActivities.clear();
108 }
109
110
Logout()111 void CHarmonyHub::Logout()
112 {
113 if (m_connectionstatus != DISCONNECTED)
114 {
115 #ifdef _DEBUG
116 std::cerr << "logout requested: close stream\n";
117 #endif
118 CloseStream(m_connection);
119 int i = 50;
120 while ((i > 0) && (m_connectionstatus != DISCONNECTED))
121 {
122 sleep_milliseconds(40);
123 CheckForHarmonyData();
124 i--;
125 }
126 if (m_connectionstatus != DISCONNECTED)
127 {
128 // something went wrong
129 #ifdef _DEBUG
130 std::cerr << "stream did not sucessfully close: killing TCP socket\n";
131 #endif
132 ResetCommunicationSocket();
133 }
134 }
135 Init();
136 }
137
138
StartHardware()139 bool CHarmonyHub::StartHardware()
140 {
141 RequestStart();
142
143 Init();
144 //Start worker thread
145 m_thread = std::make_shared<std::thread>(&CHarmonyHub::Do_Work, this);
146 SetThreadNameInt(m_thread->native_handle());
147 m_bIsStarted = true;
148 sOnConnected(this);
149 return (m_thread != nullptr);
150 }
151
152
StopHardware()153 bool CHarmonyHub::StopHardware()
154 {
155 if (m_thread)
156 {
157 RequestStop();
158 m_thread->join();
159 m_thread.reset();
160 }
161 m_bIsStarted = false;
162 m_bIsChangingActivity = false;
163 m_szHubSwVersion = "";
164 return true;
165 }
166
167
WriteToHardware(const char * pdata,const unsigned char)168 bool CHarmonyHub::WriteToHardware(const char *pdata, const unsigned char /*length*/)
169 {
170 const tRBUF *pCmd = reinterpret_cast<const tRBUF*>(pdata);
171
172 if (this->m_bIsChangingActivity)
173 {
174 _log.Log(LOG_ERROR, "Harmony Hub: Command cannot be sent. Hub is changing activity");
175 return false;
176 }
177
178 if (this->m_connectionstatus == DISCONNECTED)
179 {
180 if (this->m_szAuthorizationToken.empty() &&
181 (pCmd->LIGHTING2.id1 == 0xFF) && (pCmd->LIGHTING2.id2 == 0xFF) &&
182 (pCmd->LIGHTING2.id3 == 0xFF) && (pCmd->LIGHTING2.id4 == 0xFF) &&
183 (pCmd->LIGHTING2.cmnd == 0)
184 )
185 {
186 // "secret" undefined state request to silence connection error reporting
187 if (this->m_bShowConnectError)
188 _log.Log(LOG_STATUS, "Harmony Hub: disable connection error logging");
189 this->m_bShowConnectError = false;
190 return false;
191 }
192
193 _log.Log(LOG_STATUS, "Harmony Hub: Received a switch command but we are not connected - attempting connect now");
194 this->m_bLoginNow = true;
195 int retrycount = 0;
196 while ( (retrycount < 10) && (!IsStopRequested(500)) )
197 {
198 // give worker thread up to 5 seconds time to establish the connection
199 if ((this->m_connectionstatus == BOUND) && (!m_szCurActivityID.empty()))
200 break;
201 retrycount++;
202 }
203 if (IsStopRequested(0))
204 return true;
205
206 if (this->m_connectionstatus == DISCONNECTED)
207 {
208 _log.Log(LOG_ERROR, "Harmony Hub: Connect failed: cannot send the switch command");
209 return false;
210 }
211 }
212
213 if (pCmd->LIGHTING2.packettype == pTypeLighting2)
214 {
215 int lookUpId = (int)(pCmd->LIGHTING2.id1 << 24) | (int)(pCmd->LIGHTING2.id2 << 16) | (int)(pCmd->LIGHTING2.id3 << 8) | (int)(pCmd->LIGHTING2.id4) ;
216 std::string realID = std::to_string(lookUpId);
217
218 if (pCmd->LIGHTING2.cmnd == 0)
219 {
220 if (m_szCurActivityID != realID)
221 {
222 return false;
223 }
224 if (realID == "-1") // powering off the PowerOff activity leads to an undefined state in the frontend
225 {
226 // send it anyway, user may be trying to correct a wrong state of the Hub
227 SubmitCommand(START_ACTIVITY_COMMAND, "-1");
228 // but don't allow the frontend to update the button state to the off position
229 return false;
230 }
231 if (SubmitCommand(START_ACTIVITY_COMMAND, "-1") <= 0)
232 {
233 _log.Log(LOG_ERROR, "Harmony Hub: Error sending the power-off command");
234 return false;
235 }
236 }
237 else if (SubmitCommand(START_ACTIVITY_COMMAND, realID) <= 0)
238 {
239 _log.Log(LOG_ERROR, "Harmony Hub: Error sending the switch command");
240 return false;
241 }
242 }
243 return true;
244 }
245
246
Do_Work()247 void CHarmonyHub::Do_Work()
248 {
249 _log.Log(LOG_STATUS,"Harmony Hub: Worker thread started...");
250
251 unsigned int pcounter = 0; // ping interval counter
252 unsigned int tcounter = 0; // 1/25 seconds
253
254 char lcounter = 0; // login counter
255 char fcounter = 0; // failed login attempts
256 char hcounter = HEARTBEAT_SECONDS; // heartbeat interval counter
257
258
259 m_bLoginNow = true;
260
261 while (!IsStopRequested(0))
262 {
263 if (m_connectionstatus == BOUND)
264 {
265 if (!m_bWantAnswer)
266 {
267 if (m_szAuthorizationToken.empty())
268 {
269 // we are not yet authenticated -> send pair request
270 SendPairRequest(m_connection);
271 m_bShowConnectError = false;
272 #ifdef _DEBUG
273 std::cerr << "disable show connect error\n";
274 #endif
275 }
276 else
277 {
278 if (m_mapActivities.empty())
279 {
280 // instruct Hub to send us its config so we can retrieve the list of activities
281 SubmitCommand(GET_CONFIG_COMMAND);
282 }
283 else if (m_szCurActivityID.empty())
284 {
285 fcounter = 0;
286 _log.Log(LOG_STATUS, "Harmony Hub: Connected to Hub.");
287 SubmitCommand(GET_CURRENT_ACTIVITY_COMMAND);
288 }
289 }
290 }
291
292 if (!tcounter) // slot this to full seconds only to prevent racing
293 {
294 // Hub requires us to actively keep our connection alive by periodically pinging it
295 if ((pcounter % HARMONY_PING_INTERVAL_SECONDS) == 0)
296 {
297 #ifdef _DEBUG
298 std::cerr << "send ping\n";
299 #endif
300 if (m_bNeedEcho || SendPing() < 0)
301 {
302 // Hub dropped our connection
303 _log.Log(LOG_ERROR, "Harmony Hub: Error pinging server.. Resetting connection.");
304 ResetCommunicationSocket();
305 pcounter = HARMONY_RETRY_LOGIN_SECONDS - 5; // wait 5 seconds before attempting login again
306 }
307 }
308
309 else if (m_bNeedEcho && ((pcounter % HARMONY_PING_RETRY_SECONDS) == 0))
310 {
311 // timeout
312 #ifdef _DEBUG
313 std::cerr << "retry ping\n";
314 #endif
315 if (SendPing() < 0)
316 {
317 // Hub dropped our connection
318 _log.Log(LOG_ERROR, "Harmony Hub: Error pinging server.. Resetting connection.");
319 ResetCommunicationSocket();
320 pcounter = HARMONY_RETRY_LOGIN_SECONDS - 5; // wait 5 seconds before attempting login again
321 }
322 }
323 }
324 }
325 else if (m_connectionstatus == DISCONNECTED)
326 {
327 if (!tcounter) // slot this to full seconds only to prevent racing
328 {
329 if (!m_bLoginNow)
330 {
331 if ((pcounter % HARMONY_RETRY_LOGIN_SECONDS) == 0)
332 {
333 lcounter++;
334 if (lcounter > fcounter)
335 {
336 m_bLoginNow = true;
337 if (fcounter > 0)
338 _log.Log(LOG_NORM, "Harmony Hub: Reattempt login.");
339 }
340 }
341 }
342 if (m_bLoginNow)
343 {
344 m_bLoginNow = false;
345 m_bWantAnswer = false;
346 m_bNeedEcho = false;
347 if (fcounter < 5)
348 fcounter++;
349 lcounter = 0;
350 pcounter = 0;
351 m_szCurActivityID = "";
352 if (!SetupCommunicationSocket())
353 {
354 if ((m_connectionstatus == DISCONNECTED) && (hcounter > 3))
355 {
356 // adjust heartbeat counter for connect timeout
357 hcounter -= 3;
358 }
359 }
360 }
361 }
362 }
363 else
364 {
365 // m_connectionstatus == CONNECTED || INITIALIZED || AUTHENTICATED
366 if (!tcounter)
367 {
368 if ((pcounter % HARMONY_RETRY_LOGIN_SECONDS) > 1)
369 {
370 // timeout
371 _log.Log(LOG_ERROR, "Harmony Hub: setup command socket timed out");
372 ResetCommunicationSocket();
373 }
374 }
375 }
376
377 // check socket ready and if it is then read Harmony data
378 CheckForHarmonyData();
379
380 if (m_bReceivedMessage)
381 {
382 if (pcounter < (HARMONY_PING_INTERVAL_SECONDS - HARMONY_SEND_ACK_SECONDS))
383 {
384 // fast forward our ping counter
385 pcounter = HARMONY_PING_INTERVAL_SECONDS - HARMONY_SEND_ACK_SECONDS;
386 #ifdef _DEBUG
387 std::cerr << "fast forward ping counter to " << pcounter << " seconds\n";
388 #endif
389 }
390 m_bReceivedMessage = false;
391 }
392
393 if (m_bWantAnswer || tcounter)
394 {
395 // use a 40ms poll interval
396 sleep_milliseconds(40);
397 tcounter++;
398 if ((tcounter % 25) == 0)
399 {
400 tcounter = 0;
401 pcounter++;
402 hcounter--;
403 }
404 }
405 else
406 {
407 // use a 1s poll interval
408 if (IsStopRequested(1000))
409 break;
410 pcounter++;
411 hcounter--;
412 }
413
414 if (!hcounter)
415 {
416 // update heartbeat
417 hcounter = HEARTBEAT_SECONDS;
418 m_LastHeartbeat=mytime(NULL);
419 }
420 }
421 Logout();
422
423 _log.Log(LOG_STATUS,"Harmony Hub: Worker stopped...");
424 }
425
426
427 /************************************************************************
428 * *
429 * Update a single activity switch if exist *
430 * *
431 ************************************************************************/
CheckSetActivity(const std::string & activityID,const bool on)432 void CHarmonyHub::CheckSetActivity(const std::string &activityID, const bool on)
433 {
434 // get the device id from the db (if already inserted)
435 int actId=atoi(activityID.c_str());
436 std::stringstream hexId ;
437 hexId << std::setw(7) << std::hex << std::setfill('0') << std::uppercase << (int)( actId) ;
438 std::string actHex = hexId.str();
439 std::vector<std::vector<std::string> > result;
440 result = m_sql.safe_query("SELECT Name,DeviceID FROM DeviceStatus WHERE (HardwareID==%d) AND (DeviceID=='%q')", m_HwdID, actHex.c_str());
441 if (!result.empty())
442 {
443 UpdateSwitch((uint8_t)(atoi(result[0][1].c_str())), activityID.c_str(),on,result[0][0]);
444 }
445 }
446
447
448 /************************************************************************
449 * *
450 * Insert/Update a single activity switch (unconditional) *
451 * *
452 ************************************************************************/
UpdateSwitch(unsigned char,const char * realID,const bool bOn,const std::string & defaultname)453 void CHarmonyHub::UpdateSwitch(unsigned char /*idx*/, const char *realID, const bool bOn, const std::string &defaultname)
454 {
455 std::stringstream hexId ;
456 hexId << std::setw(7) << std::setfill('0') << std::hex << std::uppercase << (int)( atoi(realID) );
457 std::vector<std::vector<std::string> > result;
458 result = m_sql.safe_query("SELECT Name,nValue,sValue FROM DeviceStatus WHERE (HardwareID==%d) AND (DeviceID=='%q')", m_HwdID, hexId.str().c_str());
459 if (!result.empty())
460 {
461 //check if we have a change, if not do not update it
462 int nvalue = atoi(result[0][1].c_str());
463 if ((!bOn) && (nvalue == light2_sOff))
464 return;
465 if ((bOn && (nvalue != light2_sOff)))
466 return;
467 }
468 int i_Id = atoi( realID);
469 //Send as Lighting 2
470 tRBUF lcmd;
471 memset(&lcmd, 0, sizeof(RBUF));
472 lcmd.LIGHTING2.packetlength = sizeof(lcmd.LIGHTING2) - 1;
473 lcmd.LIGHTING2.packettype = pTypeLighting2;
474 lcmd.LIGHTING2.subtype = sTypeAC;
475
476 lcmd.LIGHTING2.id1 = (i_Id>> 24) & 0xFF;
477 lcmd.LIGHTING2.id2 = (i_Id>> 16) & 0xFF;
478 lcmd.LIGHTING2.id3 = (i_Id>> 8) & 0xFF;
479 lcmd.LIGHTING2.id4 = (i_Id) & 0xFF;
480 lcmd.LIGHTING2.unitcode = 1;
481 uint8_t level = 15;
482 if (!bOn)
483 {
484 level = 0;
485 lcmd.LIGHTING2.cmnd = light2_sOff;
486 }
487 else
488 {
489 level = 15;
490 lcmd.LIGHTING2.cmnd = light2_sOn;
491 }
492 lcmd.LIGHTING2.level = level;
493 lcmd.LIGHTING2.filler = 0;
494 lcmd.LIGHTING2.rssi = 12;
495 sDecodeRXMessage(this, (const unsigned char *)&lcmd.LIGHTING2, defaultname.c_str(), 255);
496 }
497
498
499 /************************************************************************
500 * *
501 * Raw socket functions *
502 * *
503 ************************************************************************/
ConnectToHarmony(const std::string & szHarmonyIPAddress,const int HarmonyPortNumber,csocket * connection)504 bool CHarmonyHub::ConnectToHarmony(const std::string &szHarmonyIPAddress, const int HarmonyPortNumber, csocket* connection)
505 {
506 if (szHarmonyIPAddress.length() == 0 || HarmonyPortNumber == 0 || HarmonyPortNumber > 65535)
507 return false;
508
509 connection->connect(szHarmonyIPAddress.c_str(), HarmonyPortNumber);
510
511 return (connection->getState() == csocket::CONNECTED);
512 }
513
514
SetupCommunicationSocket()515 bool CHarmonyHub::SetupCommunicationSocket()
516 {
517 if (m_connection)
518 ResetCommunicationSocket();
519
520 m_connection = new csocket();
521
522 if(!ConnectToHarmony(m_szHarmonyAddress, m_usHarmonyPort, m_connection))
523 {
524 if (m_bShowConnectError)
525 _log.Log(LOG_ERROR,"Harmony Hub: Cannot connect to Harmony Hub. Check IP/Port.");
526 return false;
527 }
528 m_connectionstatus = CONNECTED;
529 int ret = StartStream(m_connection);
530 return (ret >= 0);
531 }
532
533
ResetCommunicationSocket()534 void CHarmonyHub::ResetCommunicationSocket()
535 {
536 if (m_connection)
537 delete m_connection;
538 m_connection = NULL;
539 m_connectionstatus = DISCONNECTED;
540
541 m_bIsChangingActivity = false;
542 m_bWantAnswer = false;
543 m_szCurActivityID = "";
544 }
545
546
WriteToSocket(std::string * szReq)547 int CHarmonyHub::WriteToSocket(std::string *szReq)
548 {
549 int ret = m_connection->write(szReq->c_str(), static_cast<unsigned int>(szReq->length()));
550 if (ret > 0)
551 m_bWantAnswer = true;
552 return ret;
553 }
554
555
ReadFromSocket(csocket * connection)556 std::string CHarmonyHub::ReadFromSocket(csocket *connection)
557 {
558 return ReadFromSocket(connection, 0);
559 }
ReadFromSocket(csocket * connection,float waitTime)560 std::string CHarmonyHub::ReadFromSocket(csocket *connection, float waitTime)
561 {
562 if (connection == NULL)
563 {
564 return "</stream:stream>";
565 }
566
567 std::string szData;
568
569 bool bIsDataReadable = true;
570 connection->canRead(&bIsDataReadable, waitTime);
571 if (bIsDataReadable)
572 {
573 char databuffer[SOCKET_BUFFER_SIZE];
574 int bytesReceived = connection->read(databuffer, SOCKET_BUFFER_SIZE, false);
575 if (bytesReceived > 0)
576 {
577 szData = std::string(databuffer, bytesReceived);
578 }
579 }
580 return szData;
581 }
582
583
584 /************************************************************************
585 * *
586 * XMPP Stream initialization *
587 * *
588 ************************************************************************/
StartStream(csocket * connection)589 int CHarmonyHub::StartStream(csocket *connection)
590 {
591 if (connection == NULL)
592 return -1;
593 std::string szReq = "<stream:stream to='connect.logitech.com' xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' xml:lang='en' version='1.0'>";
594 return WriteToSocket(&szReq);
595 }
596
597
SendAuth(csocket * connection,const std::string & szUserName,const std::string & szPassword)598 int CHarmonyHub::SendAuth(csocket *connection, const std::string &szUserName, const std::string &szPassword)
599 {
600 if (connection == NULL)
601 return -1;
602 std::string szAuth = "<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"PLAIN\">";
603 std::string szCred = "\0";
604 szCred.append(szUserName);
605 szCred.append("\0");
606 szCred.append(szPassword);
607 szAuth.append(base64_encode(szCred));
608 szAuth.append("</auth>");
609 return WriteToSocket(&szAuth);
610 }
611
612
SendPairRequest(csocket * connection)613 int CHarmonyHub::SendPairRequest(csocket *connection)
614 {
615 if (connection == NULL)
616 return -1;
617 std::string szReq = "<iq type=\"get\" id=\"";
618 szReq.append(CONNECTION_ID);
619 szReq.append("\"><oa xmlns=\"connect.logitech.com\" mime=\"vnd.logitech.connect/vnd.logitech.pair\">method=pair");
620 szReq.append(":name=foo#iOS6.0.1#iPhone</oa></iq>");
621 return WriteToSocket(&szReq);
622 }
623
624
CloseStream(csocket * connection)625 int CHarmonyHub::CloseStream(csocket *connection)
626 {
627 if (connection == NULL)
628 return -1;
629 std::string szReq = "</stream:stream>";
630 return WriteToSocket(&szReq);
631 }
632
633
634 /************************************************************************
635 * *
636 * Ping function *
637 * *
638 ************************************************************************/
SendPing()639 int CHarmonyHub::SendPing()
640 {
641 if (m_connection == NULL || m_szAuthorizationToken.length() == 0)
642 return -1;
643
644 std::string szReq = "<iq type=\"get\" id=\"";
645 szReq.append(CONNECTION_ID);
646 szReq.append("\"><oa xmlns=\"connect.logitech.com\" mime=\"vnd.logitech.connect/vnd.logitech.ping\">token=");
647 szReq.append(m_szAuthorizationToken.c_str());
648 szReq.append(":name=foo#iOS6.0.1#iPhone</oa></iq>");
649
650 m_bNeedEcho = true;
651 return WriteToSocket(&szReq);
652 }
653
654
655 /************************************************************************
656 * *
657 * Submit command function *
658 * *
659 ************************************************************************/
SubmitCommand(const std::string & szCommand)660 int CHarmonyHub::SubmitCommand(const std::string &szCommand)
661 {
662 return SubmitCommand(szCommand, "");
663 }
SubmitCommand(const std::string & szCommand,const std::string & szActivityId)664 int CHarmonyHub::SubmitCommand(const std::string &szCommand, const std::string &szActivityId)
665 {
666 if (m_connection == NULL || m_szAuthorizationToken.empty())
667 {
668 //errorString = "SubmitCommand : NULL csocket or empty authorization token provided";
669 return false;
670
671 }
672
673 std::string szReq = "<iq type=\"get\" id=\"";
674 szReq.append(CONNECTION_ID);
675 szReq.append("\"><oa xmlns=\"connect.logitech.com\" mime=\"vnd.logitech.harmony/vnd.logitech.harmony.engine?");
676
677 // Issue the provided command
678 if (szCommand == GET_CONFIG_COMMAND)
679 {
680 szReq.append("config\"></oa></iq>");
681 }
682 else if (szCommand == START_ACTIVITY_COMMAND)
683 {
684 szReq.append("startactivity\">activityId=");
685 szReq.append(szActivityId.c_str());
686 szReq.append(":timestamp=0</oa></iq>");
687 }
688 else
689 {
690 // default action: return query for the current activity
691 szReq.append("getCurrentActivity\" /></iq>");
692 }
693
694 return WriteToSocket(&szReq);
695 }
696
697
698 /************************************************************************
699 * *
700 * Communication handler *
701 * - reconstructs network frames into a single communication block *
702 * to be handled by the parser *
703 * *
704 ************************************************************************/
CheckForHarmonyData()705 bool CHarmonyHub::CheckForHarmonyData()
706 {
707 std::lock_guard<std::mutex> lock(m_mutex);
708
709 if (m_connectionstatus == DISCONNECTED)
710 return false;
711
712 if (m_bNeedMoreData)
713 m_szHarmonyData.append(ReadFromSocket(m_connection, 0));
714 else
715 m_szHarmonyData = ReadFromSocket(m_connection, 0);
716
717 if (m_szHarmonyData.empty())
718 return false;
719
720
721 if (m_szHarmonyData.compare(0, 8, "<message") == 0)
722 {
723 // Hub is sending one or more messages
724 if (IsTransmissionComplete(&m_szHarmonyData))
725 ParseHarmonyTransmission(&m_szHarmonyData);
726 }
727
728 else if (m_szHarmonyData.compare("<iq/>") == 0)
729 {
730 // Hub just acknowledges receiving our query
731 #ifdef _DEBUG
732 std::cerr << "received ACK\n";
733 #endif
734 }
735
736 else if (m_szHarmonyData.compare(0, 3, "<iq") == 0)
737 {
738 // Hub is answering our query
739 if (IsTransmissionComplete(&m_szHarmonyData))
740 ParseHarmonyTransmission(&m_szHarmonyData);
741 }
742
743 else if (m_szHarmonyData.compare(0, 5, "<?xml") == 0)
744 {
745 // Hub responds to our start stream request
746 ProcessHarmonyConnect(&m_szHarmonyData);
747 }
748
749 else if (m_szHarmonyData.compare(0, 7, "<stream") == 0)
750 {
751 // Hub responds to our (re)start stream request
752 ProcessHarmonyConnect(&m_szHarmonyData);
753 }
754
755 else if (m_szHarmonyData.compare(0, 8, "<success") == 0)
756 {
757 // Hub accepted our credentials
758 ProcessHarmonyConnect(&m_szHarmonyData);
759 }
760
761 else if (m_szHarmonyData.compare(0, 8, "</stream") == 0)
762 {
763 // Hub is closing the connection
764 ParseHarmonyTransmission(&m_szHarmonyData);
765 }
766
767 else
768 {
769 // unhandled stanza
770 }
771 return true;
772 }
773
774
775 /************************************************************************
776 * *
777 * Helper function for the communication handler *
778 * - verifies that the communication block is complete so we can *
779 * process it or we need to wait for additional data to come in *
780 * *
781 ************************************************************************/
IsTransmissionComplete(std::string * szHarmonyData)782 bool CHarmonyHub::IsTransmissionComplete(std::string *szHarmonyData)
783 {
784 size_t msgClose = szHarmonyData->find("</message>", szHarmonyData->length() - 10);
785 if (msgClose != std::string::npos)
786 return true;
787 size_t resultClose = szHarmonyData->find("</iq>", szHarmonyData->length() - 5);
788 if (resultClose != std::string::npos)
789 return true;
790 size_t streamClose = szHarmonyData->find("</stream:stream>", szHarmonyData->length() - 16);
791 if (streamClose != std::string::npos)
792 return true;
793
794 m_bNeedMoreData = true;
795 return false;
796 }
797
798
799 /************************************************************************
800 * *
801 * Wrapper function for pairing with the Harmony Hub *
802 * *
803 ************************************************************************/
ProcessHarmonyConnect(std::string * szHarmonyData)804 void CHarmonyHub::ProcessHarmonyConnect(std::string *szHarmonyData)
805 {
806 m_bWantAnswer = false;
807 int sendStatus = 0;
808 if ((m_connectionstatus == CONNECTED) && (szHarmonyData->find("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>") != std::string::npos))
809 {
810 // stream initiated successfully
811 m_connectionstatus = INITIALIZED;
812 #ifdef _DEBUG
813 std::cerr << "connectionstatus = initialized\n";
814 #endif
815 if (m_szAuthorizationToken.empty())
816 sendStatus = SendAuth(m_connection, "guest", "gatorade");
817 else
818 sendStatus = SendAuth(m_connection, m_szAuthorizationToken, m_szAuthorizationToken);
819 }
820
821 else if ((m_connectionstatus == INITIALIZED) && (*szHarmonyData == "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>"))
822 {
823 // authentication successful - restart our stream to bind
824 #ifdef _DEBUG
825 std::cerr << "connectionstatus = authenticated\n";
826 #endif
827 m_connectionstatus = AUTHENTICATED;
828 sendStatus = StartStream(m_connection);
829 }
830 else if ((m_connectionstatus == AUTHENTICATED) && (szHarmonyData->find("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>") != std::string::npos))
831 {
832 // stream bind completed
833 m_connectionstatus = BOUND;
834 #ifdef _DEBUG
835 std::cerr << "connectionstatus = bound\n";
836 #endif
837 }
838
839 if (sendStatus < 0)
840 {
841 // error while sending commands to hub
842 _log.Log(LOG_ERROR, "Harmony Hub: Cannot setup command socket to Harmony Hub");
843 ResetCommunicationSocket();
844 }
845 }
846
847
848 /************************************************************************
849 * *
850 * Query response handler *
851 * - handles direct responses to queries we sent to Harmony Hub *
852 * *
853 ************************************************************************/
ProcessQueryResponse(std::string * szQueryResponse)854 void CHarmonyHub::ProcessQueryResponse(std::string *szQueryResponse)
855 {
856 size_t pos = 0;
857 m_bWantAnswer = false;
858
859 pos = szQueryResponse->find("errorcode=");
860 if (pos != std::string::npos)
861 {
862 std::string szErrorcode = szQueryResponse->substr(pos + 11, 3);
863 #ifdef _DEBUG
864 if (szErrorcode == "100")
865 {
866 // This errorcode has been seen with `mime='harmony.engine?startActivity'`
867 // We're currently not interested in those - see related comment section below
868
869 std::cerr << "iq message status = continue\n";
870 return;
871 }
872 #endif
873 if (szErrorcode != "200")
874 {
875 #ifdef _DEBUG
876 std::cerr << "iq message status = error\n";
877 #endif
878 return;
879 }
880 }
881
882 #ifdef _DEBUG
883 std::cerr << "iq message status = OK\n";
884 #endif
885
886
887 // mime='vnd.logitech.connect/vnd.logitech.ping'
888 if (szQueryResponse->find("vnd.logitech.ping") != std::string::npos)
889 {
890 // got ping return
891 #ifdef _DEBUG
892 std::cerr << "got ping return\n";
893 #endif
894 m_bNeedEcho = false;
895 }
896
897 // mime='vnd.logitech.harmony/vnd.logitech.harmony.engine?getCurrentActivity'
898 else if (szQueryResponse->find("engine?getCurrentActivity") != std::string::npos)
899 {
900 pos = szQueryResponse->find("<![CDATA[result=");
901 if (pos != std::string::npos)
902 {
903 std::string szJsonString = szQueryResponse->substr(pos + 16);
904 pos = szJsonString.find("]]>");
905 if (pos != std::string::npos)
906 {
907 std::string szCurrentActivity = szJsonString.substr(0, pos);
908 if (_log.IsDebugLevelEnabled(DEBUG_HARDWARE))
909 {
910 _log.Debug(DEBUG_HARDWARE, "Harmony Hub: Current activity ID = %d (%s)", atoi(szCurrentActivity.c_str()), m_mapActivities[szCurrentActivity].c_str());
911 }
912
913 if (m_szCurActivityID.empty()) // initialize all switches
914 {
915 m_szCurActivityID = szCurrentActivity;
916 for (const auto & itt : m_mapActivities)
917 {
918 UpdateSwitch(0, itt.first.c_str(), (m_szCurActivityID == itt.first), itt.second);
919 }
920 }
921 else if (m_szCurActivityID != szCurrentActivity)
922 {
923 CheckSetActivity(m_szCurActivityID, false);
924 m_szCurActivityID = szCurrentActivity;
925 CheckSetActivity(m_szCurActivityID, true);
926 }
927 }
928 }
929 }
930
931 #ifdef _DEBUG
932 // mime='vnd.logitech.harmony/vnd.logitech.harmony.engine?startactivity'
933 else if (szQueryResponse->find("engine?startactivity") != std::string::npos)
934 {
935 // doesn't appear to hold any sensible data - also always returns errorcode='200' even if activity is incorrect.
936 }
937
938 // mime='harmony.engine?startActivity'
939 else if (szQueryResponse->find("engine?startActivity") != std::string::npos)
940 {
941 // This chain of query type messages follow after startactivity, apparently to inform that commands are being
942 // executed towards specific deviceId's, but not showing what these commands are.
943
944 // Since the mime is different from what we sent to Harmony Hub this appears to be a query directed to us, but
945 // it is unknown whether we should acknowledge and/or return some response. The Hub doesn't seem to mind that
946 // we don't though.
947 }
948 #endif
949
950 // mime='vnd.logitech.harmony/vnd.logitech.harmony.engine?config'
951 else if (szQueryResponse->find("engine?config") != std::string::npos)
952 {
953
954 #ifdef _DEBUG
955 std::cerr << "reading engine config\n";
956 #endif
957 pos = szQueryResponse->find("![CDATA[{");
958 if (pos == std::string::npos)
959 {
960 #ifdef _DEBUG
961 std::cerr << "error: response does not contain a CDATA section\n";
962 #endif
963 return;
964 }
965
966 std::string szJsonString = szQueryResponse->substr(pos + 8);
967 Json::Value j_result;
968
969 bool ret = ParseJSon(szJsonString.c_str(), j_result);
970 if ((!ret) || (!j_result.isObject()))
971 {
972 _log.Log(LOG_ERROR, "Harmony Hub: Invalid data received! (Update Activities)");
973 return;
974 }
975
976 if (j_result["activity"].empty())
977 {
978 _log.Log(LOG_ERROR, "Harmony Hub: Invalid data received! (Update Activities)");
979 return;
980 }
981
982 try
983 {
984 int totActivities = (int)j_result["activity"].size();
985 for (int ii = 0; ii < totActivities; ii++)
986 {
987 std::string aID = j_result["activity"][ii]["id"].asString();
988 std::string aLabel = j_result["activity"][ii]["label"].asString();
989 m_mapActivities[aID] = aLabel;
990 }
991 }
992 catch (...)
993 {
994 _log.Log(LOG_ERROR, "Harmony Hub: Invalid data received! (Update Activities, JSon activity)");
995 }
996
997 if (_log.IsDebugLevelEnabled(DEBUG_HARDWARE))
998 {
999 std::string resultString = "Harmony Hub: Activity list: {";
1000
1001 std::map<std::string, std::string>::iterator it = m_mapActivities.begin();
1002 std::map<std::string, std::string>::iterator ite = m_mapActivities.end();
1003 for (; it != ite; ++it)
1004 {
1005 resultString.append("\"");
1006 resultString.append(it->second);
1007 resultString.append("\":\"");
1008 resultString.append(it->first);
1009 resultString.append("\",");
1010 }
1011 resultString=resultString.substr(0, resultString.size()-1);
1012 resultString.append("}");
1013
1014 _log.Debug(DEBUG_HARDWARE, resultString);
1015 }
1016 }
1017
1018 // mime='vnd.logitech.connect/vnd.logitech.pair'
1019 else if (szQueryResponse->find("vnd.logitech.pair") != std::string::npos)
1020 {
1021 pos = szQueryResponse->find("identity=");
1022 if (pos == std::string::npos)
1023 {
1024 #ifdef _DEBUG
1025 std::cerr << "GetAuthorizationToken : Logitech Harmony response does not contain a session authorization token\n";
1026 #endif
1027 return;
1028 }
1029 m_szAuthorizationToken = szQueryResponse->substr(pos + 9);
1030 pos = m_szAuthorizationToken.find(":");
1031 if (pos == std::string::npos)
1032 {
1033 #ifdef _DEBUG
1034 std::cerr << "GetAuthorizationToken : Logitech Harmony response does not contain a valid session authorization token\n";
1035 #endif
1036 m_szAuthorizationToken = "";
1037 return;
1038 }
1039 m_szAuthorizationToken = m_szAuthorizationToken.substr(0, pos);
1040 CloseStream(m_connection);
1041 m_bLoginNow = true;
1042 }
1043 }
1044
1045
1046 /************************************************************************
1047 * *
1048 * Message handler *
1049 * - handles notifications sent by Harmony Hub *
1050 * *
1051 ************************************************************************/
ProcessHarmonyMessage(std::string * szMessageBlock)1052 void CHarmonyHub::ProcessHarmonyMessage(std::string *szMessageBlock)
1053 {
1054 size_t pos = 9;
1055 size_t msgStart = 0;
1056
1057 if (szMessageBlock->compare(pos, 7, "content") != 0)
1058 {
1059 // bad message format
1060 #ifdef _DEBUG
1061 std::cerr << "error: unrecognized message format (no content-length)\n";
1062 #endif
1063 return;
1064 }
1065 pos += 16;
1066 size_t valueEnd = szMessageBlock->find("\"", pos);
1067 int msglen = atoi(szMessageBlock->substr(pos, valueEnd - pos).c_str());
1068 msgStart = valueEnd + 4;
1069 if (szMessageBlock->compare(msgStart, 8, "<message") != 0)
1070 {
1071 // message block incorrectly positioned
1072 #ifdef _DEBUG
1073 std::cerr << "warning: message block incorrectly positioned\n";
1074 #endif
1075 msgStart = szMessageBlock->find("<message", pos);
1076 if ((msgStart == std::string::npos) || ((msgStart - pos) > 8))
1077 {
1078 // unable to find the actual message block
1079 #ifdef _DEBUG
1080 std::cerr << "error: unrecognized message format\n";
1081 #endif
1082 return;
1083 }
1084 }
1085 std::string szMessage = szMessageBlock->substr(msgStart, msglen);
1086
1087 // <event xmlns="connect.logitech.com" type="connect.stateDigest?notify">
1088 if (szMessage.find("stateDigest?notify", pos) != std::string::npos)
1089 {
1090 // Event state notification
1091 char cActivityStatus = 0;
1092
1093 size_t jpos = szMessage.find("activityStatus");
1094 if (jpos != std::string::npos)
1095 {
1096 cActivityStatus = szMessage[jpos+16];
1097 bool bIsChanging = ((cActivityStatus == '1') || (cActivityStatus == '3'));
1098 if (bIsChanging != m_bIsChangingActivity)
1099 {
1100 m_bIsChangingActivity = bIsChanging;
1101 if (m_bIsChangingActivity)
1102 _log.Log(LOG_STATUS, "Harmony Hub: Changing activity");
1103 else
1104 _log.Log(LOG_STATUS, "Harmony Hub: Finished changing activity");
1105 }
1106 }
1107
1108 if (_log.IsDebugLevelEnabled(DEBUG_HARDWARE) || (m_szHubSwVersion.empty()))
1109 {
1110 jpos = szMessage.find("hubSwVersion");
1111 if (jpos != std::string::npos)
1112 {
1113 m_szHubSwVersion = szMessage.substr(jpos+15, 16); // limit string length for end delimiter search
1114 jpos = m_szHubSwVersion.find("\"");
1115 if (jpos != std::string::npos)
1116 {
1117 if (m_szHubSwVersion.empty())
1118 _log.Log(LOG_STATUS, "Harmony Hub: Software version: %s", m_szHubSwVersion.c_str());
1119 m_szHubSwVersion = m_szHubSwVersion.substr(0, jpos);
1120 }
1121 }
1122 }
1123
1124 if (_log.IsDebugLevelEnabled(DEBUG_HARDWARE))
1125 {
1126 std::string activityId, stateVersion;
1127 jpos = szMessage.find("runningActivityList");
1128 if (jpos != std::string::npos)
1129 {
1130 activityId = szMessage.substr(jpos+22, 16); // limit string length for end delimiter search
1131 jpos = activityId.find("\"");
1132 if (jpos != std::string::npos)
1133 activityId = activityId.substr(0, jpos);
1134 }
1135 if (jpos == std::string::npos)
1136 activityId = "NaN";
1137 else if (activityId.empty())
1138 activityId = "-1";
1139
1140 jpos = szMessage.find("stateVersion");
1141 if (jpos != std::string::npos)
1142 {
1143 stateVersion = szMessage.substr(jpos+14, 16); // limit string length for end delimiter search
1144 jpos = stateVersion.find(",");
1145 if (jpos != std::string::npos)
1146 stateVersion = stateVersion.substr(0, jpos);
1147 }
1148 if ((jpos == std::string::npos) || stateVersion.empty())
1149 stateVersion = "NaN";
1150
1151 _log.Debug(DEBUG_HARDWARE, "Harmony Hub: Event state notification: stateVersion = %s, hubSwVersion = %s, activityStatus = %c, activityId = %s", stateVersion.c_str(), m_szHubSwVersion.c_str(), cActivityStatus, activityId.c_str() );
1152 }
1153 }
1154
1155 // <event xmlns="connect.logitech.com" type="harmony.engine?startActivityFinished">
1156 // <event xmlns="connect.logitech.com" type="harmony.engine?helpdiscretes">
1157 else if ((szMessage.find("engine?startActivityFinished", pos) != std::string::npos) ||
1158 (szMessage.find("engine?helpdiscretes", pos) != std::string::npos))
1159 {
1160 // start activity finished
1161 size_t jpos = szMessage.find("activityId");
1162 std::string szActivityId;
1163 if (jpos != std::string::npos)
1164 {
1165 szActivityId = szMessage.substr(jpos+11, 16); // limit string length for end delimiter search
1166 jpos = szActivityId.find(":");
1167 if (jpos == std::string::npos)
1168 jpos = szActivityId.find("]");
1169 if (jpos != std::string::npos)
1170 szActivityId = szActivityId.substr(0, jpos);
1171 }
1172
1173 if (jpos == std::string::npos)
1174 return;
1175
1176 if (szActivityId != m_szCurActivityID)
1177 {
1178 CheckSetActivity(m_szCurActivityID, false);
1179 m_szCurActivityID = szActivityId;
1180 CheckSetActivity(m_szCurActivityID, true);
1181 }
1182 }
1183
1184 }
1185
1186
1187 /************************************************************************
1188 * *
1189 * XMPP communication block parser *
1190 * - splits the communication block we received into its individual *
1191 * components and sends it to the correct handler *
1192 * *
1193 ************************************************************************/
ParseHarmonyTransmission(std::string * szHarmonyData)1194 void CHarmonyHub::ParseHarmonyTransmission(std::string *szHarmonyData)
1195 {
1196 m_bNeedMoreData = false;
1197 size_t pos = 0;
1198 while ((pos != std::string::npos) && (pos < szHarmonyData->length()))
1199 {
1200 if (szHarmonyData->compare(pos, 5, "<iq/>") == 0)
1201 {
1202 #ifdef _DEBUG
1203 std::cerr << "received ACK\n";
1204 #endif
1205 pos += 5;
1206 }
1207
1208 else if (szHarmonyData->compare(pos, 3, "<iq") == 0)
1209 {
1210 size_t iqStart = pos;
1211 pos = szHarmonyData->find("</iq>", iqStart);
1212 pos += 5;
1213 std::string szQueryResponse = szHarmonyData->substr(iqStart, pos - iqStart);
1214 #ifdef _DEBUG
1215 std::cerr << szQueryResponse << '\n';
1216 #endif
1217 ProcessQueryResponse(&szQueryResponse);
1218 }
1219
1220 else if (szHarmonyData->compare(pos, 8, "<message") == 0)
1221 {
1222 size_t msgStart = pos;
1223 pos = szHarmonyData->find("</message>", msgStart);
1224 pos += 10;
1225 std::string szMessage = szHarmonyData->substr(msgStart, pos - msgStart);
1226 #ifdef _DEBUG
1227 std::cerr << szMessage << '\n';
1228 #endif
1229 ProcessHarmonyMessage(&szMessage);
1230 m_bReceivedMessage = true; // need this to fast forward our ping interval counter
1231 }
1232
1233 else if (szHarmonyData->compare(pos, 8, "</stream") == 0)
1234 {
1235 // Hub is closing the connection
1236 #ifdef _DEBUG
1237 std::cerr << "Hub closed connection\n";
1238 #endif
1239 ResetCommunicationSocket();
1240 pos = szHarmonyData->length(); // this is always the last transmission
1241 }
1242 }
1243 }
1244
1245
1246
1247