Yahoo天気をスクレイピングしてHome Assistantに天気を出す

Home Assistantで天気を表示するのにAccuWeather APIを使っていましたが、2025年9月9日に無料版の廃止とAPI変更があり、使えなくなってしまいました。 悲しい。

(正直な所、精度が良くないのであてにしていなかったが…)

alerts.home-assistant.io

代わりにYahoo天気のWebページをスクレイピングして、天気を表示できるようにします。 こちらの方が断然精度がいいしね。

できたもの

現在の天気、週間天気、1時間毎の天気が見れるようになりました。 まあまあいい感じですね。

ちなみにスクレイピング先はこんな感じ。

設定手順

天気予報の形で表示できるようにするには、

  1. Webページをスクレイピングして、天気や気温などの要素ごとにエンティティを作成する
  2. 作成したエンティティを組み合わせて、Weatherテンプレートを構築する

の二段階の作業が必要です。ちょっとめんどくさい。

面倒な人はconfiguration.yaml生成スクリプトまで読み飛ばしてください。

1. スクレイピングとエンティティの作成

スクレイピングにはHome AssistantのScrape Integrationを使います。

www.home-assistant.io

UIでは細かい設定ができませんので、configuration.yamlに設定を書き込んでいきます。

Yahoo天気には多数の情報がありますので、全て取り込むには

  • 天気、最高気温、最低気温、降水確率 x 7日分
  • 天気、気温、降水確率、降水量、湿度、風向、風速 x 48時間分

合計364個のエンティティを作成する必要があります。 気が遠くなりますね…

…最終的に1636行のconfiguration.yaml爆誕しました。しんどい。

Home AssistantにYahoo天気を出すためのconfiguration.yaml · GitHub

2. Weatherテンプレートの作成

作成したエンティティをWeatherテンプレートにマッピングしていきます。

www.home-assistant.io

今回は以下10箇所の設定が必要です。

  • temperature_unit : 天気の単位
  • precipitation_unit : 降水量の単位
  • wind_speed_unit : 風速の単位
  • condition_template : 現在の天気
  • temperature_template : 現在の気温
  • humidity_template : 現在の湿度
  • wind_speed_template : 現在の風速
  • wind_bearing_template : 現在の風向
  • forecast_daily_template : 日別の天気
  • forecast_hourly_template : 時間別の天気

このうちcondition_template、wind_bearing_template、forecast_daily_template、forecast_hourly_templateは特殊な設定が必要です。

それ以外は数値であればOKです。

天気(condition)

condition_templateは、天気を表す特定の文字列しか受け付けてくれません。 日本語の「晴れ」を「sunny」に変換する必要があるわけです。

www.home-assistant.io

一応こんな感じで変換処理を書きました。 が、Yahoo天気の設定パターンを把握できていないため適当です。ここは要観察ですね…。

{% set mapping = {
    '晴れ': 'sunny',
    '晴時々曇': 'partlycloudy',
    '晴れのち曇り': 'partlycloudy',
    '曇り': 'cloudy',
    'くもり': 'cloudy',
    '曇': 'cloudy',
    '曇時々晴': 'partlycloudy',
    '曇りのち晴れ': 'partlycloudy',
    '曇のち雨': 'rainy',
    '雨': 'rainy',
    '雨天': 'rainy',
    '小雨': 'rainy',
    '弱雨': 'rainy',
    '強雨': 'pouring',
    '大雨': 'pouring',
    '雷雨': 'lightning-rainy',
    '雷': 'lightning',
    '雪': 'snowy',
    '大雪': 'snowy',
    'みぞれ': 'snowy-rainy',
    '風': 'windy',
    '強風': 'windy',
    '暴風': 'windy-variant',
    '霧': 'fog',
    '濃霧': 'fog'
} %}
{{ mapping.get(value) }}

そもそも…Yahoo天気には65種類ものパターンがあるのに対し、Home Assistant側は14種類しかありません。 どうしても情報が欠落するため、ここはもう妥協するしかありません…。

www.watch.impress.co.jp

風向(wind_bearing)

Yahoo天気は16方位で風向を表現しますが、Home Assistant側は北を0°とした方位角で表現します。 ここでも変換が必要です。

