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