冬に備えてSwitchBotスイッチもどきを作る話。
世の中IoTが盛んでAmazonのセールなんかでも頻りにSwitchBotが少しだけ安く売られてたりしてホイホイ飛びつく人も多かろう。そこへいくと自作IoTの話はそれほど需要がなさそうで、PVなんてのを見ても対して伸びてない。だがいいのだ。
ブログなんて所詮自己満足の世界で自家発電しているようなものだ。
自家発電して虚しくなるより自作したものが家の中で活きればそのほうが楽しい。
ということで今回はSwitchBotみたいなのを作ってみた。
かなり簡単にできた。
参考にはしていないが後から調べたらSwitchBotスイッチを同じファンヒーターにつけている先駆者がいた。
上記の人が使っている本家SwitchBotスイッチとはこれのこと↓
SwitchBot スイッチボット スイッチ ボタンに適用 指ロボット スマートホーム ワイヤレス タイマー スマホで遠隔操作 Alexa, Google Home, Siri, IFTTTなどに対応(ハブ必要)
¥3,430(2022/11/30 08:02時点の価格)
平均評価点:
>>楽天市場で探す
>>Yahoo!ショッピングで探す
これを音声でも操作したかったら↓このSwitchBotハブも必要らしい。
SwitchBot スマートリモコン ハブミニ アレクサ スイッチボット Hub Mini スマートホーム 学習リモコン 赤外線家電を管理 スケジュール 遠隔操作 Alexa Google Home IFTTT Siriに対応(ホワイト)
¥3,494(2022/11/30 08:04時点の価格)
平均評価点:
>>楽天市場で探す
>>Yahoo!ショッピングで探す
SwitchBotハブは用途が広いがSwitchBotスイッチはただの指の代わりにしてはちょっと高すぎるんじゃね?というケチな動機から自作を決意。
今回のゴール
- 室温を遠隔地から計測→使うサービスは例によってBlynkレガシー
簡易的な補正機能も備える - 石油ファンヒーターのスイッチを遠隔から押す
応用としてサーボを増やせば温度設定ボタンも押せる - 電池駆動のために電圧監視&報告
Blynkレガシーはすでに公式サービスを終了しているが自前サーバーで運用している我が家は果たして継続して使えるのか不安はあるが行けるところまで行く。
免責について
この記事の内容を完コピして使うのは構わないが何かあっても苦情等は一切受け付けないので自己責任にて行ってほしい。
またファンヒーターのスイッチ操作に今回使うが、火を扱う機器なので本来は不在中にやるべきではない。
我が家の場合はSwitchBotカメラで遠隔からでも状態を確認しつつスイッチ操作をするという前提で、さらにすぐ家に戻れるときに限るという運用をするつもりであったがどっちみち怖いから留守中は操作しない。
フロー(チャートにはなってない)
フローチャートと呼ぶには稚拙であるが流れを大まかにお伝えする必要があるだろう。
初期化(setup)
- Wi-Fiマルチ設定
想定されるどれかのWi-Fiに接続できるようにしておく - ArduinoOTA設定
これはサンプルをコピーすればでき、あとでアップデートしたいときにいちいちパソコンと繋げなくて遠隔からできるから便利だ。 - DHT初期化
温度湿度センサーの初期化でお決まりの呪文を唱える。 - タイマータスク設置
Blynkライブラリのタイマーを使うので簡単。詳細はBlynkのドキュメントを参照されたい。 - Blynk接続
Blynk.config()関数のパラメーターは1トークン、2ローカルサーバーのIPアドレス(同一LAN内か否かでも変わる、3ポート番号
さらにBlynk接続時にBLYNK_CONNECTED()がパラレルで呼ばれるのでそっちにも必要な初期化コードを書いておく - GPIO出力設定
ここではサーボのGPIOピン番号を決めてOUTPUT設定するぐらいだ。
ループ(loop)
- AuduinoOTAポーリング
- Blynkポーリング
- タイマーポーリング
- Bynk接続断回数オーバーで再起動
- タイマーフラグが立っていたら以下処理
5-1温度・湿度取得
5-2 時刻取得
5-3 温度または湿度が不正確ならMessage
5-4 温度・湿度が取得できたら所定のVPINに書き込む
5-5 温度がしきい値超えてたら警告送る
5-6 生存報告ピンに時刻を書き込む
5-7 電圧を取得して電圧ピンに書き込む
その他に3秒~5秒頻度のタイマー処理ではフラグを立てる程度の最小限の処理を行う。
準備するハード
必須部品
- ESP32
ESP32でもESP8266でも動くようにコードは書いてあるがおすすめは合法的なESP32だ。って書くとESP8266が違法みたいに感じるがあくまで技適未取得なんちゃらかんちゃら仮申請みたいなのを経ずに使ったら違法ということで買ってプログラムを焼くだけなら合法だ。 - サーボSG90など
ファンヒーターのボタンを押すだけなのでそんなにパワーがある必要はないと思うのでSG90であればいけると思う。不安ならもっと強いサーボでももちろん可。 - 温度センサーDHT11など
温度センサーは安いDHT11で良いだろう。モジュール化されているものを入手すれば次項の抵抗は不要。 - 抵抗300Ω程度
温度センサーDHT11単体で買うと抵抗を別途用意して付ける必要がある。 - 強力な両面テープ
ファンヒーターのスイッチ近くにサーボを貼り付ける。意外とこれ大事。
あればいい部品
- ブレッドボード
毎回書いているが可逆的(分解して再利用する可能性あり)に組み立てたいならこれあったほうが良い。 - ジャンプワイヤー
同上 - ケース
接触して壊れる心配がまったくないならなくても可。 - 電源取れない場所ではモバイルバッテリー
つなぎ方
▼これだけじゃ参考にならんかもな。
図を作るのは苦手なので表にすると
マイコンボード側のPIN | 制御部品の配線 |
5(ESP8266) 5(ESP32) | 電源サーボの信号線 |
14(ESP8266) 14(ESP32) | 温度サーボの信号線 |
12(ESP8266) 14(ESP32) | DHT11の信号線 |
VIN5V | サーボの電源線 |
3.3V | DHT11の電源線 |
GND | サーボ、DHT11それぞれのGND |
その他 | DHT11の電源線と信号線を適当な抵抗でつなぐ |
スマホでの設定
上から説明すると
延長=電源サーボの左回転で運転延長する
電源=電源サーボの右回転でON/OFFする
温度↓=温度サーボの左回転
温度↑=同じく右回転
2個のゲージの左が室温度、右が湿度、といってもケースの中で基板と一緒だからその影響もかなり受けると思う。
その下の折れ線グラフはボードの電圧監視。ESP8266は特に分圧しなくとも電圧測定ができるようで一応載せている。用途としてはバッテリー駆動させたときの降下警告できたらなあという程度。
数字を入れるボックスは温度の補正値を入れる。その右の決定ボタンで決定される。
グラフの下の数値入力はまず温度補正用。DHT11というセンサーはあまり正確に温度測定できないっぽいので実温度に合わせて補正をかけられるようにした。
その下の6個の数値入力ボックスは左上から
- 電源サーボ左トリム
- 電源サーボ右トリム
- 電源サーボニュートラル角度
- 温度サーボニュートラル角度
- 温度サーボ左トリム
- 温度サーボ右トリム
トリムというボックスに角度値を入れてちょうどよいボタン操作の強さを決める仕組みだ。
ハードの取り付け
両面テープで貼り付けてから回転角度の調節をしてもよい。っていうかそのつもりでプログラムも作った。
なぜなら貼り付ける位置の微妙な違いによってスイッチを押すまでのサーボ角度が変わってくるから。
そういう調整もプログラムとスマホアプリのウィジェットでできるようにした。
プログラムコード(無保証)
意味のわからないところがあったらtwitterでつぶやいてくれれば答えられるものは答えます。ちゃんと動かないんだけど?というのは無しで。
まあ誰も読まんだろうけど。
const char* version_info = "V0.0.1";
#define WIFIMULTI
#define IrqInterval 5000L //タイマー割り込み間隔定義 最後にLつける
#include "personal.h"
#include "ESP8266ESP32.h"
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#if defined(ESP8266)
#include <ESP8266mDNS.h>
#include <ESP8266WiFiMulti.h>
#else
#include <WiFiMulti.h>
#endif
// ADC_MODE 参考URL
// https://intellectualcuriosity.hatenablog.com/entry/2018/02/22/145715
#define LIMIT_VOLTAGE 2.8
#if defined(ESP8266)
ADC_MODE(ADC_VCC);
#endif
// for TIME
#define JST 3600* 9
void DisplayTime(char *);
BlynkTimer timer;
bool checkTimerTask = true;
// for Blynk communication
int BlynkDisconnect = 0;
#define VPIN_NOTICE V0
#define VPIN_ALIVE V2
#define VPIN_NUTRAL_PWR V3 // 電源中立位置角度再定義用
#define VPIN_NUTRAL_TMP V4 // 温度中立位置角度再定義用
#define VPIN_PWR_SERVO_R V5 // サーボ右回転
#define VPIN_PWR_SERVO_L V6 // サーボ左回転
#define VPIN_PWR_R_TRIM V7
#define VPIN_PWR_L_TRIM V8
#define VPIN_TMP_SERVO_R V9 // 温度上げる
#define VPIN_TMP_SERVO_L V10 // 温度下げる
#define TEMP_PIN V12 //温度通知バーチャルピン
#define HUMI_PIN V13 //湿度通知バーチャルピン
#define VPIN_CRCT V14 // 温度補正
#define VPIN_CRCT_OK V15 // 温度補正決定
#define VPIN_VOLT V16 // 電圧
#define VPIN_TMP_R_TRIM V17
#define VPIN_TMP_L_TRIM V18
int notice_flug = 0;
// for servo
int POWER_NUTRAL_DEGREE = 80; // 中立位置角度
int TEMPE_NUTRAL_DEGREE = 80; // 中立位置角度
int PWR_R_DEG, PWR_L_DEG;
int TMP_R_DEG, TMP_L_DEG;
// for NetWork communication
const char THIS_HOST_NAME[] = "FANHEATER";
const char *DeviceName = THIS_HOST_NAME;
const char* SSID_NAME = ssid_name[0];
const char* PASSWORD = passwords[0];
#if defined(ESP8266)
ESP8266WiFiMulti wifiMulti;
#else
WiFiMulti wifiMulti;
#endif
// GPIO
#if defined(ESP8266)
#define servoPIN_0 5 //D6 8266
#define servoPIN_1 14 //D5
#define DHTPIN D6 // 8266 GPIO5
#else
#define servoPIN_0 5
#define servoPIN_1 14
#define DHTPIN 18
#endif
// for 温度測定
#define DHTTYPE DHT11 // DHT型の指定
DHT dht(DHTPIN, DHTTYPE); // DHTセンサーライブラリーを使うための設定
float TEMP_CORRECT = 0; // 温度補正
float TEMP_CORRECT_TEMP = 0; // 温度補正決定までの一時保管
const String mes[6] = {
"通知ON切替",
"温度補正値変更",
"サーボ切替",
"読み取り失敗",
"読み取り成功",
"室温が高すぎです"};
void timerTask()
{
float temperature = dht.readTemperature(); // 温度読み取り
float humidity = dht.readHumidity(); // 湿度読み取り
char str[13];char str2[20];
DisplayTime(str);
// 読み取りに失敗しているかチェック
if (isnan(temperature) || isnan(humidity)) {
Serial.println("Failed to read from DHT sensor!");
sprintf(str2, "%s", mes[3]);
Serial.println(str2);
Blynk.setProperty(VPIN_ALIVE,"onLabel", str2);
}else{
sprintf(str2, "%s", mes[4]);
Blynk.setProperty(VPIN_ALIVE,"onLabel", str2);
delay(10);
// TEMP_PIN ピンに対して温度の値を送信 2022-08-15 補正値付加
Blynk.virtualWrite(TEMP_PIN, temperature + TEMP_CORRECT);
delay(10);
Blynk.virtualWrite(HUMI_PIN, humidity);
}
delay(10);
sprintf(str2, "%s %s", version_info, str);
if(temperature > 40){
//危険
Blynk.notify(mes[5]);
delay(10);
Blynk.email(YOUR_MAIL_ADDRESS, "Subject: 温度上昇", mes[5]);
delay(10);
}
// 生存報告
Blynk.setProperty(VPIN_ALIVE,"offLabel", str2);
delay(100);
#if defined(ESP8266)
float vcc = ESP.getVcc() / 1024.0;
Blynk.virtualWrite(VPIN_VOLT,vcc);
#endif
}
void my_pwm(int port, int v) {
for (int i=0;i<40;i++) {
digitalWrite(port, HIGH);
delayMicroseconds(v);
digitalWrite(port, LOW);
delayMicroseconds(10000);
delayMicroseconds(10000 - v);
}
}
void calcPWM(int port,int v)
{
my_pwm(port,v*10.4+500);
}
void PowerServoNutral()
{
int deg = POWER_NUTRAL_DEGREE;
calcPWM(servoPIN_0,deg);
}
void TempeServoNutral()
{
int deg = POWER_NUTRAL_DEGREE;
calcPWM(servoPIN_1,deg);
}
#if 1
BLYNK_CONNECTED(){
Blynk.syncAll();
PowerServoNutral();
TempeServoNutral();
Blynk.syncVirtual(VPIN_PWR_R_TRIM);
Blynk.syncVirtual(VPIN_PWR_L_TRIM);
}
BLYNK_WRITE(VPIN_ALIVE)
{
if(param.asInt()){
// ESP.restart();
}
}
BLYNK_WRITE(VPIN_NOTICE)
{
notice_flug = param.asInt();
if(notice_flug){
Blynk.notify(DeviceName + mes[0]);
}
}
#endif
BLYNK_WRITE(VPIN_NUTRAL_PWR)
{
POWER_NUTRAL_DEGREE = param.asInt();
calcPWM(servoPIN_0,POWER_NUTRAL_DEGREE);
Serial.printf("VPIN_NUTRAL_PWR servo %d\n", POWER_NUTRAL_DEGREE);
}
BLYNK_WRITE(VPIN_NUTRAL_TMP)
{
TEMPE_NUTRAL_DEGREE = param.asInt();
calcPWM(servoPIN_1,TEMPE_NUTRAL_DEGREE);
Serial.printf("VPIN_NUTRAL_TMP servo %d\n", TEMPE_NUTRAL_DEGREE);
}
BLYNK_WRITE(VPIN_PWR_SERVO_R){ //電源サーボ右制御
int hmov = param.asInt();
if(hmov){
int deg = POWER_NUTRAL_DEGREE + PWR_R_DEG;
calcPWM(servoPIN_0,deg);
Serial.printf("VPIN_PWR_SERVO_R servo %d\n", deg);
PowerServoNutral();
if(notice_flug){
Blynk.notify(DeviceName + mes[2]);
}
}
}
BLYNK_WRITE(VPIN_TMP_SERVO_R){ //温度サーボ右制御
int hmov = param.asInt();
if(hmov){
int deg = TEMPE_NUTRAL_DEGREE + TMP_R_DEG;
calcPWM(servoPIN_1,deg);
Serial.printf("VPIN_TMP_SERVO_R servo %d\n", deg);
TempeServoNutral();
if(notice_flug){
Blynk.notify(DeviceName + mes[2]);
}
}
}
BLYNK_WRITE(VPIN_PWR_SERVO_L){ //電源サーボ延長制御
int hmov = param.asInt();
if(hmov){
int deg = POWER_NUTRAL_DEGREE + PWR_L_DEG;
calcPWM(servoPIN_0,deg);
Serial.printf("VPIN_PWR_SERVO_L servo %d\n", deg);
PowerServoNutral();
if(notice_flug){
Blynk.notify(DeviceName + mes[2]);
}
}
}
BLYNK_WRITE(VPIN_TMP_SERVO_L){ //温度サーボ左制御
int hmov = param.asInt();
if(hmov){
int deg = TEMPE_NUTRAL_DEGREE + TMP_L_DEG;
calcPWM(servoPIN_1,deg);
Serial.printf("VPIN_TMP_SERVO_L servo %d\n", deg);
TempeServoNutral();
if(notice_flug){
Blynk.notify(DeviceName + mes[2]);
}
}
}
BLYNK_WRITE(VPIN_PWR_R_TRIM)
{
PWR_R_DEG = param.asInt();
Serial.printf("VPIN_PWR_R_TRIM servo %d\n", PWR_R_DEG);
}
BLYNK_WRITE(VPIN_TMP_R_TRIM)
{
TMP_R_DEG = param.asInt();
Serial.printf("VPIN_TMP_R_TRIM servo %d\n", TMP_R_DEG);
}
BLYNK_WRITE(VPIN_PWR_L_TRIM)
{
PWR_L_DEG = param.asInt();
Serial.printf("VPIN_PWR_L_TRIM servo %d\n", PWR_L_DEG);
}
BLYNK_WRITE(VPIN_TMP_L_TRIM)
{
TMP_L_DEG = param.asInt();
Serial.printf("VPIN_TMP_L_TRIM servo %d\n", TMP_L_DEG);
}
BLYNK_WRITE(VPIN_CRCT_OK)
{
if(param.asInt()){
Blynk.syncVirtual(VPIN_CRCT);
TEMP_CORRECT = TEMP_CORRECT_TEMP;
Blynk.notify(DeviceName + mes[1]);
}
}
BLYNK_WRITE(VPIN_CRCT)
{
TEMP_CORRECT_TEMP = param.asInt();
}
void checkBlynkStatus() { // called every 3 seconds by SimpleTimer
bool isconnected = Blynk.connected();
if (isconnected == false) {
BlynkDisconnect ++;
Serial.printf("Blynk disconnect :%d\n",BlynkDisconnect);
Blynk.config(AUTH, IPAddress(192,168,0,182), 8080);
}
if (isconnected == true) {
BlynkDisconnect = 0;
digitalWrite(LED_BUILTIN, HIGH); //Turn on WiFi LED
checkTimerTask = true;
}
}
void setup()
{
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
////// Wi-Fi接続処理 //////
// Wi-Fi接続
WiFi.disconnect();
WiFi.mode(WIFI_STA);
#if defined(WIFIMULTI)
for(int i = 0;i<ROUTERS;i++){ wifiMulti.addAP(ssid_name[i], passwords[i]); // add Wi-Fi networks you want to connect to } int i = 0; while (wifiMulti.run() != WL_CONNECTED) { // Wait for the Wi-Fi to connect delay(250); Serial.printf("%d ",i); i++; if(i>20)ESP.restart();
}
#else
WiFi.begin(SSID_NAME, PASSWORD);
int i = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
i++; Serial.print(i);
if (i >= 20) ESP.restart();
} // Wi-Fi接続ここまで
#endif
/////// Arduino OTA 設定
// Port defaults to 8266
// ArduinoOTA.setPort(8266);
// Hostname defaults to esp8266-[ChipID]
ArduinoOTA.setHostname(THIS_HOST_NAME);
// No authentication by default
// ArduinoOTA.setPassword(DEVICEPASSWORD);
// Password can be set with it's md5 value as well
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
Serial.println("Succesfully Connected!!!");
configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
// ArduinoOTA special script -----------------------------------------
ArduinoOTA.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH) {
type = "sketch";
} else { // U_FS
type = "filesystem";
}
// NOTE: if updating FS this would be the place to unmount FS using FS.end()
Serial.println("Start updating " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) {
Serial.println("Auth Failed");
} else if (error == OTA_BEGIN_ERROR) {
Serial.println("Begin Failed");
} else if (error == OTA_CONNECT_ERROR) {
Serial.println("Connect Failed");
} else if (error == OTA_RECEIVE_ERROR) {
Serial.println("Receive Failed");
} else if (error == OTA_END_ERROR) {
Serial.println("End Failed");
}
});
ArduinoOTA.begin();
Serial.println("Ready");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
// ArduinoOTA special script ここまで -------------------------
dht.begin();
timer.setInterval(IrqInterval, checkBlynkStatus); // check if Blynk server is connected every 3 seconds
Blynk.config(AUTH, SERVER_IP, 8080); // Blynkトークン, サーバーのIPアドレス, ポート
led_blink(50,50,20,LED_BUILTIN);
pinMode(servoPIN_0, OUTPUT); //
pinMode(servoPIN_1, OUTPUT); //
}
void loop()
{
ArduinoOTA.handle();
if (WiFi.status() != WL_CONNECTED)
{
Serial.println("WiFi Not Connected");
} else {
Blynk.run();
}
timer.run(); // Initiates SimpleTimer
if(BlynkDisconnect >=10){
WiFi.disconnect(true);
delay(50);
Serial.println("Restart");
ESP.restart();
}
if(checkTimerTask){
// タイマーで3秒ごとにフラグが立っていたら温度計測と生存報告
checkTimerTask = false;
timerTask();
led_blink(10,10,1,LED_BUILTIN);
}
}
void led_blink(int OnMsec,int OffMsec,int n,int PORT){
for(int i=0;i<n;i++){
digitalWrite(PORT, LOW); // turn the LED on (HIGH is the voltage level)
delay(OnMsec); // wait for a second
digitalWrite(PORT, HIGH); // turn the LED off by making the voltage LOW
delay(OffMsec); // wait for a second
}
}
スマートスピーカーと連携
- Google Assistant Node-RED Bridgeに登録する
Node-RED Google Assistant Bridge
- 同じくAlexa Node-RED Bridgeに登録する
Node-RED Alexa Home Skill Bridge
- 自分ちのサーバーで動いているNode-redに上記デバイスをデプロイする
- Google、アレクサそれぞれのスマホアプリを開けば新規としてデバイスが出てくるはずなのでシーンだとかルーティンだとかに登録する。
例として「アレクサ(またはOKGoogle)ファンヒーターをつけて」と言ったらFanHeaterPowerが作動するというような感じ。
実際の動き
適当にパジャマ姿で動画を撮ってしまって恥ずかしいが興味があれば確認してみてほしい。
まとめ
まとうめようにもまとめようがないが、サーボが余っていたのに何も使わないのはもったいないからリモートでファンヒーターをONやOFFできるようにした。
エアコンと違ってファンヒーターは直接火を使って暖房する器具だからリモートで動かすっていうのは不在中には絶対やらないほうがいい。
したがって実際の運用はリビングルームでまったりしているときに
「ちょっと寒くなってきたなあ、でもヒーターつけに立ち上がるのだるいなあ」
というシチュエーションで役に立つはず。間違っても留守中に温度を監視して「猫ちゃんが寒かろう」とつけてはならない。そういうときはエアコンをつけよう。
とにかく手ぶらで声だけでファンヒーターをつけられるようにしたので久しぶりにIoT機器が完成して満足感が多少ある。
やっぱり自分にはこんなの自作は無理~という方は↓これがおすすめです。
SwitchBot スイッチボット スイッチ ボタンに適用 指ロボット スマートホーム ワイヤレス タイマー スマホで遠隔操作 Alexa, Google Home, Siri, IFTTTなどに対応(ハブ必要)
¥3,430(2022/11/30 08:02時点の価格)
平均評価点:
>>楽天市場で探す
>>Yahoo!ショッピングで探す
SwitchBot スマートリモコン ハブミニ アレクサ スイッチボット Hub Mini スマートホーム 学習リモコン 赤外線家電を管理 スケジュール 遠隔操作 Alexa Google Home IFTTT Siriに対応(ホワイト)
¥3,494(2022/11/30 08:04時点の価格)
平均評価点:
>>楽天市場で探す
>>Yahoo!ショッピングで探す