SORACOM RS-LTECO2 で計測したCO2値を kintone で確認する

![CO2-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/0a44d023-f1c4-5140-c338-5305188308f9.png)

## 概要
ちょっと前に SORACOM RS-LTECO2 を購入して、以下を参考に自宅の仕事場のCO2濃度や室温などのデータを SORACOM Lagoon で可視化までやっていました。

**SORACOM レシピ：IoTで、CO2と温湿度を計測し換気促進**
https://soracom.jp/recipes_index/9972/
![CO2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/f9293439-3c2b-8e6c-b26b-b0d94741ce3e.png)
とはいえ、確認のため SORACOM Lagoon をいちいち起動するのは手間で、普段使いしている kintone で CO2濃度や室温などのデータを確認できるように可視化してみました。

## SORACOM RS-LTECO2 について
SORACOM RS-LTECO2 は SORACOM さんが委託販売しているラトックシステム株式会社のCO2や温度、湿度が計測できるセンサ機器です。白でおしゃれなコンパクトな筐体にLEDで現在のCO2濃度を知らせる機能があり、SORACOM の SIM を差し込むことで簡単な設定で SORACOM Lagoon を利用して可視化が可能です。 
![IMG_1811.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/5d3bebef-f693-8054-5696-dce0fa7ef330.jpeg)
僅かなサイズのバイナリーデータを指定間隔でLTE-M通信でバイナリーデータで送信するため、通信料も気にせずリーズナブルに利用できます。またUSB給電で、USBバッテリーなどと一緒に持ち運びして色々な場所で計測可能です。SORACOMのサイト、以下のメーカサイトのwifi版製品とともに消費電力のデータが無かったため計算はできませんでしたが、USBバッテリーでそれなりに長い時間運用可能ではと推測しています。

**wifi版製品 RS-WFCO2**
https://www.ratocsystems.com/products/subpage/wfco2.html

