Add MQTT condition and action

This commit is contained in:
WarmUpTill 2025-04-22 15:24:19 +02:00 committed by WarmUpTill
parent cab50e0922
commit 027a3e9074
6 changed files with 669 additions and 1 deletions

View File

@ -774,9 +774,11 @@ AdvSceneSwitcher.condition.streamDeck.stopListen="Stop listening"
AdvSceneSwitcher.condition.streamDeck.pluginDownload="<html><head/><body><p>The Stream Deck plugin can be found <a href=\"https://github.com/WarmUpTill/advanced-scene-switcher-streamdeck-plugin/releases\"><span style=\" text-decoration: underline; color:#268bd2;\">here on GitHub</span></a>.</p></body></html>"
AdvSceneSwitcher.condition.gameCapture="Game capture"
AdvSceneSwitcher.condition.gameCapture.entry="{{sources}}hooked a game."
AdvSceneSwitcher.condition.screenshot="Screenshot"
AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken"
AdvSceneSwitcher.condition.mqtt="MQTT"
AdvSceneSwitcher.condition.mqtt.layout.match="Message was received from{{connection}} which matches{{regex}}:"
AdvSceneSwitcher.condition.mqtt.layout.listen="Set message selection to incoming message:{{listenButton}}"
# Macro Actions
AdvSceneSwitcher.action.unknown="Unknown action"
@ -1208,6 +1210,11 @@ AdvSceneSwitcher.action.window.entry="{{actions}}{{windows}}{{regex}}"
AdvSceneSwitcher.action.log="Log"
AdvSceneSwitcher.action.log.placeholder="My log message!"
AdvSceneSwitcher.action.log.entry="Write to OBS log:{{logMessage}}"
AdvSceneSwitcher.action.mqtt="MQTT"
AdvSceneSwitcher.action.mqtt.topic="Topic:"
AdvSceneSwitcher.action.mqtt.qos="QoS:"
AdvSceneSwitcher.action.mqtt.retain="Retained message:"
AdvSceneSwitcher.action.mqtt.layout="Send message to{{connection}}"
# Hotkey
AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher"
@ -1364,6 +1371,35 @@ AdvSceneSwitcher.osc.message.type.false="False"
AdvSceneSwitcher.osc.message.type.infinity="Infinitum"
AdvSceneSwitcher.osc.message.type.null="Nil"
AdvSceneSwitcher.mqttConnection.startListen="Start listening"
AdvSceneSwitcher.mqttConnection.stopListen="Stop listening"
AdvSceneSwitcher.mqttConnection.select="--select connection--"
AdvSceneSwitcher.mqttConnection.add="Add new connection"
AdvSceneSwitcher.mqttConnection.configure="Configure connection settings"
AdvSceneSwitcher.mqttConnection.invalid="Invalid connection selection"
AdvSceneSwitcher.mqttConnection.name="Name:"
AdvSceneSwitcher.mqttConnection.address="Address:"
AdvSceneSwitcher.mqttConnection.username="Username:"
AdvSceneSwitcher.mqttConnection.password="Password:"
AdvSceneSwitcher.mqttConnection.reconnect="Reconnect automatically:"
AdvSceneSwitcher.mqttConnection.reconnectDelay="Automatically reconnect after:"
AdvSceneSwitcher.mqttConnection.connectOnStart="Connect on startup:"
AdvSceneSwitcher.mqttConnection.test="Test connection"
AdvSceneSwitcher.mqttConnection.status.connected="Connected!"
AdvSceneSwitcher.mqttConnection.status.connecting="Connecting ..."
AdvSceneSwitcher.mqttConnection.status.disconnected="Could not connect!"
AdvSceneSwitcher.mqttConnection.topic="Topic"
AdvSceneSwitcher.mqttConnection.qos="QoS"
AdvSceneSwitcher.mqttConnection.topics="Topic subscriptions:"
AdvSceneSwitcher.mqttConnection.inputWarning.emptyTopic="Topic cannot be empty."
AdvSceneSwitcher.mqttConnection.inputWarning.emptyTopic.title="Input Error"
AdvSceneSwitcher.mqttConnection.inputWarning.duplicateTopic="This topic already exists."
AdvSceneSwitcher.mqttConnection.inputWarning.duplicateTopic.title="Duplicate Topic"
AdvSceneSwitcher.mqttConnection.inputWarning.qosRange="QoS must be 0, 1, or 2."
AdvSceneSwitcher.mqttConnection.inputWarning.qosRange.title="Invalid QoS"
AdvSceneSwitcher.mqttConnection.add.layout="Add topic subscription{{topic}}with QoS{{QoS}}"
AdvSceneSwitcher.mqttConnection.add.title="Add MQTT Topic Subscription"
AdvSceneSwitcher.twitchToken.name="Account name:"
AdvSceneSwitcher.twitchToken.nameNotAvailable="Account already in use"
AdvSceneSwitcher.twitchToken.select="--select Twitch connection--"
@ -2066,6 +2102,8 @@ AdvSceneSwitcher.tempVar.http.body="Message body"
AdvSceneSwitcher.tempVar.http.error="Error"
AdvSceneSwitcher.tempVar.http.error.description="Empty when no error occurred.\nOther possible values:\n\n * Could not establish connection\n * Failed to bind IP address\n * Failed to read connection\n * Failed to write connection\n * Maximum redirect count exceeded\n * Connection handling canceled\n * SSL connection failed\n * SSL certificate loading failed\n * SSL server verification failed\n * Unsupported HTTP multipart boundary characters\n * Compression failed\n * Connection timed out\n * Proxy connection failed\n * Unknown"
AdvSceneSwitcher.tempVar.mqtt.message="Message"
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.selectCurrentScene="Current Scene"