{% set mapping = {
  '静穏': 0,
  '北': 0, '北北東': 22.5, '北東': 45, '東北東': 67.5,
  '東': 90, '東南東': 112.5, '南東': 135, '南南東': 157.5,
  '南': 180, '南南西': 202.5, '南西': 225, '西南西': 247.5,
  '西': 270, '西北西': 292.5, '北西': 315, '北北西': 337.5
} %}
{{ mapping.get(value) }}

Yahoo天気は無風の時に「静穏」という表現をします。 無風なので風向もありませんが、便宜上0°を割り当てることにします。

予報(forecast)

forecast_daily_templateとforecast_hourly_templateには、forecast型オブジェクトの配列を渡す必要があります。

www.home-assistant.io

ここで、先程作成した300超のエンティティを割り当てていくわけですが… Jinja2によるテンプレートエンジンでループ処理します。

www.home-assistant.io

Yahoo天気は最大で48時間分の天気を提供しますが、過去の天気は省略されます。 つまり48 - 現在時時間後までは有効なデータが存在し、それ以降はデータが存在しません。

無効なデータを設定しないよう、データがunavailableかチェックしてからforecastに設定します。

forecast_hourly_template: |
  {% set ns = namespace(forecast=[]) %}
  {% for i in range(1, 48) %}
    {% set forecast_data = {
      'datetime': (now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(hours=now().hour + loop.index)).isoformat(),
      'condition': states('sensor.weather_in_fujisawa_in_' ~ loop.index ~ '_hours'),
      'temperature': states('sensor.temperature_in_fujisawa_in_' ~ loop.index ~ '_hours'),
      'humidity': states('sensor.humidity_in_fujisawa_in_' ~ loop.index ~ '_hours'),
      'precipitation': states('sensor.chance_of_precipitation_in_fujisawa_in_' ~ loop.index ~ '_hours'),
      'wind_speed': states('sensor.wind_speed_in_fujisawa_in_' ~ loop.index ~ '_hours'),
      'wind_bearing': states('sensor.wind_direction_in_fujisawa_in_' ~ loop.index ~ '_hours'),
    } %}
    {% if 'unavailable' not in (forecast_data.items() |  map(attribute=1) | list) %}
        {% set ns.forecast = ns.forecast + [forecast_data] %}
    {% endif %}
  {% endfor %}
  {{ ns.forecast }}

configuration.yaml生成スクリプト

ここまで書いておいてなのですが、流石に手書きは不毛すぎるので生成スクリプトを作成しました…

引数に任意のピンポイント予報のURLと、エンティティ名に付与する地名を与えることで、configuration.yamlを生成してくれます。

$ ./gen_forecast.sh https://weather.yahoo.co.jp/weather/jp/14/4610/14205.html Fujisawa

これをHome Assistantに食わせると、こんな感じで大量のエンティティが生えてきます

天気予報カード

あとはダッシュボードで天気予報カードを作成すれば、天気予報が表示できるようになるはずです。

まとめ

死んでしまったAccuWeather APIの代わりに、Yahoo天気をスクレイピングしてHome Assistantで表示できるようにしました。

手動での設定は非常に手間がかかりますが、configuration.yaml生成スクリプトを使えば簡単に設定ができます。

みなさまもよいスマートホームライフをお過ごしください。

PlatformIOのRemote DeveropmentでESP32をリモート開発する

PlatformIOにはリモート開発の機能があります。

docs.platformio.org

開発ホストにデバイスを直接接続せずとも、リモートでファームウェアの書き込みやデバッグができる。
組込開発では装置を動かせないことはよくあるので、リモート開発できるとすごく嬉しい…。

ということで、Raspberry PiをRemote AgentにしたESP32(M5StickC)のリモート開発環境を作ります。
基本的にはQuick Startをやるだけです。

インターネット環境とPlatform IO Cloudのアカウントが必須です。 遠隔地のデバイスも扱えるのはメリットですが、ローカルで完結できないのはデメリットですね。

リモート開発環境

これを描いていて知りましたがPlantUMLのskinparamは非推奨CSSスタイルを使うべきらしいですね。

トークン取得

