Skip to content

Commit 772fb61

Browse files
authored
Merge pull request #2202 from tnys/development
Add support for Honeywell Lyric thermostat
2 parents f4e68d7 + 7462782 commit 772fb61

File tree

9 files changed

+468
-4
lines changed

9 files changed

+468
-4
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ hardware/Gpio.cpp
345345
hardware/GpioPin.cpp
346346
hardware/HardwareMonitor.cpp
347347
hardware/HarmonyHub.cpp
348+
hardware/Honeywell.cpp
348349
hardware/HEOS.cpp
349350
hardware/I2C.cpp
350351
hardware/ICYThermostat.cpp

hardware/Honeywell.cpp

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
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 "../json/json.h"
13+
#include "../webserver/Base64.h"
14+
15+
#define round(a) ( int ) ( a + .5 )
16+
17+
const std::string HONEYWELL_APIKEY = "atD3jtzXC5z4X8WPbzvo0CBqWi7S81Nh";
18+
const std::string HONEYWELL_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 kOutdoorTempDesc = "Outdoor temperature ([devicename])";
26+
const std::string kRoomTempDesc = "Room temperature ([devicename])";
27+
28+
extern http::server::CWebServerHelper m_webservers;
29+
30+
CHoneywell::CHoneywell(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) {
31+
if (Username.empty() || Password.empty()) {
32+
_log.Log(LOG_ERROR, "Honeywell: Please update your access token/request token!...");
33+
}
34+
else {
35+
mAccessToken = Username;
36+
mRefreshToken = Password;
37+
stdstring_trim(mAccessToken);
38+
stdstring_trim(mRefreshToken);
39+
}
40+
41+
m_HwdID = ID;
42+
Init();
43+
44+
}
45+
46+
CHoneywell::~CHoneywell(void) {
47+
}
48+
49+
void CHoneywell::Init() {
50+
mStopRequested = false;
51+
mNeedsTokenRefresh = true;
52+
}
53+
54+
bool CHoneywell::StartHardware() {
55+
Init();
56+
mLastMinute = -1;
57+
//Start worker thread
58+
mThread = boost::shared_ptr<boost::thread>(new boost::thread(boost::bind(&CHoneywell::Do_Work, this)));
59+
mIsStarted=true;
60+
sOnConnected(this);
61+
return (mThread!=NULL);
62+
}
63+
64+
bool CHoneywell::StopHardware() {
65+
if (mThread!=NULL)
66+
{
67+
assert(mThread);
68+
mStopRequested = true;
69+
mThread->join();
70+
}
71+
72+
mIsStarted=false;
73+
return true;
74+
}
75+
76+
#define HONEYWELL_POLL_INTERVAL 300 // 5 minutes
77+
78+
//
79+
// worker thread
80+
//
81+
void CHoneywell::Do_Work() {
82+
_log.Log(LOG_STATUS,"Honeywell: Worker started...");
83+
int sec_counter = HONEYWELL_POLL_INTERVAL-5;
84+
while (!mStopRequested)
85+
{
86+
sleep_seconds(1);
87+
sec_counter++;
88+
if (sec_counter % 12 == 0) {
89+
m_LastHeartbeat=mytime(NULL);
90+
}
91+
if (sec_counter % HONEYWELL_POLL_INTERVAL == 0)
92+
{
93+
GetMeterDetails();
94+
}
95+
}
96+
_log.Log(LOG_STATUS,"Honeywell: Worker stopped...");
97+
}
98+
99+
100+
//
101+
// callback from Domoticz backend to update the Honeywell thermostat
102+
//
103+
bool CHoneywell::WriteToHardware(const char *pdata, const unsigned char length) {
104+
const tRBUF *pCmd = reinterpret_cast<const tRBUF *>(pdata);
105+
if (pCmd->LIGHTING2.packettype == pTypeLighting2)
106+
{
107+
//Light command
108+
109+
int nodeID = pCmd->LIGHTING2.id4;
110+
int devID = nodeID / 10;
111+
std::string deviceName = mDeviceList[devID]["name"].asString();
112+
113+
114+
bool bIsOn = (pCmd->LIGHTING2.cmnd == light2_sOn);
115+
if ((nodeID % 10) == 3) {
116+
// heating on or off
117+
SetPauseStatus(devID, bIsOn, nodeID);
118+
return true;
119+
}
120+
121+
}
122+
else if (pCmd->ICMND.packettype == pTypeThermostat && pCmd->LIGHTING2.subtype == sTypeThermSetpoint)
123+
{
124+
int nodeID = pCmd->LIGHTING2.id4;
125+
int devID = nodeID / 10;
126+
const _tThermostat *therm = reinterpret_cast<const _tThermostat*>(pdata);
127+
float temp = therm->temp;
128+
int calculatedTemp = (int)temp;
129+
130+
SetSetpoint(devID, temp, nodeID);
131+
}
132+
return false;
133+
}
134+
135+
//
136+
// refresh the OAuth2 token through Honeywell API
137+
//
138+
bool CHoneywell::refreshToken() {
139+
if (mRefreshToken.empty())
140+
return false;
141+
142+
std::string sResult;
143+
144+
std::string postData = "grant_type=refresh_token&refresh_token=[refreshToken]";
145+
stdreplace(postData, "[refreshToken]", mRefreshToken);
146+
147+
std::string auth = HONEYWELL_APIKEY;
148+
auth += ":";
149+
auth += HONEYWELL_APISECRET;
150+
std::string encodedAuth = base64_encode((const unsigned char *)auth.c_str(), auth.length());
151+
152+
153+
std::vector<std::string> headers;
154+
std::string authHeader = "Authorization: [auth]";
155+
stdreplace(authHeader, "[auth]", encodedAuth);
156+
headers.push_back(authHeader);
157+
headers.push_back("Content-Type: application/x-www-form-urlencoded");
158+
159+
if (!HTTPClient::POST(HONEYWELL_TOKEN_PATH, postData, headers, sResult)) {
160+
_log.Log(LOG_ERROR, "Honeywell: Error refreshing token");
161+
mNeedsTokenRefresh = true;
162+
return false;
163+
}
164+
165+
Json::Value root;
166+
Json::Reader jReader;
167+
bool ret = jReader.parse(sResult, root);
168+
if (!ret) {
169+
_log.Log(LOG_ERROR, "Honeywell: Invalid/no data received...");
170+
mNeedsTokenRefresh = true;
171+
return false;
172+
}
173+
174+
std::string at = root["access_token"].asString();
175+
std::string rt = root["refresh_token"].asString();
176+
if (at.length() && rt.length()) {
177+
mAccessToken = at;
178+
mRefreshToken = rt;
179+
_log.Log(LOG_NORM, "Honeywell: Storing received access & refresh token");
180+
m_sql.safe_query("UPDATE Hardware SET Username='%q', Password='%q' WHERE (ID==%d)", mAccessToken.c_str(), mRefreshToken.c_str(), m_HwdID);
181+
mNeedsTokenRefresh = false;
182+
mSessionHeaders.clear();
183+
mSessionHeaders.push_back("Authorization:Bearer " + mAccessToken);
184+
mSessionHeaders.push_back("Content-Type: application/json");
185+
}
186+
else
187+
return false;
188+
189+
return true;
190+
}
191+
192+
//
193+
// Get honeywell data through Honeywell API
194+
//
195+
void CHoneywell::GetMeterDetails() {
196+
197+
if (mNeedsTokenRefresh) {
198+
if (!refreshToken())
199+
return;
200+
}
201+
202+
std::string sResult;
203+
std::string sURL = HONEYWELL_LOCATIONS_PATH;
204+
stdreplace(sURL, "[apikey]", HONEYWELL_APIKEY);
205+
if (!HTTPClient::GET(sURL, mSessionHeaders, sResult)) {
206+
_log.Log(LOG_ERROR, "Honeywell: Error getting thermostat data!");
207+
mNeedsTokenRefresh = true;
208+
return;
209+
}
210+
211+
Json::Value root;
212+
Json::Reader jReader;
213+
bool ret = jReader.parse(sResult, root);
214+
if (!ret) {
215+
_log.Log(LOG_ERROR, "Honeywell: Invalid/no data received...");
216+
mNeedsTokenRefresh = true;
217+
return;
218+
}
219+
220+
int devNr = 0;
221+
mDeviceList.clear();
222+
mLocationList.clear();
223+
for (int locCnt = 0; locCnt < root.size(); locCnt++) {
224+
Json::Value location = root[locCnt];
225+
Json::Value devices = location["devices"];
226+
for (int devCnt = 0; devCnt < devices.size(); devCnt++) {
227+
Json::Value device = devices[devCnt];
228+
std::string deviceName = device["name"].asString();
229+
mDeviceList[devNr] = device;
230+
mLocationList[devNr] = location["locationID"].asString();
231+
232+
float temperature;
233+
temperature = (float)device["indoorTemperature"].asFloat();
234+
std::string desc = kRoomTempDesc;
235+
stdreplace(desc, "[devicename]", deviceName);
236+
SendTempSensor(10*devNr + 1, 255, temperature, desc);
237+
238+
temperature = (float)device["outdoorTemperature"].asFloat();
239+
desc = kOutdoorTempDesc;
240+
stdreplace(desc, "[devicename]", deviceName);
241+
SendTempSensor(10*devNr + 2, 255, temperature, desc);
242+
243+
std::string mode = device["changeableValues"]["mode"].asString();
244+
bool bHeating = (mode == "Heat");
245+
desc = kHeatingDesc;
246+
stdreplace(desc, "[devicename]", deviceName);
247+
SendSwitch(10*devNr + 3, 1, 255, bHeating, 0, desc);
248+
249+
temperature = (float)device["changeableValues"]["heatSetpoint"].asFloat();
250+
desc = kHeatSetPointDesc;
251+
stdreplace(desc, "[devicename]", deviceName);
252+
SendSetPointSensor(10*devNr + 4, temperature, desc);
253+
devNr++;
254+
}
255+
}
256+
}
257+
258+
//
259+
// send the temperature from honeywell to domoticz backend
260+
//
261+
void CHoneywell::SendSetPointSensor(const unsigned char Idx, const float Temp, const std::string &defaultname)
262+
{
263+
_tThermostat thermos;
264+
thermos.subtype=sTypeThermSetpoint;
265+
thermos.id1=0;
266+
thermos.id2=0;
267+
thermos.id3=0;
268+
thermos.id4=Idx;
269+
thermos.dunit=0;
270+
271+
thermos.temp=Temp;
272+
273+
sDecodeRXMessage(this, (const unsigned char *)&thermos, defaultname.c_str(), 255);
274+
}
275+
276+
//
277+
// transfer pause status to Honeywell api
278+
//
279+
void CHoneywell::SetPauseStatus(const int idx, bool bHeating, const int nodeid)
280+
{
281+
std::string url = HONEYWELL_UPDATE_THERMOSTAT;
282+
std::string deviceID = mDeviceList[idx]["deviceID"].asString();
283+
284+
stdreplace(url, "[deviceid]", deviceID);
285+
stdreplace(url, "[apikey]", HONEYWELL_APIKEY);
286+
stdreplace(url, "[locationid]", mLocationList[idx]);
287+
288+
Json::Value reqRoot;
289+
reqRoot["mode"] = bHeating ? "Heat" : "Off";
290+
reqRoot["heatSetpoint"] = mDeviceList[idx]["changeableValues"]["coolHeatpoint"].asInt();
291+
reqRoot["coolSetpoint"] = mDeviceList[idx]["changeableValues"]["coolSetpoint"].asInt();
292+
reqRoot["thermostatSetpointStatus"] = "TemporaryHold";
293+
Json::FastWriter writer;
294+
295+
std::string sResult;
296+
if (!HTTPClient::POST(url, writer.write(reqRoot), mSessionHeaders, sResult, true, true)) {
297+
_log.Log(LOG_ERROR, "Honeywell: Error setting thermostat data!");
298+
return;
299+
}
300+
301+
std::string desc = kHeatingDesc;
302+
stdreplace(desc, "[devicename]", mDeviceList[idx]["name"].asString());
303+
SendSwitch(10*idx + 3, 1, 255, bHeating, 0, desc);
304+
}
305+
306+
//
307+
// transfer the updated temperature to Honeywell API
308+
//
309+
void CHoneywell::SetSetpoint(const int idx, const float temp, const int nodeid)
310+
{
311+
std::string url = HONEYWELL_UPDATE_THERMOSTAT;
312+
std::string deviceID = mDeviceList[idx]["deviceID"].asString();
313+
314+
stdreplace(url, "[deviceid]", deviceID);
315+
stdreplace(url, "[apikey]", HONEYWELL_APIKEY);
316+
stdreplace(url, "[locationid]", mLocationList[idx]);
317+
318+
Json::Value reqRoot;
319+
reqRoot["mode"] = "Heat";
320+
reqRoot["heatSetpoint"] = temp;
321+
reqRoot["coolSetpoint"] = mDeviceList[idx]["changeableValues"]["coolSetpoint"].asInt();
322+
reqRoot["thermostatSetpointStatus"] = "TemporaryHold";
323+
Json::FastWriter writer;
324+
325+
std::string sResult;
326+
if (!HTTPClient::POST(url, writer.write(reqRoot), mSessionHeaders, sResult, true, true)) {
327+
_log.Log(LOG_ERROR, "Honeywell: Error setting thermostat data!");
328+
return;
329+
}
330+
331+
std::string desc = kHeatSetPointDesc;
332+
stdreplace(desc, "[devicename]", mDeviceList[idx]["name"].asString());
333+
SendSetPointSensor(10*idx + 4, temp, desc);
334+
335+
desc = kHeatingDesc;
336+
stdreplace(desc, "[devicename]", mDeviceList[idx]["name"].asString());
337+
SendSwitch(10*idx + 3, 1, 255, true, 0, desc);
338+
}