View File

@ -30,6 +30,7 @@ install_advss_plugin_dependency(...)
add_plugin(http)
add_plugin(midi)
add_plugin(mqtt)
add_plugin(openvr)
add_plugin(stream-deck)
add_plugin(twitch)

View File

@ -0,0 +1,202 @@
#include "macro-action-mqtt.hpp"
#include "layout-helpers.hpp"
#include "mqtt-helpers.hpp"
#include "ui-helpers.hpp"
namespace advss {
const std::string MacroActionMqtt::id = "mqtt";
bool MacroActionMqtt::_registered = MacroActionFactory::Register(
MacroActionMqtt::id,
{MacroActionMqtt::Create, MacroActionMqttEdit::Create,
"AdvSceneSwitcher.action.mqtt"});
bool MacroActionMqtt::PerformAction()
{
auto connection = _connection.lock();
if (!connection) {
return true;
}
int qos = _qos;
if (qos > 2 || qos < 0) {
qos = 1;
blog(LOG_WARNING,
"%s: use QoS 1 isntead of invalid QoS value %d", __func__,
(int)_qos);
}
(void)connection->SendMessage(_topic, _message, qos, _retain);
return true;
}
void MacroActionMqtt::LogAction() const
{
ablog(LOG_INFO,
"send MQTT message to \"%s\" (topic: \"%s\" - payload: \"%s\" - qos: %d - retain: %d)",
GetWeakMqttConnectionName(_connection).c_str(), _topic.c_str(),
_message.c_str(), (int)_qos, _retain);
}
bool MacroActionMqtt::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
obs_data_set_string(obj, "connection",
GetWeakMqttConnectionName(_connection).c_str());
_message.Save(obj, "message");
_topic.Save(obj, "topic");
_qos.Save(obj, "qos");
obs_data_set_bool(obj, "retain", _retain);
return true;
}
bool MacroActionMqtt::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
SetConnection(obs_data_get_string(obj, "connection"));
_message.Load(obj, "message");
_topic.Load(obj, "topic");
_qos.Load(obj, "qos");
_retain = obs_data_get_bool(obj, "retain");
return true;
}
void MacroActionMqtt::SetConnection(const std::string &name)
{
_connection = GetWeakMqttConnectionByName(name);
}
std::weak_ptr<MqttConnection> MacroActionMqtt::GetConnection() const
{
return _connection;
}
std::string MacroActionMqtt::GetShortDesc() const
{
return GetWeakMqttConnectionName(_connection);
}
void MacroActionMqtt::ResolveVariablesToFixedValues()
{
_message.ResolveVariables();
_topic.ResolveVariables();
_qos.ResolveVariables();
}
std::shared_ptr<MacroAction> MacroActionMqtt::Create(Macro *m)
{
return std::make_shared<MacroActionMqtt>(m);
}
std::shared_ptr<MacroAction> MacroActionMqtt::Copy() const
{
return std::make_shared<MacroActionMqtt>(*this);
}
MacroActionMqttEdit::MacroActionMqttEdit(
QWidget *parent, std::shared_ptr<MacroActionMqtt> entryData)
: QWidget(parent),
_connection(new MqttConnectionSelection(this)),
_message(new VariableTextEdit(this, 5, 1, 1)),
_topic(new VariableLineEdit(this)),
_qos(new VariableSpinBox(this)),
_retain(new QCheckBox(this))
{
_qos->setMinimum(0);
_qos->setMaximum(2);
QWidget::connect(_connection, SIGNAL(SelectionChanged(const QString &)),
this,
SLOT(ConnectionSelectionChanged(const QString &)));
QWidget::connect(_message, SIGNAL(textChanged()), this,
SLOT(MqttMessageChanged()));
QWidget::connect(_topic, SIGNAL(editingFinished()), this,
SLOT(MqttTopicChanged()));
QWidget::connect(
_qos,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(QoSChanged(const NumberVariable<int> &)));
QWidget::connect(_retain, SIGNAL(stateChanged(int)), this,
SLOT(RetainChanged(int)));
auto sendLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.mqtt.layout"),
sendLayout, {{"{{connection}}", _connection}});
int row = 0;
auto grid = new QGridLayout;
grid->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.mqtt.topic")),
row, 0);
grid->addWidget(_topic, row, 1);
row++;
grid->addWidget(
new QLabel(obs_module_text("AdvSceneSwitcher.action.mqtt.qos")),
row, 0);
grid->addWidget(_qos, row, 1);
row++;
grid->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.mqtt.retain")),
row, 0);
grid->addWidget(_retain, row, 1);
auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(sendLayout);
mainLayout->addWidget(_message);
mainLayout->addLayout(grid);
setLayout(mainLayout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionMqttEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_connection->SetConnection(_entryData->GetConnection());
_message->setPlainText(_entryData->_message);
_topic->setText(_entryData->_topic);
_qos->SetValue(_entryData->_qos);
_retain->setChecked(_entryData->_retain);
adjustSize();
updateGeometry();
}
void MacroActionMqttEdit::ConnectionSelectionChanged(const QString &connection)
{
GUARD_LOADING_AND_LOCK();
_entryData->SetConnection(connection.toStdString());
emit(HeaderInfoChanged(connection));
}
void MacroActionMqttEdit::MqttMessageChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_message = _message->toPlainText().toStdString();
}
void MacroActionMqttEdit::MqttTopicChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_topic = _topic->text().toStdString();
}
void MacroActionMqttEdit::QoSChanged(const IntVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_qos = value;
}
void MacroActionMqttEdit::RetainChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_retain = value;
}
} // namespace advss

