jneilliii / OctoPrint-PlotlyTempGraph

25 stars 7 forks source link

FeatureRequest: Add ability to add custom commands for temperature #32

Closed puterboy closed 1 year ago

puterboy commented 2 years ago

It would be great if you could allow the addition of custom commands to retrieve a custom-named temperature variable without having to use another plugin such as octoprint-enclosure (which is semi-deprecated) to input the temperature value.

This could be done similar to the Navbar Temperature Plugin that allows you to give an arbitrary command whose output is displayed in the Navbar. In this case of course the output would need to be an integer in a rational temperature range.

jneilliii commented 2 years ago

It would be fairly easy to create a single file plugin to run a specific command like you mention and make it available to the plugin. I'd be happy to program up an example of this and provide in the repository as an example like I have done for a couple of other use cases. Do you have an example command that I could use for that?

puterboy commented 2 years ago

It would be fairly easy to create a single file plugin to run a specific command like you mention and make it available to the plugin. I'd be happy to program up an example of this and provide in the repository as an example like I have done for a couple of other use cases. Do you have an example command that I could use for that?

Thanks. I wrote a python script (embedded in a bash script that sets up a virtual environment) to read temperatures from my temperature sensor chip with a few command line switches to get Fahrenheit vs. Celsius.

The command is just the name of the script plus the Celsius flag PrusaTemperature.py -c

Wouldn't it be easier to just have a text box in your plugin that allows one to enter an arbitrary command string rather than having to create a single file plugin for each use case? Or am I missing something...

Thanks for all your contributions!

jneilliii commented 2 years ago

I don't necessarily want to add an open command line box. Because then someone's going to ask for two command line options, etc. and it snowballs into feature creep. Since your running python script it would actually be smarter just to wrap it in a plugin.

puterboy commented 2 years ago

Actually, it's a shell script wrapper for a python script whose first couple of lines launch a venv before 'exec'ing the python script so technically it's a shell script.

The first lines are:

#!/usr/bin/sh
"exec" "$(dirname $(readlink -f $0))/.venv/bin/python3" "$0" "$@"

Then the python code follows...

I'm sure there is a way to do this elegantly in pure python, but I am a python-beginner. Do you know how to do this natively in python?