hardware/Honeywell.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#pragma once
2+
3+
#include "DomoticzHardware.h"
4+
#include <iosfwd>
5+
#include "hardwaretypes.h"
6+
#include "../json/json.h"
7+
8+
class CHoneywell : public CDomoticzHardwareBase
9+
{
10+
public:
11+
CHoneywell(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);
12+
~CHoneywell(void);
13+
bool WriteToHardware(const char *pdata, const unsigned char length);
14+
private:
15+
void SetSetpoint(const int idx, const float temp, const int nodeid);
16+
void SetPauseStatus(const int idx, bool bHeating, const int nodeid);
17+
void SendSetPointSensor(const unsigned char Idx, const float Temp, const std::string &defaultname);
18+
bool refreshToken();
19+
std::string mAccessToken;
20+
std::string mRefreshToken;
21+
std::string mThermostatID;
22+
int mOutsideTemperatureIdx;
23+
volatile bool mStopRequested;
24+
bool mNeedsTokenRefresh;
25+
bool mIsStarted;
26+
boost::shared_ptr<boost::thread> mThread;
27+
std::vector<std::string> mSessionHeaders;
28+
std::map<int, Json::Value> mDeviceList;
29+
std::map<int, std::string> mLocationList;
30+
31+
int mLastMinute;
32+
33+
void Init();
34+
bool StartHardware();
35+
bool StopHardware();
36+
void Do_Work();
37+
void GetMeterDetails();
38+
};

main/RFXNames.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ const char *Hardware_Type_Desc(int hType)
275275
{ HTYPE_RaspberryMCP23017, "I2C sensor GPIO 16bit expander MCP23017" },
276276
{ HTYPE_eHouseTCP, "eHouse UDP+TCP with LAN interface" },
277277
{ HTYPE_EcoCompteur, "EcoCompteur Legrand with LAN interface" },
278+
{ HTYPE_Honeywell, "Honeywell Thermostat" },
278279
{ 0, NULL, NULL }
279280
};
280281
return findTableIDSingle1 (Table, hType);

0 commit comments

Comments
 (0)