Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

357 lines
7.8 KiB

  1. /*
  2. iFan02 MODULE
  3. Copyright (C) 2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
  4. Original implementation via RELAY module
  5. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  6. */
  7. #include "espurna.h"
  8. #if IFAN_SUPPORT
  9. #include "api.h"
  10. #include "button.h"
  11. #include "mqtt.h"
  12. #include "relay.h"
  13. #include "terminal.h"
  14. #include <array>
  15. #include <utility>
  16. namespace ifan02 {
  17. enum class Speed {
  18. Off,
  19. Low,
  20. Medium,
  21. High
  22. };
  23. const char* speedToPayload(Speed value) {
  24. switch (value) {
  25. case Speed::Off:
  26. return "off";
  27. case Speed::Low:
  28. return "low";
  29. case Speed::Medium:
  30. return "medium";
  31. case Speed::High:
  32. return "high";
  33. }
  34. return "";
  35. }
  36. Speed payloadToSpeed(const char* payload) {
  37. auto len = strlen(payload);
  38. if (len == 1) {
  39. switch (payload[0]) {
  40. case '0':
  41. return Speed::Off;
  42. case '1':
  43. return Speed::Low;
  44. case '2':
  45. return Speed::Medium;
  46. case '3':
  47. return Speed::High;
  48. }
  49. } else if (len > 1) {
  50. String cmp(payload);
  51. if (cmp == "off") {
  52. return Speed::Off;
  53. } else if (cmp == "low") {
  54. return Speed::Low;
  55. } else if (cmp == "medium") {
  56. return Speed::Medium;
  57. } else if (cmp == "high") {
  58. return Speed::High;
  59. }
  60. }
  61. return Speed::Off;
  62. }
  63. Speed payloadToSpeed(const String& string) {
  64. return payloadToSpeed(string.c_str());
  65. }
  66. } // namespace ifan02
  67. namespace settings {
  68. namespace internal {
  69. template <>
  70. ifan02::Speed convert(const String& value) {
  71. return ifan02::payloadToSpeed(value);
  72. }
  73. } // namespace internal
  74. } // namespace settings
  75. namespace ifan02 {
  76. constexpr unsigned long DefaultSaveDelay { 1000ul };
  77. // Remote presses trigger GPIO pushbutton events
  78. // Attach to a specific ID to trigger an action
  79. constexpr unsigned char DefaultRelayId { 0u };
  80. constexpr unsigned char DefaultLowButtonId { 1u };
  81. constexpr unsigned char DefaultMediumButtonId { 2u };
  82. constexpr unsigned char DefaultHighButtonId { 3u };
  83. // We expect to write a specific 'mask' via GPIO LOW & HIGH to set the speed
  84. // Sync up with the relay and write it on ON / OFF status events
  85. constexpr size_t Gpios { 3ul };
  86. using State = std::array<int8_t, Gpios>;
  87. using Pin = std::pair<int, BasePinPtr>;
  88. using StatePins = std::array<Pin, Gpios>;
  89. // XXX: while these are hard-coded, we don't really benefit from having these in the hardware cfg
  90. StatePins statePins() {
  91. return {
  92. {{5, nullptr},
  93. {4, nullptr},
  94. {15, nullptr}}
  95. };
  96. }
  97. struct Config {
  98. unsigned long save { DefaultSaveDelay };
  99. unsigned char relayId { DefaultRelayId };
  100. unsigned char buttonLowId { DefaultLowButtonId };
  101. unsigned char buttonMediumId { DefaultMediumButtonId };
  102. unsigned char buttonHighId { DefaultHighButtonId };
  103. Speed speed { Speed::Off };
  104. StatePins state_pins;
  105. };
  106. Config readSettings() {
  107. return {
  108. getSetting("ifanSave", DefaultSaveDelay),
  109. getSetting("ifanRelayId", DefaultRelayId),
  110. getSetting("ifanBtnLowId", DefaultLowButtonId),
  111. getSetting("ifanBtnLowId", DefaultMediumButtonId),
  112. getSetting("ifanBtnHighId", DefaultHighButtonId),
  113. getSetting("ifanSpeed", Speed::Medium)
  114. };
  115. }
  116. Config config;
  117. void configure() {
  118. config = readSettings();
  119. }
  120. void report(Speed speed [[gnu::unused]]) {
  121. #if MQTT_SUPPORT
  122. mqttSend(MQTT_TOPIC_SPEED, speedToPayload(speed));
  123. #endif
  124. }
  125. void save(Speed speed) {
  126. static Ticker ticker;
  127. config.speed = speed;
  128. ticker.once_ms(config.save, []() {
  129. const char* value = speedToPayload(config.speed);
  130. setSetting("ifanSpeed", value);
  131. DEBUG_MSG_P(PSTR("[IFAN] Saved speed setting \"%s\"\n"), value);
  132. });
  133. }
  134. void cleanupPins(StatePins& pins) {
  135. for (auto& pin : pins) {
  136. if (!pin.second) continue;
  137. gpioUnlock(pin.second->pin());
  138. pin.second.reset(nullptr);
  139. }
  140. }
  141. StatePins setupStatePins() {
  142. StatePins pins = statePins();
  143. for (auto& pair : pins) {
  144. auto ptr = gpioRegister(pair.first);
  145. if (!ptr) {
  146. DEBUG_MSG_P(PSTR("[IFAN] Could not set up GPIO%d\n"), pair.first);
  147. cleanupPins(pins);
  148. return pins;
  149. }
  150. ptr->pinMode(OUTPUT);
  151. pair.second = std::move(ptr);
  152. }
  153. return pins;
  154. }
  155. State stateFromSpeed(Speed speed) {
  156. switch (speed) {
  157. case Speed::Low:
  158. return {HIGH, LOW, LOW};
  159. case Speed::Medium:
  160. return {HIGH, HIGH, LOW};
  161. case Speed::High:
  162. return {HIGH, LOW, HIGH};
  163. case Speed::Off:
  164. break;
  165. }
  166. return {LOW, LOW, LOW};
  167. }
  168. const char* maskFromSpeed(Speed speed) {
  169. switch (speed) {
  170. case Speed::Low:
  171. return "0b100";
  172. case Speed::Medium:
  173. return "0b110";
  174. case Speed::High:
  175. return "0b101";
  176. case Speed::Off:
  177. return "0b000";
  178. }
  179. return "";
  180. }
  181. void setSpeed(StatePins& pins, Speed speed) {
  182. auto state = stateFromSpeed(speed);
  183. DEBUG_MSG_P(PSTR("[IFAN] State mask: %s\n"), maskFromSpeed(speed));
  184. for (size_t index = 0; index < pins.size(); ++ index) {
  185. if (!pins[index].second) continue;
  186. pins[index].second->digitalWrite(state[index]);
  187. }
  188. }
  189. void setSpeed(Speed speed) {
  190. setSpeed(config.state_pins, speed);
  191. }
  192. // Note that we use API speed endpoint strictly for the setting
  193. // (which also allows to pre-set the speed without turning the relay ON)
  194. void setSpeedFromPayload(const char* payload) {
  195. auto speed = payloadToSpeed(payload);
  196. switch (speed) {
  197. case Speed::Low:
  198. case Speed::Medium:
  199. case Speed::High:
  200. setSpeed(speed);
  201. report(speed);
  202. save(speed);
  203. break;
  204. case Speed::Off:
  205. break;
  206. }
  207. }
  208. void setSpeedFromPayload(const String& payload) {
  209. setSpeedFromPayload(payload.c_str());
  210. }
  211. #if MQTT_SUPPORT
  212. void onMqttEvent(unsigned int type, const char* topic, const char* payload) {
  213. switch (type) {
  214. case MQTT_CONNECT_EVENT:
  215. mqttSubscribe(MQTT_TOPIC_SPEED);
  216. break;
  217. case MQTT_MESSAGE_EVENT: {
  218. auto parsed = mqttMagnitude(topic);
  219. if (parsed.startsWith(MQTT_TOPIC_SPEED)) {
  220. setSpeedFromPayload(payload);
  221. }
  222. break;
  223. }
  224. }
  225. }
  226. #endif // MQTT_SUPPORT
  227. void setSpeedFromStatus(bool status) {
  228. setSpeed(status ? config.speed : Speed::Off);
  229. }
  230. void setup() {
  231. config.state_pins = setupStatePins();
  232. if (!config.state_pins.size()) {
  233. return;
  234. }
  235. configure();
  236. espurnaRegisterReload(configure);
  237. #if BUTTON_SUPPORT
  238. buttonSetCustomAction([](unsigned char id) {
  239. if (config.buttonLowId == id) {
  240. setSpeed(Speed::Low);
  241. } else if (config.buttonMediumId == id) {
  242. setSpeed(Speed::Medium);
  243. } else if (config.buttonHighId == id) {
  244. setSpeed(Speed::High);
  245. }
  246. });
  247. #endif
  248. #if RELAY_SUPPORT
  249. setSpeedFromStatus(relayStatus(config.relayId));
  250. relaySetStatusChange([](unsigned char id, bool status) {
  251. if (config.relayId == id) {
  252. setSpeedFromStatus(status);
  253. }
  254. });
  255. #endif
  256. #if MQTT_SUPPORT
  257. mqttRegister(onMqttEvent);
  258. #endif
  259. #if API_SUPPORT
  260. apiRegister(F(MQTT_TOPIC_SPEED),
  261. [](ApiRequest& request) {
  262. request.send(speedToPayload(config.speed));
  263. return true;
  264. },
  265. [](ApiRequest& request) {
  266. setSpeedFromPayload(request.param(F("value")));
  267. return true;
  268. }
  269. );
  270. #endif
  271. #if TERMINAL_SUPPORT
  272. terminalRegisterCommand(F("SPEED"), [](const terminal::CommandContext& ctx) {
  273. if (ctx.argc == 2) {
  274. setSpeedFromPayload(ctx.argv[1]);
  275. }
  276. ctx.output.println(speedToPayload(config.speed));
  277. terminalOK(ctx);
  278. });
  279. #endif
  280. }
  281. } // namespace ifan
  282. void ifanSetup() {
  283. ifan02::setup();
  284. }
  285. #endif // IFAN_SUPPORT