Fixes #92, configurable temps; help text & docs clean-up; versions are now built into binaries at compile time; clean up old extensions params; adds --list command; adds --write-config command; fixes bug in colorscheme loading; fixes bug in Colorschemes -- all had empty Name attrs; adds ability to list devices.

This commit is contained in:
Sean E. Russell 2020-04-23 10:41:20 -05:00
parent 3fcab5a420
commit 5ada5315d9
15 changed files with 316 additions and 90 deletions

View File

@ -29,5 +29,3 @@ jobs:
repository: xxxserxxx/gotop-linux
event-type: my-release
client-payload: '{"tag": "${{ steps.tag_name.outputs.tag }}"}'
# TODO: Build the plugins too

View File

@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ignore lines matching `/^#.*/` in layout files.
- Instructions for Gentoo (thanks @tormath1!)
- Graph labels that don't fit (vertically) in the window are now drawn in additional columns (#40)
- Adds ability to filter reported temperatures (#92)
### Changed
@ -44,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Memory line colors were inconsistently assigned (#91)
- The disk code was truncating values instead of rounding (#90)
- Temperatures on Darwin were all over the place, and wrong (#48)
- Config file loading from `~/.config/gotop` wasn't working
## [3.5.1] - 2020-04-09

View File

@ -127,12 +127,30 @@ Move `gotop` to somewhere in your `$PATH`.
- click to select process
- mouse wheel to scroll through processes
### Config file
Most command-line settings can be persisted into a configuration file. The config file is named `gotop.conf` and can be located in several places. The first place gotop will look is in the current directory; after this, the locations depend on the OS and distribution. On Linux using XDG, for instance, the home location of `~/.config/gotop/gotop.conf` is the second location. The last location is a system-wide global location, such as `/etc/gotop/gotop.conf`. The `-h` help command will print out all of the locations, in order. Command-line options override values in any config files, and only the first config file found is loaded.
A configuration file can be created using the `--write-config` command-line argument. This will try to place the config file in the home config directory (the second location), but if it's unable to do so it'll write a file to the current directory.
Config file changes can be made by combining command-line arguments with `--write-config`. For example, to persist the `solarized` theme, call:
```
gotop -c solarized --write-config
```
### Colorschemes
gotop ships with a few colorschemes which can be set with the `-c` flag followed by the name of one. You can find all the colorschemes in the [colorschemes folder](./colorschemes).
To make a custom colorscheme, check out the [template](./colorschemes/template.go) for instructions and then use [default.json](./colorschemes/default.json) as a starter. Then put the file at `~/.config/gotop/<name>.json` and load it with `gotop -c <name>`. Colorschemes PR's are welcome!
To list all built-in color schemes, call:
```
gotop --list colorschemes
```
### Layouts
gotop can parse and render layouts from a specification file. The format is
@ -206,6 +224,49 @@ and these are separated by spaces.
Yes, you're clever enough to break the layout algorithm, but if you try to
build massive edifices, you're in for disappointment.
To list all built-in color schemes, call:
```
gotop --list layouts
```
### Device filtering
Some devices have quite a number of data points; on OSX, for instance, there are dozens of temperature readings. These can be filtered through a configuration file. There is no command-line argument for this filter.
The list will grow, but for now the only device that supports filtering is the temperature widget. The configuration entry is called `temperature`, and it contains an exact-match list of comma-separated values with no spaces. To see the list of valid values, run gotop with the `--list devices` command. Gotop will print out the type of device and the legal values. For example, on Linux:
```
$ gotop --list devices
Temperatures:
acpitz
nvme_composite
nvme_sensor1
nvme_sensor2
pch_cannonlake
coretemp_packageid0
coretemp_core0
coretemp_core1
coretemp_core2
coretemp_core3
ath10k_hwmon
```
You might then add the following line to the config file. First, find where gotop looks for config files:
```
$ gotop -h | tail -n 6
Colorschemes & layouts that are not built-in are searched for (in order) in:
/home/USER/workspace/gotop.d/gotop, /home/USER/.config/gotop, /etc/xdg/gotop
The first path in this list is always the cwd. The config file
'gotop.config' can also reside in one of these directories.
Log files are stored in /home/ser/.cache/gotop/errors.log
```
So you might use `/home/YOU/.config/gotop.conf`, and add (or modify) this line:
```
temperatures=acpitz,coretemp_core0,ath10k_hwmon
```
This will cause the temp widget to show only four of the eleven temps.
### CLI Options
Run `gotop -h` to see the list of all command line options.

View File

@ -8,6 +8,7 @@ import (
"os"
"os/signal"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"
@ -20,6 +21,7 @@ import (
"github.com/xxxserxxx/gotop/v3"
"github.com/xxxserxxx/gotop/v3/colorschemes"
"github.com/xxxserxxx/gotop/v3/devices"
"github.com/xxxserxxx/gotop/v3/layout"
"github.com/xxxserxxx/gotop/v3/logging"
w "github.com/xxxserxxx/gotop/v3/widgets"
@ -37,8 +39,8 @@ const (
)
var (
// TODO: Set this at compile time; having to check this in sucks.
Version = "3.6.dev"
Version = "0.0.0"
BuildDate = "Hadean"
conf gotop.Config
help *w.HelpMenu
bar *w.StatusBar
@ -48,17 +50,14 @@ var (
// TODO: Add tab completion for Linux https://gist.github.com/icholy/5314423
// TODO: state:merge #135 linux console font (cmatsuoka/console-font)
// TODO: state:deferred 157 FreeBSD fixes & Nvidia GPU support (kraust/master). Significant CPU use impact for NVidia changes.
// TODO: Virtual devices from Prometeus metrics @feature
// TODO: Abstract out the UI toolkit. mum4k/termdash, VladimirMarkelov/clui, gcla/gowid, rivo/tview, marcusolsson/tui-go might work better for some OS/Archs. Performance/memory use comparison would be interesting.
// TODO: Add fans
func parseArgs(conf *gotop.Config) error {
cds := conf.ConfigDir.QueryFolders(configdir.All)
cpaths := make([]string, len(cds))
for i, p := range cds {
cpaths[i] = p.Path
}
usage := fmt.Sprintf(`
usage := fmt.Sprintln(`
Usage: gotop [options]
Options:
@ -77,40 +76,15 @@ Options:
-l, --layout=NAME Name of layout spec file for the UI. Looks first in $XDG_CONFIG_HOME/gotop, then as a path. Use "-" to pipe.
-i, --interface=NAME Select network interface [default: all]. Several interfaces can be defined using comma separated values. Interfaces can also be ignored using !
-x, --export=PORT Enable metrics for export on the specified port.
-X, --extensions=NAMES Enables the listed extensions. This is a comma-separated list without the .so suffix. The current and config directories will be searched.
--mbps Net widget shows mb(its)ps for RX/TX instead of scaled bytes per second.
--test Runs tests and exits with success/failure code.
--print-paths List out the paths that gotop will look for gotop.conf, layouts, color schemes, and extensions.
--print-keys Show the keyboard bindings.
Built-in layouts:
default
minimal
battery
kitchensink
Colorschemes:
default
default-dark (for white background)
solarized
solarized16-dark
solarized16-light
monokai
vice
Colorschemes and layouts that are not built-in are searched for (in order) in:
%s
The first path in this list is always the cwd.
Log files are stored in %s
`, strings.Join(cpaths, ", "), filepath.Join(conf.ConfigDir.QueryCacheFolder().Path, logging.LOGFILE))
var err error
conf.Colorscheme, err = colorschemes.FromName(conf.ConfigDir, "default")
if err != nil {
return err
}
--list <devices|layouts|colorschemes|paths|keys>
devices: Prints out device names for widgets supporting filters.
layouts: Lists build-in layouts
colorschemes: Lists built-in colorschemes
paths: List out the paths that gotop will look for gotop.conf, layouts, and color schemes.
keys: Show the keyboard bindings.
--write-config Write out a sample config file, either to the home config location, or current directory. Command-line arguments specificed at the same time will overwrite values in the config file.`)
args, err := docopt.ParseArgs(usage, os.Args[1:], Version)
if err != nil {
@ -127,6 +101,7 @@ Log files are stored in %s
}
conf.Colorscheme = cs
}
if args["--averagecpu"].(bool) {
conf.AverageLoad, _ = args["--averagecpu"].(bool)
}
@ -166,10 +141,6 @@ Log files are stored in %s
if val, _ := args["--interface"]; val != nil {
conf.NetInterface, _ = args["--interface"].(string)
}
if val, _ := args["--extensions"]; val != nil {
exs, _ := args["--extensions"].(string)
conf.Extensions = strings.Split(exs, ",")
}
if val, _ := args["--test"]; val != nil {
conf.Test = val.(bool)
}
@ -189,16 +160,35 @@ Log files are stored in %s
if args["--mbps"].(bool) {
conf.Mbps = true
}
if args["--print-paths"].(bool) {
paths := make([]string, 0)
for _, d := range conf.ConfigDir.QueryFolders(configdir.All) {
paths = append(paths, d.Path)
}
fmt.Println(strings.Join(paths, "\n"))
os.Exit(0)
}
if args["--print-keys"].(bool) {
fmt.Println(`
if val, _ := args["--list"]; val != nil {
switch val {
case "layouts":
fmt.Println("Built-in layouts:")
fmt.Println("\tdefault")
fmt.Println("\tminimal")
fmt.Println("\tbattery")
fmt.Println("\tkitchensink")
case "colorschemes":
fmt.Println("Built-in colorschemes:")
fmt.Println("\tdefault")
fmt.Println("\tdefault-dark (for white background)")
fmt.Println("\tsolarized")
fmt.Println("\tsolarized16-dark")
fmt.Println("\tsolarized16-light")
fmt.Println("\tmonokai")
fmt.Println("\tvice")
case "paths":
fmt.Println("Loadable colorschemes & layouts, and the config file, are searched for, in order:")
paths := make([]string, 0)
for _, d := range conf.ConfigDir.QueryFolders(configdir.All) {
paths = append(paths, d.Path)
}
fmt.Println(strings.Join(paths, "\n"))
fmt.Printf("\nThe log file is in %s\n", filepath.Join(conf.ConfigDir.QueryCacheFolder().Path, logging.LOGFILE))
case "devices":
listDevices()
case "keys":
fmt.Println(`
Quit: q or <C-c>
Process navigation:
k and <Up>: up
@ -227,6 +217,19 @@ CPU and Mem graph scaling:
h: scale in
l: scale out
?: toggles keybind help menu`)
default:
fmt.Printf("Unknown option \"%s\"; try layouts, colorschemes, or devices\n", val)
os.Exit(1)
}
os.Exit(0)
}
if args["--write-config"].(bool) {
path, err := conf.Write()
if err != nil {
fmt.Printf("Failed to write configuration file: %s\n", err)
os.Exit(1)
}
fmt.Printf("Config written to %s\n", path)
os.Exit(0)
}
@ -437,9 +440,11 @@ func makeConfig() gotop.Config {
MaxLogSize: 5000000,
Layout: "default",
}
conf.Colorscheme, _ = colorschemes.FromName(conf.ConfigDir, "default")
return conf
}
// TODO: Add fans
// TODO: mpd visualizer widget
func main() {
// This is just to make sure gotop returns a useful exit code, but also
@ -567,3 +572,14 @@ func runTests(conf gotop.Config) int {
fmt.Printf("PASS")
return 0
}
func listDevices() {
ms := devices.Domains
sort.Strings(ms)
for _, m := range ms {
fmt.Printf("%s:\n", m)
for _, d := range devices.Devices(m) {
fmt.Printf("\t%s\n", d)
}
}
}

View File

@ -29,6 +29,7 @@ func register(name string, c Colorscheme) {
if registry == nil {
registry = make(map[string]Colorscheme)
}
c.Name = name
registry[name] = c
}

103
config.go
View File

@ -2,10 +2,10 @@ package gotop
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@ -15,7 +15,6 @@ import (
"github.com/xxxserxxx/gotop/v3/widgets"
)
// TODO: test, build, release [#119] [#120] [#121]
type Config struct {
ConfigDir configdir.ConfigDir
@ -32,30 +31,31 @@ type Config struct {
Layout string
MaxLogSize int64
ExportPort string
Extensions []string
Mbps bool
Temps []string
Test bool
}
func (conf *Config) Load() error {
var in io.Reader
var in []byte
var err error
cfn := "gotop.conf"
folder := conf.ConfigDir.QueryFolderContainsFile(cfn)
if folder != nil {
// FIXME: Shouldn't this be looking in folder??
if cf, err := os.Open(cfn); err == nil {
defer cf.Close()
} else {
if in, err = folder.ReadFile(cfn); err != nil {
return err
}
} else {
return nil
}
r := bufio.NewScanner(in)
r := bufio.NewScanner(bytes.NewReader(in))
var lineNo int
for r.Scan() {
l := strings.TrimSpace(r.Text())
if l[0] == '#' {
continue
}
kv := strings.Split(l, "=")
if len(kv) != 2 {
return fmt.Errorf("bad config file syntax; should be KEY=VALUE, was %s", l)
@ -70,72 +70,125 @@ func (conf *Config) Load() error {
log.Printf("logdir is deprecated. Ignored logdir=%s", kv[1])
case "logfile":
log.Printf("logfile is deprecated. Ignored logfile=%s", kv[1])
case "graphhorizontalscale":
case graphhorizontalscale:
iv, err := strconv.Atoi(kv[1])
if err != nil {
return err
}
conf.GraphHorizontalScale = iv
case "helpvisible":
case helpvisible:
bv, err := strconv.ParseBool(kv[1])
if err != nil {
return fmt.Errorf("line %d: %v", lineNo, err)
}
conf.HelpVisible = bv
case "colorscheme":
case colorscheme:
cs, err := colorschemes.FromName(conf.ConfigDir, kv[1])
if err != nil {
return fmt.Errorf("line %d: %v", lineNo, err)
}
conf.Colorscheme = cs
case "updateinterval":
case updateinterval:
iv, err := strconv.Atoi(kv[1])
if err != nil {
return err
}
conf.UpdateInterval = time.Duration(iv)
case "averagecpu":
case averagecpu:
bv, err := strconv.ParseBool(kv[1])
if err != nil {
return fmt.Errorf("line %d: %v", lineNo, err)
}
conf.AverageLoad = bv
case "percpuload":
case percpuload:
bv, err := strconv.ParseBool(kv[1])
if err != nil {
return fmt.Errorf("line %d: %v", lineNo, err)
}
conf.PercpuLoad = bv
case "tempscale":
case tempscale:
iv, err := strconv.Atoi(kv[1])
if err != nil {
return err
}
conf.TempScale = widgets.TempScale(iv)
case "statusbar":
case statusbar:
bv, err := strconv.ParseBool(kv[1])
if err != nil {
return fmt.Errorf("line %d: %v", lineNo, err)
}
conf.Statusbar = bv
case "netinterface":
case netinterface:
conf.NetInterface = kv[1]
case "layout":
case layout:
conf.Layout = kv[1]
case "maxlogsize":
case maxlogsize:
iv, err := strconv.Atoi(kv[1])
if err != nil {
return err
}
conf.MaxLogSize = int64(iv)
case "export":
case export:
conf.ExportPort = kv[1]
case "extensions":
conf.Extensions = strings.Split(kv[1], ",")
case "mbps":
case mbps:
conf.Mbps = true
case temperatures:
conf.Temps = strings.Split(kv[1], ",")
}
}
return nil
}
func (c *Config) Write() (string, error) {
cfn := "gotop.conf"
ds := c.ConfigDir.QueryFolders(configdir.Global)
if len(ds) == 0 {
ds = c.ConfigDir.QueryFolders(configdir.Local)
if len(ds) == 0 {
return "", fmt.Errorf("error locating config folders")
}
}
marshalled := marshal(c)
err := ds[0].WriteFile(cfn, marshalled)
if err != nil {
return "", err
}
return filepath.Join(ds[0].Path, cfn), nil
}
func marshal(c *Config) []byte {
buff := bytes.NewBuffer(nil)
fmt.Fprintf(buff, "%s=%d\n", graphhorizontalscale, c.GraphHorizontalScale)
fmt.Fprintf(buff, "%s=%t\n", helpvisible, c.HelpVisible)
fmt.Fprintf(buff, "%s=%s\n", colorscheme, c.Colorscheme.Name)
fmt.Fprintf(buff, "%s=%d\n", updateinterval, c.UpdateInterval)
fmt.Fprintf(buff, "%s=%t\n", averagecpu, c.AverageLoad)
fmt.Fprintf(buff, "%s=%t\n", percpuload, c.PercpuLoad)
fmt.Fprintf(buff, "%s=%d\n", tempscale, c.TempScale)
fmt.Fprintf(buff, "%s=%t\n", statusbar, c.Statusbar)
fmt.Fprintf(buff, "%s=%s\n", netinterface, c.NetInterface)
fmt.Fprintf(buff, "%s=%s\n", layout, c.Layout)
fmt.Fprintf(buff, "%s=%d\n", maxlogsize, c.MaxLogSize)
fmt.Fprintf(buff, "%s=%s\n", export, c.ExportPort)
fmt.Fprintf(buff, "%s=%t\n", mbps, c.Mbps)
fmt.Fprintf(buff, "%s=%s\n", temperatures, strings.Join(c.Temps, ","))
return buff.Bytes()
}
const (
graphhorizontalscale = "graphhorizontalscale"
helpvisible = "helpvisible"
colorscheme = "colorscheme"
updateinterval = "updateinterval"
averagecpu = "averagecpu"
percpuload = "percpuload"
tempscale = "tempscale"
statusbar = "statusbar"
netinterface = "netinterface"
layout = "layout"
maxlogsize = "maxlogsize"
export = "metricsexportport"
mbps = "mbps"
temperatures = "temperatures"
)

View File

@ -2,7 +2,13 @@ package devices
import "log"
const (
Temperatures = "Temperatures"
)
var Domains []string = []string{Temperatures}
var shutdownFuncs []func() error
var _devs map[string][]string
// RegisterShutdown stores a function to be called by gotop on exit, allowing
// extensions to properly release resources. Extensions should register a
@ -24,3 +30,18 @@ func Shutdown() {
}
}
}
func RegisterDeviceList(typ string, f func() []string) {
if _devs == nil {
_devs = make(map[string][]string)
}
if ls, ok := _devs[typ]; ok {
_devs[typ] = append(ls, f()...)
return
}
_devs[typ] = f()
}
func Devices(domain string) []string {
return _devs[domain]
}

View File

@ -6,6 +6,7 @@ import smc "github.com/xxxserxxx/iSMC"
func init() {
RegisterTemp(update)
RegisterDeviceList(Temperatures, devs)
ts = make(map[string]float32)
}
@ -17,7 +18,19 @@ func update(temps map[string]int) map[string]error {
return map[string]error{"temps": err}
}
for k, v := range ts {
temps[k] = int(v + 0.5)
if _, ok := temps[k]; ok {
temps[k] = int(v + 0.5)
}
}
return nil
}
// TODO: Set reasonable default devices
// CPU (TC[01]P), GPU (TG0P), Memory (Ts0S) and Disk (TH0P)
func devs() []string {
rv := make([]string, len(smc.AppleTemp))
for i, v := range smc.AppleTemp {
rv[i] = v.Desc
}
return rv
}

View File

@ -12,6 +12,7 @@ import (
func init() {
RegisterTemp(update)
RegisterDeviceList(Temperatures, devs)
}
var sensorOIDS = map[string]string{
@ -23,6 +24,9 @@ func update(temps map[string]int) map[string]error {
var errors map[string]error
for k, v := range sensorOIDS {
if _, ok := temps[k]; !ok {
continue
}
output, err := exec.Command("sysctl", "-n", k).Output()
if err != nil {
errors[v] = err
@ -43,3 +47,11 @@ func update(temps map[string]int) map[string]error {
return errors
}
func devs() []string {
rv := make([]string, 0, len(sensorOIDS))
for k, _ := range sensorOIDS {
rv = append(rv, k)
}
return rv
}

View File

@ -10,6 +10,7 @@ import (
func init() {
RegisterTemp(getTemps)
RegisterDeviceList(Temperatures, devs)
}
func getTemps(temps map[string]int) map[string]error {
@ -18,12 +19,31 @@ func getTemps(temps map[string]int) map[string]error {
return map[string]error{"psHost": err}
}
for _, sensor := range sensors {
// only sensors with input in their name are giving us live temp info
if strings.Contains(sensor.SensorKey, "input") && sensor.Temperature != 0 {
// removes '_input' from the end of the sensor name
label := sensor.SensorKey[:strings.Index(sensor.SensorKey, "_input")]
temps[label] = int(sensor.Temperature)
// removes '_input' from the end of the sensor name
idx := strings.Index(sensor.SensorKey, "_input")
if idx >= 0 {
label := sensor.SensorKey[:idx]
if _, ok := temps[label]; ok {
temps[label] = int(sensor.Temperature)
}
}
}
return nil
}
func devs() []string {
sensors, err := psHost.SensorsTemperatures()
if err != nil {
return []string{}
}
rv := make([]string, 0, len(sensors))
for _, sensor := range sensors {
// only sensors with input in their name are giving us live temp info
if strings.Contains(sensor.SensorKey, "input") && sensor.Temperature != 0 {
// removes '_input' from the end of the sensor name
label := sensor.SensorKey[:strings.Index(sensor.SensorKey, "_input")]
rv = append(rv, label)
}
}
return rv
}

View File

@ -15,6 +15,7 @@ import (
"unsafe"
)
// TODO: Add filtering. Getting the temperature sensor names is non-trivial for OpenBSD, and until I can test it, leave it unimplemented
func init() {
RegisterTemp(update)
}
@ -66,7 +67,9 @@ func getTemp(temps map[string]int, mib []C.int, mlen int, snsrdev *C.struct_sens
key := C.GoString(&snsrdev.xname[0]) + ".temp" + strconv.Itoa(index)
temp := int((snsr.value - 273150000.0) / 1000000.0)
temps[key] = temp
if _, ok := temps[key]; ok {
temps[key] = temp
}
}
}
}

View File

@ -8,6 +8,7 @@ import (
func init() {
RegisterTemp(update)
RegisterDeviceList(Temperatures, devs)
}
func update(temps map[string]int) map[string]error {
@ -16,9 +17,23 @@ func update(temps map[string]int) map[string]error {
return map[string]error{"gopsutil": err}
}
for _, sensor := range sensors {
if sensor.Temperature != 0 {
temps[sensor.SensorKey] = int(sensor.Temperature)
if _, ok := temps[sensor.SensorKey]; ok {
temps[sensor.SensorKey] = int(sensor.Temperature + 0.5)
}
}
return nil
}
func devs() []string {
sensors, err := psHost.SensorsTemperatures()
if err != nil {
return []string{}
}
rv := make([]string, 0, len(sensors))
for _, sensor := range sensors {
if sensor.Temperature != 0 {
rv = append(rv, sensor.SensorKey)
}
}
return rv
}

View File

@ -177,7 +177,7 @@ func makeWidget(c gotop.Config, widRule widgetRule) interface{} {
assignColors(b.Data, c.Colorscheme.BattLines, b.LineColors)
w = b
case "temp":
t := widgets.NewTempWidget(c.TempScale)
t := widgets.NewTempWidget(c.TempScale, c.Temps)
t.TempLowColor = ui.Color(c.Colorscheme.TempLow)
t.TempHighColor = ui.Color(c.Colorscheme.TempHigh)
w = t

View File

@ -32,6 +32,8 @@ type DiskWidget struct {
metric map[string]prometheus.Gauge
}
// TODO: Add filtering
// TODO: Abstract out device from widget
func NewDiskWidget() *DiskWidget {
self := &DiskWidget{
Table: ui.NewTable(),

View File

@ -32,7 +32,7 @@ type TempWidget struct {
}
// TODO: state:deferred 156 Added temperatures for NVidia GPUs (azak-azkaran/master). Crashes on non-nvidia machines.
func NewTempWidget(tempScale TempScale) *TempWidget {
func NewTempWidget(tempScale TempScale, filter []string) *TempWidget {
self := &TempWidget{
Block: ui.NewBlock(),
updateInterval: time.Second * 5,
@ -41,6 +41,15 @@ func NewTempWidget(tempScale TempScale) *TempWidget {
TempScale: tempScale,
}
self.Title = " Temperatures "
if len(filter) > 0 {
for _, t := range filter {
self.Data[t] = 0
}
} else {
for _, t := range devices.Devices(devices.Temperatures) {
self.Data[t] = 0
}
}
if tempScale == Fahrenheit {
self.TempThreshold = utils.CelsiusToFahrenheit(self.TempThreshold)