xplg_wemohue.ino 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. /*
  2. xplg_wemohue.ino - wemo and hue support for Sonoff-Tasmota
  3. Copyright (C) 2018 Heiko Krupp and Theo Arends
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. //#define min(a,b) ((a)<(b)?(a):(b))
  16. //#define max(a,b) ((a)>(b)?(a):(b))
  17. #if defined(USE_WEBSERVER) && defined(USE_EMULATION)
  18. /*********************************************************************************************\
  19. * Belkin WeMo and Philips Hue bridge emulation
  20. \*********************************************************************************************/
  21. #define UDP_BUFFER_SIZE 200 // Max UDP buffer size needed for M-SEARCH message
  22. #define UDP_MSEARCH_SEND_DELAY 1500 // Delay in ms before M-Search response is send
  23. #include <Ticker.h>
  24. Ticker TickerMSearch;
  25. boolean udp_connected = false;
  26. char packet_buffer[UDP_BUFFER_SIZE]; // buffer to hold incoming UDP packet
  27. IPAddress ipMulticast(239,255,255,250); // Simple Service Discovery Protocol (SSDP)
  28. uint32_t port_multicast = 1900; // Multicast address and port
  29. bool udp_response_mutex = false; // M-Search response mutex to control re-entry
  30. IPAddress udp_remote_ip; // M-Search remote IP address
  31. uint16_t udp_remote_port; // M-Search remote port
  32. /*********************************************************************************************\
  33. * WeMo UPNP support routines
  34. \*********************************************************************************************/
  35. const char WEMO_MSEARCH[] PROGMEM =
  36. "HTTP/1.1 200 OK\r\n"
  37. "CACHE-CONTROL: max-age=86400\r\n"
  38. "DATE: Fri, 15 Apr 2016 04:56:29 GMT\r\n"
  39. "EXT:\r\n"
  40. "LOCATION: http://{r1:80/setup.xml\r\n"
  41. "OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n"
  42. "01-NLS: b9200ebb-736d-4b93-bf03-835149d13983\r\n"
  43. "SERVER: Unspecified, UPnP/1.0, Unspecified\r\n"
  44. "ST: {r3\r\n" // type1 = urn:Belkin:device:**, type2 = upnp:rootdevice
  45. "USN: uuid:{r2::{r3\r\n" // type1 = urn:Belkin:device:**, type2 = upnp:rootdevice
  46. "X-User-Agent: redsonic\r\n"
  47. "\r\n";
  48. String WemoSerialnumber(void)
  49. {
  50. char serial[16];
  51. snprintf_P(serial, sizeof(serial), PSTR("201612K%08X"), ESP.getChipId());
  52. return String(serial);
  53. }
  54. String WemoUuid(void)
  55. {
  56. char uuid[27];
  57. snprintf_P(uuid, sizeof(uuid), PSTR("Socket-1_0-%s"), WemoSerialnumber().c_str());
  58. return String(uuid);
  59. }
  60. void WemoRespondToMSearch(int echo_type)
  61. {
  62. char message[TOPSZ];
  63. TickerMSearch.detach();
  64. if (PortUdp.beginPacket(udp_remote_ip, udp_remote_port)) {
  65. String response = FPSTR(WEMO_MSEARCH);
  66. response.replace("{r1", WiFi.localIP().toString());
  67. response.replace("{r2", WemoUuid());
  68. if (1 == echo_type) { // type1 echo 1g & dot 2g
  69. response.replace("{r3", F("urn:Belkin:device:**"));
  70. } else { // type2 echo 2g (echo, plus, show)
  71. response.replace("{r3", F("upnp:rootdevice"));
  72. }
  73. PortUdp.write(response.c_str());
  74. PortUdp.endPacket();
  75. snprintf_P(message, sizeof(message), PSTR(D_RESPONSE_SENT));
  76. } else {
  77. snprintf_P(message, sizeof(message), PSTR(D_FAILED_TO_SEND_RESPONSE));
  78. }
  79. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_UPNP D_WEMO " " D_JSON_TYPE " %d, %s " D_TO " %s:%d"),
  80. echo_type, message, udp_remote_ip.toString().c_str(), udp_remote_port);
  81. AddLog(LOG_LEVEL_DEBUG);
  82. udp_response_mutex = false;
  83. }
  84. /*********************************************************************************************\
  85. * Hue Bridge UPNP support routines
  86. * Need to send 3 response packets with varying ST and USN
  87. *
  88. * Using Espressif Inc Mac Address of 5C:CF:7F:00:00:00
  89. * Philips Lighting is 00:17:88:00:00:00
  90. \*********************************************************************************************/
  91. const char HUE_RESPONSE[] PROGMEM =
  92. "HTTP/1.1 200 OK\r\n"
  93. "HOST: 239.255.255.250:1900\r\n"
  94. "CACHE-CONTROL: max-age=100\r\n"
  95. "EXT:\r\n"
  96. "LOCATION: http://{r1:80/description.xml\r\n"
  97. "SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.17.0\r\n"
  98. "hue-bridgeid: {r2\r\n";
  99. const char HUE_ST1[] PROGMEM =
  100. "ST: upnp:rootdevice\r\n"
  101. "USN: uuid:{r3::upnp:rootdevice\r\n"
  102. "\r\n";
  103. const char HUE_ST2[] PROGMEM =
  104. "ST: uuid:{r3\r\n"
  105. "USN: uuid:{r3\r\n"
  106. "\r\n";
  107. const char HUE_ST3[] PROGMEM =
  108. "ST: urn:schemas-upnp-org:device:basic:1\r\n"
  109. "USN: uuid:{r3\r\n"
  110. "\r\n";
  111. String HueBridgeId(void)
  112. {
  113. String temp = WiFi.macAddress();
  114. temp.replace(":", "");
  115. String bridgeid = temp.substring(0, 6) + "FFFE" + temp.substring(6);
  116. return bridgeid; // 5CCF7FFFFE139F3D
  117. }
  118. String HueSerialnumber(void)
  119. {
  120. String serial = WiFi.macAddress();
  121. serial.replace(":", "");
  122. serial.toLowerCase();
  123. return serial; // 5ccf7f139f3d
  124. }
  125. String HueUuid(void)
  126. {
  127. String uuid = F("f6543a06-da50-11ba-8d8f-");
  128. uuid += HueSerialnumber();
  129. return uuid; // f6543a06-da50-11ba-8d8f-5ccf7f139f3d
  130. }
  131. void HueRespondToMSearch(void)
  132. {
  133. char message[TOPSZ];
  134. TickerMSearch.detach();
  135. if (PortUdp.beginPacket(udp_remote_ip, udp_remote_port)) {
  136. String response1 = FPSTR(HUE_RESPONSE);
  137. response1.replace("{r1", WiFi.localIP().toString());
  138. response1.replace("{r2", HueBridgeId());
  139. String response = response1;
  140. response += FPSTR(HUE_ST1);
  141. response.replace("{r3", HueUuid());
  142. PortUdp.write(response.c_str());
  143. PortUdp.endPacket();
  144. response = response1;
  145. response += FPSTR(HUE_ST2);
  146. response.replace("{r3", HueUuid());
  147. PortUdp.write(response.c_str());
  148. PortUdp.endPacket();
  149. response = response1;
  150. response += FPSTR(HUE_ST3);
  151. response.replace("{r3", HueUuid());
  152. PortUdp.write(response.c_str());
  153. PortUdp.endPacket();
  154. snprintf_P(message, sizeof(message), PSTR(D_3_RESPONSE_PACKETS_SENT));
  155. } else {
  156. snprintf_P(message, sizeof(message), PSTR(D_FAILED_TO_SEND_RESPONSE));
  157. }
  158. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_UPNP D_HUE " %s " D_TO " %s:%d"),
  159. message, udp_remote_ip.toString().c_str(), udp_remote_port);
  160. AddLog(LOG_LEVEL_DEBUG);
  161. udp_response_mutex = false;
  162. }
  163. /*********************************************************************************************\
  164. * Belkin WeMo and Philips Hue bridge UDP multicast support
  165. \*********************************************************************************************/
  166. boolean UdpDisconnect(void)
  167. {
  168. if (udp_connected) {
  169. WiFiUDP::stopAll();
  170. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_UPNP D_MULTICAST_DISABLED));
  171. udp_connected = false;
  172. }
  173. return udp_connected;
  174. }
  175. boolean UdpConnect(void)
  176. {
  177. if (!udp_connected) {
  178. if (PortUdp.beginMulticast(WiFi.localIP(), ipMulticast, port_multicast)) {
  179. AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_REJOINED));
  180. udp_response_mutex = false;
  181. udp_connected = true;
  182. } else {
  183. AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_JOIN_FAILED));
  184. udp_connected = false;
  185. }
  186. }
  187. return udp_connected;
  188. }
  189. void PollUdp(void)
  190. {
  191. if (udp_connected && !udp_response_mutex) {
  192. if (PortUdp.parsePacket()) {
  193. int len = PortUdp.read(packet_buffer, UDP_BUFFER_SIZE -1);
  194. if (len > 0) {
  195. packet_buffer[len] = 0;
  196. }
  197. String request = packet_buffer;
  198. // AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: Packet received"));
  199. // AddLog_P(LOG_LEVEL_DEBUG_MORE, packet_buffer);
  200. if (request.indexOf("M-SEARCH") >= 0) {
  201. request.toLowerCase();
  202. request.replace(" ", "");
  203. // AddLog_P(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: M-SEARCH Packet received"));
  204. // AddLog_P(LOG_LEVEL_DEBUG_MORE, request.c_str());
  205. udp_remote_ip = PortUdp.remoteIP();
  206. udp_remote_port = PortUdp.remotePort();
  207. if (EMUL_WEMO == Settings.flag2.emulation) {
  208. if (request.indexOf(F("urn:belkin:device:**")) > 0) { // type1 echo dot 2g, echo 1g's
  209. udp_response_mutex = true;
  210. TickerMSearch.attach_ms(UDP_MSEARCH_SEND_DELAY, WemoRespondToMSearch, 1);
  211. }
  212. else if ((request.indexOf(F("upnp:rootdevice")) > 0) || // type2 Echo 2g (echo & echo plus)
  213. (request.indexOf(F("ssdpsearch:all")) > 0) ||
  214. (request.indexOf(F("ssdp:all")) > 0)) {
  215. udp_response_mutex = true;
  216. TickerMSearch.attach_ms(UDP_MSEARCH_SEND_DELAY, WemoRespondToMSearch, 2);
  217. }
  218. }
  219. else if ((EMUL_HUE == Settings.flag2.emulation) &&
  220. ((request.indexOf(F("urn:schemas-upnp-org:device:basic:1")) > 0) ||
  221. (request.indexOf(F("upnp:rootdevice")) > 0) ||
  222. (request.indexOf(F("ssdpsearch:all")) > 0) ||
  223. (request.indexOf(F("ssdp:all")) > 0))) {
  224. udp_response_mutex = true;
  225. TickerMSearch.attach_ms(UDP_MSEARCH_SEND_DELAY, HueRespondToMSearch);
  226. }
  227. }
  228. }
  229. }
  230. }
  231. /*********************************************************************************************\
  232. * Wemo web server additions
  233. \*********************************************************************************************/
  234. const char WEMO_EVENTSERVICE_XML[] PROGMEM =
  235. "<scpd xmlns=\"urn:Belkin:service-1-0\">"
  236. "<actionList>"
  237. "<action>"
  238. "<name>SetBinaryState</name>"
  239. "<argumentList>"
  240. "<argument>"
  241. "<retval/>"
  242. "<name>BinaryState</name>"
  243. "<relatedStateVariable>BinaryState</relatedStateVariable>"
  244. "<direction>in</direction>"
  245. "</argument>"
  246. "</argumentList>"
  247. "</action>"
  248. "<action>"
  249. "<name>GetBinaryState</name>"
  250. "<argumentList>"
  251. "<argument>"
  252. "<retval/>"
  253. "<name>BinaryState</name>"
  254. "<relatedStateVariable>BinaryState</relatedStateVariable>"
  255. "<direction>out</direction>"
  256. "</argument>"
  257. "</argumentList>"
  258. "</action>"
  259. "</actionList>"
  260. "<serviceStateTable>"
  261. "<stateVariable sendEvents=\"yes\">"
  262. "<name>BinaryState</name>"
  263. "<dataType>Boolean</dataType>"
  264. "<defaultValue>0</defaultValue>"
  265. "</stateVariable>"
  266. "<stateVariable sendEvents=\"yes\">"
  267. "<name>level</name>"
  268. "<dataType>string</dataType>"
  269. "<defaultValue>0</defaultValue>"
  270. "</stateVariable>"
  271. "</serviceStateTable>"
  272. "</scpd>\r\n\r\n";
  273. const char WEMO_METASERVICE_XML[] PROGMEM =
  274. "<scpd xmlns=\"urn:Belkin:service-1-0\">"
  275. "<specVersion>"
  276. "<major>1</major>"
  277. "<minor>0</minor>"
  278. "</specVersion>"
  279. "<actionList>"
  280. "<action>"
  281. "<name>GetMetaInfo</name>"
  282. "<argumentList>"
  283. "<retval />"
  284. "<name>GetMetaInfo</name>"
  285. "<relatedStateVariable>MetaInfo</relatedStateVariable>"
  286. "<direction>in</direction>"
  287. "</argumentList>"
  288. "</action>"
  289. "</actionList>"
  290. "<serviceStateTable>"
  291. "<stateVariable sendEvents=\"yes\">"
  292. "<name>MetaInfo</name>"
  293. "<dataType>string</dataType>"
  294. "<defaultValue>0</defaultValue>"
  295. "</stateVariable>"
  296. "</serviceStateTable>"
  297. "</scpd>\r\n\r\n";
  298. const char WEMO_RESPONSE_STATE_SOAP[] PROGMEM =
  299. "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
  300. "<s:Body>"
  301. "<u:SetBinaryStateResponse xmlns:u=\"urn:Belkin:service:basicevent:1\">"
  302. "<BinaryState>{x1</BinaryState>"
  303. "</u:SetBinaryStateResponse>"
  304. "</s:Body>"
  305. "</s:Envelope>\r\n";
  306. const char WEMO_SETUP_XML[] PROGMEM =
  307. "<?xml version=\"1.0\"?>"
  308. "<root xmlns=\"urn:Belkin:device-1-0\">"
  309. "<device>"
  310. "<deviceType>urn:Belkin:device:controllee:1</deviceType>"
  311. "<friendlyName>{x1</friendlyName>"
  312. "<manufacturer>Belkin International Inc.</manufacturer>"
  313. "<modelName>Socket</modelName>"
  314. "<modelNumber>3.1415</modelNumber>"
  315. "<UDN>uuid:{x2</UDN>"
  316. "<serialNumber>{x3</serialNumber>"
  317. "<binaryState>0</binaryState>"
  318. "<serviceList>"
  319. "<service>"
  320. "<serviceType>urn:Belkin:service:basicevent:1</serviceType>"
  321. "<serviceId>urn:Belkin:serviceId:basicevent1</serviceId>"
  322. "<controlURL>/upnp/control/basicevent1</controlURL>"
  323. "<eventSubURL>/upnp/event/basicevent1</eventSubURL>"
  324. "<SCPDURL>/eventservice.xml</SCPDURL>"
  325. "</service>"
  326. "<service>"
  327. "<serviceType>urn:Belkin:service:metainfo:1</serviceType>"
  328. "<serviceId>urn:Belkin:serviceId:metainfo1</serviceId>"
  329. "<controlURL>/upnp/control/metainfo1</controlURL>"
  330. "<eventSubURL>/upnp/event/metainfo1</eventSubURL>"
  331. "<SCPDURL>/metainfoservice.xml</SCPDURL>"
  332. "</service>"
  333. "</serviceList>"
  334. "</device>"
  335. "</root>\r\n";
  336. /********************************************************************************************/
  337. void HandleUpnpEvent(void)
  338. {
  339. AddLog_P(LOG_LEVEL_DEBUG, S_LOG_HTTP, PSTR(D_WEMO_BASIC_EVENT));
  340. String request = WebServer->arg(0);
  341. String state_xml = FPSTR(WEMO_RESPONSE_STATE_SOAP);
  342. //differentiate get and set state
  343. if (request.indexOf(F("SetBinaryState")) > 0) {
  344. uint8_t power = POWER_TOGGLE;
  345. if (request.indexOf(F("State>1</Binary")) > 0) {
  346. power = POWER_ON;
  347. }
  348. else if (request.indexOf(F("State>0</Binary")) > 0) {
  349. power = POWER_OFF;
  350. }
  351. if (power != POWER_TOGGLE) {
  352. uint8_t device = (light_type) ? devices_present : 1; // Select either a configured light or relay1
  353. ExecuteCommandPower(device, power, SRC_WEMO);
  354. }
  355. }
  356. else if(request.indexOf(F("GetBinaryState")) > 0){
  357. state_xml.replace(F("Set"), F("Get"));
  358. }
  359. state_xml.replace("{x1", String(bitRead(power, devices_present -1)));
  360. WebServer->send(200, FPSTR(HDR_CTYPE_XML), state_xml);
  361. }
  362. void HandleUpnpService(void)
  363. {
  364. AddLog_P(LOG_LEVEL_DEBUG, S_LOG_HTTP, PSTR(D_WEMO_EVENT_SERVICE));
  365. WebServer->send(200, FPSTR(HDR_CTYPE_PLAIN), FPSTR(WEMO_EVENTSERVICE_XML));
  366. }
  367. void HandleUpnpMetaService(void)
  368. {
  369. AddLog_P(LOG_LEVEL_DEBUG, S_LOG_HTTP, PSTR(D_WEMO_META_SERVICE));
  370. WebServer->send(200, FPSTR(HDR_CTYPE_PLAIN), FPSTR(WEMO_METASERVICE_XML));
  371. }
  372. void HandleUpnpSetupWemo(void)
  373. {
  374. AddLog_P(LOG_LEVEL_DEBUG, S_LOG_HTTP, PSTR(D_WEMO_SETUP));
  375. String setup_xml = FPSTR(WEMO_SETUP_XML);
  376. setup_xml.replace("{x1", Settings.friendlyname[0]);
  377. setup_xml.replace("{x2", WemoUuid());
  378. setup_xml.replace("{x3", WemoSerialnumber());
  379. WebServer->send(200, FPSTR(HDR_CTYPE_XML), setup_xml);
  380. }
  381. /*********************************************************************************************\
  382. * Hue web server additions
  383. \*********************************************************************************************/
  384. const char HUE_DESCRIPTION_XML[] PROGMEM =
  385. "<?xml version=\"1.0\"?>"
  386. "<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
  387. "<specVersion>"
  388. "<major>1</major>"
  389. "<minor>0</minor>"
  390. "</specVersion>"
  391. // "<URLBase>http://{x1/</URLBase>"
  392. "<URLBase>http://{x1:80/</URLBase>"
  393. "<device>"
  394. "<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>"
  395. "<friendlyName>Amazon-Echo-HA-Bridge ({x1)</friendlyName>"
  396. // "<friendlyName>Philips hue ({x1)</friendlyName>"
  397. "<manufacturer>Royal Philips Electronics</manufacturer>"
  398. "<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>"
  399. "<modelName>Philips hue bridge 2012</modelName>"
  400. "<modelNumber>929000226503</modelNumber>"
  401. "<serialNumber>{x3</serialNumber>"
  402. "<UDN>uuid:{x2</UDN>"
  403. "</device>"
  404. "</root>\r\n"
  405. "\r\n";
  406. const char HUE_LIGHTS_STATUS_JSON[] PROGMEM =
  407. "{\"on\":{state},"
  408. "\"bri\":{b},"
  409. "\"hue\":{h},"
  410. "\"sat\":{s},"
  411. "\"xy\":[0.5, 0.5],"
  412. "\"ct\":{t},"
  413. "\"alert\":\"none\","
  414. "\"effect\":\"none\","
  415. "\"colormode\":\"{m}\","
  416. "\"reachable\":true}";
  417. const char HUE_LIGHTS_STATUS_JSON2[] PROGMEM =
  418. ",\"type\":\"Extended color light\","
  419. "\"name\":\"{j1\","
  420. "\"modelid\":\"LCT007\","
  421. "\"uniqueid\":\"{j2\","
  422. "\"swversion\":\"5.50.1.19085\"}";
  423. const char HUE_GROUP0_STATUS_JSON[] PROGMEM =
  424. "{\"name\":\"Group 0\","
  425. "\"lights\":[{l1],"
  426. "\"type\":\"LightGroup\","
  427. "\"action\":";
  428. // "\"scene\":\"none\",";
  429. const char HueConfigResponse_JSON[] PROGMEM =
  430. "{\"name\":\"Philips hue\","
  431. "\"mac\":\"{ma\","
  432. "\"dhcp\":true,"
  433. "\"ipaddress\":\"{ip\","
  434. "\"netmask\":\"{ms\","
  435. "\"gateway\":\"{gw\","
  436. "\"proxyaddress\":\"none\","
  437. "\"proxyport\":0,"
  438. "\"bridgeid\":\"{br\","
  439. "\"UTC\":\"{dt\","
  440. "\"whitelist\":{\"{id\":{"
  441. "\"last use date\":\"{dt\","
  442. "\"create date\":\"{dt\","
  443. "\"name\":\"Remote\"}},"
  444. "\"swversion\":\"01041302\","
  445. "\"apiversion\":\"1.17.0\","
  446. "\"swupdate\":{\"updatestate\":0,\"url\":\"\",\"text\":\"\",\"notify\": false},"
  447. "\"linkbutton\":false,"
  448. "\"portalservices\":false"
  449. "}";
  450. const char HUE_LIGHT_RESPONSE_JSON[] PROGMEM =
  451. "{\"success\":{\"/lights/{id/state/{cm\":{re}}";
  452. const char HUE_ERROR_JSON[] PROGMEM =
  453. "[{\"error\":{\"type\":901,\"address\":\"/\",\"description\":\"Internal Error\"}}]";
  454. /********************************************************************************************/
  455. String GetHueDeviceId(uint8_t id)
  456. {
  457. String deviceid = WiFi.macAddress() + F(":00:11-") + String(id);
  458. deviceid.toLowerCase();
  459. return deviceid; // 5c:cf:7f:13:9f:3d:00:11-1
  460. }
  461. String GetHueUserId(void)
  462. {
  463. char userid[7];
  464. snprintf_P(userid, sizeof(userid), PSTR("%03x"), ESP.getChipId());
  465. return String(userid);
  466. }
  467. void HandleUpnpSetupHue(void)
  468. {
  469. AddLog_P(LOG_LEVEL_DEBUG, S_LOG_HTTP, PSTR(D_HUE_BRIDGE_SETUP));
  470. String description_xml = FPSTR(HUE_DESCRIPTION_XML);
  471. description_xml.replace("{x1", WiFi.localIP().toString());
  472. description_xml.replace("{x2", HueUuid());
  473. description_xml.replace("{x3", HueSerialnumber());
  474. WebServer->send(200, FPSTR(HDR_CTYPE_XML), description_xml);
  475. }
  476. void HueNotImplemented(String *path)
  477. {
  478. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_HTTP D_HUE_API_NOT_IMPLEMENTED " (%s)"), path->c_str());
  479. AddLog(LOG_LEVEL_DEBUG_MORE);
  480. WebServer->send(200, FPSTR(HDR_CTYPE_JSON), "{}");
  481. }
  482. void HueConfigResponse(String *response)
  483. {
  484. *response += FPSTR(HueConfigResponse_JSON);
  485. response->replace("{ma", WiFi.macAddress());
  486. response->replace("{ip", WiFi.localIP().toString());
  487. response->replace("{ms", WiFi.subnetMask().toString());
  488. response->replace("{gw", WiFi.gatewayIP().toString());
  489. response->replace("{br", HueBridgeId());
  490. response->replace("{dt", GetDateAndTime(DT_UTC));
  491. response->replace("{id", GetHueUserId());
  492. }
  493. void HueConfig(String *path)
  494. {
  495. String response = "";
  496. HueConfigResponse(&response);
  497. WebServer->send(200, FPSTR(HDR_CTYPE_JSON), response);
  498. }
  499. bool g_gotct = false;
  500. void HueLightStatus1(byte device, String *response)
  501. {
  502. float hue = 0;
  503. float sat = 0;
  504. float bri = 254;
  505. uint16_t ct = 500;
  506. if (light_type) {
  507. LightGetHsb(&hue, &sat, &bri, g_gotct);
  508. ct = LightGetColorTemp();
  509. }
  510. *response += FPSTR(HUE_LIGHTS_STATUS_JSON);
  511. response->replace("{state}", (power & (1 << (device-1))) ? "true" : "false");
  512. response->replace("{h}", String((uint16_t)(65535.0f * hue)));
  513. response->replace("{s}", String((uint8_t)(254.0f * sat)));
  514. response->replace("{b}", String((uint8_t)(254.0f * bri)));
  515. response->replace("{t}", String(ct));
  516. response->replace("{m}", g_gotct?"ct":"hs");
  517. }
  518. void HueLightStatus2(byte device, String *response)
  519. {
  520. *response += FPSTR(HUE_LIGHTS_STATUS_JSON2);
  521. response->replace("{j1", Settings.friendlyname[device-1]);
  522. response->replace("{j2", GetHueDeviceId(device));
  523. }
  524. void HueGlobalConfig(String *path)
  525. {
  526. String response;
  527. uint8_t maxhue = (devices_present > MAX_FRIENDLYNAMES) ? MAX_FRIENDLYNAMES : devices_present;
  528. path->remove(0,1); // cut leading / to get <id>
  529. response = F("{\"lights\":{\"");
  530. for (uint8_t i = 1; i <= maxhue; i++) {
  531. response += i;
  532. response += F("\":{\"state\":");
  533. HueLightStatus1(i, &response);
  534. HueLightStatus2(i, &response);
  535. if (i < maxhue) {
  536. response += ",\"";
  537. }
  538. }
  539. response += F("},\"groups\":{},\"schedules\":{},\"config\":");
  540. HueConfigResponse(&response);
  541. response += "}";
  542. WebServer->send(200, FPSTR(HDR_CTYPE_JSON), response);
  543. }
  544. void HueAuthentication(String *path)
  545. {
  546. char response[38];
  547. snprintf_P(response, sizeof(response), PSTR("[{\"success\":{\"username\":\"%s\"}}]"), GetHueUserId().c_str());
  548. WebServer->send(200, FPSTR(HDR_CTYPE_JSON), response);
  549. }
  550. void HueLights(String *path)
  551. {
  552. /*
  553. * http://sonoff/api/username/lights/1/state?1={"on":true,"hue":56100,"sat":254,"bri":254,"alert":"none","transitiontime":40}
  554. */
  555. String response;
  556. uint8_t device = 1;
  557. uint16_t tmp = 0;
  558. float bri = 0;
  559. float hue = 0;
  560. float sat = 0;
  561. uint16_t ct = 0;
  562. bool resp = false;
  563. bool on = false;
  564. bool change = false;
  565. uint8_t maxhue = (devices_present > MAX_FRIENDLYNAMES) ? MAX_FRIENDLYNAMES : devices_present;
  566. path->remove(0,path->indexOf("/lights")); // Remove until /lights
  567. if (path->endsWith("/lights")) { // Got /lights
  568. response = "{\"";
  569. for (uint8_t i = 1; i <= maxhue; i++) {
  570. response += i;
  571. response += F("\":{\"state\":");
  572. HueLightStatus1(i, &response);
  573. HueLightStatus2(i, &response);
  574. if (i < maxhue) {
  575. response += ",\"";
  576. }
  577. }
  578. response += "}";
  579. WebServer->send(200, FPSTR(HDR_CTYPE_JSON), response);
  580. }
  581. else if (path->endsWith("/state")) { // Got ID/state
  582. path->remove(0,8); // Remove /lights/
  583. path->remove(path->indexOf("/state")); // Remove /state
  584. device = atoi(path->c_str());
  585. if ((device < 1) || (device > maxhue)) {
  586. device = 1;
  587. }
  588. if (WebServer->args()) {
  589. response = "[";
  590. StaticJsonBuffer<400> jsonBuffer;
  591. JsonObject &hue_json = jsonBuffer.parseObject(WebServer->arg((WebServer->args())-1));
  592. if (hue_json.containsKey("on")) {
  593. response += FPSTR(HUE_LIGHT_RESPONSE_JSON);
  594. response.replace("{id", String(device));
  595. response.replace("{cm", "on");
  596. on = hue_json["on"];
  597. switch(on)
  598. {
  599. case false : ExecuteCommandPower(device, POWER_OFF, SRC_HUE);
  600. response.replace("{re", "false");
  601. break;
  602. case true : ExecuteCommandPower(device, POWER_ON, SRC_HUE);
  603. response.replace("{re", "true");
  604. break;
  605. default : response.replace("{re", (power & (1 << (device-1))) ? "true" : "false");
  606. break;
  607. }
  608. resp = true;
  609. }
  610. if (light_type) {
  611. LightGetHsb(&hue, &sat, &bri, g_gotct);
  612. }
  613. if (hue_json.containsKey("bri")) { // Brightness is a scale from 1 (the minimum the light is capable of) to 254 (the maximum). Note: a brightness of 1 is not off.
  614. tmp = hue_json["bri"];
  615. tmp = tmax(tmp, 1);
  616. tmp = tmin(tmp, 254);
  617. bri = (float)tmp / 254.0f;
  618. if (resp) {
  619. response += ",";
  620. }
  621. response += FPSTR(HUE_LIGHT_RESPONSE_JSON);
  622. response.replace("{id", String(device));
  623. response.replace("{cm", "bri");
  624. response.replace("{re", String(tmp));
  625. resp = true;
  626. change = true;
  627. }
  628. if (hue_json.containsKey("hue")) { // The hue value is a wrapping value between 0 and 65535. Both 0 and 65535 are red, 25500 is green and 46920 is blue.
  629. tmp = hue_json["hue"];
  630. hue = (float)tmp / 65535.0f;
  631. if (resp) {
  632. response += ",";
  633. }
  634. response += FPSTR(HUE_LIGHT_RESPONSE_JSON);
  635. response.replace("{id", String(device));
  636. response.replace("{cm", "hue");
  637. response.replace("{re", String(tmp));
  638. g_gotct = false;
  639. resp = true;
  640. change = true;
  641. }
  642. if (hue_json.containsKey("sat")) { // Saturation of the light. 254 is the most saturated (colored) and 0 is the least saturated (white).
  643. tmp = hue_json["sat"];
  644. tmp = tmax(tmp, 0);
  645. tmp = tmin(tmp, 254);
  646. sat = (float)tmp / 254.0f;
  647. if (resp) {
  648. response += ",";
  649. }
  650. response += FPSTR(HUE_LIGHT_RESPONSE_JSON);
  651. response.replace("{id", String(device));
  652. response.replace("{cm", "sat");
  653. response.replace("{re", String(tmp));
  654. g_gotct = false;
  655. resp = true;
  656. change = true;
  657. }
  658. if (hue_json.containsKey("ct")) { // Color temperature 153 (Cold) to 500 (Warm)
  659. ct = hue_json["ct"];
  660. if (resp) {
  661. response += ",";
  662. }
  663. response += FPSTR(HUE_LIGHT_RESPONSE_JSON);
  664. response.replace("{id", String(device));
  665. response.replace("{cm", "ct");
  666. response.replace("{re", String(ct));
  667. g_gotct = true;
  668. change = true;
  669. }
  670. if (change) {
  671. if (light_type) {
  672. LightSetHsb(hue, sat, bri, ct, g_gotct);
  673. }
  674. change = false;
  675. }
  676. response += "]";
  677. if (2 == response.length()) {
  678. response = FPSTR(HUE_ERROR_JSON);
  679. }
  680. }
  681. else {
  682. response = FPSTR(HUE_ERROR_JSON);
  683. }
  684. WebServer->send(200, FPSTR(HDR_CTYPE_JSON), response);
  685. }
  686. else if(path->indexOf("/lights/") >= 0) { // Got /lights/ID
  687. path->remove(0,8); // Remove /lights/
  688. device = atoi(path->c_str());
  689. if ((device < 1) || (device > maxhue)) {
  690. device = 1;
  691. }
  692. response += F("{\"state\":");
  693. HueLightStatus1(device, &response);
  694. HueLightStatus2(device, &response);
  695. WebServer->send(200, FPSTR(HDR_CTYPE_JSON), response);
  696. }
  697. else {
  698. WebServer->send(406, FPSTR(HDR_CTYPE_JSON), "{}");
  699. }
  700. }
  701. void HueGroups(String *path)
  702. {
  703. /*
  704. * http://sonoff/api/username/groups?1={"name":"Woonkamer","lights":[],"type":"Room","class":"Living room"})
  705. */
  706. String response = "{}";
  707. uint8_t maxhue = (devices_present > MAX_FRIENDLYNAMES) ? MAX_FRIENDLYNAMES : devices_present;
  708. if (path->endsWith("/0")) {
  709. response = FPSTR(HUE_GROUP0_STATUS_JSON);
  710. String lights = F("\"1\"");
  711. for (uint8_t i = 2; i <= maxhue; i++) {
  712. lights += ",\"" + String(i) + "\"";
  713. }
  714. response.replace("{l1", lights);
  715. HueLightStatus1(1, &response);
  716. response += F("}");
  717. }
  718. WebServer->send(200, FPSTR(HDR_CTYPE_JSON), response);
  719. }
  720. void HandleHueApi(String *path)
  721. {
  722. /* HUE API uses /api/<userid>/<command> syntax. The userid is created by the echo device and
  723. * on original HUE the pressed button allows for creation of this user. We simply ignore the
  724. * user part and allow every caller as with Web or WeMo.
  725. *
  726. * (c) Heiko Krupp, 2017
  727. *
  728. * Hue URL
  729. * http://sonoff/api/username/lights/1/state with post data {"on":true,"hue":56100,"sat":254,"bri":254,"alert":"none","transitiontime":40}
  730. * is converted by webserver to
  731. * http://sonoff/api/username/lights/1/state with arg plain={"on":true,"hue":56100,"sat":254,"bri":254,"alert":"none","transitiontime":40}
  732. */
  733. uint8_t args = 0;
  734. path->remove(0, 4); // remove /api
  735. uint16_t apilen = path->length();
  736. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_HTTP D_HUE_API " (%s)"), path->c_str());
  737. AddLog(LOG_LEVEL_DEBUG_MORE); // HTP: Hue API (//lights/1/state)
  738. for (args = 0; args < WebServer->args(); args++) {
  739. String json = WebServer->arg(args);
  740. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_HTTP D_HUE_POST_ARGS " (%s)"), json.c_str());
  741. AddLog(LOG_LEVEL_DEBUG_MORE); // HTP: Hue POST args ({"on":false})
  742. }
  743. if (path->endsWith("/invalid/")) {} // Just ignore
  744. else if (!apilen) HueAuthentication(path); // New HUE App setup
  745. else if (path->endsWith("/")) HueAuthentication(path); // New HUE App setup
  746. else if (path->endsWith("/config")) HueConfig(path);
  747. else if (path->indexOf("/lights") >= 0) HueLights(path);
  748. else if (path->indexOf("/groups") >= 0) HueGroups(path);
  749. else if (path->endsWith("/schedules")) HueNotImplemented(path);
  750. else if (path->endsWith("/sensors")) HueNotImplemented(path);
  751. else if (path->endsWith("/scenes")) HueNotImplemented(path);
  752. else if (path->endsWith("/rules")) HueNotImplemented(path);
  753. else HueGlobalConfig(path);
  754. }
  755. void HueWemoAddHandlers(void)
  756. {
  757. if (EMUL_WEMO == Settings.flag2.emulation) {
  758. WebServer->on("/upnp/control/basicevent1", HTTP_POST, HandleUpnpEvent);
  759. WebServer->on("/eventservice.xml", HandleUpnpService);
  760. WebServer->on("/metainfoservice.xml", HandleUpnpMetaService);
  761. WebServer->on("/setup.xml", HandleUpnpSetupWemo);
  762. }
  763. if (EMUL_HUE == Settings.flag2.emulation) {
  764. WebServer->on("/description.xml", HandleUpnpSetupHue);
  765. }
  766. }
  767. #endif // USE_WEBSERVER && USE_EMULATION