作成: 2026-04-27
最終更新: 2026-04-30(temperature source と気象情報 source を含む仕様へ整理)
対象機能: 温湿度センサー(Inkbird IBS-M2S-B)によるモニタリング・積算温度管理
実装状況: 第3段階完了 / 第4段階運用確認中 / 第5段階表示系実装進行
関連イシュー: keinasystem#46、inkbird#4
VPS 上の Windmill が Inkbird IBS-M2S-B から温湿度データを定期取得し、keinasystem に蓄積する。
単なるリアルタイムモニターではなく、浸種・育苗などの農業プロセスにおける温度管理と積算温度追跡が主目的。
センサーは3台を流用・移動して使うため、「チャネル番号」ではなく「計測セッション」を軸にデータを管理する。
| IN(実装対象) | OUT(対象外) |
|---|---|
| 計測セッションの作成・開始・終了 | アラート通知(メール・プッシュ) |
| 温湿度の定期取得・蓄積(デルタ圧縮あり) | Inkbird Cloud 経由の取得 |
| リアルタイム最新値表示 + Inkbird 風履歴グラフ | 複数セッションの横断グラフ比較 |
| セッション別時系列グラフ | 品種別標準積算温度のマスタ管理 |
| 積算温度(GDD)の計算・表示 | |
| 温度 source の選択(Inkbird温湿度計 / Open-Meteo / 気象庁 など) |
Windmill(VPS上、2分ごと定期実行)
└→ scripts/inkbird_local_fetch.py(WireGuard経由でIBS-M2S-Bに接続)
└→ POST /api/sensor/readings/(X-API-Key認証、気象データと共用)
└→ keinasystem Backend(Django)
└→ デルタ圧縮判定 → PostgreSQL に保存
└→ Frontend(Next.js)
├── 最新値表示 + 直近履歴グラフ
└── セッション別グラフ・積算温度
VPS (10.0.0.1 / 162.43.33.56)
└─ WireGuard ─→ Raspberry Pi (10.0.0.2 / 192.168.68.141)
└─ IP Forward ─→ IBS-M2S-B (192.168.68.139)
TUYA_TIMEOUT で変更可)TUYA_SCAN_SUBNET(デフォルト 192.168.68.0/24)TUYA_DEVICE_IP 固定ではなく、TUYA_SCAN_SUBNET を使った TCP:6668 スキャンで機器 IP を毎回解決するTUYA_DEVICE_IP は診断用フロー (inkbird_fetch_log_only) での切り分け用途に限定し、本番運用条件には含めないwindmill.keinafarm.net, workspace admins) からの手動実行で f/sensor/inkbird_fetch_and_post が成功TUYA_SCAN_SUBNET を使うスキャン経由で device_ip = 192.168.68.139 を自動解決POST /api/sensor/readings/ への送信まで通り、post_result = {"saved": 3, "skipped": 0} を確認IBS-M2S-B は既知症状として updatedps() の DPS 102 を返さない回があり、その回は一時的に取得失敗となる2分ごと schedule で運用観測を進め、第5段階では /sensor 画面へ Inkbird 風の温度・湿度履歴グラフを追加済み~/develop/inkbird の GUI モニターが保持している SQLite 履歴 out/history.db を、keinasystem 側の SensorRecord に取り込めるようにするpython backend/manage.py import_inkbird_history で行う/home/akira/develop/inkbird/out/history.dbchannel + recorded_at の既存レコードは重複取り込みしないrecorded_at が既存 SensorSession の期間に入る場合は、その session を自動付与する--from-ts / --to-ts、件数確認のみなら --dry-run を使うDjango アプリ: backend/apps/sensor/
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| name | varchar(200) | required | 例「浸種 2026年」「育苗 第1ハウス 2026年」 |
| purpose | varchar(20) | required | soaking(浸種)/ seedling(育苗)/ other(その他) |
| source_key | varchar(100) | required | 使用する温度 source。例 inkbird:ch1, weather:openmeteo:kubokawa_daily |
| base_temp | float | default=10.0 | 積算温度の基準温度(℃)。手動設定 |
| start_at | datetime | required | 計測開始日時 |
| end_at | datetime | nullable | 計測終了日時。null = 計測中 |
| note | text | blank | メモ(source 変更履歴など) |
| created_at | datetime | auto |
source_key は source provider を解決するための識別子であり、少なくとも以下を想定する。
inkbird:ch1inkbird:ch2inkbird:ch3weather:openmeteo:kubokawa_daily同一 source を複数セッションで参照することは許可する。
ただし、誤操作対策の主眼は「重複検出」ではなく「source 選択時の誤認防止」に置く。
source 変更時: session.source_key を更新し note に変更理由・日時を記録する。
purpose の表示名
| 値 | 表示 |
|---|---|
soaking |
浸種 |
seedling |
育苗 |
other |
その他 |
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| session | FK(SensorSession) | PROTECT, nullable | センサー source を使う既存運用との互換のため残置。将来的には参照用途を縮小可 |
| recorded_at | datetime | required | 記録日時(UTC) |
| channel | int | required | 記録時点のチャネル番号(セッション変更後も履歴保全) |
| temp_c | float | required | 温度(℃) |
| humidity_pct | float | required | 湿度(%) |
インデックス: (session_id, recorded_at) / (channel, recorded_at)
補足:
SensorRecord は Inkbird 系の raw 保存先であるapps.weather 側の WeatherRecord とするsession.source_key に応じて SensorRecord または WeatherRecord を provider 経由で参照して構成するサーバーストレージ節約のため、keinasystem 側でフィルタリングを行う。
Windmill は毎回全チャネルの生データを投げるだけ(圧縮ロジックを持たない)。
よって、取得間隔を短くしても保存爆発を防ぐ責務は backend 側にある。
保存条件(いずれかを満たす場合に保存):
|新温度 - 最後の保存温度| >= 1.0 ℃|新湿度 - 最後の保存湿度| >= 5.0 %このため、たとえば Windmill を 1 分間隔で実行しても、値変化が小さい大半の回は skipped となり、毎回保存はされない。
「最後の保存値」は Inkbird raw 側の最終保存値 を基準とする。
このデルタ圧縮は POST /api/sensor/readings/ に入る Inkbird 温湿度データ専用の処理であり、比較基準はチャネル単位の最終レコードとする。
積算温度(GDD)の計算
GDD = Σ max(0, temp_c - base_temp) × duration_hours
duration_hours = 次のレコードの recorded_at - 現在のレコードの recorded_at(時間単位)end_at(または現在時刻)までは最後の温度が継続したと見なす/api/sensor/)/api/sensor/readings/Windmill からのデータ受信。デルタ圧縮を適用して保存。
MAIL_API_KEY 設定値、気象データと共用){
"fetched_at": "2026-04-27T10:00:00Z",
"channels": [
{"ch": 1, "temp_c": 19.2, "humidity_pct": 75.6},
{"ch": 2, "temp_c": 21.1, "humidity_pct": 74.7},
{"ch": 3, "temp_c": 18.7, "humidity_pct": 69.9}
]
}
{"saved": 2, "skipped": 1}
saved: デルタ圧縮を通過して保存したレコード数
skipped: 変化が閾値未満でスキップしたレコード数
/api/sensor/latest/全チャネルの最新値を返す。リアルタイムモニター用。
{
"fetched_at": "2026-04-27T10:00:00Z",
"channels": [
{
"ch": 1,
"temp_c": 19.2,
"humidity_pct": 75.6,
"recorded_at": "2026-04-27T09:58:00Z",
"session": {"id": 3, "name": "浸種 2026年", "purpose": "soaking"}
},
{
"ch": 2,
"temp_c": 21.1,
"humidity_pct": 74.7,
"recorded_at": "2026-04-27T09:58:00Z",
"session": null
},
{
"ch": 3,
"temp_c": 18.7,
"humidity_pct": 69.9,
"recorded_at": "2026-04-27T09:58:00Z",
"session": {"id": 4, "name": "育苗 第1ハウス 2026年", "purpose": "seedling"}
}
]
}
/api/sensor/history/ダッシュボード用の全チャネル履歴を返す。Inkbird プロジェクトと同系統の温度・湿度 2 段グラフに使用する。
?hours=1 / ?hours=6 / ?hours=24 / ?hours=168{
"hours": 24,
"since": "2026-04-28T10:00:00Z",
"channels": [
{
"ch": 1,
"label": "CH1",
"points": [
{"recorded_at": "2026-04-29T08:00:00Z", "temp_c": 19.2, "humidity_pct": 75.6}
]
},
{
"ch": 2,
"label": "CH2",
"points": [
{"recorded_at": "2026-04-29T08:00:00Z", "temp_c": 21.1, "humidity_pct": 74.7}
]
}
]
}
/api/sensor/sessions/セッション一覧。
?active=true(計測中のみ)/ ?purpose=soakingstart_at 降順)[
{
"id": 3,
"name": "浸種 2026年",
"purpose": "soaking",
"source_key": "inkbird:ch1",
"source_label": "CH1",
"source_type": "sensor",
"base_temp": 10.0,
"start_at": "2026-04-25T08:00:00Z",
"end_at": null,
"note": "",
"gdd": 45.2,
"latest_temp_c": 19.2,
"latest_recorded_at": "2026-04-27T09:58:00Z"
}
]
gdd: セッション開始から現在(または end_at)までの積算温度(オンザフライ計算)
latest_temp_c / latest_recorded_at: 最新レコードの値
/api/sensor/sessions/セッション作成・開始。
{
"name": "浸種 2026年",
"purpose": "soaking",
"source_key": "weather:openmeteo:kubokawa_daily",
"base_temp": 10.0,
"start_at": "2026-04-25T08:00:00Z",
"note": ""
}
source_key が未登録または無効な場合 → 400 Bad Requestsource_key を使用中のアクティブセッションが存在しても、作成自体は許可するsource_key に対応する provider を解決し、start_at 以降の時系列データを provider 経由で参照して、その期間のグラフと GDD を構成するdata_kind=forecast を正式な積算温度に使うかどうかは source ごとの運用ルールに従う/api/sensor/sessions/{id}/セッション更新(終了・source 変更など)。
end_at を設定してセッション終了、source_key 変更、note 追記source_key が未登録または無効な場合は 400 Bad Requestend_at が現在より前の場合、その時刻より後の raw データはセッション計算対象から除外する。raw 自体は source 側保存先に残す。/api/sensor/sessions/{id}/records/セッションの時系列レコード取得。グラフ描画用。
?start=2026-04-25T00:00:00Z&end=2026-04-27T23:59:59Z{
"session": {
"id": 3,
"name": "浸種 2026年",
"base_temp": 10.0,
"start_at": "2026-04-25T08:00:00Z",
"end_at": null,
"gdd": 45.2
},
"records": [
{"recorded_at": "2026-04-25T08:00:00Z", "temp_c": 15.0, "humidity_pct": 80.0, "cumulative_gdd": 0.0},
{"recorded_at": "2026-04-25T09:30:00Z", "temp_c": 16.2, "humidity_pct": 79.0, "cumulative_gdd": 7.5},
...
]
}
cumulative_gdd: その時点までの累積 GDD
フロントエンド: frontend/src/app/sensor/
/sensor → メイン画面(最新値 + Inkbird 風履歴グラフ + アクティブセッション一覧)
/sensor/sessions → セッション一覧
/sensor/sessions/new → セッション作成
/sensor/sessions/[id] → セッション詳細(グラフ・積算温度)
/sensor — メイン画面╔══════════════════════════════════════════════════════════════╗
║ 温湿度モニター 最終取得 10:00:12 ║
╠══════════════════════════════════════════════════════════════╣
║ サマリー ║
║ 稼働中セッション / 割当済みチャネル / 最新レコード時刻 ║
╠══════════════════════════════════════════════════════════════╣
║ [ 1h ] [ 6h ] [ 24h ] [ 7d ] [ 手動更新 ] ║
║ ║
║ 温度 (°C) ║
║ CH1 / CH2 / CH3 の重ね描き折れ線 ║
║ ║
║ 湿度 (%) ║
║ CH1 / CH2 / CH3 の重ね描き折れ線 ║
╠══════════════════════════════════════════════════════════════╣
║ 現在の計測値 ║
║ ║
║ ┌──────────────────┐ ┌──────────────────┐ ┌────────────┐ ║
║ │ CH1 浸種 2026年 │ │ CH2 (未割当) │ │ CH3 育苗 │ ║
║ │ 19.2 °C │ │ 21.1 °C │ │ 18.7 °C │ ║
║ │ 75.6 % │ │ 74.7 % │ │ 69.9 % │ ║
║ │ 10:00 取得 │ │ 10:00 取得 │ │ 10:00 取得│ ║
║ └──────────────────┘ └──────────────────┘ └────────────┘ ║
║ ║
║ ※ 取得未反映チャネルがある場合は警告表示 ║
╠══════════════════════════════════════════════════════════════╣
║ アクティブセッション [ + 新しいセッション ] ║
║ ║
║ ┌─────────────────────────────────────────────────────────┐ ║
║ │ 浸種 2026年 CH1 [詳細 →] │ ║
║ │ 開始: 2026-04-25 08:00 最新記録: 4/29 10:00 │ ║
║ │ 積算温度: 45.2 ℃・日 基準温度: 10℃ │ ║
║ └─────────────────────────────────────────────────────────┘ ║
║ ┌─────────────────────────────────────────────────────────┐ ║
║ │ 育苗 第1ハウス 2026年 CH3 [詳細 →] │ ║
║ │ 開始: 2026-04-26 06:00 最新記録: 4/29 10:00 │ ║
║ │ 積算温度: 12.8 ℃・日 基準温度: 10℃ │ ║
║ └─────────────────────────────────────────────────────────┘ ║
╚══════════════════════════════════════════════════════════════╝
1h / 6h / 24h / 7d/sensor/sessions/new — セッション作成╔══════════════════════════════════════════════════════════════╗
║ 新しい計測セッション ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ セッション名 [ 浸種 2026年 ] ║
║ タイプ ( Inkbird温湿度計 ) ( Open-Meteo ) ( 気象庁 ) ║
║ ソース [ センサー候補A / 窪川日次 / ... ] ║
║ 用途 ( 浸種 ) ( 育苗 ) ( その他 ) ║
║ 基準温度 [ 10.0 ] ℃ ║
║ 開始日時 [ 2026-04-27 08:00 ] (デフォルト: 今) ║
║ メモ [ ] ║
║ ║
║ [ キャンセル ] [ 開始する ] ║
╚══════════════════════════════════════════════════════════════╝
タイプ と ソース を選択するタイプ は利用者向けの provider 表示とし、Inkbird温湿度計、Open-Meteo、気象庁 などを出すInkbird温湿度計 選択時のソース候補には、その時点で取得中のセンサー候補を出すOpen-Meteo / 気象庁 選択時のソース候補には、Windmill により取得・保存済みの気象 source を出す最新値または代表値、取得時刻、観測地点または対象、データ種別(実測/過去実績/予報)、湿度有無 を表示する気象情報 を選んだ場合は、観測地点・データ種別(実測/過去実績/予報)・湿度有無を補足表示する/sensor/sessions/[id] — セッション詳細╔══════════════════════════════════════════════════════════════╗
║ 浸種 2026年 CH1 開始: 2026-04-25 08:00 ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ 積算温度(基準 10℃) ║
║ ╔══════════════════════════════════════════════════════╗ ║
║ ║ 45.2 ℃・日 ║ ║
║ ║ 経過日数: 2.1日 現在温度: 19.2℃ ║ ║
║ ╚══════════════════════════════════════════════════════╝ ║
║ ║
╠══════════════════════════════════════════════════════════════╣
║ [ 24h ] [ 3d ] [ 全期間 ] ║
║ ║
║ 温度 (°C) ║
║ 25 ┤ ║
║ 20 ┤ ╭──────────╮ ╭──────── ║
║ 15 ┤─────╯ ╰──╯ ║
║ 10 ┤ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─(基準温度 10℃) ║
║ └────────────────────────────────────────────── ║
║ ║
║ 湿度 (%) ║
║ 90 ┤────────────────────────────────────────── ║
║ 80 ┤ ║
║ └────────────────────────────────────────────── ║
║ ║
╠══════════════════════════════════════════════════════════════╣
║ [ セッションを終了する ] [ 設定を編集 ] ║
╚══════════════════════════════════════════════════════════════╝
end_at を現在時刻に設定/sensor/sessions — セッション一覧inkbird_fetch_and_postスケジュール: 2分ごと(VPS 上 Windmill の本番運用値)
処理内容:
# 1. inkbird_local_fetch.fetch_once() を実行
channels = fetch_once() # [{"ch":1,"temp_c":19.2,"humidity_pct":75.6}, ...]
# 2. keinasystem API に POST
import httpx, os, datetime
payload = {
"fetched_at": datetime.datetime.utcnow().isoformat() + "Z",
"channels": channels
}
httpx.post(
f"{os.environ['KEINASYSTEM_BASE_URL']}/api/sensor/readings/",
json=payload,
headers={"X-API-Key": os.environ["KEINASYSTEM_API_KEY"]},
timeout=30
)
必要な環境変数:
| 変数名 | 説明 |
|---|---|
KEINASYSTEM_BASE_URL |
例 https://keinafarm.net |
KEINASYSTEM_API_KEY |
Windmill 側で使う API キー変数。値は backend の MAIL_API_KEY と同一 |
TUYA_REGION |
Tuya リージョン |
TUYA_API_KEY |
Tuya API キー |
TUYA_API_SECRET |
Tuya API シークレット |
TUYA_API_DEVICE_ID |
デバイス ID |
TUYA_GATEWAY_DEVICE_ID |
ゲートウェイデバイス ID |
TUYA_SCAN_SUBNET |
デフォルト 192.168.68.0/24 |
TUYA_TIMEOUT |
デフォルト 15(秒) |
補足:
MAIL_API_KEY を参照するが、Windmill 側変数名は KEINASYSTEM_API_KEY でよいTUYA_DEVICE_IP を必須にしないTUYA_DEVICE_IP は診断用フローでだけ使用してよいDPS 102 が載らない回は既知症状として記録し、後続回で回復する前提で観測する2分ごと 実行でもストレージ増加を緩和できるDjango アプリ作成: backend/apps/sensor/
models.py: SensorSession, SensorRecordmakemigrations → migrateadmin.py 登録API 実装:
POST /api/sensor/readings/(デルタ圧縮ロジック込み)GET /api/sensor/latest/GET /api/sensor/history/GET/POST /api/sensor/sessions/PATCH /api/sensor/sessions/{id}/GET /api/sensor/sessions/{id}/records/Windmill フロー作成: inkbird_fetch_and_post
フロントエンド実装:
/sensor メイン画面(最新値 + 履歴グラフ + 自動更新)/sensor/sessions/new セッション作成/sensor/sessions/[id] セッション詳細(グラフ + GDD)/sensor/sessions 一覧ナビゲーション追加: サイドバーに「温湿度」を追加
この節は、現行仕様を支える設計原則と、今後 source 種別が増えたときの判断基準をまとめた補足である。実装者向けの詳細メモなので、本体仕様とは分けて後置する。
温度データの取得元を表す概念として temperature source を置く。Inkbird の各チャネルも、気象情報も、将来の別センサーもこの枠で扱う。
想定フィールド:
| フィールド | 型 | 説明 |
|---|---|---|
| id | int | PK |
| source_type | varchar(30) | 内部分類。inkbird_channel / weather_openmeteo / weather_jma / other_sensor など |
| source_key | varchar(100) | 例 inkbird:ch1, weather:openmeteo:kubokawa_daily |
| display_name | varchar(200) | 画面表示名 |
| location_label | varchar(200) | 設置場所や観測地点名 |
| temp_unit | varchar(10) | 当面 celsius 固定想定 |
| humidity_supported | bool | 湿度取得可否 |
| is_active | bool | 利用中か |
重要なのは、すべての温度 source を1つの同型データとして無理に押し込めないことと、利用者側から見える共通の振る舞いだけを抽象化することである。
特に大事なのは、本当に共通化が必要なのはデータ構造ではなく呼び出し口であるという点である。
Inkbird rawOpen-Meteo raw気象庁 rawはそれぞれ別の構造・粒度・意味を持っていてよい。
利用者側にとって重要なのは、
という操作の契約であり、内部で同じ配列構造へ正規化しているかどうかではない。
は、それぞれ raw データの構造、粒度、意味が異なる。
そのため、利用者側(セッション管理、GDD 計算、グラフ表示)は raw データを直接扱わず、source type ごとの中間層が提供する共通の呼び出し口を使う前提にする。
ここでいう中間層は、温度の実体を保持する新しい保存テーブルを意味しない。役割は以下に限定する。
したがって、中間層が観測値を二重保存することは原則避ける。
利用者側が要求したいのは、たとえば次のような内容である。
したがって、利用者側は Inkbird温湿度計 や Open-Meteo や 気象庁 の違いに依存するのではなく、温度系列を供給する provider / adapter の共通 interface に依存する構造を目指す。
利用者から見える共通の呼び出し口として、少なくとも次の操作をそろえる。
| 操作 | 説明 |
|---|---|
get_gdd(start_at, end_at, base_temp) |
指定期間の積算温度を返す |
get_graph_series(start_at, end_at) |
グラフ描画用データを返す |
get_source_metadata() |
表示名、種別、地点、粒度などを返す |
利用者側はこの呼び出し口だけに依存し、source ごとの raw 構造や計算方法の違いを意識しない。
中間層の内部実装として、raw をいったん共通観測形式へ寄せてから GDD を計算してもよい。
ただしそれは内部実装上の都合であり、利用者向けに固定すべき契約ではない。
つまり、
呼び出し口内部で使うかもしれない中間表現である。
本機能で扱う raw データは、source 種別ごとにそれぞれ既存の保存先を持つ前提とする。
apps.sensor 側の温湿度記録を raw 保存先とするapps.weather 系レコードを raw 保存先とするdocs/12_マスタードキュメント_気象データ編.mdつまり、気象情報 source をセッションで使う場合も、毎回外部 API を直接呼びに行く前提ではない。原則として、既存の DB 保存済みデータを provider / adapter 経由で読む。
OOP 上は共通 interface で扱えても、DB 上は必ずしも1テーブルに統合する必要はない。
のどちらでもよい。重要なのは、保存形よりも、利用者側にどう見せるかを先に固定することである。
apps.sensor と apps.weather のどのモデル/クエリを source provider の正式入力面にするか/home/akira/develop/inkbird/docs/IBS-M2S-B_マスタードキュメント_06_GUIモニター.md/home/akira/develop/inkbird/docs/IBS-M2S-B_マスタードキュメント_03_Local取得_DPS102.mdhttps://gitea.keinafarm.net/akira/inkbird/issues/4docs/12_マスタードキュメント_気象データ編.mdhttps://gitea.keinafarm.net/akira/keinasystem/issues/46