View File

@ -0,0 +1,73 @@
#pragma once
#include "macro-action-edit.hpp"
#include "mqtt-helpers.hpp"
#include "variable-line-edit.hpp"
#include "variable-spinbox.hpp"
#include "variable-text-edit.hpp"
namespace advss {
class MacroActionMqtt : public MacroAction {
public:
MacroActionMqtt(Macro *m) : MacroAction(m, true) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
void SetConnection(const std::string &name);
std::weak_ptr<MqttConnection> GetConnection() const;
std::string GetShortDesc() const;
void ResolveVariablesToFixedValues();
std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
StringVariable _message = "payload";
StringVariable _topic = "/topic";
IntVariable _qos = 1;
bool _retain = false;
private:
std::weak_ptr<MqttConnection> _connection;
static bool _registered;
static const std::string id;
};
class MacroActionMqttEdit : public QWidget {
Q_OBJECT
public:
MacroActionMqttEdit(
QWidget *parent,
std::shared_ptr<MacroActionMqtt> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionMqttEdit(
parent,
std::dynamic_pointer_cast<MacroActionMqtt>(action));
}
private slots:
void ConnectionSelectionChanged(const QString &);
void MqttMessageChanged();
void MqttTopicChanged();
void QoSChanged(const NumberVariable<int> &value);
void RetainChanged(int value);
signals:
void HeaderInfoChanged(const QString &);
private:
MqttConnectionSelection *_connection;
VariableTextEdit *_message;
VariableLineEdit *_topic;
VariableSpinBox *_qos;
QCheckBox *_retain;
std::shared_ptr<MacroActionMqtt> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@ -0,0 +1,268 @@
#include "macro-condition-mqtt.hpp"
#include "layout-helpers.hpp"
#include "macro-helpers.hpp"
#include "ui-helpers.hpp"
namespace advss {
const std::string MacroConditionMqtt::id = "mqtt";
bool MacroConditionMqtt::_registered = MacroConditionFactory::Register(
MacroConditionMqtt::id,
{MacroConditionMqtt::Create, MacroConditionMqttEdit::Create,
"AdvSceneSwitcher.condition.mqtt"});
bool MacroConditionMqtt::CheckCondition()
{
if (!_messageBuffer) {
return false;
}
const bool macroWasPausedSinceLastCheck =
MacroWasPausedSince(GetMacro(), _lastCheck);
_lastCheck = std::chrono::high_resolution_clock::now();
if (macroWasPausedSinceLastCheck) {
_messageBuffer->Clear();
return false;
}
const auto messageMatches = [this](const std::string &message) -> bool {
if (_regex.Enabled()) {
return _regex.Matches(message, _message);
}
return message != std::string(_message);
};
while (!_messageBuffer->Empty()) {
auto message = _messageBuffer->ConsumeMessage();
if (!message) {
continue;
}
if (!messageMatches(*message)) {
continue;
}
SetTempVarValue("message", *message);
if (_clearBufferOnMatch) {
_messageBuffer->Clear();
}
return true;
}
return false;
}
bool MacroConditionMqtt::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
_message.Save(obj, "message");
_regex.Save(obj);
obs_data_set_string(obj, "connection",
GetWeakMqttConnectionName(_connection).c_str());
obs_data_set_bool(obj, "clearBufferOnMatch", _clearBufferOnMatch);
return true;
}
bool MacroConditionMqtt::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_message.Load(obj, "message");
_regex.Load(obj);
_clearBufferOnMatch = obs_data_get_bool(obj, "clearBufferOnMatch");
SetConnection(obs_data_get_string(obj, "connection"));
return true;
}
std::string MacroConditionMqtt::GetShortDesc() const
{
return GetWeakMqttConnectionName(_connection);
}
void MacroConditionMqtt::SetConnection(const std::string &name)
{
_connection = GetWeakMqttConnectionByName(name);
auto connection = _connection.lock();
if (!connection) {
return;
}
_messageBuffer = connection->RegisterForEvents();
}
std::weak_ptr<MqttConnection> MacroConditionMqtt::GetConnection() const
{
return _connection;
}
void MacroConditionMqtt::SetupTempVars()
{
MacroCondition::SetupTempVars();
AddTempvar("message",
obs_module_text("AdvSceneSwitcher.tempVar.mqtt.message"));
}
MacroConditionMqttEdit::MacroConditionMqttEdit(
QWidget *parent, std::shared_ptr<MacroConditionMqtt> entryData)
: QWidget(parent),
_connection(new MqttConnectionSelection(this)),
_message(new VariableTextEdit(this, 5, 1, 1)),
_regex(new RegexConfigWidget(parent)),
_listen(new QPushButton(obs_module_text(
"AdvSceneSwitcher.mqttConnection.startListen"))),
_clearBufferOnMatch(new QCheckBox(
obs_module_text("AdvSceneSwitcher.clearBufferOnMatch")))
{
QWidget::connect(_message, SIGNAL(textChanged()), this,
SLOT(MqttMessageChanged()));
QWidget::connect(_regex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(RegexChanged(const RegexConfig &)));
QWidget::connect(_connection, SIGNAL(SelectionChanged(const QString &)),
this,
SLOT(ConnectionSelectionChanged(const QString &)));
QWidget::connect(_listen, SIGNAL(clicked()), this,
SLOT(ToggleListen()));
QWidget::connect(_clearBufferOnMatch, SIGNAL(stateChanged(int)), this,
SLOT(ClearBufferOnMatchChanged(int)));
QWidget::connect(&_listenTimer, SIGNAL(timeout()), this,
SLOT(SetMessageSelectionToLastReceived()));
auto entryLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.mqtt.layout.match"),
entryLayout,
{{"{{connection}}", _connection}, {"{{regex}}", _regex}});
auto listenLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.condition.mqtt.layout.listen"),
listenLayout, {{"{{listenButton}}", _listen}});
auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(entryLayout);
mainLayout->addWidget(_message);
mainLayout->addLayout(listenLayout);
mainLayout->addWidget(_clearBufferOnMatch);
setLayout(mainLayout);
_listenTimer.setInterval(100);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
MacroConditionMqttEdit::~MacroConditionMqttEdit()
{
EnableListening(false);
}
void MacroConditionMqttEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_message->setPlainText(_entryData->_message);
_connection->SetConnection(_entryData->GetConnection());
_regex->SetRegexConfig(_entryData->_regex);
_clearBufferOnMatch->setChecked(_entryData->_clearBufferOnMatch);
adjustSize();
updateGeometry();
}
void MacroConditionMqttEdit::ConnectionSelectionChanged(
const QString &connection)
{
GUARD_LOADING_AND_LOCK();
_entryData->SetConnection(connection.toStdString());
emit(HeaderInfoChanged(connection));
}
void MacroConditionMqttEdit::MqttMessageChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_message = _message->toPlainText().toStdString();
}
void MacroConditionMqttEdit::ClearBufferOnMatchChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_clearBufferOnMatch = value;
}
void MacroConditionMqttEdit::RegexChanged(const RegexConfig &conf)
{
GUARD_LOADING_AND_LOCK();
_entryData->_regex = conf;
adjustSize();
updateGeometry();
}
void MacroConditionMqttEdit::EnableListening(bool enable)
{
if (_currentlyListening == enable) {
return;
}
if (enable) {
auto connectionItem = _connection->GetCurrentItem();
if (!connectionItem) {
return;
}
auto weakConnection = GetWeakMqttConnectionByQString(
QString::fromStdString(connectionItem->Name()));
auto connection = weakConnection.lock();
if (!connection) {
return;
}
_messageBuffer = connection->RegisterForEvents();
_listenTimer.start();
} else {
_messageBuffer.reset();
_listenTimer.stop();
}
}
void MacroConditionMqttEdit::ToggleListen()
{
if (!_entryData) {
return;
}
_listen->setText(
_currentlyListening
? obs_module_text(
"AdvSceneSwitcher.mqttConnection.startListen")
: obs_module_text(
"AdvSceneSwitcher.mqttConnection.stopListen"));
EnableListening(!_currentlyListening);
_currentlyListening = !_currentlyListening;
_message->setDisabled(_currentlyListening);
}
void MacroConditionMqttEdit::SetMessageSelectionToLastReceived()
{
auto lock = LockContext();
if (!_entryData || !_messageBuffer || _messageBuffer->Empty()) {
return;
}
std::optional<std::string> message;
while (!_messageBuffer->Empty()) {
message = _messageBuffer->ConsumeMessage();
if (!message) {
continue;
}
}
if (!message) {
return;
}
const QSignalBlocker blocker(_message);
_message->setPlainText(*message);
_entryData->_message = *message;
}
} // namespace advss

