Tessie API와 Telegram으로 손쉽게 만드는 테슬라 제어 봇

안녕하세요, 테슬라 차주 여러분!

오늘은 여러분의 테슬라 차량을 더욱 편리하게 관리할 수 있는 Telegram 봇을 만드는 방법을 소개하려고 합니다. 초보자도 쉽게 따라할 수 있도록 단계별로 자세히 설명드리니, 천천히 따라와 주세요!

Telegram 봇이란?

Telegram 봇(Telegram Bot)은 Telegram 메신저에서 자동으로 작동하는 프로그램입니다. 이를 통해 메시지를 주고받거나, 특정 명령어에 반응하여 다양한 작업을 수행할 수 있습니다. 이번 글에서는 Telegram 봇을 이용해 테슬라 차량의 상태를 조회하고, 잠금/잠금 해제, 서리 제거, 창문 닫기 등 여러 기능을 제어하는 방법을 알아보겠습니다.

준비물

봇을 만들기 전에 몇 가지 준비물이 필요합니다:

  1. Telegram 계정: Telegram 앱을 설치하고 가입하세요.
  2. Heroku 계정: 무료로 사용할 수 있는 클라우드 플랫폼입니다. Heroku 가입하기
  3. Tessie 계정: Tessie는 테슬라 차량을 제어하기 위한 API를 제공합니다. Tessie 가입하기 (가입 및 API 키 발급 과정은 아래에서 자세히 설명)
  4. Python 설치: Python은 프로그래밍 언어로, 봇을 작성하는 데 사용됩니다. Python 다운로드
  5. Git 설치: 버전 관리 도구로, Heroku에 코드를 배포할 때 필요합니다. Git 다운로드

단계별 가이드

1단계: Tessie 가입하고 API 토큰 받기

Tessie 웹사이트 방문하기

회원가입 및 로그인

  • Tessie 계정이 없다면 회원가입을 진행하세요. 이미 계정이 있다면 로그인합니다.

API 키 발급받기

  • 로그인 후, 대시보드에서 API 섹션으로 이동합니다.
  • 새로운 API 키를 생성하고, 안전한 장소에 저장하세요. 이 키는 봇이 테슬라 차량을 제어하는 데 필요합니다.
  • 예시:
TESSIE_API_KEY=abcdefghijklmnopqrstuvwxyz1234567890

차량 VIN 확인하기

  • Tessie 대시보드에서 제어할 차량의 VIN(차대번호)를 확인합니다. VIN은 차량 등록증이나 차량 자체에서 확인할 수 있습니다.
  • 예시:
VEHICLE_VIN=LRWYGCFS9RC562139

2단계: Telegram 봇 생성하기

BotFather와 대화하기

  • Telegram 앱을 열고, 검색창에 @BotFather를 입력하여 BotFather를 찾습니다.
  • BotFather와 대화를 시작한 후, /start 명령어를 입력합니다.

새 봇 생성하기

  • /newbot 명령어를 입력합니다.
  • 봇의 이름을 입력합니다. 예: MyTeslaBot
  • 봇의 사용자 이름을 입력합니다. 예: my_tesla_bot (끝에 _bot을 꼭 붙여야 합니다)

API 토큰 받기

  • 봇이 성공적으로 생성되면, BotFather가 API 토큰을 제공합니다. 이 토큰은 봇을 제어하는 데 필요하니 안전하게 보관하세요.
  • 예시:
123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ

3단계: Heroku 준비하기

Heroku에 로그인하기

Heroku CLI 설치하기

  • Heroku CLI(Command Line Interface)는 터미널을 통해 Heroku를 관리할 수 있게 해줍니다.
  • Heroku CLI 다운로드 페이지에서 운영체제에 맞는 설치 파일을 다운로드하여 설치하세요.

Heroku에 로그인하기

  • 터미널(명령 프롬프트)을 열고 다음 명령어를 입력하여 Heroku에 로그인합니다:
$ heroku login
  • 웹 브라우저가 열리며 Heroku 계정에 로그인하라는 메시지가 표시됩니다. 로그인 후 터미널로 돌아갑니다.

4단계: 프로젝트 준비하기

프로젝트 디렉토리 만들기

  • 터미널에서 프로젝트를 저장할 디렉토리를 만듭니다. 예를 들어, my_tesla_bot이라는 폴더를 만듭니다:

Python 가상환경 설정하기

