diff --git a/.gitignore b/.gitignore
index 5561ef5..370412b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,4 +106,6 @@ dist
# Stats
stats.html
-.old/
\ No newline at end of file
+.old/
+
+run-local.ts
diff --git a/README.md b/README.md
index be2805f..25e5a09 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@

# ThinkPad Thermal GNOME Shell Extension
-Extension that displays thermal and fan status on ThinkPads
+Extension that displays device info, thermals and fan status on ThinkPads
## Requirements
- [thinkpad-acpi](https://www.kernel.org/doc/Documentation/laptops/thinkpad-acpi.txt), check contents of `/proc/acpi/ibm/thermal` and `/proc/acpi/ibm/fan`
@@ -31,6 +31,41 @@ You can clone this repo and build the extension manually with `yarn build:packag
- Run `./run-log.sh` and check the generated logs `hw.log, err.log`
- Clone the repo, install dependencies, `yarn dev` and `./run-nested-shell.sh`
+### Unsupported firmwares
+Generated `hw.log` contains a message like the one below, see [quirks mode](#Quirks-mode)
+```
+$ hw.log
+...
+thinkpad_acpi: ThinkPad firmware release *fw_str* doesn't match the known patterns
+thinkpad_acpi: please report this to ibm-acpi-devel@lists.sourceforge.net
+thinkpad_acpi: ThinkPad ACPI Extras v0.26
+...
+```
+
+
+### Quirks mode
+Can be enabled via extension settings.
+- Disables fan control via the extension
+- Derived readings for CPU/GPU via lm-sensors if those are not available via [thinkpad_acpi driver 0.26](https://github.com/torvalds/linux/blob/master/drivers/platform/x86/thinkpad_acpi.c) ie. `/proc/acpi/ibm/thermal` is missing and/or firmware is not supported.
+
+**Readings pipeline:**
+- CPU: thinkpad-isa-\*.CPU > k10-temp-\*.Tctl > avg coretemp-isa-\* > 0
+- GPU: thinkpad-isa-\*.GPU > amdgpu-\*.edge > -128
+- FAN: avg thinkpad-isa-\*.fanX > 0
+
+### Level 0
+This extension does not provide this option out of the box. Thinkpad-acpi docs regarding [fan levels](https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/laptops/thinkpad-acpi.rst#fan-levels) contain a warning about setting the level to 0 ie. turning the fan off.
+
+```
+WARNING WARNING WARNING: do not leave the fan disabled unless you are
+monitoring all of the temperature sensor readings and you are ready
+to enable it if necessary to avoid overheating.
+```
+
+If you really want to have the option available, you can modify the `../extension.js` and change `DISABLED_LEVELS = [0, 'disengaged']` to `DISABLED_LEVELS = ['disengaged']`.
+
+**Note:** Level `disengaged` is an alias for `full-speed`
+
## Todo
- [x] thinkpad-acpi
@@ -39,4 +74,5 @@ You can clone this repo and build the extension manually with `yarn build:packag
- [x] lsblk
- [x] Fan speed control
- [x] Settings dialog
+ - [x] Quirks mode
- [ ] Multilang
diff --git a/resources/prefs.xml b/resources/prefs.xml
index 10b9e92..1628361 100644
--- a/resources/prefs.xml
+++ b/resources/prefs.xml
@@ -35,6 +35,21 @@
+
+
+
+
+
diff --git a/resources/schemas/gschemas.compiled b/resources/schemas/gschemas.compiled
index e7bf2f4..d639a13 100644
Binary files a/resources/schemas/gschemas.compiled and b/resources/schemas/gschemas.compiled differ
diff --git a/resources/schemas/org.gnome.shell.extensions.thinkpadthermal.gschema.xml b/resources/schemas/org.gnome.shell.extensions.thinkpadthermal.gschema.xml
index 0357e13..d8bb4f1 100644
--- a/resources/schemas/org.gnome.shell.extensions.thinkpadthermal.gschema.xml
+++ b/resources/schemas/org.gnome.shell.extensions.thinkpadthermal.gschema.xml
@@ -9,6 +9,12 @@
Interval in seconds between temperature checks
+
+ false
+ Enable quirks mode
+ Use when thinkpad_acpi is not providing the readings via the driver ie. unsupported firmware release. Disables fan control, derives cpu/gpu values if those are provided by lmsensors via coretemp, k10temp, amdgpu modules/drivers.
+
+
diff --git a/run-log.sh b/run-log.sh
index b611db1..e0e49af 100644
--- a/run-log.sh
+++ b/run-log.sh
@@ -12,6 +12,11 @@ dmi=(
{
echo -e "\n========== $(date) =========="
+ # driver
+ journalctl -k -b | grep thinkpad_
+ echo "=========="
+ ls /proc/acpi/ibm -l
+ # info
echo -e "\n=== dmi ==="
for file in "${dmi[@]}"
do
diff --git a/src/Console.ts b/src/Console.ts
index e2cdab4..700642d 100644
--- a/src/Console.ts
+++ b/src/Console.ts
@@ -16,13 +16,39 @@ export default class ConsoleUtil extends GObject.Object {
private _command: string[]
- constructor(...args: string[]) {
+ protected config = {} as ThinkPadThermal.Config
+ protected setConfig(next = {}) {
+ this.config = {
+ ...this.config,
+ ...next,
+ }
+ }
+ protected data: unknown
+ protected setData(next = {}) {
+ this.data = {
+ ...(this.data || {}),
+ ...next,
+ }
+ }
+
+ constructor(
+ cmd: string,
+ ...args: (string | ThinkPadThermal.Config | undefined)[]
+ ) {
super()
- assert(!!args[0], 'Util not defined')
- assert(!!GLib.find_program_in_path(args[0]), `Util ${args[0]} not found`)
+ assert(!!cmd, 'Util not defined')
+ assert(!!GLib.find_program_in_path(cmd), `Util ${cmd} not found`)
- this._command = ConsoleUtil.args(args.join(' '))
+ let rest = args
+ const last = rest[rest.length - 1]
+
+ if (typeof last === 'object') {
+ this.setConfig(last)
+ rest = args.slice(0, args.length - 1)
+ }
+
+ this._command = ConsoleUtil.args([cmd, ...rest].join(' '))
if (
this.available &&
@@ -65,6 +91,10 @@ export default class ConsoleUtil extends GObject.Object {
)
} catch (e) {
logError(e)
+ if (/no such file or directory/i.test(e as string)) {
+ console.log('Disabling utility', this._command)
+ this._command = []
+ }
}
}
@@ -94,4 +124,9 @@ export default class ConsoleUtil extends GObject.Object {
static revs(n: number) {
return `${n} RPM`
}
+ static average(values: number[]) {
+ return values.length
+ ? Math.ceil(values.reduce((acc, curr) => acc + curr, 0) / values.length)
+ : 0
+ }
}
diff --git a/src/Dmi.ts b/src/Dmi.ts
index 42e010a..c1af38a 100644
--- a/src/Dmi.ts
+++ b/src/Dmi.ts
@@ -31,22 +31,22 @@ export default class DmiUtil extends ConsoleUtil {
'sys_vendor',
] as const
- private data: {
- [K in (typeof DmiUtil.TAGS)[number]]: string
- }
+ protected override data = {} as ThinkPadThermal.DmiData
private parse(str: string) {
- const values = str.split('\n')
+ const values = str.trim().split('\n')
- this.data = DmiUtil.TAGS.reduce((acc, curr, i) => {
- acc[curr] = (values[i] ?? '')
- .replace(/\(\s+/g, '(')
- .replace(/\s+\)/g, ')')
- .trim()
- return acc
- }, {}) as typeof this.data
+ this.setData(
+ DmiUtil.TAGS.reduce((acc, curr, i) => {
+ acc[curr] = (values[i] ?? '')
+ .replace(/\(\s+/g, '(')
+ .replace(/\s+\)/g, ')')
+ .trim()
+ return acc
+ }, {}) as typeof this.data
+ )
- this.emit('updated', this.dmi)
+ this.emit('updated', { dmi: this.dmi })
}
get dmi() {
diff --git a/src/IbmAcpi.ts b/src/IbmAcpi.ts
index 590d871..fec6046 100644
--- a/src/IbmAcpi.ts
+++ b/src/IbmAcpi.ts
@@ -1,52 +1,14 @@
import GObject from 'gi://GObject'
import ConsoleUtil from './Console.js'
-import microdiff, { type DifferenceChange } from './vendor/microdiff.js'
export default class IbmAcpiUtil extends ConsoleUtil {
static {
GObject.registerClass(
{
- Properties: {
- cpu: GObject.ParamSpec.string(
- 'cpu',
- 'CPU temperature',
- 'Current CPU temperature',
- GObject.ParamFlags.READABLE,
- '...'
- ),
- gpu: GObject.ParamSpec.string(
- 'gpu',
- 'GPU temperature',
- 'Current GPU temperature',
- GObject.ParamFlags.READABLE,
- '...'
- ),
- speed: GObject.ParamSpec.string(
- 'speed',
- 'Fan speed',
- 'Current fan speed',
- GObject.ParamFlags.READABLE,
- '...'
- ),
- status: GObject.ParamSpec.string(
- 'status',
- 'Fan status',
- 'Current fan status',
- GObject.ParamFlags.READABLE,
- '...'
- ),
- level: GObject.ParamSpec.string(
- 'level',
- 'Fan level',
- 'Current fan level',
- GObject.ParamFlags.READABLE,
- '...'
- ),
- },
Signals: {
updated: {
- param_types: [GObject.TYPE_JSOBJECT, GObject.TYPE_JSOBJECT],
+ param_types: [GObject.TYPE_JSOBJECT],
},
},
},
@@ -54,7 +16,23 @@ export default class IbmAcpiUtil extends ConsoleUtil {
)
}
- private data: ThinkPadThermal.IbmAcpiData = {
+ private static NOTIFY = [
+ 'cpu',
+ 'gpu',
+ 'speed',
+ 'level',
+ 'status',
+ 'levels',
+ 'hasDedicatedGpu',
+ 'isControllable',
+ ]
+ private static CHECKS = [-128, 0]
+ private static DISABLED_LEVELS: (string | number)[] = [0, 'disengaged']
+
+ public static isValidSensor = (v: number): boolean =>
+ IbmAcpiUtil.CHECKS.every((check) => check !== v)
+
+ protected override data: ThinkPadThermal.IbmAcpiData = {
cpu: 0,
gpu: 0,
status: 'disabled',
@@ -62,12 +40,10 @@ export default class IbmAcpiUtil extends ConsoleUtil {
level: 'auto',
levels: [],
}
- private prev: ThinkPadThermal.IbmAcpiData | object = {}
- private config: ThinkPadThermal.Config
+ private prev = {} as ThinkPadThermal.IbmAcpiData
- constructor(config: ThinkPadThermal.Config) {
- super('cat', '/proc/acpi/ibm/thermal', '/proc/acpi/ibm/fan')
- this.update(config)
+ constructor(config?: ThinkPadThermal.Config) {
+ super('cat', '/proc/acpi/ibm/thermal', '/proc/acpi/ibm/fan', config)
}
// temperatures: 43 50 0 0 0 0 0 0
@@ -129,43 +105,22 @@ export default class IbmAcpiUtil extends ConsoleUtil {
}
async update(config?: object) {
- if (config) {
- this.config = {
- ...(this.config || {}),
- ...config,
- }
- }
-
- if (config && Object.keys(this.prev).length > 0) {
- for (const k of IbmAcpiUtil.NOTIFY) this.notify(k)
- const diffs = IbmAcpiUtil.NOTIFY.map(
- (k) =>
- ({
- type: 'CHANGE',
- path: [k],
- value: this[k],
- oldValue: '*',
- }) as DifferenceChange
- )
- this.emit('updated', IbmAcpiUtil.NOTIFY, diffs)
- return
- }
-
- try {
- this.data = await super.execute(this.parse.bind(this))
-
- const diff = microdiff(this.prev, this.data)
- this.prev = this.data
-
- if (diff.length === 0) return
+ if (!this.available) return
- const keys = diff
- .flatMap(({ path }) => path as string[])
- .filter(IbmAcpiUtil.isNotifiable)
+ if (config) this.setConfig(config)
- for (const k of keys) this.notify(k as string)
+ this.prev = this.data
- this.emit('updated', keys, diff)
+ try {
+ this.setData(await super.execute(this.parse.bind(this)))
+
+ this.emit(
+ 'updated',
+ IbmAcpiUtil.NOTIFY.reduce((acc, key) => {
+ acc[key] = this[key]
+ return acc
+ }, {})
+ )
} catch (e) {
logError(e as Error)
}
@@ -181,16 +136,6 @@ export default class IbmAcpiUtil extends ConsoleUtil {
)
}
- private static NOTIFY = ['cpu', 'gpu', 'speed', 'level', 'status']
- private static CHECKS = [-128, 0]
- private static DISABLED_LEVELS = [0, 'disengaged']
-
- private static isNotifiable = (key: string): boolean =>
- IbmAcpiUtil.NOTIFY.includes(key)
-
- private static isValidSensor = (v: number): boolean =>
- IbmAcpiUtil.CHECKS.every((check) => check !== v)
-
get cpu() {
return ConsoleUtil.temperature(
this.data.cpu,
diff --git a/src/Lsblk.ts b/src/Lsblk.ts
index edb6131..4810bf1 100644
--- a/src/Lsblk.ts
+++ b/src/Lsblk.ts
@@ -6,34 +6,31 @@ class LsnvmeUtil extends ConsoleUtil {
GObject.registerClass(LsnvmeUtil)
}
- private data: {
- [key: string]: string
- } = {}
+ protected override data: ThinkPadThermal.ValueReadings = {}
constructor() {
super('ls', '-l', '/dev/disk/by-path')
}
private static IS = {
- NVME: /nvme/i,
- PART: /part/i,
+ NVME: /^(?=.*nvme)(?!.*-part).*/i,
}
private parse(str: string) {
- this.data = str
- .split('\n')
- .filter((l) => !LsnvmeUtil.IS.PART.test(l))
+ const ids = str
+ .split('\n') //
.filter((l) => LsnvmeUtil.IS.NVME.test(l))
- .map((l) => l.slice(l.indexOf('pci-')))
- .map((l) => l.replace(/(\.\.\/)/gim, '').split('->') as [string, string])
- .map(([a, b]): [string, string] => [
- b.trim(),
- ['nvme', 'pci', a.slice(9, 14).replace(/[:.]/gim, '')].join('-'),
- ])
- .reduce((acc, [path, name]) => {
- acc[path] = name
- return acc
- }, {})
+ .map((l) => l.slice(l.indexOf('pci-')).split('->') as [string, string])
+ .map(
+ ([a, b]) =>
+ [
+ b.slice(b.indexOf('nvme')).trim(),
+ ['nvme-pci', a.slice(9, 14).replace(/[:.]/gim, '')].join('-'),
+ ] as [string, string]
+ )
+ for (const [path, id] of ids) {
+ this.data[path] = id
+ }
}
update() {
@@ -52,9 +49,7 @@ export default class LsblkUtil extends ConsoleUtil {
private _lsnvme = new LsnvmeUtil()
- private data: {
- [key: string]: string
- } = {}
+ protected override data: ThinkPadThermal.ValueReadings = {}
constructor() {
super('lsblk', '-o', 'HCTL,MODEL,NAME,TRAN', '-dnJ')
@@ -62,8 +57,8 @@ export default class LsblkUtil extends ConsoleUtil {
private parse(str: string) {
const { blockdevices } = JSON.parse(str)
- this.data = blockdevices.reduce(
- (acc: object, { hctl, model, name, tran }) => {
+ this.setData(
+ blockdevices.reduce((acc: object, { hctl, model, name, tran }) => {
if (hctl) {
const key = [
'drivetemp',
@@ -81,8 +76,7 @@ export default class LsblkUtil extends ConsoleUtil {
}
return acc
- },
- {}
+ }, {})
)
}
@@ -92,8 +86,8 @@ export default class LsblkUtil extends ConsoleUtil {
name(key: string): string {
if (!this.data[key]) {
- this.update()
this._lsnvme.update()
+ this.update()
}
return this.data[key] ?? key
}
diff --git a/src/Lscpu.ts b/src/Lscpu.ts
index 63efc18..2751061 100644
--- a/src/Lscpu.ts
+++ b/src/Lscpu.ts
@@ -5,14 +5,14 @@ export default class LscpuUtil extends ConsoleUtil {
static {
GObject.registerClass(LscpuUtil)
}
- private _data = {}
+ protected override data = {}
constructor() {
super('lscpu', '-e=MODELNAME,SOCKET', '-J')
}
private extractModel(modelName: string): string {
- if (modelName.toLowerCase().includes('intel')) {
+ if (/intel/i.test(modelName)) {
return (
// @ts-ignore
modelName
@@ -24,13 +24,15 @@ export default class LscpuUtil extends ConsoleUtil {
)
}
- if (modelName.toLowerCase().includes('amd')) {
+ if (/amd/i.test(modelName)) {
return (
// @ts-ignore
modelName
.split('with')[0]
.split(/\s+\d+-Core/)[0]
.split(/\s+[A-Za-z]+-Core/)[0]
+ .split('w/')[0]
+ .replace('AMD', 'AMD®')
.trim() || 'AMD CPU'
)
}
@@ -40,16 +42,18 @@ export default class LscpuUtil extends ConsoleUtil {
private parse(str: string) {
const { cpus } = JSON.parse(str) as ThinkPadThermal.LscpuEntries
- this._data = Object.values(cpus).reduce>(
- (acc, curr) => {
- let key = curr.socket.toString().padStart(4, '0')
- key = `coretemp-isa-${key}`
+ this.setData(
+ Object.values(cpus).reduce>((acc, curr) => {
+ const key = /intel/i.test(curr.modelname)
+ ? `coretemp-isa-${curr.socket.toString().padStart(4, '0')}`
+ : 'k10temp' // 'k10temp-pci-00c3'
+
acc[key] = this.extractModel(curr.modelname)
return acc
- },
- {}
+ }, {})
)
- return this._data
+
+ return this.data
}
update() {
@@ -57,6 +61,10 @@ export default class LscpuUtil extends ConsoleUtil {
}
name(key: string): string {
- return this._data[key] ?? key
+ return (
+ this.data[key] ?? //
+ this.data[key.slice(0, key.indexOf('-'))] ??
+ key
+ )
}
}
diff --git a/src/Sensors.ts b/src/Sensors.ts
index 7a50c33..34a0048 100644
--- a/src/Sensors.ts
+++ b/src/Sensors.ts
@@ -3,6 +3,7 @@ import GObject from 'gi://GObject'
import ConsoleUtil from './Console.js'
import LsblkUtil from './Lsblk.js'
import LscpuUtil from './Lscpu.js'
+import IbmAcpiUtil from './IbmAcpi.js'
export default class SensorsUtil extends ConsoleUtil {
static {
@@ -17,11 +18,12 @@ export default class SensorsUtil extends ConsoleUtil {
SensorsUtil
)
}
- private static NOTIFY = ['cpu', 'hdd', 'fan', 'other']
+ private static NOTIFY = ['cpus', 'hdds', 'fans', 'other']
private static IS = {
INPUT: /_input$/,
FANS: /^fan/i,
- CPU: /^coretemp/i,
+ CPU: /^(coretemp|k10temp)/i,
+ GPU: /^amdgpu/i,
DRIVETEMP: /^drivetemp/i,
NVME: /^nvme/i,
TPISA: /^thinkpad-isa/i,
@@ -31,16 +33,15 @@ export default class SensorsUtil extends ConsoleUtil {
private _lscpu: LscpuUtil
private _lsblk: LsblkUtil
- private data: object = {}
- private config: ThinkPadThermal.Config
+ protected override data: object = {}
+
+ protected prev: { cpu: number; gpu?: number }
constructor(config?: ThinkPadThermal.Config) {
- super('sensors', '-A', '-j')
+ super('sensors', '-A', '-j', config)
this._lscpu = new LscpuUtil()
this._lsblk = new LsblkUtil()
-
- this.update(config)
}
private parse(str: string | object) {
@@ -49,7 +50,7 @@ export default class SensorsUtil extends ConsoleUtil {
if (keys.length === 0) return obj
- if (keys.length === 1)
+ if (keys.length === 1 && keys[0] !== 'Tctl')
return this.parse(Object.values(obj)[0] as string | object)
const input = keys.find((k) => SensorsUtil.IS.INPUT.test(k))
@@ -62,40 +63,112 @@ export default class SensorsUtil extends ConsoleUtil {
}
async update(config?: ThinkPadThermal.Config) {
- this.config = {
- ...this.config,
- ...config,
- }
+ if (!this.available) return
- try {
- this.data = await super.execute(this.parse.bind(this))
- const obj = SensorsUtil.NOTIFY.reduce((acc, key) => {
- acc[key] = this[key]
- return acc
- }, {})
+ this.setConfig(config)
- this.emit('updated', obj)
+ try {
+ this.setData(await super.execute(this.parse.bind(this)))
+
+ this.emit(
+ 'updated',
+ SensorsUtil.NOTIFY.reduce((acc, key) => {
+ acc[key] = this[key]
+ return acc
+ }, {})
+ )
} catch (error) {
logError(error)
}
}
- private select(
- f: FilterFn,
- r: ReduceFn,
- key?: string
- ) {
- return Object.keys(key ? this.data[key] : this.data)
- .filter(f) //
- .reduce(r, {})
+ isGpuDetected(): this is { prev: { gpu: number } } {
+ return (
+ typeof this.prev?.gpu === 'number' &&
+ IbmAcpiUtil.isValidSensor(this.prev.gpu)
+ )
+ }
+
+ private select =
+ (
+ f: RegExp | RegExp[] | FilterFn,
+ r: ReduceFn,
+ i: I = {} as I
+ ) =>
+ (key?: string | RegExp): I => {
+ const source =
+ key instanceof RegExp
+ ? Object.keys(this.data).find((k) => key.test(k))
+ : key
+
+ const data = source ? this.data[source] : this.data
+ const keys = Object.keys(data) as T[]
+
+ let fn: FilterFn
+ if (typeof f === 'function') {
+ fn = f
+ } else {
+ const test = Array.isArray(f) ? f : [f]
+ fn = (k) => test.some((exp) => exp.test(String(k)))
+ }
+
+ return keys
+ .filter(fn) //
+ .reduce((acc, k) => r(acc, k, data[k]), i)
+ }
+
+ get quirks() {
+ const cpu = this.select(
+ [SensorsUtil.IS.TPISA, SensorsUtil.IS.CPU],
+ (acc, _, data) =>
+ Number.parseInt(
+ (data.CPU || data.Tctl) ?? ConsoleUtil.average(Object.values(data))
+ ) || acc,
+ 0
+ )()
+
+ let gpu = this.select(
+ [SensorsUtil.IS.TPISA, SensorsUtil.IS.GPU],
+ (acc, _, data) => Number.parseInt(data.GPU ?? data.edge) || acc,
+ -128
+ )()
+
+ const speed = this.select(
+ SensorsUtil.IS.FANS,
+ (acc, _, value) => acc.concat(value),
+ []
+ )(SensorsUtil.IS.TPISA)
+
+ const hasDedicatedGpu = this.isGpuDetected()
+ // GPU Fallback, flaky sensor, {} -> -128
+ if (gpu <= 0 && hasDedicatedGpu) {
+ gpu = this.prev.gpu
+ }
+
+ this.prev = { cpu, gpu }
+
+ return {
+ cpu: ConsoleUtil.temperature(
+ Math.round(cpu),
+ this.config.temperatureUnit
+ ),
+ gpu: ConsoleUtil.temperature(
+ Math.round(gpu),
+ this.config.temperatureUnit
+ ),
+ speed: ConsoleUtil.revs(ConsoleUtil.average(speed)),
+ hasDedicatedGpu,
+ status: 'disabled',
+ isControllable: false,
+ }
}
- get cpu() {
+ get cpus() {
return this.select(
- (k) => SensorsUtil.IS.CPU.test(k),
- (acc, k) => {
- const name = this._lscpu.name(k)
- const value = { ...this.data[k] }
+ SensorsUtil.IS.CPU, //
+ (acc, k, data) => {
+ const name = this._lscpu.name(k) ?? k
+ const value = { ...(data as object) }
for (const key of Object.keys(value)) {
value[key] = ConsoleUtil.temperature(
@@ -108,15 +181,15 @@ export default class SensorsUtil extends ConsoleUtil {
return acc
}
- )
+ )()
}
- get hdd() {
+ get hdds() {
return this.select(
- (k) => SensorsUtil.IS.DRIVETEMP.test(k) || SensorsUtil.IS.NVME.test(k),
- (acc, k) => {
+ [SensorsUtil.IS.DRIVETEMP, SensorsUtil.IS.NVME],
+ (acc, k, data) => {
const name = this._lsblk.name(k)
- let value = this.data[k]
+ let value = data
if (typeof value === 'object') {
value = Math.max(...(Object.values(value) as number[]))
@@ -126,45 +199,36 @@ export default class SensorsUtil extends ConsoleUtil {
return acc
}
- )
+ )()
}
- get bat() {
+ get bats() {
return this.select(
- (k) => SensorsUtil.IS.BATTERIES.test(k),
- (acc, k) => {
- acc[k] = this.data[k]
+ SensorsUtil.IS.BATTERIES, //
+ (acc, k, value) => {
+ acc[k] = value
return acc
}
- )
+ )()
}
- get fan() {
- const key = Object.keys(this.data).find((k) =>
- SensorsUtil.IS.TPISA.test(k)
- ) as string
-
+ get fans() {
return this.select(
- (k) => SensorsUtil.IS.FANS.test(k),
- (acc, k) => {
- acc[k] = ConsoleUtil.revs(this.data[key][k])
+ SensorsUtil.IS.FANS, //
+ (acc, k, value) => {
+ acc[k] = ConsoleUtil.revs(value)
return acc
- },
- key
- )
+ }
+ )(SensorsUtil.IS.TPISA)
}
get other() {
return this.select(
- (k) =>
- Object.keys(SensorsUtil.IS).every(
- (check) => !SensorsUtil.IS[check].test(k)
- ),
- (acc, k) => {
- const value = this.data[k]
+ (k) => !Object.values(SensorsUtil.IS).some((exp) => exp.test(k)),
+ (acc, k, value) => {
acc[k] = ConsoleUtil.temperature(value, this.config.temperatureUnit)
return acc
}
- )
+ )()
}
}
diff --git a/src/ThermalButton.ts b/src/ThermalButton.ts
index 8fd91bc..8199fec 100644
--- a/src/ThermalButton.ts
+++ b/src/ThermalButton.ts
@@ -1,5 +1,5 @@
import type Gio from 'gi://Gio'
-import type IbmAcpiUtil from './IbmAcpi.js'
+import type ThermalData from './ThermalData.js'
import GObject from 'gi://GObject'
import St from 'gi://St'
@@ -10,7 +10,7 @@ class ThermalButton extends PanelMenu.Button {
static {
GObject.registerClass(ThermalButton)
}
- private _data: IbmAcpiUtil
+ private _data: ThermalData
private layout: St.BoxLayout = new St.BoxLayout({
style_class: 'layout',
})
@@ -18,7 +18,7 @@ class ThermalButton extends PanelMenu.Button {
constructor(
align: number,
name: string,
- data: IbmAcpiUtil,
+ data: ThermalData,
settings: Gio.Settings
) {
super(align, name)
@@ -29,10 +29,10 @@ class ThermalButton extends PanelMenu.Button {
this.addIndicator('cpu')()
- this.addIndicator('gpu')((el, next) => {
- if (next.hasDedicatedGpu) {
+ this.addIndicator('gpu')((el, { hasDedicatedGpu, gpu }) => {
+ if (hasDedicatedGpu) {
el.show()
- el.update(next.gpu)
+ el.update(gpu)
} else {
el.hide()
}
@@ -64,7 +64,7 @@ class ThermalButton extends PanelMenu.Button {
key: K,
icon?: string
) {
- return (handler?: (child: ButtonSection, next: IbmAcpiUtil) => void) => {
+ return (handler?: (child: ButtonSection, next: ThermalData) => void) => {
const text = this._data[key]
const child = new ButtonSection(key, text, icon)
diff --git a/src/ThermalData.ts b/src/ThermalData.ts
index 963bd31..622f6aa 100644
--- a/src/ThermalData.ts
+++ b/src/ThermalData.ts
@@ -1,36 +1,116 @@
import type Gio from 'gi://Gio'
+import GObject from 'gi://GObject'
import GLib from 'gi://GLib'
import SensorsUtil from './Sensors.js'
import IbmAcpiUtil from './IbmAcpi.js'
import DmiUtil from './Dmi.js'
-class ThermalData {
+import microdiff from './vendor/microdiff.js'
+
+class ThermalData extends GObject.Object {
+ static {
+ GObject.registerClass(
+ {
+ Properties: {
+ cpu: GObject.ParamSpec.string(
+ 'cpu',
+ 'CPU temperature',
+ 'Current CPU temperature',
+ GObject.ParamFlags.READABLE,
+ '...'
+ ),
+ gpu: GObject.ParamSpec.string(
+ 'gpu',
+ 'GPU temperature',
+ 'Current GPU temperature',
+ GObject.ParamFlags.READABLE,
+ '...'
+ ),
+ speed: GObject.ParamSpec.string(
+ 'speed',
+ 'Fan speed',
+ 'Current fan speed',
+ GObject.ParamFlags.READABLE,
+ '...'
+ ),
+ status: GObject.ParamSpec.string(
+ 'status',
+ 'Fan status',
+ 'Current fan status',
+ GObject.ParamFlags.READABLE,
+ '...'
+ ),
+ level: GObject.ParamSpec.string(
+ 'level',
+ 'Fan level',
+ 'Current fan level',
+ GObject.ParamFlags.READABLE,
+ '...'
+ ),
+ levels: GObject.ParamSpec.jsobject(
+ 'levels',
+ 'Fan levels',
+ 'Supported fan levels',
+ GObject.ParamFlags.READABLE
+ ),
+ },
+ Signals: {
+ updated: {
+ param_types: [
+ GObject.TYPE_JSOBJECT,
+ GObject.TYPE_JSOBJECT,
+ GObject.TYPE_JSOBJECT,
+ ],
+ },
+ },
+ },
+ ThermalData
+ )
+ }
+
private _interval: number | null
private config: ThinkPadThermal.Config
+ private data: Partial = {}
+ private prev: Partial = {}
+
acpi: IbmAcpiUtil
dmi: DmiUtil
sensors: SensorsUtil
constructor(settings: Gio.Settings) {
+ super()
+
this.config = {
checkInterval: settings.get_int('check-interval'),
- temperatureUnit: settings.get_string('temperature-unit'),
+ temperatureUnit: settings.get_string(
+ 'temperature-unit'
+ ) as ThinkPadThermal.Unit,
+ quirksMode: settings.get_boolean('quirks-mode'),
}
this.dmi = new DmiUtil()
this.acpi = new IbmAcpiUtil(this.config)
this.sensors = new SensorsUtil(this.config)
+ this.acpi.connect('updated', this.sync)
+ this.dmi.connect('updated', this.sync)
+ this.sensors.connect('updated', this.sync)
+
settings.connect('changed::check-interval', () => {
this.config.checkInterval = settings.get_int('check-interval')
this.startInterval()
})
settings.connect('changed::temperature-unit', () => {
- this.config.temperatureUnit = settings.get_string('temperature-unit')
- this.acpi.update(this.config)
- this.sensors.update(this.config)
+ this.config.temperatureUnit = settings.get_string(
+ 'temperature-unit'
+ ) as ThinkPadThermal.Unit
+ this.startInterval()
+ })
+ settings.connect('changed::quirks-mode', () => {
+ this.config.quirksMode = settings.get_boolean('quirks-mode')
+ this.startInterval()
})
this.startInterval()
@@ -39,18 +119,50 @@ class ThermalData {
private startInterval() {
if (this._interval) GLib.source_remove(this._interval)
+ const fetch = () => {
+ this.acpi.update(this.config)
+ this.sensors.update(this.config)
+ //
+ return GLib.SOURCE_CONTINUE
+ }
+
+ fetch()
+
this._interval = GLib.timeout_add_seconds(
GLib.PRIORITY_DEFAULT,
this.config.checkInterval,
- this.fetchData.bind(this)
+ fetch.bind(this)
)
}
- private fetchData() {
- this.acpi.update()
- this.sensors.update()
- //
- return GLib.SOURCE_CONTINUE
+ private static NOTIFY = ['cpu', 'gpu', 'speed', 'level', 'status']
+ private static isNotifiable = (s: Set) =>
+ ThermalData.NOTIFY.filter((k) => s.has(k))
+
+ private setData(next: object) {
+ this.data = {
+ ...this.data,
+ ...next,
+ }
+ }
+
+ private sync = (_: object, updated: object) => {
+ this.setData(updated)
+
+ if (this.config.quirksMode) this.setData(this.sensors.quirks)
+
+ const diff = microdiff(this.prev, this.data, { cyclesFix: false })
+
+ if (diff.length === 0) return
+
+ this.prev = this.data
+
+ const keys = new Set(diff.flatMap(({ path }) => path as string[]))
+ const notify = ThermalData.isNotifiable(keys)
+
+ for (const k of notify) this.notify(k as string)
+
+ this.emit('updated', this.data, diff, keys)
}
destroy() {
@@ -59,6 +171,47 @@ class ThermalData {
this._interval = null
}
}
+
+ get cpu() {
+ return this.data.cpu ?? '...'
+ }
+ get gpu() {
+ return this.data.gpu ?? '...'
+ }
+ get speed() {
+ return this.data.speed ?? '...'
+ }
+ get status() {
+ return this.data.status ?? 'initializing'
+ }
+ get level() {
+ return this.data.level ?? 'auto'
+ }
+ set level(next) {
+ this.acpi.setLevel(next)
+ }
+ get levels() {
+ return this.data.levels ?? []
+ }
+ get hasDedicatedGpu() {
+ return this.data.hasDedicatedGpu ?? false
+ }
+ get isControllable() {
+ return this.data.isControllable ?? false
+ }
+
+ get cpus() {
+ return this.data.cpus ?? {}
+ }
+ get hdds() {
+ return this.data.hdds ?? {}
+ }
+ get fans() {
+ return this.data.fans ?? {}
+ }
+ get other() {
+ return this.data.other ?? {}
+ }
}
export default ThermalData
diff --git a/src/ThermalPopup.ts b/src/ThermalPopup.ts
index bb41c26..deb3494 100644
--- a/src/ThermalPopup.ts
+++ b/src/ThermalPopup.ts
@@ -1,7 +1,4 @@
import type ThermalData from './ThermalData.js'
-import type DmiUtil from './Dmi.js'
-import type IbmAcpiUtil from './IbmAcpi.js'
-import type SensorsUtil from './Sensors.js'
import St from 'gi://St'
import { PopupMenu } from 'resource:///org/gnome/shell/ui/popupMenu.js'
@@ -16,30 +13,30 @@ import {
} from './ThermalUI.js'
class Dmi extends PopupSection {
- constructor(data: DmiUtil) {
+ constructor(data: ThermalData) {
super('Device info', data, false)
this.addMenuItem(new Groups('dmi', 'thinkpad'))
}
}
class Sensors extends PopupSection {
- constructor(data: SensorsUtil) {
+ constructor(data: ThermalData) {
super('Sensors', data, false)
- this.addMenuItem(new Groups('cpu'))
- this.addMenuItem(new Group('hdd', 'Disks', data.hdd))
+ this.addMenuItem(new Groups('cpus'))
+ this.addMenuItem(new Group('hdds', 'Disks', data.hdds))
this.addMenuItem(new Group('other', 'Thermal', data.other, 'sensor'))
- this.addMenuItem(new Group('fan', 'Cooling', data.fan))
+ this.addMenuItem(new Group('fans', 'Cooling', data.fans))
}
}
class Acpi extends PopupSection {
- constructor(data: IbmAcpiUtil) {
+ constructor(data: ThermalData) {
super('ACPI', data)
this.addMenuItem(new Item('cpu', 'CPU', data.cpu, 'cpu'))
this.addMenuItem(new Item('gpu', 'GPU', data.gpu, 'gpu'))
- data.connect('notify::gpu', (next: IbmAcpiUtil) => {
+ data.connect('notify::gpu', (next: ThermalData) => {
if (!next.hasDedicatedGpu) {
this.item('gpu')?.hide()
} else {
@@ -50,7 +47,7 @@ class Acpi extends PopupSection {
}
class FanControl extends PopupSection {
- constructor(data: IbmAcpiUtil) {
+ constructor(data: ThermalData) {
super('Fan control', data)
this.addMenuItem(new Item('status', 'Status', data.status))
@@ -62,21 +59,17 @@ class FanControl extends PopupSection {
export default class ThermalPopup extends PopupMenu {
_dd: QuickDropdown | null
- constructor(
- align: number,
- actor: St.Widget,
- { acpi, dmi, sensors }: ThermalData
- ) {
+ constructor(align: number, actor: St.Widget, data: ThermalData) {
super(actor, align, St.Side.TOP)
this.actor.add_style_class_name('tpt-popup')
- this.addMenuItem(new Dmi(dmi))
- this.addMenuItem(new Sensors(sensors))
- this.addMenuItem(new Acpi(acpi))
- this.addMenuItem(new FanControl(acpi))
+ this.addMenuItem(new Dmi(data))
+ this.addMenuItem(new Sensors(data))
+ this.addMenuItem(new Acpi(data))
+ this.addMenuItem(new FanControl(data))
- acpi.connect('notify::status', (data: IbmAcpiUtil) => {
+ data.connect('notify::status', (data: ThermalData) => {
//
if (!data.isControllable) {
this._dd?.destroy()
@@ -91,14 +84,16 @@ export default class ThermalPopup extends PopupMenu {
'fan',
data.levels,
data.level,
- (next) => data.setLevel(next)
+ (next) => {
+ data.level = next
+ }
)
Main.panel.statusArea.quickSettings //
.addExternalIndicator(this._dd)
})
- acpi.connect('notify::level', (data: IbmAcpiUtil) => {
+ data.connect('notify::level', (data: ThermalData) => {
this._dd?.status(
data.level,
'ThinkPad Fan Control',
diff --git a/src/ThermalUI.ts b/src/ThermalUI.ts
index c19e0e3..a63808d 100644
--- a/src/ThermalUI.ts
+++ b/src/ThermalUI.ts
@@ -16,8 +16,8 @@ import {
SystemIndicator,
} from 'resource:///org/gnome/shell/ui/quickSettings.js'
-import microdiff from './vendor/microdiff.js'
import { ME } from './extension.js'
+import type ThermalData from './ThermalData.js'
export class ButtonSection extends St.BoxLayout {
static {
@@ -89,9 +89,15 @@ export class Icon extends St.Icon {
static createIcon(filename: string) {
if (!ME) return null
- return new Gio.FileIcon({
- file: ME.dir.resolve_relative_path(`icons/${filename}-symbolic.svg`),
- })
+ let file = ME.dir.resolve_relative_path(`icons/${filename}-symbolic.svg`)
+
+ if (!file.query_exists(null)) {
+ file = ME.dir.resolve_relative_path(
+ `icons/${filename.slice(0, filename.length - 1)}-symbolic.svg`
+ )
+ }
+
+ return new Gio.FileIcon({ file })
}
}
@@ -314,7 +320,7 @@ export class Groups extends PopupMenuSection {
}
private CHANGE(path: (string | number)[], value: string) {
const key = path[0] as string
- this.element(key).update([
+ this.element(key)?.update([
{
type: 'CHANGE',
path: path.slice(1),
@@ -325,7 +331,7 @@ export class Groups extends PopupMenuSection {
}
private REMOVE(path: (string | number)[]) {
const key = path[0] as string
- this.element(key).destroy()
+ this.element(key)?.destroy()
}
update(diffs: ThinkPadThermal.Diffs) {
@@ -338,18 +344,18 @@ export class Groups extends PopupMenuSection {
export class PopupSection extends PopupMenuSection {
name = 'Section'
- constructor(name: string, data: ThinkPadThermal.Util, createTitle = true) {
+ constructor(name: string, data: ThermalData, createTitle = true) {
super()
this.name = name
this.actor.y_expand = false
if (createTitle) this.addMenuItem(new Title(name))
- data.connect('updated', (next) => this.sync(next))
+ data.connect('updated', this.sync.bind(this))
}
private get elements() {
- return (this._getMenuItems() as ThinkPadThermal.PrevElement[]) //
+ return (this._getMenuItems() as ThinkPadThermal.Element[]) //
.filter((e) => e.key)
}
@@ -357,29 +363,46 @@ export class PopupSection extends PopupMenuSection {
return this.elements.find((e) => e.key === key)
}
- private sync(data: object) {
+ private sync(
+ _: object,
+ data: object,
+ diff: ThinkPadThermal.Diffs,
+ keys: Set
+ ) {
for (const el of this.elements) {
- const { key, prev } = el
- const value = data[key]
-
- if (prev === value) continue
-
- if (
- typeof value === 'object' ||
- el instanceof Group ||
- el instanceof Groups
- ) {
- const diffs = microdiff(prev ?? {}, value)
-
+ if (!keys.has(el.key)) continue
+
+ const diffs = diff
+ .filter((d) => d.path.includes(el.key))
+ .reduce((acc, curr) => {
+ if (
+ curr.path.length === 1 &&
+ curr.type === 'CREATE' &&
+ typeof curr.value === 'object'
+ ) {
+ const innerDiffs = Object.keys(curr.value).map((k) => ({
+ type: curr.type,
+ path: [k],
+ value: curr.value[k],
+ })) as ThinkPadThermal.Diffs
+ return acc.concat(innerDiffs)
+ }
+
+ return acc.concat([
+ {
+ ...curr,
+ path: curr.path.filter((k) => k !== el.key),
+ },
+ ])
+ }, [] as ThinkPadThermal.Diffs)
+
+ if (el instanceof Group || el instanceof Groups) {
if (diffs.length === 0) continue
-
- el.prev = value
if ('update' in el) el.update(diffs)
continue
}
- el.prev = value
- el.value = value
+ el.value = data[el.key]
}
}
}
diff --git a/src/extension.ts b/src/extension.ts
index 20679f8..e6203b5 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -23,16 +23,23 @@ export default class ThinkPadThermal extends Extension {
this._indicator = new ThermalButton(
0.5,
'ThinkPad Thermal',
- this._data.acpi,
+ this._data,
this._settings
)
this._indicator.setMenu(new ThermalPopup(0.5, this._indicator, this._data))
- Main.panel.addToStatusArea(this.uuid, this._indicator, this._position, this._area)
+ Main.panel.addToStatusArea(
+ this.uuid,
+ this._indicator,
+ this._position,
+ this._area
+ )
this._settings.connect('changed', (_, change) => {
if (change.startsWith('position-')) this.reposition()
})
+
+ if (this._settings.get_boolean('position-enable')) this.ensurePosition(6)
}
get _position() {
@@ -46,7 +53,8 @@ export default class ThinkPadThermal extends Extension {
: 'right'
}
get _box() {
- return Main.panel.get_child_at_index(['left', 'center', 'right'].indexOf(this._area))
+ const i = ['left', 'center', 'right'].indexOf(this._area)
+ return Main.panel.get_child_at_index(i)
}
private reposition() {
if (!this._settings.get_boolean('position-enable')) return
@@ -58,6 +66,11 @@ export default class ThinkPadThermal extends Extension {
this._box
)
}
+ private ensurePosition(n: number) {
+ if (n === 0) return
+ this.reposition()
+ setTimeout(() => this.ensurePosition(n - 1), 500)
+ }
override disable() {
this._indicator?.destroy()
diff --git a/src/prefs.ts b/src/prefs.ts
index d3d0a8b..7a41f93 100644
--- a/src/prefs.ts
+++ b/src/prefs.ts
@@ -21,6 +21,12 @@ export default class ThinkpadThermalPreferences extends ExtensionPreferences {
'value',
Gio.SettingsBindFlags.DEFAULT
)
+ settings.bind(
+ 'quirks-mode',
+ builder.get_object('field_quirks_mode'),
+ 'active',
+ Gio.SettingsBindFlags.DEFAULT
+ )
settings.bind(
'temperature-unit',
builder.get_object('field_unit'),
diff --git a/src/types.d.ts b/src/types.d.ts
index 4015178..ce93bdb 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -10,7 +10,7 @@ type TupleOf = R['length'] extends N
declare global {
type FilterFn = (value: T, index: number, array: T[]) => boolean
- type ReduceFn = (accumulator: U, value: T) => U
+ type ReduceFn = (accumulator: U, value: T, data) => U
type SizedArray = TupleOf
namespace ThinkPadThermal {
@@ -19,20 +19,45 @@ declare global {
type Unit = 'celsius' | 'fahrenheit'
type Config = {
- temperatureUnit: TemperatureUnit
+ temperatureUnit: Unit
checkInterval: number
fanSpeedUnit?: string
+ quirksMode: boolean
}
- type IbmAcpiData = {
- cpu: number
- gpu: number
+ type ValueReadings = {
+ [k: string]: string
+ }
+
+ type DmiData = {
+ [K in (typeof DmiUtil.TAGS)[number]]: string
+ }
+
+ type IbmAcpiData = {
+ cpu: V
+ gpu: V
status: 'initializing' | 'enabled' | 'disabled'
- speed: number
+ speed: V
level: 'auto' | 'disengaged' | 'full-speed' | string
levels: string[]
}
+ type SensorsData = {
+ cpus: {
+ [name: string]: ValueReadings
+ }
+ hdds: ValueReadings
+ // bats: object
+ fans: ValueReadings
+ other: ValueReadings
+ }
+
+ type ThermalData = IbmAcpiData &
+ SensorsData & {
+ hasDedicatedGpu: boolean
+ isControllable: boolean
+ }
+
type LscpuEntries = {
cpus: {
socket: number