NooLite + Raspberry Pi + Telegram =スマートホーム

2年前、私は田舎の家で暖房装置の遠隔制御を実現するという課題に直面しました。 この記事では、最終的に出てきた自動化とリモートコントロールのバージョンを共有したいと思います。 この趣味のプロジェクトを作成するプロセス全体と詳細をカバーし、直面しなければならないすべての困難を共有しようとします。 実装のプロセスでは、記事の名前が示すように、Noolite(記事で説明します)、Telegram、およびかなりの量のPythonを使用しました。







画像






はじめに



このプロジェクトは、2014年に、田舎の家で暖房器具のリモートコントロールを提供するという課題に直面したときに始まりました。 事実、週末ごとに家族と私は田舎で過ごしています。 そして、夏に何らかの理由で市内に滞在して、家に到着してすぐに寝ることができた場合、冬に温度が-30度に下がると、私は家を燃やすのに3-4時間費やさなければなりませんでした。 この問題に対する次の解決策を見ました。







  1. 「愚かな決定」 -内蔵サーモスタットを備えたヒーターを最低温度に保ち、熱を維持できます。 実際、この決定には「賢い」ものは何もありませんが、木製のカントリーハウスで24時間365日稼働している暖房器具は自信を刺激しません。 私は、彼らの状態、自動化、および何らかのフィードバックを少なくとも最小限に抑えたいと思っていました。







  2. GSMソケット -このソリューションは、隣人が夏の別荘で使用します。 誰かがそれらに慣れていない場合、これは単にコンセントに差し込まれ、ヒーター自体がそれに接続されているSMSコマンドによって制御されるアダプターです。 家全体に暖房を提供する必要がある場合、市場へのリンク -最も予算のソリューションではありません。 私はそれを実装するのが最も簡単で労力が少ないと考えていますが、操作中に次のような欠点があります:SIMカードの束全体とポジティブバランスを維持するための作業SMS機能;







  3. 「スマートホーム」 -実際には「スマートホーム」の実装に基づいて構築されたソリューション。


最も有望な解決策として、3番目のオプションを選択しました。次の質問は、「どのプラットフォームを選択して実装するか」です。







適切なオプションを探すのにどれだけ時間を費やしたか覚えていませんが、店舗の予算と手頃な価格のソリューションの結果、システム: NooLiteCoCo (現在はTrustに改名)を見つけました。 それらを比較するとき、NooLiteにはブロックを管理するためのオープンで文書化されたAPIがあるという事実が、私にとって決定的な役割を果たしました。 当時、それは必要ありませんでしたが、私はすぐにこれが将来どのような柔軟性を与える可能性があるかに気付きました。 NooLiteの価格は大幅に低かった。 最後に、私はNooLiteを選択しました。







実装1-NooLite自動化



NooLiteシステムは、電源モジュール(さまざまな種類の負荷用)、センサー(温度、湿度、動き)、およびそれらを制御する機器(無線リモート、壁スイッチ、コンピューター用のUSBアダプターまたはPR1132イーサネットゲートウェイ)で構成されます。 これらはすべて、さまざまな組み合わせで使用したり、直接接続したり、USBアダプターやゲートウェイを介して制御したりできます。詳細については、メーカーの公式ウェブサイトをご覧ください。







私の仕事では、スマートホームの中心要素として、電源ユニットを制御し、センサーから情報を受信するPR1132イーサネットゲートウェイを選択しました。 イーサネットゲートウェイが機能するには、ケーブルでネットワークに接続する必要があります。Wi-Fiサポートはありません。 当時、Asus rt-n16 WiFiルーターとインターネットにアクセスするためのUSBモデムで構成されるネットワークは、すでに私の家で組織されていました。 したがって、NooLiteのインストール全体は、ゲートウェイをケーブルでルーターに接続し、温度センサーを家に置き、電源ユニットを中央の電気パネルに取り付けるだけでした。







NooLiteには、さまざまな負荷容量に対応した多数の電源ユニットがあります。 最も「強力な」ユニットは、最大5000ワットの負荷を管理できます。 私の場合のように、より大きな負荷を管理する必要がある場合は、制御されたリレーを介して負荷を接続できます。リレーは、NooLiteパワーユニットによって制御されます。







画像

配線図







画像






PR1132イーサネットゲートウェイおよびAsus RT-N16ルーター







画像






ワイヤレス温湿度センサーPT111







画像






屋外設置用の電気パネルと電源ユニットSR211-このユニットの代わりに後で内部設置にユニットを使用し、電気パネルに直接配置しました