開発ホストでPlatformIOアカウントにログインし、pio account tokenトークン取得できます。

$ pio account token
Password: 
Personal Authentication Token: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

VSCodeのUIからでも取れます。

リモート(Raspberry Pi)

今回はDockerを使うのでインストールしておきます。

$ sudo su
$ curl -L get.docker.com | bash

PlatformIO Core CLIが使えるコンテナを作ります。
pio remote agentは初回起動時に依存関係をインストールする仕様のため、コンテナだと起動に時間がかかってしまいます。
なのでビルド時にpio remote agent --helpすることで一度起動し、コンテナ内に依存関係が入るようにします。

FROM python:3.13-alpine

RUN apk add --no-cache gcc musl-dev python3-dev libffi-dev openssl-dev cargo
RUN python3 -m pip install --no-cache-dir --upgrade pip
RUN python3 -m pip install --no-cache-dir --upgrade setuptools
RUN python3 -m pip install --no-cache-dir --upgrade platformio
RUN pio remote agent --help

ENV PLATFORMIO_AGENT_NAME=docker-pio-agent

CMD ["ash", "-c", "pio remote agent start -n ${PLATFORMIO_AGENT_NAME}"]

M5StickCをRaspberry Piに接続し、ttyを調べておきます。

$ ls -l /dev/serial/by-id/
total 0
lrwxrwxrwx 1 root root 13 Mar 20 22:46 usb-Hades2001_M5stack_4D52627DAB-if00-port0 -> ../../ttyUSB0

compose.yml を設定します。 ポイントは

services:
  pio_remote_agent:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: pio_remote_agent
    restart: unless-stopped
    environment:
      PLATFORMIO_AUTH_TOKEN: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    devices:
      - /dev/ttyUSB0:/dev/ttyUSB0

コンテナをビルド&起動します。

$ docker compose up -d

設定が正しければSuccessfully authorizedになるはずです。

$ docker compose logs -f
pio_remote_agent  | 2025-03-21 08:45:51 [info] Name: docker-pio-agent
pio_remote_agent  | 2025-03-21 08:45:51 [info] Connecting to PlatformIO Remote Development Cloud
pio_remote_agent  | 2025-03-21 08:45:52 [info] Successfully connected
pio_remote_agent  | 2025-03-21 08:45:52 [info] Authenticating
pio_remote_agent  | 2025-03-21 08:45:53 [info] Successfully authorized

ホスト(VSCode + Platform IO)

pio remote device listすると、リモート接続可能なデバイスが取得できるはずです。

$ pio remote device list 

Agent docker-pio-agent
==================
/dev/ttyUSB0
------------
Hardware ID: USB VID:PID=0403:6001 SER=4D52627DAB LOCATION=1-1.3
Description: M5stack

ビルド&アップロード&書き込みができます。

$ pio remote --agent docker-pio-agent run -t upload

標準出力も見れます。

$ pio remote --agent docker-pio-agent device monitor 

Starting Serial Monitor on docker-pio-agent:/dev/ttyUSB0
--- Terminal on socket://localhost:41218 | 115200 8-N-1
--- Available filters and text transformations: colorize, debug, default, direct, esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
[355669][I][main.cpp:8] loop(): Hello World!
[356669][I][main.cpp:8] loop(): Hello World!
[357669][I][main.cpp:8] loop(): Hello World!
[358669][I][main.cpp:8] loop(): Hello World!
[359669][I][main.cpp:8] loop(): Hello World!

スマートメータから電力を取得してHomeAssistantと連携する

モチベーション

OSSと手頃なハードウェアを使ってHEMS(っぽいもの)を作りたい。

実現方法

  • M5StickCにWi-SUNアダプタを載せて、スマートメーターから定期的にデータを取得する
  • 取得したデータをMQTT BrokerにPublishする
  • Home AssistantでMQTT BrokerをSubscribeしてHome Energy Managementに統合する

必要なもの

ハードウェア

M5StickC Plus

M5StickC/Plus用Wi-SUN HATキット

完成品はこんな感じです。 Wi-SUNは障害物に強く通信距離が長いので、設置場所はかなり自由度が高いです。 家のどこに置いてもつながるんじゃないでしょうか。

