Experiment record

SwitchBot 5分Raw収集における毎時04分欠測の原因特定とScriptLock競合対策

毎時04分付近に集中していたSwitchBot 5分Raw欠測を調査し、共有ScriptLock範囲を短縮してAPI取得を保存Lockより前へ移した後、毎時04分34/34・全5分周期411/411・欠測0を確認したCompleted Experiment。

概要

AIEL-2026-0007では、毎時04分付近に欠測が集中していたSwitchBot 5分Raw収集の原因と対策を検証しました。

保持された修正前Apps Script Sourceには具体的なfailure mechanismがありました。SwitchBotPowerはSwitchBot API取得より前に共有ScriptLockを取得しようとし、20秒以内にLockを取得できない場合は観測を記録せずに終了し得る構造でした。同じApps Scriptプロジェクト内のガス側処理にも、より長い処理区間で同じ共有Lockを保持する経路がありました。

そこで、SwitchBot API取得を保存Lockより前へ移し、API取得時刻を観測Timestampとして固定し、Lockは短い書込み部分だけに保持する構造へ変更しました。ガス側も処理全体で共有Lockを保持せず、書込み部分だけに縮小しました。

修正後の効果判定は5分Rawだけで行いました。約34時間の検証で、毎時04分取得は34/34 = 100%、期待5分周期は411/411でした。欠測、遅延保存欠測後観測、7分超間隔はいずれも0件でした。

重複観測は1件だけで、直前観測から約38.5秒後に発生しました。既存の重複防御により積算対象外となり、異常間隔の横断チェックでも再発傾向は確認されませんでした。

今回対象とした毎時04分欠測問題について、結果はCOMPLETED / SUCCESSです。

実験の背景

問題は、Schedulerの実行が少し遅れること自体ではなく、保存用Lockが外部観測そのものを実行不能にしていた可能性です。

修正前のSwitchBot経路は概念的に次の順序でした。

定期起動 → 共有ScriptLock取得 → SwitchBot API取得 → Raw書込み

共有Lockを20秒以内に取得できない場合、API取得前に実行が終了し得ます。この場合、後から保存できる観測値自体が存在しないため、Rawには無記録欠測が残ります。

定期collectorでは、外部観測と永続化を分離できる場合、保存競合が観測失敗に直結しない設計が有利です。観測を先に取得して取得時刻を保持すれば、保存処理の競合を別問題として扱えます。

このfailure mechanism自体はSourceから直接確認できます。一方、「毎時04分に集中した実際の欠測の主要原因が共有ScriptLock競合だった」という因果帰属は、各欠測時点の直接Lock traceが残っていないため、INFERRED / HIGH confidenceとします。Source上のfailure mechanismと、Lock範囲変更後に対象パターンが消失した事実を組み合わせた推論です。

実験方法

  1. 修正前Apps Scriptの制御フローを確認し、観測前Lockとearly returnによる欠測経路を特定する。
  2. SwitchBot API取得をLock外へ移し、取得時刻を観測時刻として固定し、Lockを保存部分だけへ縮小する。ガス側も処理全体Lockを廃止し書込み部分だけへ縮小する。
  3. 既存Rawを書き換えず、そのまま5分収集を継続する。
  4. 修正後Rawだけを用いて、期待5分周期、毎時04分取得、欠測、重複間隔、遅延保存欠測後観測、7分超間隔を確認する。

完了判定には日次集計を使用していません。一次Rawの完全性を後段集計で隠したり、逆に作り出したりしないためです。

日次利用時間算出は本Experimentの範囲外です。特に、

電力量積算対象時間 ≠ 観測カバレッジ ≠ 電源ON利用時間

として別概念に分離し、後続Experimentで扱います。

実験ログ

修正前の制御フロー

保持されたSwitchBotPowerロジックはSwitchBot API取得前にScriptLockを取得していました。20秒以内にLockを取得できない場合、Raw観測を残さず実行終了し得る構造でした。

