この記事は、SORACOM Advent Calendar 2021 の18日目の記事です。
今回はだいぶ前に買ったんですが、色々とやっているうち、セットアップしたものの、放置しっぱなしになっていた(汗)、
LTE-M CO2センサー RS-LTECO2 スターターキットを使って、室内監視をしてみた話になります。
LTE-M CO2センサー RS-LTECO2 スターターキットとは。
ラトックシステム株式会社が製造しているCO2センサーです。
愛用しているGPSマルチユニットとは違い、リファンレスデバイスではなく、
ラトックシステム株式会社の委託販売商品となっているようです。
LTE-M CO2センサー RS-LTECO2 スターターキット
LTE-M版はSORACOMのIoTストアなどでしか買えない製品ですが、
Wifi版があり、そちらは、Amazon.comなどでも購入可能です。
それがこちらです。ちょっとだけお安い。
RS-WFCO2
あと、
PM2.5計測や気圧も計測できる上位機種もあるみたいですね。
こっちもLTE-M版出ないかなと思ったりしてます。
RS-WFEVS1
搭載している各種センサー系は、
スイスに本社を置く、センシリオン社製のセンサーを搭載しているとのこと。
CO2に関しては、PA方式(光音響方式)で精度の高いCO2濃度計測が可能だそうです。
PA方式って何って方は、センサーを作っているセンシリオン社に、まとまっていたので、ご覧ください(読んでも理解できてない人です)
PASens®技術
セットアップしてみる
実際の手順に関しては、こちらに詳細があるので、割愛します。
SORACOM IoT レシピ:IoTで、CO2と温湿度を計測し換気促進
手順にも書いてありますが、
SIMを押し込むのがかなり難しかったですね。
SIMを入れて、電源を入れ、ユーザーコンソールで、SIMグループの設定(バイナリパーサー設定、Harverst設定)
あとはセンサーデータが蓄積されていきます。
あら簡単です。
Hervest & Lagoonで可視化
まず送られるデータですが、
- CO2(co2)
- 温度(temp)
- 湿度(humid)
- 送信間隔(interval)
の4つです。
送信間隔については、設定値なので、それ以外の値を可視化することになります。
Harvest、Lagoonで可視化すると、こんな感じです。
Harvestで可視化
2021/12/17の0:00〜23:59までのグラフになります。
(これ活動時間がバレバレだな・・・。)
まとめて出すこともできますが、CO2の値が他の値に比べて大きいので、分けています。
※大きすぎるため、温度と湿度がほぼ平行になってしまう。
GPSマルチユニットとの比較
ここで、他製品と比べてみます。
同じように手軽に温湿度(あと位置情報)を可視化することができる便利なデバイスとして、
GPSマルチユニット SORACOM Edition
があります。
上がGPSマルチユニット SORACOM Edition
その下にあるのが、CO2センサーです。
GPSマルチユニットのセンサー仕様ですが、以下のようになっています。(赤枠内)
※製造元の京セラさんの製品仕様書からの抜粋
温度に関しては、最大値は同じですが、最低値は
GPSマルチユニットは-20℃、CO2センサーは5℃となっています。
自分が使う分には問題なさそう(マイナス値になることはそんなにない)ですが、
マイナス値もちゃんと取得したい場合は、
ただ、精度に関しては、GPSマルチユニットは、
±2.0℃、(バッテリー駆動中)、±3.5℃(USB給電中)、CO2センサーの方が±0.2℃なので、精度に関しては、
CO2センサーのほうが良さそうです。
湿度に関しては、測定レンジ、精度含めて同じですね。実は同じセンサーなのかな?
Lagoon上で2つのデータを比べてみます。
CO2センサーは、アンテナ上の部分にセンサーがついている(あれアンテナだと思ってた人)ので、
上の写真の通り、大体同じような位置になる(無理があるかな)ので、近しい値になるかと思いますが・・・如何に。
今回は、先ほどと同じく2021/12/17ですが、GPSマルチユニットの稼働時間が、10:00〜20:00(最終送信は20:03頃)なので、
10:00〜20:59までにしています。
- 温度
薄い青がGPSマルチユニット、濃い青がCO2センサーの値になります。
概ねGPSマルチユニットは1度ほど高い値を示しているようです。
精度を考えると、大体同じになるのかな???
- 湿度
薄いオレンジがGPSマルチユニット、濃いオレンジがCO2センサーの値になります。
GPSマルチユニットが最大6%ぐらい高い値を示すという結果になりました。
無論、逆に低い時もあるので、この辺もセンサー精度の差かもしれませんね。
置き方を変えると数値が変わるとかあるのかなと思いますが、この辺は別途検証するかもしれません。
通知を飛ばす
今回ここがメインです。
このデバイスからのデータを使ってアラート発砲したい場合は、以下の3つになるかなと思います。
- 熱中症警告(夏季向け)
- 乾燥注意(冬季向け)
- CO2濃度警報
上の2つはGPSマルチユニットと同じですね。
ここで1つ問題が発生しました。
上記した通り、
このデバイスは、電源が入っている限りは、一定間隔でデータを送信します。
※電源落とせばいい話ですが、それは人力なので一応避けます。
そして、現状、CO2濃度が高まるのは、夜間寝ている時間なのです。
正直寝ている時に通知がこられても困ります。
以前、このデバイスの勉強会があった際に、時間制御できないものかという質問もしましたが、
残念ながらできないという回答をいただきました。
なので、今回は、SORACOM FunkからAWS Lambdaを呼び出して、
Lambda内で計算および通知(送信制御含む)を行いたいと思います。(前置き長い)
なお、GPSマルチユニットを使う際はSORACOM Orbitを使って、
Orbit内で、温湿度の値を元に計算して、Lagoonから通知を行うようにしています。
詳細はGPSマルチユニットを使って、そろそろ冷房つけたらを通知するをご覧ください。
間隔を変えてみる
まず、現状5分の通知感覚を変えてみます(Lambdaの実行回数を減らそうかと・・・w)
方法は、SORACOM Airのメタデータサービスを有効にして、SIMのタグintervalというキーに、通知間隔値(分)をセットします。
AWS Lambdaの実装
SORACOM Funkから呼び出すAWS Lambda関数の実装自体はこんな感じです。(TypeScript)
突貫工事で作ったので、ちょっと微妙なところは直して、後日Githubにて公開します。
なお、最近多用しているAWS CDKのv2が最近出たので、それでデプロイしています。
処理として行っていることは、
- Line Notify用のToken取得
- 通知時間チェック
- 熱中症警告チェック(WBGT値チェック)通知可否
- 乾燥チェック(絶対湿度チェック)通知可否
- WBGT値計算
- 絶対湿度計算
- LINEへのメッセージ生成
- 通常メッセージ
- CO2濃度警告通知
- 熱中症警告通知
- 乾燥警告通知
- LINEにメッセージ送信
です。
通知可否のチェックとして、
そもそもの通知可否は時間、
熱中症警告チェック(WBGT値チェック)と乾燥チェック(絶対湿度チェック)は、月でチェックするようにしています。
それら期間はLambdaの環境変数に持たせて、制御できるようにしてます。ああ、昔より便利になったわ・・・(トオイメ)
月だけ、年跨ぎするようになってたりしていますが、他にいい実装あるかも。
CO2濃度、熱中症警告チェック(WBGT値チェック)と乾燥チェック(絶対湿度チェック)は、
閾値を、同じくLambdaの環境変数に持たせて、その閾値と比較して、出す文字列を変えています。
LINEへの通知部と通知で使うLINE Notify用のToken取得部は、
JAWS PANKRATION用に開発したLambda関数で使っているものの流用です。
https://github.com/Kenichiro-Wada/amazon-location-service-geofence-system
const moment = require('moment-timezone');
moment.tz.setDefault('Asia/Tokyo');
const AWSXRay = require('aws-xray-sdk');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
AWS.config.update({ region: 'ap-northeast-1' });
const ssm = new AWS.SSM();
const axios = require('axios');
const qs = require('querystring');
AWS.config.logger = console;
/**
* 定数
*/
const alertSendTimeRange: string = String(process.env.ALERT_TIME_RANGE);
const wbgtAlertSendMonthRange: string = String(
process.env.WBGT_ALERT_MONTH_RANGE
);
const absHumidAlertSendMonthRange: string = String(
process.env.ABS_HUMID_ALERT_MONTH_RANGE
);
const co2WarningLevel: number = Number(process.env.CO2_WARNING_LEVEL);
const co2AlertLevel: number = Number(process.env.CO2_ALERT_LEVEL);
const wbgtWarningLevel: number = Number(process.env.WBGT_WARNING_LEVEL);
const wbgtAlertLevel: number = Number(process.env.WBGT_ALERT_LEVEL);
const dryWarningLevel: number = Number(process.env.DRY_WARNING_LEVEL);
const dryAlertLevel: number = Number(process.env.DRY_ALERT_LEVEL);
exports.sendNotificationHandler = async function (event: any, context: any) {
console.log('event:', JSON.stringify(event, null, 2));
console.log('context:', JSON.stringify(context, null, 2));
const lineNotifyToken = await getLineNotifyToken();
const co2 = event.co2;
const temp = event.temp;
const humid = event.humid;
const now: any = moment();
// 通知可否チェック
const isAlert = await checkAlertTime(now);
const isAlertWbgt = await checkWbgtAlertMonth(now);
const isAlertDry = await checkDryAlertMonth(now);
let messageArray: string[] = [];
messageArray.push(await createMessageSencorData(temp, humid, co2));
if (isAlertWbgt) {
const wbgt: number = await calcWbgt(temp, humid);
console.log('wbgt:' + wbgt);
messageArray.push(await createMessageWbgt(wbgt));
}
if (isAlertDry) {
const absHumid: number = await calcAbsHumid(temp, humid);
console.log('absHumid:' + absHumid);
messageArray.push(await createMessageAbsHumid(absHumid));
}
messageArray.push(await createMessageCo2(co2));
if (isAlert) {
const message: string = messageArray.join('\n');
const result: boolean = await sendNotifyMessage(lineNotifyToken, message);
if (result) {
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'Line Notify Send Successful.',
},
null,
2
),
};
} else {
return {
statusCode: 500,
body: JSON.stringify(
{
message: 'Line Notify Send Error.',
},
null,
2
),
};
}
} else {
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'Nothing Alert.',
},
null,
2
),
};
}
};
/**
* SSMからLINE Notify用トークン取得
* @returns トークン
*/
async function getLineNotifyToken(): Promise<string> {
const ssmRequest = {
Name: process.env.LINE_NOTIFY_TOKEN,
WithDecryption: true,
};
const response: any = await ssm.getParameter(ssmRequest).promise();
return String(response.Parameter.Value);
}
/**
* WBGT計算
* @param temp
* @param humid
* @returns wbgt値
*/
async function calcWbgt(temp: number, humid: number): Promise<number> {
return Math.round(
0.735 * temp + 0.0374 * humid + 0.00292 * temp * humid - 4.064
);
}
/**
* 絶対湿度計算
* @param temp
* @param humid
* @returns 絶対湿度
*/
async function calcAbsHumid(temp: number, humid: number): Promise<number> {
const ePow = (7.5 * temp) / (temp + 237.3);
const e = 6.1078 * Math.pow(10, ePow); // 飽和水蒸気圧
const a = (217 * e) / (temp + 273.15); // 飽和水蒸気量
const rhP = humid / 100;
const absHumid = a * rhP; // 絶対湿度
return Math.round(absHumid);
}
/**
* 通知期間チェック
* @param now
* @returns
*/
async function checkAlertTime(now: any): Promise<boolean> {
const hour: number = now.hour();
const startTime: number = Number(alertSendTimeRange.substring(0, 2)); // 通知開始時
const endTime: number = Number(alertSendTimeRange.substring(2)); // 通知終了時
console.log(
`現在時刻:${hour} 通知開始時間:${startTime} 通知終了時間:${endTime}`
);
if (startTime <= hour && hour <= endTime) {
return true;
} else {
return false;
}
}
/**
* WBGT値通知期間チェック(夏季)
* @param now
* @returns
*/
async function checkWbgtAlertMonth(now: any): Promise<boolean> {
const month: number = now.month() + 1;
const startMonth: number = Number(wbgtAlertSendMonthRange.substring(0, 2)); //通知開始月
let endMonth: number = Number(wbgtAlertSendMonthRange.substring(2)); //通知終了月
if (endMonth <= startMonth) { // 1003ってなった場合は12足してますが、こっちはいらないかも
endMonth = endMonth + 12;
}
console.log(
`現在月:${month} WBGT通知開始月:${startMonth} WBGT通知終了月:${endMonth}`
);
if (startMonth <= month && month <= endMonth) {
return true;
} else {
return false;
}
}
/**
* 絶対湿度通知期間チェック(冬季)
* @param now
* @returns
*/
async function checkDryAlertMonth(now: any): Promise<boolean> {
const month: number = now.month() + 1;
const startMonth: number = Number(
absHumidAlertSendMonthRange.substring(0, 2)
); // 通知開始月
let endMonth: number = Number(absHumidAlertSendMonthRange.substring(2)); // 通知終了月
if (endMonth <= startMonth) { // こちらも1003ってなった場合は12足してます
endMonth = endMonth + 12;
}
console.log(
`現在月:${month} 絶対温度通知開始月:${startMonth} 絶対温度通知終了月:${endMonth}`
);
if (startMonth <= month && month <= endMonth) {
return true;
} else {
return false;
}
}
/**
* センサーデータ通知メッセージ
* @param temp
* @param humid
* @param co2
* @returns 通知メッセージ
*/
async function createMessageSencorData(
temp: number,
humid: number,
co2: number
): Promise<string> {
return `温度: ${temp} 湿度: ${humid} CO2濃度: ${co2}`;
}
/**
* CO2濃度警告通知メッセージ
* @param co2
* @returns
*/
async function createMessageCo2(co2: number): Promise<string> {
if (co2WarningLevel <= co2) {
if (co2AlertLevel <= co2) {
return `CO2濃度: ${co2} \n CO2濃度が危険なレベルになっています。換気などをして、適切な温度にしましょう!`;
} else {
return `CO2濃度: ${co2} \n CO2濃度が高くなっています。換気などをして、適切な濃度にしましょう!`;
}
}
return '';
}
/**
* WBGT値通知メッセージ
* @param wbgt
* @returns 通知メッセージ
*/
async function createMessageWbgt(wbgt: number): Promise<string> {
let levelStr: string = '適切な室温です。';
if (wbgtWarningLevel < wbgt) {
if (wbgtAlertLevel < wbgt) {
levelStr =
'熱中症の危険があります。冷房をつけるなどして、室温を適切に保ちましょう!';
} else {
levelStr =
'熱中症に警戒しましょう。冷房をつけるなどして、室温を適切に保ちましょう!';
}
}
return `WBGT値: ${wbgt} \n ${levelStr}`;
}
/**
* 絶対湿度通知メッセージ
* @param absHumid
* @returns
*/
async function createMessageAbsHumid(absHumid: number): Promise<string> {
let levelStr: string = '適切な温度と湿度です。';
if (absHumid < dryAlertLevel) {
levelStr =
'空気が乾燥し、ウィルスが蔓延しやすくなっています。換気したり、加湿器を使用するなどでして、温度と湿度を適切に保ちましょう!';
} else {
if (absHumid < dryWarningLevel) {
levelStr =
'少し乾燥しているようです。換気したり、加湿器を使用するなどでして、温度と湿度を適切に保ちましょう!';
}
}
return `乾燥指数: ${absHumid} ${levelStr}`;
}
/**
* LINE Notify通知部
* @param lineNotifyToken
* @param message
* @returns
*/
async function sendNotifyMessage(
lineNotifyToken: String,
message: string
): Promise<boolean> {
const lineNotifyUrl = 'https://notify-api.line.me/api/notify';
// リクエスト設定
const payload: { [key: string]: string } = {
message: message,
};
console.log('payload:', JSON.stringify(payload, null, 2));
const config = {
url: lineNotifyUrl,
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${lineNotifyToken}`,
},
data: qs.stringify({
message: message,
}),
};
// メッセージ送信
try {
const result = await axios.request(config);
console.log(result);
if (result.data.message === 'ok') {
return true;
} else {
return false;
}
} catch (error) {
console.error(error);
return false;
}
}
実際動かしてみると、こんな感じで通知がきます。(めっちゃテストデータです。)
うーん、もうちょっと綺麗にしたい。
終わり
CO2センサーを設定し、Lagoonで可視化、Funk経由でLine Notifyまで通知するところまでやってみました。
無論、計算をSORACOM Orbitでやり、Lagoonでアラート送信をすることもできますので、
そっちの方が手軽かなという気もしますが、
変な時間に警告くるのは避けられない気がします。(と思っているけど、手段あるかも)
ただ、今のままだと、休日だろう(現に来てる)と、15分起きにLINEから通知くるので、ここまで要らんかなといきなり思っているので、
本当にやばい時だけ通知するに改修しようかなと思います。
こちら、ちょっと来年早々にも改修して、再度アップデートします。
明日は、ソラコム片山先生の2年ぶりの新作「Sim City 2021」です。期待しかないです!