1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

気温や湿度のグラフをインタラクティブに作成する

1
Posted at

前回の生成AIを使った自然言語で問い合わせをするというところで、ollamaをインストールして使ったのですが、TimescaleDBやGrafana、Metabaseに合わせてコンテナ上で動作するようにします。

ラズパイ上で動作しているollamaを停止して、起動しないようにします。

sudo systemctl stop ollama
sudo systemctl disable ollama

Ollamaのステータス確認

sudo systemctl status ollama
起動中
● ollama.service - Ollama Service
     Loaded: loaded (/etc/systemd/system/ollama.service; enabled; preset: enabled)
     Active: active (running) since Sun 2026-02-08 10:54:06 JST; 1 day 19h ago
 Invocation: b5c9d1e01ccf48779c419d5e053fe244
   Main PID: 1767 (ollama)
      Tasks: 9 (limit: 19362)
        CPU: 96ms
     CGroup: /system.slice/ollama.service
             └─1767 /usr/local/bin/ollama serve

Feb 08 10:54:06 raspi5 systemd[1]: Started ollama.service - Ollama Service.
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.371+09:00 level=INFO source=routes.go:1636 msg="server config" env="map[CUDA_VISIBLE_DEVICES: GGML_VK_VISIBLE_D>
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=images.go:473 msg="total blobs: 11"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=images.go:480 msg="total unused blobs removed: 0"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=routes.go:1689 msg="Listening on 127.0.0.1:11434 (version 0.15.5)"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.377+09:00 level=INFO source=runner.go:67 msg="discovering available GPUs..."
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.378+09:00 level=INFO source=server.go:430 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engi>
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.798+09:00 level=INFO source=server.go:430 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engi>
Feb 08 10:54:08 raspi5 ollama[1767]: time=2026-02-08T10:54:08.182+09:00 level=INFO source=types.go:60 msg="inference compute" id=cpu library=cpu compute="" name=cpu descript>
Feb 08 10:54:08 raspi5 ollama[1767]: time=2026-02-08T10:54:08.182+09:00 level=INFO source=routes.go:1739 msg="vram-based default context" total_vram="0 B" default_num_ctx=40>
停止後
○ ollama.service - Ollama Service
     Loaded: loaded (/etc/systemd/system/ollama.service; disabled; preset: enabled)
     Active: inactive (dead)

Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=images.go:480 msg="total unused blobs removed: 0"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.375+09:00 level=INFO source=routes.go:1689 msg="Listening on 127.0.0.1:11434 (version 0.15.5)"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.377+09:00 level=INFO source=runner.go:67 msg="discovering available GPUs..."
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.378+09:00 level=INFO source=server.go:430 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 40417"
Feb 08 10:54:07 raspi5 ollama[1767]: time=2026-02-08T10:54:07.798+09:00 level=INFO source=server.go:430 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 36165"
Feb 08 10:54:08 raspi5 ollama[1767]: time=2026-02-08T10:54:08.182+09:00 level=INFO source=types.go:60 msg="inference compute" id=cpu library=cpu compute="" name=cpu description=cpu libdirs=ollama driver="" pci_id=">
Feb 08 10:54:08 raspi5 ollama[1767]: time=2026-02-08T10:54:08.182+09:00 level=INFO source=routes.go:1739 msg="vram-based default context" total_vram="0 B" default_num_ctx=4096
Feb 10 06:31:23 raspi5 systemd[1]: Stopping ollama.service - Ollama Service...
Feb 10 06:31:23 raspi5 systemd[1]: ollama.service: Deactivated successfully.
Feb 10 06:31:23 raspi5 systemd[1]: Stopped ollama.service - Ollama Service.

本題に入ります

Webアプリの作成

Webアプリ用のディレクトリを作成します。

mkdir -p ~/iot-factory/ai_app
cd ~/iot-factory/ai_app

ライブラリの一覧

requirements.txtにライブラリの一覧を書きます

vi requirements.txt
requirements.txt
streamlit
pandas
psycopg2-binary
ollama

