/* iFan02 MODULE Copyright (C) 2021 by Maxim Prokhorov Original implementation via RELAY module Copyright (C) 2016-2019 by Xose PĂ©rez */ #include "espurna.h" #if IFAN_SUPPORT #include "api.h" #include "button.h" #include "mqtt.h" #include "relay.h" #include "terminal.h" #include #include namespace ifan02 { enum class Speed { Off, Low, Medium, High }; const char* speedToPayload(Speed value) { switch (value) { case Speed::Off: return "off"; case Speed::Low: return "low"; case Speed::Medium: return "medium"; case Speed::High: return "high"; } return ""; } Speed payloadToSpeed(const char* payload) { auto len = strlen(payload); if (len == 1) { switch (payload[0]) { case '0': return Speed::Off; case '1': return Speed::Low; case '2': return Speed::Medium; case '3': return Speed::High; } } else if (len > 1) { String cmp(payload); if (cmp == "off") { return Speed::Off; } else if (cmp == "low") { return Speed::Low; } else if (cmp == "medium") { return Speed::Medium; } else if (cmp == "high") { return Speed::High; } } return Speed::Off; } Speed payloadToSpeed(const String& string) { return payloadToSpeed(string.c_str()); } } // namespace ifan02 namespace settings { namespace internal { template <> ifan02::Speed convert(const String& value) { return ifan02::payloadToSpeed(value); } } // namespace internal } // namespace settings namespace ifan02 { constexpr unsigned long DefaultSaveDelay { 1000ul }; // Remote presses trigger GPIO pushbutton events // Attach to a specific ID to trigger an action constexpr unsigned char DefaultLowButtonId { 1u }; constexpr unsigned char DefaultMediumButtonId { 2u }; constexpr unsigned char DefaultHighButtonId { 3u }; // We expect to write a specific 'mask' via GPIO LOW & HIGH to set the speed // Sync up with the relay and write it on ON / OFF status events constexpr size_t Gpios { 3ul }; using State = std::array; using Pin = std::pair; using StatePins = std::array; // XXX: while these are hard-coded, we don't really benefit from having these in the hardware cfg StatePins statePins() { return { {{5, nullptr}, {4, nullptr}, {15, nullptr}} }; } constexpr int controlPin() { return 12; } struct Config { Config() = default; explicit Config(unsigned long save_, unsigned char buttonLowId_, unsigned char buttonMediumId_, unsigned char buttonHighId_, Speed speed_) : save(save_), buttonLowId(buttonLowId_), buttonMediumId(buttonMediumId_), buttonHighId(buttonHighId_), speed(speed_) {} unsigned long save { DefaultSaveDelay }; unsigned char buttonLowId { DefaultLowButtonId }; unsigned char buttonMediumId { DefaultMediumButtonId }; unsigned char buttonHighId { DefaultHighButtonId }; Speed speed { Speed::Off }; StatePins state_pins; }; Config readSettings() { return Config( getSetting("ifanSave", DefaultSaveDelay), getSetting("ifanBtnLowId", DefaultLowButtonId), getSetting("ifanBtnMediumId", DefaultMediumButtonId), getSetting("ifanBtnHighId", DefaultHighButtonId), getSetting("ifanSpeed", Speed::Medium) ); } Config config; void configure() { config = readSettings(); } void report(Speed speed [[gnu::unused]]) { #if MQTT_SUPPORT mqttSend(MQTT_TOPIC_SPEED, speedToPayload(speed)); #endif } void save(Speed speed) { static Ticker ticker; config.speed = speed; ticker.once_ms(config.save, []() { const char* value = speedToPayload(config.speed); setSetting("ifanSpeed", value); DEBUG_MSG_P(PSTR("[IFAN] Saved speed setting \"%s\"\n"), value); }); } void cleanupPins(StatePins& pins) { for (auto& pin : pins) { if (!pin.second) continue; gpioUnlock(pin.second->pin()); pin.second.reset(nullptr); } } StatePins setupStatePins() { StatePins pins = statePins(); for (auto& pair : pins) { auto ptr = gpioRegister(pair.first); if (!ptr) { DEBUG_MSG_P(PSTR("[IFAN] Could not set up GPIO%d\n"), pair.first); cleanupPins(pins); return pins; } ptr->pinMode(OUTPUT); pair.second = std::move(ptr); } return pins; } State stateFromSpeed(Speed speed) { switch (speed) { case Speed::Low: return {HIGH, LOW, LOW}; case Speed::Medium: return {HIGH, HIGH, LOW}; case Speed::High: return {HIGH, LOW, HIGH}; case Speed::Off: break; } return {LOW, LOW, LOW}; } const char* maskFromSpeed(Speed speed) { switch (speed) { case Speed::Low: return "0b100"; case Speed::Medium: return "0b110"; case Speed::High: return "0b101"; case Speed::Off: return "0b000"; } return ""; } // Note that we use API speed endpoint strictly for the setting // (which also allows to pre-set the speed without turning the relay ON) using FanSpeedUpdate = std::function; FanSpeedUpdate onSpeedUpdate = [](Speed) { }; void updateSpeed(Config& config, Speed speed) { switch (speed) { case Speed::Low: case Speed::Medium: case Speed::High: save(speed); report(speed); onSpeedUpdate(speed); break; case Speed::Off: break; } } void updateSpeed(Speed speed) { updateSpeed(config, speed); } void updateSpeedFromPayload(const char* payload) { updateSpeed(payloadToSpeed(payload)); } void updateSpeedFromPayload(const String& payload) { updateSpeedFromPayload(payload.c_str()); } #if MQTT_SUPPORT void onMqttEvent(unsigned int type, const char* topic, const char* payload) { switch (type) { case MQTT_CONNECT_EVENT: mqttSubscribe(MQTT_TOPIC_SPEED); break; case MQTT_MESSAGE_EVENT: { auto parsed = mqttMagnitude(topic); if (parsed.startsWith(MQTT_TOPIC_SPEED)) { updateSpeedFromPayload(payload); } break; } } } #endif // MQTT_SUPPORT class FanProvider : public RelayProviderBase { public: explicit FanProvider(BasePinPtr&& pin, const Config& config, FanSpeedUpdate& callback) : _pin(std::move(pin)), _config(config) { callback = [this](Speed speed) { change(speed); }; } const char* id() const override { return "fan"; } void change(Speed speed) { _pin->digitalWrite((Speed::Off != speed) ? HIGH : LOW); auto state = stateFromSpeed(speed); DEBUG_MSG_P(PSTR("[IFAN] State mask: %s\n"), maskFromSpeed(speed)); for (size_t index = 0; index < _config.state_pins.size(); ++index) { auto& pin = _config.state_pins[index].second; if (!pin) { continue; } pin->digitalWrite(state[index]); } } void change(bool status) override { change(status ? _config.speed : Speed::Off); } private: BasePinPtr _pin; const Config& _config; }; void setup() { config.state_pins = setupStatePins(); if (!config.state_pins.size()) { return; } configure(); espurnaRegisterReload(configure); auto relay_pin = gpioRegister(controlPin()); if (relay_pin) { relay_pin->pinMode(OUTPUT); auto provider = std::make_unique(std::move(relay_pin), config, onSpeedUpdate); if (!relayAdd(std::move(provider))) { DEBUG_MSG_P(PSTR("[IFAN] Could not add relay provider for GPIO%d\n"), controlPin()); gpioUnlock(controlPin()); } } #if BUTTON_SUPPORT buttonSetCustomAction([](unsigned char id) { if (config.buttonLowId == id) { updateSpeed(Speed::Low); } else if (config.buttonMediumId == id) { updateSpeed(Speed::Medium); } else if (config.buttonHighId == id) { updateSpeed(Speed::High); } }); #endif #if MQTT_SUPPORT mqttRegister(onMqttEvent); #endif #if API_SUPPORT apiRegister(F(MQTT_TOPIC_SPEED), [](ApiRequest& request) { request.send(speedToPayload(config.speed)); return true; }, [](ApiRequest& request) { updateSpeedFromPayload(request.param(F("value"))); return true; } ); #endif #if TERMINAL_SUPPORT terminalRegisterCommand(F("SPEED"), [](const terminal::CommandContext& ctx) { if (ctx.argc == 2) { updateSpeedFromPayload(ctx.argv[1]); } ctx.output.println(speedToPayload(config.speed)); terminalOK(ctx); }); #endif } } // namespace ifan void ifanSetup() { ifan02::setup(); } #endif // IFAN_SUPPORT