1 #include "stdafx.h"
2 #include "Honeywell.h"
3 #include "../main/Helper.h"
4 #include "../main/Logger.h"
5 #include "hardwaretypes.h"
6 #include "../main/localtime_r.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 #include "../webserver/Base64.h"
14
15 #define round(a) ( int ) ( a + .5 )
16
17 const std::string HONEYWELL_DEFAULT_APIKEY = "atD3jtzXC5z4X8WPbzvo0CBqWi7S81Nh";
18 const std::string HONEYWELL_DEFAULT_APISECRET = "TXDzy2aHpAJw6YiO";
19 const std::string HONEYWELL_LOCATIONS_PATH = "https://api.honeywell.com/v2/locations?apikey=[apikey]";
20 const std::string HONEYWELL_UPDATE_THERMOSTAT = "https://api.honeywell.com/v2/devices/thermostats/[deviceid]?apikey=[apikey]&locationId=[locationid]";
21 const std::string HONEYWELL_TOKEN_PATH = "https://api.honeywell.com/oauth2/token";
22
23 const std::string kHeatSetPointDesc = "Target temperature ([devicename])";
24 const std::string kHeatingDesc = "Heating ([devicename])";
25 const std::string kOperationStatusDesc = "Heating state ([devicename])";
26 const std::string kOutdoorTempDesc = "Outdoor temperature ([devicename])";
27 const std::string kRoomTempDesc = "Room temperature ([devicename])";
28 const std::string kAwayDesc = "Away ([name])";
29
30 extern http::server::CWebServerHelper m_webservers;
31
CHoneywell(const int ID,const std::string & Username,const std::string & Password,const std::string & Extra)32 CHoneywell::CHoneywell(const int ID, const std::string &Username, const std::string &Password, const std::string &Extra)
33 {
34 m_HwdID = ID;
35 mAccessToken = Username;
36 mRefreshToken = Password;
37 stdstring_trim(mAccessToken);
38 stdstring_trim(mRefreshToken);
39
40 // get the data from the extradata field
41 std::vector<std::string> strextra;
42 StringSplit(Extra, "|", strextra);
43 if (strextra.size() == 2)
44 {
45 mApiKey = base64_decode(strextra[0]);
46 mApiSecret = base64_decode(strextra[1]);
47 }
48 if (mApiKey.empty()) {
49 _log.Log(LOG_STATUS, "Honeywell: No API key was set. Using default API key. This will result in many errors caused by quota limitations.");
50 mApiKey = HONEYWELL_DEFAULT_APIKEY;
51 mApiSecret = HONEYWELL_DEFAULT_APISECRET;
52 }
53 if (Username.empty() || Password.empty()) {
54 _log.Log(LOG_ERROR, "Honeywell: Please update your access token/request token!...");
55 }
56 mLastMinute = -1;
57 Init();
58 }
59
~CHoneywell(void)60 CHoneywell::~CHoneywell(void)
61 {
62 }
63
Init()64 void CHoneywell::Init()
65 {
66 mTokenExpires = mytime(NULL);
67 }
68
StartHardware()69 bool CHoneywell::StartHardware()
70 {
71 RequestStart();
72
73 Init();
74 mLastMinute = -1;
75 //Start worker thread
76 m_thread = std::make_shared<std::thread>(&CHoneywell::Do_Work, this);
77 SetThreadNameInt(m_thread->native_handle());
78 mIsStarted = true;
79 sOnConnected(this);
80 return (m_thread != nullptr);
81 }
82
StopHardware()83 bool CHoneywell::StopHardware()
84 {
85 if (m_thread)
86 {
87 RequestStop();
88 m_thread->join();
89 m_thread.reset();
90 }
91
92 mIsStarted = false;
93 return true;
94 }
95
96 #define HONEYWELL_POLL_INTERVAL 300 // 5 minutes
97 #define HWAPITIMEOUT 30 // 30 seconds
98
99 //
100 // worker thread
101 //
Do_Work()102 void CHoneywell::Do_Work()
103 {
104 _log.Log(LOG_STATUS, "Honeywell: Worker started...");
105 int sec_counter = HONEYWELL_POLL_INTERVAL - 5;
106 while (!IsStopRequested(1000))
107 {
108 sec_counter++;
109 if (sec_counter % 12 == 0) {
110 m_LastHeartbeat = mytime(NULL);
111 }
112 if (sec_counter % HONEYWELL_POLL_INTERVAL == 0)
113 {
114 GetThermostatData();
115 }
116 }
117 _log.Log(LOG_STATUS, "Honeywell: Worker stopped...");
118 }
119
120
121 //
122 // callback from Domoticz backend to update the Honeywell thermostat
123 //
WriteToHardware(const char * pdata,const unsigned char)124 bool CHoneywell::WriteToHardware(const char *pdata, const unsigned char /*length*/)
125 {
126 const tRBUF *pCmd = reinterpret_cast<const tRBUF *>(pdata);
127 if (pCmd->LIGHTING2.packettype == pTypeLighting2)
128 {
129 //Light command
130
131 int nodeID = pCmd->LIGHTING2.id4;
132 int devID = nodeID / 10;
133 std::string deviceName = mDeviceList[devID]["name"].asString();
134
135
136 bool bIsOn = (pCmd->LIGHTING2.cmnd == light2_sOn);
137 if ((nodeID % 10) == 3) {
138 // heating on or off
139 SetPauseStatus(devID, bIsOn, nodeID);
140 return true;
141 }
142
143 }
144 else if (pCmd->ICMND.packettype == pTypeThermostat && pCmd->LIGHTING2.subtype == sTypeThermSetpoint)
145 {
146 int nodeID = pCmd->LIGHTING2.id4;
147 int devID = nodeID / 10;
148 const _tThermostat *therm = reinterpret_cast<const _tThermostat*>(pdata);
149 SetSetpoint(devID, therm->temp, nodeID);
150 }
151 return false;
152 }
153
154 //
155 // refresh the OAuth2 token through Honeywell API
156 //
refreshToken()157 bool CHoneywell::refreshToken()
158 {
159 if (mRefreshToken.empty())
160 return false;
161
162 if (mTokenExpires > mytime(NULL))
163 return true;
164
165 std::string sResult;
166
167 std::string postData = "grant_type=refresh_token&refresh_token=[refreshToken]";
168 stdreplace(postData, "[refreshToken]", mRefreshToken);
169
170 std::string auth = mApiKey;
171 auth += ":";
172 auth += mApiSecret;
173 std::string encodedAuth = base64_encode(auth);
174
175
176 std::vector<std::string> headers;
177 std::string authHeader = "Authorization: [auth]";
178 stdreplace(authHeader, "[auth]", encodedAuth);
179 headers.push_back(authHeader);
180 headers.push_back("Content-Type: application/x-www-form-urlencoded");
181
182 HTTPClient::SetConnectionTimeout(HWAPITIMEOUT);
183 HTTPClient::SetTimeout(HWAPITIMEOUT);
184 if (!HTTPClient::POST(HONEYWELL_TOKEN_PATH, postData, headers, sResult)) {
185 _log.Log(LOG_ERROR, "Honeywell: Error refreshing token");
186 return false;
187 }
188
189 Json::Value root;
190 bool ret = ParseJSon(sResult, root);
191 if (!ret) {
192 _log.Log(LOG_ERROR, "Honeywell: Invalid/no data received...");
193 return false;
194 }
195
196 std::string at = root["access_token"].asString();
197 std::string rt = root["refresh_token"].asString();
198 std::string ei = root["expires_in"].asString();
199 if (at.length() && rt.length() && ei.length()) {
200 int expires_in = std::stoi(ei);
201 mTokenExpires = mytime(NULL) + (expires_in > 0 ? expires_in : 600) - HWAPITIMEOUT;
202 mAccessToken = at;
203 mRefreshToken = rt;
204 _log.Log(LOG_NORM, "Honeywell: Storing received access & refresh token. Token expires after %d seconds.",expires_in);
205 m_sql.safe_query("UPDATE Hardware SET Username='%q', Password='%q' WHERE (ID==%d)", mAccessToken.c_str(), mRefreshToken.c_str(), m_HwdID);
206 mSessionHeaders.clear();
207 mSessionHeaders.push_back("Authorization:Bearer " + mAccessToken);
208 mSessionHeaders.push_back("Content-Type: application/json");
209 }
210 else
211 return false;
212
213 return true;
214 }
215
216 //
217 // Get honeywell data through Honeywell API
218 //
GetThermostatData()219 void CHoneywell::GetThermostatData()
220 {
221 if (!refreshToken())
222 return;
223
224 std::string sResult;
225 std::string sURL = HONEYWELL_LOCATIONS_PATH;
226 stdreplace(sURL, "[apikey]", mApiKey);
227
228 HTTPClient::SetConnectionTimeout(HWAPITIMEOUT);
229 HTTPClient::SetTimeout(HWAPITIMEOUT);
230 if (!HTTPClient::GET(sURL, mSessionHeaders, sResult)) {
231 _log.Log(LOG_ERROR, "Honeywell: Error getting thermostat data!");
232 return;
233 }
234
235 Json::Value root;
236 bool ret = ParseJSon(sResult, root);
237 if (!ret) {
238 _log.Log(LOG_ERROR, "Honeywell: Invalid/no data received...");
239 return;
240 }
241
242 int devNr = 0;
243 mDeviceList.clear();
244 mLocationList.clear();
245 for (int locCnt = 0; locCnt < (int)root.size(); locCnt++)
246 {
247 Json::Value location = root[locCnt];
248 Json::Value devices = location["devices"];
249 for (int devCnt = 0; devCnt < (int)devices.size(); devCnt++)
250 {
251 Json::Value device = devices[devCnt];
252 std::string deviceName = device["name"].asString();
253 mDeviceList[devNr] = device;
254 mLocationList[devNr] = location["locationID"].asString();
255
256 float temperature;
257 temperature = (float)device["indoorTemperature"].asFloat();
258 std::string desc = kRoomTempDesc;
259 stdreplace(desc, "[devicename]", deviceName);
260 SendTempSensor(10 * devNr + 1, 255, temperature, desc);
261
262 temperature = (float)device["outdoorTemperature"].asFloat();
263 desc = kOutdoorTempDesc;
264 stdreplace(desc, "[devicename]", deviceName);
265 SendTempSensor(10 * devNr + 2, 255, temperature, desc);
266
267 std::string mode = device["changeableValues"]["mode"].asString();
268 bool bHeating = (mode == "Heat");
269 desc = kHeatingDesc;
270 stdreplace(desc, "[devicename]", deviceName);
271 SendSwitch(10 * devNr + 3, 1, 255, bHeating, 0, desc);
272
273 temperature = (float)device["changeableValues"]["heatSetpoint"].asFloat();
274 desc = kHeatSetPointDesc;
275 stdreplace(desc, "[devicename]", deviceName);
276 SendSetPointSensor((uint8_t)(10 * devNr + 4), temperature, desc);
277 devNr++;
278
279 std::string operationstatus = device["operationStatus"]["mode"].asString();
280 bool bStatus = (operationstatus != "EquipmentOff");
281 desc = kOperationStatusDesc;
282 stdreplace(desc, "[devicename]", deviceName);
283 SendSwitch(10 * devNr + 5, 1, 255, bStatus, 0, desc);
284 }
285
286 bool geoFenceEnabled = location["geoFenceEnabled"].asBool();
287 if (geoFenceEnabled) {
288
289 Json::Value geofences = location["geoFences"];
290 bool bAway = true;
291 for (int geofCnt = 0; geofCnt < (int)geofences.size(); geofCnt++)
292 {
293 int withinFence = geofences[geofCnt]["geoOccupancy"]["withinFence"].asInt();
294 if (withinFence > 0) {
295 bAway = false;
296 break;
297 }
298 }
299 std::string desc = kAwayDesc;
300 stdreplace(desc, "[name]", location["name"].asString());
301 SendSwitch(10 * devNr + 6, 1, 255, bAway, 0, desc);
302 }
303 }
304 }
305
306 //
307 // send the temperature from honeywell to domoticz backend
308 //
SendSetPointSensor(const unsigned char Idx,const float Temp,const std::string & defaultname)309 void CHoneywell::SendSetPointSensor(const unsigned char Idx, const float Temp, const std::string &defaultname)
310 {
311 _tThermostat thermos;
312 thermos.subtype = sTypeThermSetpoint;
313 thermos.id1 = 0;
314 thermos.id2 = 0;
315 thermos.id3 = 0;
316 thermos.id4 = Idx;
317 thermos.dunit = 0;
318
319 thermos.temp = Temp;
320
321 sDecodeRXMessage(this, (const unsigned char *)&thermos, defaultname.c_str(), 255);
322 }
323
324 //
325 // transfer pause status to Honeywell api
326 //
SetPauseStatus(const int idx,bool bHeating,const int)327 void CHoneywell::SetPauseStatus(const int idx, bool bHeating, const int /*nodeid*/)
328 {
329 if (!refreshToken()) {
330 _log.Log(LOG_ERROR,"Honeywell: No token available. Failed setting thermostat data");
331 return;
332 }
333
334 std::string url = HONEYWELL_UPDATE_THERMOSTAT;
335 std::string deviceID = mDeviceList[idx]["deviceID"].asString();
336
337 stdreplace(url, "[deviceid]", deviceID);
338 stdreplace(url, "[apikey]", mApiKey);
339 stdreplace(url, "[locationid]", mLocationList[idx]);
340
341 Json::Value reqRoot;
342 reqRoot["mode"] = bHeating ? "Heat" : "Off";
343 reqRoot["heatSetpoint"] = mDeviceList[idx]["changeableValues"]["coolHeatpoint"].asInt();
344 reqRoot["coolSetpoint"] = mDeviceList[idx]["changeableValues"]["coolSetpoint"].asInt();
345 reqRoot["thermostatSetpointStatus"] = "TemporaryHold";
346
347 std::string sResult;
348 HTTPClient::SetConnectionTimeout(HWAPITIMEOUT);
349 HTTPClient::SetTimeout(HWAPITIMEOUT);
350 if (!HTTPClient::POST(url, JSonToRawString(reqRoot), mSessionHeaders, sResult, true, true)) {
351 _log.Log(LOG_ERROR, "Honeywell: Error setting thermostat data!");
352 return;
353 }
354
355 std::string desc = kHeatingDesc;
356 stdreplace(desc, "[devicename]", mDeviceList[idx]["name"].asString());
357 SendSwitch(10 * idx + 3, 1, 255, bHeating, 0, desc);
358 }
359
360 //
361 // transfer the updated temperature to Honeywell API
362 //
SetSetpoint(const int idx,const float temp,const int)363 void CHoneywell::SetSetpoint(const int idx, const float temp, const int /*nodeid*/)
364 {
365 if (!refreshToken()) {
366 _log.Log(LOG_ERROR, "Honeywell: No token available. Error setting thermostat data!");
367 return;
368 }
369
370 std::string url = HONEYWELL_UPDATE_THERMOSTAT;
371 std::string deviceID = mDeviceList[idx]["deviceID"].asString();
372
373 stdreplace(url, "[deviceid]", deviceID);
374 stdreplace(url, "[apikey]", mApiKey);
375 stdreplace(url, "[locationid]", mLocationList[idx]);
376
377 Json::Value reqRoot;
378 reqRoot["mode"] = "Heat";
379 reqRoot["heatSetpoint"] = temp;
380 reqRoot["coolSetpoint"] = mDeviceList[idx]["changeableValues"]["coolSetpoint"].asInt();
381 reqRoot["thermostatSetpointStatus"] = "TemporaryHold";
382
383 std::string sResult;
384 HTTPClient::SetConnectionTimeout(HWAPITIMEOUT);
385 HTTPClient::SetTimeout(HWAPITIMEOUT);
386 if (!HTTPClient::POST(url, JSonToRawString(reqRoot), mSessionHeaders, sResult, true, true)) {
387 _log.Log(LOG_ERROR, "Honeywell: Error setting thermostat data!");
388 return;
389 }
390
391 std::string desc = kHeatSetPointDesc;
392 stdreplace(desc, "[devicename]", mDeviceList[idx]["name"].asString());
393 SendSetPointSensor((uint8_t)(10 * idx + 4), temp, desc);
394
395 desc = kHeatingDesc;
396 stdreplace(desc, "[devicename]", mDeviceList[idx]["name"].asString());
397 SendSwitch(10 * idx + 3, 1, 255, true, 0, desc);
398 }
399