diff --git a/README.md b/README.md index 1233ca7..4982f85 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ -# RingPi-Bot \ No newline at end of file +# RingPi-Bot + +When the doorbell rings, a message is sent to a private Telegram chat group. + +Based on RaspBerryPi Zero and switching a GPIO via optocoupler through 12VAC +from the doorbell, see [wiring.png](hardware/wiring.png) + + + diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..efca3b0 --- /dev/null +++ b/bot/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# __init__.py +# created 01.10.2021 +# Thomas Kaulke, kaulketh@gmail.com +# https://github.com/kaulketh +# ----------------------------------------------------------- +from .ring_bot import * +from .singleton import * diff --git a/bot/ring_bot.py b/bot/ring_bot.py new file mode 100644 index 0000000..e35db4f --- /dev/null +++ b/bot/ring_bot.py @@ -0,0 +1,112 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# bot +# created 01.10.2021 +# Thomas Kaulke, kaulketh@gmail.com +# https://github.com/kaulketh +# ----------------------------------------------------------- +import os +import signal +import time +from multiprocessing import Process + +import telepot +from telepot.loop import MessageLoop + +from bot import singleton +from config import RING_BOT_TOKEN, RING_BOT_NAME, RING_RING_GROUP, \ + THK # no public deployment (secret.py) +from config import switch_state, DING_DONG, WELCOME, RUNNING, STOPPED, \ + UNKNOWN_CMD, UNKNOWN_TYPE, CMD_START, CMD_STOP, CMD_REBOOT, REBOOT, \ + START, STARTED, STOPPING +from logger import LOGGER + + +class RingBot(singleton.Singleton): + """ Bot class using telepot framework + (https://telepot.readthedocs.io), + Python >= 3 + """ + + def __init__(self, token, admin): + self.__log = LOGGER + self.__log.debug(f"Initialize instance of {self.__class__.__name__}") + self.__token = token + self.__admin = admin + self.__bot = telepot.Bot(self.__token) + self.__ding_dong = DING_DONG.format(RING_BOT_NAME) + self.__receiver = RING_RING_GROUP + self.__checker = None + + def __check_bell(self, timeout=.25): + while True: + if switch_state(): + self.__log.info(switch_state()) + self.__send(self.__receiver, self.__ding_dong) + time.sleep(timeout) + + def __send(self, chat_id, text): + self.__log.debug( + f"Message posted: " + f"{chat_id}|{text}".replace("\n", " ")) + self.__bot.sendMessage(chat_id, text) + + def __handle(self, msg): + content_type, chat_type, chat_id = telepot.glance(msg) + self.__log.debug(msg) + # check user + if chat_id != self.__admin: + # TODO: wrong id + pass + return + # check content + if content_type == 'text': + command = msg['text'] + self.__log.info(f"Got command '{command}'") + # commands + # start + if command == CMD_START: + if self.__checker is None: + self.__checker = Process(target=self.__check_bell) + self.__checker.start() + self.__send(self.__admin, STARTED) + self.__send(self.__admin, RUNNING) + # stop + elif command == CMD_STOP: + if isinstance(self.__checker, Process): + self.__checker.terminate() + self.__checker = None + self.__send(self.__admin, STOPPING) + self.__send(self.__admin, STOPPED) + elif command == CMD_REBOOT: + self.__send(self.__admin, REBOOT.format(RING_BOT_NAME)) + os.system("sudo reboot") + else: + self.__send(self.__admin, UNKNOWN_CMD) + else: + self.__send(self.__admin, UNKNOWN_TYPE) + + def start(self): + try: + MessageLoop(self.__bot, + {'chat': self.__handle}).run_as_thread() + self.__log.info(START) + self.__send(self.__admin, WELCOME.format(RING_BOT_NAME)) + while True: + try: + signal.pause() + except KeyboardInterrupt: + self.__log.warning('Program interrupted') + exit() + except Exception as e: + self.__log.error(f"An error occurred: {e}") + exit() + + +def run(): + RingBot(RING_BOT_TOKEN, THK).start() + + +if __name__ == '__main__': + pass diff --git a/bot/singleton.py b/bot/singleton.py new file mode 100644 index 0000000..e8e3347 --- /dev/null +++ b/bot/singleton.py @@ -0,0 +1,21 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# singleton +# created 01.10.2021 +# Thomas Kaulke, kaulketh@gmail.com +# https://github.com/kaulketh +# ----------------------------------------------------------- +class _Singleton(type): + """ A metaclass that creates a Singleton base class when called. """ + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(_Singleton, cls).__call__(*args, + **kwargs) + return cls._instances[cls] + + +class Singleton(_Singleton('SingletonMeta', (object,), {})): + pass diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..fc41d31 --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# __init__.py +# created 01.10.2021 +# Thomas Kaulke, kaulketh@gmail.com +# https://github.com/kaulketh +# ----------------------------------------------------------- +from .constants import * +from .gpio import * +from .secret import * # no public deployment diff --git a/config/constants.py b/config/constants.py new file mode 100644 index 0000000..28c0824 --- /dev/null +++ b/config/constants.py @@ -0,0 +1,33 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# constants +# created 01.10.2021 +# Thomas Kaulke, kaulketh@gmail.com +# https://github.com/kaulketh +# ----------------------------------------------------------- +CHECK_NAME = "Klingel-Überwachung" + +CMD_START = "/start" +CMD_STOP = "/stop" +CMD_REBOOT = "/reboot" + +UNKNOWN_CMD = "UNKNOWN COMMAND!" +UNKNOWN_TYPE = "UNKNOWN CONTENT TYPE!" + +REBOOT = "{} wird neu gestarte!" +START = "Bot is running..." +WELCOME = "{} einsatzbereit!\n Starten mit '/start'!" + +STARTED = f"{CHECK_NAME} gestartet." +RUNNING = f"{CHECK_NAME} läuft..." +STOPPING = f"{CHECK_NAME} wird angehalten." +STOPPED = f"{CHECK_NAME} gestoppt!" + +DING_DONG = "{}\n\n\U0001F514 DING DONG \U0001F514\nEs hat an der Tür " \ + "geklingelt\U00002755" + +# CMD_LIST_BOT_FATHER = +# start - Start Klingel-Check +# stop - Stop Klingel-Check +# reboot - Reboot Thk1220RingBot diff --git a/config/gpio.py b/config/gpio.py new file mode 100644 index 0000000..4314ad1 --- /dev/null +++ b/config/gpio.py @@ -0,0 +1,21 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# gpio +# created 01.10.2021 +# Thomas Kaulke, kaulketh@gmail.com +# https://github.com/kaulketh +# ----------------------------------------------------------- +import RPi.GPIO as GPIO + +PIN = 23 +GPIO.setmode(GPIO.BCM) +GPIO.setup(PIN, GPIO.IN) + + +def switch_state(): + return False if GPIO.input(PIN) == 0 else True + + +if __name__ == '__main__': + pass diff --git a/hardware/rpi_zero_gpio.png b/hardware/rpi_zero_gpio.png new file mode 100644 index 0000000..1618200 Binary files /dev/null and b/hardware/rpi_zero_gpio.png differ diff --git a/hardware/wiring.fzz b/hardware/wiring.fzz new file mode 100644 index 0000000..7526f97 Binary files /dev/null and b/hardware/wiring.fzz differ diff --git a/hardware/wiring.png b/hardware/wiring.png new file mode 100644 index 0000000..29fe225 Binary files /dev/null and b/hardware/wiring.png differ diff --git a/logger/__init__.py b/logger/__init__.py new file mode 100644 index 0000000..814a96b --- /dev/null +++ b/logger/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# __init__.py +# created 01.10.2021 +# Thomas Kaulke, kaulketh@gmail.com +# https://github.com/kaulketh +# ----------------------------------------------------------- +from .logger import get_logger + +LOGGER = get_logger() diff --git a/logger/debug.ini b/logger/debug.ini new file mode 100644 index 0000000..d0a1e2f --- /dev/null +++ b/logger/debug.ini @@ -0,0 +1,22 @@ +[loggers] +keys = root + +[handlers] +keys = consoleHandler + +[formatters] +keys = sampleFormatter + +[logger_root] +level = DEBUG +handlers = consoleHandler + +[handler_consoleHandler] +class = StreamHandler +level = DEBUG +formatter = sampleFormatter +args = (sys.stdout,) + +[formatter_sampleFormatter] +format = %(asctime)s %(levelname)-7s %(module)s.%(funcName)s (linenr.%(lineno)s) %(message)s +datefmt = %Y-%m-%d %H:%M:%S \ No newline at end of file diff --git a/logger/logger.py b/logger/logger.py new file mode 100644 index 0000000..f7cca06 --- /dev/null +++ b/logger/logger.py @@ -0,0 +1,70 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# logger +# created 01.10.2021 +# Thomas Kaulke, kaulketh@gmail.com +# https://github.com/kaulketh +# ----------------------------------------------------------- +import errno +import logging +import os +from logging.config import fileConfig + +# runtime location +this_folder = os.path.dirname(os.path.abspath(__file__)) +# define log folder related to location +log_folder = os.path.join(this_folder, '../logs') + +# define ini and log files +ini_file = 'debug.ini' +info_log_file = log_folder + '/info.log' +error_log_file = log_folder + '/error.log' + +# check if exists or create log folder +try: + os.makedirs(log_folder, exist_ok=True) # Python>3.2 +except TypeError: + try: + os.makedirs(log_folder) + except OSError as exc: # Python >2.5 + if exc.errno == errno.EEXIST and os.path.isdir(log_folder): + pass + else: + raise + +# setup configuration +config_file = os.path.join(this_folder, ini_file) +fileConfig(config_file, disable_existing_loggers=True) + +# create handlers +handler_info = logging.FileHandler(os.path.join(this_folder, info_log_file)) +handler_error = logging.FileHandler(os.path.join(this_folder, error_log_file)) +# set levels +handler_info.setLevel(logging.INFO) +handler_error.setLevel(logging.ERROR) + +# create formatters and add to handlers +format_info = \ + logging.Formatter('%(asctime)s %(levelname)s ' + '[ %(module)s.%(funcName)s linenr.%(lineno)s ] ' + '%(message).180s', datefmt='%Y-%m-%d %H:%M:%S') +format_error = \ + logging.Formatter( + '%(asctime)s %(levelname)s ' + '[ %(module)s.%(funcName)s linenr.%(lineno)s ] ' + '[ thread: %(threadName)s ] %(message)s') +handler_info.setFormatter(format_info) +handler_error.setFormatter(format_error) + + +def get_logger(name: str = __name__): + logger = logging.getLogger(name) + # add handler + logger.addHandler(handler_info) + logger.addHandler(handler_error) + return logger + + +if __name__ == '__main__': + pass diff --git a/main.py b/main.py new file mode 100644 index 0000000..dfba6e3 --- /dev/null +++ b/main.py @@ -0,0 +1,12 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# ----------------------------------------------------------- +# main +# created 01.10.2021 +# Thomas Kaulke, kaulketh@gmail.com +# https://github.com/kaulketh +# ----------------------------------------------------------- +from bot import run + +if __name__ == '__main__': + run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2a7547c --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +telepot~=12.7