1 #include "stdafx.h"
2 #include "Kodi.h"
3 #include "../hardware/hardwaretypes.h"
4 #include "../main/json_helper.h"
5 #include "../main/EventSystem.h"
6 #include "../main/Helper.h"
7 #include "../main/HTMLSanitizer.h"
8 #include "../main/Logger.h"
9 #include "../main/mainworker.h"
10 #include "../main/SQLHelper.h"
11 #include "../main/WebServer.h"
12 #include "../notifications/NotificationHelper.h"
13 
14 #define MAX_TITLE_LEN 40
15 
16 //Giz: To Author, please try to rebuild this class using ASyncTCP
17 //Giz: Are those 'new'/thread lines not a memory leak ? Maybe use a shared_ptr for them ?
18 
Clear()19 void CKodiNode::CKodiStatus::Clear()
20 {
21 	m_nStatus = MSTAT_UNKNOWN;
22 	m_sStatus = "";
23 	m_iPlayerID = -1;
24 	m_sType = "";
25 	m_sTitle = "";
26 	m_sArtist = "";
27 	m_sChannel = "";
28 	m_iSeason = 0;
29 	m_iEpisode = 0;
30 	m_sLabel = "";
31 	m_sPercent = "";
32 	m_sYear = "";
33 	m_sLive = "";
34 	m_tLastOK = mytime(NULL);
35 }
36 
LogMessage()37 std::string	CKodiNode::CKodiStatus::LogMessage()
38 {
39 	std::string	sLogText = "";
40 	if (m_sType != "")
41 	{
42 		if (m_sType == "episode")
43 		{
44 			if (m_sShowTitle.length()) sLogText = m_sShowTitle;
45 			if (m_iSeason) sLogText += " [S" + std::to_string(m_iSeason) + "E" + std::to_string(m_iEpisode) + "]";
46 			if (m_sTitle.length()) sLogText += ", " + m_sTitle;
47 			if ((m_sLabel != m_sTitle) && (m_sLabel.length())) sLogText += ", " + m_sLabel;
48 		}
49 		else if (m_sType == "song")
50 		{
51 			if (m_sArtist.length()) sLogText = m_sArtist;
52 			if (m_sAlbum.length()) sLogText += " (" + m_sAlbum + ")";
53 			if (m_sTitle.length()) sLogText += ", " + m_sTitle;
54 			if ((m_sLabel != m_sTitle) && (m_sLabel.length())) sLogText += ", " + m_sLabel;
55 		}
56 		else if (m_sType == "channel")
57 		{
58 			if (m_sChannel.length()) sLogText = m_sChannel;
59 			if (m_sLive.length()) sLogText += " " + m_sLive;
60 			if (m_sTitle.length()) sLogText += ", " + m_sTitle;
61 			if ((m_sLabel != m_sTitle) && (m_sLabel != m_sChannel) && (m_sLabel.length())) sLogText += ", " + m_sLabel;
62 		}
63 		else if (m_sType == "movie")
64 		{
65 			if (m_sTitle.length()) sLogText = m_sTitle;
66 			if ((m_sLabel != m_sTitle) && (m_sLabel.length())) sLogText += ", " + m_sLabel;
67 		}
68 		else if (m_sType == "picture")
69 		{
70 			if (m_sLabel.length()) sLogText = m_sLabel;
71 		}
72 		else
73 		{
74 			if (m_sTitle.length()) sLogText = m_sTitle;
75 			if ((m_sLabel != m_sTitle) && (m_sLabel.length())) {
76 				if (sLogText.length()) sLogText += ", ";
77 				sLogText += m_sLabel;
78 			}
79 		}
80 		if (m_sYear.length()) sLogText += " " + m_sYear;
81 	}
82 
83 	return sLogText;
84 }
85 
StatusMessage()86 std::string	CKodiNode::CKodiStatus::StatusMessage()
87 {
88 	// if title is too long shorten it by removing things in brackets, followed by things after a ", "
89 	std::string	sStatus = LogMessage();
90 	if (sStatus.length() > MAX_TITLE_LEN)
91 	{
92 		boost::algorithm::replace_all(sStatus, " - ", ", ");
93 		boost::algorithm::replace_first(sStatus, ", ", " - ");  // Leave 1st hyphen (after state)
94 	}
95 	while (sStatus.length() > MAX_TITLE_LEN)
96 	{
97 		size_t begin = sStatus.find_first_of("(",0);
98 		size_t end = sStatus.find_first_of(")", begin);
99 		if ((std::string::npos == begin) || (std::string::npos == end) || (begin >= end)) break;
100 		sStatus.erase(begin, end - begin + 1);
101 	}
102 	while (sStatus.length() > MAX_TITLE_LEN)
103 	{
104 		size_t end = sStatus.find_last_of(",");
105 		if (std::string::npos == end) break;
106 		sStatus = sStatus.substr(0, end);
107 	}
108 	boost::algorithm::trim(sStatus);
109 	stdreplace(sStatus, " ,", ",");
110 	sStatus = sStatus.substr(0, MAX_TITLE_LEN);
111 
112 	if (m_sPercent.length()) sStatus += ", " + m_sPercent;
113 
114 	return sStatus;
115 }
116 
LogRequired(CKodiStatus & pPrevious)117 bool CKodiNode::CKodiStatus::LogRequired(CKodiStatus& pPrevious)
118 {
119 	return ((LogMessage() != pPrevious.LogMessage()) || (m_nStatus != pPrevious.Status()));
120 }
121 
UpdateRequired(CKodiStatus & pPrevious)122 bool CKodiNode::CKodiStatus::UpdateRequired(CKodiStatus& pPrevious)
123 {
124 	return ((StatusMessage() != pPrevious.StatusMessage()) || (m_nStatus != pPrevious.Status()));
125 }
126 
OnOffRequired(CKodiStatus & pPrevious)127 bool CKodiNode::CKodiStatus::OnOffRequired(CKodiStatus& pPrevious)
128 {
129 	return ((m_nStatus == MSTAT_OFF) || (pPrevious.Status() == MSTAT_OFF)) && (m_nStatus != pPrevious.Status());
130 }
131 
NotificationType()132 _eNotificationTypes	CKodiNode::CKodiStatus::NotificationType()
133 {
134 	switch (m_nStatus)
135 	{
136 	case MSTAT_OFF:		return NTYPE_SWITCH_OFF;
137 	case MSTAT_ON:		return NTYPE_SWITCH_ON;
138 	case MSTAT_PAUSED:	return NTYPE_PAUSED;
139 	case MSTAT_VIDEO:	return NTYPE_VIDEO;
140 	case MSTAT_AUDIO:	return NTYPE_AUDIO;
141 	case MSTAT_PHOTO:	return NTYPE_PHOTO;
142 	default:			return NTYPE_SWITCH_OFF;
143 	}
144 }
145 
CKodiNode(boost::asio::io_service * pIos,const int pHwdID,const int PollIntervalsec,const int pTimeoutMs,const std::string & pID,const std::string & pName,const std::string & pIP,const std::string & pPort)146 CKodiNode::CKodiNode(boost::asio::io_service *pIos, const int pHwdID, const int PollIntervalsec, const int pTimeoutMs,
147 	const std::string& pID, const std::string& pName, const std::string& pIP, const std::string& pPort)
148 {
149 	m_Busy = false;
150 	m_Stoppable = false;
151 	m_PlaylistPosition = 0;
152 
153 	m_Ios = pIos;
154 	m_HwdID = pHwdID;
155 	m_DevID = atoi(pID.c_str());
156 	sprintf(m_szDevID, "%X%02X%02X%02X", 0, 0, (m_DevID & 0xFF00) >> 8, m_DevID & 0xFF);
157 	m_Name = pName;
158 	m_IP = pIP;
159 	m_Port = pPort;
160 	m_iTimeoutCnt = (pTimeoutMs > 999) ? pTimeoutMs / 1000 : pTimeoutMs;
161 	m_iPollIntSec = PollIntervalsec;
162 	m_iMissedPongs = 0;
163 
164 	m_Socket = NULL;
165 
166 	_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Created.", m_Name.c_str());
167 
168 	std::vector<std::vector<std::string> > result2;
169 	result2 = m_sql.safe_query("SELECT ID,nValue,sValue FROM DeviceStatus WHERE (HardwareID==%d) AND (DeviceID=='%q') AND (Unit == 1)", m_HwdID, m_szDevID);
170 	if (result2.size() == 1)
171 	{
172 		m_ID = atoi(result2[0][0].c_str());
173 		m_PreviousStatus.Status((_eMediaStatus)atoi(result2[0][1].c_str()));
174 		m_PreviousStatus.Status(result2[0][2]);
175 	}
176 	m_CurrentStatus = m_PreviousStatus;
177 }
178 
~CKodiNode(void)179 CKodiNode::~CKodiNode(void)
180 {
181 	handleDisconnect();
182 	_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Destroyed.", m_Name.c_str());
183 }
184 
handleMessage(std::string & pMessage)185 void CKodiNode::handleMessage(std::string& pMessage)
186 {
187 	try
188 	{
189 		Json::Value root;
190 		std::string	sMessage;
191 		std::stringstream ssMessage;
192 
193 		_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Handling message: '%s'.", m_Name.c_str(), pMessage.c_str());
194 		bool bRet = ParseJSon(pMessage, root);
195 		if ((!bRet) || (!root.isObject()))
196 		{
197 			_log.Log(LOG_ERROR, "Kodi: (%s) PARSE ERROR: '%s'", m_Name.c_str(), pMessage.c_str());
198 		}
199 		else if (root["error"].empty() != true)
200 		{
201 			int		iMessageID = root["id"].asInt();
202 			switch (iMessageID)
203 			{
204 			case 2002: // attempt to start music playlist (error is because playlist does not exist, try video)
205 				m_PlaylistType = "1";
206 				ssMessage << "{\"jsonrpc\":\"2.0\",\"method\":\"Playlist.Add\",\"params\":{\"playlistid\":" << m_PlaylistType << ",\"item\":{\"directory\": \"special://profile/playlists/video/" << m_Playlist << ".xsp\", \"media\":\"video\"}},\"id\":2003}";
207 				handleWrite(ssMessage.str());
208 				break;
209 			case 2003: // error because video playlist does not exist, stop.
210 				_log.Log(LOG_ERROR, "Kodi: (%s) Playlist '%s' could not be added, probably does not exist.", m_Name.c_str(), m_Playlist.c_str());
211 				break;
212 			default:
213 				/* e.g {"error":{"code":-32100,"message":"Failed to execute method."},"id":1001,"jsonrpc":"2.0"}	*/
214 				_log.Log(LOG_ERROR, "Kodi: (%s) Code %d Text '%s' ID '%d' Request '%s'", m_Name.c_str(), root["error"]["code"].asInt(), root["error"]["message"].asCString(), root["id"].asInt(), m_sLastMessage.c_str());
215 			}
216 		}
217 		else
218 		{
219 			// Kodi generated messages
220 			if (root.isMember("params"))
221 			{
222 				if (root["params"].isMember("sender"))
223 				{
224 					if (root["params"]["sender"] != "xmbc")
225 					{
226 						if (root.isMember("method"))
227 						{
228 							if ((root["method"] == "Player.OnStop") || (root["method"] == "System.OnWake"))
229 							{
230 								m_CurrentStatus.Clear();
231 								m_CurrentStatus.Status(MSTAT_ON);
232 								UpdateStatus();
233 							}
234 							else if ((root["method"] == "Player.OnPlay") || (root["method"] == "Player.OnResume"))
235 							{
236 								m_CurrentStatus.Clear();
237 								m_CurrentStatus.PlayerID(root["params"]["data"]["player"]["playerid"].asInt());
238 								if (root["params"]["data"]["item"]["type"] == "picture")
239 									m_CurrentStatus.Status(MSTAT_PHOTO);
240 								else if (root["params"]["data"]["item"]["type"] == "episode")
241 									m_CurrentStatus.Status(MSTAT_VIDEO);
242 								else if (root["params"]["data"]["item"]["type"] == "channel")
243 									m_CurrentStatus.Status(MSTAT_VIDEO);
244 								else if (root["params"]["data"]["item"]["type"] == "movie")
245 									m_CurrentStatus.Status(MSTAT_VIDEO);
246 								else if (root["params"]["data"]["item"]["type"] == "song")
247 									m_CurrentStatus.Status(MSTAT_AUDIO);
248 								else if (root["params"]["data"]["item"]["type"] == "musicvideo")
249 									m_CurrentStatus.Status(MSTAT_VIDEO);
250 								else
251 								{
252 									_log.Log(LOG_ERROR, "Kodi: (%s) Message error, unknown type in OnPlay/OnResume message: '%s' from '%s'", m_Name.c_str(), root["params"]["data"]["item"]["type"].asCString(), pMessage.c_str());
253 								}
254 
255 								if (m_CurrentStatus.PlayerID() != "")  // if we now have a player id then request more details
256 								{
257 									sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetItem\",\"id\":1003,\"params\":{\"playerid\":" + m_CurrentStatus.PlayerID() + ",\"properties\":[\"artist\",\"album\",\"year\",\"channel\",\"showtitle\",\"season\",\"episode\",\"title\"]}}";
258 									handleWrite(sMessage);
259 								}
260 							}
261 							else if (root["method"] == "Player.OnPause")
262 							{
263 								m_CurrentStatus.Status(MSTAT_PAUSED);
264 								UpdateStatus();
265 							}
266 							else if (root["method"] == "Player.OnSeek")
267 							{
268 								if (m_CurrentStatus.PlayerID() != "")
269 									sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetProperties\",\"id\":1002,\"params\":{\"playerid\":" + m_CurrentStatus.PlayerID() + ",\"properties\":[\"live\",\"percentage\",\"speed\"]}}";
270 								else
271 									sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetActivePlayers\",\"id\":1005}";
272 								handleWrite(sMessage);
273 							}
274 							else if ((root["method"] == "System.OnQuit") || (root["method"] == "System.OnSleep") || (root["method"] == "System.OnRestart"))
275 							{
276 								m_CurrentStatus.Clear();
277 								m_CurrentStatus.Status(MSTAT_OFF);
278 								UpdateStatus();
279 							}
280 							else if (root["method"] == "Application.OnVolumeChanged")
281 							{
282 								float		iVolume = root["params"]["data"]["volume"].asFloat();
283 								bool		bMuted = root["params"]["data"]["muted"].asBool();
284 								_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Volume changed to %3.5f, Muted: %s.", m_Name.c_str(), iVolume, bMuted?"true":"false");
285 							}
286 							else if (root["method"] == "Player.OnSpeedChanged")
287 							{
288 								_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Speed changed.", m_Name.c_str());
289 							}
290 							else
291 								_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Message warning, unhandled method: '%s'", m_Name.c_str(), root["method"].asCString());
292 						}
293 						else
294 							_log.Log(LOG_ERROR, "Kodi: (%s) Message error, params but no method: '%s'", m_Name.c_str(), pMessage.c_str());
295 					}
296 					else
297 						_log.Log(LOG_ERROR, "Kodi: (%s) Message error, invalid sender: '%s'", m_Name.c_str(), root["params"]["sender"].asCString());
298 				}
299 				else
300 					_log.Log(LOG_ERROR, "Kodi: (%s) Message error, params but no sender: '%s'", m_Name.c_str(), pMessage.c_str());
301 			}
302 			else  // responses to queries
303 			{
304 				if ((root.isMember("result") && (root.isMember("id"))))
305 				{
306 					bool		bCanShutdown = false;
307 					bool		bCanHibernate = false;
308 					bool		bCanSuspend = false;
309 					int		iMessageID = root["id"].asInt();
310 					switch (iMessageID)
311 					{
312 					case 1001:		//Ping response
313 						if (root["result"] == "pong")
314 						{
315 							m_iMissedPongs = 0;
316 							m_CurrentStatus.Clear();
317 							m_CurrentStatus.Status(MSTAT_ON);
318 							UpdateStatus();
319 						}
320 						break;
321 					case 1002:		//Poll response
322 						if (root["result"].isMember("live"))
323 						{
324 							m_CurrentStatus.Live(root["result"]["live"].asBool());
325 						}
326 						if (root["result"].isMember("percentage"))
327 						{
328 							m_CurrentStatus.Percent(root["result"]["percentage"].asFloat());
329 						}
330 						if (root["result"].isMember("speed"))
331 						{
332 							if (!root["result"]["speed"].asInt())
333 								m_CurrentStatus.Status(MSTAT_PAUSED);	// useful when Domoticz restarts when media aleady paused
334 							if (root["result"]["speed"].asInt() && m_CurrentStatus.Status() == MSTAT_PAUSED)
335 							{
336 								// Buffering when playing internet streams show 0 speed but don't trigger OnPause/OnPlay so force a refresh when speed is not 0 again
337 								sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetItem\",\"id\":1003,\"params\":{\"playerid\":" + m_CurrentStatus.PlayerID() + ",\"properties\":[\"artist\",\"album\",\"year\",\"channel\",\"showtitle\",\"season\",\"episode\",\"title\"]}}";
338 								handleWrite(sMessage);
339 							}
340 						}
341 						UpdateStatus();
342 						break;
343 					case 1003:		//OnPlay media details response
344 						if (root["result"].isMember("item"))
345 						{
346 							if (root["result"]["item"].isMember("type"))			m_CurrentStatus.Type(root["result"]["item"]["type"].asCString());
347 							if (m_CurrentStatus.Type() == "song")
348 							{
349 								m_CurrentStatus.Status(MSTAT_AUDIO);
350 								if (root["result"]["item"]["artist"][0].empty() != true)
351 								{
352 									m_CurrentStatus.Artist(root["result"]["item"]["artist"][0].asCString());
353 								}
354 								if (root["result"]["item"].isMember("album"))
355 								{
356 									m_CurrentStatus.Album(root["result"]["item"]["album"].asCString());
357 								}
358 							}
359 							if (m_CurrentStatus.Type() == "episode")
360 							{
361 								m_CurrentStatus.Status(MSTAT_VIDEO);
362 								if (root["result"]["item"].isMember("showtitle"))	m_CurrentStatus.ShowTitle(root["result"]["item"]["showtitle"].asCString());
363 								if (root["result"]["item"].isMember("season"))		m_CurrentStatus.Season((int)root["result"]["item"]["season"].asInt());
364 								if (root["result"]["item"].isMember("episode"))		m_CurrentStatus.Episode((int)root["result"]["item"]["episode"].asInt());
365 							}
366 							if (m_CurrentStatus.Type() == "channel")
367 							{
368 								m_CurrentStatus.Status(MSTAT_VIDEO);
369 								if (root["result"]["item"].isMember("channel"))		m_CurrentStatus.Channel(root["result"]["item"]["channel"].asCString());
370 							}
371 							if (m_CurrentStatus.Type() == "unknown")
372 							{
373 								m_CurrentStatus.Status(MSTAT_VIDEO);
374 							}
375 							if (m_CurrentStatus.Type() == "picture")
376 							{
377 								m_CurrentStatus.Status(MSTAT_PHOTO);
378 							}
379 							if (root["result"]["item"].isMember("title"))			m_CurrentStatus.Title(root["result"]["item"]["title"].asCString());
380 							if (root["result"]["item"].isMember("year"))			m_CurrentStatus.Year(root["result"]["item"]["year"].asInt());
381 							if (root["result"]["item"].isMember("label"))			m_CurrentStatus.Label(root["result"]["item"]["label"].asCString());
382 							if ((m_CurrentStatus.PlayerID() != "") && (m_CurrentStatus.Type() != "picture")) // request final details
383 							{
384 								sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetProperties\",\"id\":1002,\"params\":{\"playerid\":" + m_CurrentStatus.PlayerID() + ",\"properties\":[\"live\",\"percentage\",\"speed\"]}}";
385 								handleWrite(sMessage);
386 							}
387 							UpdateStatus();
388 						}
389 						break;
390 					case 1004:		//Shutdown details response
391 						{
392 							m_Stoppable = false;
393 							std::string	sAction = "Nothing";
394 							if (root["result"].isMember("canshutdown"))
395 							{
396 								bCanShutdown = root["result"]["canshutdown"].asBool();
397 								if (bCanShutdown) sAction = "Shutdown";
398 							}
399 							if (root["result"].isMember("canhibernate") && sAction != "Nothing")
400 							{
401 								bCanHibernate = root["result"]["canhibernate"].asBool();
402 								if (bCanHibernate) sAction = "Hibernate";
403 							}
404 							if (root["result"].isMember("cansuspend")&& sAction != "Nothing")
405 							{
406 								bCanSuspend = root["result"]["cansuspend"].asBool();
407 								if (bCanSuspend) sAction = "Suspend";
408 							}
409 							_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Switch Off: CanShutdown:%s, CanHibernate:%s, CanSuspend:%s. %s requested.", m_Name.c_str(),
410 								bCanShutdown ? "true" : "false", bCanHibernate ? "true" : "false", bCanSuspend ? "true" : "false", sAction.c_str());
411 
412 							if (sAction != "Nothing")
413 							{
414 								m_Stoppable = true;
415 								sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"System." + sAction + "\",\"id\":1008}";
416 								handleWrite(sMessage);
417 							}
418 						}
419 						break;
420 					case 1005:		//GetPlayers response, normally requried when Domoticz starts up and media is already streaming
421 						if (root["result"][0].isMember("playerid"))
422 						{
423 							m_CurrentStatus.PlayerID(root["result"][0]["playerid"].asInt());
424 							sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetItem\",\"id\":1003,\"params\":{\"playerid\":" + m_CurrentStatus.PlayerID() + ",\"properties\":[\"artist\",\"album\",\"year\",\"channel\",\"showtitle\",\"season\",\"episode\",\"title\"]}}";
425 							handleWrite(sMessage);
426 						}
427 						break;
428 					case 1006:		//Remote Control response
429 						if (root["result"] != "OK")
430 							_log.Log(LOG_ERROR, "Kodi: (%s) Send Command Failed: '%s'", m_Name.c_str(), root["result"].asCString());
431 						break;
432 					case 1007:		//Can Shutdown response (after connect)
433 						handleWrite(std::string("{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetActivePlayers\",\"id\":1005}"));
434 						if (root["result"].isMember("canshutdown"))
435 						{
436 							bCanShutdown = root["result"]["canshutdown"].asBool();
437 						}
438 						if (root["result"].isMember("canhibernate"))
439 						{
440 							bCanHibernate = root["result"]["canhibernate"].asBool();
441 						}
442 						if (root["result"].isMember("cansuspend"))
443 						{
444 							bCanSuspend = root["result"]["cansuspend"].asBool();
445 						}
446 						m_Stoppable = (bCanShutdown || bCanHibernate || bCanSuspend);
447 						break;
448 					case 1008:		//Shutdown response
449 						if (root["result"] == "OK")
450 							_log.Log(LOG_NORM, "Kodi: (%s) Shutdown command accepted.", m_Name.c_str());
451 						break;
452 					case 1009:		//SetVolume response
453 						_log.Log(LOG_NORM, "Kodi: (%s) Volume set to %d.", m_Name.c_str(), root["result"].asInt());
454 						break;
455 					case 1010:		//Execute Addon response
456 						_log.Log(LOG_NORM, "Kodi: (%s) Executed Addon %s.", m_Name.c_str(), root["result"].asString().c_str());
457 						break;
458 					// 2000+ messages relate to playlist triggering functionality
459 					case 2000: // clear video playlist response
460 						handleWrite("{\"jsonrpc\":\"2.0\",\"method\":\"Playlist.Clear\",\"params\":{\"playlistid\":1},\"id\":2001}");
461 						break;
462 					case 2001: // clear music playlist response
463 						ssMessage << "{\"jsonrpc\":\"2.0\",\"method\":\"Playlist.Add\",\"params\":{\"playlistid\":" << m_PlaylistType << ",\"item\":{\"directory\": \"special://profile/playlists/music/" << m_Playlist << ".xsp\", \"media\":\"music\"}},\"id\":2002}";
464 						handleWrite(ssMessage.str());
465 						break;
466 					case 2002: // attempt to add playlist response
467 					case 2003:
468 						ssMessage << "{\"jsonrpc\":\"2.0\",\"method\":\"Player.Open\",\"params\":{\"item\":{\"playlistid\":" << m_PlaylistType << ",\"position\":" << m_PlaylistPosition << "}},\"id\":2004}";
469 						handleWrite(ssMessage.str());
470 						break;
471 					case 2004: // signal outcome
472 						if (root["result"] == "OK")
473 							_log.Log(LOG_NORM, "Kodi: (%s) Playlist command '%s' at position %d accepted.", m_Name.c_str(), m_Playlist.c_str(), m_PlaylistPosition);
474 						break;
475 					// 2100+ messages relate to favorites triggering functionality
476 					case 2100: // return favorites response
477 						if (root["result"].isMember("favourites")) {
478 							if (root["result"]["limits"].isMember("total"))
479 							{
480 								int	iFavCount = root["result"]["limits"]["total"].asInt();
481 								// set play position to last entry if greater than total
482 								if (m_PlaylistPosition < 0) m_PlaylistPosition = 0;
483 								if (m_PlaylistPosition >= iFavCount) m_PlaylistPosition = iFavCount - 1;
484 								if (iFavCount)
485 									for (int i = 0; i < iFavCount; i++) {
486 										_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Favourites %d is '%s', type '%s'.", m_Name.c_str(), i, root["result"]["favourites"][i]["title"].asCString(), root["result"]["favourites"][i]["type"].asCString());
487 										std::string sType = root["result"]["favourites"][i]["type"].asCString();
488 										if (i == m_PlaylistPosition) {
489 											if (sType == "media") {
490 												std::string sPath = root["result"]["favourites"][i]["path"].asCString();
491 												_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Favourites %d has path '%s' and will be played.", m_Name.c_str(), i, sPath.c_str());
492 												ssMessage << "{\"jsonrpc\":\"2.0\",\"method\":\"Player.Open\",\"params\":{\"item\":{\"file\":\"" << sPath << "\"}},\"id\":2101}";
493 												handleWrite(ssMessage.str());
494 												break;
495 											}
496 											else {
497 												_log.Log(LOG_NORM, "Kodi: (%s) Requested Favourite ('%s') is not playable, next playable item will be selected.", m_Name.c_str(), root["result"]["favourites"][i]["title"].asCString());
498 												m_PlaylistPosition++;
499 											}
500 										}
501 									}
502 								else
503 									_log.Log(LOG_NORM, "Kodi: (%s) No Favourites returned.", m_Name.c_str());
504 							}
505 						}
506 						break;
507 					case 2101: // signal outcome
508 						if (root["result"] == "OK")
509 							_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Favourite play request successful.", m_Name.c_str());
510 						break;
511 					default:
512 						_log.Log(LOG_ERROR, "Kodi: (%s) Message error, unknown ID found: '%s'", m_Name.c_str(), pMessage.c_str());
513 					}
514 				}
515 			}
516 		}
517 	}
518 	catch (std::exception& e)
519 	{
520 		_log.Log(LOG_ERROR, "Kodi: (%s) Exception: %s", m_Name.c_str(), e.what());
521 	}
522 }
523 
UpdateStatus()524 void CKodiNode::UpdateStatus()
525 {
526 	//This has to be rebuild! No direct poking in the database, please use CMainWorker::UpdateDevice
527 
528 	std::vector<std::vector<std::string> > result;
529 	m_CurrentStatus.LastOK(mytime(NULL));
530 
531 	// 1:	Update the DeviceStatus
532 	if (m_CurrentStatus.UpdateRequired(m_PreviousStatus))
533 	{
534 		result = m_sql.safe_query("UPDATE DeviceStatus SET nValue=%d, sValue='%q', LastUpdate='%q' WHERE (HardwareID == %d) AND (DeviceID == '%q') AND (Unit == 1) AND (SwitchType == %d)",
535 			int(m_CurrentStatus.Status()), m_CurrentStatus.StatusMessage().c_str(), m_CurrentStatus.LastOK().c_str(), m_HwdID, m_szDevID, STYPE_Media);
536 	}
537 
538 	// 2:	Log the event if the actual status has changed (not counting the percentage)
539 	std::string	sLogText = m_CurrentStatus.StatusText();
540 	if (m_CurrentStatus.LogRequired(m_PreviousStatus))
541 	{
542 		if (m_CurrentStatus.IsStreaming()) sLogText += " - " + m_CurrentStatus.LogMessage();
543 		result = m_sql.safe_query("INSERT INTO LightingLog (DeviceRowID, nValue, sValue, User) VALUES (%d, %d, '%q','%q')", m_ID, int(m_CurrentStatus.Status()), sLogText.c_str(), "Kodi");
544 		_log.Log(LOG_NORM, "Kodi: (%s) Event: '%s'.", m_Name.c_str(), sLogText.c_str());
545 	}
546 
547 	// 3:	Trigger On/Off actions
548 	if (m_CurrentStatus.OnOffRequired(m_PreviousStatus))
549 	{
550 		result = m_sql.safe_query("SELECT StrParam1,StrParam2 FROM DeviceStatus WHERE (HardwareID==%d) AND (ID = '%q') AND (Unit == 1)", m_HwdID, m_szDevID);
551 		if (!result.empty())
552 		{
553 			m_sql.HandleOnOffAction(m_CurrentStatus.IsOn(), result[0][0], result[0][1]);
554 		}
555 	}
556 
557 	// 4:	Trigger Notifications & events on status change
558 	if (m_CurrentStatus.Status() != m_PreviousStatus.Status())
559 	{
560 		m_notifications.CheckAndHandleNotification(m_ID, m_Name, m_CurrentStatus.NotificationType(), sLogText);
561 		m_mainworker.m_eventsystem.ProcessDevice(m_HwdID, m_ID, 1, int(pTypeLighting2), int(sTypeAC), 12, 100, int(m_CurrentStatus.Status()), m_CurrentStatus.StatusMessage().c_str(), m_Name.c_str());
562 	}
563 
564 	m_PreviousStatus = m_CurrentStatus;
565 }
566 
handleConnect()567 void CKodiNode::handleConnect()
568 {
569 	try
570 	{
571 		if (!IsStopRequested(0) && !m_Socket)
572 		{
573 			m_iMissedPongs = 0;
574 			boost::system::error_code ec;
575 			boost::asio::ip::tcp::resolver resolver(*m_Ios);
576 			boost::asio::ip::tcp::resolver::query query(m_IP, (m_Port[0] != '-' ? m_Port : m_Port.substr(1)));
577 			boost::asio::ip::tcp::resolver::iterator iter = resolver.resolve(query);
578 			boost::asio::ip::tcp::endpoint endpoint = *iter;
579 			m_Socket = new boost::asio::ip::tcp::socket(*m_Ios);
580 			m_Socket->connect(endpoint, ec);
581 			if (!ec)
582 			{
583 				_log.Log(LOG_NORM, "Kodi: (%s) Connected to '%s:%s'.", m_Name.c_str(), m_IP.c_str(), (m_Port[0] != '-' ? m_Port.c_str() : m_Port.substr(1).c_str()));
584 				if (m_CurrentStatus.Status() == MSTAT_OFF)
585 				{
586 					m_CurrentStatus.Clear();
587 					m_CurrentStatus.Status(MSTAT_ON);
588 					UpdateStatus();
589 				}
590 				m_Socket->async_read_some(boost::asio::buffer(m_Buffer, sizeof m_Buffer),
591 					boost::bind(&CKodiNode::handleRead, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
592 				handleWrite(std::string("{\"jsonrpc\":\"2.0\",\"method\":\"System.GetProperties\",\"params\":{\"properties\":[\"canhibernate\",\"cansuspend\",\"canshutdown\"]},\"id\":1007}"));
593 			}
594 			else
595 			{
596 				if (
597 					(ec.value() != 113) &&
598 					(ec.value() != 111) &&
599 					(ec.value() != 10060) &&
600 					(ec.value() != 10061) &&
601 					(ec.value() != 10064) //&&
602 					//(ec.value() != 10061)
603 					)
604 				{
605 					_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Connect to '%s:%s' failed: (%d) %s", m_Name.c_str(), m_IP.c_str(), (m_Port[0] != '-' ? m_Port.c_str() : m_Port.substr(1).c_str()), ec.value(), ec.message().c_str());
606 				}
607 				delete m_Socket;
608 				m_Socket = NULL;
609 				m_CurrentStatus.Clear();
610 				m_CurrentStatus.Status(MSTAT_OFF);
611 				UpdateStatus();
612 			}
613 		}
614 	}
615 	catch (std::exception& e)
616 	{
617 		_log.Log(LOG_ERROR, "Kodi: (%s) Exception: '%s' connecting to '%s'", m_Name.c_str(), e.what(), m_IP.c_str());
618 	}
619 }
620 
handleRead(const boost::system::error_code & e,std::size_t bytes_transferred)621 void CKodiNode::handleRead(const boost::system::error_code& e, std::size_t bytes_transferred)
622 {
623 	if (!e)
624 	{
625 		//do something with the data
626 		std::string sData(m_Buffer.begin(), bytes_transferred);
627 		sData = m_RetainedData + sData;  // if there was some data left over from last time add it back in
628 		int iPos = 1;
629 		while (iPos) {
630 			iPos = sData.find("}{", 0) + 1;		//  Look for message separater in case there is more than one
631 			if (!iPos) // no, just one or part of one
632 			{
633 				if ((sData.substr(sData.length()-1, 1) == "}") &&
634 					(std::count(sData.begin(), sData.end(), '{') == std::count(sData.begin(), sData.end(), '}'))) // whole message so process
635 				{
636 					handleMessage(sData);
637 					sData = "";
638 				}
639 			}
640 			else  // more than one message so process the first one
641 			{
642 				std::string sMessage = sData.substr(0, iPos);
643 				sData = sData.substr(iPos);
644 				handleMessage(sMessage);
645 			}
646 		}
647 		m_RetainedData = sData;
648 
649 		//ready for next read
650 		if (!IsStopRequested(0) && m_Socket)
651 			m_Socket->async_read_some(	boost::asio::buffer(m_Buffer, sizeof m_Buffer),
652 										boost::bind(&CKodiNode::handleRead,
653 										shared_from_this(),
654 										boost::asio::placeholders::error,
655 										boost::asio::placeholders::bytes_transferred));
656 	}
657 	else
658 	{
659 		if (e.value() != 1236)		// local disconnect cause by hardware reload
660 		{
661 			if ((e.value() != 2) && (e.value() != 121))	// Semaphore tmieout expiry or end of file aka 'lost contact'
662 				_log.Log(LOG_ERROR, "Kodi: (%s) Async Read Exception: %d, %s", m_Name.c_str(), e.value(), e.message().c_str());
663 			m_CurrentStatus.Clear();
664 			m_CurrentStatus.Status(MSTAT_OFF);
665 			UpdateStatus();
666 			handleDisconnect();
667 		}
668 	}
669 }
670 
handleWrite(std::string pMessage)671 void CKodiNode::handleWrite(std::string pMessage)
672 {
673 	if (!IsStopRequested(0)) {
674 		if (m_Socket)
675 		{
676 			_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Sending data: '%s'", m_Name.c_str(), pMessage.c_str());
677 			m_Socket->write_some(boost::asio::buffer(pMessage.c_str(), pMessage.length()));
678 			m_sLastMessage = pMessage;
679 		}
680 		else
681     {
682       _log.Log(LOG_ERROR, "Kodi: (%s) Data not sent to NULL socket: '%s'", m_Name.c_str(), pMessage.c_str());
683     }
684   }
685 }
686 
handleDisconnect()687 void CKodiNode::handleDisconnect()
688 {
689 	if (m_Socket)
690 	{
691 		_log.Log(LOG_NORM, "Kodi: (%s) Disconnected.", m_Name.c_str());
692 		boost::system::error_code	ec;
693 		m_Socket->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
694 		m_Socket->close();
695 		delete m_Socket;
696 		m_Socket = NULL;
697 	}
698 }
699 
Do_Work()700 void CKodiNode::Do_Work()
701 {
702 	m_Busy = true;
703 	_log.Debug(DEBUG_HARDWARE, "Kodi: (%s) Entering work loop.", m_Name.c_str());
704 	int	iPollCount = 2;
705 
706 	try
707 	{
708 		while (!IsStopRequested(1000))
709 		{
710 			if (!m_Socket)
711 			{
712 				handleConnect();
713 				iPollCount = 1;
714 			}
715 
716 			if (!iPollCount--)
717 			{
718 				iPollCount = m_iPollIntSec - 1;
719 				std::string	sMessage;
720 				if (m_CurrentStatus.IsStreaming())
721 				{	// Update percentage if playing media (required because Player.OnPropertyChanged never get received as of Kodi 'Helix')
722 					if (m_CurrentStatus.PlayerID() != "")
723 						sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetProperties\",\"id\":1002,\"params\":{\"playerid\":" + m_CurrentStatus.PlayerID() + ",\"properties\":[\"live\",\"percentage\",\"speed\"]}}";
724 					else
725 						sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Player.GetActivePlayers\",\"id\":1005}";
726 				}
727 				else
728 				{
729 					sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"JSONRPC.Ping\",\"id\":1001}";
730 					if (m_iMissedPongs++ > m_iTimeoutCnt)
731 					{
732 						_log.Log(LOG_NORM, "Kodi: (%s) Missed %d pings, assumed off.", m_Name.c_str(), m_iTimeoutCnt);
733 						boost::system::error_code	ec;
734 						m_Socket->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
735 						continue;
736 					};
737 				}
738 				handleWrite(sMessage);
739 			}
740 		}
741 	}
742 	catch (std::exception& e)
743 	{
744 		_log.Log(LOG_ERROR, "Kodi: (%s) Exception: %s", m_Name.c_str(), e.what());
745 	}
746 	_log.Log(LOG_NORM, "Kodi: (%s) Exiting work loop.", m_Name.c_str());
747 	m_Busy = false;
748 	delete m_Socket;
749 	m_Socket = NULL;
750 }
751 
SendCommand(const std::string & command)752 void CKodiNode::SendCommand(const std::string &command)
753 {
754 	std::string	sKodiCall;
755 	std::string	sKodiParam = "";
756 	if (command == "Home")
757 	{
758 		sKodiCall = "Input.Home";
759 	}
760 	else if (command == "Up")
761 	{
762 		sKodiCall = "Input.Up";
763 	}
764 	else if (command == "Down")
765 	{
766 		sKodiCall = "Input.Down";
767 	}
768 	else if (command == "Left")
769 	{
770 		sKodiCall = "Input.Left";
771 	}
772 	else if (command == "Right")
773 	{
774 		sKodiCall = "Input.Right";
775 	}
776 	else  // Assume generic ExecuteAction  for any unrecognised strings
777 	{
778 		sKodiCall = "Input.ExecuteAction";
779 		std::string	sLower = command;
780 		std::transform(sLower.begin(), sLower.end(), sLower.begin(), ::tolower);
781 		sKodiParam = sLower;
782 	}
783 
784 	if (sKodiCall.length())
785 	{
786 		//		http://kodi.wiki/view/JSON-RPC_API/v6#Input.Action
787 		//		{ "jsonrpc": "2.0", "method": "Input.ExecuteAction", "params": { "action": "stop" }, "id": 1006 }
788 		std::string	sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"" + sKodiCall + "\",\"params\":{";
789 		if (sKodiParam.length()) sMessage += "\"action\":\"" + sKodiParam + "\"";
790 		sMessage += "},\"id\":1006}";
791 
792 		if (m_Socket != NULL)
793 		{
794 			handleWrite(sMessage);
795 			_log.Log(LOG_NORM, "Kodi: (%s) Sent command: '%s %s'.", m_Name.c_str(), sKodiCall.c_str(), sKodiParam.c_str());
796 		}
797 		else
798 		{
799 			_log.Log(LOG_NORM, "Kodi: (%s) Command not sent, Kodi is not connected: '%s %s'.", m_Name.c_str(), sKodiCall.c_str(), sKodiParam.c_str());
800 		}
801 	}
802 	else
803 	{
804 		_log.Log(LOG_ERROR, "Kodi: (%s) Command: '%s'. Unknown command.", m_Name.c_str(), command.c_str());
805 	}
806 }
807 
SendCommand(const std::string & command,const int iValue)808 void CKodiNode::SendCommand(const std::string &command, const int iValue)
809 {
810 	std::stringstream ssMessage;
811 	std::string	sMessage;
812 	std::string	sKodiCall;
813 	if (command == "setvolume")
814 	{
815 		sKodiCall = "Set Volume";
816 		ssMessage << "{\"jsonrpc\":\"2.0\",\"method\":\"Application.SetVolume\",\"params\":{\"volume\":" << iValue << "},\"id\":1009}";
817 		sMessage = ssMessage.str();
818 	}
819 
820 	if (command == "playlist")
821 	{
822 		// clear any current playlists starting with audio, state machine in handleMessage will take care of the rest
823 		m_PlaylistPosition = iValue;
824 		sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Playlist.Clear\",\"params\":{\"playlistid\":0},\"id\":2000}";
825 	}
826 
827 	if (command == "favorites")
828 	{
829 		// Favorites are effectively a playlist but rewuire different handling to start items playing
830 		sKodiCall = "Favourites";
831 		m_PlaylistPosition = iValue;
832 		sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"Favourites.GetFavourites\",\"params\":{\"properties\":[\"path\"]},\"id\":2100}";
833 	}
834 
835 	if (command == "execute")
836 	{
837 		sKodiCall = "Execute Addon " + m_ExecuteCommand;
838 		//		ssMessage << "{\"jsonrpc\":\"2.0\",\"method\":\"Addons.GetAddons\",\"id\":1010}";
839 		ssMessage << "{\"jsonrpc\":\"2.0\",\"method\":\"Addons.ExecuteAddon\",\"params\":{\"addonid\":\"" << m_ExecuteCommand << "\"},\"id\":1010}";
840 		sMessage = ssMessage.str();
841 		m_ExecuteCommand = "";
842 	}
843 
844 	if (sMessage.length())
845 	{
846 		if (m_Socket != NULL)
847 		{
848 			handleWrite(sMessage);
849 			_log.Log(LOG_NORM, "Kodi: (%s) Sent command: '%s'.", m_Name.c_str(), sKodiCall.c_str());
850 		}
851 		else
852 		{
853 			_log.Log(LOG_NORM, "Kodi: (%s) Command not sent, Kodi is not connected: '%s'.", m_Name.c_str(), sKodiCall.c_str());
854 		}
855 	}
856 	else
857 	{
858 		_log.Log(LOG_ERROR, "Kodi: (%s) Command: '%s'. Unknown command.", m_Name.c_str(), command.c_str());
859 	}
860 }
861 
SendShutdown()862 bool CKodiNode::SendShutdown()
863 {
864 	std::string	sMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"System.GetProperties\",\"params\":{\"properties\":[\"canhibernate\",\"cansuspend\",\"canshutdown\"]},\"id\":1004}";
865 	handleWrite(sMessage);
866 
867 	if (m_Stoppable) _log.Log(LOG_NORM, "Kodi: (%s) Shutdown requested and is supported.", m_Name.c_str());
868 	else 			 _log.Log(LOG_NORM, "Kodi: (%s) Shutdown requested but is probably not supported.", m_Name.c_str());
869 	return m_Stoppable;
870 }
871 
SetPlaylist(const std::string & playlist)872 void CKodiNode::SetPlaylist(const std::string& playlist)
873 {
874 	m_Playlist = playlist;
875 	m_PlaylistType = "0";
876 	if (m_Playlist.length() > 0)
877 	{
878 		if (m_CurrentStatus.IsStreaming())  // Stop Kodi if it is currently stream media
879 		{
880 			SendCommand("stop");
881 		}
882 	}
883 }
884 
SetExecuteCommand(const std::string & command)885 void CKodiNode::SetExecuteCommand(const std::string& command)
886 {
887 	m_ExecuteCommand = command;
888 }
889 
890 std::vector<std::shared_ptr<CKodiNode> > CKodi::m_pNodes;
891 
CKodi(const int ID,const int PollIntervalsec,const int PingTimeoutms)892 CKodi::CKodi(const int ID, const int PollIntervalsec, const int PingTimeoutms)
893 {
894 	m_HwdID = ID;
895 	SetSettings(PollIntervalsec, PingTimeoutms);
896 }
897 
CKodi(const int ID)898 CKodi::CKodi(const int ID)
899 {
900 	m_HwdID = ID;
901 	SetSettings(10, 3000);
902 }
903 
~CKodi(void)904 CKodi::~CKodi(void)
905 {
906 	m_bIsStarted = false;
907 }
908 
StartHardware()909 bool CKodi::StartHardware()
910 {
911 	StopHardware();
912 
913 	RequestStart();
914 
915 	m_bIsStarted = true;
916 	sOnConnected(this);
917 
918 	StartHeartbeatThread();
919 
920 	//Start worker thread
921 	m_thread = std::make_shared<std::thread>(&CKodi::Do_Work, this);
922 	SetThreadNameInt(m_thread->native_handle());
923 	_log.Log(LOG_STATUS, "Kodi: Started");
924 
925 	return true;
926 }
927 
StopHardware()928 bool CKodi::StopHardware()
929 {
930 	StopHeartbeatThread();
931 
932 	try {
933 		//needs to be tested by the author if we can remove the try/catch here
934 		if (m_thread)
935 		{
936 			RequestStop();
937 			m_thread->join();
938 			m_thread.reset();
939 		}
940 	}
941 	catch (...)
942 	{
943 
944 	}
945 	m_bIsStarted = false;
946 	return true;
947 }
948 
Do_Work()949 void CKodi::Do_Work()
950 {
951 	int scounter = 0;
952 
953 	ReloadNodes();
954 
955 	while (!IsStopRequested(500))
956 	{
957 		if (scounter++ >= (m_iPollInterval*2))
958 		{
959 			std::lock_guard<std::mutex> l(m_mutex);
960 
961 			scounter = 0;
962 			bool bWorkToDo = false;
963 			std::vector<std::shared_ptr<CKodiNode> >::iterator itt;
964 			for (itt = m_pNodes.begin(); itt != m_pNodes.end(); ++itt)
965 			{
966 				if (!(*itt)->IsBusy())
967 				{
968 					_log.Log(LOG_NORM, "Kodi: (%s) - Restarting thread.", (*itt)->m_Name.c_str());
969 					boost::thread* tAsync = new boost::thread(&CKodiNode::Do_Work, (*itt));
970 					SetThreadName(tAsync->native_handle(), "KodiNode");
971 					m_ios.stop();
972 				}
973 				if ((*itt)->IsOn()) bWorkToDo = true;
974 			}
975 
976 			if (bWorkToDo && m_ios.stopped())  // make sure that there is a boost thread to service i/o operations
977 			{
978 				m_ios.reset();
979 				// Note that this is the only thread that handles async i/o so we don't
980 				// need to worry about locking or concurrency issues when processing messages
981 				_log.Log(LOG_NORM, "Kodi: Restarting I/O service thread.");
982 				boost::thread bt(boost::bind(&boost::asio::io_service::run, &m_ios));
983 				SetThreadName(bt.native_handle(), "KodiIO");
984 			}
985 		}
986 	}
987 	UnloadNodes();
988 
989 	_log.Log(LOG_STATUS, "Kodi: Worker stopped...");
990 }
991 
SetSettings(const int PollIntervalsec,const int PingTimeoutms)992 void CKodi::SetSettings(const int PollIntervalsec, const int PingTimeoutms)
993 {
994 	//Defaults
995 	m_iPollInterval = 30;
996 	m_iPingTimeoutms = 1000;
997 
998 	if (PollIntervalsec > 1)
999 		m_iPollInterval = PollIntervalsec;
1000 	if ((PingTimeoutms / 1000 < m_iPollInterval) && (PingTimeoutms != 0))
1001 		m_iPingTimeoutms = PingTimeoutms;
1002 }
1003 
WriteToHardware(const char * pdata,const unsigned char)1004 bool CKodi::WriteToHardware(const char *pdata, const unsigned char /*length*/)
1005 {
1006 	const tRBUF *pSen = reinterpret_cast<const tRBUF*>(pdata);
1007 
1008 	unsigned char packettype = pSen->ICMND.packettype;
1009 
1010 	if (packettype != pTypeLighting2)
1011 		return false;
1012 
1013 	long	DevID = (pSen->LIGHTING2.id3 << 8) | pSen->LIGHTING2.id4;
1014 
1015 	std::vector<std::shared_ptr<CKodiNode> >::iterator itt;
1016 	for (itt = m_pNodes.begin(); itt != m_pNodes.end(); ++itt)
1017 	{
1018 		if ((*itt)->m_DevID == DevID)
1019 		{
1020 			if ((*itt)->IsOn()) {
1021 				int iParam = pSen->LIGHTING2.level;
1022 				switch (pSen->LIGHTING2.cmnd)
1023 				{
1024 				case light2_sOff:
1025 				case light2_sGroupOff:
1026 					return (*itt)->SendShutdown();
1027 				case gswitch_sStop:
1028 					(*itt)->SendCommand("stop");
1029 					return true;
1030 				case gswitch_sPlay:
1031 					(*itt)->SendCommand("play");
1032 					return true;
1033 				case gswitch_sPause:
1034 					(*itt)->SendCommand("pause");
1035 					return true;
1036 				case gswitch_sSetVolume:
1037 					(*itt)->SendCommand("setvolume", iParam);
1038 					return true;
1039 				case gswitch_sPlayPlaylist:
1040 					(*itt)->SendCommand("playlist", iParam);
1041 					return true;
1042 				case gswitch_sPlayFavorites:
1043 					(*itt)->SendCommand("favorites", iParam);
1044 					return true;
1045 				case gswitch_sExecute:
1046 					(*itt)->SendCommand("execute", iParam);
1047 					return true;
1048 				default:
1049 					return true;
1050 				}
1051 			}
1052 			else
1053 				_log.Log(LOG_NORM, "Kodi: (%s) Command not sent, Device is 'Off'.", (*itt)->m_Name.c_str());
1054 		}
1055 	}
1056 
1057 	_log.Log(LOG_ERROR, "Kodi: (%ld) Shutdown. Device not found.", DevID);
1058 	return false;
1059 }
1060 
AddNode(const std::string & Name,const std::string & IPAddress,const int Port)1061 void CKodi::AddNode(const std::string &Name, const std::string &IPAddress, const int Port)
1062 {
1063 	std::vector<std::vector<std::string> > result;
1064 
1065 	//Check if exists
1066 	result = m_sql.safe_query("SELECT ID FROM WOLNodes WHERE (HardwareID==%d) AND (Name=='%q') AND (MacAddress=='%q')", m_HwdID, Name.c_str(), IPAddress.c_str());
1067 	if (result.size()>0)
1068 		return; //Already exists
1069 	m_sql.safe_query("INSERT INTO WOLNodes (HardwareID, Name, MacAddress, Timeout) VALUES (%d, '%q', '%q', %d)", m_HwdID, Name.c_str(), IPAddress.c_str(), Port);
1070 
1071 	result = m_sql.safe_query("SELECT ID FROM WOLNodes WHERE (HardwareID==%d) AND (Name=='%q') AND (MacAddress='%q')", m_HwdID, Name.c_str(), IPAddress.c_str());
1072 	if (result.size()<1)
1073 		return;
1074 
1075 	int ID = atoi(result[0][0].c_str());
1076 
1077 	char szID[40];
1078 	sprintf(szID, "%X%02X%02X%02X", 0, 0, (ID & 0xFF00) >> 8, ID & 0xFF);
1079 
1080 	//Also add a light (push) device
1081 	m_sql.InsertDevice(m_HwdID, szID, 1, pTypeLighting2, sTypeAC, STYPE_Media, 0, "Unavailable", Name, 12, 255, 1);
1082 
1083 	ReloadNodes();
1084 }
1085 
UpdateNode(const int ID,const std::string & Name,const std::string & IPAddress,const int Port)1086 bool CKodi::UpdateNode(const int ID, const std::string &Name, const std::string &IPAddress, const int Port)
1087 {
1088 	std::vector<std::vector<std::string> > result;
1089 
1090 	//Check if exists
1091 	result = m_sql.safe_query("SELECT ID FROM WOLNodes WHERE (HardwareID==%d) AND (ID==%d)", m_HwdID, ID);
1092 	if (result.size()<1)
1093 		return false; //Not Found!?
1094 
1095 	m_sql.safe_query("UPDATE WOLNodes SET Name='%q', MacAddress='%q', Timeout=%d WHERE (HardwareID==%d) AND (ID==%d)", Name.c_str(), IPAddress.c_str(), Port, m_HwdID, ID);
1096 
1097 	char szID[40];
1098 	sprintf(szID, "%X%02X%02X%02X", 0, 0, (ID & 0xFF00) >> 8, ID & 0xFF);
1099 
1100 	//Also update Light/Switch
1101 	m_sql.safe_query("UPDATE DeviceStatus SET Name='%q' WHERE (HardwareID==%d) AND (DeviceID=='%q')", Name.c_str(), m_HwdID, szID);
1102 	ReloadNodes();
1103 	return true;
1104 }
1105 
RemoveNode(const int ID)1106 void CKodi::RemoveNode(const int ID)
1107 {
1108 	m_sql.safe_query("DELETE FROM WOLNodes WHERE (HardwareID==%d) AND (ID==%d)", m_HwdID, ID);
1109 
1110 	//Also delete the switch
1111 	char szID[40];
1112 	sprintf(szID, "%X%02X%02X%02X", 0, 0, (ID & 0xFF00) >> 8, ID & 0xFF);
1113 
1114 	m_sql.safe_query("DELETE FROM DeviceStatus WHERE (HardwareID==%d) AND (DeviceID=='%q')", m_HwdID, szID);
1115 	ReloadNodes();
1116 }
1117 
RemoveAllNodes()1118 void CKodi::RemoveAllNodes()
1119 {
1120 	std::lock_guard<std::mutex> l(m_mutex);
1121 
1122 	m_sql.safe_query("DELETE FROM WOLNodes WHERE (HardwareID==%d)", m_HwdID);
1123 
1124 	//Also delete the all switches
1125 	m_sql.safe_query("DELETE FROM DeviceStatus WHERE (HardwareID==%d)", m_HwdID);
1126 	ReloadNodes();
1127 }
1128 
ReloadNodes()1129 void CKodi::ReloadNodes()
1130 {
1131 	UnloadNodes();
1132 
1133 	m_ios.reset();	// in case this is not the first time in
1134 
1135 	std::vector<std::vector<std::string> > result;
1136 	result = m_sql.safe_query("SELECT ID,Name,MacAddress,Timeout FROM WOLNodes WHERE (HardwareID==%d)", m_HwdID);
1137 	if (!result.empty())
1138 	{
1139 		std::lock_guard<std::mutex> l(m_mutex);
1140 
1141 		// create a vector to hold the nodes
1142 		for (std::vector<std::vector<std::string> >::const_iterator itt = result.begin(); itt != result.end(); ++itt)
1143 		{
1144 			std::vector<std::string> sd = *itt;
1145 			std::shared_ptr<CKodiNode>	pNode = (std::shared_ptr<CKodiNode>) new CKodiNode(&m_ios, m_HwdID, m_iPollInterval, m_iPingTimeoutms, sd[0], sd[1], sd[2], sd[3]);
1146 			m_pNodes.push_back(pNode);
1147 		}
1148 		// start the threads to control each kodi
1149 		for (std::vector<std::shared_ptr<CKodiNode> >::iterator itt = m_pNodes.begin(); itt != m_pNodes.end(); ++itt)
1150 		{
1151 			_log.Log(LOG_NORM, "Kodi: (%s) Starting thread.", (*itt)->m_Name.c_str());
1152 			boost::thread* tAsync = new boost::thread(&CKodiNode::Do_Work, (*itt));
1153 			SetThreadName(tAsync->native_handle(), "KodiNode");
1154 		}
1155 		sleep_milliseconds(100);
1156 		_log.Log(LOG_NORM, "Kodi: Starting I/O service thread.");
1157 		boost::thread bt(boost::bind(&boost::asio::io_service::run, &m_ios));
1158 		SetThreadName(bt.native_handle(), "KodiIO");
1159 	}
1160 }
1161 
UnloadNodes()1162 void CKodi::UnloadNodes()
1163 {
1164 	std::lock_guard<std::mutex> l(m_mutex);
1165 
1166 	m_ios.stop();	// stop the service if it is running
1167 	sleep_milliseconds(100);
1168 
1169 	while (((!m_pNodes.empty()) || (!m_ios.stopped())))
1170 	{
1171 		std::vector<std::shared_ptr<CKodiNode> >::iterator itt;
1172 		for (itt = m_pNodes.begin(); itt != m_pNodes.end(); ++itt)
1173 		{
1174 			(*itt)->StopRequest();
1175 			if (!(*itt)->IsBusy())
1176 			{
1177 				_log.Log(LOG_NORM, "Kodi: (%s) Removing device.", (*itt)->m_Name.c_str());
1178 				m_pNodes.erase(itt);
1179 				break;
1180 			}
1181 		}
1182 		sleep_milliseconds(150);
1183 	}
1184 	m_pNodes.clear();
1185 }
1186 
SendCommand(const int ID,const std::string & command)1187 void CKodi::SendCommand(const int ID, const std::string &command)
1188 {
1189 	std::vector<std::shared_ptr<CKodiNode> >::iterator itt;
1190 	for (itt = m_pNodes.begin(); itt != m_pNodes.end(); ++itt)
1191 	{
1192 		if ((*itt)->m_ID == ID)
1193 		{
1194 			(*itt)->SendCommand(command);
1195 			return;
1196 		}
1197 	}
1198 
1199 	_log.Log(LOG_ERROR, "Kodi: (%d) Command: '%s'. Device not found.", ID, command.c_str());
1200 }
1201 
SetPlaylist(const int ID,const std::string & playlist)1202 bool CKodi::SetPlaylist(const int ID, const std::string &playlist)
1203 {
1204 	std::vector<std::shared_ptr<CKodiNode> >::iterator itt;
1205 	for (itt = m_pNodes.begin(); itt != m_pNodes.end(); ++itt)
1206 	{
1207 		if ((*itt)->m_ID == ID)
1208 		{
1209 			(*itt)->SetPlaylist(playlist);
1210 			return true;
1211 		}
1212 	}
1213 	return false;
1214 }
1215 
SetExecuteCommand(const int ID,const std::string & command)1216 bool CKodi::SetExecuteCommand(const int ID, const std::string &command)
1217 {
1218 	std::vector<std::shared_ptr<CKodiNode> >::iterator itt;
1219 	for (itt = m_pNodes.begin(); itt != m_pNodes.end(); ++itt)
1220 	{
1221 		if ((*itt)->m_ID == ID)
1222 		{
1223 			(*itt)->SetExecuteCommand(command);
1224 			return true;
1225 		}
1226 	}
1227 	return false;
1228 }
1229 
1230 //Webserver helpers
1231 namespace http {
1232 	namespace server {
Cmd_KodiGetNodes(WebEmSession & session,const request & req,Json::Value & root)1233 		void CWebServer::Cmd_KodiGetNodes(WebEmSession & session, const request& req, Json::Value &root)
1234 		{
1235 			if (session.rights != 2)
1236 			{
1237 				session.reply_status = reply::forbidden;
1238 				return; //Only admin user allowed
1239 			}
1240 			std::string hwid = request::findValue(&req, "idx");
1241 			if (hwid == "")
1242 				return;
1243 			int iHardwareID = atoi(hwid.c_str());
1244 			CDomoticzHardwareBase *pHardware = m_mainworker.GetHardware(iHardwareID);
1245 			if (pHardware == NULL)
1246 				return;
1247 			if (pHardware->HwdType != HTYPE_Kodi)
1248 				return;
1249 
1250 			root["status"] = "OK";
1251 			root["title"] = "KodiGetNodes";
1252 
1253 			std::vector<std::vector<std::string> > result;
1254 			result = m_sql.safe_query("SELECT ID,Name,MacAddress,Timeout FROM WOLNodes WHERE (HardwareID==%d)", iHardwareID);
1255 			if (!result.empty())
1256 			{
1257 				std::vector<std::vector<std::string> >::const_iterator itt;
1258 				int ii = 0;
1259 				for (itt = result.begin(); itt != result.end(); ++itt)
1260 				{
1261 					std::vector<std::string> sd = *itt;
1262 
1263 					root["result"][ii]["idx"] = sd[0];
1264 					root["result"][ii]["Name"] = sd[1];
1265 					root["result"][ii]["IP"] = sd[2];
1266 					root["result"][ii]["Port"] = atoi(sd[3].c_str());
1267 					ii++;
1268 				}
1269 			}
1270 		}
1271 
Cmd_KodiSetMode(WebEmSession & session,const request & req,Json::Value & root)1272 		void CWebServer::Cmd_KodiSetMode(WebEmSession & session, const request& req, Json::Value &root)
1273 		{
1274 			if (session.rights != 2)
1275 			{
1276 				session.reply_status = reply::forbidden;
1277 				return; //Only admin user allowed
1278 			}
1279 			std::string hwid = request::findValue(&req, "idx");
1280 			std::string mode1 = request::findValue(&req, "mode1");
1281 			std::string mode2 = request::findValue(&req, "mode2");
1282 			if (
1283 				(hwid == "") ||
1284 				(mode1 == "") ||
1285 				(mode2 == "")
1286 				)
1287 				return;
1288 			int iHardwareID = atoi(hwid.c_str());
1289 			CDomoticzHardwareBase *pBaseHardware = m_mainworker.GetHardware(iHardwareID);
1290 			if (pBaseHardware == NULL)
1291 				return;
1292 			if (pBaseHardware->HwdType != HTYPE_Kodi)
1293 				return;
1294 			CKodi *pHardware = reinterpret_cast<CKodi*>(pBaseHardware);
1295 
1296 			root["status"] = "OK";
1297 			root["title"] = "KodiSetMode";
1298 
1299 			int iMode1 = atoi(mode1.c_str());
1300 			int iMode2 = atoi(mode2.c_str());
1301 
1302 			m_sql.safe_query("UPDATE Hardware SET Mode1=%d, Mode2=%d WHERE (ID == '%q')", iMode1, iMode2, hwid.c_str());
1303 			pHardware->SetSettings(iMode1, iMode2);
1304 			pHardware->Restart();
1305 		}
1306 
Cmd_KodiAddNode(WebEmSession & session,const request & req,Json::Value & root)1307 		void CWebServer::Cmd_KodiAddNode(WebEmSession & session, const request& req, Json::Value &root)
1308 		{
1309 			if (session.rights != 2)
1310 			{
1311 				session.reply_status = reply::forbidden;
1312 				return; //Only admin user allowed
1313 			}
1314 
1315 			std::string hwid = request::findValue(&req, "idx");
1316 			std::string name = HTMLSanitizer::Sanitize(request::findValue(&req, "name"));
1317 			std::string ip = HTMLSanitizer::Sanitize(request::findValue(&req, "ip"));
1318 			int Port = atoi(request::findValue(&req, "port").c_str());
1319 			if (
1320 				(hwid == "") ||
1321 				(name == "") ||
1322 				(ip == "") ||
1323 				(Port == 0)
1324 				)
1325 				return;
1326 			int iHardwareID = atoi(hwid.c_str());
1327 			CDomoticzHardwareBase *pBaseHardware = m_mainworker.GetHardware(iHardwareID);
1328 			if (pBaseHardware == NULL)
1329 				return;
1330 			if (pBaseHardware->HwdType != HTYPE_Kodi)
1331 				return;
1332 			CKodi *pHardware = reinterpret_cast<CKodi*>(pBaseHardware);
1333 
1334 			root["status"] = "OK";
1335 			root["title"] = "KodiAddNode";
1336 			pHardware->AddNode(name, ip, Port);
1337 		}
1338 
Cmd_KodiUpdateNode(WebEmSession & session,const request & req,Json::Value & root)1339 		void CWebServer::Cmd_KodiUpdateNode(WebEmSession & session, const request& req, Json::Value &root)
1340 		{
1341 			if (session.rights != 2)
1342 			{
1343 				session.reply_status = reply::forbidden;
1344 				return; //Only admin user allowed
1345 			}
1346 
1347 			std::string hwid = request::findValue(&req, "idx");
1348 			std::string nodeid = request::findValue(&req, "nodeid");
1349 			std::string name = HTMLSanitizer::Sanitize(request::findValue(&req, "name"));
1350 			std::string ip = HTMLSanitizer::Sanitize(request::findValue(&req, "ip"));
1351 			int Port = atoi(request::findValue(&req, "port").c_str());
1352 			if (
1353 				(hwid == "") ||
1354 				(nodeid == "") ||
1355 				(name == "") ||
1356 				(ip == "") ||
1357 				(Port == 0)
1358 				)
1359 				return;
1360 			int iHardwareID = atoi(hwid.c_str());
1361 			CDomoticzHardwareBase *pBaseHardware = m_mainworker.GetHardware(iHardwareID);
1362 			if (pBaseHardware == NULL)
1363 				return;
1364 			if (pBaseHardware->HwdType != HTYPE_Kodi)
1365 				return;
1366 			CKodi *pHardware = reinterpret_cast<CKodi*>(pBaseHardware);
1367 
1368 			int NodeID = atoi(nodeid.c_str());
1369 			root["status"] = "OK";
1370 			root["title"] = "KodiUpdateNode";
1371 			pHardware->UpdateNode(NodeID, name, ip, Port);
1372 		}
1373 
Cmd_KodiRemoveNode(WebEmSession & session,const request & req,Json::Value & root)1374 		void CWebServer::Cmd_KodiRemoveNode(WebEmSession & session, const request& req, Json::Value &root)
1375 		{
1376 			if (session.rights != 2)
1377 			{
1378 				session.reply_status = reply::forbidden;
1379 				return; //Only admin user allowed
1380 			}
1381 
1382 			std::string hwid = request::findValue(&req, "idx");
1383 			std::string nodeid = request::findValue(&req, "nodeid");
1384 			if (
1385 				(hwid == "") ||
1386 				(nodeid == "")
1387 				)
1388 				return;
1389 			int iHardwareID = atoi(hwid.c_str());
1390 			CDomoticzHardwareBase *pBaseHardware = m_mainworker.GetHardware(iHardwareID);
1391 			if (pBaseHardware == NULL)
1392 				return;
1393 			if (pBaseHardware->HwdType != HTYPE_Kodi)
1394 				return;
1395 			CKodi *pHardware = reinterpret_cast<CKodi*>(pBaseHardware);
1396 
1397 			int NodeID = atoi(nodeid.c_str());
1398 			root["status"] = "OK";
1399 			root["title"] = "KodiRemoveNode";
1400 			pHardware->RemoveNode(NodeID);
1401 		}
1402 
Cmd_KodiClearNodes(WebEmSession & session,const request & req,Json::Value & root)1403 		void CWebServer::Cmd_KodiClearNodes(WebEmSession & session, const request& req, Json::Value &root)
1404 		{
1405 			if (session.rights != 2)
1406 			{
1407 				session.reply_status = reply::forbidden;
1408 				return; //Only admin user allowed
1409 			}
1410 
1411 			std::string hwid = request::findValue(&req, "idx");
1412 			if (hwid == "")
1413 				return;
1414 			int iHardwareID = atoi(hwid.c_str());
1415 			CDomoticzHardwareBase *pBaseHardware = m_mainworker.GetHardware(iHardwareID);
1416 			if (pBaseHardware == NULL)
1417 				return;
1418 			if (pBaseHardware->HwdType != HTYPE_Kodi)
1419 				return;
1420 			CKodi *pHardware = reinterpret_cast<CKodi*>(pBaseHardware);
1421 
1422 			root["status"] = "OK";
1423 			root["title"] = "KodiClearNodes";
1424 			pHardware->RemoveAllNodes();
1425 		}
1426 
Cmd_KodiMediaCommand(WebEmSession & session,const request & req,Json::Value & root)1427 		void CWebServer::Cmd_KodiMediaCommand(WebEmSession & session, const request& req, Json::Value &root)
1428 		{
1429 			std::string sIdx = request::findValue(&req, "idx");
1430 			std::string sAction = request::findValue(&req, "action");
1431 			if (sIdx.empty())
1432 				return;
1433 			int idx = atoi(sIdx.c_str());
1434 			root["status"] = "OK";
1435 			root["title"] = "KodiMediaCommand";
1436 
1437 			std::vector<std::vector<std::string> > result;
1438 			result = m_sql.safe_query("SELECT DS.SwitchType, H.Type, H.ID FROM DeviceStatus DS, Hardware H WHERE (DS.ID=='%q') AND (DS.HardwareID == H.ID)", sIdx.c_str());
1439 			if (result.size() == 1)
1440 			{
1441 				_eSwitchType	sType = (_eSwitchType)atoi(result[0][0].c_str());
1442 				_eHardwareTypes	hType = (_eHardwareTypes)atoi(result[0][1].c_str());
1443 				int HwID = atoi(result[0][2].c_str());
1444 				// Is the device a media Player?
1445 				if (sType == STYPE_Media)
1446 				{
1447 					switch (hType) {
1448 					case HTYPE_Kodi:
1449 					{
1450 						CKodi	Kodi(HwID);
1451 						Kodi.SendCommand(idx, sAction);
1452 						break;
1453 					}
1454 #ifdef ENABLE_PYTHON
1455 					case HTYPE_PythonPlugin:
1456 						Cmd_PluginCommand(session, req, root);
1457 						break;
1458 #endif
1459 						// put other players here ...
1460 					}
1461 				}
1462 			}
1463 		}
1464 
1465 	}
1466 }
1467