Dokcerfileの作成

vi Dockerfile
Dockerfile
FROM python:3.11-slim

WORKDIR /app

# Postgres用ライブラリとcurl(ヘルスチェック用)
RUN apt-get update && apt-get install -y \
    libpq-dev gcc curl \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Streamlitのポート
EXPOSE 8501

CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]

動作するWebアプリを作成

ここはGeminiに頑張ってもらいました。w

vi app.py
app.py
import streamlit as st
import ollama
import psycopg2
import pandas as pd
import re
import os

# --- 設定エリア ---
# DB接続設定
DB_CONFIG = {
    "host": "timescaledb",  # Dockerのサービス名
    "database": "factory_iot",
    "user": "postgres",
    "password": "password1234",
    "port": "5432"
}

# Ollamaクライアントの初期化
# コンテナ間通信のため、ホスト名を指定したクライアントを作成します
ollama_host = os.getenv("OLLAMA_HOST", "http://ollama:11434")
client = ollama.Client(host=ollama_host) 

# モデル設定
SQL_MODEL = "qwen2.5-coder:3b"
REPORT_MODEL = "llama3.2:3b"

SCHEMA_INFO = """
テーブル名: mqtt_consumer
カラム:
- time (TIMESTAMPTZ): 計測時刻
- company_name(TEXT):会社名
- office_name(TEXT):事業所名
- factory_name(TEXT):工場名
- line_name(TEXT):ライン名
- equipment_name(TEXT):機器名
- temperature (FLOAT): 気温
- pressure (FLOAT): 気圧
- humidity (FLOAT): 湿度
- noise (FLOAT): 騒音
- light (FLOAT): 照度
- etvoc (FLOAT): 二酸化炭素
- eco2 (INT): CO2濃度
"""

# --- 関数定義 ---
def get_sql_from_ai(question):
    prompt = f"""
    You are a PostgreSQL expert. Convert the question to a SQL query.
    - Return ONLY the SQL. No markdown.
    - Use 'sensor_data' table.
    Schema: {SCHEMA_INFO}
    Question: {question}
    """
    try:
        # ★ここを修正: ollama.chat() ではなく client.chat() を使う
        response = client.chat(model=SQL_MODEL, messages=[{'role': 'user', 'content': prompt}])
        sql = response['message']['content'].strip()
        sql = re.sub(r'```sql\n?', '', sql)
        sql = re.sub(r'```', '', sql)
        return sql
    except Exception as e:
        return f"Error: {e}"

def execute_query_to_df(sql):
    conn = None
    try:
        conn = psycopg2.connect(**DB_CONFIG)
        df = pd.read_sql(sql, conn)
        return df, None
    except Exception as e:
        return None, str(e)
    finally:
        if conn: conn.close()

def generate_report(question, df):
    # データが空の場合のガード処理を追加
    if df is None or df.empty:
        return "データがないためレポートを作成できません。"

    summary = df.describe().to_string()
    prompt = f"""
    Answer the user's question based on the data summary in Japanese.
    Question: {question}
    Data Summary: {summary}
    """
    try:
        # ★ここを修正: ollama.chat() ではなく client.chat() を使う
        response = client.chat(model=REPORT_MODEL, messages=[{'role': 'user', 'content': prompt}])
        return response['message']['content']
    except Exception as e:
        return f"Error: {e}"

# --- 画面構築 ---
st.set_page_config(page_title="Environment AI Dashboard", layout="wide")
st.title("📊 環境データ AI分析ダッシュボード")

if prompt := st.chat_input("質問を入力 (例: 昨日の気温の変化をグラフで見せて)"):
    st.chat_message("user").markdown(prompt)
    
    with st.chat_message("assistant"):
        with st.spinner("AIが考え中..."):
            sql = get_sql_from_ai(prompt)
            st.code(sql, language="sql")
            
            if "Error" in sql:
                st.error(sql)
            else:
                df, error = execute_query_to_df(sql)
                if error:
                    st.error(f"SQLエラー: {error}")
                elif df is None or df.empty:
                    st.warning("データなし")
                else:
                    if 'time' in df.columns:
                        df['time'] = pd.to_datetime(df['time'])
                        df = df.set_index('time')
                        st.line_chart(df)
                    else:
                        st.dataframe(df)
                    
                    report = generate_report(prompt, df)
                    st.markdown(report)

