У меня есть самописный движок автоматизации для IoT на PHP + MySQL + MQTT. Он умеет запускать действия по расписанию — например, включать фильтр в аквариуме в 8:00 и выключать в 23:00. У кого аквариум стоит в спальне, тот меня поймет, как бесит этот фильтр булькать ночью )
Раньше логика была двух типов, чать правил с проверкой на выполнение, к примеру по дням недели, и простые правила: каждые 10 секунд скрипт проверял, совпадает ли текущее время в минутах (H:i) со временем в правиле. Но потом я купил iot кормушку для рыбок, а там надо включать на 3-5 секунд. Пришлось добавить поддержку секунд — стал сравнивать H:i:s — и стал создавать правила вроде:
08:00:00→ включить реле23:00:00→ выключить реле
Вроде всё логично. Но однажды ночью фильтр не выключился. Почему?
Проблема: точное совпадение по секунде — ненадёжно
Мой цикл работает с sleep(5) (а потом я уменьшил до sleep(1)). Но даже при проверке раз в секунду:
- Скрипт может стартовать в середине секунды (например, 22:59:59.7)
- Запросы к БД, перезагрузка правил и другие операции могут занять >1 секунду
- В итоге проверка в 23:00:00 пропускается — скрипт «проскакивает» её и попадает только в 23:00:02
А так как условие было строгое — if (текущее_время === правило.время) — правило не срабатывало. Никогда.
Решение: «время настало или прошло» + защита от повторов
Я переделал логику. Теперь вместо точного совпадения используется условие:
«Если текущее время больше или равно заданному, и правило ещё не выполнялось сегодня — запустить его один раз».
Для этого я использую уже существующее поле в таблице automations — last_triggered (тип TIMESTAMP).
Алгоритм:
- Получаю текущее время в формате
H:i:sи датуY-m-d. - Сравниваю:
если (текущее_время >= правило.время) - И проверяю:
если дата(last_triggered) != сегодня - Если оба условия верны — выполняю действие и обновляю
last_triggered = NOW()
Теперь даже если скрипт проверит правило в 23:00:03 — условие '23:00:03' >= '23:00:00' истинно, и фильтр выключится. А повторного запуска не будет, потому что last_triggered уже обновлён на сегодняшнюю дату.
Плюсы такого подхода
- ✅ Гарантированное выполнение, даже при задержках
- ✅ Нет дублирования — только один запуск в сутки
- ✅ Не требует изменения структуры БД (используется уже существующее поле)
- ✅ Работает с любой частотой проверки (даже с
sleep(5))
Что делать, если у вас похожая система?
Если вы пишете свой IoT-планировщик на PHP (или любом другом языке) — никогда не полагайтесь на точное совпадение по секунде. Всегда используйте «окно срабатывания» и механизм защиты от повторов.
Это особенно критично для задач, которые должны выполниться один раз в сутки (выключение насосов, полив, обогрев и т.д.).
Вариант 1: использовать поле last_triggered (без изменения БД)
В таблице automations уже есть колонка:
last_triggered TIMESTAMP NULL COMMENT 'Когда последний раз сработало'
Её достаточно для отслеживания выполнения за день:
$lastDate = $rule['last_triggered']
? date('Y-m-d', strtotime($rule['last_triggered']))
: null;
$today = date('Y-m-d');
if ($currentTime >= $ruleTime && $lastDate !== $today) {
handleAction($rule);
// last_triggered обновится автоматически в handleAction()
}
Плюсы: не требует ALTER TABLE, использует уже существующую логику обновления времени срабатывания.
Минусы: если last_triggered используется где-то ещё (например, для статистики), возможна коллизия по смыслу — но в вашем случае это маловероятно.
Вариант 2: добавить отдельное поле last_triggered_date
Можно создать специальную колонку только для даты:
ALTER TABLE automations ADD COLUMN last_triggered_date DATE NULL
COMMENT 'Дата последнего срабатывания (для daily-правил)';
Тогда проверка становится проще и семантически чище:
$today = date('Y-m-d');
if ($currentTime >= $ruleTime &&
($rule['last_triggered_date'] !== $today || is_null($rule['last_triggered_date']))) {
handleAction($rule);
$pdo->prepare("UPDATE automations SET last_triggered_date = ? WHERE id = ?")
->execute([$today, $rule['id']]);
}
Плюсы: чёткое разделение ответственности — last_triggered для точного времени, last_triggered_date для защиты от повторов.
Минусы: нужно изменять структуру БД, что может быть нежелательно в продакшене без миграций.
Что дальше?
Позже я планирую добавить поддержку временных зон и более сложных расписаний (например, «каждые 30 минут с 8 до 22»), но базовая логика останется той же: «настало или прошло» + «ещё не делали сегодня».
Надёжность важнее эстетики точного времени.
Комментарии
Пока нет комментариев. Будьте первым!