Skip to content
Draft
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
80 changes: 80 additions & 0 deletions octoprint_moonraker_connector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from urllib.parse import urljoin

import requests
import subprocess
from flask import jsonify
from flask_babel import gettext

Expand All @@ -12,6 +13,9 @@

from . import schema

URL_KLIPPER_RESTART_MOONRAKER = "http://{host}:{port}/machine/services/restart"
URL_FIRMWARE_RESTART_MOONRAKER = "http://{host}:{port}/printer/firmware_restart"
URL_HOST_RESTART_MOONRAKER = "http://{host}:{port}/printer/restart"
URL_WEBCAM_INFO_MOONRAKER = "http://{host}:{port}/server/webcams/list"
URL_WEBCAM_INFO_FLUIDD_LEGACY = (
"http://{host}:{port}/server/database/item?namespace=fluidd&key=cameras"
Expand Down Expand Up @@ -93,6 +97,82 @@ def on_api_get(self, request):
)
return jsonify(response.model_dump(by_alias=True))

def get_api_commands(self):
return dict(
restart_host=[],
restart_firmware=[],
restart_klipper_service=[]
)

def on_api_command(self, command, data):
if command == "restart_host":
params = self._get_connector_params()
if params is not None:
host = params["host"]
port = params["port"]
apikey = params["apikey"]

if host is not None and port is not None:
headers = {}
if apikey:
headers["X-Api-Key"] = apikey

r = requests.post(
URL_HOST_RESTART_MOONRAKER.format(host=host, port=port), headers=headers
)
ret = r.json()

if ret['result'] == "ok":
return jsonify(success=True, message="Host Restart sent")
else:
return jsonify(success=False, message="Host Restart failed")
elif command == "restart_firmware":
params = self._get_connector_params()
if params is not None:
host = params["host"]
port = params["port"]
apikey = params["apikey"]

if host is not None and port is not None:
headers = {}
if apikey:
headers["X-Api-Key"] = apikey

r = requests.post(
URL_FIRMWARE_RESTART_MOONRAKER.format(host=host, port=port), headers=headers
)
ret = r.json()

if ret['result'] == "ok":
return jsonify(success=True, message="Firmware Restart sent")
else:
return jsonify(success=False, message="Firmware Restart failed")
if command == "restart_klipper_service":
params = self._get_connector_params()
if params is not None:
host = params["host"]
port = params["port"]
apikey = params["apikey"]

if host is not None and port is not None:
headers = {}
if apikey:
headers["X-Api-Key"] = apikey

payload = {
"service": "klipper"
}

r = requests.post(
URL_KLIPPER_RESTART_MOONRAKER.format(host=host, port=port), headers=headers, json=payload
)
ret = r.json()

if ret['result'] == "ok":
return jsonify(success=True, message="Klipper Restart sent")
else:
return jsonify(success=False, message="Klipper Restart failed")

def is_api_protected(self):
return True

Expand Down
153 changes: 124 additions & 29 deletions octoprint_moonraker_connector/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,27 @@ class Configfile(BaseModel):


class TemperatureDataPoint:
type: str = "temperature_sensor"
id: str = ""
hadTarget: bool = True
actual: float = 0.0
target: float = 0.0

def __init__(self, actual: float = 0.0, target: float = 0.0):
def __init__(self, type: str = "temperature_sensor", id: str = "", hasTarget: bool = True, actual: float = 0.0, target: float = 0.0):
self.type = type
self.id = id
self.hasTarget = hasTarget
self.actual = actual
self.target = target

def __str__(self):
return f"{self.actual} / {self.target}"
if self.hasTarget:
return f"{self.actual} / {self.target}"
else:
return f"{self.actual}"

def __repr__(self):
return f"TemperatureDataPoint({self.actual}, {self.target})"
return f"TemperatureDataPoint({self.type}, {self.id}, {self.hasTarget}, {self.actual}, {self.target})"