$ mkdir my_tesla_bot
$ cd my_tesla_bot
  • 가상환경을 사용하면 프로젝트마다 필요한 패키지를 독립적으로 관리할 수 있습니다.
$ python -m venv venv
  • 가상환경을 활성화합니다:
  • Windows:
$ venv\Scripts\activate
  • macOS/Linux:
$ source venv/bin/activate

필요한 패키지 설치하기

  • 필요한 Python 패키지를 설치합니다:
$ pip install python-telegram-bot requests python-dotenv

필수 파일 만들기

  • bot.py: 봇의 메인 코드 파일
  • Procfile: Heroku가 애플리케이션을 실행하는 방법을 알려주는 파일
  • .env: 환경 변수를 저장하는 파일 (API 토큰 등)

5단계: 코드 작성하기

  1. bot.py 파일 작성하기
  • 텍스트 편집기(예: VS Code, 메모장)를 열고 bot.py 파일을 만듭니다.
  • 아래의 코드를 복사하여 붙여넣습니다:
import os
import logging
from telegram.ext import Updater, CommandHandler
from telegram import Update
import requests
from dotenv import load_dotenv

# 로깅 설정
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO
)

logger = logging.getLogger(__name__)

# 환경 변수 로드
load_dotenv()

# 환경 변수에서 토큰 및 API 키 가져오기
TELEGRAM_TOKEN = os.getenv('TELEGRAM_TOKEN')
TESSIE_API_KEY = os.getenv('TESSIE_API_KEY')
VEHICLE_VIN = os.getenv('VEHICLE_VIN')  # 차량의 VIN 번호

# Tessie API 헤더 설정
TESSIE_HEADERS = {
    "accept": "application/json",
    "authorization": f"Bearer {TESSIE_API_KEY}"
}

# 승인된 사용자 ID 목록 (환경 변수에서 가져오기)
AUTHORIZED_USERS = os.getenv('AUTHORIZED_USERS')
if AUTHORIZED_USERS:
    AUTHORIZED_USERS = [int(user_id) for user_id in AUTHORIZED_USERS.split(',')]
else:
    AUTHORIZED_USERS = []

def restricted(func):
    """함수 접근을 승인된 사용자로 제한"""
    def wrapper(update: Update, context):
        user_id = update.effective_user.id
        logger.info(f"사용자 ID: {user_id}")
        logger.info(f"승인된 사용자: {AUTHORIZED_USERS}")
        if user_id not in AUTHORIZED_USERS:
            update.message.reply_text("🚫 접근 권한이 없습니다.")
            return
        return func(update, context)
    return wrapper

# 마일을 킬로미터로 변환하는 함수
def miles_to_km(miles):
    try:
        return miles * 1.60934
    except (TypeError, ValueError):
        return 'N/A'

# API 요청 함수
def get_vehicle_info():
    url = "https://api.tessie.com/vehicles"
    response = requests.get(url, headers=TESSIE_HEADERS)
    if response.status_code == 200:
        data = response.json()
        if 'results' in data:
            return data
        else:
            logger.error(f"Unexpected response structure: {data}")
            return None
    else:
        logger.error(f"Failed to get vehicle info: {response.text}")
        return None

def send_vehicle_command(command, params=None):
    url = f"https://api.tessie.com/{VEHICLE_VIN}/command/{command}"
    try:
        response = requests.post(url, headers=TESSIE_HEADERS, params=params)
        if response.status_code == 200:
            data = response.json()
            if 'result' in data:
                return data
            else:
                logger.error(f"Unexpected response structure: {data}")
                return None
        else:
            logger.error(f"Failed to execute command '{command}': {response.text}")
            return None
    except requests.RequestException as e:
        logger.error(f"Request exception: {e}")
        return None

def get_drivers():
    url = f"https://api.tessie.com/{VEHICLE_VIN}/drivers"
    response = requests.get(url, headers=TESSIE_HEADERS)
    if response.status_code == 200:
        return response.json()
    else:
        logger.error(f"Failed to get drivers: {response.text}")
        return None

def get_invitations():
    url = f"https://api.tessie.com/{VEHICLE_VIN}/invitations"
    response = requests.get(url, headers=TESSIE_HEADERS)
    if response.status_code == 200:
        return response.json()
    else:
        logger.error(f"Failed to get invitations: {response.text}")
        return None

# 명령어 핸들러 함수
@restricted
def start(update: Update, context):
    update.message.reply_text('안녕하세요! 테슬라 봇입니다. 명령어 목록을 보시려면 /help를 입력하세요.')

