Files
espurna/code/espurna/ifan.cpp
Maxim Prokhorov 972188c83b fan: common types and actions
update custom action callback to also get the event
buttons fan actions
ifan buttons are handled through action instead of manually
fan settings and api are (sort-of) generic
2021-01-19 23:30:12 +03:00

376 lines
8.0 KiB
C++

/*
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 "fan.h"
#include "mqtt.h"
#include "relay.h"
#include "terminal.h"
#include <array>
#include <utility>
// TODO: in case there are more FANs, move externally
namespace ifan02 {
const char* speedToPayload(FanSpeed value) {
switch (value) {
case FanSpeed::Off:
return "off";
case FanSpeed::Low:
return "low";
case FanSpeed::Medium:
return "medium";
case FanSpeed::High:
return "high";
}
return "";
}
FanSpeed payloadToSpeed(const char* payload) {
auto len = strlen(payload);
if (len == 1) {
switch (payload[0]) {
case '0':
return FanSpeed::Off;
case '1':
return FanSpeed::Low;
case '2':
return FanSpeed::Medium;
case '3':
return FanSpeed::High;
}
} else if (len > 1) {
String cmp(payload);
if (cmp == "off") {
return FanSpeed::Off;
} else if (cmp == "low") {
return FanSpeed::Low;
} else if (cmp == "medium") {
return FanSpeed::Medium;
} else if (cmp == "high") {
return FanSpeed::High;
}
}
return FanSpeed::Off;
}
FanSpeed payloadToSpeed(const String& string) {
return payloadToSpeed(string.c_str());
}
} // namespace ifan02
namespace settings {
namespace internal {
template <>
FanSpeed convert(const String& value) {
return ifan02::payloadToSpeed(value);
}
} // namespace internal
} // namespace settings
namespace ifan02 {
constexpr unsigned long DefaultSaveDelay { 1000ul };
// 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_, FanSpeed speed_) :
save(save_),
speed(speed_)
{}
unsigned long save { DefaultSaveDelay };
FanSpeed speed { FanSpeed::Off };
StatePins state_pins;
};
Config readSettings() {
return Config(
getSetting("fanSave", DefaultSaveDelay),
getSetting("fanSpeed", FanSpeed::Medium)
);
}
Config config;
void configure() {
config = readSettings();
}
void report(FanSpeed speed [[gnu::unused]]) {
#if MQTT_SUPPORT
mqttSend(MQTT_TOPIC_SPEED, speedToPayload(speed));
#endif
}
void save(FanSpeed speed) {
static Ticker ticker;
config.speed = speed;
ticker.once_ms(config.save, []() {
const char* value = speedToPayload(config.speed);
setSetting("fanSpeed", 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(FanSpeed speed) {
switch (speed) {
case FanSpeed::Low:
return {HIGH, LOW, LOW};
case FanSpeed::Medium:
return {HIGH, HIGH, LOW};
case FanSpeed::High:
return {HIGH, LOW, HIGH};
case FanSpeed::Off:
break;
}
return {LOW, LOW, LOW};
}
const char* maskFromSpeed(FanSpeed speed) {
switch (speed) {
case FanSpeed::Low:
return "0b100";
case FanSpeed::Medium:
return "0b110";
case FanSpeed::High:
return "0b101";
case FanSpeed::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(FanSpeed)>;
FanSpeedUpdate onFanSpeedUpdate = [](FanSpeed) {
};
void updateSpeed(Config& config, FanSpeed speed) {
switch (speed) {
case FanSpeed::Low:
case FanSpeed::Medium:
case FanSpeed::High:
save(speed);
report(speed);
onFanSpeedUpdate(speed);
break;
case FanSpeed::Off:
break;
}
}
void updateSpeed(FanSpeed 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](FanSpeed speed) {
change(speed);
};
_pin->pinMode(OUTPUT);
}
const char* id() const override {
return "fan";
}
void change(FanSpeed speed) {
_pin->digitalWrite((FanSpeed::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 : FanSpeed::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) {
auto provider = std::make_unique<FanProvider>(std::move(relay_pin), config, onFanSpeedUpdate);
if (!relayAdd(std::move(provider))) {
DEBUG_MSG_P(PSTR("[IFAN] Could not add relay provider for GPIO%d\n"), controlPin());
gpioUnlock(controlPin());
}
}
#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
FanSpeed fanSpeed() {
return ifan02::config.speed;
}
void fanSpeed(FanSpeed speed) {
ifan02::updateSpeed(FanSpeed::Low);
}
void fanSetup() {
ifan02::setup();
}
#endif // IFAN_SUPPORT