From a40eca30ad79315afdb67afa0b0743d4c0087e93 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Thu, 14 Jan 2021 13:58:36 +0300 Subject: [PATCH] ifan: refactor into a separate module --- code/espurna/config/general.h | 9 + code/espurna/config/hardware.h | 18 +- code/espurna/ifan.cpp | 364 +++++++++++++++++++++++++++++++++ code/espurna/ifan.h | 11 + code/espurna/main.cpp | 4 + code/espurna/mqtt.cpp | 4 +- code/espurna/mqtt.h | 2 +- code/espurna/relay.cpp | 65 +----- code/espurna/tuya.cpp | 2 +- code/test/build/nondefault.h | 1 + 10 files changed, 402 insertions(+), 78 deletions(-) create mode 100644 code/espurna/ifan.cpp create mode 100644 code/espurna/ifan.h diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index 9a748a5a..a58b7bf7 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -1805,6 +1805,7 @@ //-------------------------------------------------------------------------------- // TUYA switch & dimmer support //-------------------------------------------------------------------------------- + #ifndef TUYA_SUPPORT #define TUYA_SUPPORT 0 #endif @@ -1837,6 +1838,14 @@ #define PROMETHEUS_SUPPORT 0 #endif +//-------------------------------------------------------------------------------- +// ITEAD iFan support +//-------------------------------------------------------------------------------- + +#ifndef IFAN_SUPPORT +#define IFAN_SUPPORT 0 +#endif + // ============================================================================= // Configuration helpers // ============================================================================= diff --git a/code/espurna/config/hardware.h b/code/espurna/config/hardware.h index eb493a39..56e2eb9d 100644 --- a/code/espurna/config/hardware.h +++ b/code/espurna/config/hardware.h @@ -1010,25 +1010,17 @@ #define MANUFACTURER "ITEAD" #define DEVICE "SONOFF_IFAN02" - // These are virtual buttons triggered by the remote + // Base module + #define IFAN_SUPPORT 1 + + // These buttons are triggered by the remote #define BUTTON1_PIN 0 #define BUTTON2_PIN 9 #define BUTTON3_PIN 10 #define BUTTON4_PIN 14 - #define BUTTON1_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH - #define BUTTON2_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH - #define BUTTON3_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH - #define BUTTON4_CONFIG BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH - // Relays + // Only one relay by default, controlling the ON / OFF #define RELAY1_PIN 12 - #define RELAY2_PIN 5 - #define RELAY3_PIN 4 - #define RELAY4_PIN 15 - #define RELAY1_TYPE RELAY_TYPE_NORMAL - #define RELAY2_TYPE RELAY_TYPE_NORMAL - #define RELAY3_TYPE RELAY_TYPE_NORMAL - #define RELAY4_TYPE RELAY_TYPE_NORMAL // LEDs #define LED1_PIN 13 diff --git a/code/espurna/ifan.cpp b/code/espurna/ifan.cpp new file mode 100644 index 00000000..1fca883b --- /dev/null +++ b/code/espurna/ifan.cpp @@ -0,0 +1,364 @@ +/* + +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 diff --git a/code/espurna/ifan.h b/code/espurna/ifan.h new file mode 100644 index 00000000..55a5395a --- /dev/null +++ b/code/espurna/ifan.h @@ -0,0 +1,11 @@ +/* + +iFan MODULE + +Copyright (C) 2016-2019 by Xose Pérez + +*/ + +#pragma once + +void ifanSetup(); diff --git a/code/espurna/main.cpp b/code/espurna/main.cpp index 40265d15..ea7943d3 100644 --- a/code/espurna/main.cpp +++ b/code/espurna/main.cpp @@ -34,6 +34,7 @@ along with this program. If not, see . #include "garland.h" #include "i2c.h" #include "influxdb.h" +#include "ifan.h" #include "ir.h" #include "led.h" #include "light.h" @@ -304,6 +305,9 @@ void setup() { #if KINGART_CURTAIN_SUPPORT kingartCurtainSetup(); #endif + #if IFAN_SUPPORT + ifanSetup(); + #endif // 3rd party code hook #if USE_EXTRA diff --git a/code/espurna/mqtt.cpp b/code/espurna/mqtt.cpp index 1b849a64..a8ac0291 100644 --- a/code/espurna/mqtt.cpp +++ b/code/espurna/mqtt.cpp @@ -523,7 +523,7 @@ void _mqttCallback(unsigned int type, const char * topic, const char * payload) if (type == MQTT_MESSAGE_EVENT) { // Match topic - String t = mqttMagnitude((char *) topic); + String t = mqttMagnitude(topic); // Actions if (t.equals(MQTT_TOPIC_ACTION)) { @@ -646,7 +646,7 @@ void _mqttOnMessage(char* topic, char* payload, unsigned int len) { @param topic the full MQTT topic @return String object with the magnitude part. */ -String mqttMagnitude(char * topic) { +String mqttMagnitude(const char* topic) { String pattern = _mqtt_topic + _mqtt_setter; int position = pattern.indexOf("#"); diff --git a/code/espurna/mqtt.h b/code/espurna/mqtt.h index 7ad4f479..f056c465 100644 --- a/code/espurna/mqtt.h +++ b/code/espurna/mqtt.h @@ -26,7 +26,7 @@ void mqttRegister(mqtt_callback_f callback); String mqttTopic(const char * magnitude, bool is_set); String mqttTopic(const char * magnitude, unsigned int index, bool is_set); -String mqttMagnitude(char * topic); +String mqttMagnitude(const char* topic); bool mqttSendRaw(const char * topic, const char * message, bool retain); bool mqttSendRaw(const char * topic, const char * message); diff --git a/code/espurna/relay.cpp b/code/espurna/relay.cpp index b539543c..4e9ae84a 100644 --- a/code/espurna/relay.cpp +++ b/code/espurna/relay.cpp @@ -739,34 +739,6 @@ void _relayProcess(bool mode) { } } -#if defined(ITEAD_SONOFF_IFAN02) - -unsigned char _relay_ifan02_speeds[] = {0, 1, 3, 5}; - -unsigned char getSpeed() { - unsigned char speed = - (_relays[1].target_status ? 1 : 0) + - (_relays[2].target_status ? 2 : 0) + - (_relays[3].target_status ? 4 : 0); - for (unsigned char i=0; i<4; i++) { - if (_relay_ifan02_speeds[i] == speed) return i; - } - return 0; -} - -void setSpeed(unsigned char speed) { - if ((0 <= speed) & (speed <= 3)) { - if (getSpeed() == speed) return; - unsigned char states = _relay_ifan02_speeds[speed]; - for (unsigned char i=0; i<3; i++) { - relayStatus(i+1, states & 1 == 1); - states >>= 1; - } - } -} - -#endif - // ----------------------------------------------------------------------------- // RELAY // ----------------------------------------------------------------------------- @@ -1350,20 +1322,6 @@ void relaySetupAPI() { } ); - #if defined(ITEAD_SONOFF_IFAN02) - apiRegister(F(MQTT_TOPIC_SPEED), { - [](ApiRequest& request) { - request.send(String(static_cast(getSpeed()))); - return true; - }, - [](ApiRequest& request) { - setSpeed(atoi(request.param(F("value")))); - return true; - }, - nullptr - }); - #endif - } #endif // API_SUPPORT @@ -1432,13 +1390,6 @@ void relayMQTT(unsigned char id) { _relayMQTTGroup(id); } - // Send speed for IFAN02 - #if defined (ITEAD_SONOFF_IFAN02) - char buffer[5]; - snprintf(buffer, sizeof(buffer), "%u", getSpeed()); - mqttSend(MQTT_TOPIC_SPEED, buffer); - #endif - } void relayMQTT() { @@ -1473,6 +1424,10 @@ void relayStatusWrap(unsigned char id, PayloadStatus value, bool is_group_topic) void relayMQTTCallback(unsigned int type, const char * topic, const char * payload) { + if (!relayCount()) { + return; + } + if (type == MQTT_CONNECT_EVENT) { // Send status on connect @@ -1490,10 +1445,6 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo snprintf_P(pulse_topic, sizeof(pulse_topic), PSTR("%s/+"), MQTT_TOPIC_PULSE); mqttSubscribe(pulse_topic); - #if defined(ITEAD_SONOFF_IFAN02) - mqttSubscribe(MQTT_TOPIC_SPEED); - #endif - // Subscribe to group topics for (unsigned char i=0; i < _relays.size(); i++) { const auto t = getSetting({"mqttGroup", i}); @@ -1547,13 +1498,6 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo } } - // Itead Sonoff IFAN02 - #if defined (ITEAD_SONOFF_IFAN02) - if (t.startsWith(MQTT_TOPIC_SPEED)) { - setSpeed(atoi(payload)); - } - #endif - } // TODO: safeguard against network issues. this one has good intentions, but we may end up @@ -1583,7 +1527,6 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo } void relaySetupMQTT() { - if (!relayCount()) return; mqttRegister(relayMQTTCallback); } diff --git a/code/espurna/tuya.cpp b/code/espurna/tuya.cpp index f0671983..a319e199 100644 --- a/code/espurna/tuya.cpp +++ b/code/espurna/tuya.cpp @@ -2,7 +2,7 @@ TUYA MODULE -Copyright (C) 2019 by Maxim Prokhorov +Copyright (C) 2019-2021 by Maxim Prokhorov */ diff --git a/code/test/build/nondefault.h b/code/test/build/nondefault.h index 56016d01..7fd71955 100644 --- a/code/test/build/nondefault.h +++ b/code/test/build/nondefault.h @@ -18,3 +18,4 @@ #define MCP23S08_SUPPORT 1 #define RELAY_PROVIDER_DUAL_SUPPORT 1 #define RELAY_PROVIDER_STM_SUPPORT 1 +#define IFAN_SUPPORT 1