1 // Tado plugin for Domoticz
2 //
3 // This plugin uses the same API as the my.tado.com web interface. Unfortunately this
4 // API is unofficial and undocumented, but until Tado releases an official and
5 // documented API, it's the best we've got.
6 //
7 // Main documentation for parts of the API can be found at
8 // http://blog.scphillips.com/posts/2017/01/the-tado-api-v2/ but unfortunately
9 // this information is slightly outdated, the authentication part in particular.
10 
11 
12 #include "stdafx.h"
13 #include "../main/Helper.h"
14 #include "../main/Logger.h"
15 #include "hardwaretypes.h"
16 #include "../main/localtime_r.h"
17 #include "../main/RFXtrx.h"
18 #include "../main/SQLHelper.h"
19 #include "../httpclient/HTTPClient.h"
20 #include "../httpclient/UrlEncode.h"
21 #include "../main/mainworker.h"
22 #include "../main/json_helper.h"
23 #include "../webserver/Base64.h"
24 #include "Tado.h"
25 
26 #define TADO_POLL_INTERVAL 30		// The plugin should collect information from the API every n seconds.
27 #define TADO_API_ENVIRONMENT_URL "https://my.tado.com/webapp/env.js"
28 #define TADO_TOKEN_MAXLOOPS 12		// Default token validity is 600 seconds before it needs to be refreshed.
29 									// Each cycle takes 30-35 seconds, so let's stay a bit on the safe side.
30 
~CTado(void)31 CTado::~CTado(void)
32 {
33 }
34 
CTado(const int ID,const std::string & username,const std::string & password)35 CTado::CTado(const int ID, const std::string &username, const std::string &password):
36 m_TadoUsername(username),
37 m_TadoPassword(password)
38 {
39 	m_HwdID = ID;
40 
41 	Init();
42 }
43 
StartHardware()44 bool CTado::StartHardware()
45 {
46 	RequestStart();
47 
48 	Init();
49 	//Start worker thread
50 	m_thread = std::make_shared<std::thread>(&CTado::Do_Work, this);
51 	SetThreadNameInt(m_thread->native_handle());
52 	m_bIsStarted = true;
53 	sOnConnected(this);
54 	return (m_thread != nullptr);
55 }
56 
Init()57 void CTado::Init()
58 {
59 	m_bDoLogin = true;
60 	m_bDoGetHomes = true;
61 	m_bDoGetZones = false;
62 	m_bDoGetEnvironment = true;
63 
64 	stdstring_trim(m_TadoUsername);
65 	stdstring_trim(m_TadoPassword);
66 }
67 
StopHardware()68 bool CTado::StopHardware()
69 {
70 	if (m_thread)
71 	{
72 		RequestStop();
73 		m_thread->join();
74 		m_thread.reset();
75 	}
76 	m_bIsStarted = false;
77 
78 	//if (!m_bDoLogin)
79 	//	Logout();
80 
81 	return true;
82 }
83 
WriteToHardware(const char * pdata,const unsigned char length)84 bool CTado::WriteToHardware(const char * pdata, const unsigned char length)
85 {
86 	if (m_TadoAuthToken.size() == 0)
87 		return false;
88 
89 	const tRBUF *pCmd = reinterpret_cast<const tRBUF *>(pdata);
90 	if (pCmd->LIGHTING2.packettype != pTypeLighting2)
91 		return false;
92 
93 	int node_id = pCmd->LIGHTING2.id4;
94 
95 	bool bIsOn = (pCmd->LIGHTING2.cmnd == light2_sOn);
96 
97 
98 	int HomeIdx = node_id / 1000;
99 	int ZoneIdx = (node_id % 1000) / 100;
100 	int ServiceIdx = (node_id % 1000) % 100;
101 
102 	_log.Debug(DEBUG_HARDWARE, "Tado: Node %d = home %s zone %s device %d", node_id, m_TadoHomes[HomeIdx].Name.c_str(), m_TadoHomes[HomeIdx].Zones[ZoneIdx].Name.c_str(), ServiceIdx);
103 
104 	// ServiceIdx 1 = Away (Read only)
105 	// ServiceIdx 2 = Setpoint => should be handled in SetSetPoint
106 	// ServiceIdx 3 = TempHum (Read only)
107 	// ServiceIdx 4 = Setpoint Override
108 	// ServiceIdx 5 = Heating Enabled
109 	// ServiceIdx 6 = Heating On (Read only)
110 	// ServiceIdx 7 = Heating Power (Read only)
111 
112 	// Cancel setpoint override.
113 	if (ServiceIdx == 4 && !bIsOn) return CancelOverlay(node_id);
114 
115 	// Enable heating (= cancel overlay that turns off heating)
116 	if (ServiceIdx == 5 && bIsOn) return CancelOverlay(node_id);
117 
118 	// Disable heating (= create overlay that turns off heating for an indeterminate amount of time)
119 	if (ServiceIdx == 5 && !bIsOn) return CreateOverlay(node_id, -1, false, "MANUAL");
120 
121 	// If the writetohardware command is not handled by now, fail.
122 	return false;
123 }
124 
125 // Changing the setpoint or heating mode is an overlay on the schedule.
126 // An overlay can end automatically (TADO_MODE, TIMER) or manually (MANUAL).
CreateOverlay(const int idx,const float temp,const bool heatingEnabled,const std::string & terminationType)127 bool CTado::CreateOverlay(const int idx, const float temp, const bool heatingEnabled, const std::string &terminationType)
128 {
129 	_log.Log(LOG_NORM, "Tado: CreateOverlay() called with idx=%d, temp=%f, termination type=%s", idx, temp, terminationType.c_str());
130 
131 	int HomeIdx = idx / 1000;
132 	int ZoneIdx = (idx % 1000) / 100;
133 	int ServiceIdx = (idx % 1000) % 100;
134 
135 	// Check if the zone actually exists.
136 	if (m_TadoHomes.size() == 0 || m_TadoHomes[HomeIdx].Zones.size() == 0)
137 	{
138 		_log.Log(LOG_ERROR, "Tado: no such home/zone combo found: %d/%d", HomeIdx, ZoneIdx);
139 		return false;
140 	}
141 
142 	_log.Debug(DEBUG_HARDWARE, "Tado: Node %d = home %s zone %s device %d", idx, m_TadoHomes[HomeIdx].Name.c_str(), m_TadoHomes[HomeIdx].Zones[ZoneIdx].Name.c_str(), ServiceIdx);
143 
144 	std::string _sUrl = m_TadoEnvironment["tgaRestApiV2Endpoint"] + "/homes/" + m_TadoHomes[HomeIdx].Id + "/zones/" + m_TadoHomes[HomeIdx].Zones[ZoneIdx].Id + "/overlay";
145 	std::string _sResponse;
146 	Json::Value _jsPostData;
147 	Json::Value _jsPostDataSetting;
148 
149 	Json::Value _jsPostDataTermination;
150 	_jsPostDataSetting["type"] = "HEATING";
151 	_jsPostDataSetting["power"] = (heatingEnabled ? "ON" : "OFF");
152 
153 	if (temp > -1)
154 	{
155 		Json::Value _jsPostDataSettingTemperature;
156 		_jsPostDataSettingTemperature["celsius"] = temp;
157 		_jsPostDataSetting["temperature"] = _jsPostDataSettingTemperature;
158 	}
159 
160 	_jsPostData["setting"] = _jsPostDataSetting;
161 	_jsPostDataTermination["type"] = terminationType;
162 	_jsPostData["termination"] = _jsPostDataTermination;
163 
164 	Json::Value _jsRoot;
165 
166 	try
167 	{
168 		SendToTadoApi(Put, _sUrl, _jsPostData.toStyledString(), _sResponse, *(new std::vector<std::string>()), _jsRoot);
169 	}
170 	catch (std::exception& e)
171 	{
172 		std::string what = e.what();
173 		_log.Log(LOG_ERROR, "Tado: Failed to set setpoint via Api: %s", what.c_str());
174 		return false;
175 	}
176 
177 	_log.Debug(DEBUG_HARDWARE, "Tado: Response: %s", _sResponse.c_str());
178 
179 	// Trigger a zone refresh
180 	GetZoneState(HomeIdx, ZoneIdx, m_TadoHomes[HomeIdx], m_TadoHomes[HomeIdx].Zones[ZoneIdx]);
181 
182 	return true;
183 }
184 
SetSetpoint(const int idx,const float temp)185 void CTado::SetSetpoint(const int idx, const float temp)
186 {
187 	_log.Log(LOG_NORM, "Tado: SetSetpoint() called with idx=%d, temp=%f", idx, temp);
188 	CreateOverlay(idx, temp, true, "TADO_MODE");
189 }
190 
191 // Requests an authentication token from the Tado OAuth Api.
GetAuthToken(std::string & authtoken,std::string & refreshtoken,const bool refreshUsingToken=false)192 bool CTado::GetAuthToken(std::string &authtoken, std::string &refreshtoken, const bool refreshUsingToken = false)
193 {
194 	try
195 	{
196 		if (m_TadoUsername.size() == 0 && !refreshUsingToken)
197 		{
198 			_log.Log(LOG_ERROR, "Tado: No username specified.");
199 			return false;
200 		}
201 		if (m_TadoPassword.size() == 0 && !refreshUsingToken)
202 		{
203 			_log.Log(LOG_ERROR, "Tado: No password specified.");
204 			return false;
205 		}
206 		if (m_bDoGetEnvironment)
207 		{
208 			_log.Log(LOG_ERROR, "Tado: Environment not (yet) set up.");
209 			return false;
210 		}
211 
212 		std::string _sUrl = m_TadoEnvironment["apiEndpoint"] + "/token";
213 		std::ostringstream s;
214 		std::string _sGrantType = (refreshUsingToken ? "refresh_token" : "password");
215 
216 		s << "client_id=" << m_TadoEnvironment["clientId"] << "&grant_type=";
217 		s << _sGrantType << "&scope=home.user&client_secret=";
218 		s << m_TadoEnvironment["clientSecret"];
219 
220 		if (refreshUsingToken)
221 		{
222 			s << "&refresh_token=" << refreshtoken;
223 		}
224 		else
225 		{
226 			s << "&password=" << CURLEncode::URLEncode(m_TadoPassword);
227 			s << "&username=" << CURLEncode::URLEncode(m_TadoUsername);
228 		}
229 
230 		std::string sPostData = s.str();
231 
232 		std::string _sResponse;
233 		std::vector<std::string> _vExtraHeaders;
234 		_vExtraHeaders.push_back("Content-Type: application/x-www-form-urlencoded");
235 		std::vector<std::string> _vResponseHeaders;
236 
237 		Json::Value _jsRoot;
238 
239 		try
240 		{
241 			SendToTadoApi(Post, _sUrl, sPostData, _sResponse, _vExtraHeaders, _jsRoot, true, false, false);
242 		}
243 		catch (std::exception& e)
244 		{
245 			std::string what = e.what();
246 			_log.Log(LOG_ERROR, "Tado: Failed to get token from Api: %s", what.c_str());
247 			return false;
248 		}
249 
250 		authtoken = _jsRoot["access_token"].asString();
251 		if (authtoken.size() == 0)
252 		{
253 			_log.Log(LOG_ERROR, "Tado: Received token is zero length.");
254 			return false;
255 		}
256 
257 		refreshtoken = _jsRoot["refresh_token"].asString();
258 		if (refreshtoken.size() == 0)
259 		{
260 			_log.Log(LOG_ERROR, "Tado: Received refresh token is zero length.");
261 			return false;
262 		}
263 
264 		_log.Log(LOG_STATUS, "Tado: Received access token from API.");
265 		_log.Log(LOG_STATUS, "Tado: Received refresh token from API.");
266 
267 		return true;
268 	}
269 	catch (std::exception& e) {
270 		std::string what = e.what();
271 		_log.Log(LOG_ERROR, "Tado: GetAuthToken: %s", what.c_str());
272 		return false;
273 	}
274 }
275 
276 // Gets the status information of a zone.
GetZoneState(const int HomeIndex,const int ZoneIndex,const _tTadoHome & home,_tTadoZone & zone)277 bool CTado::GetZoneState(const int HomeIndex, const int ZoneIndex, const _tTadoHome &home, _tTadoZone &zone)
278 {
279 	try
280 	{
281 		std::string _sUrl = m_TadoEnvironment["tgaRestApiV2Endpoint"] + "/homes/" + zone.HomeId + "/zones/" + zone.Id + "/state";
282 		Json::Value _jsRoot;
283 		std::string _sResponse;
284 
285 		try
286 		{
287 			SendToTadoApi(Get, _sUrl, "", _sResponse, *(new std::vector<std::string>()), _jsRoot);
288 		}
289 		catch (std::exception& e)
290 		{
291 			std::string what = e.what();
292 			_log.Log(LOG_ERROR, "Tado: Failed to get information on zone '%s': %s", zone.Name.c_str(), what.c_str());
293 			return false;
294 		}
295 
296 		// Zone Home/away
297 		//bool _bTadoAway = !(_jsRoot["tadoMode"].asString() == "HOME");
298 		//UpdateSwitch((unsigned char)ZoneIndex * 100 + 1, _bTadoAway, home.Name + " " + zone.Name + " Away");
299 
300 		// Zone setpoint
301 		float _fSetpointC = 0;
302 		if (_jsRoot["setting"]["temperature"]["celsius"].isNumeric())
303 			_fSetpointC = _jsRoot["setting"]["temperature"]["celsius"].asFloat();
304 		if (_fSetpointC > 0) {
305 			SendSetPointSensor((unsigned char)ZoneIndex * 100 + 2, _fSetpointC, home.Name + " " + zone.Name + " Setpoint");
306 		}
307 
308 		// Current zone inside temperature
309 		float _fCurrentTempC = 0;
310 		if (_jsRoot["sensorDataPoints"]["insideTemperature"]["celsius"].isNumeric())
311 			_fCurrentTempC = _jsRoot["sensorDataPoints"]["insideTemperature"]["celsius"].asFloat();
312 
313 		// Current zone humidity
314 		float fCurrentHumPct = 0;
315 		if (_jsRoot["sensorDataPoints"]["humidity"]["percentage"].isNumeric())
316 			fCurrentHumPct = _jsRoot["sensorDataPoints"]["humidity"]["percentage"].asFloat();
317 		if (_fCurrentTempC > 0) {
318 			SendTempHumSensor(ZoneIndex * 100 + 3, 255, _fCurrentTempC, (int)fCurrentHumPct, home.Name + " " + zone.Name + " TempHum");
319 		}
320 
321 		// Manual override of zone setpoint
322 		bool _bManualControl = false;
323 		if (!_jsRoot["overlay"].isNull() && _jsRoot["overlay"]["type"].asString() == "MANUAL")
324 		{
325 			_bManualControl = true;
326 		}
327 		UpdateSwitch((unsigned char)ZoneIndex * 100 + 4, _bManualControl, home.Name + " " + zone.Name + " Manual Setpoint Override");
328 
329 
330 		// Heating Enabled
331 		std::string _sType = _jsRoot["setting"]["type"].asString();
332 		std::string _sPower = _jsRoot["setting"]["power"].asString();
333 		bool _bHeatingEnabled = false;
334 		if (_sType == "HEATING" && _sPower == "ON")
335 			_bHeatingEnabled = true;
336 		UpdateSwitch((unsigned char)ZoneIndex * 100 + 5, _bHeatingEnabled, home.Name + " " + zone.Name + " Heating Enabled");
337 
338 		// Heating Power percentage
339 		std::string _sHeatingPowerType = _jsRoot["activityDataPoints"]["heatingPower"]["type"].asString();
340 		int _sHeatingPowerPercentage = _jsRoot["activityDataPoints"]["heatingPower"]["percentage"].asInt();
341 		bool _bHeatingOn = false;
342 		if (_sHeatingPowerType == "PERCENTAGE" && _sHeatingPowerPercentage >= 0 && _sHeatingPowerPercentage <= 100)
343 		{
344 			_bHeatingOn = _sHeatingPowerPercentage > 0;
345 			UpdateSwitch((unsigned char)ZoneIndex * 100 + 6, _bHeatingOn, home.Name + " " + zone.Name + " Heating On");
346 
347 			SendPercentageSensor(ZoneIndex * 100 + 7, 0, 255, (float)_sHeatingPowerPercentage, home.Name + " " + zone.Name + " Heating Power");
348 		}
349 
350 		return true;
351 	}
352 	catch (std::exception& e)
353 	{
354 		std::string what = e.what();
355 		_log.Log(LOG_ERROR, "Tado: GetZoneState: %s", what.c_str());
356 		return false;
357 	}
358 }
359 
GetHomeState(const int HomeIndex,_tTadoHome & home)360 bool CTado::GetHomeState(const int HomeIndex, _tTadoHome & home)
361 {
362 	try {
363 		std::stringstream _sstr;
364 		_sstr << m_TadoEnvironment["tgaRestApiV2Endpoint"] << "/homes/" << home.Id << "/state";
365 		std::string _sUrl = _sstr.str();
366 		Json::Value _jsRoot;
367 		std::string _sResponse;
368 		try
369 		{
370 			SendToTadoApi(Get, _sUrl, "", _sResponse, *(new std::vector<std::string>()), _jsRoot);
371 		}
372 		catch (std::exception& e)
373 		{
374 			std::string what = e.what();
375 			_log.Log(LOG_ERROR, "Tado: Failed to get state information on home '%s': %s", home.Name.c_str(), what.c_str());
376 			return false;
377 		}
378 
379 		// Home/away
380 		bool _bTadoAway = !(_jsRoot["presence"].asString() == "HOME");
381 		UpdateSwitch((unsigned char)HomeIndex * 1000 + 0, _bTadoAway, home.Name + " Away");
382 
383 		return true;
384 	}
385 	catch (std::exception& e)
386 	{
387 		std::string what = e.what();
388 		_log.Log(LOG_ERROR, "Tado: GetZoneState: %s", what.c_str());
389 		return false;
390 	}
391 }
392 
SendSetPointSensor(const unsigned char Idx,const float Temp,const std::string & defaultname)393 void CTado::SendSetPointSensor(const unsigned char Idx, const float Temp, const std::string & defaultname)
394 {
395 	_tThermostat thermos;
396 	thermos.subtype = sTypeThermSetpoint;
397 	thermos.id1 = 0;
398 	thermos.id2 = 0;
399 	thermos.id3 = 0;
400 	thermos.id4 = Idx;
401 	thermos.dunit = 0;
402 
403 	thermos.temp = Temp;
404 
405 	sDecodeRXMessage(this, (const unsigned char *)&thermos, defaultname.c_str(), 255);
406 }
407 
408 // Creates or updates on/off switches.
UpdateSwitch(const unsigned char Idx,const bool bOn,const std::string & defaultname)409 void CTado::UpdateSwitch(const unsigned char Idx, const bool bOn, const std::string &defaultname)
410 {
411 	//bool bDeviceExits = true;
412 	char szIdx[10];
413 	sprintf(szIdx, "%X%02X%02X%02X", 0, 0, 0, Idx);
414 	std::vector<std::vector<std::string> > result;
415 	result = m_sql.safe_query("SELECT Name,nValue,sValue FROM DeviceStatus WHERE (HardwareID==%d) AND (Type==%d) AND (SubType==%d) AND (DeviceID=='%q')",
416 		m_HwdID, pTypeLighting2, sTypeAC, szIdx);
417 	if (!result.empty())
418 	{
419 		//check if we have a change, if not do not update it
420 		int nvalue = atoi(result[0][1].c_str());
421 		if ((!bOn) && (nvalue == 0))
422 			return;
423 		if ((bOn && (nvalue != 0)))
424 			return;
425 	}
426 
427 	//Send as Lighting 2
428 	tRBUF lcmd;
429 	memset(&lcmd, 0, sizeof(RBUF));
430 	lcmd.LIGHTING2.packetlength = sizeof(lcmd.LIGHTING2) - 1;
431 	lcmd.LIGHTING2.packettype = pTypeLighting2;
432 	lcmd.LIGHTING2.subtype = sTypeAC;
433 	lcmd.LIGHTING2.id1 = 0;
434 	lcmd.LIGHTING2.id2 = 0;
435 	lcmd.LIGHTING2.id3 = 0;
436 	lcmd.LIGHTING2.id4 = Idx;
437 	lcmd.LIGHTING2.unitcode = 1;
438 	int level = 15;
439 	if (!bOn)
440 	{
441 		level = 0;
442 		lcmd.LIGHTING2.cmnd = light2_sOff;
443 	}
444 	else
445 	{
446 		level = 15;
447 		lcmd.LIGHTING2.cmnd = light2_sOn;
448 	}
449 	lcmd.LIGHTING2.level = level;
450 	lcmd.LIGHTING2.filler = 0;
451 	lcmd.LIGHTING2.rssi = 12;
452 	sDecodeRXMessage(this, (const unsigned char *)&lcmd.LIGHTING2, defaultname.c_str(), 255);
453 }
454 
455 // Removes any active overlay from a specific zone.
CancelOverlay(const int Idx)456 bool CTado::CancelOverlay(const int Idx)
457 {
458 	_log.Debug(DEBUG_HARDWARE, "Tado: CancelSetpointOverlay() called with idx=%d", Idx);
459 
460 	int HomeIdx = Idx / 1000;
461 	int ZoneIdx = (Idx % 1000) / 100;
462 	//int ServiceIdx = (Idx % 1000) % 100;
463 
464 	// Check if the home and zone actually exist.
465 	if (m_TadoHomes.size() == 0 || m_TadoHomes[HomeIdx].Zones.size() == 0)
466 	{
467 		_log.Log(LOG_ERROR, "Tado: no such home/zone combo found: %d/%d", HomeIdx, ZoneIdx);
468 		return false;
469 	}
470 
471 	std::stringstream _sstr;
472 	_sstr << m_TadoEnvironment["tgaRestApiV2Endpoint"] << "/homes/" << m_TadoHomes[HomeIdx].Id << "/zones/" << m_TadoHomes[HomeIdx].Zones[ZoneIdx].Id << "/overlay";
473 	std::string _sUrl = _sstr.str();
474 	std::string _sResponse;
475 
476 	try
477 	{
478 		SendToTadoApi(Delete, _sUrl, "", _sResponse, *(new std::vector<std::string>()), *(new Json::Value), false, true, true);
479 
480 	}
481 	catch (std::exception& e)
482 	{
483 		std::string what = e.what();
484 		_log.Log(LOG_ERROR, "Tado: error cancelling the setpoint overlay: %s", what.c_str());
485 		return false;
486 	}
487 
488 	// Trigger a zone refresh
489 	_log.Log(LOG_STATUS, "Tado: Setpoint overlay cancelled.");
490 	GetZoneState(HomeIdx, ZoneIdx, m_TadoHomes[HomeIdx], m_TadoHomes[HomeIdx].Zones[ZoneIdx]);
491 
492 	return true;
493 }
494 
Do_Work()495 void CTado::Do_Work()
496 {
497 	_log.Log(LOG_STATUS, "Tado: Worker started. Will poll every %d seconds.", TADO_POLL_INTERVAL);
498 	int iSecCounter = TADO_POLL_INTERVAL - 5;
499 	int iTokenCycleCount = 0;
500 
501 	while (!IsStopRequested(1000))
502 	{
503 		iSecCounter++;
504 		if (iSecCounter % 12 == 0)
505 		{
506 			m_LastHeartbeat = mytime(NULL);
507 		}
508 
509 		if (iSecCounter % TADO_POLL_INTERVAL == 0)
510 		{
511 			// Only login if we should.
512 			if (m_bDoLogin)
513 			{
514 				// Lets try to get authentication set up.
515 				// If not, try again next time.
516 				m_bDoLogin = false;
517 				if (!Login())
518 				{
519 					// Mark that we still need to log in.
520 					m_bDoLogin = true;
521 					// Not much of a point doing other actions.
522 					continue;
523 				}
524 			}
525 
526 			// Check if we should get homes from tado account
527 			if (m_bDoGetHomes)
528 			{
529 				// If we fail to do so, abort.
530 				if (!GetHomes())
531 				{
532 					_log.Log(LOG_ERROR, "Tado: Failed to get homes from Tado account.");
533 					// Try to get homes next iteration. Other than that we can't do much now.
534 					continue;
535 				}
536 				// Else move on to getting zones for each of the homes.
537 				else {
538 					m_bDoGetZones = true;
539 					m_bDoGetHomes = false;
540 				}
541 			}
542 
543 			// Check if we should be collecting zones for each of the homes.
544 			if (m_bDoGetZones)
545 			{
546 				// Mark that we don't need to collect zones any more.
547 				m_bDoGetZones = false;
548 				for (int i = 0; i < (int)m_TadoHomes.size(); i++)
549 				{
550 					if (!GetZones(m_TadoHomes[i])){
551 						// Something went wrong, indicate that we do need to collect zones next time.
552 						m_bDoGetZones = true;
553 						_log.Log(LOG_ERROR, "Tado: Failed to get zones for home '%s'", m_TadoHomes[i].Name.c_str());
554 					}
555 				}
556 			}
557 
558 			// Check if the authentication token is still useable.
559 			if (iTokenCycleCount++ > TADO_TOKEN_MAXLOOPS)
560 			{
561 				_log.Log(LOG_NORM, "Tado: refreshing token.");
562 				if (!GetAuthToken(m_TadoAuthToken, m_TadoRefreshToken, true)) {
563 					_log.Log(LOG_ERROR, "Tado: failed to refresh authentication token. Skipping this cycle.");
564 				}
565 				// Reset the counter to its initial value.
566 				else iTokenCycleCount = 0;
567 			}
568 			// Increase the number of cycles that the token has been used by 1.
569 			else iTokenCycleCount++;
570 
571 			// Iterate through the discovered homes and zones. Get some state information.
572 			for (int HomeIndex = 0; HomeIndex < (int)m_TadoHomes.size(); HomeIndex++) {
573 
574 				if (!GetHomeState(HomeIndex, m_TadoHomes[HomeIndex]))
575 				{
576 					_log.Log(LOG_ERROR, "Tado: Failed to get state for home '%s'", m_TadoHomes[HomeIndex].Name.c_str());
577 					// Skip to the next home
578 					continue;
579 				}
580 
581 				for (int ZoneIndex = 0; ZoneIndex < (int)m_TadoHomes[HomeIndex].Zones.size(); ZoneIndex++)
582 				{
583 					if (!GetZoneState(HomeIndex, ZoneIndex, m_TadoHomes[HomeIndex], m_TadoHomes[HomeIndex].Zones[ZoneIndex]))
584 					{
585 						_log.Log(LOG_ERROR, "Tado: Failed to get state for home '%s', zone '%s'", m_TadoHomes[HomeIndex].Name.c_str(), m_TadoHomes[HomeIndex].Zones[ZoneIndex].Name.c_str());
586 					}
587 				}
588 			}
589 		}
590 	}
591 	_log.Log(LOG_STATUS, "Tado: Worker stopped.");
592 }
593 
594 // Splits a string inputString by delimiter. If specified returns up to maxelements elements.
595 // This is an extension of the ::StringSplit function in the helper class.
StringSplitEx(const std::string & inputString,const std::string & delimiter,const int maxelements)596 std::vector<std::string> CTado::StringSplitEx(const std::string &inputString, const std::string &delimiter, const int maxelements)
597 {
598 	// Split using the Helper class' StringSplitEx function.
599 	std::vector<std::string> array;
600 	StringSplit(inputString, delimiter, array);
601 
602 	// If we don't have a max number of elements specified we're done.
603 	if (maxelements == 0) return array;
604 
605 	// Else we're building a new vector with all the overflowing elements merged into the last element.
606 	std::vector<std::string> cappedArray;
607 	for (int i = 0; (unsigned int)i < array.size(); i++)
608 	{
609 		if (i <= maxelements-1)
610 		{
611 			cappedArray.push_back(array[i]);
612 		}
613 		else {
614 			cappedArray[maxelements-1].append(array[i]);
615 		}
616 	}
617 
618 	return cappedArray;
619 }
620 
621 
622 // Runs through the Tado web interface environment file and attempts to regex match the specified key.
MatchValueFromJSKey(const std::string & sKeyName,const std::string & sJavascriptData,std::string & sValue)623 bool CTado::MatchValueFromJSKey(const std::string &sKeyName, const std::string &sJavascriptData, std::string &sValue)
624 {
625 	// Rewritten to no longer use regex matching. Regex matching is the preferred, more robust way
626 	// but for various reasons we're not supposed to leverage it. Not using boost library either for
627 	// the same reasons, so various std::string features are unavailable and have to be implemented manually.
628 
629 	std::vector<std::string> _sJavascriptDataLines;
630 	std::map<std::string, std::string> _mEnvironmentKeys;
631 
632 	// Get the javascript response and split its lines
633 	StringSplit(sJavascriptData, "\n", _sJavascriptDataLines);
634 
635 	_log.Debug(DEBUG_HARDWARE, "Tado: MatchValueFromJSKey: Got %zu lines from javascript data.", _sJavascriptDataLines.size());
636 
637 	if (_sJavascriptDataLines.size() <= 0)
638 	{
639 		_log.Log(LOG_ERROR, "Tado: Failed to get any lines from javascript environment file.");
640 		return false;
641 	}
642 
643 	// Process each line.
644 	for (int i = 0; i < (int)_sJavascriptDataLines.size(); i++)
645 	{
646 		std::string _sLine = _sJavascriptDataLines[i];
647 
648 		_log.Debug(DEBUG_HARDWARE, "Tado: MatchValueFromJSKey: Processing line: '%s'", _sLine.c_str());
649 
650 		std::string _sLineKey = "";
651 		std::string _sLineValue = "";
652 
653 		// Let's split each line on a colon.
654 		std::vector<std::string> _sLineParts = StringSplitEx(_sLine, ": ", 2);
655 		if (_sLineParts.size() != 2)
656 		{
657 			continue;
658 		}
659 
660 		for (int j = 0; j < (int)_sLineParts.size(); j++)
661 		{
662 			// Do some cleanup on the parts, so we only keep the text that we want.
663 			std::string _sLinePart = _sLineParts[j];
664 			stdreplace(_sLinePart, "\t", "");
665 			stdreplace(_sLinePart, "',", "");
666 			stdreplace(_sLinePart, "'","");
667 			_sLinePart = stdstring_trim(_sLinePart);
668 
669 			// Check if a Key is already set for the key-value pair. If we don't have a key yet
670 			// assume that this first entry in the line is the key.
671 			if (_sLineKey == "")
672 			{
673 				_sLineKey = _sLinePart;
674 			}
675 			else
676 			{
677 				// Since we already have a key, assume that this second entry is the value.
678 				_sLineValue = _sLinePart;
679 
680 				// Now that we've got both a key and a value put it in the map
681 				_mEnvironmentKeys[_sLineKey] = _sLineValue;
682 
683 				_log.Debug(DEBUG_HARDWARE, "Tado: MatchValueFromJSKey: Line: '%s':'%s'", _sLineKey.c_str(), _sLineValue.c_str());
684 			}
685 		}
686 	}
687 
688 
689 	// Check the map to get the value we were looking for in the first place.
690 	if (_mEnvironmentKeys[sKeyName].empty())
691 	{
692 		_log.Log(LOG_ERROR, "Tado: Failed to grab %s from the javascript data.", sKeyName.c_str());
693 		return false;
694 	}
695 
696 	sValue = _mEnvironmentKeys[sKeyName];
697 	if (sValue.size() == 0)
698 	{
699 		_log.Log(LOG_ERROR, "Tado: Value for key %s is zero length.", sKeyName.c_str());
700 		return false;
701 	}
702 	return true;
703 }
704 
705 // Grabs the web app environment file
GetTadoApiEnvironment(std::string sUrl)706 bool CTado::GetTadoApiEnvironment(std::string sUrl)
707 {
708 	_log.Debug(DEBUG_HARDWARE, "Tado: GetTadoApiEnvironment called with sUrl=%s", sUrl.c_str());
709 
710 	// This is a bit of a special case. Since we pretend to be the web
711 	// application (my.tado.com) we have to play by its rules. It works
712 	// with some information like a client id and a client secret. We
713 	// have to pluck that environment information from the web page and
714 	// then parse it so we can use it in our future calls.
715 
716 	std::string _sResponse;
717 
718 	// Download the API environment file
719 	if (!HTTPClient::GET(sUrl, _sResponse, false)) {
720 		_log.Log(LOG_ERROR, "Tado: Failed to retrieve API environment from %s", sUrl.c_str());
721 		return false;
722 	}
723 
724 	// Determine which keys we want to grab from the environment
725 	std::vector<std::string> _vKeysToFetch;
726 	_vKeysToFetch.push_back("clientId");
727 	_vKeysToFetch.push_back("clientSecret");
728 	_vKeysToFetch.push_back("apiEndpoint");
729 	_vKeysToFetch.push_back("tgaRestApiV2Endpoint");
730 
731 	// The key values will be stored in a map, lets clean it out first.
732 	m_TadoEnvironment.clear();
733 
734 	for (int i = 0; i < (int)_vKeysToFetch.size(); i++)
735 	{
736 		// Feed the function the javascript response, and have it attempt to grab the provided key's value from it.
737 		// Value is stored in m_TadoEnvironment[keyName]
738 		std::string _sKeyName = _vKeysToFetch[i];
739 		if (!MatchValueFromJSKey(_sKeyName, _sResponse, m_TadoEnvironment[_sKeyName])) {
740 			_log.Log(LOG_ERROR, "Tado: Failed to retrieve/match key '%s' from the API environment.", _sKeyName.c_str());
741 			return false;
742 		}
743 	}
744 
745 	_log.Log(LOG_STATUS, "Tado: Retrieved webapp environment from API.");
746 
747 	// Mark this action as completed.
748 	m_bDoGetEnvironment = false;
749 	return true;
750 }
751 
752 // Sets up the environment and grabs an auth token.
Login()753 bool CTado::Login()
754 {
755 	_log.Log(LOG_NORM, "Tado: Attempting login.");
756 
757 	if (m_bDoGetEnvironment) {
758 		// First get information about the environment of the web application.
759 		if (!GetTadoApiEnvironment(TADO_API_ENVIRONMENT_URL))
760 		{
761 			return false;
762 		}
763 	}
764 
765 	// Now fetch the token.
766 	if (!GetAuthToken(m_TadoAuthToken, m_TadoRefreshToken, false))
767 	{
768 		return false;
769 	}
770 
771 	_log.Log(LOG_NORM, "Tado: Login succesful.");
772 
773 	return true;
774 }
775 
776 // Gets all the homes in the account.
GetHomes()777 bool CTado::GetHomes() {
778 
779 	_log.Debug(DEBUG_HARDWARE, "Tado: GetHomes() called.");
780 
781 	std::stringstream _sstr;
782 	_sstr << m_TadoEnvironment["tgaRestApiV2Endpoint"] << "/me";
783 	std::string _sUrl = _sstr.str();
784 
785 	Json::Value _jsRoot;
786 	std::string _sResponse;
787 
788 	try
789 	{
790 		SendToTadoApi(Get, _sUrl, "", _sResponse, *(new std::vector<std::string>()), _jsRoot);
791 	}
792 	catch (std::exception& e)
793 	{
794 		std::string what = e.what();
795 		_log.Log(LOG_ERROR, "Tado: failed to get homes: %s", what.c_str());
796 		return false;
797 	}
798 
799 	// Make sure we start with an empty list.
800 	m_TadoHomes.clear();
801 
802 	Json::Value _jsAllHomes = _jsRoot["homes"];
803 
804 	_log.Debug(DEBUG_HARDWARE, "Tado: Found %d homes.", _jsAllHomes.size());
805 
806 	for (int i = 0; i < (int)_jsAllHomes.size(); i++) {
807 		// Store the tado home information in a map.
808 
809 		if (!_jsAllHomes[i].isObject()) continue;
810 
811 		_tTadoHome _structTadoHome;
812 		_structTadoHome.Name = _jsAllHomes[i]["name"].asString();
813 		_structTadoHome.Id = _jsAllHomes[i]["id"].asString();
814 		m_TadoHomes.push_back(_structTadoHome);
815 
816 		_log.Log(LOG_STATUS, "Tado: Registered Home '%s' with id %s", _structTadoHome.Name.c_str(), _structTadoHome.Id.c_str());
817 	}
818 	// Sort the homes so they end up in the same order every time.
819 	sort(m_TadoHomes.begin(), m_TadoHomes.end());
820 
821 	return true;
822 }
823 
824 // Gets all the zones for a particular home
GetZones(_tTadoHome & tTadoHome)825 bool CTado::GetZones(_tTadoHome &tTadoHome) {
826 
827 	std::stringstream ss;
828 	ss << m_TadoEnvironment["tgaRestApiV2Endpoint"] << "/homes/" << tTadoHome.Id << "/zones";
829 	std::string _sUrl = ss.str();
830 	std::string _sResponse;
831 	Json::Value _jsRoot;
832 
833 	tTadoHome.Zones.clear();
834 
835 	try
836 	{
837 		SendToTadoApi(Get, _sUrl, "", _sResponse, *(new std::vector<std::string>()), _jsRoot);
838 	}
839 	catch (std::exception& e)
840 	{
841 		std::string what = e.what();
842 		_log.Log(LOG_ERROR, "Tado: Failed to get zones from API for Home %s: %s", tTadoHome.Id.c_str(), what.c_str());
843 		return false;
844 	}
845 
846 	// Loop through each of the zones
847 	for (int zoneIdx = 0; zoneIdx < (int)_jsRoot.size(); zoneIdx++) {
848 		_tTadoZone _TadoZone;
849 
850 		_TadoZone.HomeId = tTadoHome.Id;
851 		_TadoZone.Id = _jsRoot[zoneIdx]["id"].asString();
852 		_TadoZone.Name = _jsRoot[zoneIdx]["name"].asString();
853 		_TadoZone.Type = _jsRoot[zoneIdx]["type"].asString();
854 
855 		_log.Log(LOG_STATUS, "Tado: Registered Zone %s '%s' of type %s", _TadoZone.Id.c_str(), _TadoZone.Name.c_str(), _TadoZone.Type.c_str());
856 
857 		tTadoHome.Zones.push_back(_TadoZone);
858 	}
859 
860 	// Sort the zones based on Id (defined in structure) so we always get them in the same order.
861 	sort(tTadoHome.Zones.begin(), tTadoHome.Zones.end());
862 
863 	return true;
864 }
865 
866 // Sends a request to the Tado API.
SendToTadoApi(const eTadoApiMethod eMethod,const std::string & sUrl,const std::string & sPostData,std::string & sResponse,const std::vector<std::string> & vExtraHeaders,Json::Value & jsDecodedResponse,const bool bDecodeJsonResponse,const bool bIgnoreEmptyResponse,const bool bSendAuthHeaders)867 bool CTado::SendToTadoApi(const eTadoApiMethod eMethod, const std::string &sUrl, const std::string &sPostData,
868 				std::string &sResponse, const std::vector<std::string> & vExtraHeaders, Json::Value &jsDecodedResponse,
869 				const bool bDecodeJsonResponse, const bool bIgnoreEmptyResponse, const bool bSendAuthHeaders)
870 {
871 	try {
872 		// If there is no token stored then there is no point in doing a request. Unless we specifically
873 		// decide not to do authentication.
874 		if (m_TadoAuthToken.size() == 0 && bSendAuthHeaders) {
875 			_log.Log(LOG_ERROR, "Tado: No access token available.");
876 			return false;
877 		}
878 
879 		// Prepare the headers. Copy supplied vector.
880 		std::vector<std::string> _vExtraHeaders = vExtraHeaders;
881 
882 		// If the supplied postdata validates as json, add an appropriate content type header
883 		if (sPostData.size() > 0)
884 		{
885 			if (ParseJSon(sPostData, *(new Json::Value))) {
886 				_vExtraHeaders.push_back("Content-Type: application/json");
887 			}
888 		}
889 
890 		// Prepare the authentication headers if requested.
891 		if (bSendAuthHeaders) _vExtraHeaders.push_back("Authorization: Bearer " + m_TadoAuthToken);
892 
893 		std::vector<std::string> _vResponseHeaders;
894 		std::stringstream _ssResponseHeaderString;
895 
896 		switch (eMethod)
897 		{
898 			case Put:
899 				if (!HTTPClient::PUT(sUrl, sPostData, _vExtraHeaders, sResponse, bIgnoreEmptyResponse))
900 				{
901 					_log.Log(LOG_ERROR, "Tado: Failed to perform PUT request to Tado Api: %s", sResponse.c_str());
902 					return false;
903 				}
904 				break;
905 
906 			case Post:
907 				if (!HTTPClient::POST(sUrl, sPostData, _vExtraHeaders, sResponse, _vResponseHeaders, true, bIgnoreEmptyResponse))
908 				{
909 					for (unsigned int i = 0; i < _vResponseHeaders.size(); i++) _ssResponseHeaderString << _vResponseHeaders[i];
910 					_log.Log(LOG_ERROR, "Tado: Failed to perform POST request to Tado Api: %s; Response headers: %s", sResponse.c_str(), _ssResponseHeaderString.str().c_str());
911 					return false;
912 				}
913 				break;
914 
915 			case Get:
916 				if (!HTTPClient::GET(sUrl, _vExtraHeaders, sResponse, _vResponseHeaders, bIgnoreEmptyResponse))
917 				{
918 					for (unsigned int i = 0; i < _vResponseHeaders.size(); i++) _ssResponseHeaderString << _vResponseHeaders[i];
919 					_log.Log(LOG_ERROR, "Tado: Failed to perform GET request to Tado Api: %s; Response headers: %s", sResponse.c_str(), _ssResponseHeaderString.str().c_str());
920 					return false;
921 				}
922 				break;
923 
924 			case Delete:
925 				if (!HTTPClient::Delete(sUrl, sPostData, _vExtraHeaders, sResponse, bIgnoreEmptyResponse)) {
926 					{
927 						_log.Log(LOG_ERROR, "Tado: Failed to perform DELETE request to Tado Api: %s", sResponse.c_str());
928 						return false;
929 					}
930 				}
931 				break;
932 
933 			default:
934 				{
935 					_log.Log(LOG_ERROR, "Tado: Unknown method specified.");
936 					return false;
937 				}
938 		}
939 
940 		if (sResponse.size() == 0)
941 		{
942 			if (!bIgnoreEmptyResponse) {
943 				_log.Log(LOG_ERROR, "Tado: Received an empty response from Api.");
944 				return false;
945 			}
946 		}
947 
948 		if (bDecodeJsonResponse) {
949 			if (!ParseJSon(sResponse, jsDecodedResponse)) {
950 				_log.Log(LOG_ERROR, "Tado: Failed to decode Json response from Api.");
951 				return false;
952 			}
953 		}
954 	}
955 	catch (std::exception& e)
956 	{
957 		std::string what = e.what();
958 		_log.Log(LOG_ERROR, "Tado: Error sending information to Tado API: %s", what.c_str());
959 		return false;
960 	}
961 	return true;
962 }
963