【Raspberry Pi】エアコンをPHPから操作

こちらの記事を自分なりに咀嚼した内容を書いていきます。
[Raspberry Piで三菱のエアコンのリモコンをスキャン・解析・送信してみる](https://qiita.com/Hiroki_Kawakami/items/37cdb412a4e511a58103)

#環境

- Macbook
- Raspberry Pi3 ModelB
- 三菱 霧ヶ峰(エアコン)


#実際の操作

赤外線の受信をしたあと(赤外線受信は以前の[こちら](https://qiita.com/yktk435/items/f4bdd2024b1b9bd0f5c8)の記事の方法でやります)のファイルの中を除くとこの様になっています。

```json:codes
{"aircon:on": [3440,1720,430,1290,430,1290,430,430,430,430,430,430,430,1290,430,430,430,430,430,1290,430,1290,430,430,430,1290,430,430,430,430,430,1290,430,1290,430,430,430,1290,430,1290,430,430,430,430,430,1290,430,430,430,430,430,1290,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,1290,430,1290,430,430,430,1290,430,430,430,430,430,1290,430,1290,430,1290,430,430,430,430,430,430,430,430,430,430,430,1290,430,1290,430,430,430,1290,430,430,430,430,430,430,430,1290,430,430,430,430,430,1290,430,430,430,430,430,1290,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,1290,430,430,430,1290,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,430,1290,430,1290,430,1290,430,430,430,1290,430,1290,430,1290,430]}
```

説明しやすくするために少し加工します。


```
3469  1697  
428  1290
428  1290
428  430
428  430
428  430
428  430
428  430
428  430
428  430

(中略)

428  430
428  1290
428  430
428  1290
428  430
428  430
428  430
428  430
428  
```

**左の列は赤外線が点灯している時間
右の列は赤外線が消灯している時間です。**

一番最後が点灯時間だけで終わっているのは、、消灯しっぱなしのため記録されません。

一番上の`3469`と`1697`はLeaderと呼ばれ
下記を参考に`3469≒8×1697`が成り立つため、このリモコンは**家製協フォーマット**であると言えます。
[赤外線リモコンの通信フォーマット](http://elm-chan.org/docs/ir_format.html)

#解析結果の表

解析結果とプログラムを参考にPHPのプログラムにしていきます。

解析結果![https___qiita-image-store.s3.amazonaws.com_0_242402_8227486d-9575-6147-3c5d-33c8e94b4954.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136271/32f4d15d-35c6-a984-d15d-a4f5275e31c8.png)
>出典：[Raspberry Piで三菱のエアコンのリモコンをスキャン・解析・送信してみる](https://qiita.com/Hiroki_Kawakami/items/37cdb412a4e511a58103)

2020年8月10日 追記-------------------------------
この表ですが、3文字で書かれているところ、例えば0x2や0x6等はそれぞれ0x02や0x06を表しているようです。
追記終わり-------------------------------

これに追加して内部クリーンに関する命令も発見したので書いておきます。

|バイト|機能|設定値|
|---|---|---|
|14バイト目|内部クリーン|入：14 切：16|

内部クリーンを有効にしておくとエアコンをオフにした後、エアコン側で内部クリーンなるものをするのですが、なんだか匂いがきついんで`切：16`を固定値として動かしていきます。

#プログラム
[Raspberry Piで三菱のエアコンのリモコンをスキャン・解析・送信してみる](https://qiita.com/Hiroki_Kawakami/items/37cdb412a4e511a58103)でC言語で書かれていたプログラムをPHPに書き換えたのがこちらです。
まだまだ修正すべき点があるため、今後も更新予定です。



```php5:exec.php
<?php
const FILE_PATH='/var/www/html/aircon/php/';
if (file_exists(FILE_PATH.'savedata')) {
    $bytes=unserialize(file_get_contents(FILE_PATH.'savedata'));
    
    $bytes[17]=0;
} else {
    $bytes=[
    '0'=>35, //固定値
    '1'=>203,//固定値
    '2'=>38, //固定値
    '3'=>1,  //固定値
    '4'=>0,  //固定値
    '5'=>32, //電源  入：32 切：0
    '6'=>88, //モード 冷房(体感入)：88 冷房(体感切)：24 暖房(体感入)：72 暖房(体感切)：8 除湿(体感入)：80 除湿(体感切)：16 送風：56
    '7'=>10, //設定温度  設定温度-16
    '8'=>22, //風左右、除湿強度、
    '9'=>73, //動作音、風上下、風速
    '10'=>0, //固定値
    '11'=>0, //固定値
    '12'=>0, //固定値
    '13'=>0, //風エリア
    '14'=>16,//内部クリーン 入:14 切：16
    '15'=>0, //風速パワフル
    '16'=>0, //固定値
    '17'=>0  //チェック
  ];
}
function aeha($T, $bytes, $repeats, $interval)
{
    $bytes=updateCheck($bytes);
    $i=0;
    $length=count($bytes);
    $file=fopen(FILE_PATH.'codes', 'w');
    flock($file, LOCK_EX);
    fwrite($file, '{"aircon:on": [');
    while (1) {
        fwrite($file, ($T*8).",".($T*4).",");
        for ($j=0; $j < $length; $j++) {
            for ($k=0; $k < 8 ; $k++) {
                if ($bytes[$j] & (1 << $k)) {
                    fwrite($file, $T.",".($T*3).",");
                } else {
                    fwrite($file, $T.",".$T.",");
                }
            }
        }
        if (++$i >= $repeats) {
            fwrite($file, $T);
            break;
        } else {
            fwrite($file, $T." ".$interval);
        }
    }
    fwrite($file, ']}');
    flock($file, LOCK_UN);
    fclose($file);
    shell_exec('python3 '.FILE_PATH.'irrp.py -p -g25 -f '.FILE_PATH.'codes aircon:on');
    if ($bytes[5]==32) {
        SaveData($bytes);
    }
}

function SaveData($bytes)
{
    $s=serialize($bytes);
    file_put_contents(FILE_PATH.'savedata', $s);
}

function GetOption()
{
    $bytes=unserialize(file_get_contents(FILE_PATH.'savedata'));
    $mode=[
      88=>'冷房(体感入)',
      24=>'冷房(体感切)',
      72=>'暖房(体感入)',
      8=>'暖房(体感切)',
      80=>'除湿(体感入)',
      16=>'除湿(体感切)',
      56=>'送風'][$bytes[6]];
    $speed=[
      '自動',
      '弱',
      '中',
      '強'][$bytes[9]&7];
    $area=[
      0=>'風左右',
      128=>'全体',
      64=>'左半分',
      192=>'右半分'][$bytes[13]];
    $dryintensity=[
      0=>'強',
      2=>'標準',
      4=>'弱',
      6=>'冷房'][$bytes[8]&15];
      
    print "モード:{$mode}\n";
    print "設定温度:".($bytes[7]+16)."℃\n";
    print "除湿強度:{$dryintensity}\n";
    print "風速:{$speed}\n";
    print "風エリア:{$area}\n";
}

function SetPower($power)
{
    global $bytes;
    if (!$power || $power === "off" || $power === "false") {
        $bytes[5] = 0;
    } else {
        $bytes[5] = 32;
    }
}

function SetMode($mode)
{// モードをセット（cool: 冷房, heat: 暖房, dry: 除湿, wind: 送風）
    global $bytes;
    $b=["cool"=> 88, "heat"=> 72, "dry"=> 80, "wind"=> 56][$mode];
    if (!$b) {
        print "Mode {$mode} is not defined!\n";
        return false;
    }
    SetPower(true);
    $bytes[6]=$b;
    return true;
}

function SetDryIntensity($intensity)
{ // 除湿強度（high: 強, normal: 標準, low: 弱）
    global $bytes;
    $b = ["high"=> 0, "normal"=> 32, "low"=> 64][$intensity];
    if (!$b) {
        print "Dry intensity {$intensity} is not defined!" ;
        return false;
    }
    $bytes[8] = ($bytes[8] & 240) | $b;
    return true;
}

function SetTemperature($temperature)
{// 設定温度をセット（16~31）
    global $bytes;
    if ($temperature < 16 || $temperature > 31) {
        print "Temperature {$temperature} is out of range (16 ~ 31).";
        return false;
    }
    $bytes[7] = $temperature - 16;
    return true;
}

function SetWindSpeed($speed)
{ // 風速（0: 自動, 1: 弱, 2: 中, 3: 強, 4: パワフル）
    global $bytes;
    if ($speed < 0 || $speed > 4) {
        print "Wind speed {$speed} is out of range (0 ~ 4).";
        return false;
    }
    $powerful = 0;
    if ($speed == 4) {
        $speed = 3;
        $powerful = 16;
    }
    $bytes[9] = ($bytes[9] & 248) | $speed;
    $bytes[15] = $powerful;
    return true;
}

function SetWindVertical($vertical)
{ // 風向上下（0: 自動, 1: 最上 ~ 5: 最下, 6: 回転）
    global $bytes;
    if ($vertical < 0 || $vertical > 6) {
        print "Vertical wind direction {$vertical} is out of range (0 ~ 6)." ;
        return false;
    }
    if ($vertical == 6) {
        $vertical = 7;
    }
    $bytes[9] = ($bytes[8] & 199) | ($vertical << 3);
    return true;
}

function SetWindArea($area)
{ // 風エリア（none: 風左右の値を利用, whole: 全体, left: 左半分, right: 右半分）
    global $bytes;
    $b = ["none"=> 0, "whole"=> 128, "left"=> 64, "right"=>192 ][$area];
    if (!$b) {
        print "Wind area {$area} is not defined!";
        return false;
    }
    $bytes[13] = $b;
    return true;
}

function SetSysVolume($vol)
{//音(なし：0, あり：1)

    global $bytes;
    if ($vol=='0' || $vol =='1') {
        $bytes[9] = ($bytes[9] & 63) | ($vol << 6);
        return true;
    } else {
        print "0 or 1 (0:mute , 1:sound)" ;
        return false;
    }
}

function updateCheck($bytes)
{
    $sum=0;
    for ($i=0; $i <count($bytes)-1 ; $i++) {
        $sum+=$bytes[$i];
    }
    $bytes[17]=$sum&255;
    return $bytes;
}

if (@$_SERVER["REQUEST_METHOD"] == "POST") {
    switch ($_POST['data']) {
      case 'on':
        break;
      case 'off':
        SetPower('off');
        break;
      case 'up':
        SetTemperature($bytes[7]+17);
        break;
      case 'down':
        SetTemperature($bytes[7]+15);
        break;
      case 'get':
        GetOption();
        exit;
        break;
      default:
        exit;
        break;
  }
} elseif (count($argv)==1) {
    $bytes=unserialize(file_get_contents(FILE_PATH.'savedata'));
} else {
    for ($i=1; $i<count($argv);) {
        if ($argv[$i] == "-p" || $argv[$i] == "--power") {
            SetPower($argv[$i + 1]);
            $i+=2;
        } elseif ($argv[$i] == "-m" || $argv[$i] == "--mode") {
            if (!SetMode($argv[$i + 1])) {
                print "Invalid value of mode option {$argv[i + 1]}";
            }
            $i+=2;
        } elseif ($argv[$i] == "-t" || $argv[$i] == "--temperature") {
            if (!SetTemperature($argv[$i + 1])) {
                print "Invalid value of temperature option {$argv[$i + 1]}";
            }
            $i+=2;
        } elseif ($argv[$i] == "-d" || $argv[$i] == "--dry_intensity") {
            if (!SetDryIntensity($argv[$i + 1])) {
                print "Invalid value of dry intensity option {$argv[$i + 1]}";
            }
            $i+=2;
        } elseif ($argv[$i] == "-h" || $argv[$i] == "--horizontal") {
            if (!SetWindHorizontal($argv[$i + 1])) {
                print "Invalid value of wind horizontal option {$argv[$i + 1]}";
            }
            $i+=2;
        } elseif ($argv[$i] == "-v" || $argv[$i] == "--vertical") {
            if (!SetWindVertical($argv[$i + 1])) {
                print "Invalid value of wind vertical option {$argv[$i + 1]}";
            }
            $i+=2;
        } elseif ($argv[$i] == "-s" || $argv[$i] == "--speed") {
            if (!SetWindSpeed($argv[$i + 1])) {
                print "Invalid value of wind speed option {$argv[$i + 1]}";
            }
            $i+=2;
        } elseif ($argv[$i] == "-a" || $argv[$i] == "--area") {
            if (!SetWindArea($argv[$i + 1])) {
                print "Invalid value of wind area option {$argv[$i + 1]}";
            }
            $i+=2;
        } elseif ($argv[$i] == "-g" || $argv[$i] == "--get") {
            GetOption();
            exit;
            $i++;
        } elseif ($argv[$i] == "-vo" || $argv[$i] == "--volume") {
            if (!SetSysVolume($argv[$i + 1])) {
                print "IInvalid value of system volume option {$argv[$i + 1]}";
                var_dump(SetSysVolume($argv[$i + 1]));
            }
            $i+=2;
        } else {
            print "Invalid option key \"{$argv[$i]}\"";
            $i+=2;
            exit;
        }
    }
}



aeha(430, $bytes, 1, 13300);
$_POST=[];

```
#ちょっとだけ解説
`FILE_PATH`にこのコードを配置したパスを記載します。

`FILE_PATH.'savedata'`は赤外線のデータを配列にしたものをシリアライズ化したファイルで、2回め以降の実行の際はこのデータを使用し、初回使用時で`FILE_PATH.'savedata'`がない場合は適当なデータを指定しています。

`aeha`関数は以下の役割を担っています。
- 解析したデータを赤外線コードに変換し、赤外線送信用コードファイル`codes`を作成
- 赤外線の送信

`updateCheck`は`$bytes`の`$bytes[17]`以外のすべての値の合計値を`$bytes[17]`に格納しています。

赤外線データは以下の2つのペアで構成されています。
- 430 430
- 430 1290

これはつまりこういうことです。

|ペア|意味|
|---|---|
|点灯時間  消灯時間|0|
|点灯時間  3✕消灯時間|1|

つまりは信号は「0」と「1」の信号が送られいるわけです。

赤外線コード一部抜粋を0と1で見てみるとこうなります。

|赤外線コード|意味|
|---|---|
|428  1290|1|
|428  1290|1|
|428  430|0|
|428  430|0|
|428  430|0|
|428  430|0|
|428  430|0|
|428  430|0|

これからのコードが8ビットずつ17回送られています。

コードのはじめはLeaderと呼ばれ、
8✕点灯時間と4✕消灯時間で構成されています。以降で430と1290の組み合わせを生成していきます。

流れとしては、$bytesのそれぞれの値を8回ビットシフトさせ0のときは[430 430]、1のときは[430 1290]を生成していきます。

そうして送信用コード`codes`を作成し、`shell_exec`で実行しその後`SaveData`で送信したデータを記録しておきます。
この記録に関してはデータベースを使用する予定です。

後はirrp.pyに送信命令をして完了って感じです。

#実行方法

実際には使い方は以下のようにします。

例

```
$  php exec.php -m cool -t 24 -p on -s 4
```
上記だと、冷房、24℃、風速パワフルで運転するという命令になります。