## SORACOM の設定
先に紹介した[「SORACOM レシピ：IoTで、CO2と温湿度を計測し換気促進」](https://soracom.jp/recipes_index/9972/)を参考に、SORACOM Harvest Data の設定 まで行ってください。但し、SORACOM Harvest Dataの有効化 は以降のSORACOM Lagoon で可視化までやらずに、kintone のみで確認する場合は必須ではありません。

## kintone の準備
kintone の複数スレッドを持つ適当なスペースに以下の項目を持つアプリを追加します。**「お知らせ」を利用しますので、必ず複数スレッドを持つスペースでアプリを作成**します。

|フィールド名|種類|フィールドコード|その他|
|:--|:--|:--|:--|
|計測日時|日時|dateTime||
|CO2|数値|co2|少数以下0|
|温度|数値|temp|少数以下1|
|湿度|数値|humid|少数以下1|
|計測間隔|数値|interval|少数以下0|

![CO2-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/28c545ee-33f0-cb3c-4d33-71a359f52e51.png)
フォーム以外にはAPIトークンの発行を行います。その他は適宜以下のような今日のCO2を表示するグラフを作成しておくと便利でしょう。
![CO2-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/2fd22024-c9b7-56d9-1634-36e2bef15b28.png)
APIトークンは次の AWS Lambda で SORACOM RS-LTECO2 より受け取ったデータを kintone 追加するために使用します。
![CO2-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/cc8f4a98-026a-8bb5-0ad6-c9f9bdfbe567.png)
以上で、一旦 kintone の準備は完了です。 

## AWS Lambda の準備
AWS 側の事前準備として、アカウントが必要になります。こちらの設定は以前 SORACOM GPS マルチユニット で説明した内容と同じですので、以下を参照ください。

**AWS アカウントの追加**
https://qiita.com/yukataoka/items/c07b78f5151c29ac8858#aws-%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E8%BF%BD%E5%8A%A0
**ユーザの追加に成功したら、アクセスキーIDと、シークレットアクセスキーは以降の設定で必要になりますので、控えておきます。**

AWS Lambda で以下のように関数を作成します。
![CO2-30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/aabb5931-4bf1-6d4c-723f-87f2bcdc2722.png)
「一から作成」を選択し、適当な「関数名」を設定し、「ランタイム」（言語）は最新の Node.js を選択して関数を作成します。
![CO2-31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/e1cef9ef-9bf1-e9ff-425e-c5f74ad0c988.png)
**関数のARNは以降の設定で必要になりますので、控えておきます。**
コードは以下になります。

```index.js
'use strict';

const request = require('request-promise');
const moment  = require("moment");

const Url     = "https://" + process.env['subdomain'] + ".cybozu.com/k/v1/record.json";
const AppId   = process.env['appid'];
const Token   = process.env['token'];

exports.handler = async function(event, context, callback) {
    console.log('Function Start.');
    
    // 取得したデータをkintone用のJSONに編集
    const dateTime = moment().format("YYYY-MM-DDTHH:mm:ssZ");
    const json = {
        "dateTime" : { "value" : dateTime },
        "co2"      : { "value" : event.co2 },
        "temp"     : { "value" : event.temp },
        "humid"    : { "value" : event.humid },
        "interval" : { "value" : event.interval },
    };
    
    // kintoneのアプリにデータを追加
    await PostKintoneRecode(request, Url, AppId, Token, json);
    
    console.log('Function Stop.');
};

// kintoneにデータを追加
async function PostKintoneRecode(request, url, appId, token, json)
{
    try {
        const options = {
            url: url,
            method: 'POST',
            headers: {
                'Content-type': 'application/json',
                'X-Cybozu-API-Token': token
            },
            json: { app : appId, record: json },
        };
        await request(options);
        return true;
    } catch (err) {
        console.error(JSON.stringify(err));
        return false;
    }
}

```

event の引数で受け取った CO2 などの値をAPIで追加するだけで、簡単に kintone にデータを追加できます。

AWS Lambda のコードには、以下の node.js のパッケージ環境 request-promise と moment を追加する必要があります。
https://www.npmjs.com/package/request-promise
https://www.npmjs.com/package/moment
request-promise は現在非推奨になっています。本格的に利用する場合は request-promise を使わず Aysnc/Await を使って HTTP リクエストを使って実装する方が良いでしょう。

Lambda で node.js のパッケージを使う説明は割愛しますが、以下などを参考に設定すると良いでしょう。
**Lambda の Node.js でもっといろんなパッケージを使いたいとき**
https://tech-lab.sios.jp/archives/9017
**AWS Lambda Layersでnode_modulesを使う**
https://xp-cloud.jp/blog/2019/01/12/4630/

その他の設定ではタイムアウトがデフォルトでは短すぎるので、15秒程度に値を変更しておくと良いでしょう。
![CO2-32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/431d986b-b933-54da-c5d4-e23142975856.png)
kintone の環境が変わってもコードを変更しなくて済むように、process.env['キー'] で環境変数から設定値を取得するようにしています。
![CO2-33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/122662a5-7306-727d-febd-51f09749fcf2.png)

|キー|値|
|:--|:--|:--|
|appid|先に追加した kintone のアプリID|
|subdomain|kintone のサブドメイン xxxx.cybozu.com の xxxx の部分|
|token|先に追加した kintone アプリのAPIトークン|

以上の設定が完了したら、SORACOM のコンソール画面で SORACOM Funk の設定を行います。

## SORACOM Funk の設定
SORACOM のコンソール画面で、先の「SORACOM の設定」SIMグループで作成したSIMグループの設定を変更します。
![CO2-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/5e0e8fd7-8ac4-9156-b2b9-2c2b454d0e50.png)

「SORACOM Air for セルラー設定」のバイナリパーサーが以下のように設定されているか確認します。
![CO2-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/7932a93c-9c1b-f33c-973e-e28bdf772b28.png)

```
co2:0:uint:16 temp:2:int:16:/10 humid:4:uint:16:/10 interval:6:uint:8
```

「SORACOM Air for セルラー設定」で、以下のように設定します。
![CO2-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/4236652b-27ef-ea00-cae5-64c32e21c3e8.png)
関数のARMは先に控えたものを利用します。認証情報の設定については、先に控えたアクセスキーIDと、シークレットアクセスキーを使って以下を参照に行います。

**SORACOM コンソールで SORACOM Funkの設定**
https://qiita.com/yukataoka/items/c07b78f5151c29ac8858#sim%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%81%AE%E8%BF%BD%E5%8A%A0%E3%81%A8%E8%A8%AD%E5%AE%9A

以上を完了すると、kintone のアプリにデータが追加されるようになります。
![CO2-15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/80a48d82-7e27-f6cd-7272-ae4d142a83e1.png)

## kintone のスペースの「お知らせ」に表示
kintoneアプリを開かないとCO2などのデータを確認できないのは面倒ですので、kintone の JavaScript カスタマイズでよく利用するスペースの「お知らせ」にデータを表示できるようにします。但し、この作業には kintone の管理者権限が必要です。

kinotne のカスタマイズではスペース表示イベントを取得できるようになりましたので、このイベントのタイミングでスペースの「お知らせ」に最新のCO2等のデータを表示するようにカスタマイズします。

**スペース表示イベント**
https://developer.cybozu.io/hc/ja/articles/900006291023

スペースの「お知らせ」に何かを表示させるためには、HTML の DOM 構造を解析して、データを表示したい部分の id か class に表示したい内容を追加します。但し、kintone の仕様変更があった場合は影響を受ける可能性が高く推奨できる方法ではありません。ですが他に手がなく現状ではこの方法を使って実装しています。とはいえ、全ての DOM 構造を解析するのは大変なので、今回は以下を参考に kintone-space-events.js を利用させていただきました。

**kintone ポータルイベントが追加されたので、スペースイベント作ってみた。**
https://qiita.com/Naoto00/items/a87312d5168de63543e5

後は、日付処理に Moment.js の後継的なライブラリにあたる Luxon を利用しています。

**Luxon を使って kintone の日付や日時フィールドのフォーマットをカスタマイズする**
https://developer.cybozu.io/hc/ja/articles/900000985463

```kintoneTop1.js
(function() {
    "use strict";
    
    // スペースの表示イベント処理
    const space = new SpaceCustomize(8, 'kinotoeのサブドメイン');
    kintone.events.on('space.portal.show', function(event) {
         funcEnvSetInfomation();
        return event;
    });
    
    // スペースのお知らせに最新の環境計測情報を表示
    function funcEnvSetInfomation(){
        
        // LTE-M CO2センサーサンプル（SORACOM）アプリから環境情報を取得しスペースのお知らせに表示
        let params = {
            'app': 'kintoneアプリのID',
            'query': ' order by dateTime desc limit 1 ',
        };
        
        // 最新の環境計測情報をアプリから取得
        kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params, function(resp) {
            if (resp.records[0] !== null) {
                
                // 取得したデータから表示内容を編集
                const dt = luxon.DateTime.fromSQL(resp.records[0]['dateTime'].value.replace('T', ' ').replace('Z', ''));
                let infoText = "<span style=\"font-weight:bold;font-size:120%;\">"+dt.plus({hours: 9}).toFormat('yyyy年MM月dd日 HH時mm分')+" 現在のオフィスの環境</span><br />";
                infoText += "CO2濃度："+resp.records[0]['co2'].value+" ppm（1000ppmを超えたら換気を！）<br />";
                infoText += "温度："+resp.records[0]['temp'].value+" ℃（28℃を超えたら熱中症に注意！）<br />";
                infoText += "湿度："+resp.records[0]['humid'].value+" ％<br />";
                infoText += "次回更新："+resp.records[0]['interval'].value+" 分後&nbsp;&nbsp;&nbsp;<a href=\"JavaScript:window.location.reload();\">表示更新</a><br />";
                
                // スペースのお知らせに情報を表示
                let info = space.getNtfWidget();
                let elmText = info.getElementsByClassName("gaia-argoui-widget-menu");
                elmText[0].style.display ="block";
                elmText[0].style.marginTop ="1em";
                elmText[0].style.marginLeft ="1em";
                let newElm = document.createElement("div");
                newElm.innerHTML = infoText;
                elmText[0].appendChild(newElm);
            }
        });
    }
})();
```

通常ならアプリの設定画面で JavaScript プログラムをアップロードしますが、スペースのお知らせをカスタマイズする場合は、以下の kinotne のシステム管理から行います。
![CO2-10a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/a7176a71-bff8-096c-e3d2-05c671d78564.png)
カスタマイズ表示の下の「JavaScript / CSSでカスタマイズ」を開きます。
![CO2-10b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/122af30b-49b8-ef97-74d7-0778c13e3530.png)
以下の画面でライブラリィの追加と、ソースコードファイルをアップロードします。
![CO2-10c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/e9e669fb-bd38-61ad-50a6-20a4a83d9a6d.png)
以上で以下のように、スペースの「お知らせ」にCO2などの計測結果を表示できるようになります。
![CO2-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/577c4966-d6d4-1bc7-c079-93c3d2f435e7.png)
グラフなどはお知らせの標準機能で編集して出力できますので、以下のように組み合わせると SORACOM Lagoon の表示に近づけることができます。
![CO2-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/0a44d023-f1c4-5140-c338-5305188308f9.png)

## おまけ（Windows アプリで通知）
普段PCに向かって仕事している間はCO2の上昇に気づきづらいため、Windowsフォームアプリケーション（.NET Freamwork）で以下のような通知する簡単なアプリを作成してみました。
![CO2-22a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/12edb46d-bdc1-2f2e-8982-7682a7232e74.png)
![CO2-22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75892/004e5454-7b15-4505-3473-c9aeb68dafc7.png)
興味ある方は以下にコードを公開しますので、お試しください。

### 設定メモ
・C# Windowsフォームアプリケーション（.NET Freamwork）のプロジェクト追加
・ConfigurationManager 参照追加
　「ソリューションエクスプローラー」→「参照」右クリック→「参照の追加」
　アセンブリ：フレームワーク内にある「System.Configuration」にチェックを入れて「OK」。
・NuGet で Newtonsoft.Json 13.0.1 を追加
　https://www.nuget.org/packages/Newtonsoft.Json/

### 設定ファイル

```App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="kintoneDomain" value="kintoneサブドメイン名" />
    <add key="kintoneToken" value="kintoneトークン" />
    <add key="kintoneAppId" value="kintoneアプリID" />
    <add key="logFile" value="c:\work\AlertFromKintone\Error.log" />
  </appSettings>
  <startup> 
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  </startup>
</configuration>
```

### フォーム

```Form1.cs
using System;
using System.Configuration;
using System.IO;
using System.Windows.Forms;

namespace AlertFromKintone
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            label1.Text = "";
            GetKintoneData();

            timer1.Interval = 10 * 60 * 1000;  // 10分毎に確認
            timer1.Start();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            GetKintoneData();
        }

        private void GetKintoneData()
        {
            DateTime dt = DateTime.Now;
            var dtNow = dt.ToString("yyyy/MM/dd HH:mm");
            dt = dt.AddMinutes(-11);
            var kintoneDt = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0);
            var queryDate = kintoneDt.AddHours(-9).ToString("yyyy-MM-ddTHH:mm:ssZ");

            var doQuery = " dateTime > \"" + queryDate + "\" order by dateTime desc";
            var appId = ConfigurationManager.AppSettings["kintoneAppId"];
            try
            {
                var json = KintoneTools.GetJsonData(doQuery, appId);
                if (json != null && json.totalCount > 0)
                {
                    var co2 = (int)json.records[0].co2.value;
                    if (co2 > 2500)
                    {
                        MessageBox.Show("室内のC02が " + co2.ToString() + "ppm です！\n危険ですので即座に換気してください！",
                            "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                    else if (co2 > 2000)
                    {
                        MessageBox.Show("室内のC02が " + co2.ToString() + "ppm です！\n体調不良を避けるため換気してください！",
                            "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                    else if (co2 > 1200)
                    {
                        MessageBox.Show("室内のC02が " + co2.ToString() + "ppm です！\n快適な環境にするため換気しましょう！",
                            "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }

                    var temp = (float)json.records[0].temp.value;
                    if (temp > 38)
                    {
                        MessageBox.Show("室内の気温が " + temp.ToString() + "℃ です！\n即座に冷房するか部屋から出てください！",
                            "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                    else if (temp > 34)
                    {
                        MessageBox.Show("室内の気温が " + temp.ToString() + "℃ です！\n冷房してください！",
                            "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                    else if (temp > 30)
                    {
                        MessageBox.Show("室内の気温が " + temp.ToString() + "℃ です！\n冷房しましょう！",
                            "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                    label1.Text = dtNow + " 確認　CO2 " + co2.ToString() 
                        + "ppm　気温 " + temp.ToString() + "℃  計測時刻 UTC " + json.records[0].dateTime.value;
                }
                else
                {
                    MessageBox.Show("kintone から最新のデータが取得できません！", 
                        "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Question);
                }
            }
            catch (Exception ex)
            {
                File.AppendAllText(ConfigurationManager.AppSettings["logFile"], 
                    dt.ToString("yyyy/MM/dd HH:mm:ss") + " " + ex.Message);
            }
        }
    }
}
```

### kintoneからデータを取得

```KintoneTools.cs
using Newtonsoft.Json;
using System;
using System.Configuration;
using System.IO;
using System.Net;
using System.Text;

namespace AlertFromKintone
{
    public static class KintoneTools
    {
        private static readonly string domain      = ConfigurationManager.AppSettings["kintoneDomain"];
        private static readonly string apiUrl      = "https://" + domain + ".cybozu.com/k/v1/records.json";
        private static readonly string apiSinglUrl = "https://" + domain + ".cybozu.com/k/v1/record.json";
        private static readonly string apiToken    = ConfigurationManager.AppSettings["kintoneToken"];
        private static readonly string logFilePath = ConfigurationManager.AppSettings["logFile"];

        public static dynamic GetJsonData(string query, string appId)
        {
            var respons = GetRespons(query, appId);
            if (respons == null || respons.Contains("<!DOCTYPE HTML>"))
            {
                return null;
            }
            return JsonConvert.DeserializeObject(respons);
        }

        private static string GetRespons(string query, string appId)
        {
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

            string url;
            if (query == "")
            {
                url = apiUrl + Uri.EscapeUriString("?app=" + appId + "&totalCount=true");
            }
            else
            {
                url = apiUrl + Uri.EscapeUriString("?app=" + appId + "&query=" + query + "&totalCount=true");
            }

            var request = WebRequest.Create(url);
            var enc = Encoding.GetEncoding("UTF-8");
            request.Headers.Add("X-Cybozu-API-Token:" + apiToken);
            try
            {
                using (var respons = request.GetResponse())
                {
                    using (var stream = respons.GetResponseStream())
                    {
                        var sr = new StreamReader(stream, enc);
                        string text = sr.ReadToEnd();
                        sr.Close();
                        stream.Close();
                        return text;
                    }
                }
            }
            catch (WebException)
            {
                DateTime dt = DateTime.Now;
                File.AppendAllText(logFilePath, dt.ToString("yyyy/MM/dd HH:mm:ss") 
                    + " " + "WebException. GET data = " + url + "\r\n");
                return null;
            }
        }
    }
}

```

## 参考情報
**LTE-M CO2センサー RS-LTECO2 スターターキット**
https://soracom.jp/store/12112/
** wifi版製品 RS-WFCO2 **
https://www.ratocsystems.com/products/subpage/wfco2.html
**SORACOM レシピ：IoTで、CO2と温湿度を計測し換気促進**
https://soracom.jp/recipes_index/9972/
**SORACOM GPS マルチユニット のデータを kintone に保存する**
https://qiita.com/yukataoka/items/c07b78f5151c29ac8858
**request-promise**
https://www.npmjs.com/package/request-promise
**moment**
https://www.npmjs.com/package/moment
**Lambda の Node.js でもっといろんなパッケージを使いたいとき**
https://tech-lab.sios.jp/archives/9017
**AWS Lambda Layersでnode_modulesを使う**
https://xp-cloud.jp/blog/2019/01/12/4630/
**スペース表示イベント**
https://developer.cybozu.io/hc/ja/articles/900006291023
**kintone ポータルイベントが追加されたので、スペースイベント作ってみた。**
https://qiita.com/Naoto00/items/a87312d5168de63543e5
**Luxon を使って kintone の日付や日時フィールドのフォーマットをカスタマイズする**
https://developer.cybozu.io/hc/ja/articles/900000985463
**NuGet で Newtonsoft.Json 13.0.1 を追加**
https://www.nuget.org/packages/Newtonsoft.Json/