Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 95 additions & 28 deletions octoprint_mqtt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from collections import deque

import octoprint.plugin
import flask

from octoprint.events import Events
from octoprint.util import dict_minimal_mergediff, RepeatedTimer
Expand All @@ -19,6 +20,7 @@ class MqttPlugin(octoprint.plugin.SettingsPlugin,
octoprint.plugin.ProgressPlugin,
octoprint.plugin.TemplatePlugin,
octoprint.plugin.AssetPlugin,
octoprint.plugin.SimpleApiPlugin,
octoprint.printer.PrinterCallback):

EVENT_CLASS_TO_EVENT_LIST = dict(server = (Events.STARTUP, Events.SHUTDOWN, Events.CLIENT_OPENED,
Expand Down Expand Up @@ -349,6 +351,51 @@ def get_update_information(self):
)
)

##~~ SimpleApiPlugin API

def get_api_commands(self):
return dict(
connect=[],
disconnect=[]
)

def is_api_adminonly(self):
return True

def is_api_protected(self):
return True

def on_api_command(self, command, data):
if command == "connect":
self._logger.info("Connect command received, connecting to MQTT broker")
self.mqtt_connect()

# Wait for async connection to establish (up to 3 seconds)
for i in range(30):
if self._mqtt_connected:
break
time.sleep(0.1)

return flask.jsonify(dict(success=True, connected=self._mqtt_connected))

elif command == "disconnect":
self._logger.info("Disconnect command received, disconnecting from MQTT broker")
self.mqtt_disconnect(force=True)
return flask.jsonify(dict(success=True, connected=self._mqtt_connected))

def on_api_get(self, request):
# Verify actual connection status with the MQTT client
actual_connected = False
if self._mqtt is not None:
actual_connected = self._mqtt.is_connected()
# Sync our flag with actual status
if actual_connected != self._mqtt_connected:
self._logger.debug("Connection status out of sync, updating from {} to {}".format(
self._mqtt_connected, actual_connected))
self._mqtt_connected = actual_connected

return flask.jsonify(dict(connected=actual_connected))

##~~ helpers

def mqtt_connect(self):
Expand Down Expand Up @@ -410,13 +457,18 @@ def mqtt_disconnect(self, force=False, incl_lwt=True, lwt=None):
if self._mqtt is None:
return

if incl_lwt:
# Publish Last Will Testament if requested and connected
if incl_lwt and self._mqtt.is_connected():
if lwt is None:
lwt = self._get_topic("lw")
if lwt:
_retain = self._settings.get_boolean(["broker", "lwRetain"])
self._mqtt.publish(lwt, self.LWT_DISCONNECTED, qos=1, retain=_retain)

# Actually disconnect from the broker if connected
if self._mqtt.is_connected():
self._mqtt.disconnect()

self._mqtt.loop_stop()

if force:
Expand Down Expand Up @@ -510,33 +562,42 @@ def _on_mqtt_connect(self, client, userdata, flags, rc):
return

self._logger.info("Connected to mqtt broker")
lw_active = self._settings.get_boolean(["publish", "lwActive"])
lw_topic = self._get_topic("lw")
lw_retain = self._settings.get_boolean(["broker", "lwRetain"])
if lw_active and lw_topic:
self._mqtt.publish(lw_topic, self.LWT_CONNECTED, qos=1, retain=lw_retain)

_retain = self._settings.get_boolean(["broker", "retain"])
if self._mqtt_publish_queue:
try:
while True:
topic, payload, qos = self._mqtt_publish_queue.popleft()
self._mqtt.publish(topic, payload=payload, retain=_retain, qos=qos)
except IndexError:
# that's ok, queue is just empty
pass

subbed_topics = list(map(lambda t: (t, 0), {topic for topic, _, _, _ in self._mqtt_subscriptions}))
if subbed_topics:
self._mqtt.subscribe(subbed_topics)
self._logger.debug("Subscribed to topics")

