Home IoT Hub Series · 24

Part 24 — AI Butler Method B: A Safe AI Broker on an Old PC, Controlled via Telegram

Part 24 — AI Butler Method B: building a safe AI broker with an old PC, cover image

In Part 23 we called the AI API directly from the ESP32. It worked — but honestly, it was a horrible security hole. The API key was sitting in flash as plaintext, TLS verification was turned off, and the only way to see the response was with the PC plugged in over USB.

What we'll fix in this part:

Method A's problemMethod B's fix
The API key is exposed on the ESP32The API key lives only on the PC; the ESP32 never sees it
You can't see the response when you're awayA Telegram bot pushes it to your phone
The AI only talks, it can't actYou send a command over Telegram → a real actuator fires
The headache of managing TLS certificatesPython's requests handles it all for you

Parts and cost

PartCost (approx. USD)Notes
Any PC/laptop you already have$0Windows 10, a MacBook, Ubuntu — anything. Even a 10-year-old machine is plenty
Python 3.8 or newer$0Free download from python.org
A Telegram account + app$099% chance it's already on your phone
AI API keyPay-as-you-goSame as Part 23. About $0.0015 per 1K tokens on GPT-3.5-turbo
ESP32 (the brain)You already have it from Part 22
Additional cost$0

Before you start

First time doing this? You'll need the tools below installed to follow along. If any one of them is missing, spend just five minutes installing it and come back. They're all free.
What you needInstall guideHow to check
Arduino IDEHow to installDoes it launch and show a blank sketch window?
ESP32 board setupHow to installDo you see ESP32 Dev Module under Tools → Board?
Python 3.8+How to installDoes python --version work in a terminal?
Telegram + botHow to installDoes @MyHomeAIBot respond to /start?
Required librariesHow to installAre TFT_eSPI and Adafruit SSD1306 installed?
Once everything's installed, let's keep going. From here on, it's the real build.

Concepts for this part — jargon vs. plain words

Here are the new terms that show up in this part. Don't be intimidated. It's nothing serious.

JargonWhat it really means (analogy)
FlaskA tool for building web servers in Python. Think of it as an "order counter." When data arrives at an address like /data, it takes it and processes it
BrokerThe friend who runs errands in the middle. "ESP32, hand over the data. AI, analyze this. Telegram, send this out." It does all of that by itself
REST APIAn agreement for exchanging data over the internet. The rule that says "send data to this address and I'll receive it"
Telegram botA Telegram account that sends messages automatically. It's a program, not a person, but from the outside it just looks like a Telegram friend
The rest of the terms were already explained under Development software. If you hit a word you don't know, look it up there.

Overall architecture

Here's the simple version. The ESP32 is the thermometer in your house. It collects things like temperature, humidity, and door-open events and sends them to the PC. The PC is your assistant. It takes the thermometer's readings, asks the AI "analyze this for me," and forwards the AI's answer to your phone. Telegram is the assistant texting you. Even when you're out, if you ask "how's the house right now?" the assistant answers.

Drawn as a picture, it looks like this:

┌──────────┐  HTTP POST   ┌──────────────┐  HTTPS      ┌───────────┐
│  ESP32   │ ────────────→│  Old PC      │ ──────────→ │ AI API     │
│ (brain)  │←──────────── │  Flask server│←────────────│ (GPT etc.) │
│          │  HTTP GET    │  (Python)    │  JSON reply │            │
│20 sensors│  /command    │              │             │            │
│ IR LED   │              │  Parse the   │             │            │
│ Buzzer   │              │  AI reply    │             │            │
└──────────┘              │  and act     │             │            │
                          │              │             │            │
                          │Telegram link │  ──→  📱 your phone       │
                          │              │  ←──  "turn off AC"       │
                          └──────────────┘

Flow: 1. Every 30 seconds the ESP32 HTTP-POSTs its 20 sensor values to the PC (/data). 2. The PC's Flask server receives and stores them, and every 5 minutes sends an analysis request to the AI API. 3. It parses the AI response and sends it out over Telegram. 4. When you type "turn off the AC" in Telegram, the Flask server interprets it and stores it at /command. 5. The ESP32 periodically checks /command and executes things like sending IR.

🚨 ESP32 code — just copy this as-is and upload it

Below is the complete code, combining the Part 22 code with all the new Part 24 features. No edits needed — just paste this one file into the Arduino IDE, change PC_URL to your PC's IP, and upload.

