Расписание и таймеры в умном доме

Настоящий умный дом живёт по часам: свет в аквариуме включается в 8 утра, полив — вечером, а уведомление о двери — только днём.

Датчики реагируют на события "сейчас". Расписание добавляет измерение времени — действия, привязанные к часам и минутам.

До сих пор наши проекты реагировали на датчики. Теперь добавим время: действия по расписанию. На Pi для этого есть два пути — таймеры внутри Python и системный планировщик cron, который запускает скрипты по часам даже без работающей программы.

Представь будильник на телефоне. Ты ставишь его на 7:00, выключаешь экран, кладёшь телефон на тумбочку — и утром он звонит сам, хотя ты ничего не запускал. cron работает ровно так же, только вместо звонка он выполняет твою команду. Это очень удобно: тебе не нужно держать программу включённой круглые сутки, чтобы дождаться нужного часа. Система сама помнит расписание и в нужную минуту дёргает скрипт. Для подростка это как иметь умного помощника, который никогда не забывает: полить цветы в шесть вечера, выключить подсветку в полночь, проверить почтовый ящик каждые пятнадцать минут.

Чем расписание отличается от обычной реакции на датчик? Датчик — это "если случилось — сделай". Расписание — это "когда наступит такое-то время — сделай". Эти два подхода прекрасно дополняют друг друга: днём свет включает датчик движения, а вечером по расписанию загорается мягкая подсветка, даже если в комнате никого нет.

Как работает под капотом

cron — это служба Linux, которая по таблице расписаний запускает команды. Строка в crontab задаёт минуту, час, день и команду:

# открыть таблицу расписаний
crontab -e

# запускать скрипт каждый день в 8:00
0 8 * * * python3 /home/codechick/lights_on.py

# каждые 15 минут
*/15 * * * * python3 /home/codechick/check.py

Каждая строка cron состоит из пяти полей времени и команды. Порядок полей легко запомнить по схеме слева направо: минута, час, день месяца, месяц, день недели. Звёздочка означает "любое значение", то есть "каждый". Запись */15 читается как "каждые 15" — каждые пятнадцать минут. Вот как это выглядит наглядно:

   *    *    *    *    *   команда
   |    |    |    |    |
   |    |    |    |    +-- день недели (0-6)
   |    |    |    +------- месяц (1-12)
   |    |    +------------ день месяца (1-31)
   |    +----------------- час (0-23)
   +---------------------- минута (0-59)

Сама служба cron просыпается раз в минуту, сверяет текущее время со всеми строками таблицы и запускает те команды, у которых поля совпали. Важно понимать: cron не "следит" за твоей программой и ничего не помнит между запусками — он просто стреляет командой в нужный момент и забывает о ней. Если действие должно зависеть от состояния (например "полей, только если земля сухая"), эту проверку делает уже сам скрипт, который cron запустил.

А логику расписания внутри программы отладим на чистом Python. Проверим для набора правил, должно ли действие сработать в заданный час. Попробуй сам ▶

# Расписание умного дома: что включать в какой час
schedule = {
    8:  "включить свет в аквариуме",
    19: "включить полив",
    22: "выключить свет в аквариуме",
}

def at_hour(hour):
    # вернуть действие для текущего часа, если оно есть
    return schedule.get(hour)

# имитируем сутки
for hour in range(24):
    action = at_hour(hour)
    if action:
        print(f"{hour:02d}:00 -> {action}")

print("Проверка 12:00:", at_hour(12) or "ничего не запланировано")

Мы храним расписание в словаре "час — действие". Это легко расширять и читать. На реальном Pi такой словарь проверялся бы раз в минуту или через cron.

Усложним модель: добавим интервальные задания — действия, которые повторяются каждые N минут, а не в фиксированный час. Это та же идея, что */15 в cron, только на чистом Python. Попробуй сам ▶

# Интервальные задачи: запускать раз в N минут
tasks = [
    {"name": "проверить датчик двери", "every": 5},
    {"name": "записать температуру",   "every": 15},
    {"name": "сделать резервную копию", "every": 60},
]

# имитируем 60 минут работы
for minute in range(0, 61):
    for task in tasks:
        if minute % task["every"] == 0 and minute > 0:
            print(f"{minute:02d} мин -> {task['name']}")

Здесь оператор остатка от деления % работает как фильтр: если минута делится на интервал без остатка, значит время сработать. Именно так планировщики решают, наступил ли нужный момент, не храня длинных списков точных времён.

Частые ошибки

  • Скрипт по cron не находит файлы. cron запускает с другим окружением — используй абсолютные пути.
  • Неверный часовой пояс. Проверь, что на Pi выставлено правильное время (timedatectl).
  • Нет логов. Перенаправляй вывод cron-задачи в файл, иначе не поймёшь, сработала ли она.
  • Перепутаны поля времени. Очень легко поставить час на место минут и получить запуск раз в час вместо нужного времени — сверяйся со схемой полей.
  • Задача висит дольше минуты. Если скрипт работает долго, cron может запустить второй экземпляр поверх первого — следи за длительностью или ставь блокировку.

Best practices

  • Для разовых действий по часам используй cron, для сложной логики — программу-демон.
  • Храни расписание данными (словарь/список), а не россыпью if.
  • Всегда указывай абсолютные пути в cron-задачах и логируй результат.
  • Тестируй команду вручную в терминале до того, как ставить её в crontab.
  • Добавляй комментарии в crontab прямо над строкой — через полгода ты забудешь, зачем нужна та или иная задача.

Итоги. Расписание добавляет проектам измерение времени. cron запускает скрипты по часам на уровне системы, читая пять полей времени и просыпаясь раз в минуту, а внутри программы логику расписания удобно хранить словарём и отлаживать на чистом Python — как точечные действия по часам, так и интервальные задачи через остаток от деления. Дальше превратим Pi в сетевой сервер.

Проверьте себя
1. Что такое cron в Linux?
AТекстовый редактор
BСлужба, которая по расписанию (минута, час, день) автоматически запускает команды
CДатчик времени
DБиблиотека Python
2. Почему в cron-задачах важно использовать абсолютные пути к файлам?
AТак короче
Bcron запускает команды с другим окружением, и относительные пути могут не найтись
CЭто требование Python
DАбсолютные пути работают быстрее