/* 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 DefaultRelayId { 0u }; constexpr unsigned char DefaultStateButton { 0u }; constexpr unsigned char DefaultLowButton { 1u }; constexpr unsigned char DefaultMediumButton { 2u }; constexpr unsigned char DefaultHighButton { 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}} }; } struct Config { unsigned long save { DefaultSaveDelay }; unsigned char relayId { RELAY_NONE }; unsigned char buttonLowId { RELAY_NONE }; unsigned char buttonMediumId { RELAY_NONE }; unsigned char buttonHighId { RELAY_NONE }; Speed speed { Speed::Off }; StatePins state_pins; }; Config readSettings() { return { getSetting("ifanSave", DefaultSaveDelay), getSetting("ifanRelayId", DefaultRelayId), getSetting("ifanBtnLowId", DefaultMediumButton), getSetting("ifanBtnLowId", DefaultMediumButton), getSetting("ifanBtnHighId", DefaultHighButton), 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 ""; } void setSpeed(StatePins& pins, Speed speed) { auto state = stateFromSpeed(speed); DEBUG_MSG_P(PSTR("[IFAN] State mask: %s\n"), maskFromSpeed(speed)); for (size_t index = 0; index < pins.size(); ++ index) { if (!pins[index].second) continue; pins[index].second->digitalWrite(state[index]); } } void setSpeed(Speed speed) { setSpeed(config.state_pins, speed); } // Note that we use API speed endpoint strictly for the setting // (which also allows to pre-set the speed without turning the relay ON) void setSpeedFromPayload(const char* payload) { auto speed = payloadToSpeed(payload); switch (speed) { case Speed::Low: case Speed::Medium: case Speed::High: setSpeed(speed); report(speed); save(speed); break; case Speed::Off: break; } } void setSpeedFromPayload(const String& payload) { setSpeedFromPayload(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)) { setSpeedFromPayload(payload); } break; } } } #endif // MQTT_SUPPORT void setSpeedFromStatus(bool status) { setSpeed(status ? config.speed : Speed::Off); } void setup() { config.state_pins = setupStatePins(); if (!config.state_pins.size()) { return; } configure(); espurnaRegisterReload(configure); #if BUTTON_SUPPORT ButtonBroker::Register([](unsigned char id, button_event_t event) { // TODO: add special 'custom' action for buttons, and trigger via basic callback? // that way we don't depend on the event type and directly trigger with whatever cfg says if (event != button_event_t::Click) { return; } if (config.buttonLowId == id) { setSpeed(Speed::Low); } else if (config.buttonMediumId == id) { setSpeed(Speed::Medium); } else if (config.buttonHighId == id) { setSpeed(Speed::High); } }); #endif #if RELAY_SUPPORT setSpeedFromStatus(relayStatus(config.relayId)); relaySetStatusChange([](unsigned char id, bool status) { if (config.relayId == id) { setSpeedFromStatus(status); } }); #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) { setSpeedFromPayload(request.param(F("value"))); return true; } ); #endif #if TERMINAL_SUPPORT terminalRegisterCommand(F("SPEED"), [](const terminal::CommandContext& ctx) { if (ctx.argc == 2) { setSpeedFromPayload(ctx.argv[1]); } ctx.output.println(speedToPayload(config.speed)); terminalOK(ctx); }); #endif } } // namespace ifan void ifanSetup() { ifan02::setup(); } #endif // IFAN_SUPPORT