はじめに
最近、暑くなってきましたね。私の住んでいる地域では先日最高気温が早くも30℃を超えたそうです。
そんな時、帰ってきた下宿の部屋がキンキンに冷えてたら、幸せな気分に慣れそうです。
そんな思いからこの作品を制作しました。
動画
概要
自分の部屋のエアコンをSlackやWebページから管理することができるようになりました。
Webページのほうからは
- エアコンのオン、オフ、温度設定
- 室温の情報の確認
- リモコンの信号の登録
Slackのほうからは
- エアコンのオン、オフ、温度設定
をすることができます。
運用としては、部屋の大体の温度を計測した後、暑いなと思ったらリモートでエアコンのスイッチをオンにするということを想定しています。
Slackのほうの設定は誰でもすることができるので、部屋の主の意志関係なしに部屋の電気を食い散らかすことができます。
必要なもの
組み立て
トップ画のように刺すだけです。
仕組み
まずSlackからどのようにエアコンを操作するのかを説明します。まずSlackで「エアコン」というキーワードを送信するとOutgoing Webhooksに検知され、obnizのWebhookをたたきます。このWebhookの設定の仕方はこちらで説明されています。次に、赤外線情報を得るためにGoogle Apps ScriptにPOSTリクエストを送ります。するとGoogle Apps Scriptは事前にGoogleスプレッドシートに記録していた赤外線情報を返します。
その情報をもとに赤外線を送信した後、SlackのIncoming Webhookをたたくことで、ちゃんと送信できたかどうかをSlackに通知させることができます。
次にWebページのほうの仕組みを説明します。大体の仕組みはSlackのものと同じなのですが、仕様として赤外線信号の登録と室温のプロットを加えています。室温のプロットはこちらを参考にしました。赤外線信号の登録はSlackのものと同じようにGASにPOSTをしてGoogleスプレッドシートに書き込むようにしました。
コード
Slackからの処理
<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" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
crossorigin="anonymous" />
<link href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" rel="stylesheet" crossorigin="anonymous" />
<link rel="stylesheet" type="text/css" href="https://obniz.com/css/app-common.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>
<script src="https://obniz.com/js/popper-1.12.9/popper.min.js"></script>
<script src="https://obniz.com/js/bootstrap-4.0.0/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.2/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
<script src="https://unpkg.com/obniz@3.x/obniz.js" crossorigin="anonymous"></script>
</head>
<body>
<script>
var obniz = new Obniz("OBNIZ_ID_HERE");
obniz.onconnect = async function () {
obniz.display.clear();
var module = obniz.wired("IRModule", {
vcc: 0,
send: 1,
recv: 2,
gnd: 3
});
module.start(function (arr) {
console.log(arr);
arr_now = arr;
});
let request = {};
if (typeof req === "object") {
request = req;
console.log("start");
console.log(request.query);
console.log(request.body);
console.log(request.body.text);
let cmd = request.body.text;
let txt = cmd.split(" ");
var r_txt;
var arr_on;
console.log(txt[0]);
for (let i = 0; i < 3; i++) {
switch (txt[1]) {
case "ON":
sendData("GETON");
await obniz.wait(5000);
console.log("GETON");
arr_on = "[" + r_txt + "]";
arr_on = JSON.parse(arr_on);
console.log(arr_on);
if (arr_on == void 0) {
slack("信号が未登録です");
} else {
module.send(arr_on);
slack("エアコンをオンにしました");
}
break;
case "OFF":
sendData("GETOFF");
await obniz.wait(5000);
console.log("GETOFF");
arr_on = "[" + r_txt + "]";
arr_on = JSON.parse(arr_on);
console.log(arr_on);
if (arr_on == void 0) {
slack("信号が未登録です");
} else {
module.send(arr_on);
slack("エアコンをオフにしました");
}
break;
case "TEMP":
var temp = txt[2];
sendData("GETTEMP " + temp);
await obniz.wait(5000);
arr_on = "[" + r_txt + "]";
arr_on = JSON.parse(arr_on);
console.log(arr_on);
if (arr_on == void 0) {
slack("信号が未登録です");
} else {
module.send(arr_on);
slack("エアコンの温度を" + temp + "℃に設定しました");
}
}
}
if (typeof done === "function") {
done();
}
function sendData(data) {
var xhr = new XMLHttpRequest();
xhr.open(
"POST",
"GoogleAppScript_URL"//GASのWebhookURL
);
xhr.setRequestHeader(
"content-type",
"application/x-www-form-urlencoded;charset=UTF-8"
);
xhr.send(data);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
r_txt = xhr.responseText;
}
};
}
function slack(message) {
const url =
"Slack_URL";//slackのIncomingWebhookURL
const data = {
text: message,
};
const xml = new XMLHttpRequest();
xml.open("POST", url, false);
xml.setRequestHeader(
"content-type",
"application/x-www-form-urlencoded;charset=UTF-8"
);
xml.send(`payload=${JSON.stringify(data)}`);
}
}
};
</script>
</body>
</html>
Webページの処理
<html>
<head>
<title>zeke's temp controler</title>
<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" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
crossorigin="anonymous" />
<link href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" rel="stylesheet" crossorigin="anonymous" />
<link rel="stylesheet" type="text/css" href="https://obniz.com/css/app-common.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>
<script src="https://obniz.com/js/popper-1.12.9/popper.min.js"></script>
<script src="https://obniz.com/js/bootstrap-4.0.0/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.2/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
<script src="https://unpkg.com/obniz@3.x/obniz.js" crossorigin="anonymous"></script>
</head>
<body>
<style>
.chart-container {
position: relative;
margin: 20px auto;
max-height: 500px;
height: 50vh;
width: 100%;
}
</style>
<div id="obniz-debug"></div>
<div class="container app-content">
<div class="conditioner-control_head">
<h1>Air conditioner manual control</h1>
</div>
<div class="conditioner-control_body">
<button class="btn btn-primary" id="on" style="background-color:red">
ON
</button>
<button class="btn btn-primary" id="off" style="background-color:red">
OFF</button><br />
<button class="btn btn-primary" id="up" style="background-color:red">
UP
</button>
<button class="btn btn-primary" id="down" style="background-color:red">
DOWN
</button>
<p>
<button class="btn btn-primary" id="temp" style="background-color:blue">
SET
</button>
<input type="number" id="set_temp_value" size="2" value="28" maxlength="2" />℃
</p>
<div id="conditioner_state">現在エアコンはオフ状態です</div>
<div id="conditionertemp_state"></div>
</div>
<div class="app-content_head">
<h1>Temperature Logger</h1>
</div>
<div class="app-content_body">
<div class="bg-white app-content_body-inner">
<div class="chart-container">
<div class="chartjs-size-monitor"
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: hidden; pointer-events: none; visibility: hidden; z-index: -1;">
<div class="chartjs-size-monitor-expand"
style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;">
<div style="position:absolute;width:1000000px;height:1000000px;left:0;top:0"></div>
</div>
<div class="chartjs-size-monitor-shrink"
style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;">
<div style="position:absolute;width:200%;height:200%;left:0; top:0"></div>
</div>
</div>
<canvas id="myChart" style="width: 100%; height: 100%; display: block;"
class="chartjs-render-monitor"></canvas>
</div>
<script>
var obniz = new Obniz("OBNIZ_ID_HERE");
let myChart;
var r_txt;
setupCharts();
obniz.onconnect = async function () {
var module = obniz.wired("IRModule", {
vcc: 0,
send: 1,
recv: 2,
gnd: 3
});
var sensor = obniz.wired("SHT31", {
vcc: 7,
sda: 8,
scl: 9,
adr: 10,
gnd: 11,
addressmode: 5,
});
var arr_now;
var arr_on;
obniz.display.clear();
module.start(function (arr) {
document.getElementById("conditioner_log").innerText = "赤外線信号を検知!";
console.log(arr);
arr_now = arr;
});
$("#on").click(async function () {
console.log("GETON");
sendData("GETON");
await obniz.wait(5000);
arr_on = "[" + r_txt + "]";
arr_on = JSON.parse(arr_on);
console.log(arr_on);
if (arr_on == void 0) {
document.getElementById("conditioner_state").innerText =
"エアコンをオンにする信号が未登録です";
} else {
document.getElementById("conditioner_state").innerText = "";
module.send(arr_on);
obniz.display.clear();
obniz.display.print("ON");
document.getElementById("conditioner_state").innerText =
"現在エアコンはオン状態です";
}
});
$("#off").click(async function () {
sendData("GETOFF");
await obniz.wait(5000);
arr_on = "[" + r_txt + "]";
arr_on = JSON.parse(arr_on);
console.log(arr_on);
if (arr_on == void 0) {
document.getElementById("conditioner_state").innerText =
"エアコンをオンにする信号が未登録です";
} else {
document.getElementById("conditioner_state").innerText = "";
module.send(arr_on);
obniz.display.clear();
obniz.display.print("OFF");
document.getElementById("conditioner_state").innerText =
"現在エアコンはオフ状態です";
}
});
$("#temp").click(async function () {
var temp = document.getElementById("set_temp_value").value;
sendData("GETTEMP " + temp);
await obniz.wait(5000);
arr_on = "[" + r_txt + "]";
arr_on = JSON.parse(arr_on);
console.log(arr_on);
if (arr_on == void 0) {
document.getElementById("conditioner_state").innerText =
temp + "℃の信号が未登録です";
} else {
document.getElementById("conditioner_state").innerText = "";
module.send(arr_on);
obniz.display.clear();
obniz.display.print(temp + "℃");
document.getElementById("conditionertemp_state").innerText =
"現在" + temp + "℃に設定されています";
}
});
$("#detect_on").click(function () {
if (arr_now != null) {
let cmd = "REGISTON " + arr_now;
sendData(cmd);
document.getElementById("conditioner_log").innerText =
"エアコンをオンにする信号を登録";
document.getElementById("on").style.background = "blue";
arr_on = arr_now;
} else {
document.getElementById("conditioner_log").innerText = "赤外線信号未検出";
}
});
$("#detect_off").click(function () {
if (arr_now != null) {
let cmd = "REGISTOFF " + arr_now;
sendData(cmd);
document.getElementById("conditioner_log").innerText =
"エアコンをオフにする信号を登録";
document.getElementById("off").style.background = "blue";
} else {
document.getElementById("conditioner_log").innerText = "赤外線信号未検出";
}
});
$("#detect_temp").click(function () {
if (arr_now != null) {
var temp = document.getElementById("temp_value").value;
if (temp >= 100) {
document.getElementById("conditioner_log").innerText =
"100℃以上は設定できません!!";
} else if (temp <= 0) {
document.getElementById("conditioner_log").innerText =
"0℃以下は設定できません!!";
} else {
let cmd = "REGISTTEMP " + temp + " " + arr_now;
sendData(cmd);
console.log(cmd);
document.getElementById("conditioner_log").innerText =
temp + "度の信号を記録";
document.getElementById("down").style.background = "blue";
}
} else {
document.getElementById("conditioner_log").innerText = "赤外線信号未検出";
}
});
obniz.onloop = async function () {
obniz.display.clear();
document.getElementById("conditioner_log").innerText = "";
var data = await sensor.getAllWait();
obniz.display.print("temp:" + Math.round(data.temperature) + "℃");
obniz.display.print("humid:" + Math.round(data.humidity) + "%");
var temp = data.temperature;
addChart(temp);
scrollToRight();
myChart.update();
await obniz.wait(2000);
};
};
function setupCharts() {
let ctx = document.getElementById("myChart").getContext("2d");
let dataSet = [
{
label: "temperature",
data: [],
borderWidth: 2,
fill: false,
lineTension: 0,
borderColor: "rgb(252,109,83)",
backgroundColor: "rgb(252,109,83)",
},
];
myChart = new Chart(ctx, {
type: "line",
data: {
datasets: dataSet,
},
options: {
animation: {
duration: 0,
},
legend: {
display: false,
labels: {
fontFamily: "montserrat",
},
},
scales: {
xAxes: [
{
type: "time",
time: {},
scaleLabel: {
display: true,
position: "left",
labelString: "Time",
fontFamily: "montserrat",
},
ticks: {
fontFamily: "montserrat",
},
},
],
yAxes: [
{
scaleLabel: {
display: true,
labelString: "Temperature (℃)",
fontColor: "rgb(252,109,83)",
fontFamily: "montserrat",
},
ticks: {
suggestedMin: 20,
suggestedMax: 36,
fontFamily: "montserrat",
},
},
],
},
},
});
}
function addChart(temp) {
myChart.data.datasets[0].data.push({ x: new Date(), y: temp });
}
function scrollToRight() {
let now = new Date();
myChart.options.scales.xAxes[0].time.max = now;
myChart.options.scales.xAxes[0].time.min = new Date(
now.getTime() - 60 * 1000
);
for (let i = 0; i < myChart.data.datasets.length; i++) {
myChart.data.datasets[i].data = myChart.data.datasets[i].data.filter(
(elm) => {
return elm.x.getTime() > myChart.options.scales.xAxes[0].time.min;
}
);
}
}
function sendData(data) {
var xhr = new XMLHttpRequest();
xhr.open(
"POST",
"GoogleAppScript_URL"//GASのWebhookURL
);
xhr.setRequestHeader(
"content-type",
"application/x-www-form-urlencoded;charset=UTF-8"
);
xhr.send(data);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
r_txt = xhr.responseText;
}
};
}
obniz.onconnect();
</script>
</div>
<h3>Setting</h3>
<h5>今の信号は…</h5>
<button class="btn btn-primary" id="detect_on">ON</button>
<button class="btn btn-primary" id="detect_off">OFF</button>
<p>
<button class="btn btn-primary" id="detect_temp">温度</button>
<input type="number" id="temp_value" size="2" value="28" maxlength="2" />℃
</p>
<div id="conditioner_log"></div>
</div>
</div>
</body>
</html>
GASの処理
function Getsheet(state) {
let spreadSheet = SpreadsheetApp.openById("SPREADSHEET_ID");
let data=spreadSheet.getRange(state).getValue();
return data;
}
function Setsheet(state,data) {
let spreadSheet = SpreadsheetApp.openById("SPREADSHEET_ID");
spreadSheet.getRange(state).setValue(data);
}
function doPost(e) {
let spreadSheet = SpreadsheetApp.openById("SPREADSHEET_ID");
let properties = PropertiesService.getScriptProperties();
let data=e.postData.contents;
let text2=data.split(" ");
let state;
let r_data;
switch(text2[0]){
case "REGISTTEMP":
state="D"+text2[1];
Setsheet(state,text2[2]);
break;
case "GETTEMP":
state="D"+text2[1];
r_data=Getsheet(state);
return ContentService.createTextOutput(r_data);
case "REGISTON":
state="B1";
Setsheet(state,text2[1]);
break;
case "REGISTOFF":
state="B2";
Setsheet(state,text2[1]);
break;
case "GETON":
state="B1";
r_data=Getsheet(state);
return ContentService.createTextOutput(r_data);
break;
case "GETOFF":
state="B2";
r_data=Getsheet(state);
return ContentService.createTextOutput(r_data);
break;
}
return ContentService.createTextOutput("finished");
}
まとめ
今回作ったエアコンをリモートで操作するシステムは、職場のエアコンをSlackに参加している人なら誰でも操作できるという点で実用性があるのかなと思っています。しかし今回テストしたエアコンは下宿のエアコンなので、放置しておくととても危険です。
また、今回初めてjsとhtmlを触ったのでコードにつたない部分があるかもしれませんがご容赦ください。
-
zeke
さんが
2021/05/16
に
編集
をしました。
(メッセージ: 初版)
-
zeke
さんが
2021/05/16
に
編集
をしました。
-
zeke
さんが
2021/05/16
に
編集
をしました。
ログインしてコメントを投稿する