team_shinkaiLabのアイコン画像
team_shinkaiLab 2021年05月13日作成
製作品 製作品 閲覧数 2710
team_shinkaiLab 2021年05月13日作成 製作品 製作品 閲覧数 2710

お出かけ先の天気と連動するIoT傘立て

概要

obnizを使ったIoT傘立てを作ってみました。
スマホの接近をトリガーとして、カレンダーにあるお出かけ先の天気予報を取得し、雨の予報であれば傘を倒して知らせます。
「Bluetooth」x「Google カレンダー」x「OpenWeatherMap」x「ソレノイド」を組み合わせて実装しました。

デモ動画

ここに動画が表示されます

傘立てに接近し、行先の予報が雨の場合のデモ動画です。

用意するもの

  • Android端末

構成部品

  • obniz Board 1Y
  • プッシュソレノイド(タカハ機工 CBS1240)
  • 傘立て
  • ストッパー部品
  • モバイルバッテリー(5V)

傘立て本体

キャプションを入力できます
傘立ては、フリーに回転できる傘のストッパー部品と、ストッパー部品をロックするソレノイドで構成されます。
ソレノイドが動作することでストッパー部品のロックが解除されます。
ロックが解除されると傘の自重でストッパー部品が回転し、傘のストッパーも外れます。

動作の流れ
Step 0 傘をストッパーに立てかけます。
Step 1 ソレノイドを動作させ、ストッパー部品の回転を自由にします。
    ソレノイドは図中矢印1の方向に動作します。
Step 2 ストッパー部品が傘の自重で回転し、傘が倒れます。
    ストッパーは図中矢印2の方向に回転します。

配線図

キャプションを入力できます

システムフロー

実装方法

お出かけ検出

お出かけ先取得

  • Googleカレンダーから本日の予定を取得する
    • APIキー、OAuthクライアントIDの取得
      • Google Cloud Platformでプロジェクトを作成してGoogle Calendar APIを有効にする
      • [API とサービス]→[OAuth 同意画面] でOAuth 同意画面を構成する
      • [API とサービス]→[認証情報] の[認証情報を作成]でAPIキーとOAuthクライアントIDを作成する
    • obnizのWEBアプリ上からGoogleアカウントを認証する
    • お出かけ検出時にカレンダー取得
  • 予定(Events)内のlocationからお出かけ先を文字列として取得する
  • 場所文字列を緯度・経度に変換する
    • LocationIQのGeocoding APIを使用する
    • LocationIQでアカウントを作成し、APIキーを取得する
    • 場所文字列とAPIキーを指定してfetchすることで緯度・経度が取得できる
  • (デフォルトの現在地として東京の緯度・経度は直指定で常に検索される)

天気検出

  • OpenWeatherMapを使用する
    • OpenWeatherMapでアカウントを作成し、APIキーを取得する
    • One Call APIに緯度・経度とAPIキーを指定してfetchすることで日別の天気が取得できる
    • 本日が悪天候の場合はソレノイドをオンにする

ソースコード

