Разработка на фронтенде не ограничивается интернет-ресурсами, а бекенд может оказаться неожиданным. К старту курса о Fullstack-разработке на Python делимся переводом статьи, автор которой в условиях Великобритании, где центрального отопления в привычном нам смысле этих слов нет, столкнулся с неудобствами отопления в новом доме и самостоятельно собрал электронный термостат, для управления прибором написав веб-интерфейс, а также бекенд на Flask.
Недавно моим родителям установили «умный» термостат. И мне подумалось: неужели я не смогу сам сотворить нечто подобное? Отлично помню себя маленьким — я был одержим технологиями, особенно меня восхищали миниатюрные портативные устройства. Восторг вызывали мини-телевизоры, игровые приставки начала девяностых, наладонники Palm Pilot и коммуникаторы Nokia конца этого же десятилетия, карманные компьютеры, появившиеся на рубеже двухтысячных. Как же я мечтал об этом! И думал, что миниатюрные электронные устройства и системы домашней автоматики — это увлечение сильных мира сего, Брюса Уэйна или Тони Старка. Пока у меня не появилось это чудо:
Если хочется сразу перейти к коду, пропустите введение. Конечно же, я знал, что одноплатники существуют — у меня уже несколько лет была модель Pi 3B, которая работала по-разному: как эмулятор игровой консоли, как медиацентр, файловый сервер, веб-сервер, песочница kali linux и т. д. Но будем честными: модель справляется со всеми этими задачами, но без особого блеска. От медиацентра на Raspberry Pi 3 руки чешутся собрать что-то покруче!
Настоящий потенциал Pi я ощутил недавно. Оказывается, мощь компьютера Raspberry Pi кроется в его выводах GPIO (General Purpose Input/Output). Я пересмотрел множество видео на YouTube, ролики канала Explaining Computers от моего любимого Кристофера Барнатта, на которые подписан. В них подробно рассказывается о проектах и пробах с GPIO, но в попытках освоить тонкости хакерского искусства по роликам YouTube у меня не получалось придумать достойный проект, бросить всё и погрузиться в схемотехнику. До экспериментов дело не доходило.
Что мешало начать:
Опасение вывести из строя мой Pi.
Кабели-перемычки, модули, платы и т. д, они дорогие, их пришлось бы докупать.
Врождённая лень.
Разберёмся с пунктами.
Приступив к работе, вы сразу поймёте, насколько удобно расположены выводы, большинство из которых в целом одинаковы — нужно только понять, как они расположены.
Затраты окажутся удивительно незначительными, особенно если всё грамотно спланировать. Можно заказать недорогие комплектующие у сторонних производителей, но найти их и дождаться… Это может занять много времени.
Потеряв терпение, ради экспериментов я пожертвовал кабелями, которые вытащил из других мест и наугад подсоединил к выводам GPIO — всё прошло нормально, ничего не сломалось, но я вспомнил пункт #1.
Как настоящий начинающий хакер, я аккуратно подобрал инструменты первой необходимости — паяльники, мелкие отвёртки. Монтажная плата для макетов или тестов так и не понадобилась. И лень — обычное жизненное обстоятельство. Все мы справляемся с ней по-своему. Нужно себя пересилить, придумать идею и разработать план.
Достойный проект
Год назад я переехал в новый дом. Этой зимой во всей красе проявились недостатки центрального отопления, система которого имеет отдельные ответвления вниз и вверх, каждое со своим программатором. Они устроены так, что температура выставляется четыре раза в сутки, а в будни и в выходные система ведёт себя по-разному.
Например, можно запрограммировать нагрев до 20° С в 6 утра, затем снизить до 5° С в 8, потому что дома никого нет и поднять до 20° С в 6 вечера, перед сном снизив температуру до 5° С.
В субботу и воскресенье можно настроить другой режим. Это комбинированная система. У большинства систем, с которыми я имел дело, были отдельные терморегулятор и таймер; на мой взгляд, объединение этих устройств освобождает пространство на стене, но ограничивает функциональность, например потому, что у настенной модели нет кнопки, чтобы на один час усилить отопление.
Когда нужно задать какое-нибудь нестандартное время нагрева, единственное, что можно сделать, — внимательно следить за температурой на экране до какого-то порога и зафиксировать её на этом уровне до следующего изменения, а затем, когда придёт время, убавить её. Эти раздражающие действия автоматизируются, а узнав, что родители приобрели управляемый мобильным приложением термостат, это заставило задуматься, как дистанционно обогреть дом и насколько сложно собрать прибор самому.
Настройка реле отключения через GPIO RPi
Когда температура опускается ниже порогового значения, раздаётся щелчок, а звук реле ни с чем не спутаешь, поэтому я полагал, что схема работает благодаря реле. Бойлер нагревается — температура поднимается; щелчок — и бойлер остывает. В сети я заказал самое недорогое реле, которое работает с Pi. Я был взволнован и даже слегка опасался за первый проект GPIO.
Для своей модели я выбрал реле Adafruit Power Relay Featherwing. Реле на 5 ампер и 250 вольт должно справляться с британским напряжением и надёжно срабатывать от выходного напряжения Pi 3В.
Реле прибыло, я приступил к программированию. Вначале я запустил тестовый скрипт, о котором узнал на канале Explaining Computers.
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BOARD)
GPIO.setup(13, GPIO.OUT)
try:
while True:
GPIO.output(13, True)
time.sleep(1)
GPIO.output(13, False)
time.sleep(1)
finally:
GPIO.cleanup()
Первое испытание
В примере выше после импорта RPi.GPIO я установил последовательный режим Board Numbering, а затем входным контактом сделал выбранный сигнальный вывод GPIO 27, обозначенный в коде числом 13.
Затем я запустил цикл while, который с задержкой в секунду включает и выключает реле. Блок try/finally перед выходом из программы удаляет настройки. Всё заработало сразу (спасибо, Крис!). Невообразимо приятно было слышать щелчки реле и видеть, как мой скрипт работает с физическим объектом!
Оставалось ещё много свободных контактов, и это было хорошо: нужно было подключить температурный модуль. Я поискал немного и выбрал BMP280 от Bosch — самый недорогой модуль с хорошими отзывами:
Ещё пара поисков в сети, и я нашёл полезную схему и руководство о том, как подсоединить этот модуль к Pi:
В нашем случае важно знать назначение контактов, поскольку 3 и 5 (GPIO 2(SDA) и 3(SCL)) задействованы последовательной асимметричной шиной для связи между интегральными схемами.
Я выбрал конфигурацию выше, пришлось переместить кабель 3В реле на контакт Pi 3В в позиции 17. В остальном температурный датчик не должен мешать работе реле, я подключил его без проблем:
Конечно же, сначала я написал скрипты для тестирования реле и модуля датчика, и если первая часть это работы не представила для меня никаких затруднений, то со второй пришлось повозиться: передача данных зависела от характеристик I2C-выводов Pi. С ними то и дело возникали проблемы прав доступа, особенно не на Raspbian. Я работаю с Ubuntu 20.04, но всё разрешилось благополучно — достаточно было кое-что поискать и пару раз зайти на Stack Overflow.
Проблема возникала из-за моей давней приятельницы — ошибки PermissionError
, срабатывающей при попытке запустить скрипт не от имени root. Неидеальный вариант, если нужно, чтобы скрипт запускался автономно на веб-сервере.
В итоге я нашёл фантастически полезный пакет pigpiowhich
, позволяющий обходить эти разрешения, если запущен демон pigpiod
. Он может служить как замена RPi.GPIO, в настройке он значительно проще. Установить его на Ubuntu и Raspbian можно так:
sudo apt install pigpiod
Затем устанавливается модуль Python:
pip3 install pigpio
Нужно было учесть другие зависимости, пришлось установить smbus2 и pimoroni-bme280
:
pip3 install smbus2 pimoroni-bme280
Устранение неисправностей
Я рекомендую установить i2c-tools, который помог обнаружить проблему плохой пайки.
sudo apt install i2c-tools
Пакет позволяет просмотреть занятые адреса I2C. Если все контакты свободны, вы увидите такой вывод:
Если всё установлено, но ничего не работает, возможно, проблема в неаккуратной пайке. Припаивая крохотные GPIO-контакты к Pi Zero W, я не проследил, чтобы каждая капля припоя проникала непосредственно в отверстие. Сразу после исправления ошибки i2cdetect нашёл модуль:
В этот момент беспокойство #1 достигло пика. Больше всего я опасался, что случайно припаяю один крохотный контакт к другому и случится короткое замыкание. Но всё обошлось, свой Pi я не повредил, хотя оставил несколько пятен припоя в нижней части платы.
Код
Отладка оборудования была завершена, настало время написать тот самый, нетестовый код. Для системы отопления я решил создать особый класс, отвечающий за выполнение необходимых операций, так, чтобы легко импортировать его, например, в приложение Flask.
import json
import time
from datetime import datetime
from threading import Thread
import pigpio
import requests
from requests.exceptions import ConnectionError
class Heating:
def __init__(self):
self.pi = pigpio.pi()
self.advance = False
self.advance_start_time = None
self.on = False
self.tstat = False
self.temperature = self.check_temperature()
self.humidity = self.check_humidity()
self.pressure = self.check_pressure()
self.desired_temperature = 20
self.timer_program = {
'on_1': '07:30',
'off_1': '09:30',
'on_2': '17:30',
'off_2': '22:00',
}
def thermostatic_control(self):
self.tstat = True
while self.tstat:
time_check = datetime.strptime(datetime.utcnow().time().strftime('%H:%M'), '%H:%M').time()
on_1 = datetime.strptime(self.timer_program['on_1'], '%H:%M').time()
off_1 = datetime.strptime(self.timer_program['off_1'], '%H:%M').time()
on_2 = datetime.strptime(self.timer_program['on_2'], '%H:%M').time()
off_2 = datetime.strptime(self.timer_program['off_2'], '%H:%M').time()
if (on_1 < time_check < off_1) or (on_2 < time_check < off_2):
if self.check_temperature() < int(self.desired_temperature) and not self.check_state():
self.switch_on_relay()
elif self.check_temperature() > int(self.desired_temperature) + 0.5 and self.check_state():
self.switch_off_relay()
time.sleep(5)
else:
if self.check_state():
self.switch_off_relay()
time.sleep(900)
return
def thermostat_thread(self):
self.on = True
t1 = Thread(target=self.thermostatic_control)
t1.daemon = True
t1.start()
def stop_thread(self):
self.on = False
self.tstat = False
self.switch_off_relay()
def sensor_api(self):
try:
req = requests.get('http://192.168.1.88/')
data = json.loads(req.text)
return data
except ConnectionError:
return {
'temperature': self.temperature,
'humidity': self.humidity,
'pressure': self.pressure,
}
def check_temperature(self):
self.temperature = self.sensor_api()['temperature']
return self.temperature
def check_pressure(self):
self.pressure = self.sensor_api()['pressure']
return self.pressure
def check_humidity(self):
self.humidity = self.sensor_api()['humidity']
return self.humidity
def switch_on_relay(self):
self.pi.write(27, 1)
def switch_off_relay(self):
self.pi.write(27, 0)
def check_state(self):
return self.pi.read(27)
def start_time(self):
if not self.advance_start_time:
self.advance_start_time = datetime.now().strftime('%b %d, %Y %H:%M:%S')
return self.advance_start_time
if __name__ == '__main__':
hs = Heating()
while True:
print(f'''________________________________________________________________
{datetime.utcnow().time()}
Temp: {hs.check_temperature()}
Pressure: {hs.check_pressure()}
Humidity: {hs.check_humidity()}
________________________________________________________________
''')
time.sleep(2)
Я включил в код необходимые Flask проверки: метод start_time()
создаёт передаваемую в html-шаблон переменную, чтобы таймер JavaScript отсчитывал время независимо от обновлений страницы и от того, используется ли другое устройство.
API терморегулятора
Как можно заметить, в скрипте нет функции проверки самого температурного модуля. Я сделал так намеренно, потому что считаю удобным, когда Pi 3B работает с реле, а более портативный Pi Zero W через API получает данные от модуля датчика. Код этого API для Flask с методами BME280 выглядит так:
#!/usr/bin/env/python3
import time
import pigpio
from smbus2 import SMBus
from bme280 import BME280
from flask import Flask, jsonify, make_response
app = Flask(__name__)
pi = pigpio.pi()
bus = SMBus(1)
bme = BME280(i2c_dev=bus)
# throwaway readings:
for i in range(3):
bme.get_temperature()
bme.get_humidity()
bme.get_pressure()
@app.route('/')
def sensor_api():
response = make_response(jsonify({'temperature': bme.get_temperature(),
'humidity': bme.get_humidity(),
'pressure': bme.get_pressure()}))
response.status_code = 200
return response
Единственная конечная точка возвращает ответ с текущими показаниями счётчика в формате JSON.
Реверс-инжинеринг моей системы
Что мне больше всего нравится в электронике? Большинство компонентов имеют понятную маркировку и качественную документацию. Но когда дело касается электричества, всё намного запутаннее! Когда я снял крышку с распределительной коробки, что висела за сушильным шкафом, то обнаружил несколько проводов, подключённых к исполнительному механизму регулирующего клапана, который обслуживает два контура отопления: беспорядочный набор проводов, пропущенных через отверстие в стене, и четыре провода, идущих к каждому исполнительному механизму, то есть всего восемь проводов.
Я поискал и нашёл официальную документацию на клапаны исполнительного механизма и понял, что это за четыре провода и какой из них является «главным», сигнал которого прерывается реле в блоке программатора.
Из моего рассказа может сложиться впечатление, что всё у меня проходило гладко и без запинок. Но дело заняло довольно много времени — достаточно сказать, что все провода на выходе оставшейся части оказались коричневыми. Пытаясь понять, в каком направлении искать, я просмотрел огромное количество роликов на YouTube на тему «как подключить систему отопления в Великобритании».
Затем нужно было просто замкнуть реле на стене и разорвать с его помощью цепь внутри сушильного шкафа, одновременно подавая питание на оба привода. Я также решил добавить в шкаф розетку для подачи питания на Pi. Это позволило спрятать провода, которые моя жена ненавидит. Качество сигнала почти не пострадало, хотя кабель был проложен в шкафу. Это удивительно!
Контур от приводов к насосу был один, его можно было не трогать; два других вели к программаторам на стенах — сверху и снизу. Как уже говорилось, я замкнул одно реле и использовал этот контур для удлинителя розетки. Когда-нибудь я доработаю это решение, а пока пусть повисит так, тем более всё нормально.
Интерфейс управления термостатом
Вначале я проектировал маршруты на фронтенде параллельно с тестированием системы. Несколько дней пришлось ждать посылку с температурным модулем, поэтому вначале получилась версия только с таймером.
Приложение Flask простое. Я написал маршруты и представления для путей /
, /on
, /off
, /advance
и /settings
, элементарная аутентификация по простому коду на моём RPi уже работала, я решил оставить её. После кода вы увидите скриншоты интерфейсов.
#!/usr/bin/env python3
import time
from threading import Thread
from flask import Flask, redirect, url_for, render_template, request, session, jsonify, make_response
from .heating import Heating
app = Flask(__name__)
hs = Heating()
# Throwaway temp checks:
hs.check_temperature()
time.sleep(1)
hs.check_temperature()
@app.route('/heating')
def home():
if 'verified' in session:
start_time = hs.start_time() if hs.advance else None
return render_template('heating.html', on=hs.on, relay_on=hs.check_state(),
current_temp=int(hs.check_temperature()), desired_temp=int(hs.desired_temperature),
advance=hs.advance, time=start_time,
)
return redirect(url_for('login'))
@app.route('/', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
if 'verified' in session:
return redirect(url_for('menu'))
return render_template('login.html')
else:
name = request.form.get('name')
if name == 'PASSWORD':
session['verified'] = True
return redirect(url_for('menu'))
else:
return render_template('login.html', message='You are not allowed to enter.')
@app.route('/menu')
def menu():
if 'verified' in session:
return render_template('menu.html')
return redirect(url_for('login'))
@app.route('/on')
def on():
if 'verified' in session:
hs.thermostat_thread()
return redirect(url_for('home'))
return redirect(url_for('login'))
@app.route('/off')
def off():
if 'verified' in session:
hs.stop_thread()
hs.advance = False
hs.advance_start_time = None
return redirect(url_for('home'))
return redirect(url_for('login'))
def advance_thread():
interrupt = False
if hs.tstat:
hs.tstat = False
interrupt = True
hs.switch_on_relay()
time.sleep(900)
hs.switch_off_relay()
hs.advance = False
hs.advance_start_time = None
hs.on = False
if interrupt:
hs.thermostat_thread()
@app.route('/advance')
def advance():
if 'verified' in session:
hs.on = True
hs.advance = True
t1 = Thread(target=advance_thread)
t1.daemon = True
t1.start()
return redirect(url_for('home'))
return redirect(url_for('login'))
@app.route('/settings', methods=['GET', 'POST'])
def settings():
if request.method == 'GET':
if 'verified' in session:
return render_template('settings.html', des_temp=hs.desired_temperature, timer_prog=hs.timer_program)
return render_template('login.html')
else:
interrupt = False
if hs.tstat:
hs.tstat = False
interrupt = True
des_temp = request.form.get('myRange')
on_1 = request.form.get('on_1')
off_1 = request.form.get('off_1')
on_2 = request.form.get('on_2')
off_2 = request.form.get('off_2')
new_timer_prog = {
'on_1': on_1,
'off_1': off_1,
'on_2': on_2,
'off_2': off_2
}
hs.desired_temperature = des_temp
hs.timer_program = new_timer_prog
if interrupt:
hs.thermostat_thread()
return redirect(url_for('home'))
@app.route('/temp', methods=['GET'])
def fetch_temp() -> int:
response = make_response(jsonify({"temp": int(hs.check_temperature()),
"on": hs.check_state()}), 200)
return response
@app.route('/radio')
def radio():
return render_template('radio.html')
@app.errorhandler(404)
def page_not_found(e):
return redirect(url_for('home'))
if __name__ == '__main__':
app.secret_key = 'SECRET KEY'
app.run(debug=True, host='0.0.0.0', port=5000)
Исключительно ради стиля я добавил представления и элементы интерфейса. Представление отвечает на асинхронный запрос функции JavaScript, который обновляет температуру на дисплее в реальном времени и при включении реле обозначает её красным цветом:
Железо крупным планом
Если работа с Python не оставляет вас равнодушными и хочется научиться писать на этом языке или поднять навыки владения им на новый уровень, вы можете обратить внимание на наш курс по Fullstack-разработке на Python, а если есть желание чувствовать себя ближе к железу, то вы можете присмотреться к нашему курсу по C++. Также можно узнать о том, как начать карьеру или прокачаться в других направлениях:
Data Science и Machine Learning
Python, веб-разработка
Мобильная разработка
Java и C#
От основ — в глубину
А также:
Комментарии (8)
sisaenkov
26.07.2021 22:38Возможно, автор оригинала хотел применить свои свежие навыки программирования в деле. Хотя есть тот же MicroPython для микроконтроллеров.
Для тех, кому тема интересна, и уже есть Home Assistant, я предлагаю ознакомиться с моей статьей по созданию контроллера отопления на ESP8266.
gxcreator
26.07.2021 23:49+2Что мне больше всего нравится в электронике? Большинство компонентов имеют понятную маркировку и качественную документацию.
Вот это было больно.
Bonio
27.07.2021 00:28+1По моему мнению автоматизацию лучше делать в node-red, исполнительные устройства оставить тупыми, типа простых wifi реле, а панель управления сделать через home assistant. В таком случае всю логику можно перекроить вообще как вздумается в любой момент, не притрагиваясь к электронике и программатору.
Да, был бы у меня свой дом, я бы так и сделал.
ZXY000
27.07.2021 10:59-1Статья в стиле Код на GitHub
"Если работа с Python не оставляет вас равнодушными ... желание чувствовать себя ближе к железу, то вы можете присмотреться к нашему курсу по C++. Также можно узнать о том, как начать карьеру или"
Рядовая статья, ничем не отличающаяся от кучи подобных с весьма не показательными методиками освоения языков, с ссылкой на GitHUB, собственно CEO которого призывает к более продвинутым процессам девелопмента и ускорению его естественной эволюции в ИТ:
"Coding is not the main event anymore. Building software is the main event."
он так же справедливо сетует на тот факт что:
"“We talk about software eating the world, we talk about all this great technological innovation, and yet at the end of the day we're still just hitting buttons on a keyboard.”
megahertz
27.07.2021 16:14Тем кто не осилил DIY могу посоветовать Terneo. Прекрасные впечатления после первого отопительного сезона.
cat_crash
Несколько вопросов?
В чем глубокий смысл гонять R Pi когда можно исползовать ESP 8266 или ESP32
Как решена проблема стабильности? Что если SD перестанет читаться или linux грузиться после очередного скачка питания?
Пробовали ли вы искать готовые решения на том же HomeAssistant с прикрученным ESPHome?