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

/*
iFan02 MODULE
Copyright (C) 2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
Original implementation via RELAY module
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "espurna.h"
#if IFAN_SUPPORT
#include "api.h"
#include "button.h"
#include "mqtt.h"
#include "relay.h"
#include "terminal.h"
#include <array>
#include <utility>
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<int8_t, Gpios>;
using Pin = std::pair<int, BasePinPtr>;
using StatePins = std::array<Pin, Gpios>;
// 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<void(Speed)>;
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<FanProvider>(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