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.

401 lines
8.9 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 DefaultLowButtonId { 1u };
  80. constexpr unsigned char DefaultMediumButtonId { 2u };
  81. constexpr unsigned char DefaultHighButtonId { 3u };
  82. // We expect to write a specific 'mask' via GPIO LOW & HIGH to set the speed
  83. // Sync up with the relay and write it on ON / OFF status events
  84. constexpr size_t Gpios { 3ul };
  85. using State = std::array<int8_t, Gpios>;
  86. using Pin = std::pair<int, BasePinPtr>;
  87. using StatePins = std::array<Pin, Gpios>;
  88. // XXX: while these are hard-coded, we don't really benefit from having these in the hardware cfg
  89. StatePins statePins() {
  90. return {
  91. {{5, nullptr},
  92. {4, nullptr},
  93. {15, nullptr}}
  94. };
  95. }
  96. constexpr int controlPin() {
  97. return 12;
  98. }
  99. struct Config {
  100. Config() = default;
  101. explicit Config(unsigned long save_, unsigned char buttonLowId_,
  102. unsigned char buttonMediumId_, unsigned char buttonHighId_, Speed speed_) :
  103. save(save_),
  104. buttonLowId(buttonLowId_),
  105. buttonMediumId(buttonMediumId_),
  106. buttonHighId(buttonHighId_),
  107. speed(speed_)
  108. {}
  109. unsigned long save { DefaultSaveDelay };
  110. unsigned char buttonLowId { DefaultLowButtonId };
  111. unsigned char buttonMediumId { DefaultMediumButtonId };
  112. unsigned char buttonHighId { DefaultHighButtonId };
  113. Speed speed { Speed::Off };
  114. StatePins state_pins;
  115. };
  116. Config readSettings() {
  117. return Config(
  118. getSetting("ifanSave", DefaultSaveDelay),
  119. getSetting("ifanBtnLowId", DefaultLowButtonId),
  120. getSetting("ifanBtnMediumId", DefaultMediumButtonId),
  121. getSetting("ifanBtnHighId", DefaultHighButtonId),
  122. getSetting("ifanSpeed", Speed::Medium)
  123. );
  124. }
  125. Config config;
  126. void configure() {
  127. config = readSettings();
  128. }
  129. void report(Speed speed [[gnu::unused]]) {
  130. #if MQTT_SUPPORT
  131. mqttSend(MQTT_TOPIC_SPEED, speedToPayload(speed));
  132. #endif
  133. }
  134. void save(Speed speed) {
  135. static Ticker ticker;
  136. config.speed = speed;
  137. ticker.once_ms(config.save, []() {
  138. const char* value = speedToPayload(config.speed);
  139. setSetting("ifanSpeed", value);
  140. DEBUG_MSG_P(PSTR("[IFAN] Saved speed setting \"%s\"\n"), value);
  141. });
  142. }
  143. void cleanupPins(StatePins& pins) {
  144. for (auto& pin : pins) {
  145. if (!pin.second) continue;
  146. gpioUnlock(pin.second->pin());
  147. pin.second.reset(nullptr);
  148. }
  149. }
  150. StatePins setupStatePins() {
  151. StatePins pins = statePins();
  152. for (auto& pair : pins) {
  153. auto ptr = gpioRegister(pair.first);
  154. if (!ptr) {
  155. DEBUG_MSG_P(PSTR("[IFAN] Could not set up GPIO%d\n"), pair.first);
  156. cleanupPins(pins);
  157. return pins;
  158. }
  159. ptr->pinMode(OUTPUT);
  160. pair.second = std::move(ptr);
  161. }
  162. return pins;
  163. }
  164. State stateFromSpeed(Speed speed) {
  165. switch (speed) {
  166. case Speed::Low:
  167. return {HIGH, LOW, LOW};
  168. case Speed::Medium:
  169. return {HIGH, HIGH, LOW};
  170. case Speed::High:
  171. return {HIGH, LOW, HIGH};
  172. case Speed::Off:
  173. break;
  174. }
  175. return {LOW, LOW, LOW};
  176. }
  177. const char* maskFromSpeed(Speed speed) {
  178. switch (speed) {
  179. case Speed::Low:
  180. return "0b100";
  181. case Speed::Medium:
  182. return "0b110";
  183. case Speed::High:
  184. return "0b101";
  185. case Speed::Off:
  186. return "0b000";
  187. }
  188. return "";
  189. }
  190. // Note that we use API speed endpoint strictly for the setting
  191. // (which also allows to pre-set the speed without turning the relay ON)
  192. using FanSpeedUpdate = std::function<void(Speed)>;
  193. FanSpeedUpdate onSpeedUpdate = [](Speed) {
  194. };
  195. void updateSpeed(Config& config, Speed speed) {
  196. switch (speed) {
  197. case Speed::Low:
  198. case Speed::Medium:
  199. case Speed::High:
  200. save(speed);
  201. report(speed);
  202. onSpeedUpdate(speed);
  203. break;
  204. case Speed::Off:
  205. break;
  206. }
  207. }
  208. void updateSpeed(Speed speed) {
  209. updateSpeed(config, speed);
  210. }
  211. void updateSpeedFromPayload(const char* payload) {
  212. updateSpeed(payloadToSpeed(payload));
  213. }
  214. void updateSpeedFromPayload(const String& payload) {
  215. updateSpeedFromPayload(payload.c_str());
  216. }
  217. #if MQTT_SUPPORT
  218. void onMqttEvent(unsigned int type, const char* topic, const char* payload) {
  219. switch (type) {
  220. case MQTT_CONNECT_EVENT:
  221. mqttSubscribe(MQTT_TOPIC_SPEED);
  222. break;
  223. case MQTT_MESSAGE_EVENT: {
  224. auto parsed = mqttMagnitude(topic);
  225. if (parsed.startsWith(MQTT_TOPIC_SPEED)) {
  226. updateSpeedFromPayload(payload);
  227. }
  228. break;
  229. }
  230. }
  231. }
  232. #endif // MQTT_SUPPORT
  233. class FanProvider : public RelayProviderBase {
  234. public:
  235. explicit FanProvider(BasePinPtr&& pin, const Config& config, FanSpeedUpdate& callback) :
  236. _pin(std::move(pin)),
  237. _config(config)
  238. {
  239. callback = [this](Speed speed) {
  240. change(speed);
  241. };
  242. }
  243. const char* id() const override {
  244. return "fan";
  245. }
  246. void change(Speed speed) {
  247. _pin->digitalWrite((Speed::Off != speed) ? HIGH : LOW);
  248. auto state = stateFromSpeed(speed);
  249. DEBUG_MSG_P(PSTR("[IFAN] State mask: %s\n"), maskFromSpeed(speed));
  250. for (size_t index = 0; index < _config.state_pins.size(); ++index) {
  251. auto& pin = _config.state_pins[index].second;
  252. if (!pin) {
  253. continue;
  254. }
  255. pin->digitalWrite(state[index]);
  256. }
  257. }
  258. void change(bool status) override {
  259. change(status ? _config.speed : Speed::Off);
  260. }
  261. private:
  262. BasePinPtr _pin;
  263. const Config& _config;
  264. };
  265. void setup() {
  266. config.state_pins = setupStatePins();
  267. if (!config.state_pins.size()) {
  268. return;
  269. }
  270. configure();
  271. espurnaRegisterReload(configure);
  272. auto relay_pin = gpioRegister(controlPin());
  273. if (relay_pin) {
  274. relay_pin->pinMode(OUTPUT);
  275. auto provider = std::make_unique<FanProvider>(std::move(relay_pin), config, onSpeedUpdate);
  276. if (!relayAdd(std::move(provider))) {
  277. DEBUG_MSG_P(PSTR("[IFAN] Could not add relay provider for GPIO%d\n"), controlPin());
  278. gpioUnlock(controlPin());
  279. }
  280. }
  281. #if BUTTON_SUPPORT
  282. buttonSetCustomAction([](unsigned char id) {
  283. if (config.buttonLowId == id) {
  284. updateSpeed(Speed::Low);
  285. } else if (config.buttonMediumId == id) {
  286. updateSpeed(Speed::Medium);
  287. } else if (config.buttonHighId == id) {
  288. updateSpeed(Speed::High);
  289. }
  290. });
  291. #endif
  292. #if MQTT_SUPPORT
  293. mqttRegister(onMqttEvent);
  294. #endif
  295. #if API_SUPPORT
  296. apiRegister(F(MQTT_TOPIC_SPEED),
  297. [](ApiRequest& request) {
  298. request.send(speedToPayload(config.speed));
  299. return true;
  300. },
  301. [](ApiRequest& request) {
  302. updateSpeedFromPayload(request.param(F("value")));
  303. return true;
  304. }
  305. );
  306. #endif
  307. #if TERMINAL_SUPPORT
  308. terminalRegisterCommand(F("SPEED"), [](const terminal::CommandContext& ctx) {
  309. if (ctx.argc == 2) {
  310. updateSpeedFromPayload(ctx.argv[1]);
  311. }
  312. ctx.output.println(speedToPayload(config.speed));
  313. terminalOK(ctx);
  314. });
  315. #endif
  316. }
  317. } // namespace ifan
  318. void ifanSetup() {
  319. ifan02::setup();
  320. }
  321. #endif // IFAN_SUPPORT