Простой ответ: установить и регулярно соблюдать режим сна. Ответ простой, но, к сожалению, способ сложный. Казалось бы, просто поставь будильник. Но тут и кроется проблема, ради решения которой я и пошел на все эти костыли.

Я не могу встать по обычному будильнику. Я либо проснусь и сброшу его, либо не проснусь вовсе. Причем не важно, орущий телефон это или вибрирующие часы. Мне нужен будильник достаточно громкий и у которого нет таймера остановки или он сильно больше, чем пара минут стандартных будильников в телефонах. А так же, чтобы его нельзя было бы так просто сбросить или отложить.

Приложение

Первым, хоть как то рабочим решением, было приложение Alarmy (конечно же реклама). Оно позволяло телефону орать без остановки и что самое главное, выбрать способ отключения будильника. Из интересных вариантов: решение математических выражений и сканирование штрих кода. Выбрав последнее, по утрам мне было нужно взять iPad, почему не телефон скажу позже, дойти до ванной и отсканировать штрих с пачки спиртовых салфеток.

Так я жил несколько лет. Вариант рабочий, но не без минусов. Из самого банального, если выбросить упаковку со штрихом, но не переназначить на что-то другое, утром, штатно будильник не остановишь. Резервного сброса у приложения я не нашел. Если приложение зависнет, система выгрузит его из памяти или после перезагрузки устройства не запустив его повторно - будильник не сработает. В какой-то момент приложение на андроиде начало работать через раз. Возможно именно из-за выгрузки его из памяти. Если уйти, оставив планшет дома, но не отключив при этом будильник - он будет орать до тех пор, пока iPad не сядет. Возможно именно это ментально травмирует вашу кошку или другого зверя не умеющего пользоваться сенсорным экраном. А если взять с собой, но штрих останется дома… Думаю вы понимаете. В подобных случаях, для отключения звонка я выключал iPad целиком.

Еще один минус - постоянная работа в фоне. От этого аккумулятор садится так быстро, что ощущение будто будильник майнит крипту на твоем железе. Ну и конечно же, если устройство за ночь сядет и выключится, будильник не сработает. Для основного телефона, с уставшей батареей, данное приложение противопоказано. По этому я поставил его на iPad и заряжал его каждую ночь.

Умный дом

Решив избавиться от этого приложения, решено было реализовать похожую схему средствами умного дома.

У сервера УД нет акустики. К нему можно ее подключить, но он находится не в моей комнате, а громкость, напомню, важна. Другой вариант, у меня есть raspberry, и колонки основного PC с двумя аудио входами. С источником звука разобрались, далее остановка будильника. Берем zigbee кнопку и закидываем ее в ванную комнату. По ее нажатию останавливаем сигнал. Помимо этого, мой умный дом знает, находится ли кто-то дома, так что лишний раз будильник включать не будем. У меня даже есть информация спит ли сейчас кто-то. Так что можно сделать отключение будильника по покиданию кровати, и следить за тем, чтобы я туда не вернулся. Но этим пока что заморачиваться не будем.

MQTT спикер

В качестве связи с умным домом используем MQTT, скрипт напишем на языке Python, а запустим на одноплатнике Raspberry Pi.

Подготовка Raspberry Pi

Я обязываю вас использовать именно Raspberry Pi 4. Более того, данный одноплатный компьютер является оверкилом для данной задачи, и пременен мной только из-за того, что у меня небыло других подходящих устройств с аудио jack’ом.

Первым делом скачиваем прошивку. Для данной задачи будет достаточно лайт версии. Далее загружаем прошивку на карту памяти, для этого предлагаю воспользоваться BalenaEtcher.

Поскольку SSH по умолчанию не включен, необходимо один раз подключиться к малине с монитором и клавиатурой.

В новых сборках Raspberry Pi OS, при первом запуске запрашивают имя и пароль для нового пользователя. А ранее создавался стандартный пользователь с логином pi и паролем raspberry. В этом случае советую сменить пароль:

1
passwd

Залогинившись активируем SSH через raspi-config. Пункт меню: Interfacing OptionsSSH.

1
sudo raspi-config