self._mqtt_connected = True

if self._mqtt_reset_state:
self._update_progress("", "")
self.on_slicing_progress("", "", "", "", "", 0)
self._mqtt_reset_state = False

try:
lw_active = self._settings.get_boolean(["publish", "lwActive"])
lw_topic = self._get_topic("lw")
lw_retain = self._settings.get_boolean(["broker", "lwRetain"])
if lw_active and lw_topic:
self._mqtt.publish(lw_topic, self.LWT_CONNECTED, qos=1, retain=lw_retain)

_retain = self._settings.get_boolean(["broker", "retain"])
if self._mqtt_publish_queue:
try:
while True:
topic, payload, qos = self._mqtt_publish_queue.popleft()
self._mqtt.publish(topic, payload=payload, retain=_retain, qos=qos)
except IndexError:
# that's ok, queue is just empty
pass

# Filter out invalid topics (None, empty string)
valid_topics = {topic for topic, _, _, _ in self._mqtt_subscriptions if topic}
subbed_topics = list(map(lambda t: (t, 0), valid_topics))
if subbed_topics:
self._mqtt.subscribe(subbed_topics)
self._logger.debug("Subscribed to topics")

self._mqtt_connected = True

# Notify frontend of connection status change
self._plugin_manager.send_plugin_message(self._identifier, {"connected": True})

if self._mqtt_reset_state:
self._update_progress("", "")
self.on_slicing_progress("", "", "", "", "", 0)
self._mqtt_reset_state = False
except Exception as e:
self._logger.error("Exception in _on_mqtt_connect: {}".format(e), exc_info=True)

def _on_mqtt_disconnect(self, client, userdata, rc):
if not client == self._mqtt:
Expand All @@ -549,6 +610,12 @@ def _on_mqtt_disconnect(self, client, userdata, rc):

self._mqtt_connected = False

# Notify frontend of connection status change
try:
self._plugin_manager.send_plugin_message(self._identifier, {"connected": False})
except Exception as e:
self._logger.error("Exception while sending disconnect message to frontend: {}".format(e), exc_info=True)