ソフトウェア

諸々

  • Bルートの開通手続き

開発環境

できたもの

導入方法

Home Assistant / Mosquitto (MQTT Broker)

導入自体は既にたくさん情報があるので割愛します…

configuration.yaml

M5StickCはスマートメーターから取得したデータをMQTT Publishしますので、Home Assistant側にSubscribeの設定をしておきます。 設定はREADMEの通りです。 重要なのはstate_topic、device_class、unit_of_measurement、state_classで、それ以外は多分適当に変えても大丈夫です。

ESP32 B Route to MQTT Smart Meter

  1. esp32-broute2mqtt-smartmeterをチェックアウトする
  2. _SmartMeterConfig.hSmartMeterConfig.hにリネームして自分の環境に合わせて設定する
  3. PlatformIOでビルドして書き込み起動する
  4. 設定があっていればシリアルに取得値を出力する
[ 87618][I][main.cpp:101] setup(): ConvertCumulativeEnergyUnit : 0.100000
[ 87619][I][main.cpp:102] setup(): SyntheticTransformationRatio: 1
[ 91834][I][main.cpp:119] loop(): InstantaneousPower       : 767 W
[ 91834][I][main.cpp:120] loop(): CumulativeEnergyPositive : 4757.399902 kWh
[ 91836][I][main.cpp:121] loop(): InstantaneousCurrentR    : 4.000000 A
[ 91842][I][main.cpp:122] loop(): InstantaneousCurrentT    : 5.000000 A

うまくいっていれば、この時点でHome Assistantから取得値が見れるようになります。

うまくいかない場合はデバッグログを有効にするといいかもしれません。

Home Assistant - Home Energy Management

Home Assistantインスタンス/config/energyからHome Energy Managementを設定します。

グリッド消費の設定にCumulative Energy Positive(積算電力量計測値(正方向))を追加します。 1kWhあたりの使用料金を入れておくと、コストも追跡できるようになります。

基本的な設定はこれで完了です。

できること

リアルタイムなデータ取得

瞬時電力計測値、瞬時電流計測値(T相)、瞬時電流計測値(R相)、積算電力量計測値(正方向)がそこそこリアルタイムで見れます。 スマートメータとの通信周期は5秒にしていますが、データ取得に2,3秒程度かかるため、Home Assistant側の処理時間も合わせると10秒ぐらい遅延してます。

この値をトリガにしてオートメーションすることもできます。 電気の使いすぎをスマホに通知するとか、契約A数付近になったらスイッチオフしてブレーカーが落ちるのを防ぐとか。

データ蓄積

取得したデータを個別のグラフで見れます。

消費電力とコストの追跡

エネルギーのビューから消費電力とコストが追跡できます。 在宅の日と出社の日で消費電力が1.5倍ぐらい違う…なんてことが可視化されてしまいます。

OPNsenseのHAProxyで443ポートを使い回す(https,ssh,OpenVPN)

HAProxyについて

HAProxyはTCPおよびHTTP向けの高機能なロードバランサー/プロキシである。 バックエンドへの振り分け条件はパケットレベルで定義できるため、条件を上手に定義できればプロトコルレベルのマルチプレクサとして振る舞うこともできる。

OPNsenseはこのHAProxyをプラグインとしてサポートしている。 そこで、様々な理由で競合しがちな443ポートを複数のプロコトルで使い回せるようにしてみる。

OPNsense設定

前提条件として、WAN、LAN、DMZの3つのゾーンが存在し、OPNsenseは3つのインタフェースでそれぞれと接続しているという環境。

f:id:nullsnet:20210918221755p:plain

System: Settings: Administration

まずはOPNsense自身が443ポートを待ち受けできるようにする。 デフォルトでは管理画面へのアクセスに使用している可能性があるので見直す。 Listen InterfacesをLANだけにし、WAN側はHAProxyで使えるようにしておく。

f:id:nullsnet:20210918220636p:plain

Services: HAProxy: Settings:

Real Servers

バックエンドとなるサーバを登録する。 今回はOpenVPNSSHHTTPSの3つを登録する。

Virtual Services: Backend Pools

