インターホンやテレビドアホン、かつてはいろいろなメーカーが出していた。今はおそらくパナソニックとアイホン(iPhoneの商標はアイホン株式会社のライセンスにもとづき使用されています、のアイホン)の2社がシェアのほとんどではなかろうか。

我が家のそれはパナソニック製だが当然ネットには非対応。本体に録画機能があって留守中に誰かが来たことはわかるが、それは帰宅して本体を見るまではわからない。そう、固定電話の留守番電話みたいなもので「新着あり」表示が点灯しているのを見て初めて「ああ、誰か来たんだな」ということがわかる程度。
今ならネットに対応し、リアルタイムで外部からスマホで応答するようなものもあるかもしれないが、あってもきっと高い。まぁ壊れるまで買い替えるようなものでもないし、そういうのは決まって専用アプリが必要とか、子機だけ異様に高いとか、どこかがクソ仕様なのは昔から決まっている。
ので・・・ひとまず本体VL-MV39の裏を見ると、この機種には「A接点出力」があるではないか。

取説によれば、A接点に接続可能な機器として挙げられているのは「光るチャイム、メロディサイン、警報ランプ付ブザー、回転灯」など。つまり「玄関の呼出ボタンを押すと連動してこれらがONとなる端子」と理解できる。つまりこの端子を拝借して、ここがONしたらネットに通知するようにすれば「ネット対応」にできるわけだ(一方通行だけどないよりマシ)。
自宅のWi-Fiを経由してネットに飛ばし、自分のスマホ宛に通知する仕掛けが出来れば「何かカッコイイ」し、何よりリアルタイムにどこにいても来客があったことは確認できる、というよりは(もちろん、防犯的にはそれでも良いと思う)、誰かが来たことがあらかじめわかっていれば帰宅してから本体の録画をチェックすればいいのであって、そのチェック忘れの防止にもなるわけだ。今回の目的はどちらかと言えばそっち。
そう、いつの間にか「新着あり」表示が点灯していて録画を見ると何件も溜まっていたなんてことは常である。とはいえ、郵便屋さんと宅配の人がほとんどで、再配達の伝票があればそこで突き合わせてOKなんだけど。それ以外ってセールスとか宗教とか得体の知れないやつだけど、たまに近所の人とかが来てることがあって「あれ、何だろう?」と気になったりはするしね。
というわけで、ここにESP32を使用する。通知はLINE Notifyが簡単で良い(LINE Notifyはサービス終了になったのでSlackに変更した)。ほんで、こんな具合にA接点出力とGPIOをつないでプログラムをごにょごにょと書く。
#include <WiFi.h> //Wi-Fi
#include <WiFiClientSecure.h> //Wi-Fi
#include <ArduinoJson.h>
#define A_SW 25 //ドアホンA接点
#define LED_BUILTIN 2 //内蔵LED
#define JST 3600 * 9
#define PUSH_SHORT 50 // チャタリング防止用時間(ms). 50-100で調整
// --- Slack Webhook 設定 ---
const char* slackHost = "hooks.slack.com";
const char* slackPath = "/services/XXXXXXXXX/XXXXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxx";
// WiFi設定
const char* ssid = "your SSID";
const char* password = "your PASSWORD";
String hostname = "INTPHONE";
bool done = true;
RTC_DATA_ATTR int bootCount = 0;
unsigned long lastRestart = 0;
//----------------------------------
// デバウンス用構造体
//----------------------------------
struct Debounce {
uint8_t pin;
int lastStable; // 最後に確定した安定状態 (HIGH/LOW)
unsigned long lastChange; // millis() を記録
bool reported; // その LOW 確定を報告済みか (立下がりを一度だけ返すため)
};
Debounce dbA = { A_SW, HIGH, 0UL, false };
//----------------------------------
// デバウンス検出関数(立下がりを1回だけtrueで返す)
// 使い方: if (debounceFalling(&dbA)) { ... }
//----------------------------------
bool debounceFalling(Debounce *db) {
int reading = digitalRead(db->pin);
if (reading != db->lastStable) {
// 状態が変化した(暫定) -> タイマーをリセット
db->lastChange = millis();
db->lastStable = reading; // update the last observed level for timing
db->reported = false; // 未報告に戻す
return false;
} else {
// 状態が変わらない -> 経過時間が閾値を超えたら確定
if ((millis() - db->lastChange) > PUSH_SHORT) {
// LOWが確定していて、まだ報告していなければ立下がりを返す
if (reading == LOW && db->reported == false) {
db->reported = true; // 1回だけ報告する
return true;
}
}
}
return false;
}
// ドアホン用(3回繰り返し)
void interphoneCall() {
Serial.println("ドアホン呼出検知");
}
//----------------------------------
void setup() {
Serial.begin(115200);
lastRestart = millis();
pinMode(A_SW, INPUT_PULLUP); //ドアホンA接点短絡でON
pinMode(LED_BUILTIN, OUTPUT); //LED
digitalWrite(LED_BUILTIN, LOW);//LED初期状態OFF
// WiFi接続
WiFi.disconnect(true, true);
WiFi.mode(WIFI_STA);
WiFi.setHostname(hostname.c_str());
WiFi.begin(ssid, password);
while (done) {
Serial.print("Wi-Fi connecting");
auto last = millis();
while (WiFi.status() != WL_CONNECTED && last + 5000 > millis()) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
done = false;
} else {
Serial.println("Retry");
WiFi.disconnect(true, true);
//WiFi.reconnect();
ESP.restart();
}
}
Serial.println("Wi-Fi connected.");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
configTime(JST, 0, "pool.ntp.org", "jp.pool.ntp.org");
delay(5000);
sendSlackMessage("🤖 起動OK!");
Serial.println("起動OK!");
}
// Slack にメッセージを送信する関数
void sendSlackMessage(const char* message) {
WiFiClientSecure client;
client.setInsecure(); // 簡易的に証明書検証を無効化
if (!client.connect(slackHost, 443)) {
Serial.println("Slack connection failed");
return;
}
String payload = String("{\"text\":\"") + message + "\"}";
String request = String("POST ") + slackPath + " HTTP/1.1\r\n" +
"Host: " + slackHost + "\r\n" +
"Content-Type: application/json\r\n" +
"Content-Length: " + payload.length() + "\r\n\r\n" +
payload;
client.print(request);
// レスポンスを読み出して確認
while (client.connected()) {
String line = client.readStringUntil('\n');
if (line == "\r") break; // ヘッダ終了
Serial.println(line); // デバッグ出力
}
String response = client.readString();
Serial.println("Response: " + response);
}
//----------------------------------
void loop() {
///////////////////////
time_t t;
struct tm* tm;
static const char* wd[7] = { "Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat" };
t = time(NULL);
tm = localtime(&t);
char timeStr[16];
sprintf(timeStr,"%02d:%02d:%02d",tm->tm_hour, tm->tm_min, tm->tm_sec);
// 毎日06:05にRestart(ハングアップ対策)
if ((tm->tm_hour == 06) && (tm->tm_min == 05) && (tm->tm_sec == 00)) {
ESP.restart();
}
// ドアホン呼出 (立下がり検出)
if (debounceFalling(&dbA)) {
interphoneCall();
sendSlackMessage(String("🚪 おや?誰か来たようだ! (" + String(timeStr) + ")").c_str());
}
}
USBから書き込んで、動作確認したら完了。なお、インターホン本体にAC100Vが直接接続されているモノは電気工事士でないと電源は外すことが出来ない(私は電気工事士免状を持っているので無問題)。玄関子機への配線や、コンセントタイプの電源であればもちろん無資格で外すことは可能。一旦外してボックスを追加してESP32を収納し、内部から電源を取って動作するように加工した。信頼できる超小型の5V電源ってiPhone純正の充電器くらいしか思い浮かばないんだけど、インターホン本体内部から5V取れたらそうしようかな。壁付けから宙に浮いて見えるようになって、むしろカッコイイ(とは言わない配線や基板が剥き出しのものがごちゃごちゃするのは美しくないのでそれよりはマシ)。

これが

こうなったわけだが、

そんなに違和感ない。

玄関ドアの開閉通知はこっちに記載。