def _on_mqtt_message(self, client, userdata, msg):
if not client == self._mqtt:
return
Expand Down
153 changes: 153 additions & 0 deletions octoprint_mqtt/static/js/mqtt.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,44 @@ $(function() {
self.settings = undefined;
self.availableProtocols = ko.observableArray(['MQTTv31','MQTTv311']);

// Connection state observables
self.isConnected = ko.observable(false);
self.isConnecting = ko.observable(false);
self.isDisconnecting = ko.observable(false);

// Computed observables for UI elements
self.connectionStatusHtml = ko.pureComputed(function() {
if (self.isConnected()) {
return '<i class="fa fa-check-circle" style="color: green;"></i> <span style="color: green;">Connected</span>';
} else {
return '<i class="fa fa-times-circle" style="color: #d9534f;"></i> <span style="color: #d9534f;">Disconnected</span>';
}
});

self.connectButtonEnabled = ko.pureComputed(function() {
return !self.isConnecting() && !self.isDisconnecting() && !self.isConnected();
});

self.connectButtonHtml = ko.pureComputed(function() {
if (self.isConnecting()) {
return '<i class="fa fa-spinner fa-spin"></i> Connecting...';
} else {
return '<i class="fa fa-link"></i> Connect';
}
});

self.disconnectButtonEnabled = ko.pureComputed(function() {
return !self.isConnecting() && !self.isDisconnecting() && self.isConnected();
});

self.disconnectButtonHtml = ko.pureComputed(function() {
if (self.isDisconnecting()) {
return '<i class="fa fa-spinner fa-spin"></i> Disconnecting...';
} else {
return '<i class="fa fa-unlink"></i> Disconnect';
}
});

self.onBeforeBinding = function () {
self.settings = self.global_settings.settings.plugins.mqtt;

Expand All @@ -18,6 +56,121 @@ $(function() {

// show client_id options if client_id is set
self.showClientID(!!self.settings.client.client_id());

// check connection status on load
self.checkConnectionStatus();
};

self.checkConnectionStatus = function() {
$.ajax({
url: API_BASEURL + "plugin/mqtt",
type: "GET",
dataType: "json",
success: function(response) {
self.updateConnectionStatus(response.connected);
},
error: function() {
self.updateConnectionStatus(false);
}
});
};

self.updateConnectionStatus = function(connected) {
self.isConnected(connected);
};

self.connectMqtt = function() {
self.isConnecting(true);

$.ajax({
url: API_BASEURL + "plugin/mqtt",
type: "POST",
dataType: "json",
data: JSON.stringify({
command: "connect"
}),
contentType: "application/json; charset=UTF-8",
success: function(response) {
if (response.connected) {
new PNotify({
title: "MQTT Connected",
text: "Successfully connected to MQTT broker",
type: "success"
});
self.updateConnectionStatus(true);
} else {
new PNotify({
title: "MQTT Connection",
text: "Connection initiated, but not yet established. Check broker settings.",
type: "warning"
});
self.updateConnectionStatus(false);
}
},
error: function() {
new PNotify({
title: "MQTT Connection Failed",
text: "Failed to connect to MQTT broker. Check logs for details.",
type: "error"
});
self.updateConnectionStatus(false);
},
complete: function() {
self.isConnecting(false);
}
});
};

self.disconnectMqtt = function() {
self.isDisconnecting(true);

$.ajax({
url: API_BASEURL + "plugin/mqtt",
type: "POST",
dataType: "json",
data: JSON.stringify({
command: "disconnect"
}),
contentType: "application/json; charset=UTF-8",
success: function(response) {
if (!response.connected) {
new PNotify({
title: "MQTT Disconnected",
text: "Successfully disconnected from MQTT broker",
type: "success"
});
self.updateConnectionStatus(false);
} else {
new PNotify({
title: "MQTT Disconnection",
text: "Disconnect initiated, but still showing as connected.",
type: "warning"
});
self.updateConnectionStatus(true);
}
},
error: function() {
new PNotify({
title: "MQTT Disconnection Failed",
text: "Failed to disconnect from MQTT broker. Check logs for details.",
type: "error"
});
},
complete: function() {
self.isDisconnecting(false);
}
});
};

// Handle plugin messages from backend for real-time connection status updates
self.onDataUpdaterPluginMessage = function(plugin, data) {
if (plugin !== "mqtt") {
return;
}

if (data.hasOwnProperty("connected")) {
self.updateConnectionStatus(data.connected);
}
};
}

Expand Down
8 changes: 8 additions & 0 deletions octoprint_mqtt/templates/mqtt_settings.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@

<div class="tab-content">
<div id="settings_plugin_mqtt_broker" class="tab-pane active">
<div class="control-group">
<label class="control-label">{{ _('Connection Status') }}</label>
<div class="controls">
<span style="display: inline-flex; align-items: center; gap: 5px;" data-bind="html: connectionStatusHtml"></span>
<button class="btn btn-danger btn-small" data-bind="click: disconnectMqtt, enable: disconnectButtonEnabled, html: disconnectButtonHtml" style="margin-left: 10px;"></button>
<button class="btn btn-success btn-small" data-bind="click: connectMqtt, enable: connectButtonEnabled, html: connectButtonHtml"></button>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Host') }}</label>
<div class="controls">
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
plugin_license = "AGPLv3"

# Any additional requirements besides OctoPrint should be listed here
plugin_requires = ["OctoPrint>=1.3.5", "six", "paho-mqtt<2"]
plugin_requires = ["OctoPrint>=1.3.5", "six", "paho-mqtt>=1.4.0,<2"]

### --------------------------------------------------------------------------------------------------------------------
### More advanced options that you usually shouldn't have to touch follow after this point
Expand Down