2 years ago, I was faced with the task of implementing remote control of heaters in my country house. In this article I want to share my version of automation and remote control, to which I eventually came. I will try to cover the whole process and details of creating this hobby project and share all the difficulties that I had to face. In the process of implementation, as the name of the article shows, I used Noolite (I’ll talk about it in the article), Telegram, and quite a bit of Python.
This project originates in 2014, when I faced the task of providing remote control of heaters in my country house. The fact is that almost every weekend we spend with our family in the country. And if in the summer we, having lingered for one reason or another in the city, when we arrived at the house, could immediately go to bed, in the winter, when the temperature drops to -30 degrees, I had to spend 3-4 hours for the heating of the house. I have seen the following solutions to this problem:
"Stupid solution" - you can leave the heaters with built-in thermostats on at the minimum temperature for maintaining heat. Actually there is nothing “smart” in this decision, but 24/7 working heaters in a wooden country house do not inspire confidence. I wanted at least minimal control over their condition, automation, and some feedback;
GSM sockets - my neighbors in the dacha use this solution. If someone is not familiar with them, then this is just an adapter controlled via SMS commands that is plugged into the outlet, and the heater itself is plugged into it. Not the most budget solution, if you need to provide heating of the whole house - a link to the market . I see it as the simplest and less labor-consuming to implement, but having disadvantages during operation, such as: a whole bunch of SIM cards and work on maintaining their positive balance, since each room needs at least one heater, the limitations and inconveniences of controlling them SMS;
As the most promising solution, I chose the third option and the next question on the agenda was “Which platform for implementation to choose?”.
I don’t remember how much time I spent searching for suitable options, but in the end I found the following systems from the budget and affordable solutions in stores: NooLite and CoCo (now renamed Trust). When comparing them, the decisive role for me was played by the fact that NooLite has an open and documented API for managing any of its blocks. At that time there was no need for it, but I immediately noted how much further flexibility this can give. And the price of NooLite was significantly lower. In the end, I opted for NooLite.
The NooLite system consists of power modules (for different types of loads), sensors (temperature, humidity, movement) and equipment controlling them: radio consoles, wall switches, USB adapters for a computer or PR1132 Ethernet gateway. All this can be used in various combinations, connected to each other directly or managed via usb adapters or a gateway, you can read more about it on the official website of the manufacturer.
For my task, I chose the PR1132 Ethernet gateway, which will control the power units and receive information from the sensors, as the central element of the smart home. For the Ethernet gateway to work, you need to connect it to the network with a cable, there is no Wi-Fi support in it. At that time, a network consisting of the Asus rt-n16 WiFi router and a USB modem for accessing the Internet was already organized in my house. Therefore, for me, the entire installation of NooLite was only to connect the gateway with a cable to the router, locate the temperature sensors in the house and mount the power units in the central electric panel.
NooLite has a number of power units for different pluggable loads. The most powerful unit can control loads up to 5000 watts. If more load control is required, as in my case, then a load connection can be made via a controlled relay, which, in turn, will be controlled by the NooLite power unit.
Wiring diagram
PR1132 Ethernet gateway and Asus rt-n16 router
Wireless temperature and humidity sensor PT111
Electrical panel and power unit for external installation of the SR211 - in the future, instead of this unit, I used the unit for internal installation and placed it directly in the electrical panel
Ethernet gateway PR1132 has a web-interface through which the binding / decoupling of power units, sensors and their management is carried out. The interface itself is made in a rather "clumsy" minimalist style, but this is quite enough for access to all the necessary functionality of the system:
Settings
Control
Page one switch group
Details on the binding and configuration of all this - again on the official website.
At that time I could:
Just timers of automation for some time solved my initial task. On Friday morning and afternoon, the heaters were turned on, and by the evening we were arriving at a warm house. In case our plans change, a second timer was set up, which turned off the batteries closer to the night.
The first implementation allowed me to partially solve my problem, but still I wanted to control the online home and the availability of feedback. I started looking for options for organizing access to the dacha network from outside
As I mentioned in the previous section, the dacha network has access to the Internet via a usb modem from one of the mobile operators. By default, mobile modems have a gray ip address and cannot receive a white fixed ip without additional monthly expenses. With such a gray IP, various no-ip services will not help either.
The only option that I managed to come up with at the time was VPN. I had a VPN server configured on the city router, which I used from time to time. I needed to configure a VPN client on the country router and register static routes to the country network.
Wiring diagram
As a result, the country router kept the VPN connection to the city router and in order to access the NooLite gateway, I needed to connect from the client device (laptop, phone) via VPN to the city router.
At this stage I could:
In general, it is almost 100% cover the original task. However, I understood that this implementation is far from optimal and easy to use, since every time I had to perform a number of additional actions on connecting to a VPN. For me it was not a particular problem, but for the rest of the family it was not very convenient. Also in this implementation there were a lot of intermediaries, which affected the resiliency of the entire system as a whole. However, for some time I stopped on this version.
With the advent of bots in Telegram, I took note that this could be a fairly convenient interface for managing a smart home and, as soon as I had enough free time, I started developing in Python 3.
The bot should have been somewhere and, as the most energy-efficient solution, I chose the Raspberry Pi. Although this was my first experience with him, there was no particular difficulty in setting it up. The image on the memory card, ethernet cable to the port and via ssh is a full-fledged Linux.
As I already said, NooLite has a documented API, which was useful to me at this stage. First, I wrote a simple wrapper for more convenient interaction with the API:
""" NooLite API wrapper """ import requests from requests.auth import HTTPBasicAuth from requests.exceptions import ConnectTimeout, ConnectionError import xml.etree.ElementTree as ET class NooLiteSens: """ , """ def __init__(self, temperature, humidity, state): self.temperature = float(temperature.replace(',', '.')) if temperature != '-' else None self.humidity = int(humidity) if humidity != '-' else None self.state = state class NooLiteApi: """ NooLite""" def __init__(self, login, password, base_api_url, request_timeout=10): self.login = login self.password = password self.base_api_url = base_api_url self.request_timeout = request_timeout def get_sens_data(self): """ xml :return: NooLiteSens :rtype: list """ response = self._send_request('{}/sens.xml'.format(self.base_api_url)) sens_states = { 0: ' , ', 1: ' ', 2: ' ', 3: ' ' } response_xml_root = ET.fromstring(response.text) sens_list = [] for sens_number in range(4): sens_list.append(NooLiteSens( response_xml_root.find('snst{}'.format(sens_number)).text, response_xml_root.find('snsh{}'.format(sens_number)).text, sens_states.get(int(response_xml_root.find('snt{}'.format(sens_number)).text)) )) return sens_list def send_command_to_channel(self, data): """ NooLite NooLite url data :param data: url :type data: dict :return: response """ return self._send_request('{}/api.htm'.format(self.base_api_url), params=data) def _send_request(self, url, **kwargs): """ NooLite url kwargs :param url: url :type url: str :return: response NooLite """ try: response = requests.get(url, auth=HTTPBasicAuth(self.login, self.password), timeout=self.request_timeout, **kwargs) except ConnectTimeout as e: print(e) raise NooLiteConnectionTimeout('Connection timeout: {}'.format(self.request_timeout)) except ConnectionError as e: print(e) raise NooLiteConnectionError('Connection timeout: {}'.format(self.request_timeout)) if response.status_code != 200: raise NooLiteBadResponse('Bad response: {}'.format(response)) else: return response # NooLiteConnectionTimeout = type('NooLiteConnectionTimeout', (Exception,), {}) NooLiteConnectionError = type('NooLiteConnectionError', (Exception,), {}) NooLiteBadResponse = type('NooLiteBadResponse', (Exception,), {}) NooLiteBadRequestMethod = type('NooLiteBadRequestMethod', (Exception,), {})
And then the bot itself was written using the python-telegram-bot package:
import os import logging import functools import yaml import requests import telnetlib from requests.exceptions import ConnectionError from telegram import ReplyKeyboardMarkup, ParseMode from telegram.ext import Updater, CommandHandler, Filters, MessageHandler, Job from noolite_api import NooLiteApi, NooLiteConnectionTimeout,\ NooLiteConnectionError, NooLiteBadResponse # config = yaml.load(open('conf.yaml')) # logger = logging.getLogger() logger.setLevel(logging.INFO) formatter = logging.Formatter( '%(asctime)s - %(filename)s:%(lineno)s - %(levelname)s - %(message)s' ) stream_handler = logging.StreamHandler() stream_handler.setFormatter(formatter) logger.addHandler(stream_handler) # NooLite updater = Updater(config['telegtam']['token']) noolite_api = NooLiteApi( config['noolite']['login'], config['noolite']['password'], config['noolite']['api_url'] ) job_queue = updater.job_queue def auth_required(func): """ """ @functools.wraps(func) def wrapped(bot, update): if update.message.chat_id not in config['telegtam']['authenticated_users']: bot.sendMessage( chat_id=update.message.chat_id, text=" .\n /auth password." ) else: return func(bot, update) return wrapped def log(func): """ """ @functools.wraps(func) def wrapped(bot, update): logger.info('Received message: {}'.format( update.message.text if update.message else update.callback_query.data) ) func(bot, update) logger.info('Response was sent') return wrapped def start(bot, update): """ """ bot.sendMessage( chat_id=update.message.chat_id, text=" .\n" " /auth password." ) def auth(bot, update): """ , """ if config['telegtam']['password'] in update.message.text: if update.message.chat_id not in config['telegtam']['authenticated_users']: config['telegtam']['authenticated_users'].append(update.message.chat_id) custom_keyboard = [ ['/_', '/_'], ['/_', '/_'], ['/'] ] reply_markup = ReplyKeyboardMarkup(custom_keyboard) bot.sendMessage( chat_id=update.message.chat_id, text=" .", reply_markup=reply_markup ) else: bot.sendMessage(chat_id=update.message.chat_id, text=" .") def send_command_to_noolite(command): """ NooLite. . , . """ try: logger.info('Send command to noolite: {}'.format(command)) response = noolite_api.send_command_to_channel(command) except NooLiteConnectionTimeout as e: logger.info(e) return None, "* !*\n`{}`".format(e) except NooLiteConnectionError as e: logger.info(e) return None, "*!*\n`{}`".format(e) except NooLiteBadResponse as e: logger.info(e) return None, "* !*\n`{}`".format(e) return response.text, None # ========================== Commands ================================ @log @auth_required def outdoor_light_on(bot, update): """ """ response, error = send_command_to_noolite({'ch': 2, 'cmd': 2}) logger.info('Send message: {}'.format(response or error)) bot.sendMessage(chat_id=update.message.chat_id, text="{}".format(response or error)) @log @auth_required def outdoor_light_off(bot, update): """ """ response, error = send_command_to_noolite({'ch': 2, 'cmd': 0}) logger.info('Send message: {}'.format(response or error)) bot.sendMessage(chat_id=update.message.chat_id, text="{}".format(response or error)) @log @auth_required def heaters_on(bot, update): """ """ response, error = send_command_to_noolite({'ch': 0, 'cmd': 2}) logger.info('Send message: {}'.format(response or error)) bot.sendMessage(chat_id=update.message.chat_id, text="{}".format(response or error)) @log @auth_required def heaters_off(bot, update): """ """ response, error = send_command_to_noolite({'ch': 0, 'cmd': 0}) logger.info('Send message: {}'.format(response or error)) bot.sendMessage(chat_id=update.message.chat_id, text="{}".format(response or error)) @log @auth_required def send_temperature(bot, update): """ """ try: sens_list = noolite_api.get_sens_data() except NooLiteConnectionTimeout as e: logger.info(e) bot.sendMessage( chat_id=update.message.chat_id, text="* !*\n`{}`".format(e), parse_mode=ParseMode.MARKDOWN ) return except NooLiteBadResponse as e: logger.info(e) bot.sendMessage( chat_id=update.message.chat_id, text="* !*\n`{}`".format(e), parse_mode=ParseMode.MARKDOWN ) return except NooLiteConnectionError as e: logger.info(e) bot.sendMessage( chat_id=update.message.chat_id, text="* noolite!*\n`{}`".format(e), parse_mode=ParseMode.MARKDOWN ) return if sens_list[0].temperature and sens_list[0].humidity: message = ": *{}C*\n: *{}%*".format( sens_list[0].temperature, sens_list[0].humidity ) else: message = " : {}".format(sens_list[0].state) logger.info('Send message: {}'.format(message)) bot.sendMessage(chat_id=update.message.chat_id, text=message, parse_mode=ParseMode.MARKDOWN) @log @auth_required def send_log(bot, update): """ """ bot.sendDocument( chat_id=update.message.chat_id, document=open('/var/log/telegram_bot/err.log', 'rb') ) @log def unknown(bot, update): """ """ bot.sendMessage(chat_id=update.message.chat_id, text=" ") def power_restore(bot, job): """ """ for user_chat in config['telegtam']['authenticated_users']: bot.sendMessage(user_chat, ' ') def check_temperature(bot, job): """ E , - """ try: sens_list = noolite_api.get_sens_data() except NooLiteConnectionTimeout as e: print(e) return except NooLiteConnectionError as e: print(e) return except NooLiteBadResponse as e: print(e) return if sens_list[0].temperature and \ sens_list[0].temperature < config['noolite']['temperature_alert']: for user_chat in config['telegtam']['authenticated_users']: bot.sendMessage( chat_id=user_chat, parse_mode=ParseMode.MARKDOWN, text='* {} : {}!*'.format( config['noolite']['temperature_alert'], sens_list[0].temperature ) ) def check_internet_connection(bot, job): """ - telnet . - Raspberry Pi """ try: requests.get('http://ya.ru') config['noolite']['internet_connection_counter'] = 0 except ConnectionError: if config['noolite']['internet_connection_counter'] == 2: tn = telnetlib.Telnet(config['router']['ip']) tn.read_until(b"login: ") tn.write(config['router']['login'].encode('ascii') + b"\n") tn.read_until(b"Password: ") tn.write(config['router']['password'].encode('ascii') + b"\n") tn.write(b"reboot\n") elif config['noolite']['internet_connection_counter'] == 4: os.system("sudo reboot") else: config['noolite']['internet_connection_counter'] += 1 dispatcher = updater.dispatcher dispatcher.add_handler(CommandHandler('start', start)) dispatcher.add_handler(CommandHandler('auth', auth)) dispatcher.add_handler(CommandHandler('', send_temperature)) dispatcher.add_handler(CommandHandler('_', heaters_on)) dispatcher.add_handler(CommandHandler('_', heaters_off)) dispatcher.add_handler(CommandHandler('_', outdoor_light_on)) dispatcher.add_handler(CommandHandler('_', outdoor_light_off)) dispatcher.add_handler(CommandHandler('log', send_log)) dispatcher.add_handler(MessageHandler([Filters.command], unknown)) job_queue.put(Job(check_internet_connection, 60*5), next_t=60*5) job_queue.put(Job(check_temperature, 60*30), next_t=60*6) job_queue.put(Job(power_restore, 60, repeat=False)) updater.start_polling(bootstrap_retries=-1)
This bot runs on Raspberry Pi under Supervisor, which monitors its status and starts it upon reboot.
Bot operation
When starting the bot:
Commands are hardcoded in the code and include:
An example of communication with the bot:
As a result, I and all family members received a fairly convenient interface for managing a smart home via Telegram. All you need to do is to install a client telegram on your device and know the password to start communicating with the bot.
In the end, I can:
This implementation is 100% solved the original task, it was convenient and intuitive to use.
Budget (at current prices):
On the way out I got a fairly flexible budget system that can be easily expanded as needed (NooLite gateway supports up to 32 channels). I and family members can easily use it without having to perform any additional actions: I went into the telegram - checked the temperature - turned on the heaters.
In fact, this implementation is not the last. Just a week ago, I connected this entire system to Apple HomeKit, which allowed me to add control through the iOS app “Home” and the corresponding integration with Siri for voice control. But the implementation process pulls on a separate article. If the community is interested in this topic, it is ready in the near future to prepare another article.
UPD: second article about HomeKit integration
Related Links:
Source: https://habr.com/ru/post/312328/
All Articles