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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,6 @@ dist
# Stats
stats.html

.old/
.old/

run-local.ts
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
![Panel indicator, Dropdown menu, Quick settings fan control](images/screens.png)

# ThinkPad Thermal GNOME Shell Extension
<b>Extension that displays thermal and fan status on ThinkPads</b>
<b>Extension that displays device info, thermals and fan status on ThinkPads</b>

## 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`
Expand Down Expand Up @@ -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
Expand All @@ -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
15 changes: 15 additions & 0 deletions resources/prefs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@
</child>
</object>
</child>

<!-- Quirks Mode Setting -->
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Enable quirks mode</property>
<property name="subtitle" translatable="yes">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.</property>
<property name="activatable-widget">field_quirks_mode</property>
<child>
<object class="GtkSwitch" id="field_quirks_mode">
<property name="valign">center</property>
<property name="active">true</property>
</object>
</child>
</object>
</child>
</object>
</child>

Expand Down
Binary file modified resources/schemas/gschemas.compiled
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
<description>Interval in seconds between temperature checks</description>
</key>

<key name="quirks-mode" type="b">
<default>false</default>
<summary>Enable quirks mode</summary>
<description>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.</description>
</key>

<key name="temperature-unit" type="s">
<choices>
<choice value="celsius"/>
Expand Down
5 changes: 5 additions & 0 deletions run-log.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 39 additions & 4 deletions src/Console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down Expand Up @@ -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 = []
}
}
}

Expand Down Expand Up @@ -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
}
}
24 changes: 12 additions & 12 deletions src/Dmi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
123 changes: 34 additions & 89 deletions src/IbmAcpi.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,49 @@
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],
},
},
},
IbmAcpiUtil
)
}

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',
speed: 0,
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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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,
Expand Down
Loading