본문 바로가기
IT/HomeLabs

[HomeLabs] Home Assistant를 텔레그램으로 제어하기: Gemini AI 자연어 스마트홈 봇 만들기

by 수누다 2026. 4. 5.

Home Assistant를 텔레그램으로 제어하기: Gemini AI 자연어 스마트홈 봇 만들기

"거실 불 꺼줘"라고 텔레그램에 보내면 집의 조명이 꺼진다면? Home Assistant(이하 HA)를 사용하면서 항상 아쉬웠던 점이 있었습니다. 외출 중에 기기를 제어하려면 HA 앱을 열어야 하고, 자동화를 확인하려면 웹 UI에 접속해야 했거든요. 특히 아내에게 "HA 대시보드에서 거실 에어컨 entity 찾아서 꺼줘"라고 말할 수는 없잖아요.

그래서 텔레그램 봇 + Google Gemini AI를 조합해서, 자연어로 스마트홈을 제어할 수 있는 봇을 만들었습니다. Proxmox 홈랩의 Docker 컨테이너 하나로 돌아가고, 메모리도 50~80MB밖에 안 먹습니다.

완성된 봇이 할 수 있는 것

이 봇으로 할 수 있는 것들을 먼저 정리해보겠습니다.

기능 방법 예시
기기 상태 확인 /status 방별 조명/에어컨 상태 한눈에
기기 켜기/끄기 /on 거실 에어컨 이름으로 검색하여 제어
자동화 관리 /auto 자동화 목록 + 활성화/비활성화
자연어 제어 텍스트 입력 "거실 불 꺼줘", "에어컨 24도로 설정"
기기 목록 /devices 전체 기기를 도메인별로 정리

자연어 제어가 핵심입니다. Gemini AI가 사용자의 말을 분석해서 어떤 entity_id에 어떤 서비스를 호출해야 하는지 자동으로 판단합니다.

사전 준비

환경 정보

  • Home Assistant: Proxmox VM/LXC에서 실행 중
  • 연동 기기: 코맥스 월패드(SmartThings 경유), SmartThings 기기, LG TV, Roborock 로봇청소기, Bambu Lab 3D 프린터
  • 실행 환경: Proxmox LXC 컨테이너 (Docker)
  • Python: 3.12

필요한 것

  1. HA Long-Lived Access Token — HA 웹 UI → 프로필 → 보안 → 장기 액세스 토큰 생성
  2. 텔레그램 봇 토큰 — @BotFather에서 /newbot으로 생성
  3. Gemini API 키 — Google AI Studio에서 무료 발급
  4. Docker — 컨테이너로 실행할 환경

프로젝트 구조

ha-bot/
├── config/
│   └── settings.py          # 환경 설정 (HA URL, 토큰 등)
├── ha/
│   ├── client.py             # HA REST API 클라이언트
│   └── automations.py        # 자동화 설정 정의
├── ai/
│   └── assistant.py          # Gemini AI 자연어 처리
├── telegram_bot.py           # 텔레그램 봇 메인
├── Dockerfile
├── docker-compose.yml
└── .env                      # 환경변수 (API 키 등)

핵심 구현

1. HA REST API 클라이언트

HA의 REST API를 비동기(aiohttp)로 호출하는 클라이언트입니다. 텔레그램 봇이 비동기로 동작하기 때문에 API 클라이언트도 async로 만들어야 합니다.

import aiohttp

class HAClient:
    def __init__(self):
        self.base_url = "http://192.168.20.110:8123"
        self.headers = {
            "Authorization": f"Bearer {HA_TOKEN}",
            "Content-Type": "application/json",
        }

    async def call_service(self, domain, service, entity_id=None, **data):
        """HA 서비스 호출 — 기기 제어의 핵심"""
        payload = dict(data)
        if entity_id:
            payload["entity_id"] = entity_id

        url = f"{self.base_url}/api/services/{domain}/{service}"
        async with aiohttp.ClientSession(headers=self.headers) as session:
            async with session.post(url, json=payload) as resp:
                return resp.status in (200, 201)

    async def turn_on(self, entity_id, **kwargs):
        domain = entity_id.split(".")[0]
        return await self.call_service(domain, "turn_on", entity_id, **kwargs)

    async def turn_off(self, entity_id):
        domain = entity_id.split(".")[0]
        return await self.call_service(domain, "turn_off", entity_id)