(Also, there are other sensors that I may want access from a bash script that reads from GPIO's... so would be good to have a general framework too)

jneilliii commented 2 years ago

Yeah, so it's just executing python command line and piping the python code through. Can you share the whole thing possibly?

puterboy commented 2 years ago

Here is my code (though I don't really know Python - I much prefer Perl) It's rough so no argument or error checking.

If you just want to get a Celsius number run with arguments "2 1"

#!/usr/bin/sh
"exec" "$(dirname $(readlink -f $0))/.venv/bin/python3" "$0" "$@"
################################################################################
###Read temperature of Prusa Enclosure
################################################################################
import sys
import board
import adafruit_mcp9808

i2c = board.I2C() # uses board.SCL and board.SDA

addr = 0x18 #Default address with all set to low
#addr = i2c.scan()[0] #Get first i2c

mcp = adafruit_mcp9808.MCP9808(i2c, address=addr)

tempC = mcp.temperature
tempF = tempC * 1.8 + 32

if len(sys.argv) > 2 :
    degF = ""
    degC = ""
else:  #\xb0 is the unicode degree sign
    degF = "\xb0F"
    degC = "\xb0C"

if len(sys.argv) > 1 and sys.argv[1] == "1" :
    print("%.1f%s" % (tempF, degF))
elif len(sys.argv) > 1 and sys.argv[1] == "2" :
    print("%.1f%s" % (tempC, degC))
else :
    print("%.1f%s (%.1f%s)" % (tempF, degF, tempC, degC))
jneilliii commented 2 years ago

thanks. this is actually trickier than I thought using the adafruit circuitpython modules. do you still have the steps you used to install this in the venv? Not sure if my error is because I don't have a device or the modules aren't installed properly in my pi.

puterboy commented 2 years ago

Install was simple for me:

python3 -m venv .venv
source .venv/bin/activate

pip3 install pip --upgrade
pip3 install adafruit-circuitpython-mcp9808
puterboy commented 2 years ago

As an alternate "kludge" could you create a stripped-down plugin that allowed you to do a system call on an arbitrary script, using something like os.system()

jneilliii commented 2 years ago

I actually just realized I was installing the modules on a different pi than I was using the plugin on...lol. But now I can't get to either of them from work, so will play with it a bit tonight after getting back home.

jneilliii commented 2 years ago

ok, definitely works when you install the modules in the right spot. of course, I get i2c errors since I don't have the device. SSH to your pi and run these commands one at a time. Adjust the pi username to match your installation. These command assume default octopi install.

sudo apt-get install -y i2c-tools libgpiod-dev
/home/pi/oprint/bin/pip install adafruit-blinka
/home/pi/oprint/bin/pip install adafruit-circuitpython-mcp9808

This will install the required dependencies into OctoPrint's venv.

Then in OctoPrint's plugin manager > get more, copy/paste the URL below into ...from URL and click install.

https://github.com/jneilliii/OctoPrint-PlotlyTempGraph/raw/rc/MCP9808Graph.py

Once installed it will default to Celsius. If you want to have in Fahrenheit run this command in SSH.

/home/pi/oprint/bin/octoprint config set --bool plugins.MCP9808Graph.use_fahrenheit True
puterboy commented 2 years ago

Thanks. And what do I need to do to configure the plugin - and specifically to get PlotlyTempGraph to recognize it (i.e what name should I use?)

And is it sufficient to download the plugin manually and put it the Octoprint venv plugin directory?

jneilliii commented 2 years ago

once it's installed it should automatically pick-up the new temperature, assuming it is reporting. there is nothing to change outside of that one setting via commandline if you want to use fahrenheit values instead of celsius. if you want to download and install, you would actually put it into the basedir for octoprint under the plugins subfolder. the default for that is /home/pi/.octoprint/plugins, but like I mentioned in my last post you can just copy/paste the URL into plugin manager > get more > ...fromURL and that will install it where it needs to be automatically.

puterboy commented 2 years ago

Thanks. I actually ended up writing my own script since I didn't like the idea of the venv dependency and the need to duplicate all the Adafruit python libraries in Octoprint.

So based on the method used to access the MPC9808 in the OctoPrint Enclosure plugin and some code cleanup from reading the manpages from the chip, I was able to get the following "native" code to work based on your framework that doesn't require any additional python library install for me.

# coding=utf-8

################################################################################
### MCP9808 Sensor Plugin for Plotty Temp Graph plugin
### Based on plugin structure provided by jneilliii
### Based on MCP9808 code provided by Adafruit and from Octoprint-Enclosure
### Plus reference to datasheet: https://ww1.microchip.com/downloads/en/DeviceDoc/25095A.pdf
###
### Jeffrey J. Kosowsky
### August 7, 2022
###
################################################################################

from __future__ import absolute_import

import octoprint.plugin
from octoprint.util import RepeatedTimer
import smbus

# default I2C address for device.
MCP9808_I2CADDR_DEFAULT = 0x18

# register addresses.
MCP9808_REG_CONFIG = 0x01
MCP9808_REG_UPPER_TEMP = 0x02
MCP9808_REG_LOWER_TEMP = 0x03
MCP9808_REG_CRIT_TEMP = 0x04
MCP9808_REG_AMBIENT_TEMP = 0x05
MCP9808_REG_MANUF_ID = 0x06
MCP9808_REG_DEVICE_ID = 0x07
MCP9808_REG_RESOLUTION = 0x08

# configuration register values.
MCP9808_REG_CONFIG_CONTCONV = 0x0000
MCP9808_REG_CONFIG_SHUTDOWN = 0x0100
MCP9808_REG_CONFIG_CRITLOCKED = 0x0080
MCP9808_REG_CONFIG_WINLOCKED = 0x0040
MCP9808_REG_CONFIG_INTCLR = 0x0020
MCP9808_REG_CONFIG_ALERTSTAT = 0x0010
MCP9808_REG_CONFIG_ALERTCTRL = 0x0008
MCP9808_REG_CONFIG_ALERTSEL = 0x0002
MCP9808_REG_CONFIG_ALERTPOL = 0x0002
MCP9808_REG_CONFIG_ALERTMODE = 0x0001

# other config values (JJK)
MCP9808_RES_MEDIUM = 0x01
MCP9808_RES_HIGH = 0x02
MCP9808_RES_PRECISION = 0x03
DEFAULT_SMBUS_I2C_NUMBER = 0x01    #Default (first) i2c bus
DEFAULT_UPDATE_PERIOD = 2

class MCP9808Graph(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin):
        def __init__(self):
                self.last_temps = dict()
                self.poll_temps = None
                self.bus = smbus.SMBus(DEFAULT_SMBUS_I2C_NUMBER) #Use default (first) SMBus
                self.addr = MCP9808_I2CADDR_DEFAULT #Default address with all set to low (0x18)

                config = [MCP9808_REG_CONFIG_CONTCONV, 0x00] #Continuous conversion mode, power-up default
                self.bus.write_i2c_block_data(self.addr, MCP9808_REG_CONFIG, config)
                self.bus.write_byte_data(self.addr, MCP9808_REG_RESOLUTION, MCP9808_RES_PRECISION) #High precision +0.0625 degC, 0x03(03)

        def get_settings_defaults(self):
                return {"convertTo_celcius": False,
                        "convertTo_fahrenheit": False,}

        def on_after_startup(self):
                # start repeated timer for checking temp from sensor, runs every 5 seconds
                self.poll_temps = RepeatedTimer(DEFAULT_UPDATE_PERIOD, self.read_temp)
                self.poll_temps.start()

        def read_temp(self):
                currtemp = None
                try:
                        data = self.bus.read_i2c_block_data(self.addr, MCP9808_REG_AMBIENT_TEMP, 2) #2-byte temperature (temp-MSB, temp-LSB) from reg 0x05
                        #Note 16 bits of data are of form: AAASMMMM LLLLLLLL (where A=alert, M=MSB, L=LSB)
                        #i.e data is stored in 12-bits plus sign
                        #See https://ww1.microchip.com/downloads/en/DeviceDoc/25095A.pdf

                        currtemp = (data[0] & 0x0F) * 16 + data[1] * .0625 #Multiply MSB by 16, divide LSB by 16
                        if data[0] & 0x10 : #Sign bit
                                currtemp = 256 - currtemp

                        if currtemp:
                                if self._settings.get_boolean(["convertTo_fahrenheit"]):
                                        currtemp = currtemp * 1.8 + 32
                                elif self._settings.get_boolean(["convertTo_celcius"]):
                                        currtemp = (currtemp - 32) * 5/9                                
                                self.last_temps["MCP9808"] = (round(currtemp,1), None)
                except:
                        self._logger.debug("There was an error getting temperature from the MCP9808 temperature sensor")

        def temp_callback(self, comm, parsed_temps):
                parsed_temps.update(self.last_temps)
                return parsed_temps

__plugin_name__ = "MCP9808 Plotly Temp Graph Integration"
__plugin_pythoncompat__ = ">=3,<4"
__plugin_version__ = "0.1.0"
__plugin_implementation__ = MCP9808Graph()
__plugin_hooks__ = {
        "octoprint.comm.protocol.temperatures.received": __plugin_implementation__.temp_callback
}
puterboy commented 2 years ago

Addendum: I would like to extend the code to allow for more configuration variables -- including the i2C bus, the i2C address, and potentially the temp resolution.

I tried the following for the i2C address:

def __init__(self):
                self.last_temps = dict()
                self.poll_temps = None
                self.addr = self._settings.get_int(["i2c_addr"])
...
        def get_settings_defaults(self):
                return {"use_fahrenheit": False, "i2c_addr": MCP9808_I2CADDR_DEFAULT}

But it seems like the default settings are not accessible that way from the init portion... (or else perhaps I have some other Python error given that I no almost no Python)

So any suggestions how to make default configs work in the init section.

jneilliii commented 2 years ago

Yeah, you'd need to either add a super class to your init (not quite sure how that works) or initialize as none and then set it in on_after_startup or in the worker function read_temp altogether. Putting it in worker function would probably be better so that if it gets changed it picks up on the next repeated cycle.

puterboy commented 2 years ago

Here is a a version that allows you to call an arbitrary shell script that provides a temperature reading. The default config uses a shell command to read the RPi system temperature which seems like a generically useful thing to graph. However everything is configurable via the command line including: name of sensor, command to run, update frequency, output precision and convert to celsius/fahrenheit. So users should generally not need to touch the code itself.

# coding=utf-8

################################################################################
### SystemCmd Plugin for Plotty Temp Graph plugin
### Based on plugin structure provided by jneilliii
###
### Jeffrey J. Kosowsky
### August 5, 2022
###
### Note: configure from cli using:
###       /home/kosowsky/octoprint/venv/bin/octoprint config set [-bool|-int] plugins.SystemCmdGraph-.<config_variable> <config_value>
###
################################################################################

from __future__ import absolute_import

import octoprint.plugin
from octoprint.util import RepeatedTimer
import subprocess

# Config default values
SENSOR_NAME = "RPi"
SENSOR_CMD = "PATH='/usr/bin';vcgencmd measure_temp | sed 's/^temp=\([0-9.]*\).*/\\1/'"
OUTPUT_PRECISION = 1 #Output precision in digits
UPDATE_PERIOD = 5 #Seconds

class SystemCmdGraph(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin):
        def __init__(self):
                self.last_temps = dict()
                self.poll_temps = None

                self.sensor_name = None
                self.sensor_cmd = None
                self.output_precision = None
                self.update_period = None
                self.convertTo_celsius = None
                self.convertTo_fahrenheit = None

        def get_settings_defaults(self):
                return {
                        "sensor_name": SENSOR_NAME,
                        "sensor_cmd": SENSOR_CMD,
                        "output_precision": 1,
                        "convertTo_celsius": False,
                        "convertTo_fahrenheit": False,
                        "update_period": UPDATE_PERIOD,
                }

        def on_after_startup(self):
                self.sensor_name = self._settings.get(["sensor_name"])
                self.sensor_cmd = self._settings.get(["sensor_cmd"])
                self.output_precision = self._settings.get_int(["output_precision"])                
                self.convertTo_celsius = self._settings.get_boolean(["convertTo_celsius"])
                self.convertTo_fahrenheit = self._settings.get_boolean(["convertTo_fahrenheit"])
                self.update_period = self._settings.get_int(["update_period"])

                # start repeated timer for checking temp from sensor, runs every 5 seconds
                self.poll_temps = RepeatedTimer(self.update_period, self.read_temp)
                self.poll_temps.start()

        def read_temp(self):
                currtemp = None

                try:
                        currtemp = float(subprocess.check_output(self.sensor_cmd, shell=True))

                        if currtemp:
                                if self.convertTo_fahrenheit:
                                        currtemp = currtemp * 1.8 + 32
                                elif self.convertTo_celsius:
                                        currtemp = (currtemp - 32) * 5/9
                                currtemp = round(currtemp, self.output_precision)
                                self.last_temps[self.sensor_name] = (currtemp, None)
#                                self._logger.warning("JJK: {} : {}".format(self.sensor_name, currtemp)) #JJKDEBUG
                except:
                        self._logger.debug("There was an error getting temperature from the SystemCmd temperature plugin")

        def temp_callback(self, comm, parsed_temps):
                parsed_temps.update(self.last_temps)
                return parsed_temps

__plugin_name__ = "SystemCmd Plotly Temp Graph Integration"
__plugin_pythoncompat__ = ">=3,<4"
__plugin_version__ = "0.1.0"
__plugin_implementation__ = SystemCmdGraph()
__plugin_hooks__ = {
        "octoprint.comm.protocol.temperatures.received": __plugin_implementation__.temp_callback
}
puterboy commented 2 years ago

And here is an updated version of the python-native MCP9808 plugin with similar cli configurability:

# coding=utf-8

################################################################################
### MCP9808 Sensor Plugin for Plotty Temp Graph plugin
### Based on plugin structure provided by jneilliii
### Based on MCP9808 code provided by Adafruit and from Octoprint-Enclosure
### Plus reference to datasheet: https://ww1.microchip.com/downloads/en/DeviceDoc/25095A.pdf
###
### Jeffrey J. Kosowsky
### August 7, 2022
###
### Note: configure from cli using:
###       /home/kosowsky/octoprint/venv/bin/octoprint config set [-bool|-int] plugins.MCP9808Graph.<config_variable> <config_value>
###
################################################################################

from __future__ import absolute_import

import octoprint.plugin
from octoprint.util import RepeatedTimer
import smbus

# register addresses.
MCP9808_REG_CONFIG = 0x01
MCP9808_REG_UPPER_TEMP = 0x02
MCP9808_REG_LOWER_TEMP = 0x03
MCP9808_REG_CRIT_TEMP = 0x04
MCP9808_REG_AMBIENT_TEMP = 0x05
MCP9808_REG_MANUF_ID = 0x06
MCP9808_REG_DEVICE_ID = 0x07
MCP9808_REG_RESOLUTION = 0x08

# configuration register values.
MCP9808_REG_CONFIG_CONTCONV = 0x0000
MCP9808_REG_CONFIG_SHUTDOWN = 0x0100
MCP9808_REG_CONFIG_CRITLOCKED = 0x0080
MCP9808_REG_CONFIG_WINLOCKED = 0x0040
MCP9808_REG_CONFIG_INTCLR = 0x0020
MCP9808_REG_CONFIG_ALERTSTAT = 0x0010
MCP9808_REG_CONFIG_ALERTCTRL = 0x0008
MCP9808_REG_CONFIG_ALERTSEL = 0x0002
MCP9808_REG_CONFIG_ALERTPOL = 0x0002
MCP9808_REG_CONFIG_ALERTMODE = 0x0001

#Other chip defs (JJK)
MCP9808_RES_MEDIUM = 0x01
MCP9808_RES_HIGH = 0x02
MCP9808_RES_PRECISION = 0x03

# Config default values
SMBUS_NUMBER = 0x01 #Default (first) i2c bus
I2C_ADDR = 0x18 #Default i2c address with all set to low (0x18)
SENSOR_PRECISION = MCP9808_RES_PRECISION #Precision of the sensor itself
SENSOR_NAME = "MCP9808"
OUTPUT_PRECISION = 1 #Digits of output format resolution
UPDATE_PERIOD = 5 #Seconds

class MCP9808Graph(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin):
        def __init__(self):
                self.last_temps = dict()
                self.poll_temps = None

                self.smbus = None
                self.i2c_addr = None
                self.sensor_precision = None
                self.sensor_name = None
                self.output_precision = None
                self.convertTo_celsius = None
                self.convertTo_fahrenheit = None                
                self.update_period = None

        def get_settings_defaults(self):
                return {
                        "smbus_number": SMBUS_NUMBER,
                        "i2c_addr": I2C_ADDR,
                        "sensor_precision": SENSOR_PRECISION,
                        "sensor_name": SENSOR_NAME,
                        "output_precision": OUTPUT_PRECISION,
                        "convertTo_celsius": False,
                        "convertTo_fahrenheit": False,
                        "update_period" : UPDATE_PERIOD,
                }

        def on_after_startup(self):
               self.smbus = smbus.SMBus(self._settings.get(["smbus_number"]))
                self.i2c_addr = self._settings.get(["i2c_addr"])
                self.sensor_precision = self._settings.get(["sensor_precision"])
                self.sensor_name = self._settings.get(["sensor_name"])                
                self.output_precision = self._settings.get_int(["output_precision"])                
                self.convertTo_celsius = self._settings.get_boolean(["convertTo_celsius"])
                self.convertTo_fahrenheit = self._settings.get_boolean(["convertTo_fahrenheit"])
                self.update_period = self._settings.get_int(["update_period"])

                mcp9808_config = [MCP9808_REG_CONFIG_CONTCONV, 0x00] #Continuous conversion mode, power-up default
                self.smbus.write_i2c_block_data(self.i2c_addr, MCP9808_REG_CONFIG, mcp9808_config)
                self.smbus.write_byte_data(self.i2c_addr, MCP9808_REG_RESOLUTION, self.sensor_precision) #High precision +0.0625 degC, 0x03(03)    

                # start repeated timer for checking temp from sensor, runs every 5 seconds
                self.poll_temps = RepeatedTimer(self.update_period, self.read_temp)
                self.poll_temps.start()

        def read_temp(self):
                currtemp = None
                try:
                        data = self.smbus.read_i2c_block_data(self.i2c_addr, MCP9808_REG_AMBIENT_TEMP, 2) #2-byte temperature (temp-MSB, temp-LSB) from reg 0x05
                        #Note 16 bits of data are of form: AAASMMMM LLLLLLLL (where A=alert, M=MSB, L=LSB)
                        #i.e data is stored in 12-bits plus sign
                        #See https://ww1.microchip.com/downloads/en/DeviceDoc/25095A.pdf

                        currtemp = (data[0] & 0x0F) * 16 + data[1] * .0625 #Multiply MSB by 16, divide LSB by 16
                        if data[0] & 0x10: #Sign bit
                                currtemp = 256 - currtemp

                        if currtemp:
                                if self.convertTo_fahrenheit:
                                        currtemp = currtemp * 1.8 + 32
                                elif self.convertTo_celsius:
                                        currtemp = (currtemp - 32) * 5/9
                                currtemp = round(currtemp, self.output_precision)
                                self.last_temps[self.sensor_name] = (currtemp, None)
#                                self._logger.warning("JJK: {} : {}".format(self.sensor_name, currtemp)) #JJKDEBUG
                except:
                        self._logger.debug("There was an error getting temperature from the MCP9808 temperature sensor")

        def temp_callback(self, comm, parsed_temps):
                parsed_temps.update(self.last_temps)
                return parsed_temps

__plugin_name__ = "MCP9808 Plotly Temp Graph Integration"
__plugin_pythoncompat__ = ">=3,<4"
__plugin_version__ = "0.1.0"
__plugin_implementation__ = MCP9808Graph()
__plugin_hooks__ = {
        "octoprint.comm.protocol.temperatures.received": __plugin_implementation__.temp_callback
}
puterboy commented 2 years ago

And here is a version that accesses the RPi temp via the sysfs.

# coding=utf-8

################################################################################
### RPi sysfs Plugin for Plotty Temp Graph plugin
### Based on plugin structure provided by jneilliii
###
### Jeffrey J. Kosowsky
### August 7, 2022
###
### Note: configure from cli using:
###       /home/kosowsky/octoprint/venv/bin/octoprint config set [-bool|-int] plugins.RPiSysGraph.<config_variable> <config_value>
###
################################################################################

from __future__ import absolute_import

import octoprint.plugin
from octoprint.util import RepeatedTimer

# Config default values
SENSOR_NAME = "RPiSys"
SENSOR_PATH = "/sys/class/thermal/thermal_zone0/temp"
SENSOR_DIVISOR = 1000
OUTPUT_PRECISION = 1 #Output precision in digits
UPDATE_PERIOD = 5 #Seconds

class RPiSysGraph(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin):
        def __init__(self):
                self.last_temps = dict()
                self.poll_temps = None

                self.sensor_name = None
                self.sensor_path = None
                self.sensor_divisor = None
                self.output_precision = None
                self.update_period = None
                self.convertTo_celsius = None
                self.convertTo_fahrenheit = None

        def get_settings_defaults(self):
                return {
                        "sensor_name": SENSOR_NAME,
                        "sensor_path": SENSOR_PATH,
                        "sensor_divisor": SENSOR_DIVISOR,
                        "output_precision": 1,
                        "convertTo_celsius": False,
                        "convertTo_fahrenheit": False,
                        "update_period": UPDATE_PERIOD,
                }

        def on_after_startup(self):
                self.sensor_name = self._settings.get(["sensor_name"])
                self.sensor_path = self._settings.get(["sensor_path"])
                self.sensor_divisor = self._settings.get(["sensor_divisor"])
                self.output_precision = self._settings.get_int(["output_precision"])
                self.convertTo_celsius = self._settings.get_boolean(["convertTo_celsius"])
                self.convertTo_fahrenheit = self._settings.get_boolean(["convertTo_fahrenheit"])
                self.update_period = self._settings.get_int(["update_period"])

                # start repeated timer for checking temp from sensor, runs every 5 seconds
                self.poll_temps = RepeatedTimer(self.update_period, self.read_temp)
                self.poll_temps.start()

        def read_temp(self):
                currtemp = None

                try:
                        with open(r"/sys/class/thermal/thermal_zone0/temp") as File:
                                        currtemp = (float(File.readline()))/self.sensor_divisor

                        if currtemp:
                                if self.convertTo_fahrenheit:
                                        currtemp = currtemp * 1.8 + 32
                                elif self.convertTo_celsius:
                                        currtemp = (currtemp - 32) * 5/9
                                currtemp = round(currtemp, self.output_precision)
                                self.last_temps[self.sensor_name] = (currtemp, None)
#                                self._logger.warning("JJK: {} : {}".format(self.sensor_name, currtemp)) #JJKDEBUG
                except:
                        self._logger.debug("There was an error getting temperature from the SystemCmd temperature plugin")
#                        self._logger.warning ("There was an error getting temperature from the SystemCmd temperature plugin") #JJKDEBUG

        def temp_callback(self, comm, parsed_temps):
                parsed_temps.update(self.last_temps)
                return parsed_temps

__plugin_name__ = "RPiSys Plotly Temp Graph Integration"
__plugin_pythoncompat__ = ">=3,<4"
__plugin_version__ = "0.1.0"
__plugin_implementation__ = RPiSysGraph()
__plugin_hooks__ = {
        "octoprint.comm.protocol.temperatures.received": __plugin_implementation__.temp_callback
}
puterboy commented 2 years ago

Hope the above variations are helpful to people :)

jneilliii commented 2 years ago

I'm stripping them out and adding to an examples sub folder to release with the next version. I appreciate the contributions.

jneilliii commented 2 years ago

Not sure how much different the RPiSys one is different than my plotly_temp_graph_cpu_reporting.py example.

puterboy commented 2 years ago

@jneilliii, I would have liked each plugin to be able to host an array of like sensors. For example, if you have multiple MC9808 sensors, then it would have been nice to manage an array of such sensors in one plugin. Similarly, for the command version, it would be nice to allow an array of command accessing a bunch of different sensors potentially through very different commands. However, I couldn't figure out how to do that since temp_callback needs to return a pointer to a single parsed_temps [not sure if I am using the right python terminology].

Am I missing something simple? Because it would be really great if multiple "like" sensors could be hosted on a single plugin.

jneilliii commented 2 years ago

self.last_temps is a dictionary, so you can stuff as much data you want into it with each having their own unique "key". So something like this would add both temperature lines.

self.last_temps["sensor1"] = (currtemp, None)
self.last_temps["sensor2"] = (currtemp, None)
etc....
puterboy commented 2 years ago

Not sure how much different the RPiSys one is different than my plotly_temp_graph_cpu_reporting.py example.

Ahhh... I didn't see that example (it doesn't seem to be in the github heirarchy) so very possible that not very different. I did however facilitate changing parameters via configs by moving the setup to the on_startup section (which you had suggested and which seems to be the way others handle the situation).