docker-compose.ymlの修正

前回使っていたdocker-compose.ymlを修正します。

cd ~/iot-factory/
vi docker-compose.yml
docker-compose.yml
services:
  # --- 1. MQTT Broker ---
  mosquitto:
    image: eclipse-mosquitto:latest
    container_name: mosquitto
    restart: always
    ports:
      - "1883:1883"
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ./mosquitto/data:/mosquitto/data
      - ./mosquitto/log:/mosquitto/log

  # --- 2. Database (TimescaleDB / PostgreSQL) ---
  timescaledb:
    image: timescale/timescaledb:latest-pg16
    container_name: timescaledb
    restart: always
    environment:
      - POSTGRES_PASSWORD=password1234
      - POSTGRES_USER=postgres
      - POSTGRES_DB=factory_iot
    ports:
      - "5432:5432"
    volumes:
      - ./timescaledb:/var/lib/postgresql/data
    # ヘルスチェックを追加(他のコンテナが待てるようにするため)
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  # --- 3. Data Collector (Telegraf) ---
  telegraf:
    image: telegraf:latest
    container_name: telegraf
    restart: always
    depends_on:
      timescaledb:
        condition: service_healthy
      mosquitto:
        condition: service_started
    volumes:
      - ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro
    environment:
      - POSTGRES_PASSWORD=password1234

  # --- 4. Visualization A (Grafana) ---
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: always
    ports:
      - "3000:3000"
    volumes:
      - ./grafana:/var/lib/grafana

  # --- 5. Visualization B (Metabase) ---
  metabase:
    image: metabase/metabase:latest
    container_name: metabase
    restart: always
    ports:
      - "3001:3000"
    volumes:
      - ./metabase:/metabase-data
    environment:
      - MB_DB_FILE=/metabase-data/metabase.db

  # --- 6. AI Engine (Ollama) [NEW] ---
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: always
    ports:
      - "11434:11434" # ホストからもAPIを叩けるように公開
    volumes:
      - ./ollama_data:/root/.ollama # モデルデータを永続化

  # --- 7. AI Dashboard (Streamlit + Pandas) [NEW] ---
  ai_dashboard:
    build: ./ai_app # ai_appフォルダのDockerfileを使ってビルド
    container_name: ai_dashboard
    restart: always
    ports:
      - "8501:8501" # ブラウザでアクセスするポート
    volumes:
      - ./ai_app:/app # コード変更を即反映させる
    environment:
      # コンテナ内からOllamaへの接続先を指定
      - OLLAMA_HOST=http://ollama:11434
    depends_on:
      timescaledb:
        condition: service_healthy
      ollama:
        condition: service_started

ビルドして起動

docker compose up -d --build
実行結果
[+] up 6/6
 ✔ Image ollama/ollama:latest Pulled                                                                                                                                                                              59.5s