@restricted
def status(update: Update, context):
    vehicle_info = get_vehicle_info()
    logger.info(f"vehicle_info: {vehicle_info}")  # 디버깅을 위한 로그 출력
    if vehicle_info and 'results' in vehicle_info and len(vehicle_info['results']) > 0:
        vehicle = vehicle_info['results'][0]  # 첫 번째 차량 사용
        last_state = vehicle.get('last_state', {})
        charge_state = last_state.get('charge_state', {})
        climate_state = last_state.get('climate_state', {})
        drive_state = last_state.get('drive_state', {})
        vehicle_state = last_state.get('vehicle_state', {})
        vehicle_config = last_state.get('vehicle_config', {})
        
        # 차량 기본 정보
        display_name = last_state.get('display_name', 'N/A')
        vin = vehicle.get('vin', 'N/A')
        plate = vehicle.get('plate', 'N/A')
        state = last_state.get('state', 'N/A')
        
        # 충전 상태
        battery_level = charge_state.get('battery_level', 'N/A')
        battery_range = charge_state.get('battery_range', 'N/A')
        if isinstance(battery_range, (int, float)):
            battery_range_km = miles_to_km(battery_range)  # 마일을 km로 변환
            battery_range = f"{battery_range_km:.2f} km"
        charging_state = charge_state.get('charging_state', 'N/A')
        charge_rate = charge_state.get('charge_rate', 'N/A')
        if isinstance(charge_rate, (int, float)):
            charge_rate = f"{charge_rate:.2f} km/h"
        time_to_full_charge = charge_state.get('time_to_full_charge', 'N/A')
        if isinstance(time_to_full_charge, (int, float)):
            time_to_full_charge = f"{time_to_full_charge:.2f} 시간"
        
        # 기후 상태
        inside_temp = climate_state.get('inside_temp', 'N/A')
        if isinstance(inside_temp, (int, float)):
            inside_temp = f"{inside_temp:.1f}°C"
        outside_temp = climate_state.get('outside_temp', 'N/A')
        if isinstance(outside_temp, (int, float)):
            outside_temp = f"{outside_temp:.1f}°C"
        is_climate_on = climate_state.get('is_climate_on', False)
        
        # 주행 상태
        speed = drive_state.get('speed', 'N/A')
        if isinstance(speed, (int, float)):
            speed = f"{speed:.1f} km/h"
        power = drive_state.get('power', 'N/A')
        if isinstance(power, (int, float)):
            power = f"{power:.1f} kW"
        latitude = drive_state.get('latitude', 'N/A')
        longitude = drive_state.get('longitude', 'N/A')
        heading = drive_state.get('heading', 'N/A')
        if isinstance(heading, (int, float)):
            heading = f"{heading:.1f}°"
        
        # 차량 상태
        locked = vehicle_state.get('locked', 'N/A')
        odometer = vehicle_state.get('odometer', 'N/A')
        if isinstance(odometer, (int, float)):
            odometer_km = miles_to_km(odometer)  # 마일을 km로 변환
            odometer = f"{odometer_km:.2f} km"
        sentry_mode = vehicle_state.get('sentry_mode', 'N/A')
        
        # 차량 구성
        car_type = vehicle_config.get('car_type', 'N/A')
        exterior_color = vehicle_config.get('exterior_color', 'N/A')
        wheel_type = vehicle_config.get('wheel_type', 'N/A')
        charger_voltage = charge_state.get('charger_voltage', 'N/A')
        charger_power = charge_state.get('charger_power', 'N/A')
        
        # 위치 정보 링크 생성
        if latitude != 'N/A' and longitude != 'N/A':
            location_url = f"https://www.google.com/maps/search/?api=1&query={latitude},{longitude}"
            location_text = f"[지도에서 보기]({location_url})"
        else:
            location_text = "N/A"
        
        # 상태 메시지 작성
        status_text = f"""
🚗 **차량 이름**: {display_name}
🔢 **VIN**: {vin}
🔖 **번호판**: {plate}
📡 **차량 상태**: {state}

🔋 **배터리 수준**: {battery_level}%
🔌 **배터리 주행 가능 거리**: {battery_range}
⚡️ **충전 상태**: {charging_state}
⚡️ **충전 속도**: {charge_rate}
⏳ **완충까지 시간**: {time_to_full_charge}
🔌 **충전 전압**: {charger_voltage} V
⚡️ **충전 전력**: {charger_power} kW

🌡 **실내 온도**: {inside_temp}
🌡 **실외 온도**: {outside_temp}
❄️ **에어컨 상태**: {'켜짐' if is_climate_on else '꺼짐'}

🚙 **속도**: {speed}
🔋 **전력 소비**: {power}
📍 **위치**: {location_text}
🧭 **방향**: {heading}°

🔒 **잠금 상태**: {'잠김' if locked else '열림'}
🛣 **주행 거리계**: {odometer}
🎥 **센트리 모드**: {'활성화' if sentry_mode else '비활성화'}

🚘 **차량 종류**: {car_type}
🎨 **외장 색상**: {exterior_color}
🛞 **휠 종류**: {wheel_type}
"""
        update.message.reply_text(status_text, parse_mode='Markdown', disable_web_page_preview=True)
    else:
        update.message.reply_text('차량 정보를 가져올 수 없습니다.')

