1 #include "stdafx.h"
2 #include "AtagOne.h"
3 #include "../main/Helper.h"
4 #include "hardwaretypes.h"
5 #include "../main/localtime_r.h"
6 #include "../main/Logger.h"
7 #include "../main/WebServerHelper.h"
8 #include "../main/RFXtrx.h"
9 #include "../main/SQLHelper.h"
10 #include "../httpclient/HTTPClient.h"
11 #include "../main/mainworker.h"
12 #include "../main/json_helper.h"
13 
14 extern http::server::CWebServerHelper m_webservers;
15 
16 //Inspidred by https://github.com/kozmoz/atag-one-api
17 
18 #define ATAGONE_URL_LOGIN "https://portal.atag-one.com/Account/Login"
19 #define ATAGONE_URL_DEVICE_HOME "https://portal.atag-one.com/Home/Index/{0}"
20 #define ATAGONE_URL_DIAGNOSTICS "https://portal.atag-one.com/Device/LatestReport"
21 #define ATAGONE_URL_UPDATE_DEVICE_CONTROL "https://portal.atag-one.com/Home/UpdateDeviceControl/?deviceId={0}"
22 #define ATAGONE_URL_DEVICE_SET_SETPOINT "https://portal.atag-one.com/Home/DeviceSetSetpoint"
23 #define ATAGONE_URL_AUTOMATICMODE_CONTROL "https://portal.atag-one.com/Home/AutomaticMode/?deviceId={0}"
24 #define ATAGONE_URL_DEVICE_CONTROL "https://portal.atag-one.com/Home/DeviceControl/{0}"
25 
26 #define ATAGONE_TEMPERATURE_MIN 4
27 #define ATAGONE_TEMPERATURE_MAX 27
28 
29 #ifdef _DEBUG
30 	//#define DEBUG_AtagOneThermostat
31 #endif
32 
33 #ifdef DEBUG_AtagOneThermostat
SaveString2Disk(std::string str,std::string filename)34 void SaveString2Disk(std::string str, std::string filename)
35 {
36 	FILE *fOut = fopen(filename.c_str(), "wb+");
37 	if (fOut)
38 	{
39 		fwrite(str.c_str(), 1, str.size(), fOut);
40 		fclose(fOut);
41 	}
42 }
43 #endif
44 #ifdef DEBUG_AtagOneThermostat_read
ReadFile(std::string filename)45 std::string ReadFile(std::string filename)
46 {
47 	std::ifstream file;
48 	std::string sResult = "";
49 	file.open(filename.c_str());
50 	if (!file.is_open())
51 		return "";
52 	std::string sLine;
53 	while (!file.eof())
54 	{
55 		getline(file, sLine);
56 		sResult += sLine;
57 	}
58 	file.close();
59 	return sResult;
60 }
61 #endif
62 
CAtagOne(const int ID,const std::string & Username,const std::string & Password,const int Mode1,const int Mode2,const int Mode3,const int Mode4,const int Mode5,const int Mode6)63 CAtagOne::CAtagOne(const int ID, const std::string &Username, const std::string &Password, const int Mode1, const int Mode2, const int Mode3, const int Mode4, const int Mode5, const int Mode6):
64 m_UserName(Username),
65 m_Password(Password)
66 {
67 	stdstring_trim(m_UserName);
68 	stdstring_trim(m_Password);
69 	m_HwdID=ID;
70 	m_OutsideTemperatureIdx = 0; //use build in
71 	m_LastMinute = -1;
72 	m_ThermostatID = "";
73 	SetModes(Mode1, Mode2, Mode3, Mode4, Mode5, Mode6);
74 	Init();
75 }
76 
~CAtagOne(void)77 CAtagOne::~CAtagOne(void)
78 {
79 }
80 
SetModes(const int Mode1,const int,const int,const int,const int,const int)81 void CAtagOne::SetModes(const int Mode1, const int /*Mode2*/, const int /*Mode3*/, const int /*Mode4*/, const int /*Mode5*/, const int /*Mode6*/)
82 {
83 	m_OutsideTemperatureIdx = Mode1;
84 }
85 
Init()86 void CAtagOne::Init()
87 {
88 	m_ThermostatID = "";
89 	m_bDoLogin = true;
90 }
91 
StartHardware()92 bool CAtagOne::StartHardware()
93 {
94 	RequestStart();
95 
96 	Init();
97 
98 	m_LastMinute = -1;
99 	//Start worker thread
100 	m_thread = std::make_shared<std::thread>(&CAtagOne::Do_Work, this);
101 	SetThreadNameInt(m_thread->native_handle());
102 	m_bIsStarted=true;
103 	sOnConnected(this);
104 	return (m_thread != nullptr);
105 }
106 
StopHardware()107 bool CAtagOne::StopHardware()
108 {
109 	if (m_thread)
110 	{
111 		RequestStop();
112 		m_thread->join();
113 		m_thread.reset();
114 	}
115     m_bIsStarted=false;
116     return true;
117 }
118 
GetFirstDeviceID(const std::string & shtml)119 std::string GetFirstDeviceID(const std::string &shtml)
120 {
121 	std::string sResult = shtml;
122 	// Evsdd - Updated string due to webpage change
123 	// Original format: <tr onclick="javascript:changeDeviceAndRedirect('/Home/Index/{0}','6808-1401-3109_15-30-001-544');">
124 	// New format: "/Home/Index/6808-1401-3109_15-30-001-544"
125 	size_t tpos = sResult.find("/Home/Index");
126 	if (tpos == std::string::npos)
127 		return "";
128 	sResult = sResult.substr(tpos);
129 	tpos = sResult.find("x/");
130 	if (tpos == std::string::npos)
131 		return "";
132 	sResult = sResult.substr(tpos + 2);
133 	tpos = sResult.find("\"");
134 	if (tpos == std::string::npos)
135 		return "";
136 	sResult = sResult.substr(0, tpos);
137 	return sResult;
138 }
139 
GetRequestVerificationToken(const std::string & url)140 std::string CAtagOne::GetRequestVerificationToken(const std::string &url)
141 {
142 	std::string sResult;
143 #ifdef DEBUG_AtagOneThermostat_read
144 	sResult = ReadFile("E:\\AtagOne_requesttoken.txt");
145 #else
146 	std::string sURL = url;
147 	stdreplace(sURL,"{0}", m_ThermostatID);
148 
149 	if (!HTTPClient::GET(sURL, sResult))
150 	{
151 		Log(LOG_ERROR, "Error requesting token!");
152 		return "";
153 	}
154 #ifdef DEBUG_AtagOneThermostat
155 	SaveString2Disk(sResult, "E:\\AtagOne_requesttoken.txt");
156 #endif
157 #endif
158 	// <input name="__RequestVerificationToken" type="hidden" value="lFVlMZlt2-YJKAwZWS_K_p3gsQWjZOvBNBZ3lM8io_nFGFL0oRsj4YwQUdqGfyrEqGwEUPmm0FgKH1lPWdk257tuTWQ1" />
159 	size_t tpos = sResult.find("__RequestVerificationToken");
160 	if (tpos == std::string::npos)
161 	{
162 		tpos = sResult.find("changeDeviceAndRedirect");
163 		if (tpos != std::string::npos)
164 		{
165 			m_ThermostatID = GetFirstDeviceID(sResult);
166 		}
167 		return "";
168 	}
169 	sResult = sResult.substr(tpos);
170 	tpos = sResult.find("value=\"");
171 	if (tpos == std::string::npos)
172 		return "";
173 	sResult = sResult.substr(tpos+7);
174 	tpos = sResult.find("\"");
175 	if (tpos == std::string::npos)
176 		return "";
177 	sResult = sResult.substr(0,tpos);
178 	return sResult;
179 }
180 
Login()181 bool CAtagOne::Login()
182 {
183 	if (!m_ThermostatID.empty())
184 	{
185 		Logout();
186 	}
187 	if (m_UserName.empty())
188 		return false;
189 	m_ThermostatID = "";
190 
191 	std::string sResult;
192 
193 	// We need a session (cookie) and a verification token, get them first.
194 	std::string requestVerificationToken = GetRequestVerificationToken(ATAGONE_URL_LOGIN);
195 	if (requestVerificationToken.empty())
196 	{
197 		if (!m_ThermostatID.empty())
198 		{
199 			m_bDoLogin = false;
200 			return true;
201 		}
202 		Log(LOG_ERROR, "Error login!");
203 		return false;
204 	}
205 
206 #ifdef DEBUG_AtagOneThermostat_read
207 	sResult = ReadFile("E:\\AtagOne1.txt");
208 #else
209 	std::stringstream sstr;
210 	sstr
211 		<< "__RequestVerificationToken=" << requestVerificationToken
212 		<< "&Email=" << m_UserName
213 		<< "&Password=" << m_Password
214 		<< "&RememberMe=false";
215 	std::string szPostdata = sstr.str();
216 	std::vector<std::string> ExtraHeaders;
217 
218 	//# 1. Login
219 	std::string sURL;
220 	sURL = ATAGONE_URL_LOGIN;
221 	if (!HTTPClient::POST(sURL, szPostdata, ExtraHeaders, sResult))
222 	{
223 		Log(LOG_ERROR, "Error login!");
224 		return false;
225 	}
226 
227 #ifdef DEBUG_AtagOneThermostat
228 	SaveString2Disk(sResult, "E:\\AtagOne1.txt");
229 #endif
230 #endif
231 	//# 2. Extract DeviceID
232 	// <tr onclick="javascript:changeDeviceAndRedirect('/Home/Index/{0}','6808-1401-3109_15-30-001-544');">
233 	m_ThermostatID = GetFirstDeviceID(sResult);
234 	if (m_ThermostatID.empty())
235 	{
236 		Log(LOG_ERROR, "Error getting device_id!");
237 		return false;
238 	}
239 	m_bDoLogin = false;
240 	return true;
241 }
242 
Logout()243 void CAtagOne::Logout()
244 {
245 	if (m_bDoLogin)
246 		return; //we are not logged in
247 	m_ThermostatID = "";
248 	m_bDoLogin = true;
249 }
250 
251 
252 #define AtagOne_POLL_INTERVAL 60
253 
Do_Work()254 void CAtagOne::Do_Work()
255 {
256 	Log(LOG_STATUS,"Worker started...");
257 	int sec_counter = AtagOne_POLL_INTERVAL-5;
258 	while (!IsStopRequested(1000))
259 	{
260 		sec_counter++;
261 		if (sec_counter % 12 == 0) {
262 			m_LastHeartbeat=mytime(NULL);
263 		}
264 		if (sec_counter % AtagOne_POLL_INTERVAL == 0)
265 		{
266 			//SendOutsideTemperature();
267 			GetMeterDetails();
268 		}
269 	}
270 	Log(LOG_STATUS,"Worker stopped...");
271 }
272 
GetOutsideTemperatureFromDomoticz(float & tvalue)273 bool CAtagOne::GetOutsideTemperatureFromDomoticz(float &tvalue)
274 {
275 	if (m_OutsideTemperatureIdx == 0)
276 		return false;
277 	Json::Value tempjson;
278 	std::stringstream sstr;
279 	sstr << m_OutsideTemperatureIdx;
280 	m_webservers.GetJSonDevices(tempjson, "", "temp", "ID", sstr.str(), "", "", true, false, false, 0, "");
281 
282 	size_t tsize = tempjson.size();
283 	if (tsize < 1)
284 		return false;
285 
286 	Json::Value::const_iterator itt;
287 	Json::ArrayIndex rsize = tempjson["result"].size();
288 	if (rsize < 1)
289 		return false;
290 
291 	bool bHaveTimeout = tempjson["result"][0]["HaveTimeout"].asBool();
292 	if (bHaveTimeout)
293 		return false;
294 	tvalue = tempjson["result"][0]["Temp"].asFloat();
295 	return true;
296 }
297 
WriteToHardware(const char * pdata,const unsigned char)298 bool CAtagOne::WriteToHardware(const char *pdata, const unsigned char /*length*/)
299 {
300 	const tRBUF *pCmd = reinterpret_cast<const tRBUF *>(pdata);
301 	if (pCmd->LIGHTING2.packettype == pTypeLighting2)
302 	{
303 		//Light command
304 
305 		int node_id = pCmd->LIGHTING2.id4;
306 		bool bIsOn = (pCmd->LIGHTING2.cmnd == light2_sOn);
307 		if (node_id == 1)
308 		{
309 			//Pause Switch
310 			SetPauseStatus(bIsOn);
311 			return true;
312 		}
313 	}
314 	return false;
315 }
316 
GetHTMLPageValue(const std::string & hpage,const std::string & svalueLng1,const std::string & svalueLng2,const bool asFloat)317 static std::string GetHTMLPageValue(const std::string &hpage, const std::string &svalueLng1, const std::string &svalueLng2, const bool asFloat)
318 {
319 	std::vector<std::string > m_labels;
320 	if (!svalueLng1.empty())
321 		m_labels.push_back(svalueLng1);
322 	if (!svalueLng2.empty())
323 		m_labels.push_back(svalueLng2);
324 	// HTML structure of values in page.
325 	//     <label class="col-xs-6 control-label">Apparaat alias</label>
326 	//     <div class="col-xs-6">
327 	//         <p class="form-control-static">CV-ketel</p>
328 	//     </div>
329 	for (const auto & itt : m_labels)
330 	{
331 		std::string sresult = hpage;
332 		std::string sstring = ">" + itt + "</label>";
333 		size_t tpos = sresult.find(sstring);
334 		if (tpos==std::string::npos)
335 			continue;
336 		sresult = sresult.substr(tpos + sstring.size());
337 		tpos = sresult.find("<p");
338 		if (tpos == std::string::npos)
339 			continue;
340 		sresult = sresult.substr(tpos+2);
341 		tpos = sresult.find(">");
342 		if (tpos == std::string::npos)
343 			continue;
344 		sresult = sresult.substr(tpos + 1);
345 		tpos = sresult.find("<");
346 		if (tpos == std::string::npos)
347 			continue;
348 		sresult = sresult.substr(0,tpos);
349 		stdstring_trim(sresult);
350 
351 		if (asFloat)
352 			stdreplace(sresult, ",", ".");
353 		return sresult;
354 	}
355 	return "";
356 }
357 
GetMeterDetails()358 void CAtagOne::GetMeterDetails()
359 {
360 	if (m_UserName.empty() || m_Password.empty() )
361 		return;
362 
363 	if (m_bDoLogin)
364 	{
365 		if (!Login())
366 			return;
367 	}
368 
369 	std::string sResult;
370 #ifdef DEBUG_AtagOneThermostat_read
371 	sResult = ReadFile("E:\\AtagOne_getdiag.txt");
372 #else
373 	std::string sURL = std::string(ATAGONE_URL_DIAGNOSTICS) + "?deviceId=" + CURLEncode::URLEncode(m_ThermostatID);
374 	if (!HTTPClient::GET(sURL, sResult))
375 	{
376 		Log(LOG_ERROR, "Error getting thermostat data!");
377 		m_bDoLogin = true;
378 		return;
379 	}
380 
381 #ifdef DEBUG_AtagOneThermostat
382 	SaveString2Disk(sResult, "E:\\AtagOne_getdiag.txt");
383 #endif
384 #endif
385 	//Extract all values from the HTML page, and put them in a json array
386 	Json::Value root;
387 	std::string sret;
388 	sret = GetHTMLPageValue(sResult, "Kamertemperatuur", "Room temperature", true);
389 	if (sret.empty())
390 	{
391 		Log(LOG_ERROR, "Invalid/no data received...");
392 		return;
393 	}
394 	root["roomTemperature"] = static_cast<float>(atof(sret.c_str()));
395 	root["deviceAlias"] = GetHTMLPageValue(sResult, "Apparaat alias", "Device alias", false);
396 	root["latestReportTime"] = GetHTMLPageValue(sResult, "Laatste rapportagetijd", "Latest report time", false);
397 	root["connectedTo"] = GetHTMLPageValue(sResult, "Verbonden met", "Connected to", false);
398 	root["burningHours"] = static_cast<float>(atof(GetHTMLPageValue(sResult, "Branduren", "Burning hours", true).c_str()));
399 	root["boilerHeatingFor"] = GetHTMLPageValue(sResult, "Ketel in bedrijf voor", "Boiler heating for", false);
400 	sret= GetHTMLPageValue(sResult, "Brander status", "Flame status", false);
401 	root["flameStatus"] = ((sret == "Aan") || (sret == "On")) ? true : false;
402 	root["outsideTemperature"] = static_cast<float>(atof(GetHTMLPageValue(sResult, "Buitentemperatuur", "Outside temperature", true).c_str()));
403 	root["dhwSetpoint"] = static_cast<float>(atof(GetHTMLPageValue(sResult, "Setpoint warmwater", "DHW setpoint", true).c_str()));
404 	root["dhwWaterTemperature"] = static_cast<float>(atof(GetHTMLPageValue(sResult, "Warmwatertemperatuur", "DHW water temperature", true).c_str()));
405 	root["chSetpoint"] = static_cast<float>(atof(GetHTMLPageValue(sResult, "Setpoint cv", "CH setpoint", true).c_str()));
406 	root["chWaterTemperature"] = static_cast<float>(atof(GetHTMLPageValue(sResult, "CV-aanvoertemperatuur", "CH water temperature", true).c_str()));
407 	root["chWaterPressure"] = static_cast<float>(atof(GetHTMLPageValue(sResult, "CV-waterdruk", "CH water pressure", true).c_str()));
408 	root["chReturnTemperature"] = static_cast<float>(atof(GetHTMLPageValue(sResult, "CV retourtemperatuur", "CH return temperature", true).c_str()));
409 
410 #ifdef DEBUG_AtagOneThermostat_read
411 	sResult = ReadFile("E:\\AtagOne_gettargetsetpoint.txt");
412 #else
413 	// We have to do an extra call to get the target temperature.
414 	sURL = ATAGONE_URL_UPDATE_DEVICE_CONTROL;
415 	stdreplace(sURL, "{0}", CURLEncode::URLEncode(m_ThermostatID));
416 	if (!HTTPClient::GET(sURL, sResult))
417 	{
418 		Log(LOG_ERROR, "Error getting target setpoint data!");
419 		m_bDoLogin = true;
420 		return;
421 	}
422 #ifdef DEBUG_AtagOneThermostat
423 	SaveString2Disk(sResult, "E:\\AtagOne_gettargetsetpoint.txt");
424 #endif
425 #endif
426 	Json::Value root2;
427 	bool ret = ParseJSon(sResult, root2);
428 	if ((!ret) || (!root2.isObject()))
429 	{
430 		Log(LOG_ERROR, "Invalid/no data received...");
431 		return;
432 	}
433 	if (root2["targetTemp"].empty())
434 	{
435 		Log(LOG_ERROR, "Invalid/no data received...");
436 		return;
437 	}
438 	root["targetTemperature"] = static_cast<float>(atof(root2["targetTemp"].asString().c_str()));
439 	root["currentMode"] = root2["currentMode"].asString();
440 	root["vacationPlanned"] = root2["vacationPlanned"].asBool();
441 
442 	//Handle the Values
443 	float temperature;
444 	temperature = (float)root["targetTemperature"].asFloat();
445 	SendSetPointSensor(0, 0, 1, temperature, "Room Setpoint");
446 
447 	temperature = (float)root["roomTemperature"].asFloat();
448 	SendTempSensor(2, 255, temperature, "room Temperature");
449 
450 	if (!root["outsideTemperature"].empty())
451 	{
452 		temperature = (float)root["outsideTemperature"].asFloat();
453 		SendTempSensor(3, 255, temperature, "outside Temperature");
454 	}
455 
456 	//DHW
457 	if (!root["dhwSetpoint"].empty())
458 	{
459 		temperature = (float)root["dhwSetpoint"].asFloat();
460 		SendSetPointSensor(0, 0, 2, temperature, "DHW Setpoint");
461 	}
462 	if (!root["dhwWaterTemperature"].empty())
463 	{
464 		temperature = (float)root["dhwWaterTemperature"].asFloat();
465 		SendTempSensor(4, 255, temperature, "DHW Temperature");
466 	}
467 	//CH
468 	if (!root["chSetpoint"].empty())
469 	{
470 		temperature = (float)root["chSetpoint"].asFloat();
471 		SendSetPointSensor(0, 0, 3, temperature, "CH Setpoint");
472 	}
473 	if (!root["chWaterTemperature"].empty())
474 	{
475 		temperature = (float)root["chWaterTemperature"].asFloat();
476 		SendTempSensor(5, 255, temperature, "CH Temperature");
477 	}
478 	if (!root["chWaterPressure"].empty())
479 	{
480 		float pressure = (float)root["chWaterPressure"].asFloat();
481 		SendPressureSensor(1, 1, 255, pressure, "Pressure");
482 	}
483 	if (!root["chReturnTemperature"].empty())
484 	{
485 		temperature = (float)root["chReturnTemperature"].asFloat();
486 		SendTempSensor(6, 255, temperature, "CH Return Temperature");
487 	}
488 
489 	if (!root["currentMode"].empty())
490 	{
491 		std::string actSource = root["currentMode"].asString();
492 		bool bIsScheduleMode = (actSource == "schedule_active");
493 		SendSwitch(1, 1, 255, bIsScheduleMode, 0, "Thermostat Schedule Mode");
494 	}
495 	if (!root["flameStatus"].empty())
496 	{
497 		SendSwitch(2, 1, 255, root["flameStatus"].asBool(), 0, "Flame Status");
498 	}
499 
500 }
501 
SetSetpoint(const int idx,const float temp)502 void CAtagOne::SetSetpoint(const int idx, const float temp)
503 {
504 	if (idx != 1)
505 	{
506 		Log(LOG_ERROR, "Currently only Room Temperature Setpoint allowed!");
507 		return;
508 	}
509 
510 	int rtemp = int(temp*2.0f);
511 	float dtemp = float(rtemp) / 2.0f;
512 	if (
513 		(dtemp<ATAGONE_TEMPERATURE_MIN) ||
514 		(dtemp>ATAGONE_TEMPERATURE_MAX)
515 		)
516 	{
517 		Log(LOG_ERROR, "Temperature should be between %d and %d", ATAGONE_TEMPERATURE_MIN, ATAGONE_TEMPERATURE_MAX);
518 		return;
519 	}
520 	char szTemp[20];
521 	sprintf(szTemp, "%.1f", dtemp);
522 	std::string sTemp = szTemp;
523 
524 	// Get updated request verification token first.
525 	std::string  requestVerificationToken = GetRequestVerificationToken(ATAGONE_URL_DEVICE_HOME);
526 
527 	// https://portal.atag-one.com/Home/DeviceSetSetpoint/6808-1401-3109_15-30-001-544?temperature=18.5
528 	std::string sURL = std::string(ATAGONE_URL_DEVICE_SET_SETPOINT) + "/" + m_ThermostatID + "?temperature=" + sTemp;
529 
530 	std::stringstream sstr;
531 	if (!requestVerificationToken.empty())
532 	{
533 		sstr << "__RequestVerificationToken=" << requestVerificationToken;
534 	}
535 	std::string szPostdata = sstr.str();
536 	std::vector<std::string> ExtraHeaders;
537 	std::string sResult;
538 	if (!HTTPClient::POST(sURL, szPostdata, ExtraHeaders, sResult))
539 	{
540 		Log(LOG_ERROR, "Error setting Setpoint!");
541 		return;
542 	}
543 #ifdef DEBUG_AtagOneThermostat
544 	SaveString2Disk(sResult, "E:\\AtagOne_setsetpoint.txt");
545 #endif
546 	SendSetPointSensor(0,0, (const uint8_t)idx, dtemp, "");
547 }
548 
SetPauseStatus(const bool)549 void CAtagOne::SetPauseStatus(const bool /*bIsPause*/)
550 {
551 }
552 
SendOutsideTemperature()553 void CAtagOne::SendOutsideTemperature()
554 {
555 	float temp;
556 	if (!GetOutsideTemperatureFromDomoticz(temp))
557 		return;
558 	SetOutsideTemp(temp);
559 }
560 
SetOutsideTemp(const float)561 void CAtagOne::SetOutsideTemp(const float /*temp*/)
562 {
563 }
564 
565