概要
obnizを使ったIoT傘立てを作ってみました。
スマホの接近をトリガーとして、カレンダーにあるお出かけ先の天気予報を取得し、雨の予報であれば傘を倒して知らせます。
「Bluetooth」x「Google カレンダー」x「OpenWeatherMap」x「ソレノイド」を組み合わせて実装しました。
デモ動画
傘立てに接近し、行先の予報が雨の場合のデモ動画です。
用意するもの
- 傘
- Android端末
構成部品
- obniz Board 1Y
- プッシュソレノイド(タカハ機工 CBS1240)
- 傘立て
- ストッパー部品
- モバイルバッテリー(5V)
傘立て本体
傘立ては、フリーに回転できる傘のストッパー部品と、ストッパー部品をロックするソレノイドで構成されます。
ソレノイドが動作することでストッパー部品のロックが解除されます。
ロックが解除されると傘の自重でストッパー部品が回転し、傘のストッパーも外れます。
動作の流れ
Step 0 傘をストッパーに立てかけます。
Step 1 ソレノイドを動作させ、ストッパー部品の回転を自由にします。
ソレノイドは図中矢印1の方向に動作します。
Step 2 ストッパー部品が傘の自重で回転し、傘が倒れます。
ストッパーは図中矢印2の方向に回転します。
配線図
システムフロー
実装方法
お出かけ検出
- obnizをBLE(Bluetooth Low Energy)マスター、スマホをBLEスレーブとして動かす
- 適当なビーコン(iBeacon)アプリをスマホにインストールする必要がある
- https://play.google.com/store/apps/details?id=net.alea.beaconsimulator
- obnizは常にスキャンを行って、得られるRSSI値を簡易的な距離として扱い閾値によって判別する
- ビーコンアプリで指定したUUIDを用いて対象のスマホのみを検出する
- https://obniz.com/ja/doc/reference/common/ble/central-scan
お出かけ先取得
- Googleカレンダーから本日の予定を取得する
- APIキー、OAuthクライアントIDの取得
- Google Cloud Platformでプロジェクトを作成してGoogle Calendar APIを有効にする
- [API とサービス]→[OAuth 同意画面] でOAuth 同意画面を構成する
- [API とサービス]→[認証情報] の[認証情報を作成]でAPIキーとOAuthクライアントIDを作成する
- obnizのWEBアプリ上からGoogleアカウントを認証する
- Google API Client Library for JavaScriptで上記のAPIキーとOAuthクライアントを用いて認証する
(自動ログアウトされるまでは一度だけログインすればよい)
- Google API Client Library for JavaScriptで上記のAPIキーとOAuthクライアントを用いて認証する
- お出かけ検出時にカレンダー取得
- 認証済みのGoogle API Client Library for JavaScriptインスタンスでカレンダーAPI(calendar.events)にアクセスし、取得する
- APIキー、OAuthクライアントIDの取得
- 予定(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>
投稿者の人気記事
-
team_shinkaiLab
さんが
2021/05/13
に
編集
をしました。
(メッセージ: 初版)
ログインしてコメントを投稿する