@restricted
def lock(update: Update, context):
    params = {
        'retry_duration': 40,         # 기본값
        'wait_for_completion': 'true' # 기본값
    }
    result = send_vehicle_command('lock', params=params)
    if result:
        if result.get('result'):
            update.message.reply_text('🔒 차량이 잠겼습니다.')
        else:
            update.message.reply_text('차량 잠금에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('차량 잠금에 실패했습니다. API 요청을 확인하세요.')

@restricted
def unlock(update: Update, context):
    params = {
        'retry_duration': 40,         # 기본값
        'wait_for_completion': 'true' # 기본값
    }
    result = send_vehicle_command('unlock', params=params)
    if result:
        if result.get('result'):
            update.message.reply_text('🔓 차량 잠금 해제에 성공했습니다.')
        else:
            update.message.reply_text('차량 잠금 해제에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('차량 잠금 해제에 실패했습니다. API 요청을 확인하세요.')

@restricted
def enable_guest(update: Update, context):
    result = send_vehicle_command('enable_guest')
    if result:
        if result.get('result'):
            update.message.reply_text('👥 Guest 모드가 활성화되었습니다.')
        else:
            update.message.reply_text('Guest 모드 활성화에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('Guest 모드 활성화에 실패했습니다. API 요청을 확인하세요.')

@restricted
def disable_guest(update: Update, context):
    result = send_vehicle_command('disable_guest')
    if result:
        if result.get('result'):
            update.message.reply_text('👤 Guest 모드가 비활성화되었습니다.')
        else:
            update.message.reply_text('Guest 모드 비활성화에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('Guest 모드 비활성화에 실패했습니다. API 요청을 확인하세요.')

@restricted
def enable_valet(update: Update, context):
    result = send_vehicle_command('enable_valet')
    if result:
        if result.get('result'):
            update.message.reply_text('🚗 발렛 모드가 활성화되었습니다.')
        else:
            update.message.reply_text('발렛 모드 활성화에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('발렛 모드 활성화에 실패했습니다. API 요청을 확인하세요.')

@restricted
def disable_valet(update: Update, context):
    result = send_vehicle_command('disable_valet')
    if result:
        if result.get('result'):
            update.message.reply_text('🚗 발렛 모드가 비활성화되었습니다.')
        else:
            update.message.reply_text('발렛 모드 비활성화에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('발렛 모드 비활성화에 실패했습니다. API 요청을 확인하세요.')

@restricted
def start_defrost(update: Update, context):
    params = {
        'retry_duration': 40,         # 기본값
        'wait_for_completion': 'true' # 기본값
    }
    result = send_vehicle_command('start_max_defrost', params=params)
    if result:
        if result.get('result'):
            update.message.reply_text('❄️ 차량의 서리 제거가 시작되었습니다.')
        else:
            update.message.reply_text('서리 제거 시작에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('서리 제거 시작에 실패했습니다. API 요청을 확인하세요.')

@restricted
def stop_defrost(update: Update, context):
    params = {
        'retry_duration': 40,         # 기본값
        'wait_for_completion': 'true' # 기본값
    }
    result = send_vehicle_command('stop_max_defrost', params=params)
    if result:
        if result.get('result'):
            update.message.reply_text('❄️ 차량의 서리 제거가 중지되었습니다.')
        else:
            update.message.reply_text('서리 제거 중지에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('서리 제거 중지에 실패했습니다. API 요청을 확인하세요.')

@restricted
def close_windows(update: Update, context):
    params = {
        'retry_duration': 40,         # 기본값
        'wait_for_completion': 'true' # 기본값
    }
    result = send_vehicle_command('close_windows', params=params)
    if result:
        if result.get('result'):
            update.message.reply_text('🪟 모든 창문이 닫혔습니다.')
        else:
            update.message.reply_text('창문 닫기에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('창문 닫기에 실패했습니다. API 요청을 확인하세요.')

@restricted
def open_charge_port(update: Update, context):
    params = {
        'retry_duration': 40,         # 기본값
        'wait_for_completion': 'true' # 기본값
    }
    result = send_vehicle_command('open_charge_port', params=params)
    if result:
        if result.get('result'):
            update.message.reply_text('🔌 충전 포트가 열렸습니다.')
        else:
            update.message.reply_text('충전 포트 열기에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('충전 포트 열기에 실패했습니다. API 요청을 확인하세요.')

@restricted
def close_charge_port(update: Update, context):
    params = {
        'retry_duration': 40,         # 기본값
        'wait_for_completion': 'true' # 기본값
    }
    result = send_vehicle_command('close_charge_port', params=params)
    if result:
        if result.get('result'):
            update.message.reply_text('🔒 충전 포트가 닫혔습니다.')
        else:
            update.message.reply_text('충전 포트 닫기에 실패했습니다.')
            if result.get('woke'):
                update.message.reply_text('차량이 잠들어 있습니다.')
    else:
        update.message.reply_text('충전 포트 닫기에 실패했습니다. API 요청을 확인하세요.')

@restricted
def get_drivers_command(update: Update, context):
    drivers = get_drivers()
    if drivers and 'results' in drivers:
        driver_list = ""
        for driver in drivers['results']:
            first_name = driver.get('driver_first_name', 'N/A')
            last_name = driver.get('driver_last_name', 'N/A')
            user_id = driver.get('user_id', 'N/A')
            driver_list += f"{first_name} {last_name} (ID: {user_id})\n"
        update.message.reply_text(f"🚘 등록된 드라이버 목록:\n{driver_list}")
    else:
        update.message.reply_text('드라이버 목록을 가져올 수 없습니다.')

@restricted
def get_invitations_command(update: Update, context):
    invitations = get_invitations()
    if invitations and 'results' in invitations:
        invitation_list = ""
        for invitation in invitations['results']:
            id = invitation.get('id', 'N/A')
            state = invitation.get('state', 'N/A')
            share_link = invitation.get('share_link', 'N/A')
            invitation_list += f"ID: {id}, 상태: {state}, 링크: {share_link}\n"
        update.message.reply_text(f"✉️ 초대 목록:\n{invitation_list}")
    else:
        update.message.reply_text('초대 목록을 가져올 수 없습니다.')

@restricted
def help_command(update: Update, context):
    help_text = """
사용 가능한 명령어:
/start - 봇 시작
/status - 차량 상태 조회
/lock - 차량 잠금
/unlock - 차량 잠금 해제
/start_defrost - 서리 제거 시작
/stop_defrost - 서리 제거 중지
/close_windows - 모든 창문 닫기
/open_charge_port - 충전 포트 열기
/close_charge_port - 충전 포트 닫기
/enable_guest - Guest 모드 활성화
/disable_guest - Guest 모드 비활성화
/enable_valet - 발렛 모드 활성화
/disable_valet - 발렛 모드 비활성화
/get_drivers - 드라이버 목록 조회
/get_invitations - 초대 목록 조회
    """
    update.message.reply_text(help_text)

def main():
    updater = Updater(TELEGRAM_TOKEN, use_context=True)
    dispatcher = updater.dispatcher

    # 명령어 핸들러 등록
    dispatcher.add_handler(CommandHandler('start', start))
    dispatcher.add_handler(CommandHandler('help', help_command))
    dispatcher.add_handler(CommandHandler('status', status))
    dispatcher.add_handler(CommandHandler('lock', lock))
    dispatcher.add_handler(CommandHandler('unlock', unlock))
    dispatcher.add_handler(CommandHandler('start_defrost', start_defrost))
    dispatcher.add_handler(CommandHandler('stop_defrost', stop_defrost))
    dispatcher.add_handler(CommandHandler('close_windows', close_windows))
    dispatcher.add_handler(CommandHandler('open_charge_port', open_charge_port))
    dispatcher.add_handler(CommandHandler('close_charge_port', close_charge_port))
    dispatcher.add_handler(CommandHandler('enable_guest', enable_guest))
    dispatcher.add_handler(CommandHandler('disable_guest', disable_guest))
    dispatcher.add_handler(CommandHandler('enable_valet', enable_valet))
    dispatcher.add_handler(CommandHandler('disable_valet', disable_valet))
    dispatcher.add_handler(CommandHandler('get_drivers', get_drivers_command))
    dispatcher.add_handler(CommandHandler('get_invitations', get_invitations_command))

    # 봇 시작
    updater.start_polling()
    updater.idle()

if __name__ == '__main__':
    main()

2. Procfile 작성하기

  • 프로젝트 디렉토리에 Procfile이라는 이름의 파일을 만듭니다.
  • 아래 내용을 Procfile에 작성합니다:
worker: python bot.py
  1. .env 파일 작성하기
  • 프로젝트 디렉토리에 .env 파일을 만듭니다.
  • .env 파일에 다음 내용을 추가합니다:
TELEGRAM_TOKEN=여기에_텔레그램_봇_토큰을_입력하세요
TESSIE_API_KEY=여기에_테슬라_API_키를_입력하세요
VEHICLE_VIN=여기에_차량의_VIN_번호를_입력하세요
AUTHORIZED_USERS=사용자ID1,사용자ID2
  • 각 항목을 실제 값으로 교체하세요:
  • TELEGRAM_TOKEN: BotFather에서 받은 토큰
  • TESSIE_API_KEY: Tessie에서 발급받은 API 키
  • VEHICLE_VIN: 제어할 차량의 VIN 번호
  • AUTHORIZED_USERS: 봇을 사용할 수 있는 Telegram 사용자 ID 목록 (쉼표로 구분)
주의: .env 파일은 중요한 정보가 포함되어 있으므로 절대 공유하지 마세요.

6단계: Heroku에 배포하기

Git 초기화 및 커밋하기

  • 프로젝트 디렉토리에서 Git을 초기화하고 파일을 커밋합니다:
$ git init
$ git add .
$ git commit -m "Initial commit"

Heroku 앱 생성하기

  • 터미널에서 다음 명령어를 입력하여 Heroku 앱을 생성합니다:
$ heroku create
  • 성공적으로 생성되면 앱의 URL과 Git 리포지토리 URL이 표시됩니다.

Heroku에 배포하기

  • 다음 명령어로 코드를 Heroku에 푸시합니다:
$ git push heroku master

또는

$ git push heroku main
  • 배포가 완료되면 Heroku에서 봇이 실행됩니다.

환경 변수 설정하기

  • Heroku 대시보드에서 생성한 앱을 선택하고, "Settings" 탭으로 이동합니다.
  • "Config Vars" 섹션에서 "Reveal Config Vars" 버튼을 클릭합니다.
  • .env 파일에 작성한 변수들을 Heroku에 추가합니다:
  • TELEGRAM_TOKEN: 텔레그램 봇 토큰
  • TESSIE_API_KEY: 테슬라 API 키
  • VEHICLE_VIN: 차량의 VIN 번호
  • AUTHORIZED_USERS: Telegram 사용자 ID 목록
Tip: 터미널에서 Heroku CLI를 사용하여 환경 변수를 설정할 수도 있습니다:
$ heroku config:set TELEGRAM_TOKEN=여기에_텔레그램_봇_토큰을_입력하세요
$ heroku config:set TESSIE_API_KEY=여기에_테슬라_API_키를_입력하세요
$ heroku config:set VEHICLE_VIN=여기에_차량의_VIN_번호를_입력하세요
$ heroku config:set AUTHORIZED_USERS=사용자ID1,사용자ID2

Dyno 시작하기

  • Heroku에서 Dyno가 실행 중인지 확인하고, 실행되지 않았다면 시작합니다:
$ heroku ps:scale worker=1

7단계: Telegram 봇 테스트하기

Telegram에서 봇과 대화하기

  • Telegram 앱에서 생성한 봇을 검색하고 대화를 시작합니다.
  • /start 명령어를 입력하여 봇이 제대로 작동하는지 확인합니다.

명령어 테스트하기

  • /status: 차량의 현재 상태를 조회합니다.
  • /lock: 차량을 잠급니다.
  • /unlock: 차량의 잠금을 해제합니다.
  • /start_defrost: 서리 제거를 시작합니다.
  • /stop_defrost: 서리 제거를 중지합니다.
  • /close_windows: 모든 창문을 닫습니다.
  • /open_charge_port: 충전 포트를 엽니다.
  • /close_charge_port: 충전 포트를 닫습니다.
  • /enable_guest: Guest 모드를 활성화합니다.
  • /disable_guest: Guest 모드를 비활성화합니다.
  • /enable_valet: 발렛 모드를 활성화합니다.
  • /disable_valet: 발렛 모드를 비활성화합니다.
  • /get_drivers: 등록된 드라이버 목록을 조회합니다.
  • /get_invitations: 초대 목록을 조회합니다.각 명령어를 입력한 후, 봇이 올바른 응답을 보내는지 확인하세요.

8단계: 에러 처리 및 추가 정보 표시

봇을 사용하면서 발생할 수 있는 다양한 에러 상황을 대비하여 코드를 강화했습니다. 예를 들어, 명령어 실행 실패 시 사용자에게 상세한 정보를 제공합니다.

또한, /status 명령어는 차량의 다양한 정보를 상세하게 표시합니다. 예를 들어, 배터리 수준, 충전 상태, 실내/실외 온도, 차량 위치 등을 한눈에 확인할 수 있습니다.

에러 처리 강화

명령어 실행 중 문제가 발생했을 때, 사용자에게 명확한 피드백을 제공하기 위해 코드를 수정했습니다. 예를 들어, 차량이 잠들어 있을 경우 사용자에게 이를 알리는 메시지를 추가했습니다.

def send_vehicle_command(command, params=None):
    url = f"https://api.tessie.com/{VEHICLE_VIN}/command/{command}"
    try:
        response = requests.post(url, headers=TESSIE_HEADERS, params=params)
        if response.status_code == 200:
            data = response.json()
            if 'result' in data:
                return data
            else:
                logger.error(f"Unexpected response structure: {data}")
                return None
        else:
            logger.error(f"Failed to execute command '{command}': {response.text}")
            return None
    except requests.RequestException as e:
        logger.error(f"Request exception: {e}")
        return None

추가 정보 표시

/status 명령어에서 배터리 충전 전압과 전력 소비 등의 추가 정보를 표시하도록 코드를 확장했습니다.

# 상태 메시지 작성
status_text = f"""
🚗 차량 이름: {display_name}
🔢 VIN: {vin}
🔖 번호판: {plate}
📡 차량 상태: {state}

🔋 배터리 수준: {battery_level}%
🔌 배터리 주행 가능 거리: {battery_range}
⚡️ 충전 상태: {charging_state}
⚡️ 충전 속도: {charge_rate}
⏳ 완충까지 시간: {time_to_full_charge}

🌡 실내 온도: {inside_temp}
🌡 실외 온도: {outside_temp}
❄️ 에어컨 상태: {'켜짐' if is_climate_on else '꺼짐'}

🚙 속도: {speed}
🔋 전력 소비: {power}
🔌 충전 전압: {charger_voltage}
⚡️ 충전 전력: {charger_power}
📍 위치: {location_text}
🧭 방향: {heading}

🔒 잠금 상태: {'잠김' if locked else '열림'}
🛣 주행 거리계: {odometer}
🎥 센트리 모드: {'활성화' if sentry_mode else '비활성화'}

🚘 차량 종류: {car_type}
🎨 외장 색상: {exterior_color}
🛞 휠 종류: {wheel_type}
"""

9단계: 유지 관리 및 보안

Heroku Dyno 관리하기

  • 무료 Heroku 플랜은 Dyno가 30분 동안 활동이 없으면 슬립 모드로 전환됩니다. 항상 봇을 실행 상태로 유지하려면 유료 플랜을 고려해보세요.
  • Dyno 상태를 확인하려면 터미널에서 다음 명령어를 사용하세요:
$ heroku ps

환경 변수 보호하기

  • .env 파일에 민감한 정보를 저장했지만, 이를 Git에 커밋하지 않도록 .gitignore 파일에 추가하세요.
venv/
.env

봇 보안 강화하기

  • AUTHORIZED_USERS를 통해 봇을 사용할 수 있는 Telegram 사용자 ID를 제한했습니다. 이를 통해 불특정 다수가 봇을 사용할 수 없도록 했습니다.
  • 필요에 따라 추가적인 보안 조치를 고려하세요.