class KlipperState(enum.Enum):
Expand Down Expand Up @@ -245,6 +254,11 @@ def on_moonraker_print_detected(
def on_moonraker_server_info(self, server_info: dict[str, Any]) -> None:
pass

def on_moonraker_temperature_store_update(
self, data: dict[str, TemperatureDataPoint]
) -> None:
pass

def on_moonraker_file_tree_updated(
self, root: str, path: str, tree: dict[str, dict[str, InternalFile]]
) -> None:
Expand Down Expand Up @@ -300,13 +314,17 @@ class MoonrakerClient(JsonRpcClient):
WEBSOCKET_URL = "ws://{host}:{port}/websocket"
HTTP_URL = "http://{host}:{port}"

GENERIC_HEATER_PREFIX = "heater_generic "
GENERIC_EXTRUDER_PREFIX = "extruder"
GENERIC_TMC_PREFIX = "tmc"
TMC_HAVE_TEMPERATURE = [
"tmc2240",
]
HEATERS = ();
MACRO_PREFIX = "gcode_macro "

RELEVANT_PRINTER_OBJECTS = (
"configfile",
"display_status",
"extruder",
"gcode_move",
"heater_bed",
"idle_timeout",
Expand All @@ -315,7 +333,8 @@ class MoonrakerClient(JsonRpcClient):
lambda obj_list: [
x
for x in obj_list
if x.startswith(MoonrakerClient.GENERIC_HEATER_PREFIX)
if x.startswith(MoonrakerClient.GENERIC_EXTRUDER_PREFIX)
or x.startswith(MoonrakerClient.GENERIC_TMC_PREFIX)
or x.startswith(MoonrakerClient.MACRO_PREFIX)
],
)
Expand Down Expand Up @@ -343,6 +362,7 @@ def __init__(
self._klipper_state_subscription = False
self._subbed_objs: list[str] = []

self._temperature_store_received = False
self._log_history_received = False

self._heaters: list[str] = []
Expand Down Expand Up @@ -515,9 +535,10 @@ def on_server_info(future: Future) -> None:
f"Connected to Moonraker {moonraker_version}, API version {api_version}",
)

self.get_printer_config()
self.fetch_console_history()
self.fetch_job_history()
self.subscribe_to_updates()
#self.subscribe_to_updates()

else:
# log error
Expand All @@ -526,7 +547,7 @@ def on_server_info(future: Future) -> None:
self._dual_log(logging.ERROR, error)

except Exception as exc:
self._logger.exception("Error while retrieving server info")
self._logger.exception(f"Error while retrieving server info: {exc}")
error_str = f"Error while retrieving server info: {str(exc)}. Please check moonraker.log for details."
self._listener.on_moonraker_disconnected(error=error_str)

Expand All @@ -548,7 +569,7 @@ def on_printer_objects(future: Future) -> None:
obj_list = printer_objects.get("objects", [])

matched_objs = []
for obj in self.RELEVANT_PRINTER_OBJECTS:
for obj in self.RELEVANT_PRINTER_OBJECTS + self.HEATERS:
if isinstance(obj, str) and obj in obj_list:
matched_objs.append(obj)

Expand All @@ -567,8 +588,10 @@ def on_printer_objects(future: Future) -> None:
self._heaters = [
obj
for obj in matched_objs
if obj in ("extruder", "heater_bed")
or obj.startswith(self.GENERIC_HEATER_PREFIX)
if obj in ("heater_bed")
or obj.startswith(self.GENERIC_EXTRUDER_PREFIX)
or obj.startswith(self.GENERIC_TMC_PREFIX)
or obj in self.HEATERS
]

self.query_printer_objects(matched_objs)
Expand Down Expand Up @@ -615,14 +638,29 @@ def on_result(future: Future) -> None:
payload = result["status"]
self._process_query_result(payload)

except Exception:
self._logger.exception("Error while querying printer objects")
except Exception as e:
self._logger.exception(f"Error while querying printer objects: {e}")

params = {"objects": dict.fromkeys(objs)}
future = self.call_method("printer.objects.query", params=params)
future.add_done_callback(on_result)
return future

def get_printer_config(self) -> Future:
def on_result(future: Future) -> None:
try:
results = future.result()

config = results["status"]["configfile"]["config"]
self.fetch_temperature_store(config=config)
except Exception as e:
self._logger.exception(f"Error while fetching klipper config: {e}")

params = {"objects": {"configfile": None}}
self.call_method("printer.objects.query", params=params).add_done_callback(
on_result
)

def subscribe_printer_objects(self, objs: list[str] = None) -> Future:
if objs is None:
objs = self._subbed_objs
Expand Down Expand Up @@ -657,6 +695,53 @@ def on_status(future: Future) -> None:
)
return result_future