collector変更

修正後のSwitchBot経路は次の順序になりました。

定期起動 → SwitchBot API取得+取得Timestamp → 短時間の保存Lock → Raw書込み

ガス側refreshも、同じ共有Lockを処理全体で保持する構造をやめ、書込み部分だけに限定しました。

修正後Raw検証

確認項目結果
毎時04分の期待観測34
毎時04分の実観測34
毎時04分取得成功率100%
期待5分周期411
実観測5分周期411
欠測0
遅延保存0
欠測後観測0
7分超間隔0
重複観測単発1件

重複1件は直前観測から約38.5秒後に発生しました。既存の重複防御により積算対象外となり、反復パターンは確認されませんでした。

結論

修正前Sourceには、共有ScriptLock競合によってSwitchBot API取得前に定期実行が終了し、Raw観測を残さないfailure mechanismが確認されました。修正版ではAPI取得をこのLock依存から外し、取得時刻を観測時刻として保持し、関連するSwitchBot側・ガス側の共有Lock範囲を短縮しました。

修正後約34時間のRawでは、毎時04分34/34、全5分周期411/411、欠測0、遅延保存0、欠測後観測0、7分超間隔0でした。重複は安全に積算対象外となった単発1件だけでした。

確認済みfailure mechanismと修正前後の挙動が一致するため、長時間保持された共有ScriptLockとの競合を、対象となった毎時04分欠測の主要原因と判断します。ただし、各過去欠測について直接Lock traceが残っていないため、この因果帰属は直接観測ではなくINFERREDです。一方、今回のfailure patternに対して修正後Rawで再発しなかったことはVERIFIEDです。

したがって、本ExperimentはCOMPLETED / SUCCESSで終了します。

証拠区分のまとめ

このExperimentで得られた情報のEvidence Stateは次のとおりです。

各Evidence Stateの意味はEvidence State(証拠状態)を参照してください。

記録内容Evidence State根拠
修正前SwitchBot経路はScriptLock timeoutでAPI取得前に終了しRaw観測を残さない可能性があったVERIFIED保持Source確認
修正版SwitchBot経路はAPI取得を保存Lockより前に行い、取得時刻を観測時刻として使用するVERIFIED保持された修正版Source
ガス側共有Lockは処理全体から書込み部分だけへ縮小されたVERIFIED保持された修正版Source
修正後は毎時04分34/34、全5分周期411/411、欠測0だったVERIFIED保持Raw
遅延保存欠測後観測、7分超間隔はいずれも0件だったVERIFIED保持Raw
約38.5秒後の重複1件があり、安全に積算対象外となったOBSERVED保持Raw
長時間保持された共有ScriptLock競合が毎時04分欠測の主要原因だったINFERREDfailure mechanism確認 + 修正前後Raw挙動

運用上の示唆

定期Apps Script collectorでは、外部観測と保存処理を分離できる場合、保存用Lockの取得成否が外部観測の実行可否を決めないようにする方が堅牢です。外部観測を先に取得・Timestamp化し、その後に短時間の保存Lockを使うことで、保存競合が無記録欠測へ変換されるリスクを下げられます。

この結果は今回検証したarchitectureとfailure patternに限定されます。今後のすべての欠測がLock起因だと一般化するものではありません。

日次利用時間算出は別の後続Experimentとして扱います。Rawの電源状態時系列からON時間を定義し、直接観測と補間推定を分け、電力量積算対象時間をそのまま利用時間として転用しない設計が必要です。

機械可読な実験記録

このExperimentの機械可読な正本記録をJSONとして公開しています。

AIEL-2026-0007 experiment.json

関連実験

AIEL-2026-0001では、SwitchBot OpenAPIのstale状態を調べるため独立した5分Google Apps Script収集経路を利用しました。AIEL-2026-0007が対象とするのは別レイヤーで、観測と保存Lockの結合によるcollector側欠測です。