Там же прописываем параметры WiFi, если не собираемся использовать подключение к сети по кабелю. System OptionsWireless LAN. И укажем часовой пояс, это понадобиться для корректной работы cron. Localisation OptionsTimezoneEurope → …

С настройкой raspi-config закончили, закрываем его. Но перед отключением монитора, можно подсмотреть ip малины, чтобы потом не копаться в настойках роутера.

1
ifconfig

Если есть ip отключаем лишнюю периферию, далее работать будем через ssh. Учтите что raspi-config при настройке WiFi не выдает ошибку, если указан неверный пароль. Проверьте что вы подключены, прежде чем прятать малину в дальний угол.

Подключаемся по ssh (стандартный порт 22) через терминал, Putty или Termius. Интерфейс последнего мне нравится больше всего.

Для обновления системы

1
sudo apt update; sudo apt upgrade -y

Подготовка среды

Для взаимодействия с протоколом MQTT будем использовать Python библиотеку Eclipse Paho.

Если нет pip:

1
sudo apt install python3-pip -y

Установка библиотеки:

1
pip install paho-mqtt

Для воспроизведения аудио воспользуемся консольным плеером mpg123:

1
sudo apt install mpg123 -y

Настроем громкость в системе:

1
sudo alsamixer

Скрипт

Теперь напишем небольшую программу на Python, для воспроизведения звуков.

Для запуска mpg123 нам понадобится subprocess.Popen().

1
2
3
import subprocess

proc = subprocess.Popen(['mpg123', '-q', './audio/alarm.mp3'])

Из полезного, можно дождаться завершения программы mpg123, выполнив метод wait, у вернувшегося объекта Popen.

1
proc.wait()

Однако в моем случае, наоборот нужно не ждать пока музыка доиграет, а слушать MQTT, ожидая команды сброса будильника. И если это произошло, завершить процесс немедленно.

1
proc.kill()

В случае если будильник не сброшен, а воспроизведение уже закончилась, запускаем его повторно.

1
2
if not type(proc.poll()) is None and proc.poll() == 0:
    proc = subprocess.Popen(['mpg123', '-q', './audio/alarm.mp3'])

Теперь разберемся как подключиться к MQTT. Создаем объект класса Client и установим логин, пароль, ip и порт от нашего MQTT сервера.

1
2
3
4
5
import paho.mqtt.client as mqtt

client = mqtt.Client()
client.username_pw_set('username', 'password')
client.connect('ip','port', 60)

После чего указываем функции, которые выполнятся при успешном подключении и получении сообщений.

1
2
client.on_connect = on_connect
client.on_message = on_message

Сами функции выглядят следующем образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def on_connect(client, userdata, flags, rc):
    # Подписываемся на всю ветку speaker/#
    client.subscribe("speaker/#")
    # Для выбора метода в зависимости от топика 
    # используется метод message_callback_add.
    # Все сообщения которые не указаны через этот
    # метод будут обрабатываться функцией on_message.
    client.message_callback_add("speaker/nodered/info", receiving_info)
    client.message_callback_add("speaker/nodered/button", receiving_info)

    # Успешно подключившись отправляем сообщение.
    # Тем самым запросив информацию у умного дома.
    client.publish("speaker/nodered/", payload='get_info', qos=0, retain=False)
    
    userdata['Connected'] = True

def on_message(client, userdata, msg_obj):
    # Выделяем текст сообщения
    message = str(msg_obj.payload)[2:-1]
    print(msg_obj.topic + " " + message)

По факту функция on_message у меня используется только на момент отладки, за обработку сообщений отвечает receiving_info.

1
2
3
def receiving_info(client, userdata, msg_obj):
    # Парсим json из сообщения и сохраняем его в userdata
    userdata.update(json.loads(str(msg_obj.payload)[2:-1]))

userdata - переменная передающаяся во все функции обработки сообщений. Она задается методом user_data_set(userdata). В через нее я сохраняю информацию от умного дома в словарь.

1
2
3
4
5
6
7
8
9
# Создание словаря с дефолтными значениями
status = {
    'PepIn': True, # Есть ли человек дома
    'Stop': False, # Остановили ли будильник
    'Connected': False, # Подключились ли успешно
    'Proc': None, # Объект процесса плеера mpg123
}

