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