entity_id에서 도메인(light, switch, climate 등)을 자동 추출해서 적절한 서비스를 호출합니다. turn_on, turn_off, toggle 같은 공통 서비스는 대부분의 도메인에서 동작합니다.

2. Gemini AI 자연어 처리

이 부분이 가장 재미있는 부분입니다. 사용자가 "거실 불 꺼줘"라고 보내면, Gemini가 이걸 분석해서 JSON 형태의 HA API 호출로 변환합니다.

from google import genai
from google.genai import types

class AIAssistant:
    def __init__(self):
        self._client = genai.Client(api_key=GEMINI_API_KEY)

    def parse_command(self, user_message, devices_context):
        prompt = f"""## 현재 기기 목록
{devices_context}

## 사용자 명령
{user_message}
"""
        response = self._client.models.generate_content(
            model="gemini-2.5-flash",
            contents=prompt,
            config=types.GenerateContentConfig(
                system_instruction=SYSTEM_PROMPT,
                temperature=0.1,
                response_mime_type="application/json",
            ),
        )
        return json.loads(response.text)

핵심은 시스템 프롬프트입니다. AI에게 현재 기기 목록(entity_id, 이름, 상태)을 전달하고, 사용자의 명령을 3가지 타입으로 분류하도록 합니다:

  • action: 기기 제어 (domain, service, entity_id, data)
  • query: 상태 조회 (조회할 entity_id 목록)
  • chat: 일반 대화

temperature=0.1로 설정해서 AI가 창의적인 해석 대신 정확한 매칭을 하도록 했습니다. response_mime_type="application/json"으로 JSON 출력을 강제하면 파싱 실패도 거의 없습니다.

3. 텔레그램 봇

python-telegram-bot 라이브러리를 사용합니다. 명령어 핸들러와 자연어 메시지 핸들러를 분리했습니다.

from telegram.ext import Application, CommandHandler, MessageHandler, filters

app = Application.builder().token(TELEGRAM_BOT_TOKEN).build()

# 기기 목록 캐싱 (5분마다 갱신)
async def _get_devices(context):
    cache = context.bot_data.get("devices_cache")
    cache_time = context.bot_data.get("devices_cache_time")
    if cache and (datetime.now() - cache_time).seconds < 300:
        return cache
    # HA API에서 새로 가져오기
    devices = await ha.get_devices_summary()
    context.bot_data["devices_cache"] = devices
    return devices

⚠️ 주의사항: HA에 기기가 많으면(제 경우 200개 이상) 매번 API를 호출하는 건 비효율적입니다. 5분 캐싱으로 해결했습니다.

자연어 메시지 처리는 이렇게 동작합니다:

async def handle_message(update, context):
    text = update.message.text
    devices = await _get_devices(context)
    devices_text = _devices_to_text(devices)  # AI에 전달할 기기 목록

    # Gemini AI로 명령 분석
    result = await loop.run_in_executor(None, ai.parse_command, text, devices_text)

    if result["type"] == "action":
        for action in result["actions"]:
            await ha.call_service(action["domain"], action["service"], action["entity_id"])
        await update.message.reply_text(result["response"])
    elif result["type"] == "query":
        # 상태 조회 후 응답
        ...

Gemini API 호출은 블로킹이라 run_in_executor로 스레드풀에서 실행합니다. 안 그러면 AI가 응답할 때까지 봇이 먹통이 됩니다.

4. 방별 상태 표시

