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.

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