<html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"/> <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <script src="https://unpkg.com/obniz@3.14.0/obniz.js" crossorigin="anonymous"></script> </head> <body> <div id="obniz-debug"></div> <h3>Connect From Your Browser</h3> <button class="btn btn-primary" id="on">Solenoid ON</button> <button class="btn btn-primary" id="off">Solenoid OFF</button> <!--Add buttons to initiate auth sequence and sign out--> <button id="authorize_button" style="display: none;">Authorize</button> <button id="signout_button" style="display: none;">Sign Out</button> <div>Demo Pin Assign</div> <ul> <li>io0: Solenoid anode</li> <li>io1: Solenoid cathode</li> </ul> <script> // BLE const BLE_UUID = "YOUR_iBeacon_UUID"; const RSSI_THRES = -80; // OpenWeatherMap const WEATHER_APIKEY = "YOUR_WEATHER_APIKEY _HERE"; const DEFAULT_LOCATION = {lat: 35.681236, lon:139.767125}; // 東京 // LocationIQ const LOCATIONIQ_APIKEY = "LOCATIONIQ_APIKEY_HERE"; // Google const GOOGLE_APIKEY = "YOUR_GOOGLE_APIKEY_HERE"; const OAUTH_CLIENT_ID = "YOUR_OAUTH_CLIENT_ID_HERE"; const DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest"]; const GAPI_SCOPES = "https://www.googleapis.com/auth/calendar.events.readonly"; const obniz = new Obniz("YOUR_OBNIZ_ID_HERE"); let fired = false; let lastTimeoutID = null; let isGoogleSignedIn = false; const authorizeButton = document.getElementById('authorize_button'); const signoutButton = document.getElementById('signout_button'); const Weather = { None: 0, Good: 1, Bad: 2, }; obniz.onconnect = async function() { const sole = obniz.wired("Solenoid", { signal: 1, gnd: 0 }); // debug $("#on").click(function() { sole.on(); obniz.display.clear(); obniz.display.print("ON"); }); $("#off").click(function() { sole.off(); obniz.display.clear(); obniz.display.print("OFF"); }); // Bluetooth await obniz.ble.initWait(); obniz.ble.scan.onfind = async function(peripheral){ console.log(`rssi = ${peripheral.rssi}`); if(peripheral.rssi > RSSI_THRES) { if(!fired) { fired = true; console.log("fire!"); let isBadWeather = (await getWeather(DEFAULT_LOCATION.lat, DEFAULT_LOCATION.lon)) == Weather.Bad; // Googleカレンダーから今日の予定を取得 if(!isBadWeather && isGoogleSignedIn){ const events = await getCalendarEvents(); const filteredEvents = events.filter(e=> isSameDate(e.date, getToday()) && e.location !== "" && !!e.location); for(let event of filteredEvents) { const location = await geocoding(event.location); if(!location){ continue; } const weather = await getWeather(location.lat, location.lon); if(weather == Weather.Bad) { isBadWeather = true; break; } } } if(isBadWeather) { console.log("Bad Weather"); sole.on(); await sleep(2000); sole.off(); } else { console.log("Good Weather"); } // 一分後に復活 clearTimeout(lastTimeoutID); lastTimeoutID = setTimeout(()=>{fired = false}, 60 * 1000); } } else { fired = false; clearTimeout(lastTimeoutID); } }; obniz.ble.scan.onfinish = async function(peripherals, error){ console.log("scan timeout!"); }; await obniz.ble.scan.startWait( { binary: [uuidToByteArray(BLE_UUID)] }, { duplicate: true, duration: null, filterOnDevice: true, }); console.log("start!") }; // 天気取得 // https://blog.obniz.com/make/led-integration-weather-api // lat 緯度 // lon 経度 async function getWeather (lat, lon) { const BadWeathers = [ "Rain", "Snow", "Thunderstorm", "Drizzle", "Fog", "Squall", "Mist", "Smoke", "Haze", "Tornado", "Ash", ]; const GoodWeathers = [ "Clouds", "Clear", "Dust", "Sand", ]; const url = `https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lon}&exclude=minutely,hourly&appid=${WEATHER_APIKEY}`; const data = await (await fetch(url)).json(); console.log(data); const currentWeather = data.current && data.current.weather.length > 0 ? data.current.weather[0].main : null; const todayWeather = data.daily && data.daily[0] && data.daily[0].weather.length > 0 ? data.daily[0].weather[0].main : null; console.log(`current = ${currentWeather}, today = ${todayWeather}`); if (BadWeathers.includes(currentWeather) || BadWeathers.includes(todayWeather)) { return Weather.Bad; } else if (GoodWeathers.includes(currentWeather) || GoodWeathers.includes(todayWeather)) { return Weather.Good; } console.log("no data"); return Weather.None; } // 緯度経度取得 async function geocoding(locationStr) { try { const trimmedLocation = locationStr.replace(/,.*/, ""); // ","以降を消す const url = `https://us1.locationiq.com/v1/search.php?key=${LOCATIONIQ_APIKEY}&q=${trimmedLocation}&format=json`; const data = await (await fetch(url)).json(); const location = data[0]; if(location){ return {lat: location.lat, lon: location.lon}; } } catch(e){ console.log(e); } return null; } // カレンダー取得 // https://developers.google.com/calendar/quickstart/js async function getCalendarEvents() { const response = await gapi.client.calendar.events.list({ 'calendarId': 'primary', 'timeMin': getToday().toISOString(), 'showDeleted': false, 'singleEvents': true, 'maxResults': 10, 'orderBy': 'startTime' }); const ret = []; for(let event of response.result.items) { const date = new Date(Date.parse(event.start.date)); const location = event.location; ret.push({date, location}); } return ret; } // Google認証関連 // https://developers.google.com/calendar/quickstart/js function handleClientLoad() { gapi.load('client:auth2', initClient); } function initClient() { gapi.client.init({ apiKey: GOOGLE_APIKEY, clientId: OAUTH_CLIENT_ID, discoveryDocs: DISCOVERY_DOCS, scope: GAPI_SCOPES }).then(function () { gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus); updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get()); authorizeButton.onclick = () => gapi.auth2.getAuthInstance().signIn(); signoutButton.onclick = ()=> gapi.auth2.getAuthInstance().signOut(); }, function(error) { console.log(JSON.stringify(error, null, 2)); }); } function updateSigninStatus(isSignedIn) { isGoogleSignedIn = isSignedIn; if (isSignedIn) { authorizeButton.style.display = 'none'; signoutButton.style.display = 'block'; } else { authorizeButton.style.display = 'block'; signoutButton.style.display = 'none'; } } // util async function sleep(msec) { return new Promise(resolve => setTimeout(resolve, msec)); } function uuidToByteArray(uuid) { return [...uuid.matchAll(/\w\w/g)].map(byte => Number.parseInt(byte, 16)) } function isSameDate(a, b){ return a.getFullYear() == b.getFullYear() && a.getMonth() == b.getMonth() && a.getDate() == b.getDate(); } function getToday() { const today = new Date(); return new Date(today.getFullYear(), today.getMonth(), today.getDate()); } </script> <script async defer src="https://apis.google.com/js/api.js" onload="this.onload=function(){};handleClientLoad()" onreadystatechange="if (this.readyState === 'complete') this.onload()"> </script> </body> </html>
ログインしてコメントを投稿する