View File

@ -0,0 +1,86 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "mqtt-helpers.hpp"
#include "regex-config.hpp"
#include "variable-text-edit.hpp"
#include <QCheckBox>
#include <QPushButton>
#include <QTimer>
namespace advss {
class MacroConditionMqtt : public MacroCondition {
public:
MacroConditionMqtt(Macro *m) : MacroCondition(m, true) {}
bool CheckCondition();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
static std::shared_ptr<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionMqtt>(m);
}
void SetConnection(const std::string &);
std::weak_ptr<MqttConnection> GetConnection() const;
StringVariable _message;
RegexConfig _regex;
bool _clearBufferOnMatch = true;
private:
void SetupTempVars();
std::weak_ptr<MqttConnection> _connection;
MqttMessageBuffer _messageBuffer;
std::chrono::high_resolution_clock::time_point _lastCheck{};
static bool _registered;
static const std::string id;
};
class MacroConditionMqttEdit : public QWidget {
Q_OBJECT
public:
MacroConditionMqttEdit(
QWidget *parent,
std::shared_ptr<MacroConditionMqtt> cond = nullptr);
virtual ~MacroConditionMqttEdit();
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionMqttEdit(
parent,
std::dynamic_pointer_cast<MacroConditionMqtt>(cond));
}
private slots:
void ConnectionSelectionChanged(const QString &);
void MqttMessageChanged();
void ClearBufferOnMatchChanged(int);
void RegexChanged(const RegexConfig &conf);
void ToggleListen();
void SetMessageSelectionToLastReceived();
signals:
void HeaderInfoChanged(const QString &);
private:
void EnableListening(bool);
MqttConnectionSelection *_connection;
VariableTextEdit *_message;
RegexConfigWidget *_regex;
QPushButton *_listen;
QCheckBox *_clearBufferOnMatch;
std::shared_ptr<MacroConditionMqtt> _entryData;
QTimer _listenTimer;
MqttMessageBuffer _messageBuffer;
bool _currentlyListening = false;
bool _loading = true;
};
} // namespace advss