Raspberry piとTVをHDMIで繋げてTVを制御する。

# 前書き
Raspberry pi4bもjetson nanoも定価で全然売っておらず、買えてません。欲しい・・・（前回に続いて２回目。）

前回の記事[LINE BOT経由でパパの居場所（androidスマホ）を地図で教える（成功編）](https://qiita.com/TVCAdev/items/ec08b008ddc3d62e629b)に続いて、TVとHDMIでつながっているRaspberry piを活用してTVを制御してみます。
子供が学校から帰ってきて、TVばっかり見て困っているので、考えてみました。

TVの電源ON/OFFのログが見れたり、TVを起動禁止（10秒周期でTV電源のON/OFFを検知し、ONだったら消す）にしたりできます。
<p>
<img width="400" alt="line1.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/2747de59-ec77-971f-68a5-90d9848a1e8a.jpeg">
<img width="400" alt="line2.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/de83b7dd-6532-d94d-d0bc-36905c1b41cd.jpeg">
</p>

ご指摘・ご質問あれば、コメントいただければと思います。

# 構成＆動作
以下に構成図と簡単な動作を示します。
![tvban.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/c0a4c04b-2800-584e-def9-40e7da2670c0.png)

TVとRaspberry piをHDMIで接続します。
Raspberry piは、CEC対応なので、そのまま使用します。

確認者がLINEでTVの状態の確認、TVの過去の電源ON/OFFの履歴および禁止のON/OFFが設定できます。
docker内でpythonを使用して、libCECライブラリ経由でTV情報を取得しています。
TVの過去の電源ON/OFFの履歴および禁止のON/OFFの設定については、firebaseのFirestore Databaseに保存しています。

:::note warn
複数ホストでdockerを動作させているため、オーケストレーションツールの導入を一度は検討しましたが、断念しました。
Docker SWARM: docker-compose.ymlのdevices:の記載が機能しなくなるため、見送りました。
k8s: メモリを使いすぎるので、導入できず。（Raspberry Pi4欲しい・・・。）
:::

# 構築
環境の構築については、
記事[LINE BOT経由でRaspberry Piに接続したUSBカメラの画像を取得してみる](https://www.diagrams.net/"https://www.diagrams.net/")を参照ください。

また、前回の記事[LINE BOT経由でパパの居場所（androidスマホ）を地図で教える（成功編）](https://qiita.com/TVCAdev/items/ec08b008ddc3d62e629b)でFirebaseのCloud Firestore（Cloud Messagingに使用するトークンの保存に使用）を使用しましたので、今回もTVの禁止設定とログに使用したいと思います。

## CEC制御部分のpythonコード
今回の肝であるCECの制御部分の説明です。
python3-cecパッケージに含まれる/usr/share/doc/python3-cec/examples/pyCecClient.pyを参考にしました。

:::note warn
Raspberry pi2で何故か、TVのステータスが何故かunknownになってしまう、/dev/cec0を見に行くなど発生しました。
色々調べてみると、/boot/configのdtoverlay=vc4-kms-v3dで指定しているドライバが原因と分かりました。
/boot/config.txtで、"dtoverlay=vc4-kms-v3d"となっていないかチェックしてください。
もし、なっていれば、"dtoverlay=vc4-fkms-v3d"に変更して試してみてください。
:::

```cec_control.py(1)
# for cec
import cec

# for flask
from flask import Flask, jsonify

# for environment
import os

app = Flask(__name__)
```

CECの制御部分はWebAPI形式で提供するので、Flaskをインポートしています。
あと、もちろんCECもインポートします。

```cec_control.py(2)
    # create new libcec_configuration
    cecconfig = cec.libcec_configuration()

    # set configulation
    set_config(cecconfig, cec_control_name)

    # set keypress callback
    set_keypresscallback(cecconfig, key_press_callback)

    # Initialise
    adapter = cec.ICECAdapter.Create(cecconfig)
    adapter.InitVideoStandalone()

    # Open adapter
    adapters = adapter.DetectAdapters()

    if (len(adapters) < 1):
        print("can not find adapter")
        exit(0)

    # get my adapter info
    my_adapter = adapters[0]
    print("my_adapter is :")
    print("  port:     " + my_adapter.strComName)
    print("  vendor:   " + hex(my_adapter.iVendorId))
    print("  product:  " + hex(my_adapter.iProductId))
    adapter_name = my_adapter.strComName

    if adapter.Open(adapter_name):
        print("connection opened.")
    else:
        print("unable to open the device on port " + adapter_name)
        exit(0)
```
CECの初期化部分です。
ほぼ定型です。

```cec_control.py(3)
# Get TV's power status
@app.route('/GetTVPowerStatus', methods=['GET'])
def getTVPowerStatus():
    power = adapter.GetDevicePowerStatus(0)
    pow_str = adapter.PowerStatusToString(power)
    json = {
        'status': pow_str,
        'name': cec_control_name
    }
    return jsonify(json)
```
初期化で取得したadapterでGetDevicePowerStatus(0)を読んでます。
0は論理アドレスでTVが割り当てられていますので、テレビの電源状態が取得できます。
取得できるのは数字のため、人間が読めるようにadapter.PowerStatusToString(power)で変換後、
名前とセットでjsonにして返しています。
電源が入っている場合は"on"、電源が切れている場合はstandbyが取れます。

```cec_control.py(4)
# Set TV's power
@app.route('/SetTVPower/<string:controlType>', methods=['PUT'])
def setTVPower(controlType):
    result = False
    if controlType == 'on':
        result = adapter.PowerOnDevices(0)
    elif controlType == 'off':
        result = adapter.StandbyDevices(0)
    json = {
        'result': result,
        'name': cec_control_name
    }
    return jsonify(json)
```
URLでon,offを指摘すると、電源のON,OFFが出来ます。
adapter.PowerOnDevices(0)、adapter.StandbyDevices(0)ですので、ターゲットはTVとなります。


```cec_control.py(5)
@app.route('/GetActiveSource', methods=['GET'])
def getActiveSource():
    activeSource = adapter.GetActiveSource()
    print('activeSource is ', activeSource)
    as_str = adapter.LogicalAddressToString(activeSource)
    json = {
        'activesource': as_str,
        'name': cec_control_name
    }
    return jsonify(json)
```
adapter.GetActiveSource()で現在、テレビに表示しているソースを取得できます。
ちなみに本APIはどこからも呼ばれておりません。

```cec_control.py(6)
# Be Active Source
@app.route('/BecomeActiveSource/<string:target>', methods=['PUT'])
def becomeActiveSource(target):
    result = False
    if target == 'TV':
        result = adapter.SetActiveSource(0)
    else:
        # If target is other than TV, my device will be active source.
        result = adapter.SetActiveSource()
    json = {
        'result': result,
        'name': cec_control_name
    }
    return jsonify(json)

```
adapter.SetActiveSource()で引数に指定した論理アドレスの機器入力を有効にします。
ですので、TVであれば0を指定します。
ちなみに指定しなければ、自身（Raspberry Piの入力）が有効になります。
ちなみに本APIはどこからも呼ばれておりません。

これらのWebAPIを外部から呼び出すことにより、TV操作を制御します。

## CEC制御部分のdockerイメージ作り
dockerイメージのbaseは、Raspbianでないといけないです。raspberry用のlibcec用ドライバとかを入れるので。
ですが、探したのですが、公式のイメージは少し古いですので、現時点で最新のBullseyeを使用したイメージを作成します。

```Raspbianのdockerイメージ作り
$ docker image import https://downloads.raspberrypi.org/raspios_lite_armhf/root.tar.xz raspiarm32-lite
Downloading from https://downloads.raspberrypi.org/raspios_lite_armhf/root.tar.xz
Importing [==================================================>]  311.3MB/311.3MB
sha256:ee3e4b69b70506aab7751d8922bc8cd6e7741dfd3132c76a1f41d8f548194f2d
$
```
raspberry piの公式から、raspberry pi os のlite(GUIが無いやつ）のroot.tar.gzをインポートして、イメージを作成します。

```EC制御部分のDockerfile
FROM raspiarm32-lite:latest

ENV TOP_DIR /cec_control

RUN mkdir ${TOP_DIR}

COPY cec_control.py ${TOP_DIR}

WORKDIR ${TOP_DIR}
RUN apt-get update && apt-get install -y \
    libcec6 \
    python3-cec \
    python3-flask

CMD ["/usr/bin/python3","cec_control.py"]
```
libcec6はlibcec関連のパッケージ、python3-cecはpythonのlibcecバインディングとかが入っているpythonからcecを使うのに必要なパッケージです。

```docker-compose.yml
  cec_control_LIVING:
    image: docker_cec_control:latest
    restart: always
    networks:
      - docker_net
    environment:
      CEC_CONTROL_HOST: "cec_control_LIVING"
      CEC_CONTROL_PORT: "3001"
      CEC_CONTROL_NAME: "LIVING"
    devices:
      - "/dev/vchiq:/dev/vchiq"
```
docker_cec_controlは、上のDockerfileから作成されたイメージです。

> "/dev/vchiq:/dev/vchiq"

Raspberry PiのlibCECは、/dev/vchiqデバイスが必要のようなので、docker上からも触れるように設定します。

あとは、外部から制御する部分だけですので、詳細は割愛します。
ソースコードをご確認ください。

CEC制御のソースコード：[cec_control](https://github.com/TVCAdev/cec_control/tree/TVBanFinished "cec_control")
heroku上のソースコード：[tvca-line-bot](https://github.com/TVCAdev/tvca-line-bot/tree/TVBanFinished "tvca-line-bot")
docker上のjavascriptのソースコード[TVCArouter](https://github.com/TVCAdev/TVCArouter/tree/TVBanFinished "TVCArouter")

# 総括
cec-clientを使用してTVを制御する記事はよく見かけますが、pythonからAPIを呼び出す記事が少なかったので、書いてみました。

この記事が少しでも、他の方の役に立てば幸いです。