[+] Building 81.6s (13/13) FINISHED                                                                                                                                                                                    
 => [internal] load local bake definitions                                                                                                                                                                        0.0s
 => => reading from stdin 546B                                                                                                                                                                                    0.0s
 => [internal] load build definition from Dockerfile                                                                                                                                                              0.1s
 => => transferring dockerfile: 450B                                                                                                                                                                              0.0s
 => [internal] load metadata for docker.io/library/python:3.11-slim                                                                                                                                               3.6s
 => [internal] load .dockerignore                                                                                                                                                                                 0.0s
 => => transferring context: 2B                                                                                                                                                                                   0.0s
 => [internal] load build context                                                                                                                                                                                 0.1s
 => => transferring context: 4.51kB                                                                                                                                                                               0.0s
 => [1/6] FROM docker.io/library/python:3.11-slim@sha256:0b23cfb7425d065008b778022a17b1551c82f8b4866ee5a7a200084b7e2eafbf                                                                                         2.9s
 => => resolve docker.io/library/python:3.11-slim@sha256:0b23cfb7425d065008b778022a17b1551c82f8b4866ee5a7a200084b7e2eafbf                                                                                         0.1s
 => => sha256:93304c4769c26f8efc1479b815e3d84a136217013671ad87e5fa98da27cea089 251B / 251B                                                                                                                        0.4s
 => => sha256:e9e50e44f49d8e67d2fb6488786b2879eeb473e5ea7f238fc2655b7154abde80 14.31MB / 14.31MB                                                                                                                  1.2s
 => => sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 30.14MB / 30.14MB                                                                                                                  1.5s
 => => sha256:05c5a241432232a0a200b8395f09c5da514ed2d1d0fff93fb57bbd229f308114 1.27MB / 1.27MB                                                                                                                    1.2s
 => => extracting sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c                                                                                                                         0.7s
 => => extracting sha256:05c5a241432232a0a200b8395f09c5da514ed2d1d0fff93fb57bbd229f308114                                                                                                                         0.1s
 => => extracting sha256:e9e50e44f49d8e67d2fb6488786b2879eeb473e5ea7f238fc2655b7154abde80                                                                                                                         0.4s
 => => extracting sha256:93304c4769c26f8efc1479b815e3d84a136217013671ad87e5fa98da27cea089                                                                                                                         0.0s
 => [2/6] WORKDIR /app                                                                                                                                                                                            3.7s
 => [3/6] RUN apt-get update && apt-get install -y     libpq-dev gcc curl     && rm -rf /var/lib/apt/lists/*                                                                                                     18.5s
 => [4/6] COPY requirements.txt .                                                                                                                                                                                 0.1s 
 => [5/6] RUN pip install --no-cache-dir -r requirements.txt                                                                                                                                                     28.4s 
 => [6/6] COPY . .                                                                                                                                                                                                0.1s 
 => exporting to image                                                                                                                                                                                           23.9s 
 => => exporting layers                                                                                                                                                                                          19.6s 
 => => exporting manifest sha256:75c0200afa75a5c42882110398097fdac36539b9264cbca3b456fbbcc7be7a5d                                                                                                                 0.0s 
 => => exporting config sha256:ad97c4d7c9910c874f505c02414714d2716874f2227d6e3cf11647956dd7c64b                                                                                                                   0.0s 
 => => exporting attestation manifest sha256:2ce78ef2b61fdfda03d46ca5bd36a08da031a5cbfe5d1c883a8b8a3fe861e6d4                                                                                                     0.0s 
 => => exporting manifest list sha256:50732867d6d5df12c9f0c4ce6941ab3418844d8bfbfe484364663951a832fdcd                                                                                                            0.0s
 => => naming to docker.io/library/iot-factory-ai_dashboard:latest                                                                                                                                                0.0s
[+] up 14/14king to docker.io/library/iot-factory-ai_dashboard:latest                                                                                                                                             4.1s
 ✔ Image ollama/ollama:latest     Pulled                                                                                                                                                                          59.5s
 ✔ Image iot-factory-ai_dashboard Built                                                                                                                                                                           81.6s
 ✔ Container grafana              Running                                                                                                                                                                         0.0s
 ✔ Container metabase             Running                                                                                                                                                                         0.0s
 ✔ Container ollama               Created                                                                                                                                                                         2.2s
 ✔ Container timescaledb          Healthy                                                                                                                                                                         13.4s
 ✔ Container mosquitto            Running                                                                                                                                                                         0.0s
 ✔ Container telegraf             Running                                                                                                                                                                         0.0s
 ✔ Container ai_dashboard         Created         

状態確認

docker compose ps
NAME           IMAGE                               COMMAND                  SERVICE        CREATED              STATUS                        PORTS
ai_dashboard   iot-factory-ai_dashboard            "streamlit run app.p…"   ai_dashboard   About a minute ago   Up About a minute             0.0.0.0:8501->8501/tcp, [::]:8501->8501/tcp
grafana        grafana/grafana:latest              "/run.sh"                grafana        2 days ago           Up 44 hours                   0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
metabase       metabase/metabase:latest            "/app/run_metabase.sh"   metabase       2 days ago           Up 44 hours                   0.0.0.0:3001->3000/tcp, [::]:3001->3000/tcp
mosquitto      eclipse-mosquitto:latest            "/docker-entrypoint.…"   mosquitto      2 days ago           Up 44 hours                   0.0.0.0:1883->1883/tcp, [::]:1883->1883/tcp
ollama         ollama/ollama:latest                "/bin/ollama serve"      ollama         About a minute ago   Up About a minute             0.0.0.0:11434->11434/tcp, [::]:11434->11434/tcp
telegraf       telegraf:latest                     "/entrypoint.sh tele…"   telegraf       2 days ago           Up 44 hours                   8092/udp, 8125/udp, 8094/tcp
timescaledb    timescale/timescaledb:latest-pg16   "docker-entrypoint.s…"   timescaledb    About a minute ago   Up About a minute (healthy)   0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp

モデルのダウンロード

Ollamaで使用するモデルをダウンロードします。
このダウンロードは初回一回のみとなります。

Ollamaコンテナに入ってコマンドを実行します。

コード生成用モデル

コード生成用モデル
docker exec -it ollama ollama pull qwen2.5-coder:3b
実行結果
pulling manifest 
pulling 4a188102020e: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.9 GB                         
pulling 66b9ea09bd5b: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏   68 B                         
pulling 1e65450c3067: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.6 KB                         
pulling 45fc3ea7579a: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 7.4 KB                         
pulling bb967eff3bda: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  487 B                         
verifying sha256 digest 
writing manifest 
success 

日本語レポート用モデル

日本語レポート用モデル
docker exec -it ollama ollama pull llama3.2:3b
実行結果
pulling manifest 
pulling dde5aa3fc5ff: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 2.0 GB                         
pulling 966de95ca8a6: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.4 KB                         
pulling fcc5a6bec9da: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 7.7 KB                         
pulling a70ff7e570d9: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 6.0 KB                         
pulling 56bb8bd477a5: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏   96 B                         
pulling 34bb5ab01051: 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  561 B                         
verifying sha256 digest 
writing manifest 
success 

実行

ブラウザで http://<ラズパイのIP>:8501 にアクセスすれば、TimescaleDBのデータを可視化できるAIダッシュボードが表示されます。

「今朝の温度をグラフで表示してください。」と聞いたら以下のような回答がきました。
時間が9時間ずれていますね。
「今朝」と指定しましたが、21時くらいの温度を表示しています。

あと、生成したSQLがエラーになることが多いのでプロンプトを工夫する必要があるかもしれません。

image.png


時刻修正

時刻を修正するために以下のようにapp.yを修正しました。

app.py抜粋
                    if 'time' in df.columns:
                        df['time'] = pd.to_datetime(df['time'])
                        # ここから
                        if df['time'].dt.tz is None:
                            df['time'] = df['time'].dt.tz_localize('UTC')
                        df['time'] = df['time'].dt.tz_convert('Asia/Tokyo').dt.tz_localize(None)
                        # ここまでを追加
                        df = df.set_index('time')
                        st.line_chart(df)

時刻は修正されて表示されました。
image.png


今回の修正でディレクトリ構成は以下のようになっています。

iot-factory/
├── docker-compose.yml      # (修正)
├── mosquitto/              # (既存)
├── grafana/                # (既存)
└── ai_app/                 # (新規作成)
    ├── Dockerfile          # 
    ├── requirements.txt    # 
    └── app.py              # 

次回は、Raspbery Pi AI Cameraでも繋げてみますか。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?