MiLightClient.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. #include <MiLightClient.h>
  2. #include <MiLightRadioConfig.h>
  3. #include <Arduino.h>
  4. #include <RGBConverter.h>
  5. #include <Units.h>
  6. #include <TokenIterator.h>
  7. #include <ParsedColor.h>
  8. #include <MiLightCommands.h>
  9. #include <functional>
  10. using namespace std::placeholders;
  11. const char* MiLightClient::FIELD_ORDERINGS[] = {
  12. // These are handled manually
  13. // GroupStateFieldNames::STATE,
  14. // GroupStateFieldNames::STATUS,
  15. GroupStateFieldNames::HUE,
  16. GroupStateFieldNames::SATURATION,
  17. GroupStateFieldNames::KELVIN,
  18. GroupStateFieldNames::TEMPERATURE,
  19. GroupStateFieldNames::COLOR_TEMP,
  20. GroupStateFieldNames::MODE,
  21. GroupStateFieldNames::COLOR,
  22. // Level/Brightness must be processed last because they're specific to a particular bulb mode.
  23. // So make sure bulb mode is set before applying level/brightness.
  24. GroupStateFieldNames::LEVEL,
  25. GroupStateFieldNames::BRIGHTNESS,
  26. GroupStateFieldNames::COMMAND,
  27. GroupStateFieldNames::COMMANDS
  28. };
  29. const std::map<const char*, std::function<void(MiLightClient*, JsonVariant)>, MiLightClient::cmp_str> MiLightClient::FIELD_SETTERS = {
  30. {
  31. GroupStateFieldNames::STATUS,
  32. [](MiLightClient* client, JsonVariant val) {
  33. client->updateStatus(parseMilightStatus(val));
  34. }
  35. },
  36. {GroupStateFieldNames::LEVEL, &MiLightClient::updateBrightness},
  37. {
  38. GroupStateFieldNames::BRIGHTNESS,
  39. [](MiLightClient* client, uint16_t arg) {
  40. client->updateBrightness(Units::rescale<uint16_t, uint16_t>(arg, 100, 255));
  41. }
  42. },
  43. {GroupStateFieldNames::HUE, &MiLightClient::updateHue},
  44. {GroupStateFieldNames::SATURATION, &MiLightClient::updateSaturation},
  45. {GroupStateFieldNames::KELVIN, &MiLightClient::updateTemperature},
  46. {GroupStateFieldNames::TEMPERATURE, &MiLightClient::updateTemperature},
  47. {
  48. GroupStateFieldNames::COLOR_TEMP,
  49. [](MiLightClient* client, uint16_t arg) {
  50. client->updateTemperature(Units::miredsToWhiteVal(arg, 100));
  51. }
  52. },
  53. {GroupStateFieldNames::MODE, &MiLightClient::updateMode},
  54. {GroupStateFieldNames::COLOR, &MiLightClient::updateColor},
  55. {GroupStateFieldNames::EFFECT, &MiLightClient::handleEffect},
  56. {GroupStateFieldNames::COMMAND, &MiLightClient::handleCommand},
  57. {GroupStateFieldNames::COMMANDS, &MiLightClient::handleCommands}
  58. };
  59. MiLightClient::MiLightClient(
  60. RadioSwitchboard& radioSwitchboard,
  61. PacketSender& packetSender,
  62. GroupStateStore* stateStore,
  63. Settings& settings,
  64. TransitionController& transitions
  65. ) : radioSwitchboard(radioSwitchboard)
  66. , updateBeginHandler(NULL)
  67. , updateEndHandler(NULL)
  68. , stateStore(stateStore)
  69. , settings(settings)
  70. , packetSender(packetSender)
  71. , transitions(transitions)
  72. , repeatsOverride(0)
  73. { }
  74. void MiLightClient::setHeld(bool held) {
  75. currentRemote->packetFormatter->setHeld(held);
  76. }
  77. void MiLightClient::prepare(
  78. const MiLightRemoteConfig* config,
  79. const uint16_t deviceId,
  80. const uint8_t groupId
  81. ) {
  82. this->currentRemote = config;
  83. if (deviceId >= 0 && groupId >= 0) {
  84. currentRemote->packetFormatter->prepare(deviceId, groupId);
  85. }
  86. }
  87. void MiLightClient::prepare(
  88. const MiLightRemoteType type,
  89. const uint16_t deviceId,
  90. const uint8_t groupId
  91. ) {
  92. prepare(MiLightRemoteConfig::fromType(type), deviceId, groupId);
  93. }
  94. void MiLightClient::updateColorRaw(const uint8_t color) {
  95. #ifdef DEBUG_CLIENT_COMMANDS
  96. Serial.printf_P(PSTR("MiLightClient::updateColorRaw: Change color to %d\n"), color);
  97. #endif
  98. currentRemote->packetFormatter->updateColorRaw(color);
  99. flushPacket();
  100. }
  101. void MiLightClient::updateHue(const uint16_t hue) {
  102. #ifdef DEBUG_CLIENT_COMMANDS
  103. Serial.printf_P(PSTR("MiLightClient::updateHue: Change hue to %d\n"), hue);
  104. #endif
  105. currentRemote->packetFormatter->updateHue(hue);
  106. flushPacket();
  107. }
  108. void MiLightClient::updateBrightness(const uint8_t brightness) {
  109. #ifdef DEBUG_CLIENT_COMMANDS
  110. Serial.printf_P(PSTR("MiLightClient::updateBrightness: Change brightness to %d\n"), brightness);
  111. #endif
  112. currentRemote->packetFormatter->updateBrightness(brightness);
  113. flushPacket();
  114. }
  115. void MiLightClient::updateMode(uint8_t mode) {
  116. #ifdef DEBUG_CLIENT_COMMANDS
  117. Serial.printf_P(PSTR("MiLightClient::updateMode: Change mode to %d\n"), mode);
  118. #endif
  119. currentRemote->packetFormatter->updateMode(mode);
  120. flushPacket();
  121. }
  122. void MiLightClient::nextMode() {
  123. #ifdef DEBUG_CLIENT_COMMANDS
  124. Serial.println(F("MiLightClient::nextMode: Switch to next mode"));
  125. #endif
  126. currentRemote->packetFormatter->nextMode();
  127. flushPacket();
  128. }
  129. void MiLightClient::previousMode() {
  130. #ifdef DEBUG_CLIENT_COMMANDS
  131. Serial.println(F("MiLightClient::previousMode: Switch to previous mode"));
  132. #endif
  133. currentRemote->packetFormatter->previousMode();
  134. flushPacket();
  135. }
  136. void MiLightClient::modeSpeedDown() {
  137. #ifdef DEBUG_CLIENT_COMMANDS
  138. Serial.println(F("MiLightClient::modeSpeedDown: Speed down\n"));
  139. #endif
  140. currentRemote->packetFormatter->modeSpeedDown();
  141. flushPacket();
  142. }
  143. void MiLightClient::modeSpeedUp() {
  144. #ifdef DEBUG_CLIENT_COMMANDS
  145. Serial.println(F("MiLightClient::modeSpeedUp: Speed up"));
  146. #endif
  147. currentRemote->packetFormatter->modeSpeedUp();
  148. flushPacket();
  149. }
  150. void MiLightClient::updateStatus(MiLightStatus status, uint8_t groupId) {
  151. #ifdef DEBUG_CLIENT_COMMANDS
  152. Serial.printf_P(PSTR("MiLightClient::updateStatus: Status %s, groupId %d\n"), status == MiLightStatus::OFF ? "OFF" : "ON", groupId);
  153. #endif
  154. currentRemote->packetFormatter->updateStatus(status, groupId);
  155. flushPacket();
  156. }
  157. void MiLightClient::updateStatus(MiLightStatus status) {
  158. #ifdef DEBUG_CLIENT_COMMANDS
  159. Serial.printf_P(PSTR("MiLightClient::updateStatus: Status %s\n"), status == MiLightStatus::OFF ? "OFF" : "ON");
  160. #endif
  161. currentRemote->packetFormatter->updateStatus(status);
  162. flushPacket();
  163. }
  164. void MiLightClient::updateSaturation(const uint8_t value) {
  165. #ifdef DEBUG_CLIENT_COMMANDS
  166. Serial.printf_P(PSTR("MiLightClient::updateSaturation: Saturation %d\n"), value);
  167. #endif
  168. currentRemote->packetFormatter->updateSaturation(value);
  169. flushPacket();
  170. }
  171. void MiLightClient::updateColorWhite() {
  172. #ifdef DEBUG_CLIENT_COMMANDS
  173. Serial.println(F("MiLightClient::updateColorWhite: Color white"));
  174. #endif
  175. currentRemote->packetFormatter->updateColorWhite();
  176. flushPacket();
  177. }
  178. void MiLightClient::enableNightMode() {
  179. #ifdef DEBUG_CLIENT_COMMANDS
  180. Serial.println(F("MiLightClient::enableNightMode: Night mode"));
  181. #endif
  182. currentRemote->packetFormatter->enableNightMode();
  183. flushPacket();
  184. }
  185. void MiLightClient::pair() {
  186. #ifdef DEBUG_CLIENT_COMMANDS
  187. Serial.println(F("MiLightClient::pair: Pair"));
  188. #endif
  189. currentRemote->packetFormatter->pair();
  190. flushPacket();
  191. }
  192. void MiLightClient::unpair() {
  193. #ifdef DEBUG_CLIENT_COMMANDS
  194. Serial.println(F("MiLightClient::unpair: Unpair"));
  195. #endif
  196. currentRemote->packetFormatter->unpair();
  197. flushPacket();
  198. }
  199. void MiLightClient::increaseBrightness() {
  200. #ifdef DEBUG_CLIENT_COMMANDS
  201. Serial.println(F("MiLightClient::increaseBrightness: Increase brightness"));
  202. #endif
  203. currentRemote->packetFormatter->increaseBrightness();
  204. flushPacket();
  205. }
  206. void MiLightClient::decreaseBrightness() {
  207. #ifdef DEBUG_CLIENT_COMMANDS
  208. Serial.println(F("MiLightClient::decreaseBrightness: Decrease brightness"));
  209. #endif
  210. currentRemote->packetFormatter->decreaseBrightness();
  211. flushPacket();
  212. }
  213. void MiLightClient::increaseTemperature() {
  214. #ifdef DEBUG_CLIENT_COMMANDS
  215. Serial.println(F("MiLightClient::increaseTemperature: Increase temperature"));
  216. #endif
  217. currentRemote->packetFormatter->increaseTemperature();
  218. flushPacket();
  219. }
  220. void MiLightClient::decreaseTemperature() {
  221. #ifdef DEBUG_CLIENT_COMMANDS
  222. Serial.println(F("MiLightClient::decreaseTemperature: Decrease temperature"));
  223. #endif
  224. currentRemote->packetFormatter->decreaseTemperature();
  225. flushPacket();
  226. }
  227. void MiLightClient::updateTemperature(const uint8_t temperature) {
  228. #ifdef DEBUG_CLIENT_COMMANDS
  229. Serial.printf_P(PSTR("MiLightClient::updateTemperature: Set temperature to %d\n"), temperature);
  230. #endif
  231. currentRemote->packetFormatter->updateTemperature(temperature);
  232. flushPacket();
  233. }
  234. void MiLightClient::command(uint8_t command, uint8_t arg) {
  235. #ifdef DEBUG_CLIENT_COMMANDS
  236. Serial.printf_P(PSTR("MiLightClient::command: Execute command %d, argument %d\n"), command, arg);
  237. #endif
  238. currentRemote->packetFormatter->command(command, arg);
  239. flushPacket();
  240. }
  241. void MiLightClient::toggleStatus() {
  242. #ifdef DEBUG_CLIENT_COMMANDS
  243. Serial.printf_P(PSTR("MiLightClient::toggleStatus"));
  244. #endif
  245. currentRemote->packetFormatter->toggleStatus();
  246. flushPacket();
  247. }
  248. void MiLightClient::updateColor(JsonVariant json) {
  249. ParsedColor color = ParsedColor::fromJson(json);
  250. if (!color.success) {
  251. Serial.println(F("Error parsing JSON color"));
  252. return;
  253. }
  254. // We consider an RGB color "white" if all color intensities are roughly the
  255. // same value. An unscientific value of 10 (~4%) is chosen.
  256. if ( abs(color.r - color.g) < RGB_WHITE_THRESHOLD
  257. && abs(color.g - color.b) < RGB_WHITE_THRESHOLD
  258. && abs(color.r - color.b) < RGB_WHITE_THRESHOLD) {
  259. this->updateColorWhite();
  260. } else {
  261. this->updateHue(color.hue);
  262. this->updateSaturation(color.saturation);
  263. }
  264. }
  265. void MiLightClient::update(JsonObject request) {
  266. if (this->updateBeginHandler) {
  267. this->updateBeginHandler();
  268. }
  269. const uint8_t parsedStatus = this->parseStatus(request);
  270. const JsonVariant jsonTransition = request[RequestKeys::TRANSITION];
  271. float transition = 0;
  272. if (!jsonTransition.isNull()) {
  273. if (jsonTransition.is<float>()) {
  274. transition = jsonTransition.as<float>();
  275. } else if (jsonTransition.is<size_t>()) {
  276. transition = jsonTransition.as<size_t>();
  277. } else {
  278. Serial.println(F("MiLightClient - WARN: unsupported transition type. Must be float or int."));
  279. }
  280. }
  281. // Always turn on first
  282. if (parsedStatus == ON) {
  283. this->updateStatus(ON);
  284. }
  285. for (const char* fieldName : FIELD_ORDERINGS) {
  286. if (request.containsKey(fieldName)) {
  287. auto handler = FIELD_SETTERS.find(fieldName);
  288. JsonVariant value = request[fieldName];
  289. if (handler != FIELD_SETTERS.end()) {
  290. if (transition != 0) {
  291. handleTransition(
  292. GroupStateFieldHelpers::getFieldByName(fieldName),
  293. value,
  294. transition
  295. );
  296. } else {
  297. handler->second(this, value);
  298. }
  299. }
  300. }
  301. }
  302. // Raw packet command/args
  303. if (request.containsKey("button_id") && request.containsKey("argument")) {
  304. this->command(request["button_id"], request["argument"]);
  305. }
  306. // Always turn off last
  307. if (parsedStatus == OFF) {
  308. this->updateStatus(OFF);
  309. }
  310. if (this->updateEndHandler) {
  311. this->updateEndHandler();
  312. }
  313. }
  314. void MiLightClient::handleCommands(JsonArray commands) {
  315. if (! commands.isNull()) {
  316. for (size_t i = 0; i < commands.size(); i++) {
  317. this->handleCommand(commands[i]);
  318. }
  319. }
  320. }
  321. void MiLightClient::handleCommand(JsonVariant command) {
  322. String cmdName;
  323. JsonObject args;
  324. if (command.is<JsonObject>()) {
  325. JsonObject cmdObj = command.as<JsonObject>();
  326. cmdName = cmdObj[GroupStateFieldNames::COMMAND].as<const char*>();
  327. args = cmdObj["args"];
  328. } else if (command.is<const char*>()) {
  329. cmdName = command.as<const char*>();
  330. }
  331. if (cmdName == MiLightCommandNames::UNPAIR) {
  332. this->unpair();
  333. } else if (cmdName == MiLightCommandNames::PAIR) {
  334. this->pair();
  335. } else if (cmdName == MiLightCommandNames::SET_WHITE) {
  336. this->updateColorWhite();
  337. } else if (cmdName == MiLightCommandNames::NIGHT_MODE) {
  338. this->enableNightMode();
  339. } else if (cmdName == MiLightCommandNames::LEVEL_UP) {
  340. this->increaseBrightness();
  341. } else if (cmdName == MiLightCommandNames::LEVEL_DOWN) {
  342. this->decreaseBrightness();
  343. } else if (cmdName == MiLightCommandNames::TEMPERATURE_UP) {
  344. this->increaseTemperature();
  345. } else if (cmdName == MiLightCommandNames::TEMPERATURE_DOWN) {
  346. this->decreaseTemperature();
  347. } else if (cmdName == MiLightCommandNames::NEXT_MODE) {
  348. this->nextMode();
  349. } else if (cmdName == MiLightCommandNames::PREVIOUS_MODE) {
  350. this->previousMode();
  351. } else if (cmdName == MiLightCommandNames::MODE_SPEED_DOWN) {
  352. this->modeSpeedDown();
  353. } else if (cmdName == MiLightCommandNames::MODE_SPEED_UP) {
  354. this->modeSpeedUp();
  355. } else if (cmdName == MiLightCommandNames::TOGGLE) {
  356. this->toggleStatus();
  357. } else if (cmdName == MiLightCommandNames::TRANSITION) {
  358. StaticJsonDocument<100> fakedoc;
  359. this->handleTransition(args, fakedoc);
  360. }
  361. }
  362. void MiLightClient::handleTransition(GroupStateField field, JsonVariant value, float duration) {
  363. BulbId bulbId = currentRemote->packetFormatter->currentBulbId();
  364. GroupState* currentState = stateStore->get(bulbId);
  365. std::shared_ptr<Transition::Builder> transitionBuilder = nullptr;
  366. if (currentState == nullptr) {
  367. Serial.println(F("Error planning transition: could not find current bulb state."));
  368. return;
  369. }
  370. if (!currentState->isSetField(field)) {
  371. Serial.println(F("Error planning transition: current state for field could not be determined"));
  372. return;
  373. }
  374. if (field == GroupStateField::COLOR) {
  375. ParsedColor currentColor = currentState->getColor();
  376. ParsedColor endColor = ParsedColor::fromJson(value);
  377. transitionBuilder = transitions.buildColorTransition(
  378. bulbId,
  379. currentColor,
  380. endColor
  381. );
  382. } else {
  383. uint16_t currentValue = currentState->getParsedFieldValue(field);
  384. uint16_t endValue = value;
  385. transitionBuilder = transitions.buildFieldTransition(
  386. bulbId,
  387. field,
  388. currentValue,
  389. endValue
  390. );
  391. }
  392. if (transitionBuilder == nullptr) {
  393. Serial.printf_P(PSTR("Unsupported transition field: %s\n"), GroupStateFieldHelpers::getFieldName(field));
  394. return;
  395. }
  396. transitionBuilder->setDuration(duration);
  397. transitions.addTransition(transitionBuilder->build());
  398. }
  399. bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj) {
  400. if (! args.containsKey(FS(TransitionParams::FIELD))
  401. || ! args.containsKey(FS(TransitionParams::START_VALUE))
  402. || ! args.containsKey(FS(TransitionParams::END_VALUE))) {
  403. responseObj[F("error")] = F("Ignoring transition missing required arguments");
  404. return false;
  405. }
  406. const char* fieldName = args[FS(TransitionParams::FIELD)];
  407. GroupStateField field = GroupStateFieldHelpers::getFieldByName(fieldName);
  408. std::shared_ptr<Transition::Builder> transitionBuilder = nullptr;
  409. if (field == GroupStateField::UNKNOWN) {
  410. char errorMsg[30];
  411. sprintf_P(errorMsg, PSTR("Unknown transition field: %s\n"), fieldName);
  412. responseObj[F("error")] = errorMsg;
  413. return false;
  414. }
  415. // These fields can be transitioned directly.
  416. switch (field) {
  417. case GroupStateField::HUE:
  418. case GroupStateField::SATURATION:
  419. case GroupStateField::BRIGHTNESS:
  420. case GroupStateField::LEVEL:
  421. case GroupStateField::KELVIN:
  422. case GroupStateField::COLOR_TEMP:
  423. transitionBuilder = transitions.buildFieldTransition(
  424. currentRemote->packetFormatter->currentBulbId(),
  425. field,
  426. args[FS(TransitionParams::START_VALUE)],
  427. args[FS(TransitionParams::END_VALUE)]
  428. );
  429. break;
  430. default:
  431. break;
  432. }
  433. // Color can be decomposed into hue/saturation and these can be transitioned separately
  434. if (field == GroupStateField::COLOR) {
  435. ParsedColor startColor = ParsedColor::fromJson(args[FS(TransitionParams::START_VALUE)]);
  436. ParsedColor endColor = ParsedColor::fromJson(args[FS(TransitionParams::END_VALUE)]);
  437. if (! startColor.success) {
  438. responseObj[F("error")] = F("Transition - error parsing start color");
  439. return false;
  440. }
  441. if (! endColor.success) {
  442. responseObj[F("error")] = F("Transition - error parsing end color");
  443. return false;
  444. }
  445. transitionBuilder = transitions.buildColorTransition(
  446. currentRemote->packetFormatter->currentBulbId(),
  447. startColor,
  448. endColor
  449. );
  450. }
  451. if (transitionBuilder == nullptr) {
  452. char errorMsg[30];
  453. sprintf_P(errorMsg, PSTR("Recognized, but unsupported transition field: %s\n"), fieldName);
  454. responseObj[F("error")] = errorMsg;
  455. return false;
  456. }
  457. if (args.containsKey(FS(TransitionParams::DURATION))) {
  458. transitionBuilder->setDuration(args[FS(TransitionParams::DURATION)]);
  459. }
  460. if (args.containsKey(FS(TransitionParams::PERIOD))) {
  461. transitionBuilder->setPeriod(args[FS(TransitionParams::PERIOD)]);
  462. }
  463. if (args.containsKey(FS(TransitionParams::NUM_PERIODS))) {
  464. transitionBuilder->setNumPeriods(args[FS(TransitionParams::NUM_PERIODS)]);
  465. }
  466. transitions.addTransition(transitionBuilder->build());
  467. return true;
  468. }
  469. void MiLightClient::handleEffect(const String& effect) {
  470. if (effect == MiLightCommandNames::NIGHT_MODE) {
  471. this->enableNightMode();
  472. } else if (effect == "white" || effect == "white_mode") {
  473. this->updateColorWhite();
  474. } else { // assume we're trying to set mode
  475. this->updateMode(effect.toInt());
  476. }
  477. }
  478. uint8_t MiLightClient::parseStatus(JsonObject object) {
  479. JsonVariant status;
  480. if (object.containsKey(GroupStateFieldNames::STATUS)) {
  481. status = object[GroupStateFieldNames::STATUS];
  482. } else if (object.containsKey(GroupStateFieldNames::STATE)) {
  483. status = object[GroupStateFieldNames::STATE];
  484. } else {
  485. return 255;
  486. }
  487. if (status.is<bool>()) {
  488. return status.as<bool>() ? ON : OFF;
  489. } else {
  490. String strStatus(status.as<const char*>());
  491. return (strStatus.equalsIgnoreCase("on") || strStatus.equalsIgnoreCase("true")) ? ON : OFF;
  492. }
  493. }
  494. void MiLightClient::setRepeatsOverride(size_t repeats) {
  495. this->repeatsOverride = repeats;
  496. }
  497. void MiLightClient::clearRepeatsOverride() {
  498. this->repeatsOverride = PacketSender::DEFAULT_PACKET_SENDS_VALUE;
  499. }
  500. void MiLightClient::flushPacket() {
  501. PacketStream& stream = currentRemote->packetFormatter->buildPackets();
  502. while (stream.hasNext()) {
  503. packetSender.enqueue(stream.next(), currentRemote, repeatsOverride);
  504. }
  505. currentRemote->packetFormatter->reset();
  506. }
  507. void MiLightClient::onUpdateBegin(EventHandler handler) {
  508. this->updateBeginHandler = handler;
  509. }
  510. void MiLightClient::onUpdateEnd(EventHandler handler) {
  511. this->updateEndHandler = handler;
  512. }