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