Compared to Part 22, the newly added parts are the three functions sendDataToPC(), checkCommand(), executeCommand(), plus two timer-call lines inside loop(). Everything else is identical to Part 22.

Part 24 — AI Butler Method B: a safe AI broker with an old PC — full code
#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
#include <HTTPClient.h>

// =============================================
// Wi-Fi settings (fill in yourself!)
// =============================================
const char* ssid     = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
const char* ap_ssid     = "IoT_Hub_Net";
const char* ap_password = "iothub1234";

// =============================================
// PC broker URL (fill in yourself!)
// =============================================
const char* PC_URL = "http://192.168.0.100:5000"; // ← change this to your PC's IP

// =============================================
// Hardware pins
// =============================================
#define BUZZER_PIN 13
#define TFT_BL     4
TFT_eSPI tft = TFT_eSPI();
WebServer server(80);

#define TS_CS  5
#define TS_IRQ 21
XPT2046_Touchscreen ts(TS_CS, TS_IRQ);

// =============================================
// 20 memory slots
// =============================================
#define SLOT_COUNT 20
String slot[SLOT_COUNT] = {
  "--", "--", "--", "--", "--",
  "--", "--", "--", "--", "--",
  "--", "--", "--", "--", "--",
  "--", "--", "--", "--", "--"
};

const char* label[SLOT_COUNT] = {
  "Living Temp","Living Hum","Outside Temp","AC",
  "Boiler","Stove","Gas Leak","Fire",
  "Front Door","Door Lock","Living Light","Fridge Temp",
  "CO2","Dust PM2.5","Power","Water",
  "Hot Water","Rain","Bath Hum","Pet Bowl"
};

// =============================================
// Colors
// =============================================
#define C_BG       TFT_BLACK
#define C_TITLE_BG TFT_WHITE
#define C_CARD     TFT_DARKGREEN
#define C_CARD_WARN TFT_RED
#define C_LABEL    TFT_SILVER
#define C_VALUE    TFT_YELLOW

// =============================================
// Timers
// =============================================
unsigned long lastScroll = 0;
unsigned long lastBuzzer = 0;
int scrollOffset = 0;
const int ITEMS_PER_PAGE = 4;
const int SCROLL_INTERVAL = 3000;
bool warningActive = false;

// PC communication timers
unsigned long lastCmdCheck = 0;
unsigned long lastDataSend = 0;
#define CMD_INTERVAL  5000
#define DATA_INTERVAL 30000

// =============================================
// Touch card zones
// =============================================
struct CardZone { int x, y, w, h, slotIdx; };
CardZone cards[4];

// =============================================
// setup
// =============================================
void setup() {
  Serial.begin(115200);

  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);

  tft.init();
  tft.setRotation(1);
  tft.fillScreen(C_BG);
  tft.setTextWrap(false);

  ledcSetup(0, 5000, 8);
  ledcAttachPin(TFT_BL, 0);
  ledcWrite(0, 200);

  ts.begin();
  ts.setRotation(1);

  tft.setTextColor(TFT_WHITE, C_BG);
  tft.drawString("IoT Hub + AI", 40, 140, 4);
  tft.drawString("iothub.co.kr", 60, 180, 2);
  delay(1000);

  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);

  tft.fillScreen(C_BG);
  tft.drawString("WiFi connecting...", 30, 100, 2);
  Serial.print("Connecting to WiFi");
  int dots = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500); Serial.print(".");
    tft.drawString(".", 180 + dots * 8, 100, 2); dots++;
  }
  Serial.println("\nSTA IP: " + WiFi.localIP().toString());

  WiFi.softAP(ap_ssid, ap_password);
  Serial.println("AP  IP: " + WiFi.softAPIP().toString());

  server.on("/set", handleSet);
  server.on("/", handleRoot);
  server.on("/status", handleStatus);
  server.onNotFound(handleNotFound);
  server.begin();

  tft.fillScreen(C_BG);
  tft.drawString("IoT Hub Ready", 20, 30, 4);
  tft.drawString("LAN: " + WiFi.localIP().toString(), 10, 80, 2);
  tft.drawString("PC : " + String(PC_URL), 10, 110, 2);
  delay(3000);
}