def fetch_temperature_store(self, config: dict) -> Future:
def on_result(future: Future) -> None:
try:
results = future.result()
for section in results:
if section.startswith(self.GENERIC_EXTRUDER_PREFIX):
continue;

if section not in config:
self._logger.warning(f"{section} not found in config")
continue

sectionconfig = config[section]
if "gcode_id" not in sectionconfig:
self._logger.warning(f"skipping {section}, no gcode_id")
continue

if " " in section:
type, name = section.split(maxsplit=1)
else:
type = ""
name = section

if name not in self._current_temperatures:
self._current_temperatures[name] = TemperatureDataPoint(
type=type,
id=sectionconfig["gcode_id"],
hasTarget=True if "target" in sectionconfig else False
)

if section not in self.HEATERS:
self.HEATERS += (section,)

if section not in self._heaters:
self._heaters.append(section)

self._temperature_store_received = True
self._listener.on_moonraker_temperature_store_update(self._current_temperatures)
self.subscribe_to_updates()

except Exception as e:
self._logger.exception(f"Error while fetching temperature store: {e}")

self.call_method("server.temperature_store").add_done_callback(
on_result
)

def fetch_console_history(self, count: int = 100, force: bool = False) -> Future:
if self._log_history_received and not force:
return
Expand Down Expand Up @@ -730,14 +815,6 @@ def trigger_emergency_stop(self) -> Future:
self._listener.on_moonraker_gcode_log("--- Triggering an Emergency Stop!")
return self.call_method("printer.emergency_stop")

def trigger_host_restart(self) -> Future:
self._listener.on_moonraker_gcode_log(">>> RESTART")
return self.call_method("printer.restart")

def trigger_firmware_restart(self) -> Future:
self._listener.on_moonraker_gcode_log(">>> FIRMWARE_RESTART")
return self.call_method("printer.firmware_restart")

# print job management

def start_print(self, path: str) -> Future:
Expand Down Expand Up @@ -1067,31 +1144,49 @@ def _process_update(self, payload: dict[str, Any]) -> None:
self._update_gcode_move(payload)
self._update_idle_timeout(payload)
self._update_print_stats(payload)
self._update_temperatures(payload)
if self._temperature_store_received:
self._update_temperatures(payload)
self._update_virtual_sdcard(payload)

def _update_temperatures(self, payload: dict[str, Any]) -> None:
dirty_actual = False
dirty_target = False
dirty_sensorslist = False

for heater in self._heaters:
if heater not in payload:
continue

name = (
heater[len(self.GENERIC_HEATER_PREFIX) :]
if heater.startswith(self.GENERIC_HEATER_PREFIX)
else heater
)
if " " in heater:
type, name = heater.split(maxsplit=1)
else:
type = ""
name = heater

if heater.startswith(self.GENERIC_TMC_PREFIX) and type not in self.TMC_HAVE_TEMPERATURE:
continue

if name not in self._current_temperatures:
dirty_sensorslist = True
self._logger.warning(f"Adding {heater} to temperatures list")

data = self._current_temperatures.get(name, TemperatureDataPoint(type=type))

if heater.startswith(self.GENERIC_TMC_PREFIX) and data.hasTarget:
data.hasTarget = False

data = self._current_temperatures.get(name, TemperatureDataPoint())
if "temperature" in payload[heater]:
data.actual = payload[heater]["temperature"]
data.actual = payload[heater]["temperature"] or 0
dirty_actual = True
if "target" in payload[heater]:
data.target = payload[heater]["target"]
dirty_target = True
self._current_temperatures[name] = data
if data.id == "C" and name != "chamber":
self._current_temperatures["chamber"] = data

if dirty_sensorslist:
self._listener.on_moonraker_temperature_store_update(self._current_temperatures)

if dirty_actual or dirty_target:
now = time.monotonic()
Expand Down
Loading