jneilliii commented 2 years ago

Ah yeah I see now, it's in my rc branch (what I typically dev in), waiting for next release. BTW, running any of my plugins I recommend running in release candidate mode to have the latest and greatest, albeit possibly buggy (not usually).

https://github.com/jneilliii/OctoPrint-PlotlyTempGraph/blob/rc/plotly_temp_graph_cpu_reporting.py

puterboy commented 2 years ago

self.last_temps is a dictionary, so you can stuff as much data you want into it with each having their own unique "key". So something like this would add both temperature lines.

self.last_temps["sensor1"] = (currtemp, None)
self.last_temps["sensor2"] = (currtemp, None)
etc....

I tried something like that by my Python skills must have been lacking :( I will probably code an example using system commands since that could be a "poor man's" ones stop solution to monitoring multiple parameters with a single plugin...

puterboy commented 2 years ago

Ah yeah I see now, it's in my rc branch (what I typically dev in), waiting for next release. BTW, running any of my plugins I recommend running in release candidate mode to have the latest and greatest, albeit possibly buggy (not usually).

https://github.com/jneilliii/OctoPrint-PlotlyTempGraph/blob/rc/plotly_temp_graph_cpu_reporting.py

Yours is more structured and high level using 'psutil' lib. (and thus better :) Mine is more hacky by directly reading /sys/class/ etc.

puterboy commented 2 years ago

Ah yeah I see now, it's in my rc branch (what I typically dev in), waiting for next release. BTW, running any of my plugins I recommend running in release candidate mode to have the latest and greatest, albeit possibly buggy (not usually).

https://github.com/jneilliii/OctoPrint-PlotlyTempGraph/blob/rc/plotly_temp_graph_cpu_reporting.py

BTW, if I am reading your code correctly, you have duplicated the cpu_thermal line

if "cpu-thermal" in temp:
    cpu_temp = temp["cpu-thermal"][0].current
if "cpu_thermal" in temp:
    cpu_temp = temp["cpu_thermal"][0].current
puterboy commented 2 years ago

Couple of finesse questions:

  1. Is it possible to set the display name, color and hidden feature from the plugin or are those fields only configurable from Plotly itself?
  2. Does Plotly always have to add "actual" to the name or is there a way to suppress that from the plugin?
jneilliii commented 2 years ago

BTW, if I am reading your code correctly, you have duplicated the cpu_thermal line

ah, thanks.

  1. No, that's only possible from this plugin's side.
  2. Those are coming from OctoPrint that way, which is why I added the label option so you can change what it displays.
jneilliii commented 2 years ago

Actually, that's not a duplication in the CPU example. dash vs underscore.

puterboy commented 2 years ago

Actually, that's not a duplication in the CPU example. dash vs underscore.

I have old eyes I guess :)