// =============================================
// loop
// =============================================
void loop() {
  server.handleClient();
  handleTouch();
  updateTFT();
  updateBuzzer();

  unsigned long now = millis();

  // Send sensor data to the PC every 30 seconds
  if (now - lastDataSend > DATA_INTERVAL) {
    lastDataSend = now;
    sendDataToPC();
  }

  // Check the PC for a new command every 5 seconds
  if (now - lastCmdCheck > CMD_INTERVAL) {
    lastCmdCheck = now;
    checkCommand();
  }
}

// =============================================
// Check warning state
// =============================================
bool checkWarning() {
  for (int i = 0; i < SLOT_COUNT; i++) {
    if ((i == 6 || i == 7 || i == 8)
        && slot[i] != "0" && slot[i] != "--") return true;
    if (i == 19 && slot[i] != "--" && slot[i].toInt() < 50) return true;
  }
  return false;
}

// =============================================
// Buzzer control
// =============================================
void updateBuzzer() {
  warningActive = checkWarning();
  if (warningActive) {
    unsigned long now = millis();
    if (now - lastBuzzer < 500) return;
    lastBuzzer = now;
    digitalWrite(BUZZER_PIN, !digitalRead(BUZZER_PIN));
  } else {
    digitalWrite(BUZZER_PIN, LOW);
  }
}

// =============================================
// GET /set?no1=value&no2=value ...
// =============================================
void handleSet() {
  int updated = 0;
  for (int i = 1; i <= SLOT_COUNT; i++) {
    String paramName = "no" + String(i);
    if (server.hasArg(paramName)) {
      slot[i - 1] = server.arg(paramName);
      updated++;
    }
  }
  server.send(200, "application/json", "{\"updated\":" + String(updated) + "}");
}

// =============================================
// GET / → HTML dashboard
// =============================================
void handleRoot() {
  String html = "<!DOCTYPE html><html lang=\"ko\"><head>"
  "<meta charset=\"UTF-8\">"
  "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">"
  "<title>IoT Hub + AI</title>"
  "<style>"
  "*{margin:0;padding:0;box-sizing:border-box;}"
  "body{font-family:system-ui,sans-serif;background:#1a1a2e;color:#eee;padding:16px;}"
  "h1{text-align:center;font-size:1.3rem;margin-bottom:4px;color:#e94560;}"
  ".sub{text-align:center;font-size:0.7rem;color:#555;margin-bottom:16px;}"
  ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));gap:10px;}"
  ".card{background:#16213e;border-radius:8px;padding:12px;border-left:4px solid #0f3460;}"
  ".card.warn{border-left-color:#e94560;background:#1c0e1a;}"
  ".card .no{font-size:0.6rem;color:#555;margin-bottom:2px;}"
  ".card .label{font-size:0.8rem;color:#a0a0b0;margin-bottom:4px;}"
  ".card .value{font-size:1.2rem;font-weight:700;}"
  ".footer{text-align:center;margin-top:16px;font-size:0.7rem;color:#444;}"
  "</style></head><body>"
  "<h1>IoT Hub + AI</h1><div class=\"sub\">Method B - PC Broker</div>"
  "<div class=\"grid\">";

  for (int i = 0; i < SLOT_COUNT; i++) {
    bool isWarn = false;
    String val = slot[i];
    if ((i == 6 || i == 7 || i == 8) && val != "0" && val != "--") isWarn = true;
    if (i == 19 && val != "--" && val.toInt() < 50) isWarn = true;
    html += "<div class=\"card" + String(isWarn ? " warn" : "") + "\">";
    html += "<div class=\"no\">#" + String(i + 1) + "</div>";
    html += "<div class=\"label\">" + String(label[i]) + "</div>";
    html += "<div class=\"value\">" + val + "</div></div>\n";
  }

  html += "</div><div class=\"footer\">IoT Hub + AI Broker | iothub.co.kr</div></body></html>";
  server.send(200, "text/html; charset=utf-8", html);
}

// =============================================
// GET /status → JSON
// =============================================
void handleStatus() {
  String json = "{";
  for (int i = 0; i < SLOT_COUNT; i++) {
    if (i > 0) json += ",";
    json += "\"no" + String(i + 1) + "\":\"" + slot[i] + "\"";
  }
  json += ",\"warning\":" + String(warningActive ? "true" : "false") + "}";
  server.send(200, "application/json", json);
}

// =============================================
// 404
// =============================================
void handleNotFound() {
  server.send(404, "text/plain", "404 - Use /set?no1=value  or  /  or  /status");
}

