team_shinkaiLabのアイコン画像

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

team_shinkaiLab 2021年05月13日に作成

概要

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>
ログインしてコメントを投稿する