הודעות דרך Telegram
שרת הלוח מפרק 14 נראה רק מתוך רשת הבית. אז איך נדע שהדלת נפתחה כשאנחנו בחוץ? הפתרון הוא להניח ללוח לדבר עם שרת ציבורי שפועל תמיד. בפרק הזה נבנה בוט ב-Telegram: הלוח שולח הודעה לטלפון שלנו, מבצע את הפקודות שנכתוב לו, ושומר על הטוקן שפותח את כל זה.
- ניצור בוט אצל BotFather ונבין מהם הטוקן ומזהה השיחה.
- נשמור את הטוקן ב-
secrets.pyונדע מה עושים אם הוא דלף. - נשלח הודעה בפונקציה
requests.postובפקודהsendMessage. - נקבל פקודות בעזרת
getUpdatesונבין את תפקידoffset. - נבנה בוט שמחזיר קריאה של חיישן ומפעיל נורית, ומציית רק לבעליו.
- נחשב את מספר הבקשות ונבחר זמן תשאול סביר.
18.1 בוט: חשבון שהלוח מפעיל (Bots and BotFather)
בוט הוא חשבון ב-Telegram שתוכנה כותבת בו במקום אדם. מכיוון שהלוח הוא שיוזם כל חיבור, אין צורך בכתובת ציבורית ואין צורך לשנות דבר בנתב: הבקשה היוצאת עוברת כמו בקשה שיוצאת מן הטלפון.
יוצרים את הבוט מן הטלפון, בלי שום תכנות:
- פותחים ב-Telegram שיחה עם @BotFather, הבוט הרשמי לניהול בוטים.
- שולחים את הפקודה
/newbot. הוא שואל אותנו מהו שם התצוגה, ואחר כך מהו שם המשתמש, שחייב להסתיים ב-bot. - הוא משיב לנו עם הטוקן. מעתיקים אותו מיד אל
secrets.pyולא מדביקים אותו בשום מקום אחר. - פותחים את השיחה עם הבוט החדש, לוחצים Start ושולחים לו הודעה כלשהי.
chat_id. מגלים אותו מן ההודעה ששלחנו אל הבוט, כפי שנראה בסעיף 18.4.123:TEST ומזהה השיחה 555. גם השרת מדומה: התשובות כתובות בעולם הסימולציה, ובזמן בניית הספר לא נשלחה אף בקשה אל Telegram.18.2 הטוקן הוא סיסמה
מי שמחזיק בטוקן מחזיק בבוט: הוא קורא את ההודעות שמגיעות אליו, שולח בשמו ומפקד על הלוח שלנו כרצונו. אין שם משתמש ואין שאלה נוספת, והטוקן לבדו מספיק. לכן מתייחסים אליו כמו אל סיסמת ה-WiFi: מקומו היחיד הוא שורה ב-secrets.py.
secrets.py בלוח, עם ערכים מלאכותיים. הקובץ הזה אינו רץ לבדו, ולכן לא הורץ בזמן בניית הספר.WIFI_SSID = "my-network" WIFI_PASS = "my-password" TELEGRAM_TOKEN = "123:TEST"
- התוכנית מייבאת את השם בלבד:
from secrets import TELEGRAM_TOKEN. כך אפשר לשתף את כל התוכנית בלי לשתף את הטוקן. - הקובץ
secrets.pyאינו עולה ל-GitHub, אינו עובר לתיקייה משותפת ואינו מופיע בצילום מסך שנשלח לחבר. - הטוקן הוא חלק מכתובת הבקשה. אם נדפיס את הכתובת לבדיקה, הטוקן ייכתב ביומן. לכן לעולם לא מדפיסים את
API.
מה עושים אם הטוקן דלף? לפי הסדר הזה:
- מבטלים את הטוקן מיד אצל BotFather: שולחים את הפקודה
/tokenובוחרים את הבוט, והוא נותן לנו טוקן חדש. התיעוד של Telegram מזכיר את הפקודה הזאת למקרה של טוקן שדלף, ובתפריט הפקודות של BotFather מופיעה גם/revoke. - מציבים את הטוקן החדש ב-
secrets.pyבכל לוח שמשתמש בו. - בודקים שהטוקן הישן כבר אינו עובד: בקשה שמשתמשת בו צריכה להידחות.
- מחפשים את מקור הדליפה וסוגרים אותו. ואם הטוקן נכנס למאגר קוד, הוא נשאר בהיסטוריה שלו גם אחרי מחיקת השורה.
18.3 שליחת הודעה (sendMessage)
כל פקודות הבוט הן בקשות HTTPS לכתובת אחת בצורה קבועה: https://api.telegram.org/bot<TOKEN>/METHOD. התשובה היא טקסט JSON ובו השדה ok, ועמו result בהצלחה או description בכישלון.
import time
import network
import requests
from secrets import WIFI_SSID, WIFI_PASS, TELEGRAM_TOKEN
CHAT = 555
API = "https://api.telegram.org/bot" + TELEGRAM_TOKEN
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.disconnect()
wlan.connect(WIFI_SSID, WIFI_PASS)
while not wlan.isconnected():
time.sleep(0.5)
r = requests.post(API + "/sendMessage",
json={"chat_id": CHAT, "text": "board is up"})
reply = r.json()
if reply["ok"]:
print(r.status_code, reply["ok"], reply["result"]["message_id"])
else:
print(r.status_code, reply["description"])
r.close()200 True 7
[http_sent]
POST https://api.telegram.org/bot123:TEST/sendMessage {'text': 'board is up', 'chat_id': 555}את הנתונים שולחים בפרמטר json=, והספרייה דואגת להמיר אותם לטקסט JSON, כך שאין צורך לקודד רווחים בכתובת. חשוב לא לשכוח את r.close(): בלעדיו החיבור נשאר פתוח והזיכרון נשאר תפוס.
עכשיו נראה מה קורה אחרי ביטול הטוקן. השרת משיב בקוד 401, ומכאן התוכנית יודעת שהטוקן אינו תקף:
from secrets import TELEGRAM_TOKEN
from tgbot import Bot
bot = Bot(TELEGRAM_TOKEN)
code = bot.send(555, "still working?")
print("status", code)
if code == 401:
print("token rejected: put the new one in secrets.py")status 401 token rejected: put the new one in secrets.py
requests במיקרו-פייתון אינה בודקת את תעודת השרת בהגדרות ברירת המחדל, ולכן אין ערובה שאנחנו מדברים באמת עם Telegram. זה מקובל בניסוי לימודי, אבל לא במערכת קריטית.tg_check_token.py אילו הטוקן היה תקף והשרת היה משיב בקוד 200?18.4 קבלת פקודות (getUpdates)
השרת אינו יכול ליזום חיבור ללוח, ולכן הוא שומר כל הודעה עד שישאלו עליה. כל הודעה שמגיעה היא עדכון שיש לו מספר update_id, והשאלה החוזרת נקראת תשאול. נכתוב את התוכנית הפשוטה ביותר:
import time
import requests
from secrets import TELEGRAM_TOKEN
API = "https://api.telegram.org/bot" + TELEGRAM_TOKEN
for i in range(3):
r = requests.get(API + "/getUpdates")
data = r.json()
r.close()
if not data["ok"]:
print(data["description"])
break
for u in data["result"]:
print("poll", i, u["update_id"], u["message"]["text"])
time.sleep(5)poll 0 100 /on poll 1 100 /on poll 2 100 /on
הסיבה היא שהשרת אינו יודע שטיפלנו בהודעה. מודיעים לו על כך בפרמטר offset:
| סמל | פירוש | יחידה |
|---|---|---|
| updateid | מספר העדכון האחרון שטיפלנו בו | — |
| offset | המספר הקטן ביותר שנרצה לראות בבקשה הבאה | — |
import time
import requests
from secrets import TELEGRAM_TOKEN
API = "https://api.telegram.org/bot" + TELEGRAM_TOKEN
offset = 0
for i in range(3):
r = requests.get("%s/getUpdates?offset=%d" % (API, offset))
data = r.json()
r.close()
if not data["ok"]:
print(data["description"])
break
print("poll", i, "new:", len(data["result"]))
for u in data["result"]:
offset = u["update_id"] + 1
m = u["message"]
print(" ", m["chat"]["id"], m["text"])
time.sleep(5)poll 0 new: 2 555 /on 555 /light poll 1 new: 1 555 /off poll 2 new: 0
getUpdates עם offset גדול ממספרו, ועדכון שלא אושר אינו נשמר בשרת יותר מ-24 שעות. לכן אם הלוח היה מנותק יום וחצי, הפקודות של היום הראשון אבדו.offset בבקשה הבאה? ומה יקרה אם נשלח 240?18.5 בוט שמחזיר קריאה של חיישן ומפעיל נורית
נאגד את הבקשות בספרייה קטנה, כדי שהתוכנית הראשית תישאר ברורה. הספרייה מקבלת את הטוקן מן התוכנית ואינה יודעת מהו ערכו:
tgbot.py.# tgbot.py - a small Telegram bot client over HTTPS.
# The token comes from secrets.py. Never print self.api: it holds the token.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import time
import requests
class Bot:
def __init__(self, token):
self.api = "https://api.telegram.org/bot" + token
self.offset = 0
def send(self, chat, text):
r = requests.post(self.api + "/sendMessage",
json={"chat_id": chat, "text": text})
code = r.status_code
r.close()
return code
def updates(self):
url = "%s/getUpdates?offset=%d" % (self.api, self.offset)
r = requests.get(url)
code = r.status_code
data = r.json()
r.close()
if code != 200:
raise OSError(code)
found = []
for u in data["result"]:
self.offset = u["update_id"] + 1
m = u.get("message")
if m and "text" in m:
found.append((m["chat"]["id"], m["text"]))
return foundimport time
from machine import Pin
import board
from convert import to_percent
from secrets import TELEGRAM_TOKEN
from tgbot import Bot
import wifi
MY_CHAT = 555
POLL_S = 10
led = Pin(board.LED, Pin.OUT)
ldr = board.analog_in(board.LDR)
bot = Bot(TELEGRAM_TOKEN)
def answer(cmd):
if cmd == "/light":
return "light %d %%" % to_percent(ldr.read_u16())
if cmd == "/on":
led.on()
return "LED on"
if cmd == "/off":
led.off()
return "LED off"
return "use /light /on /off"
wifi.connect()
while True:
try:
for chat, text in bot.updates():
if chat != MY_CHAT:
print("ignored", chat, text)
continue
reply = answer(text)
print(text, "->", reply, "| led", led.value())
bot.send(chat, reply)
except OSError as e:
print("network error", e)
time.sleep(POLL_S)/light -> light 40 % | led 0 /on -> LED on | led 1 ignored 999 /off /light -> light 70 % | led 1 hello -> use /light /on /off | led 1 network error -202 network error -202 ...
| פקודה | מה היא עושה |
|---|---|
| bot.updates() | מחזירה רשימה של זוגות (מזהה שיחה, טקסט), ומעדכנת את offset בעצמה. |
| chat != MY_CHAT | מתעלמים מכל מי שאינו בעל הבוט. |
| except OSError | ניתוק רשת הוא אירוע רגיל: מדווחים עליו ומנסים שוב בסבב הבא. |
chat זר יכול להפעיל כל מה שמחובר ללוח. וגם אין לשלוח בתשובות לפקודות שום פרט שאיננו רוצים שיקרא מי שמצא את הבוט.chat?18.6 כל כמה זמן שואלים? (Polling Rate)
כל בקשת HTTPS בספרייה requests פותחת חיבור חדש, עם לחיצת יד של הצפנה שצורכת זמן וזיכרון, ובזמן הזה התוכנית אינה עושה דבר אחר. לכן לא שואלים בכל שנייה.
| סמל | פירוש | יחידה |
|---|---|---|
| N | מספר הבקשות ביום | — |
| T | הזמן בין שתי בקשות | s |
המקרה הגרוע: הפקודה הגיעה מיד אחרי בקשה, ולכן היא ממתינה מחזור שלם, כלומר 10 s.
| זמן התשאול | בקשות ביום | ההמתנה הארוכה ביותר לפקודה |
|---|---|---|
| 1 s | 86400 | 1 s |
| 5 s | 17280 | 5 s |
| 10 s | 8640 | 10 s |
| 60 s | 1440 | 60 s |
- לפקודה שאדם מחכה לה מספיקות 5 עד 10 שניות. ולהתראה שמגיעה מן הלוח אין צורך בתשאול כלל: שולחים רק כשיש אירוע.
- לפי התיעוד של Telegram: לא שולחים יותר מהודעה אחת בשנייה לאותה שיחה. מי שחורג מכך בקביעות מקבל שגיאה עם הקוד 429.
- ל-
getUpdatesיש פרמטר בשםtimeoutשגורם לשרת להשאיר את הבקשה פתוחה עד שמגיעה הודעה. זה חוסך בקשות, אבל הלוח נשאר חסום לכל משך ההמתנה. בפרק 19 נראה למה זה חשוב לנו.
עיקרי הפרק:
- הלוח הוא שיוזם את החיבור, ולכן הבוט עובד מאחורי הנתב בלי שום הגדרה.
- הטוקן הוא סיסמה: מקומו ב-
secrets.py, ולא מדפיסים אותו ולא משתפים אותו. ואם הוא דלף, מבטלים אותו קודם כול אצל BotFather. - שליחה היא בקשת
sendMessage, וקבלה היא תשאול בפקודהgetUpdates. - בלי
offset= המספר האחרון + 1, אותה הודעה חוזרת בכל פעם. - הבוט בודק את מזהה השיחה לפני שהוא מבצע פקודה כלשהי.
- תשאול כל כמה שניות מספיק, וניתוק רשת מטפלים בו ואינו עוצר את התוכנית.
פקודות הפרק:
| פקודה | מה היא עושה |
|---|---|
| from secrets import TELEGRAM_TOKEN | הדרך היחידה שבה הטוקן מגיע אל התוכנית. |
| POST .../bot<TOKEN>/sendMessage | שליחת הודעה; השדות chat_id ו-text. |
| GET .../bot<TOKEN>/getUpdates?offset=n | העדכונים שמספרם n ומעלה. |
| /newbot | ב-BotFather: יצירת בוט. |
| /token | ב-BotFather: טוקן חדש במקום הטוקן שדלף. |
| Bot(token) | מחלקה מן הספרייה tgbot.py, ולה הפונקציות send ו-updates. |
מילון מונחים:
| עברית | English | ערבית |
|---|---|---|
| בוט | Bot | بوت |
| טוקן | Bot Token | رمز البوت |
| מזהה שיחה | Chat ID | معرّف المحادثة |
| עדכון | Update | تحديث |
| תשאול | Polling | الاستطلاع |
| ביטול טוקן | Token Revocation | إلغاء الرمز |
status 200 בלבד, כי התנאי אינו מתקיים.שאלות לתרגול:
secrets.py, והשורה from secrets import TELEGRAM_TOKEN.offset?secrets.py, אחר כך בדיקה שהישן נדחה, ואחר כך מחיקת הצילום.ALLOWED = (555, 777)
for chat, text in bot.updates():
if chat not in ALLOWED:
continue
bot.send(chat, answer(text))answer את הפקודה /status שמחזירה את מצב הנורית.if cmd == "/status":, ואחר כך מחזירים "LED %d" % led.value().import time
import board
from convert import to_percent
from secrets import TELEGRAM_TOKEN
from tgbot import Bot
MY_CHAT = 555
ldr = board.analog_in(board.LDR)
bot = Bot(TELEGRAM_TOKEN)
dark = False
while True:
level = to_percent(ldr.read_u16())
if level < 20 and not dark:
dark = True
bot.send(MY_CHAT, "it is dark: %d %%" % level)
elif level > 30:
dark = False
time.sleep(10)