client.user_data_set(status)

После всей настройки нужно запустить MQTT клиент. Делаем это либо через loop_forever(). Тогда python скрипт будет вечно крутится в фоне и слушать топики MQTT. И если вам нужны аудио уведомления от вашего умного дома, то это хороший вариант.

1
client.loop_forever()

Либо через “разовый” метод loop(). В случае будильника, проще не следить за временем и датой в самом скрипте, а воспользоваться стредствами ОС (cron). От туда будем запускать скрипт, который будет работать до момента сброса будильника.

Для этого в конструкции while следим за наличием людей в квартире, подключились ли мы вообще к серверу и затем, не нажата ли кнопка сброса.

1
2
3
4
5
6
7
8
while (status['PepIn'] or not status['Connected']) and not status['Stop']:
    client.loop()

    if not type(status['Proc'].poll()) is None and status['Proc'].poll() == 0:
        status['Proc'] = subprocess.Popen(['mpg123', '-q', './audio/alarm.mp3'])

# Если вышли из цикла, убить процесс плеера
status['Proc'].kill()

Весь код программы вылажен на GitHub. Настройки паралей, юзеров, путей аудио файлов и т. п. вынесены в отдельный конфиг settings.np.json, скопируйте публичный шаблон settings.json и введите свои данные.

1
cp ./settings.json settings.np.json

Cron

Сперва проверяем дату и время на текущей машине:

1
date

Если все ок, открываем для редактирования cron:

1
crontab -e

В данном случае я делаю это от пользователя pi, а не root (без sudo). У каждого пользователя свой crontab.

Добавляем правила:

1
2
40 4 * * 1-5 cd /home/pi/mqtt_speaker && $(which python3) mqtt_speaker.py >> mqtt_speaker.log
40 5 * * 0,6 cd /home/pi/mqtt_speaker && $(which python3) mqtt_speaker.py >> mqtt_speaker.log

Смысл тут такой:

  • С понедельника по пятницу, в 04:30, запускать mqtt_speaker.py, находящийся в директории /home/pi/mqtt_speaker, используя python3. Весь вывод программы записать mqtt_speaker.log, в той же директории.
  • С субботу и воскресенье, в 05:30, запускать mqtt_speaker.py, находящийся …

Для понимания как кодируется время в cron, советую воспользоваться калькулятором Crontab Guru или посмотреть примеры у них же.

На данный момент с отдноплатником все, переходим к настройке системы умного дома.

Node-RED

У меня вся логика УД прописана в Node-RED. Если у вас другая система, то можете работать в ней, главное чтобы в ней была поддержка mqtt и zigbee кнопки.

Задача слушать MQTT топик speaker/nodered и при получении сообщения get_info, отправить информацию в топик speaker/nodered/info. А если кнопка нажата отправить сообщение в speaker/nodered/button. Пропишем эту логику в ноде function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var roomName = 'Bedroom'
// Получаем информацию из переменной, в контексте flow.
var room = flow.get(roomName);

// Eсли переменная была пуста, запишем значения по умолчанию.
if (room === undefined) {
    room = {
        InBed: false,
        PepIn: false
    };
    flow.set(roomName, room);
}

// Если на вход ноды поступило сооющение get_info.
if (msg.payload === "get_info") {
    return {
        payload: {
            'PepIn': room.PepIn,
            'InBed': room.InBed
        },
        topic: 'speaker/nodered/info'
    };
} 

// Если получена информация от кнопки, отправить команду на остановки. 
if (msg.topic == 'Alarm_Switch' && msg.payload.buttonevent == 1000) {
    return {
        payload: {
            'Stop': true
        },
        topic: 'speaker/nodered/button'
    };
}

На вход этой ноды подключим две других - mqtt in где подпишемся на speaker/nodered/# и ноду deCONZ in. Через deCONZ я подключаю все zigbee устройства к Node-RED.

На выход подключаем mqtt out, в ней самой не будем указывать топик, так как они указываются скриптом в function.

Источники