MacBookのファンの回転数をCLIから簡単に操作出来るnpmパッケージを実装して公開した

# 概要

重い作業をしているときに MacBook のファン回転数を最大にしたいと思うことが多々あり、今までは [Macs Fan Control](https://crystalidea.com/macs-fan-control) を利用していたが、ファン回転数を操作するためにわざわざアプリを起動し、GUIで操作することが極めて面倒だと感じていた。

そこで、MacBook のファンの回転数をコマンドから変更出来る CLI ツールが無いか調べてみたところ、smcFanControl というツールを見つけた。

[Macのファンをコマンドで操作する。 – p_06c](https://p06c.wordpress.com/2016/10/15/mac%E3%81%AE%E3%83%95%E3%82%A1%E3%83%B3%E3%82%92%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%A7%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B%E3%80%82/)
[hholtmann/smcFanControl | GitHub](https://github.com/hholtmann/smcFanControl)

記事を見れば分かるが、smcFanControl を CLI で利用するのは下記の理由によりめちゃくちゃ面倒くさい。

- ファンを1つずつしか操作出来ない
- ファンモードの変更が2進数
- ファンIDの指定を文字列の途中に差し込む必要がある
- ファンの回転速度を10進数→2進数→左2ビットシフト→16進数にした数字を指定

ここまで来ると人間が使うために作られていないのでは？と思う。使いづらいなら自作してしまおうと重い、OSX の API 経由でファンの回転速度を変更出来ないか調べたが、どうやら出来ないようだった。

[Macbook Pro system fan speed control via API](https://developer.apple.com/forums/thread/657514)

仕方がないので、今回はファンの操作が出来る smcFanControl のラッパーを node.js で書くことにした。完成品は npm パッケージとして公開しているが、どのバージョンで動作するか等が全く不明なので自己責任でお願いできればと思う。

- GitHub: https://github.com/yakimelon/EasyMacFanController
- npm: https://www.npmjs.com/package/ezf

※ 2021/07/29 アップデート

- 全てのファンを最大速度で回転させるオプションを追加
- 回転速度の計算が間違っていたので修正

# 使い方

最初に smcFanControl をインストールする必要がある。その後、npm で ezf をインストールする。

```
$ brew install smcfancontrol --cask
$ npm install -g ezf
```

`ezf -h` で表示したヘルプが下記の通り。

```
Options:
  -h, --help             display help for command
  -s, --status           Check mac fan status
  -a, --auto             Change mac fan all auto mode
  -m, --max              Maximize all fans speed
  -r, --rotate [speed]   Change mac fan rotate
```

## ファンの回転速度を変更する

ファンの情報を表示して、現在のモードと最高/最低回転速度を確認。
その後、最高速度以下かつ最低速度以上の回転速度を指定。

```
$ ezf --status
$ ezf --rotate [回転速度]
```

## 全てのファンの回転速度を最大にする

全てのファンをそれぞれの最大速度で回転させる。

```
$ ezf --max
```

## 全てのファンの回転を自動制御に戻す

```
$ ezf --auto
```

# 実装方法

短いのでコードをベタ貼りしておく、エラートラッキング等は一切していない。
コマンドライン引数のパースは簡単に済ませたかったので `minimist` を利用している。

```js
#! /usr/bin/env node

const argv = require("minimist")(process.argv.slice(2));
const { execSync } = require('child_process');

// smcFanControl のコマンドは長いので、オプションを受け取る形で関数として切り分けた
function smc(options) {
    return execSync('sudo /Applications/smcFanControl.app/Contents/Resources/smc ' + options);
}

// ファンの個数を取得する
function getFanCount() {
    return smc('-f').toString().split('\n')[0].slice(-1);
}

// 全てのファンを手動制御モードに変更する
function changeAllFansForceMode(fanCount) {
    const target = ((2 ** fanCount) - 1).toString(16);
    smc('-k "FS! " -w 000' + target);
}

// ファンのスピードを変更する
function changeFanRotationSpeed(fanNumber, speed) {
    smc('-k F' + fanNumber + 'Tg -w ' + fanSpeedMapper(speed));
}

// 10進数のファンスピードをsmcが理解出来る形式に変換する
// 10進数→2進数→2ビット左シフト→16進数変換
function fanSpeedMapper(speed) {
    return (speed << 2).toString(16);
}

// show help
if (argv.help || argv.h) {
    console.log("" +
        "Options:\n" +
        "  -h, --help             display help for command\n" +
        "  -s, --status           Check mac fan status\n" +
        "  -a, --auto             Change mac fan all auto mode\n" +
        "  -m, --max              Maximize all fans speed\n" +
        "  -r, --rotate [speed]   Change mac fan rotate"
    );
    return;
}

// ファンの情報(回転数など)を標準出力するコマンド
if (argv.status !== undefined || argv.s !== undefined) {
    const stdout = execSync('/Applications/smcFanControl.app/Contents/Resources/smc -f');
    console.log(stdout.toString());
    return;
}

// 全てのファンを自動制御モードに切り替えるコマンド
if (argv.auto !== undefined || argv.a !== undefined) {
    smc('-k "FS! " -w 0000');
    console.log("changed mac fan all auto mode.");
    return;
}

// 全てのファンを最大速度で回転させるコマンド
if (argv.max !== undefined || argv.m !== undefined) {
    // ファンの個数を自動で取得後、全てのファンを手動制御モードに切り替える
    const fanCount = getFanCount();
    changeAllFansForceMode(fanCount);

    // 各ファンの最大スピードを smc -f の結果から取得して配列にする
    // 例) [fan#0 speed, fan#1 speed...]
    const maximumSpeeds = smc('-f')
        .toString()
        .split('\n')
        .filter(x => x.indexOf('Maximum') !== -1)
        .map(x => x.split(':')[1]
        .slice(1));

    // 全てのファンを最大スピードに変更する
    for (let i = 0; i < fanCount; i++) changeFanRotationSpeed(i, maximumSpeeds[i]);

    console.log("Maximize all fans speed.");
    return;
}

// 全てのファンの回転速度を指定した速度に変更するコマンド
if (argv.rotate !== undefined || argv.r !== undefined) {
    // ファンの個数を自動で取得後、全てのファンを手動制御モードに切り替える
    const fanCount = getFanCount();
    changeAllFansForceMode(fanCount);

    // 全てのファンのスピードを指定した速度に変更
    const speed = argv.r || argv.rotate;
    for (let i = 0; i < fanCount; i++) changeFanRotationSpeed(i, speed);

    console.log("changed mac fan rotation speed.");
    return;
}

console.log('Command not found. Please enter "ezf -h".');
```

1行目にある `#! /usr/bin/env node` は `npm install -g` での公開を前提とする場合は必須になる、詳細はこちらの [Stackoverflow](https://stackoverflow.com/questions/39585342/node-when-i-run-package-json-bin-command-give-me-syntax-error-near-unexp) を参照。

### 

# npmパッケージとして公開する

npm パッケージを公開した経験は無いので、良い経験だと思ってせっかくなので公開した。公開自体は難しくなかったので、各種情報をこちらで共有できればと思う。基本的な npm パッケージの公開方法は下記の記事を参照すると良いかと思われる。

- [3分でできるnpmモジュール - Qiita](https://qiita.com/fnobi/items/f6b1574fb9f4518ed520)
- [初めてのnpm パッケージ公開 - Qiita](https://qiita.com/TsutomuNakamura/items/f943e0490d509f128ae2)
- [npmに自作のCLIモジュールを公開する - memorandum](https://slowquery.hatenablog.com/entry/2014/06/02/022459)

## 簡易的な手順

簡単にまとめると下記の通り。

1. https://npmjs.org/signup から npm 開発者登録 (メール認証をしておく) <- 一瞬で終わる
1. `npm adduser` でアカウント紐付け
1. package.json を用意して本体を実装 (スクリプトは `bin/` 以下に配置した)
1. `npm publish` でアップロード

アップロード時に `README.md` が含まれているとページの見栄えが良くなるのと、package.json に repository 情報を書いておくと自動で表示してくれる。また、npm パッケージに変更を加えて更新したい場合は package.json の `version` を変更して再び `npm publish` するだけで良い。(バージョンアップはコマンドから出来るっぽい?詳しく調べなかったので不明)

## ディレクトリ構成

```
.
├── .git
├── .gitignore
├── LICENCE
├── README.md
├── bin
│   └── ezf.js
├── node_modules
│   ├── .package-lock.json
│   └── minimist
├── package-lock.json
└── package.json
```

## package.json の中身

今回自分が書いた package.json の中身は下記の通り。基本的には `npm init` で作られたものだが、追加で下記の変更を加えている。

- preferGlobal の追記 (`npm install -g` を許可する)
- main の削除 (require される部類のパッケージでは無いため不要)
- bin の追加 (ここに実行されるファイルのパスを記述する)


```json
{
  "name": "ezf",
  "version": "0.0.6",
  "description": "Easy Mac Fan Control.",
  "bin": {
    "ezf": "bin/ezf.js"
  },
  "preferGlobal": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/yakimelon/EasyMacFanController"
  },
  "author": "yakimelon@ice_arr",
  "license": "MIT",
  "dependencies": {
    "minimist": "^1.2.5"
  }
}
```

## 成果物

[npmのページ](https://www.npmjs.com/package/ezf) はこのような形になった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44388/c9ca1761-d46d-9999-7401-ee67cd36d35c.png)

また、最終的に出来上がったコマンドがこちら。

![6a9870a9c330ff037b0bc3eebea4c2d2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44388/4f72bd5e-a954-1219-d795-0ea85b27ba05.gif)

# まとめ

個人的に便利なものを作れた満足さと、今までやってこなかった「npmパッケージの公開」を実践できてとても満足した。npm install -g で npm パッケージを公開することに関しての日本語記事はあまりなく、少し戸惑ったので誰かの役に立てばと思う。

もし間違いや理解が足りていない部分がありましたら、コメントにてご指摘いただけますと幸いです。