puterboy commented 2 years ago

Here is one (hopefully) final example using multiple system commands in one plugin:


# coding=utf-8

################################################################################
### SystemCmdMulti Plugin for Plotty Temp Graph plugin
### Allows multiple system commands to be used to query multiple temperature variables
### Based on plugin structure provided by jneilliii
###
### Jeffrey J. Kosowsky
### August 8, 2022
###
### Note: configure from cli using:
###       /home/kosowsky/octoprint/venv/bin/octoprint config set [-bool|-int] plugins.SystemCmdMultiGraph.<config_variable> <config_value>
###
################################################################################

from __future__ import absolute_import

import octoprint.plugin
from octoprint.util import RepeatedTimer
import subprocess

# Config default values
POLL_INTERVAL = 5 #Seconds

SYSTEM_CMDS = {
        'RPiCmd': {
                'cmd': "PATH='/usr/bin';vcgencmd measure_temp | sed 's/^temp=\([0-9.]*\).*/\\1/'",
                'precision': 1,
                'convertTo_celsius': False,
                'convertTo_fahrenheit': False,
        },
        'RPiCmd2': {
                'cmd': "printf '%.1f' $(cat /sys/class/thermal/thermal_zone0/temp)e-3",
                },
}

#NOTE: attributes: 'precision, 'convertTo_celsius', 'convertTo_fahrenheit' are *optional*
DEFAULT_PRECISION = 1

