【新感覚?!】スマホ脳 脱却目覚まし時計⏲

## 目覚まし時計はスマホ??

皆さんは**目覚まし時計**をかけて寝ますか？
**目覚まし時計**といっても最近は **スマートフォン（スマホ）** のアラームを目覚まし時計代わりにされている方が多いのではないでしょうか。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/362ef6e2-d1e8-5678-58a2-9a707b2ea7d7.jpeg">

しかし寝室にスマホを持ち込むことには、いくつかの問題点があると考えます。

:::note warn
**睡眠の質の低下**：スマホの青色光は体内時計を狂わせ、メラトニンの分泌を抑制します。これにより、寝つきが悪くなったり、睡眠の質が低下したりする可能性があります。

**依存症のリスク**：寝る直前までスマホを使用することで、スマホ依存症のリスクが高まります。就寝時間が遅くなったり、夜中に目が覚めてスマホをチェックしたりする習慣につながる可能性があります。

**集中力の低下**：寝室にスマホがあることで、睡眠中や起床直後にSNSやメールをチェックする誘惑に駆られやすくなります。これにより、朝の時間を有効に使えなくなったり、一日の集中力が低下したりする可能性があります。
:::

いわゆる **[スマホ脳](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%9E%E3%83%9B%E8%84%B3)** ってやつですね。

### 従来の目覚まし時計は？
ただ、従来の目覚まし時計にも欠点があると考えています。
* アナログ時計の場合、秒針のカチカチ音が気になる
* 電池切れのリスクがある
* <font color="Red">**無意識にアラームを切ってしまう**</font>

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/9ccbcf5d-e655-eeb8-2327-5d03aa36c82f.png">

こんなことよくありませんか？
私はよくあります。


### ではどうすればよいのか
そこで私はobnizを利用して、以下のような仕組みの目覚まし時計を作製しました。
* 就寝前にLINE Botにアラームを鳴らしたい時刻を送る。
* アラームを鳴らす時刻になったらobnizに接続されたスピーカーが鳴動する。
* **部屋の電気が点灯する**ことでスピーカーの鳴動を止めることができる。

これにより、スマホを寝室に持ち込まず、なおかつ従来の目覚まし時計の欠点を解消できると考えました。

また、スピーカーの鳴動を止めるために**物理ボタン**ではなく**光**をトリガーとすることで、**強制的に電気をつけざるを得ないように**しました。
（電気のリモコンはベッドの近くに置かないこと前提！！）

## デモ

https://x.com/vanchan2625/status/1805537285464899963

## 仕組み
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/f4ee9fe8-7961-101a-ff55-de7bbbb16b32.jpeg">