// =============================================
// TFT rendering (same as Part 22)
// =============================================
void updateTFT() {
  unsigned long now = millis();
  if (now - lastScroll < SCROLL_INTERVAL) return;
  lastScroll = now;
  tft.fillScreen(C_BG);

  tft.fillRect(0, 0, 320, 28, TFT_WHITE);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  char buf[40];
  sprintf(buf, "IoT Hub [%d/%d]",
    scrollOffset / ITEMS_PER_PAGE + 1,
    (SLOT_COUNT + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE);
  tft.drawString(buf, 4, 6, 2);

  for (int i = 0; i < ITEMS_PER_PAGE; i++) {
    int idx = scrollOffset + i;
    if (idx >= SLOT_COUNT) break;
    int y = 36 + i * 52;
    String val = slot[idx];

    bool isWarn = false;
    uint16_t cardBg = C_CARD;
    if ((idx == 6 || idx == 7 || idx == 8) && val != "0" && val != "--") {
      isWarn = true; cardBg = C_CARD_WARN;
    }
    if (idx == 19 && val != "--" && val.toInt() < 50) {
      isWarn = true; cardBg = C_CARD_WARN;
    }

    cards[i] = {4, y, 312, 48, idx};
    tft.fillRoundRect(4, y, 312, 48, 6, cardBg);
    tft.setTextColor(C_LABEL, cardBg);
    tft.drawString(label[idx], 14, y + 4, 2);
    tft.drawLine(14, y + 24, 306, y + 24, TFT_WHITE);
    tft.setTextColor(isWarn ? TFT_WHITE : C_VALUE, cardBg);
    tft.drawString(val, 18, y + 28, 4);
    tft.setTextColor(C_LABEL, cardBg);
    tft.drawString(getUnit(idx), 250, y + 28, 2);
    if (idx == 3 || idx == 6 || idx == 7 || idx == 8 || idx == 10 || idx == 19) {
      tft.drawString(":", 290, y + 4, 2);
    }
  }

  scrollOffset += ITEMS_PER_PAGE;
  if (scrollOffset >= SLOT_COUNT) scrollOffset = 0;
}

String getUnit(int idx) {
  switch (idx) {
    case 0: case 2: case 11: case 16: return "C";
    case 1: case 18: return "%";
    case 12: return "ppm"; case 13: return "ug";
    case 14: return "W"; case 15: return "L";
    case 19: return "g";
    default: return "";
  }
}

// =============================================
// Touch handling
// =============================================
void handleTouch() {
  if (!ts.tirqTouched() && !ts.touched()) return;
  TS_Point p = ts.getPoint();
  int tx = map(p.x, 200, 3800, 0, 320);
  int ty = map(p.y, 200, 3800, 0, 240);

  for (int i = 0; i < 4; i++) {
    CardZone cz = cards[i];
    if (tx >= cz.x && tx <= cz.x + cz.w &&
        ty >= cz.y && ty <= cz.y + cz.h) {
      int idx = cz.slotIdx;
      if (idx < 0 || idx >= SLOT_COUNT) break;
      if (idx == 3) { slot[idx] = (slot[idx] == "ON") ? "OFF" : "ON"; }
      else if (idx == 10) { slot[idx] = (slot[idx] == "ON") ? "OFF" : "ON"; }
      else if (idx == 6 || idx == 7 || idx == 8 || idx == 19) { slot[idx] = "0"; }
      lastScroll = 0;
      break;
    }
  }
}

// =============================================
// 🆕 Send the 20 sensor values to the PC (new in Part 24)
// =============================================
void sendDataToPC() {
  WiFiClient client;
  HTTPClient http;

  String url = String(PC_URL) + "/data";
  String body = "{";
  for (int i = 0; i < SLOT_COUNT; i++) {
    if (i > 0) body += ",";
    body += "\"no" + String(i + 1) + "\":\"" + slot[i] + "\"";
  }
  body += "}";

  http.begin(client, url);
  http.addHeader("Content-Type", "application/json");
  int code = http.POST(body);
  http.end();

  Serial.print("Data sent: HTTP ");
  Serial.print(code);
  Serial.print(" ("); Serial.print(body.length());
  Serial.println(" bytes)");
}

// =============================================
// 🆕 Fetch commands from the PC (new in Part 24)
// =============================================
void checkCommand() {
  WiFiClient client;
  HTTPClient http;

  String url = String(PC_URL) + "/command";
  http.begin(client, url);
  int code = http.GET();

  if (code == 200) {
    String resp = http.getString();
    if (resp != "{}" && resp != "") {
      Serial.print("Command received: ");
      Serial.println(resp);
      executeCommand(resp);
    }
  }
  http.end();
}

// =============================================
// 🆕 Execute command (new in Part 24)
// =============================================
void executeCommand(String json) {
  for (int i = 1; i <= SLOT_COUNT; i++) {
    String key = "no" + String(i);
    int pos = json.indexOf("\"" + key + "\":\"");
    if (pos < 0) continue;

    int start = pos + key.length() + 4;
    int end = json.indexOf("\"", start);
    String val = json.substring(start, end);

    slot[i - 1] = val;
    Serial.printf("  Slot %d (%s) <- %s\n", i, label[i - 1], val.c_str());
  }
  lastScroll = 0;
}
Here's exactly what differs from the Part 22 code: two lines added to loop() + three functions sendDataToPC, checkCommand, executeCommand + the PC_URL definition at the top. None of the existing TFT/touch/buzzer/web-server/warning features were touched.
Why is the TFT in English but Telegram in Korean? The ESP32's TFT library doesn't support Korean fonts, so the TFT screen shows English. Telegram and the AI prompt, on the other hand, handle Korean freely. That's why the TFT shows "Living Temp" while Telegram on your phone shows "거실 온도." It's intentional, so don't let it confuse you.

Python Flask server (installed on the PC)

This is the heart of Method B. Your PC needs Python 3.8 or newer installed.

Install the libraries

In a terminal (Mac/Linux) or the Command Prompt (Windows):

Part 24 — AI Butler Method B: a safe AI broker with an old PC — full code
pip install flask requests

Full code (server.py)

Just save this one file on the PC and run it. You only need to change three spots: OPENAI_API_KEY, TELEGRAM_BOT_TOKEN, and YOUR_CHAT_ID.

Part 24 — AI Butler Method B: a safe AI broker with an old PC — full code
from flask import Flask, request, jsonify
import requests
import threading
import time
import json
import urllib.request
import urllib.parse

# =============================================
# 1. Settings (fill in yourself!)
# =============================================
OPENAI_API_KEY = "sk-your-api-key-here"
OPENAI_URL     = "https://api.openai.com/v1/chat/completions"
AI_MODEL       = "gpt-3.5-turbo"

TELEGRAM_BOT_TOKEN = "123456:ABC-DEF1234ghijk"
YOUR_CHAT_ID = 123456789

# =============================================
# 2. Flask app
# =============================================
app = Flask(__name__)

latest_data = {}
pending_commands = {}
last_ai_time = 0
AI_INTERVAL = 300

LABELS = [
    "Living Temp","Living Hum","Outside Temp","AC",
    "Boiler","Stove","Gas Leak","Fire",
    "Front Door","Door Lock","Living Light","Fridge Temp",
    "CO2","Dust PM2.5","Power","Water",
    "Hot Water","Rain","Bath Hum","Pet Bowl"
]

# =============================================
# 3. ESP32 → PC: receive sensor data
# =============================================
@app.route('/data', methods=['POST'])
def receive_data():
    global latest_data
    data = request.get_json()
    if data:
        latest_data = data
        print(f"[DATA] Received {len(data)} sensor values")
    return jsonify({"status": "ok"})

# =============================================
# 4. ESP32 ← PC: deliver commands
# =============================================
@app.route('/command', methods=['GET'])
def send_command():
    global pending_commands
    if pending_commands:
        cmd = json.dumps(pending_commands)
        pending_commands = {}
        print(f"[CMD] Sent: {cmd}")
        return cmd
    return "{}"

# =============================================
# 5. Handle Telegram commands
# =============================================
def handle_telegram_command(text):
    global pending_commands
    tl = text.lower().strip()

    if "에어컨" in text or "ac" in tl:
        if "켜" in text or "on" in tl:
            pending_commands["no4"] = "ON"
            return "에어컨 ON"
        elif "꺼" in text or "off" in tl:
            pending_commands["no4"] = "OFF"
            return "에어컨 OFF"

    if "조명" in text or "불" in text or "light" in tl:
        if "켜" in text or "on" in tl:
            pending_commands["no11"] = "ON"
            return "조명 ON"
        elif "꺼" in text or "off" in tl:
            pending_commands["no11"] = "OFF"
            return "조명 OFF"

    if "상태" in text or "집" in text or "status" in tl:
        return build_status_report()

    return None

def build_status_report():
    if not latest_data:
        return "아직 센서 데이터가 없습니다."
    report = "집 상태 보고\n\n"
    warnings = []
    for i in range(20):
        val = latest_data.get(f"no{i+1}", "--")
        report += f"  {LABELS[i]}: {val}\n"
        if i == 6 and val not in ("0", "--"): warnings.append("가스 누출!")
        if i == 7 and val not in ("0", "--"): warnings.append("화재 감지!")
        if i == 8 and val not in ("0", "--"): warnings.append("현관문 열림!")
        if i == 19 and val != "--":
            try:
                if int(val) < 50: warnings.append("반려견 밥 보충!")
            except: pass
    if warnings:
        report += "\n" + "\n".join(warnings)
    return report

# =============================================
# 6. AI analysis (background)
# =============================================
def ai_analysis_loop():
    global last_ai_time, pending_commands
    while True:
        time.sleep(30)
        if not latest_data: continue
        now = time.time()
        if now - last_ai_time < AI_INTERVAL: continue
        last_ai_time = now

        try:
            data_text = "\n".join(
                f"{LABELS[i]}: {latest_data.get(f'no{i+1}', '--')}"
                for i in range(20)
            )
            system_prompt = (
                "You are an AI home assistant. Analyze the sensor data "
                "and provide a brief English summary. "
                "1) Any warnings? 2) Any anomalies? 3) Recommended actions. "
                "Keep it under 4 sentences."
            )
            resp = requests.post(
                OPENAI_URL,
                headers={
                    "Authorization": f"Bearer {OPENAI_API_KEY}",
                    "Content-Type": "application/json"
                },
                json={
                    "model": AI_MODEL,
                    "messages": [
                        {"role": "system", "content": system_prompt},
                        {"role": "user", "content": data_text}
                    ],
                    "max_tokens": 200
                },
                timeout=30
            )
            if resp.status_code == 200:
                content = resp.json()["choices"][0]["message"]["content"]
                print(f"[AI] {content}")
                # Send the AI analysis result via Telegram
                send_telegram(YOUR_CHAT_ID, "[AI 분석]
" + content)
            else:
                print(f"[AI] API error: {resp.status_code}")
        except Exception as e:
            print(f"[AI] Error: {e}")

# =============================================
# 7. Telegram bot (polling)
# =============================================
def telegram_loop():
    base_url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}"
    last_update_id = 0
    print("[TELEGRAM] Bot started. Send /start to begin.")

    while True:
        time.sleep(3)
        try:
            url = f"{base_url}/getUpdates?offset={last_update_id + 1}&timeout=10"
            resp = requests.get(url, timeout=15).json()
            for update in resp.get("result", []):
                last_update_id = update["update_id"]
                if "message" not in update: continue
                msg = update["message"]
                chat_id = msg["chat"]["id"]
                text = msg.get("text", "")
                print(f"[TELEGRAM] From {chat_id}: {text}")

                if text == "/start":
                    send_telegram(chat_id,
                        "AI 집사입니다.\n"
                        "'상태', '에어컨 켜줘/꺼줘', '조명 켜줘/꺼줘'")
                    continue

                reply = handle_telegram_command(text)
                if reply:
                    send_telegram(chat_id, reply)
                else:
                    send_telegram(chat_id,
                        "'상태', '에어컨 켜줘/꺼줘', '조명 켜줘/꺼줘' 중에 말해주세요.")
        except Exception as e:
            print(f"[TELEGRAM] Error: {e}")

def send_telegram(chat_id, text):
    url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
    data = urllib.parse.urlencode({"chat_id": chat_id, "text": text}).encode()
    urllib.request.urlopen(url, data=data)

# =============================================
# 8. Run
# =============================================
if __name__ == "__main__":
    threading.Thread(target=ai_analysis_loop, daemon=True).start()
    threading.Thread(target=telegram_loop, daemon=True).start()
    print("=" * 50)
    print("  AI 집사 서버 시작")
    print("=" * 50)
    app.run(host="0.0.0.0", port=5000, debug=False)

Telegram setup

What's Telegram? It's a free messenger, like KakaoTalk. Search for Telegram in the app store, install it, sign up with your phone number, and you're done. Takes about a minute.

Create the bot (1 minute)

1. In the Telegram app, search for @BotFather → start a chat. 2. Type /newbot. 3. Enter a bot name → My Home AI Butler. 4. Enter a bot username → MyHomeAIBot (must be unique). 5. BotFather gives you a token → copy it and paste it into TELEGRAM_BOT_TOKEN.

Find your Chat ID (30 seconds)

Why do I need a Chat ID? The bot uses this number to tell "who it should send messages to." Without it, the bot can't message anyone. It's your personal number, so don't share it with anyone — treat it like a password.

1. In Telegram, search for @userinfobot → start a chat. 2. Type /start. 3. Id: 123456789 ← paste this number into YOUR_CHAT_ID.

How to run

Run the server on the PC

1. Save the server.py code above on the PC with the filename server.py. 2. In a terminal (Mac) or the Command Prompt (Windows), move to the folder containing server.py. 3. Install the libraries (first time only): pip install flask requests. 4. Run the server: python server.py. 5. If you see the output below, it worked:

==================================================
  AI 집사 서버 시작
==================================================
[TELEGRAM] Bot started. Send /start to begin.
Close this terminal window and the server shuts down. You'll need to run it again after rebooting the PC.

6. Find the PC's IP address: on Mac, ifconfig | grep inet; on Windows, ipconfigIPv4 Address.

Upload to the ESP32

1. Open the Arduino IDE. (If you haven't installed it, here.) 2. Copy the full ESP32 code above and paste it into a blank Arduino IDE window. 3. Change YOUR_SSID and YOUR_PASSWORD to your home router's name/password. Don't delete the double quotes. 4. Change PC_URL to your PC's IP. Example: "http://192.168.0.100:5000". (For how to find the IP, see step 6 of Run the server on the PC just above.) 5. Connect the ESP32 to the PC with a USB cable. It must be a data cable, not charge-only. 6. In the Arduino IDE: Tools → Board → ESP32 Dev Module. Tools → Flash Size → 4MB. 7. Under Tools → Port, select the port the ESP32 is on. (Windows: COM3, Mac: /dev/cu.usbserial-XXXX.) 8. Click the Upload button (→). When the black window at the bottom shows "Done uploading," it's finished. 9. Open Tools → Serial Monitor and set the number at the bottom right to 115200. If Data sent: HTTP 200 appears after 30 seconds, it worked.

What to check

Check these five things in order. If any of them fails, jump straight to "Troubleshooting" just below.

1) Is the ESP32 sending data to the PC?

  • Does Data sent: HTTP 200 appear in the ESP32 Serial Monitor (the black window)?

2) Did the PC receive the data?

  • Does [DATA] Received 20 sensor values appear in the black window running server.py?