class SystemCmdMultiGraph(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin):
        def __init__(self):
                self.last_temps = dict()
                self.poll_temps = None

                self.poll_interval = None
                self.system_cmds = None

        def get_settings_defaults(self):
                return {
                        "poll_interval": POLL_INTERVAL,
                        "system_cmds": SYSTEM_CMDS,
                }

        def on_after_startup(self):
                self.poll_interval = self._settings.get_int(["poll_interval"])
                self.system_cmds = self._settings.get(["system_cmds"])
                for sensor in self.system_cmds :
                        sensorval = self.system_cmds[sensor]
                        if not hasattr(sensorval, 'precision'):
                                sensorval['precision'] = DEFAULT_PRECISION
                        if not hasattr(sensorval, 'convertTo_fahrenheit'):
                                sensorval['convertTo_fahrenheit'] = False
                        if not hasattr(sensorval, 'convertTo_celsius'):
                                sensorval['convertTo_fahrenheit'] = False
                        if not hasattr(sensorval, 'convertTo_celsius'):
                                sensorval['convertTo_celsius'] = False                                

                # start repeated timer for checking temp from sensor, runs every 5 seconds
                self.poll_temps = RepeatedTimer(self.poll_interval, self.read_temp)
                self.poll_temps.start()

        def read_temp(self):
                for sensor in self.system_cmds :
                        sensorval = self.system_cmds[sensor]
                        current_temp = None

                        try:
                                current_temp = float(subprocess.check_output(sensorval['cmd'], shell=True))

                                if current_temp:
                                        if sensorval['convertTo_fahrenheit']:
                                                current_temp = current_temp * 1.8 + 32
                                        elif sensorval['convertTo_celsius']:
                                                current_temp = (current_temp - 32) * 5/9
                                        current_temp = round(current_temp, sensorval['precision'])
                                        self.last_temps[sensor] = (current_temp, None)
