MiLightClient.cpp 18 KB

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