OPNsenseのフロントエンドとなるサービスを定義する。 Real Serversで定義したサーバを登録していけばよい。

Virtual Services: Public Services

OPNsenseのフロントエンドとなるサービスを定義する。 今回はWAN側IPの443をリッスンする設定とする。

Rules & Checks: Conditions / Rules

いよいよプロトコルごとの振り分けを定義していく。 基本的には、TCPの3WAYハンドシェイク後の、各プロトコルの最初のパケットに対する条件(Conditions)を定義してやれば、当該コネクションが丸ごと処理対象となる。 更に、ルール(Rules)にて条件に一致したパケットをどのバックエンドに流すか定義することで、パケットの振り分けが可能となる。

SSH

Conditon

調べてみると、SSHのハンドシェイクのペイロードは「SSH-2.0」から始まるため、これのバイナリ表記である「5353482d322e30」を条件にすればよい、という情報が出てくる。 パケットをキャプチャして調べてみると、確かにそのようだ。 SYN -> SYN/ACK -> ACKの直後のパケットのペイロードが「SSH-2.0」で始まっているのが確認できる。

f:id:nullsnet:20210918231744p:plain

この条件をHAProxyに設定する。 OPNsenseのHAProxyはGUIでのACL設定ができないため、設定値のパススルーで直接設定する。 「ペイロードの0byteから7byte分がバイナリで5353482d322e30であること」としてreq.payload(0,7) -m bin 5353482d322e30を設定する。

f:id:nullsnet:20210918232114p:plain

Rule

あとは、作成したConditionsに一致するパケットをsshのバックエンドに流す、というRuleを定義すればよい。

f:id:nullsnet:20210918233946p:plain

HTTPS(TLS)

Conditon

HTTPS、というかTLSの場合は簡単で、「Client Hello」を送信してきたもの、という条件でよい。

f:id:nullsnet:20210918232547p:plain

こちらもパススルーでreq.ssl_hello_type 1を設定する。

f:id:nullsnet:20210918232617p:plain

他にも、SNIに含まれるドメイン名を指定する方法があるようだ。

Rule

こちらもSSHと同様にRuleを定義すればよい。

加えてもう一つ、先程のConditonとは別に、tcp-request content acceptのルールを作成する。 TLSを扱う場合は必要なルールのようだ。

f:id:nullsnet:20210919000234p:plain

Enhanced SSL Load Balancing with Server Name Indication (SNI) TLS Extension - HAProxy Technologies

OpenVPN

Condition

OpenVPNに関してはあまり情報がなく、default_backendをOpenVPNとすることで「HTTPSでもSSHでもなければOpenVPNへ流す」という設定例しか見つからなかった。 これは実際やってみると、443ポートへのHTTPSでもSSHでもないアクセス(おそらくbotによる攻撃の類と思われる)が全てOpenVPNに集中してしまう。 これは非常に気持ち悪いのでなんとかしたい。

sslhやOpenVPNソースコードを読んだがよくわからなかったので、力技でなんとかしてみることにした。 OpenVPNへのコネクション時のパケットを複数回キャプチャし、共通点を見つけて条件として定義する、という手法でやってみる。 この方法はOpenVPNの設定値にも依存するだろうし、他の環境でうまくいくかはわからない。

1回目

0040         00 2a 38 be 95 8e 13 12 53 5f 66 e1 aa 4a   ...*8.....S_f..J
0050   16 16 a7 eb a6 3b 27 5d 08 cc 81 88 ee cc 2c 63   .....;']......,c
0060   b4 00 00 00 01 61 45 c3 f4 00 00 00 00 00         .....aE.......

2回目

0040         00 2a 38 44 29 20 dc 73 e6 dc 19 8d 0d 2b   .J.*8D) .s.....+
0050   8b cc 75 11 47 cb aa de 00 67 e4 bc 3b 62 73 b9   ..u.G....g..;bs.
0060   dd 00 00 00 01 61 46 01 c1 00 00 00 00 00         .....aF.......