* [make](https://make.com/)（LINEからGoogleスプレッドシートまで）
* [obniz開発者コンソール](https://console.obniz.com/ja/)
([stein](https://steinhq.com/)はGoogleスプレッドシートをAPI化するために利用)

## 使用したもの
* obniz Board 1Y (obnizOS 3.2.1)
* 照度センサー (Macron International Group社製 MI527/MI5527)
* スピーカー (株式会社村田製作所 PKM13EPYH4000-A0)
* 330Ω抵抗 (FAITHFUL LINK INDUSTRIAL社製 CFS50J330RB)
* ミニブレッドボード (Cixi Wanjie Electronic社製 BB-601(白))

## 制作物
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/9cc61215-de3b-9b0b-af27-3cd258369003.jpeg">

| Port | デバイス | 機能 |
|:-:|:-:|:-:|
| 0  | スピーカー  | 出力 |
| 1  | スピーカー  | 出力 | 
| 2  | 未接続  | -  | 
| 3  | 照度センサー  | 入力  | 
| 4  | 未接続  | -  | 
| 5  | 未接続  | -  | 
| 6  | 未接続  | -  | 
| 7  | 未接続  | -  | 
| 8  | 未接続  | -  | 
| 9  | 未接続  | -  | 
| 10  | 未接続  | -  | 
| 11  | 未接続  | -  | 
| -  | 抵抗GND側  | GND | 
| 3.3V  | 未接続  | -  | 
| +  | 照度センサー電源側  | 5V | 

## makeのシナリオ
### シナリオ1
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/f49a2d33-f902-cdd7-d8ff-a59e2928f5d2.png">

### シナリオ2
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/9c00b7b3-89bd-c085-e0b9-6725040ce935.png">

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/6dc34149-efa4-a696-90e0-1dfdde71b43a.png">

## 今回のソースコード

<details><summary>←の三角マークを押すと確認することができます。</summary>

```html:obniz
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Obniz Alarm Control</title>
    <script src="https://unpkg.com/obniz@3.31.0/obniz.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
</head>
<body>
    <div id="obniz-debug"></div>
    <h1>Obniz Alarm Control</h1>
    <script>
        const SPREADSHEET_URL = 'https://api.steinhq.com/v1/storages/***'; //***はスプレッドシートIDを入力
        const LIGHT_THRESHOLD = 0.2;
        const obnizId = 'YOUR_OBNIZ_ID';
        const obniz = new Obniz(obnizId);
        let buzzerStartTime = null;
        let speaker = null;

        let currentAlarmTimeout = null;

        async function getAlarmTime() {
            try {
                const response = await axios.get(`${SPREADSHEET_URL}/Sheet1`);
                console.log("Response from Google Sheets:", response.data);
                if (!response.data || !response.data[5] || !response.data[5].value) {
                    throw new Error('Missing or invalid data structure in the response');
                }
                const alarmTimeString = response.data[5].value;
                console.log("Alarm time from B7:", alarmTimeString);
                const alarmTime = moment(alarmTimeString, "HH:mm");
                if (!alarmTime.isValid()) {
                    throw new Error('Invalid time format in B7 cell');
                }
                return alarmTime;
            } catch (error) {
                console.error('Error fetching alarm time:', error.message);
                if (error.response) {
                    console.error('Error response data:', error.response.data);
                }
                return null;
            }
        }

        async function checkAlarmTime() {
            const alarmTime = await getAlarmTime();
            if (alarmTime === null) return;
            const now = moment();
            let timeUntilAlarm = moment.duration(alarmTime.diff(now));

            if (timeUntilAlarm.asMilliseconds() < 0) {
                timeUntilAlarm.add(1, 'day');
            }

            console.log(`Alarm will sound in ${timeUntilAlarm.asMilliseconds()} milliseconds`);

            if (currentAlarmTimeout) {
                clearTimeout(currentAlarmTimeout);
            }
            
            currentAlarmTimeout = setTimeout(() => {
                speaker = obniz.wired("Speaker", {signal:0, gnd:1});
                speaker.play(440, 0.5);
                buzzerStartTime = moment();
            }, timeUntilAlarm.asMilliseconds());
        }

        obniz.onconnect = async () => {
            await checkAlarmTime();
            setInterval(checkAlarmTime, 60000);
            obniz.ad3.start((value) => {
                if (value > LIGHT_THRESHOLD && buzzerStartTime) {
                    if (speaker) {
                        speaker.stop();
                    }
                    const buzzerStopTime = moment();
                    const durationMs = buzzerStopTime.diff(buzzerStartTime);
                    const duration = (durationMs / 1000).toFixed(3);
                    updateDurationInSpreadsheet(duration);
                    console.log(duration);
                    buzzerStartTime = null;
                }
            });
        };

        // 修正された updateDurationInSpreadsheet 関数
        async function updateDurationInSpreadsheet(duration) {
            try {
                // B8セルに実行時間を記録 (この処理は以前と同じ)
                const durationResponse = await axios.put(`${SPREADSHEET_URL}/Sheet1`, {
                    "condition": { "device": "Duration" },
                    "set": { "value": duration }
                });
                console.log('Duration updated successfully in B8:', durationResponse.data);

                // 新しい行を追加し、T、duration、現在の日時を記録
                const currentTime = moment().format('YYYY-MM-DD HH:mm:ss');
                const newRowResponse = await axios.post(`${SPREADSHEET_URL}/Sheet1`, [{
                    "device": "T",
                    "value": duration,
                    "last update": currentTime
                }]);
                console.log('New row added successfully:', newRowResponse.data);
            } catch (error) {
                console.error('Error updating spreadsheet:', error.message);
                if (error.response) {
                    console.error('Error response data:', error.response.data);
                }
            }
        }

        obniz.connect();
    </script>
</body>
</html>
```
</details>

## あとがき
https://qiita.com/vanchan2625/items/3445b546e44649742c71

https://qiita.com/vanchan2625/items/3892184ef8b37bf40ce9

今回は今までとは打って変わって **[アート思考](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%BC%E3%83%88%E6%80%9D%E8%80%83)** ではなく、**[デザイン思考](https://ja.wikipedia.org/wiki/%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E6%80%9D%E8%80%83)** のモノづくりに挑戦しましたが、アイデア出しには非常に苦労しました。

普段の何気ない生活に**疑問を持つ**ことが**デザイン思考**につながるのではないかと感じました。

## WISH
~~実は上記のソースコードでは、アラームが鳴ってからアラームを止める秒数を計測しており、その時間をGoogleスプレッドシートのB8セルに転記するようになっています。~~
~~その時間をLINE Botからユーザーに返すことで、起床を**タイムアタック**のように楽しめるのではないかと考えていました。~~
~~今回は、その転記した秒数をユーザーに返すという機能まで実装しきれず、悔しい結果となってしまいました・・・~~
~~また次回以降挑戦していきたいですね。~~

アラームを止めた秒数を通知する機能を追加することができました。

:::note warn
ただ、この方法ではアラームを止めた秒数の通知まで **最大1分間** かかってしまいます・・・
:::
もっとユーザーが使いやすいものを目指していきたいですね。
それこそがデザイン思考に通ずるのではないかと思う次第です。

### <font color="red">修正1</font>

:::note info
以下のコードに修正することで **最大1分間の待ちが不要** になります。
:::

簡単に解説すると上記[makeのシナリオ](https://qiita.com/vanchan2625/items/00b9caeb1dadd36d6f5e#make%E3%81%AE%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA)では、
* シナリオ1でアラームを止めた秒数をGoogleスプレッドシートに転記
↓
* シナリオ2でアラームを止めた秒数の更新を **1分間隔** で監視する仕組みでした。

そんな回りくどいことをせず、obnizから直接LINE Botに秒数を返せばいいのでは？と思うかもしれませんが、
obniz開発者コンソールのWebブラウザ上からは **CORS(Cross-Origin Resource Sharing)に阻まれて、Webブラウザから直接LINE Botにメッセージを送れない** ため上記のような仕組みとなっていました。

CORSに関しては以下の記事が分かりやすいです。

https://qiita.com/TaikiTkwkbysh/items/ec1f9163d286a94c929d

#### 改善内容
* makeで アラームを止めた秒数をユーザーに返す **中継API** を作成する(シナリオ2の先頭のモジュールをWebhookに変えるだけ）
* obnizがアラームを止めた秒数をGoogleスプレッドシートに転記するとともに、作成した中継APIを呼び出す
* 転記した秒数がLINE Botから送られる

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/ea774186-f0d9-f1fa-6023-55a121135e9b.jpeg">

### <font color="red">修正2</font>
修正2では以下 **2点** の修正を行っていきます。

1. 上記では、Googleスプレッドシートからアラーム時刻を取得したり、逆にアラーム実行時間を記録するために **axios** のライブラリを使用しています。
しかし **axios** はライブラリのインストールが必要ということに加え、ファイルサイズが大きいという点がデメリットになります。
そこで **fetch** を使えば、JavaScriptの標準機能として組み込まれているので、追加のライブラリをインストールする必要はありません。

2. また、日付や時刻の取得は **Moment.js** を使用しています。
しかし **Moment.js** は開発が既に終了していることに加え、ファイルサイズが大きいという点がデメリットになります。
そこで **Day.js** を使えば、いまも活発に開発が進められているため、新規機能の追加などにも柔軟に対応することができます。

#### 修正したソースコード
<details><summary>←の三角マークを押すと確認することができます。</summary>

```html:obniz
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Obniz Alarm Control</title>
    <script src="https://unpkg.com/obniz@3.31.0/obniz.js"></script>
    <script src="https://unpkg.com/dayjs@1.10.7/dayjs.min.js"></script>
    <script src="https://unpkg.com/dayjs@1.10.7/plugin/customParseFormat.js"></script>
    <script src="https://unpkg.com/dayjs@1.10.7/plugin/duration.js"></script>
    <script src="https://unpkg.com/dayjs@1.10.7/plugin/utc.js"></script>
    <script src="https://unpkg.com/dayjs@1.10.7/plugin/timezone.js"></script>
</head>
<body>
    <div id="obniz-debug"></div>
    <h1>Obniz Alarm Control</h1>
    <script>
        dayjs.extend(dayjs_plugin_customParseFormat);
        dayjs.extend(dayjs_plugin_duration);
        dayjs.extend(dayjs_plugin_utc);
        dayjs.extend(dayjs_plugin_timezone);

        const SPREADSHEET_URL = 'https://api.steinhq.com/v1/storages/6678afaa4d11fd04f00baaeb';
        const LIGHT_THRESHOLD = 0.2;
        const obnizId = 'YOUR_OBNIZ_ID'; // あなたのobniz IDに置き換えてください
        const WEBHOOK_URL = 'https://hook.us1.make.com/wonmuw5mrp3noj1ngv3buux751vyavbs';
        const obniz = new Obniz(obnizId);
        let buzzerStartTime = null;
        let speaker = null;

        let currentAlarmTimeout = null;

        async function getAlarmTime() {
            try {
                const response = await fetch(`${SPREADSHEET_URL}/Sheet1`);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                const data = await response.json();
                console.log("Response from Google Sheets:", data);
                if (!data || !data[5] || !data[5].value) {
                    throw new Error('Missing or invalid data structure in the response');
                }
                const alarmTimeString = data[5].value;
                console.log("Alarm time from B7:", alarmTimeString);
                
                const alarmTime = dayjs(alarmTimeString, "HH:mm");
                if (!alarmTime.isValid()) {
                    throw new Error('Invalid time format in B7 cell');
                }
                return alarmTime;
            } catch (error) {
                console.error('Error fetching alarm time:', error.message);
                return null;
            }
        }

        async function checkAlarmTime() {
            const alarmTime = await getAlarmTime();
            if (alarmTime === null) return;
            const now = dayjs();
            let timeUntilAlarm = dayjs.duration(alarmTime.diff(now));

            if (timeUntilAlarm.asMilliseconds() < 0) {
                timeUntilAlarm = dayjs.duration(alarmTime.add(1, 'day').diff(now));
            }

            console.log(`Alarm will sound in ${timeUntilAlarm.asSeconds().toFixed(2)} seconds`);
            displayAlarmTime(alarmTime);

            if (currentAlarmTimeout) {
                clearTimeout(currentAlarmTimeout);
            }
            
            currentAlarmTimeout = setTimeout(() => {
                speaker = obniz.wired("Speaker", {signal:0, gnd:1});
                speaker.play(440, 0.5);
                buzzerStartTime = dayjs();
                console.log("Alarm start");
                obniz.display.clear();
                obniz.display.print("Alarm start");
            }, timeUntilAlarm.asMilliseconds());
        }

        function displayAlarmTime(alarmTime) {
            obniz.display.clear();
            obniz.display.print(`Alarm at ${alarmTime.format('HH:mm')}`);
        }

        obniz.onconnect = async () => {
            console.log("Obniz connected");
            await checkAlarmTime();
            setInterval(checkAlarmTime, 60000);
            obniz.ad3.start((value) => {
                if (value > LIGHT_THRESHOLD && buzzerStartTime) {
                    if (speaker) {
                        speaker.stop();
                    }
                    const buzzerStopTime = dayjs();
                    const durationMs = buzzerStopTime.diff(buzzerStartTime);
                    const duration = (durationMs / 1000).toFixed(3);
                    console.log(`Alarm duration: ${duration} seconds`);
                    obniz.display.clear();
                    obniz.display.print(`Duration: ${duration}s`);
                    updateDurationInSpreadsheet(duration)
                        .then(() => {
                            setTimeout(() => sendToWebhook(duration), 1000);
                            setTimeout(() => checkAlarmTime(), 10000);  // 10秒後に次のアラーム時刻を表示
                        })
                        .catch(error => {
                            console.error('Error in update and webhook process:', error);
                        });
                    buzzerStartTime = null;
                }
            });
        };

        async function updateDurationInSpreadsheet(duration) {
            try {
                const currentTime = dayjs().tz("Asia/Tokyo").format('YYYY-MM-DD HH:mm:ss');
                const response = await fetch(`${SPREADSHEET_URL}/Sheet1`, {
                    method: 'PUT',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        "condition": { "device": "Duration" },
                        "set": { 
                            "value": duration,
                            "last update": currentTime
                        }
                    })
                });
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                console.log('Google Spreadsheet update successful.');
                return true;
            } catch (error) {
                console.error('Error updating spreadsheet:', error.message);
                return false;
            }
        }

        async function sendToWebhook(duration) {
            try {
                const response = await fetch(WEBHOOK_URL, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ duration: duration })
                });
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                console.log('Webhook call successful');
                return true;
            } catch (error) {
                console.error('Error sending data to webhook:', error);
                return false;
            }
        }

        obniz.connect();
    </script>
</body>
</html>
```
</details>