
「今日、洗車しても大丈夫?」をひと目で確認できる、Scriptable用の洗車指数ウィジェットです。
今日の天気だけでなく、この先の雨予報までチェックして0〜100点で洗車指数を表示します。
せっかく洗車したのに翌日が雨……という失敗を減らすため、次に雨が降るまでの日数を重視して判定するのが特徴です。
小・中・大サイズのウィジェットに対応しているので、iPhoneのホーム画面はもちろん、対応環境ではCarPlayでも活用できます。
ウィジェットの表示内容
予報を使い、雨の影響などを考慮した独自の洗車指数を計算します。
サイズごとの表示
- 小:今日の洗車指数・降水確率と、翌日の目安を表示します。
- 中:今後7日間の洗車チャンスを確認できます。
- 大:7日間の指数・天気・降水確率と、おすすめの日を詳しく表示します。
設定手順
上から順に進めてください。コードを保存したら、ホーム画面に追加して郵便番号を入力します。
- Scriptableを用意する
App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。
- コードをコピーして保存する
下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、今日の洗車指数などの名前で保存してください。
保存したらこのページに戻り、次の手順へ進みます。
// Variables used by Scriptable. // These must be at the very top of the file. Do not edit. // icon-color: blue; icon-glyph: car; // Version: 1.0.0 // ===================================================== // 今日の洗車指数 for Scriptable // ===================================================== // // Parameterに郵便番号を入力 // 例:1001234 // // 100-1234 のようなハイフンありでもOK // // ・APIキー不要 // ・位置情報許可不要 // ・14日間の予報を内部取得 // ・表示は7日間 // ・7日間それぞれの洗車指数を算出 // ・小 / 中 / 大サイズ対応 // // 地点取得:HeartRails Geo API // 天気予報:Open-Meteo // // ===================================================== // ===================================================== // 設定 // ===================================================== const DISPLAY_DAYS = 7; const FORECAST_DAYS = 14; const RAIN_MM = 0.5; const RAIN_PROBABILITY = 50; const BG_COLOR = new Color("#101216"); const TEXT_COLOR = Color.white(); const SUB_COLOR = new Color("#AEB4BE"); const DIM_COLOR = new Color("#777D87"); // ===================================================== // Parameter // ===================================================== const rawParameter = typeof args !== "undefined" && args.widgetParameter ? args.widgetParameter.trim() : ""; const postalCode = rawParameter.replace(/[^0-9]/g, ""); // ===================================================== // メイン // ===================================================== async function main() { if (!/^\d{7}$/.test(postalCode)) { const widget = createErrorWidget( "郵便番号を設定してください", "例:6712572" ); Script.setWidget(widget); if (!config.runsInWidget) { await widget.presentSmall(); } Script.complete(); return; } try { // 郵便番号 → 住所・緯度経度 const location = await getLocation(postalCode); // 天気予報 const weather = await getWeather( location.latitude, location.longitude ); // 14日間それぞれの洗車指数を計算 const washResults = []; for (let i = 0; i < weather.time.length; i++) { washResults.push( calculateWashIndex(weather, i) ); } // Widget const widget = createWidget( weather, washResults, location ); Script.setWidget(widget); if (!config.runsInWidget) { if (config.widgetFamily === "medium") { await widget.presentMedium(); } else if (config.widgetFamily === "large") { await widget.presentLarge(); } else { await widget.presentSmall(); } } Script.complete(); } catch (error) { console.error(error); const widget = createErrorWidget( "データを取得できませんでした", String(error) ); Script.setWidget(widget); if (!config.runsInWidget) { await widget.presentSmall(); } Script.complete(); } } // ===================================================== // 郵便番号 → 住所・緯度経度 // HeartRails Geo API // ===================================================== async function getLocation(zip) { const url = "https://geoapi.heartrails.com/api/json" + "?method=searchByPostal" + "&postal=" + encodeURIComponent(zip); const request = new Request(url); request.timeoutInterval = 15; const json = await request.loadJSON(); if ( !json || !json.response || !json.response.location || json.response.location.length === 0 ) { throw new Error("郵便番号が見つかりません"); } const data = json.response.location[0]; const latitude = Number(data.y); const longitude = Number(data.x); if ( !Number.isFinite(latitude) || !Number.isFinite(longitude) ) { throw new Error("緯度経度を取得できません"); } return { prefecture: data.prefecture || "", city: data.city || "", town: data.town || "", postal: data.postal || zip, latitude, longitude }; } // ===================================================== // 天気取得 // Open-Meteo // ===================================================== async function getWeather(latitude, longitude) { const daily = [ "weather_code", "temperature_2m_max", "temperature_2m_min", "precipitation_sum", "precipitation_probability_max", "wind_speed_10m_max" ].join(","); const url = "https://api.open-meteo.com/v1/forecast" + "?latitude=" + encodeURIComponent(latitude) + "&longitude=" + encodeURIComponent(longitude) + "&daily=" + encodeURIComponent(daily) + "&timezone=Asia%2FTokyo" + "&forecast_days=" + FORECAST_DAYS; const request = new Request(url); request.timeoutInterval = 15; const json = await request.loadJSON(); if ( !json || !json.daily || !json.daily.time ) { throw new Error("天気予報を取得できません"); } return json.daily; } // ===================================================== // 指定日の洗車指数 // ===================================================== function calculateWashIndex(weather, targetDay) { // --------------------------------------------------- // 指定日より後、最初に雨が降る日 // --------------------------------------------------- let nextRainDay = null; for ( let i = targetDay + 1; i < weather.time.length; i++ ) { if (isRainDay(weather, i)) { nextRainDay = i - targetDay; break; } } // --------------------------------------------------- // 次の雨までの日数による基礎点 // --------------------------------------------------- let score; if (nextRainDay === null || nextRainDay >= 7) { score = 100; } else { switch (nextRainDay) { case 1: score = 30; break; case 2: score = 45; break; case 3: score = 60; break; case 4: score = 72; break; case 5: score = 82; break; case 6: score = 90; break; default: score = 100; } } // --------------------------------------------------- // 指定日自身の天候 // --------------------------------------------------- const probability = Number( weather.precipitation_probability_max[targetDay] ?? 0 ); const rain = Number( weather.precipitation_sum[targetDay] ?? 0 ); const wind = Number( weather.wind_speed_10m_max[targetDay] ?? 0 ); const maxTemp = Number( weather.temperature_2m_max[targetDay] ?? 0 ); // --------------------------------------------------- // その日自身が雨なら大幅減点 // --------------------------------------------------- if (rain >= 5) { score -= 70; } else if (rain >= 1) { score -= 50; } else if (rain >= 0.5) { score -= 35; } // --------------------------------------------------- // 降水確率 // --------------------------------------------------- if (probability >= 70) { score -= 30; } else if (probability >= 50) { score -= 20; } else if (probability >= 30) { score -= 8; } // --------------------------------------------------- // 強風 // --------------------------------------------------- if (wind >= 40) { score -= 25; } else if (wind >= 30) { score -= 15; } else if (wind >= 20) { score -= 8; } // --------------------------------------------------- // 暑すぎる // --------------------------------------------------- if (maxTemp >= 35) { score -= 15; } else if (maxTemp >= 32) { score -= 8; } // --------------------------------------------------- // 寒すぎる // --------------------------------------------------- if (maxTemp <= 3) { score -= 15; } else if (maxTemp <= 7) { score -= 8; } score = Math.max( 0, Math.min(100, Math.round(score)) ); // --------------------------------------------------- // 評価 // --------------------------------------------------- let label; let emoji; if (score >= 90) { label = "絶好の洗車日和!"; emoji = "✨"; } else if (score >= 75) { label = "洗車おすすめ"; emoji = "👍"; } else if (score >= 50) { label = "洗ってもOK"; emoji = "🙂"; } else if (score >= 25) { label = "少し待つのがおすすめ"; emoji = "☁️"; } else { label = "今日は見送り"; emoji = "☔"; } return { score, label, emoji, nextRainDay, probability, rain, wind, maxTemp }; } // ===================================================== // Widget // ===================================================== function createWidget( weather, washResults, location ) { const widget = new ListWidget(); widget.backgroundColor = BG_COLOR; const family = config.widgetFamily || "small"; if (family === "medium") { createMedium( widget, weather, washResults, location ); } else if (family === "large") { createLarge( widget, weather, washResults, location ); } else { createSmall( widget, weather, washResults, location ); } return widget; } // ===================================================== // 小サイズ // ===================================================== function createSmall( widget, weather, washResults, location ) { const today = washResults[0]; const tomorrow = washResults[1]; widget.setPadding(14, 14, 13, 14); // タイトル const title = widget.addText( "🚙 今日の洗車指数" ); title.font = Font.boldSystemFont(13); title.textColor = TEXT_COLOR; widget.addSpacer(6); // スコア const scoreStack = widget.addStack(); scoreStack.centerAlignContent(); const score = scoreStack.addText( String(today.score) ); score.font = Font.boldSystemFont(39); score.textColor = scoreColor(today.score); const max = scoreStack.addText( " / 100" ); max.font = Font.mediumSystemFont(12); max.textColor = SUB_COLOR; widget.addSpacer(2); // 評価 const label = widget.addText( today.emoji + " " + today.label ); label.font = Font.boldSystemFont(11); label.textColor = TEXT_COLOR; label.lineLimit = 1; label.minimumScaleFactor = 0.65; widget.addSpacer(7); // 次の雨 const rain = widget.addText( rainText(today.nextRainDay) ); rain.font = Font.mediumSystemFont(10); rain.textColor = SUB_COLOR; widget.addSpacer(); // 明日の指数 const tomorrowStack = widget.addStack(); tomorrowStack.centerAlignContent(); const tomorrowTitle = tomorrowStack.addText("明日 "); tomorrowTitle.font = Font.mediumSystemFont(10); tomorrowTitle.textColor = SUB_COLOR; const tomorrowMark = tomorrowStack.addText( chanceMark(tomorrow.score) + " " ); tomorrowMark.font = Font.boldSystemFont(11); tomorrowMark.textColor = scoreColor(tomorrow.score); const tomorrowScore = tomorrowStack.addText( tomorrow.score + "点" ); tomorrowScore.font = Font.boldSystemFont(10); tomorrowScore.textColor = TEXT_COLOR; } // ===================================================== // 中サイズ // ===================================================== function createMedium( widget, weather, washResults, location ) { const today = washResults[0]; widget.setPadding(13, 17, 12, 17); // --------------------------------------------------- // 上段 // --------------------------------------------------- const top = widget.addStack(); top.centerAlignContent(); const left = top.addStack(); left.layoutVertically(); const title = left.addText( "🚙 今日の洗車指数" ); title.font = Font.boldSystemFont(13); title.textColor = TEXT_COLOR; left.addSpacer(3); const scoreStack = left.addStack(); scoreStack.centerAlignContent(); const score = scoreStack.addText( String(today.score) ); score.font = Font.boldSystemFont(34); score.textColor = scoreColor(today.score); const max = scoreStack.addText( " / 100" ); max.font = Font.mediumSystemFont(11); max.textColor = SUB_COLOR; top.addSpacer(); const right = top.addStack(); right.layoutVertically(); const label = right.addText( today.emoji + " " + today.label ); label.font = Font.boldSystemFont(13); label.textColor = TEXT_COLOR; right.addSpacer(4); const rain = right.addText( rainText(today.nextRainDay) ); rain.font = Font.mediumSystemFont(11); rain.textColor = SUB_COLOR; widget.addSpacer(8); // --------------------------------------------------- // 今週の洗車チャンス // --------------------------------------------------- const chanceTitle = widget.addText( "今週の洗車チャンス" ); chanceTitle.font = Font.boldSystemFont(10); chanceTitle.textColor = SUB_COLOR; widget.addSpacer(5); const chanceRow = widget.addStack(); chanceRow.centerAlignContent(); for (let i = 0; i < DISPLAY_DAYS; i++) { const item = chanceRow.addStack(); item.layoutVertically(); item.centerAlignContent(); const day = item.addText( getDayLabel(weather.time[i], i) ); day.font = Font.mediumSystemFont(8); day.textColor = SUB_COLOR; day.centerAlignText(); item.addSpacer(2); const mark = item.addText( chanceMark(washResults[i].score) ); mark.font = Font.boldSystemFont(13); mark.textColor = scoreColor(washResults[i].score); mark.centerAlignText(); if (i < DISPLAY_DAYS - 1) { chanceRow.addSpacer(); } } widget.addSpacer(); // --------------------------------------------------- // 今日の気象条件 // --------------------------------------------------- const info = widget.addStack(); info.centerAlignContent(); addInfo( info, "🌡️", Math.round(today.maxTemp) + "℃" ); info.addSpacer(); addInfo( info, "☔", today.probability + "%" ); info.addSpacer(); addInfo( info, "💨", formatWind(today.wind) ); } // ===================================================== // 大サイズ // ===================================================== function createLarge( widget, weather, washResults, location ) { const today = washResults[0]; widget.setPadding(16, 18, 14, 18); // --------------------------------------------------- // タイトル // --------------------------------------------------- const title = widget.addText( "🚙 今日の洗車指数" ); title.font = Font.boldSystemFont(14); title.textColor = TEXT_COLOR; widget.addSpacer(5); // --------------------------------------------------- // 今日のスコア // --------------------------------------------------- const scoreStack = widget.addStack(); scoreStack.centerAlignContent(); const score = scoreStack.addText( String(today.score) ); score.font = Font.boldSystemFont(42); score.textColor = scoreColor(today.score); const max = scoreStack.addText( " / 100" ); max.font = Font.mediumSystemFont(13); max.textColor = SUB_COLOR; scoreStack.addSpacer(13); const label = scoreStack.addText( today.emoji + " " + today.label ); label.font = Font.boldSystemFont(14); label.textColor = TEXT_COLOR; widget.addSpacer(3); const rain = widget.addText( rainText(today.nextRainDay) ); rain.font = Font.mediumSystemFont(11); rain.textColor = SUB_COLOR; widget.addSpacer(9); // --------------------------------------------------- // 今日の条件 // --------------------------------------------------- const info = widget.addStack(); info.centerAlignContent(); addInfo( info, "🌡️", Math.round(today.maxTemp) + "℃" ); info.addSpacer(); addInfo( info, "☔", today.probability + "%" ); info.addSpacer(); addInfo( info, "💨", formatWind(today.wind) ); widget.addSpacer(13); // --------------------------------------------------- // 7日間の洗車予報 // --------------------------------------------------- const forecastTitle = widget.addText( "7日間の洗車予報" ); forecastTitle.font = Font.boldSystemFont(11); forecastTitle.textColor = SUB_COLOR; widget.addSpacer(7); const forecast = widget.addStack(); forecast.centerAlignContent(); for (let i = 0; i < DISPLAY_DAYS; i++) { const item = forecast.addStack(); item.layoutVertically(); item.centerAlignContent(); // 曜日 const day = item.addText( getDayLabel(weather.time[i], i) ); day.font = Font.mediumSystemFont(9); day.textColor = SUB_COLOR; day.centerAlignText(); item.addSpacer(4); // 洗車評価 const mark = item.addText( chanceMark(washResults[i].score) ); mark.font = Font.boldSystemFont(17); mark.textColor = scoreColor(washResults[i].score); mark.centerAlignText(); item.addSpacer(3); // 天気 const icon = item.addText( weatherEmoji( weather.weather_code[i] ) ); icon.font = Font.systemFont(17); icon.centerAlignText(); item.addSpacer(3); // 洗車指数 const washScore = item.addText( washResults[i].score + "点" ); washScore.font = Font.boldSystemFont(9); washScore.textColor = scoreColor(washResults[i].score); washScore.centerAlignText(); item.addSpacer(2); // 降水確率 const probability = item.addText( weather .precipitation_probability_max[i] + "%" ); probability.font = Font.mediumSystemFont(8); probability.textColor = isRainDay(weather, i) ? new Color("#69AFFF") : SUB_COLOR; probability.centerAlignText(); if (i < DISPLAY_DAYS - 1) { forecast.addSpacer(); } } widget.addSpacer(); // --------------------------------------------------- // おすすめ日 // --------------------------------------------------- const best = getBestWashDay( weather, washResults ); const bestStack = widget.addStack(); bestStack.centerAlignContent(); const bestLabel = bestStack.addText( "✨ 今週のおすすめ:" ); bestLabel.font = Font.mediumSystemFont(10); bestLabel.textColor = SUB_COLOR; const bestValue = bestStack.addText( best.label + "(" + best.score + "点)" ); bestValue.font = Font.boldSystemFont(10); bestValue.textColor = scoreColor(best.score); widget.addSpacer(); // --------------------------------------------------- // 地域 // --------------------------------------------------- const locationText = widget.addText( "📍 " + location.prefecture + location.city + location.town ); locationText.font = Font.mediumSystemFont(9); locationText.textColor = DIM_COLOR; locationText.lineLimit = 1; } // ===================================================== // 今週のおすすめ洗車日 // ===================================================== function getBestWashDay( weather, washResults ) { let bestIndex = 0; for (let i = 1; i < DISPLAY_DAYS; i++) { if ( washResults[i].score > washResults[bestIndex].score ) { bestIndex = i; } } return { label: getFullDayLabel( weather.time[bestIndex], bestIndex ), score: washResults[bestIndex].score }; } // ===================================================== // 洗車チャンスマーク // ===================================================== function chanceMark(score) { if (score >= 90) { return "◎"; } if (score >= 75) { return "○"; } if (score >= 50) { return "△"; } return "×"; } // ===================================================== // パーツ // ===================================================== function addInfo(stack, icon, text) { const item = stack.addStack(); item.centerAlignContent(); const iconText = item.addText(icon); iconText.font = Font.systemFont(11); item.addSpacer(4); const value = item.addText(text); value.font = Font.mediumSystemFont(11); value.textColor = TEXT_COLOR; } // ===================================================== // 次の雨 // ===================================================== function rainText(day) { if (day === null || day >= 7) { return "☀️ 7日間 雨予報なし"; } if (day === 1) { return "☔ 次の雨:明日"; } return ( "☔ 次の雨:" + day + "日後" ); } // ===================================================== // 雨判定 // ===================================================== function isRainDay(weather, index) { const probability = Number( weather .precipitation_probability_max[index] ?? 0 ); const precipitation = Number( weather .precipitation_sum[index] ?? 0 ); return ( precipitation >= RAIN_MM || probability >= RAIN_PROBABILITY ); } // ===================================================== // 曜日 // ===================================================== function getDayLabel(dateString, index) { if (index === 0) { return "今日"; } const date = new Date(dateString + "T12:00:00"); const days = [ "日", "月", "火", "水", "木", "金", "土" ]; return days[date.getDay()]; } function getFullDayLabel(dateString, index) { if (index === 0) { return "今日"; } if (index === 1) { return "明日"; } const date = new Date(dateString + "T12:00:00"); const days = [ "日", "月", "火", "水", "木", "金", "土" ]; return days[date.getDay()] + "曜日"; } // ===================================================== // 天気アイコン // ===================================================== function weatherEmoji(code) { if (code === 0) { return "☀️"; } if ( code === 1 || code === 2 ) { return "🌤️"; } if (code === 3) { return "☁️"; } if ( code === 45 || code === 48 ) { return "🌫️"; } if ( code >= 51 && code <= 57 ) { return "🌦️"; } if ( code >= 61 && code <= 67 ) { return "🌧️"; } if ( code >= 71 && code <= 77 ) { return "🌨️"; } if ( code >= 80 && code <= 82 ) { return "🌦️"; } if ( code >= 85 && code <= 86 ) { return "🌨️"; } if (code >= 95) { return "⛈️"; } return "☁️"; } // ===================================================== // スコア色 // ===================================================== function scoreColor(score) { if (score >= 90) { return new Color("#59D98E"); } if (score >= 75) { return new Color("#9BDF72"); } if (score >= 50) { return new Color("#FFD166"); } if (score >= 25) { return new Color("#FF9F5A"); } return new Color("#FF6B6B"); } // ===================================================== // 風速 // Open-Meteo = km/h → m/s // ===================================================== function formatWind(speed) { const ms = Number(speed) / 3.6; return ms.toFixed(1) + "m/s"; } // ===================================================== // エラーWidget // ===================================================== function createErrorWidget( titleText, message ) { const widget = new ListWidget(); widget.backgroundColor = BG_COLOR; widget.setPadding( 16, 16, 16, 16 ); const title = widget.addText("🚙 洗車指数"); title.font = Font.boldSystemFont(14); title.textColor = TEXT_COLOR; widget.addSpacer(10); const text = widget.addText(titleText); text.font = Font.boldSystemFont(13); text.textColor = TEXT_COLOR; widget.addSpacer(5); const sub = widget.addText(message); sub.font = Font.systemFont(10); sub.textColor = SUB_COLOR; sub.lineLimit = 3; return widget; } // ===================================================== // 実行 // ===================================================== await main(); - ホーム画面に追加して設定する
iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。
追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。
「Parameter」に確認したい地域の7桁の郵便番号を入力します。例:1000001。ハイフンあり・なしのどちらでも使えます。
編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。
洗車指数について
洗車指数は、単純に「今日晴れているか」だけで判定しているわけではありません。
特に重視しているのが、洗車したあと次に雨が降るまで何日あるかです。
たとえば今日が晴れていても明日が雨なら指数は低くなり、今後1週間ほど雨の心配が少なければ高い指数になります。
さらに、
降水確率 / 降水量 / 気温 / 風速
なども考慮して、0〜100点で算出します。
洗車チャンスの見方
週間予報では、洗車に向いている度合いを4段階で表示します。
◎ 絶好の洗車日和
○ 洗車おすすめ
△ 洗ってもOK
× 洗車は見送り
「今日は微妙だけど、明日は◎」といった判断もしやすくなっています。
動作確認・使用結果
| 確認した端末 | iPhone 17 ProMAX |
|---|---|
| iOS | iOS27 |
| Scriptable | 1.7.19 |
| 確認したサイズ | 小・中・大 |
| バージョン | 1.0.0 |
| 動作確認日 | 2026-09-23 |
表示・使用時のポイント
予報を使い、雨の影響などを考慮した独自の洗車指数を計算します。今日だけでなく、洗車後の天気も見ながら日を選べます。
更新履歴
1.0.0
- 初回リリース
よくあるエラーと対処
- 確認すること
- 表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
- 地域が違う
- Parameterの7桁の郵便番号を確認してください。
- 洗車チャンスが前回と変わった
- 天気予報の更新で指数も変わります。
- 予報や指数が出ない
- 通信を確認してScriptableで実行し、時間を置いて再取得してください。
注意事項・制限
- 公的な指数ではありません。予報を使った目安です。
- 花粉・黄砂・砂ぼこりや、車の保管場所などはすべて反映していません。
- ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。
データ提供・参考資料
Open-Meteo:天気予報データ。
HeartRails Geo API:郵便番号から地域と座標を取得します。
SHARE