PR1132イーサネットゲートウェイには、電源ユニット、センサー、およびセンサーをリンク/アンタイドおよび制御するためのWebインターフェイスがあります。 インターフェイス自体はかなり「不器用な」ミニマリストスタイルで作成されていますが、これはシステムのすべての必要な機能にアクセスするには十分です。







画像

設定







画像

運営管理







画像

1つのスイッチグループのページ







これらすべてのバインディングと設定に関する詳細-公式サイトで再度。







その瞬間に私はできました:









しばらくの間、自動化タイマーが私の最初のタスクを解決しました。 金曜日の朝と午後、ヒーターがオンになり、夕方までに暖かい家に到着しました。 計画が変更された場合に備えて、2番目のタイマーが設定され、夜間にバッテリーがオフになりました。







実装2-スマートホームへのリモートアクセス



最初の実装により、私の問題を部分的に解決することができましたが、それでも家のオンライン制御とフィードバックの存在が必要でした。 私は、外部からカントリーネットワークへのアクセスを整理するためのオプションを探し始めました。







前のセクションで述べたように、郊外のネットワークは、モバイルオペレーターの1つのusbモデムを介してインターネットにアクセスできます。 デフォルトでは、モバイルモデムにはグレーのIPアドレスがあり、月額の追加費用なしで白い固定IPを取得することはできません。 このような灰色のIPでは、さまざまなno-ipサービスも役に立ちません。







そのときに思いついた唯一のオプションはVPNでした。 都市のルーターで、VPNサーバーを構成しました。これは時々使用していました。 国のルーターでVPNクライアントを構成し、国のネットワークへの静的ルートを登録する必要がありました。







画像

配線図







その結果、カントリールーターは常にシティルーターとVPN接続を維持し、クライアントデバイス(ラップトップ、電話)からVPNを介してシティルーターに接続するために必要なNooLiteゲートウェイにアクセスしました。







この時点で私はできました:









一般的に、これはほぼ100%が元のタスクをカバーしていました。 しかし、VPNに接続するために多くの追加手順を実行する必要があるたびに、この実装は最適で使いやすいとはほど遠いことに気付きました。 私にとってこれは特に問題ではありませんでしたが、家族全員にとってはあまり便利ではありませんでした。 また、この実装では、システム全体のフォールトトレランスに影響する多くの仲介者がいました。 しかし、しばらくの間、私はこのオプションに落ち着きました。







実装3-テレグラムボット



Telegramでのボットの出現により、これがスマートホームを管理するための非常に便利なインターフェイスになる可能性があることに注意し、十分な時間があるとすぐにPython 3で開発を開始しました。







ボットはどこかに配置する必要があり、最もエネルギー効率の高いソリューションとして、Raspberry Piを選択しました。 これは彼との最初の経験でしたが、設定するのに特別な困難はありませんでした。 メモリカードへのイメージ、ポートおよびsshへのイーサネットケーブル-完全なLinux。







前にも言ったように、NooLiteには文書化されたAPIがあり、この段階で役立ちます。 最初に、APIとの対話をより便利にするための簡単なラッパーを作成しました。







noolite_api.py
""" 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,), {})
      
      





そして、 python-telegram-botパッケージを使用して、ボット自体が記述されました。







telegram_bot.py
 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)
      
      





このボットは、スーパーバイザーの下のRaspberry Piで起動され、その状態を監視し、再起動時に起動します。







画像

ボットのスキーム







ボットの起動時:









コマンドはコードにハードコーディングされており、次のものが含まれます。









ボットとの通信の例:







画像






その結果、私と家族全員がTelegramを介してスマートホームを管理するための非常に便利なインターフェイスを手に入れました。 必要なことは、デバイスに電報クライアントをインストールし、ボットとの通信を開始するためのパスワードを知ることだけです。







その結果、次のことができます。









この実装により、最初の問題が100%解決され、使いやすく直感的でした。







おわりに



予算(現在の価格で):









出力では、必要に応じて簡単に拡張できるかなり柔軟な予算システムを得ました(NooLiteゲートウェイは最大32チャネルをサポートします)。 私の家族と私は、追加のアクションを実行せずに簡単に使用できます。電報に入って-温度を確認して-ヒーターをオンにしました。







実際、この実装は最後ではありません。 ほんの一週間前、私はこのシステム全体をApple HomeKitに接続しました。これにより、Home用のiOSアプリを介した制御と、音声制御用のSiriとの対応する統合を追加することができました。 しかし、実装プロセスは別の記事に基づいています。 コミュニティがこのトピックに興味を持っているなら、近いうちに別の記事を準備する準備ができています。







UPD: HomeKitとの統合に関する2番目の記事







関連リンク:










All Articles