/status를 누르면 기기를 방별로 분류해서 보여줍니다. entity_id나 friendly_name에서 방 이름을 추출하는 방식입니다.

🏠 기기 상태

📍 거실
  🔴🌡️ 에어컨: off
  🔴🪟 커튼: closed
  🟢📺 TV: on

📍 안방
  🔴🌡️ 에어컨: off
  🟢🔌 멀티탭: on

📍 수현이방
  🟢💡 조명: on

🤖 로봇청소기: 대기 중

자동화 등록

봇에서 /setup 명령으로 미리 정의된 자동화를 HA에 일괄 등록할 수 있게 만들었습니다.

AWAY_MODE = {
    "id": "telegram_bot_away_mode",
    "alias": "외출 모드",
    "trigger": [{
        "platform": "state",
        "entity_id": "person.pswq",
        "to": "not_home",
        "for": {"minutes": 5},
    }],
    "action": [
        {"service": "light.turn_off", "target": {"entity_id": "all"}},
        {"service": "climate.turn_off", "target": {"entity_id": [...]}},
        {"service": "cover.close_cover", "target": {"entity_id": [...]}},
    ],
}

현재 등록한 자동화:

자동화 트리거 동작
외출 모드 5분간 외출 감지 조명/에어컨 끄기, 커튼 닫기, 청소기 시작
귀가 모드 귀가 감지 조명 켜기, 커튼 열기
아침 커튼 매일 07:00 거실 커튼 열기
저녁 커튼 매일 22:00 거실 커튼 닫기
3D 프린터 팬 출력 시작/완료 팬 자동 ON, 완료 10분 후 OFF + 알림

Docker 배포

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "telegram_bot.py"]
# docker-compose.yml
services:
  ha-bot:
    build: .
    container_name: ha-bot-telegram
    restart: unless-stopped
    env_file: .env
    volumes:
      - ./logs:/app/logs
docker compose up -d --build

이게 끝입니다. 컨테이너 하나로 메모리 50~80MB만 사용하면서 돌아갑니다. restart: unless-stopped로 서버 재시작 시에도 자동으로 올라옵니다.

⚠️ 트러블슈팅

entity_id에 특수문자가 있으면 텔레그램 HTML 파싱 실패

처음에 텔레그램 메시지를 HTML 모드(parse_mode=ParseMode.HTML)로 보냈더니, entity_id에 <>가 포함된 경우 파싱 에러가 났습니다. 해결: 기기 목록/상태 표시는 그냥 일반 텍스트로 전환했습니다. 꾸미기보다 안정성이 중요합니다.

Gemini AI가 엉뚱한 entity_id를 반환

기기 목록을 AI에 전달할 때 entity_id | friendly_name | state 형태로 주니까, AI가 가장 유사한 entity_id를 정확하게 찾아줍니다. 기기 목록을 캐싱해서 항상 최신 상태를 전달하는 게 중요합니다.

기기가 200개 이상이면 Gemini 컨텍스트가 커짐

전체 기기 목록을 매번 AI에 전달하니 토큰이 꽤 소모됩니다. 제어 가능한 도메인(light, switch, climate 등)만 필터링하면 절반 이하로 줄일 수 있습니다.

마무리

텔레그램 봇 하나로 스마트홈 제어가 이렇게 편해질 줄 몰랐습니다. 특히 Gemini AI 덕분에 entity_id를 외울 필요 없이 자연어로 제어할 수 있는 게 가장 큰 장점입니다. 아내에게 "텔레그램에서 거실 불 꺼줘라고 보내면 돼"라고 말할 수 있게 됐거든요.

다음 단계로는:

  • 코맥스 월패드 연동 (EW11 + SmartThings Edge 드라이버)
  • 세탁기/건조기 완료 알림 자동화
  • 에어컨 온도 기반 자동 제어

홈랩에서 이런 프로젝트를 하나씩 만들어가는 재미가 있습니다. 궁금한 점이 있다면 댓글로 남겨주세요.