3) Is the Telegram bot alive?

  • When you open the Telegram app on your phone and send /start to @MyHomeAIBot, does it reply?
  • If there's no reply to /start, check that server.py is running on the PC.

4) Is the AI analysis working?

  • After waiting 5 minutes, does a message starting with [AI] appear in the server.py black window?
  • And at the same time, does an AI analysis message arrive over Telegram?

5) Do commands from your phone get through?

  • Send "turn on the AC" to the Telegram bot.
  • Does the bot reply with "에어컨 ON"?
  • Does Command received appear in the ESP32 Serial Monitor?

Troubleshooting

SymptomCauseFix
ESP32 HTTP -1Wrong PC IPDouble-check the PC IP. Make sure both are on the same router
pip install failsPython not installedInstall 3.8+ from python.org. Check "Add to PATH"
Telegram bot doesn't respondChat ID mismatchRe-check the ID with @userinfobot
[AI] API error: 401API key errorCheck the key's status in the OpenAI dashboard
import requests errorLibrary not installedRe-run pip install flask requests

What we've done so far

  • ESP32: still the brain. Receives sensors + TFT + talks to the PC. The code was one copy-paste.
  • The old PC: keeps the API key safe + calls the AI + Telegram. Additional cost: $0
  • Telegram: check AI analysis and send commands even when you're out

The core loop of Physical AI (sense → decide → act) is complete.

Next post preview

Method C — fully integrated on a Raspberry Pi. A single Pi replaces the ESP32 + PC + Telegram entirely. 100 lines of Python. 3W of power. Running 24/7 with no downtime.