どうやらペイロード[0]~[2][35]~[38]が共通しているようだ。 ここから条件を作成する。 どの条件に従うかはルール側で定義するため、条件は2つに分けて定義する必要がある。 今回はそれぞれopenvpn_1、openvpn_2という名前で定義した。

  • 条件1(openvpn_1)
    • req.payload(0,3) -m bin 002a38
  • 条件2(openvpn_2)
    • req.payload(31,4) -m bin 00000001

この条件で本当に正しいかはわからないので、様子見して調整することにする。

Rule

Rule側ではopenvpn_1とopenvpn_2の両方が成立する場合としたいため、ANDを設定する。 これでOpenVPNのパケットが振り分けられる。

f:id:nullsnet:20210918234744p:plain

Virtual Services: Public Services

最後にOPNsenseがリッスンするサービスを定義する。 Listen AddressesにWAN側のIPアドレス:443を設定し、Select Rulesに定義したルールを設定すればよい。

また、Option pass-throughにはtcp-request inspect-delay 5sを設定する必要があるようだ。 Introduction to HAProxy Stick Tablesにそう書いてあったし、確かに書かないとうまく動かなかった。

You only need to use this in a frontend or backend when you have an ACL on a statement that would be processed in an earlier phase than HAProxy would normally have the information. For example, tcp-request content reject if { path_beg /foo } needs a tcp-request inspect-delay because HAProxy won’t wait in the TCP phase for the HTTP URL path data. In contrast http-request deny if { path_beg /foo } doesn’t need an tcp-request inspect-delay line because HAProxy won’t process http-request rules until it has an HTTP request.

f:id:nullsnet:20210918235359p:plain

動作確認

HAProxyのLog Fileから確認できる。 ちゃんと443で3つのプロトコルが振り分けられているようだ。

2021-09-19T00:08:36 haproxy[16827] XXX.XXX.XXX.XXX:11395 [19/Sep/2021:00:08:36.881] outbound_443 tls/tls 8/0/44 5804 -- 2/2/1/1/0 0/0
2021-09-19T00:09:40 haproxy[16827] XXX.XXX.XXX.XXX:5243 [19/Sep/2021:00:09:01.365] outbound_443 ssh/ssh 1/0/39350 2953 cD 2/2/0/0/0 0/0
2021-09-19T00:17:08 haproxy[56327] XXX.XXX.XXX.XXX:23787 [19/Sep/2021:00:17:01.754] outbound_443 openvpn/openvpn 1/0/6979 14882 -- 2/2/0/0/0 0/0    

コンテナでGPU acceralatedなWebGLを動かしてブラウザゲームを遊ぶ

モチベーション

とあるブラウザゲームをプレイしています。 このゲームは、1日の回数制限があるが報酬がおいしいコンテンツ、いわゆる日課がたくさんあります。 毎日やるのは正直面倒です。

なので、基本的に「フルオート」機能で消化します。 これはスキル発動や攻撃行動を自動で行う機能で、オンにすれば自動で敵を倒してくれるので便利です。 ただし、操作を自動化するだけで戦闘自体はリアルタイムで進行するため、画面はアクティブにしておく必要があります。 スマホで「フルオートだけオンにして放置する、別の作業をする」といったことは出来ないので、少々面倒です。

そこで、簡単にフルオート放置ができる環境を作ってみます。

  • PC/スマホから操作できること
  • ブラウザだけで操作できること
  • 利用規約に則ること

実現方法

  1. DockerとNVIDIA Container ToolkitでX11OpenGL(WebGL)が動くコンテナを作る
  2. GoogleChromeを動作させる
  3. noVNCでブラウザから操作可能にする

少し調べてみると「ホスト側のtmp/.X11-unix$DISPLAYをコンテナへ共有し、描画はホスト側で行う」という手法が多く散見されます。 ホストを汚したくない&ヘッドレスにしたいので、コンテナ側に全て閉じ込める方式にします。 具体的には、X.Org Serverもコンテナ内で実行します。

諸元

とても古い機材ですが、とりあえずNVIDIA Container Toolkitに対応してさえいればなんでもいいはずです。

CPU Core i5-3230M
Host OS Ubuntu 20.04
Container base Ubuntu 20.04
Docker version 20.10.8
GPU NVIDIA GeForce 650M
GPU driver version 470.57.02
CUDA version 11.4

構築