#                                        self._logger.warning("JJK: {} : {}".format(sensor, current_temp)) #JJKDEBUG
                        except:
                                self._logger.debug("There was an error getting temperature from the SystemCmdMulti temperature plugin: {}".format(sensor))
#                                self._logger.warning ("Error getting temperature: {} : {}".format(sensor, sensorval)) #JJKDEBUG

        def temp_callback(self, comm, parsed_temps):
                parsed_temps.update(self.last_temps)
                return parsed_temps

__plugin_name__ = "SystemCmdMulti Plotly Temp Graph Integration"
__plugin_pythoncompat__ = ">=3,<4"
__plugin_version__ = "0.1.0"
__plugin_implementation__ = SystemCmdMultiGraph()
__plugin_hooks__ = {
        "octoprint.comm.protocol.temperatures.received": __plugin_implementation__.temp_callback
}
puterboy commented 2 years ago

In case, people are looking for more examples, here is one more for Prusa MK3 that reads in the temperature from the Einsy board. (based on the code you suggested for Klipper: https://raw.githubusercontent.com/jneilliii/OctoPrint-PlotlyTempGraph/master/klipper_additional_temp.py)

# coding=utf-8

################################################################################
### MK3 Einsy Temperature Plugin for Plotty Temp Graph plugin
### Reads temperature from M105 command sent from MK3 while printing
### Based on plugin structure provided by jneilliii
###
### Jeffrey J. Kosowsky
### August 8, 2022
###
### To activate, store (copy) in: ~/.octoprint/plugins
### Note: configure from cli using:
###       ~/octoprint/venv/bin/octoprint config set [-bool|-int] plugins.MK3TempGraph.<config_variable> <config_value>
### e.g.,
###       ~/octoprint/venv/bin/octoprint config set -bool plugins.MK3TempGraph.convertTo_fahrenheit True
###
################################################################################

from __future__ import absolute_import

import octoprint.plugin
import re

# Config default values
SENSOR_NAME = "Einsy"
OUTPUT_PRECISION = 1 #Output precision in digits
TEMP_REGEX = ".+\sA:(?P<temp>\d+\.\d)"
#Note: M105 response is of form:
#      T:240.1 /240.0 B:80.1 /85.0 T0:240.1 /240.0 @:28 B@:127 P:0.0 A:40.0

class MK3TempGraph(octoprint.plugin.StartupPlugin, octoprint.plugin.SettingsPlugin):
        def __init__(self):
                self.last_temps = dict()

                self.sensor_name = None
                self.output_precision = None
                self.convertTo_celsius = None
                self.convertTo_fahrenheit = None
                self.temp_regex = None

        def get_settings_defaults(self):
                return {
                        "sensor_name": SENSOR_NAME,
                        "output_precision": 1,
                        "convertTo_celsius": False,
                        "convertTo_fahrenheit": False,
                        "temp_regex": TEMP_REGEX,
                }

        def on_after_startup(self):
                self.sensor_name = self._settings.get(["sensor_name"])
                self.output_precision = self._settings.get_int(["output_precision"])
                self.convertTo_celsius = self._settings.get_boolean(["convertTo_celsius"])
                self.convertTo_fahrenheit = self._settings.get_boolean(["convertTo_fahrenheit"])
                self.temp_regex = re.compile(self._settings.get(["temp_regex"]))

        def gcode_callback(self, comm, line, *args, **kwargs):
                if not line.startswith("T:"):
                        return line
#                self._logger.warning("JJK: {} : {}".format(self.sensor_name,line)) #JJKDEBUG                                

                try:
                        match = self.temp_regex.match(line)
                        current_temp = float(match.group("temp"))
                        if self.convertTo_fahrenheit:
                                current_temp = current_temp * 1.8 + 32
                        elif self.convertTo_celsius:
                                current_temp = (current_temp - 32) * 5/9

                        self.last_temps[self.sensor_name] = (current_temp, None)
#                        self._logger.warning("JJK: {} : {}".format(self.sensor_name, current_temp)) #JJKDEBUG                                

                except:
                        self._logger.debug("Error getting temperature from the MK3TempGraph temperature plugin")
#                        self._logger.warning ("Error getting temperature from the MK3TempGraph temperature plugin") #JJKDEBUG

                return line

        def temp_callback(self, comm, parsed_temps):
                parsed_temps.update(self.last_temps)
                return parsed_temps

__plugin_name__ = "MK3TempGraph Plotly Temp Graph Integration"
__plugin_pythoncompat__ = ">=3,<4"
__plugin_version__ = "0.1.0"
__plugin_implementation__ = MK3TempGraph()
__plugin_hooks__ = {
        "octoprint.comm.protocol.temperatures.received": __plugin_implementation__.temp_callback,
        "octoprint.comm.protocol.gcode.received": __plugin_implementation__.gcode_callback
}
jneilliii commented 2 years ago

Is this Prusa MK3 example even needed with the latest OctoPrint 1.8.0+ versions? I think OctoPrint will natively pull the A and P values won't it?

puterboy commented 2 years ago

You are correct. Still a good example but unnecessary.

puterboy commented 2 years ago

BTW, I also noticed sensor names 'E actual' and 'W actual'. Though they both always display zero (as well as 'P actual' 'W actual' starts displaying when printing starts 'E actual' starts displayin 'W actual' doesn't display

Do you know what 'E' and 'W' are?

jneilliii commented 2 years ago

If I remember correctly those are associated with time to reach temperature (at least W is). E may be time to cool down to temperature, but unsure.

puterboy commented 2 years ago

The fact that those numbers (as well as P) seem to be always 0, does that mean that the functionality is not implemented (or at least not engaged) on my MK3S+?

jneilliii commented 2 years ago

I cannot say as I do not have one of those printers or use the Prusa firmware personally. I do know that W tends to stay 0 a lot, but does come in occasionally with numbers so not sure how the actual feature works.