ホスト

NVIDIA関連のドライバやミドルウェアをインストールします。 以前は依存関係が複雑で面倒だったような気がしますが、現在は非常に簡単になったようです。

ドライバ

リポジトリを追加してcuda-driversをインストールすれば最新版が入ります。 ここの通りやるだけです。

ミドルウェア

こちらもリポジトリを追加してnvidia-docker2だけインストールすれば、container-toolkitなども同時にインストールされます。 ここの通りやるだけです。

nvidia-smiコマンドが実行できればOK。 次の作業のために、実際にインストールされたドライバのバージョンを確認しておきます。

$ nvidia-smi
Sun Aug 15 06:02:11 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 470.57.02    Driver Version: 470.57.02    CUDA Version: 11.4     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 N/A |                  N/A |
| N/A   56C    P8    N/A /  N/A |     79MiB /  2000MiB |     N/A      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

コンテナ

NVIDIAがコンテナをいくつか公開していますが、これらにはX11に必要なファイルを含んでいないようです。 そもそも現状ではX11をサポートしていません。

Display (e.g. X11, Wayland) is not officially supported.

なので自分で環境構築する必要があります。コードはこちら。

ホストとコンテナのドライバのバージョンを合わせる必要があるので、ビルド時の引数で指定します。

docker-compose build --build-arg DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader)
docker-compose up -d

:8081/vnc.htmlにアクセスすればnoVNCの画面が出ます。 自動的にOpenboxとターミナルが起動するようにしてあります。

f:id:nullsnet:20210815184433p:plain

ここからChromeを起動する場合、rootなのでgoogle-chrome --no-sandboxとする必要があります。

できたもの

コンテナ上で動作するGPU acceralatedなWebGLの環境ができました。 ブラウザを閉じても問題なくフルオートできています。 速度も問題なさそうです。 HTML5に対応してさえいれば、どんな端末からでも操作可能です。 これでフルオート放置が捗ります。

ちゃんとした(?)用途としては

とかがあるでしょうか。あんまりなさそうですが。

ドライバアップデート時

ホストOSのドライバをアップデートをしたらコンテナがうまく起動しなくなった。 Xorgの起動に失敗しているようだ。

/usr/bin/Xorg :0

X.Org X Server 1.20.13
X Protocol Version 11, Revision 0
Build Operating System: linux Ubuntu
Current Operating System: Linux 7b222ef168e6 5.4.0-104-generic #118-Ubuntu SMP Wed Mar 2 19:02:41 UTC 2022 x86_64
Kernel command line: BOOT_IMAGE=/vmlinuz-5.4.0-104-generic root=/dev/mapper/ubuntu--vg-ubuntu--lv ro maybe-ubiquity
Build Date: 14 December 2021  02:14:13PM
xorg-server 2:1.20.13-1ubuntu1~20.04.2 (For technical support please see http://www.ubuntu.com/support) 
Current version of pixman: 0.38.4
        Before reporting problems, check http://wiki.x.org
        to make sure that you have the latest version.
Markers: (--) probed, (**) from config file, (==) default setting,
        (++) from command line, (!!) notice, (II) informational,
        (WW) warning, (EE) error, (NI) not implemented, (??) unknown.
(==) Log file: "/var/log/Xorg.0.log", Time: Sun Mar 13 17:09:39 2022
(==) Using config file: "/etc/X11/xorg.conf"
(==) Using system config directory "/usr/share/X11/xorg.conf.d"
(EE) 
Fatal server error:
(EE) Cannot run in framebuffer mode. Please specify busIDs        for all framebuffer devices
(EE) 
(EE) 
Please consult the The X.Org Foundation support 
         at http://wiki.x.org
 for help. 
(EE) Please also check the log file at "/var/log/Xorg.0.log" for additional information.
(EE) 
(EE) Server terminated with error (1). Closing log file.

dmesgを見るとこんなログが出ている。

NVRM: API mismatch: the client has the version 470.57.02, but
NVRM: this kernel module has the version 470.103.01.  Please
NVRM: make sure that this kernel module

ホスト側とコンテナ側でドライバのバージョンがずれてるだけなので、コンテナをビルドしなおせばOK。