diff --git a/MANUAL.md b/MANUAL.md index a54541e..a9335e3 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -47,15 +47,17 @@ Note: The authorized Telegram chat ID(s) and bot token are stored in a hidden fi * **Clock 5** Red hour hand, blue minute hand and warm yellow second 'pendulum' over all LEDs. * **Clock 6** Similar Clock 4, but w/o seconds and Green and Blue as major colors. * **Clock 7** white minute hand from LED 1 to 12 in 5 min steps (right half of circle), blue hour hand from LED 12 to 24 (left half of circle) + * _**Colorful animations**_ + * All LEDs at once can switch to: **red**, **blue**, **green**, **white**, **yellow**, **orange**, **violet** + * **Colors**: Switching simple colors in random time periods + * **Colors II**: Fading over simple colors in random time periods + * **Rainbow**: Rainbow animation with circular fading effect + * **Rainbow II**: Rainbow animation with chaser, included fading effect. + * **Theater**: Extremely colorful animation with spinning and wiping effects + * **Theater II**: Another colorful animation with spinning effects * **Advent**: Advent calendar, works in Advent time only! For every day of December will one LED flicker like a candlelight. If it is Advent Sunday it flickers red. Should be time before December but in Advent period all LEDs are working as candlelight. If it is other than Advent time LEDs will circle in orange as warning! * **Candles**: Each LED simulates candlelight - * **Rainbow**: Rainbow animation with circular fading effect - * **Theater**: Extremely colorful animation with chaser, spinning, wiping effects * **Strobe**: Emitting brief and rapid flashes of white light in random frequency - * _**Colors**_ - * **Colors**: Switching simple colors in random time periods - * **Colors 2**: Fading over simple colors in random time periods - * All LEDs at once can switch to: **red**, **blue**, **green**, **white**, **yellow**, **orange**, **violet** * **Service menu** no app-in menu, will build due runtime but will be handled like an app-in command because of the slash in front * **/Reboot** - What should I write here? * **/Info** - Information about the latest commit/release versions on GitHub, host name, IP, memory usage, disk usage, cpu load diff --git a/bot.py b/bot.py index 501e14c..4ca7db7 100644 --- a/bot.py +++ b/bot.py @@ -7,8 +7,6 @@ __status__ = "Production" __doc__ = "Call instance of framework class." -import threading - from urllib3.exceptions import HTTPError, ProtocolError from bot_framework import * @@ -17,7 +15,6 @@ if __name__ == '__main__': try: - threading.Thread(target=peripheral_functions.get(2)).start() telepot_bot.main() except (ConnectionResetError, ProtocolError, HTTPError) as e: LOGGER.error(f"Connection error occurs: {e}") diff --git a/bot_framework/telepot_bot.py b/bot_framework/telepot_bot.py index d5acd56..ab85a7f 100644 --- a/bot_framework/telepot_bot.py +++ b/bot_framework/telepot_bot.py @@ -8,6 +8,7 @@ import codecs import signal +import traceback import telepot from telepot.loop import MessageLoop @@ -16,11 +17,14 @@ from config import AUTO_REBOOT_ENABLED, AUTO_REBOOT_TIME, ID_CHAT_THK, \ RUNNING, TOKEN_TELEGRAM_BOT, commands, m_not_allowed, m_pls_select, \ - m_rebooted, m_started, m_stopped, m_updated, m_wrong_id -from control import peripheral_functions, run_thread, service, stop_threads + m_rebooted, m_restarted, m_started, m_stopped, m_updated, m_wrong_id, \ + AUTO_START +from control import peripheral_functions, run_thread, service, \ + stop_threads from control.reboot import AutoReboot from control.update import update_bot -from logger import LOGGER +from functions import STOP_CMD +from logger import LOGGER, HISTORY admins = [ID_CHAT_THK] @@ -47,11 +51,12 @@ def __init__(self, t, ids): self.__log.debug(f"Build app keyboards and buttons.") self._remove_keyboard = ReplyKeyboardRemove() + # keys order (config) self.__keyboard_markup = ReplyKeyboardMarkup(keyboard=[ - self.__buttons([2, 3, 6, 7, 16]), self.__buttons([4, 5, 17, 18, 19, 21, 22]), - self.__buttons([15, 20]), - self.__buttons([8, 9, 10, 13, 11, 12, 14]) + self.__buttons([8, 9, 10, 13, 11, 12, 14]), + self.__buttons([15, 20, 6, 23, 7, 24]), + self.__buttons([2, 3, 16]) ]) self.__func_thread = None @@ -124,7 +129,8 @@ def __reply_wrong_command(self, ch_id, content): def __stop_function(self, ch_id, msg): if msg is not None: self.__send(ch_id, msg, reply_markup=self.rm_kb) - return True if stop_threads() else False + # return True if stop_threads() else False + return stop_threads() def __handle(self, msg): content_type, chat_type, chat_id = telepot.glance(msg) @@ -165,6 +171,9 @@ def __handle(self, msg): elif command == service.Service.c_reboot: self.__send(chat_id, m_rebooted, reply_markup=self.rm_kb) service.reboot_device(m_rebooted) + elif command == service.Service.c_restart: + self.__send(chat_id, m_restarted, reply_markup=self.rm_kb) + service.restart_service(m_restarted) elif command == service.Service.c_info: if self.__stop_function(chat_id, msg=None): info = service.system_info() @@ -198,13 +207,36 @@ def start(self): MessageLoop(self.__bot, {'chat': self.__handle}).run_as_thread() + # TODO: nfo string/text as constant w/ translations (II) + as_nfo = f"Autostart" + self.__log.info(f"{as_nfo} = {AUTO_START}") + with open(HISTORY, "r") as f: + line = f.readlines()[-1] + self.__log.warning(line) + # TODO: implement considering of translation of stored command after language change + # - search key of value/stored string and gather translations with this key + # - depending of set language execute/set command text + cmd = line.partition(" HISTORY ")[2].replace("\n", "") + _stop = (cmd == STOP_CMD) + self.__log.warning(_stop) + if AUTO_START: + if not _stop: + self.__func_thread = run_thread(cmd, ID_CHAT_THK, self) + for a in self.__admins: + self.__send(a, f"{as_nfo}: {cmd}", + reply_markup=self.kb_stop) + else: + open(HISTORY, "w").close() + self.__stop_function(ID_CHAT_THK, msg=None) - auto_reboot = f"Auto reboot enabled" - self.__log.info(f"auto_reboot = {AUTO_REBOOT_ENABLED}") + # TODO: nfo string/text as constant w/ translations (I) + ar_nfo = f"Auto-Reboot" + self.__log.info(f"{ar_nfo} = {AUTO_REBOOT_ENABLED}") if AUTO_REBOOT_ENABLED: for a in self.__admins: - self.__send(a, f"{auto_reboot} at {AUTO_REBOOT_TIME} CET", - reply_markup=self.rm_kb) + kb = self.kb_stop if AUTO_START else self.rm_kb + self.__send(a, f"{ar_nfo}: {AUTO_REBOOT_TIME} CET", + reply_markup=kb) AutoReboot(reboot_time=AUTO_REBOOT_TIME, bot=self).start() while True: @@ -214,7 +246,8 @@ def start(self): self.__log.warning('Program interrupted') exit() except Exception as e: - self.__log.error(f"Any error occurs: {e}") + self.__log.error(f"Any error occurs: {traceback.format_exc()}") + self.__log.exception(e) exit() finally: peripheral_functions.get(3)() diff --git a/config/__init__.py b/config/__init__.py index 583c481..a6c93c0 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -6,15 +6,34 @@ __maintainer__ = "Thomas Kaulke" __status__ = "Production" -from .dictionary import * -from .secret import * # no public deployment! +import json +import os + +from logger import LOGGER from .settings import * +from .secret import * # no public deployment! + +HERE = os.path.dirname(os.path.abspath(__file__)) -build_text_libraries() -set_language(LANGUAGE) +with open(os.path.join(HERE, TRANSLATIONS), 'r', encoding='utf-8') as file: + translations = json.load(file) -m_wrong_id, m_not_allowed, m_pls_select, m_called, m_started, m_rebooted, \ - m_rotated, m_stopped, m_standby, m_stop_f, m_killed, \ - m_updated = get_texts(MSG) +commands = [] +# not really needed, but better to avoid error messages in the IDE +(m_wrong_id, m_not_allowed, m_pls_select, m_called, m_started, m_rebooted, + m_restarted, m_stopped, m_standby, m_stop_f, m_killed, m_updated) \ + = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -commands = get_texts(CMD) +LOGGER.debug(f"Dynamically creating variables") +for item in translations.items(): + _type = item[1].get('type') + _name = item[1].get('name') + _value = item[1].get(LANGUAGE) + _n = item[0] + globals()[_name] = _value + if _type == "function": + # commands + _n = len(commands) + commands.append(globals().get(_name).title()) + _value_r = _value.replace("\n", "") + LOGGER.debug(f"{_type} {_n} '{_name}': '{_value_r}'") diff --git a/config/dictionary.json b/config/dictionary.json new file mode 100644 index 0000000..3d2be60 --- /dev/null +++ b/config/dictionary.json @@ -0,0 +1,268 @@ +{ + "0": { + "type": "message", + "name": "m_wrong_id", + "English": "`Hello {1}, this is a private bot!\nID {0}, {2} {3} has been blocked.\nThanks for visit!`", + "German": "`Hallo {1}, dies ist ein privater Bot!\nID {0}, {2} {3} wurde geblockt.\nDanke für den Besuch!`", + "French": "`Bonjour {1}, c'est un bot privé!\nL'Id {0}, {2} {3} a été bloqué.\nMerci de votre visite!`" + }, + "1": { + "type": "message", + "name": "m_not_allowed", + "German": "* Nicht erlaubt für diesen Bot\\! *", + "English": "* Not allowed for this bot\\! *", + "French": "* Non autorisé pour ce bot\\! *" + }, + "2": { + "type": "message", + "name": "m_pls_select", + "German": "Bitte triff eine Auswahl, {0}.", + "English": "Please make a selection, {0}!", + "French": "Veuillez faire une sélection, {0}." + }, + "3": { + "type": "message", + "name": "m_called", + "German": "*{0}* läuft bis {1}", + "English": "*{0}* runs until {1}", + "French": "*{0}* s'exécute jusqu'à {1}" + }, + "4": { + "type": "message", + "name": "m_started", + "German": "Bot ist einsatzbereit.", + "English": "Bot ready for use.", + "French": "Le bot est prêt à l'emploi." + }, + "5": { + "type": "message", + "name": "m_rebooted", + "German": "Gerät wird neu gestartet.", + "English": "Device rebooted.", + "French": "L'appareil est redémarré." + }, + "6": { + "type": "message", + "name": "m_restarted", + "German": "Bot neu gestartet.", + "English": "Service restarted.", + "French": "Service redémarré." + }, + "7": { + "type": "message", + "name": "m_stopped", + "German": "Bot pausiert!", + "English": "Bot paused.", + "French": "Bot en pause!" + }, + "8": { + "type": "message", + "name": "m_standby", + "German": "Standby: {0}\nNeustart um {1}", + "English": "Standby: {0}\nRestart at {1}", + "French": "Veille: {0}\nRedémarrez à {1}" + }, + "9": { + "type": "message", + "name": "m_stop_f", + "German": "{0} gestoppt.", + "English": "{0} stopped.", + "French": "{0} arrêté." + }, + "10": { + "type": "message", + "name": "m_killed", + "German": "Kompletten Bot-Prozess abgebrochen!", + "English": "Bot process killed!", + "French": "Processus de bot complet annulé!" + }, + "11": { + "type": "message", + "name": "m_updated", + "German": "Bot-Update, Reboot folgt.", + "English": "Bot update, reboot shortly.", + "French": "Mise à jour, le bot est redémarré." + }, + "12": { + "type": "function", + "name": "stop", + "German": "stop", + "English": "stop", + "French": "stop" + }, + "13": { + "type": "function", + "name": "start", + "German": "start", + "English": "start", + "French": "start" + }, + "14": { + "type": "function", + "name": "advent", + "German": "advent", + "English": "advent", + "French": "advent" + }, + "15": { + "type": "function", + "name": "candles", + "German": "kerzen", + "English": "candles", + "French": "chandelles" + }, + "16": { + "type": "function", + "name": "clock 1", + "German": "uhr 1", + "English": "clock 1", + "French": "horloge 1" + }, + "17": { + "type": "function", + "name": "clock 2", + "German": "uhr 2", + "English": "clock 2", + "French": "horloge 2" + }, + "18": { + "type": "function", + "name": "rainbow", + "German": "regenbogen", + "English": "rainbow", + "French": "arc en ciel" + }, + "19": { + "type": "function", + "name": "theater", + "German": "theater", + "English": "theater", + "French": "théâtre" + }, + "20": { + "type": "function", + "name": "red", + "German": "rot", + "English": "red", + "French": "rouge" + }, + "21": { + "type": "function", + "name": "blue", + "German": "blau", + "English": "blue", + "French": "bleu" + }, + "22": { + "type": "function", + "name": "green", + "German": "grün", + "English": "green", + "French": "vert" + }, + "23": { + "type": "function", + "name": "yellow", + "German": "gelb", + "English": "yellow", + "French": "jaune" + }, + "24": { + "type": "function", + "name": "orange", + "German": "orange", + "English": "orange", + "French": "orange" + }, + "25": { + "type": "function", + "name": "white", + "German": "weiß", + "English": "white", + "French": "blanc" + }, + "26": { + "type": "function", + "name": "violet", + "German": "violett", + "English": "violet", + "French": "violet" + }, + "27": { + "type": "function", + "name": "demo", + "German": "farben", + "English": "colors", + "French": "couleurs" + }, + "28": { + "type": "function", + "name": "strobe", + "German": "strobo", + "English": "strobe", + "French": "strobe" + }, + "29": { + "type": "function", + "name": "clock 3", + "German": "uhr 3", + "English": "clock 3", + "French": "horloge 3" + }, + "30": { + "type": "function", + "name": "clock 4", + "German": "uhr 4", + "English": "clock 4", + "French": "horloge 4" + }, + "31": { + "type": "function", + "name": "clock 5", + "German": "uhr 5", + "English": "clock 5", + "French": "horloge 5" + }, + "32": { + "type": "function", + "name": "demo 2", + "German": "farben 2", + "English": "colors 2", + "French": "couleurs 2" + }, + "33": { + "type": "function", + "name": "clock 6", + "German": "uhr 6", + "English": "clock 6", + "French": "horloge 6" + }, + "34": { + "type": "function", + "name": "clock 7", + "German": "uhr 7", + "English": "clock 7", + "French": "horloge 7" + }, + "35": { + "type": "function", + "name": "rainbow 2", + "German": "regenbogen 2", + "English": "rainbow 2", + "French": "arc en ciel 2" + }, + "36": { + "type": "function", + "name": "theater 2", + "German": "theater 2", + "English": "theater 2", + "French": "théâtre 2" + }, + "37": { + "type": "function", + "name": "standby", + "German": "Standby", + "English": "Standby", + "French": "Etre prêt" + } +} \ No newline at end of file diff --git a/config/dictionary.py b/config/dictionary.py deleted file mode 100644 index 613e535..0000000 --- a/config/dictionary.py +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - - -__author__ = "Thomas Kaulke" -__email__ = "kaulketh@gmail.com" -__maintainer__ = "Thomas Kaulke" -__status__ = "Production" -__doc__ = "Translations dictionary for labels and message texts." - -from logger import LOGGER - -assignment = {} -language: str = "" - -# text type constants -RUNNING = "Bot is running..." -CMD = "command" -MSG = "message" -DE = "de" -EN = "en" -FR = "fr" -NAME = "name" - -languages = { - DE: {NAME: "German"}, - EN: {NAME: "English"}, - FR: {NAME: "French"}, -} - -# noinspection SpellCheckingInspection -texts = { - MSG: { - 0: "m_wrong_id", - 1: "m_not_allowed", - 2: "m_pls_select", - 3: "m_called", - 4: "m_started", - 5: "m_rebooted", - 6: "m_rotated", - 7: "m_stopped", - 8: "m_standby", - 9: "m_stop_f", - 10: "m_killed", - 11: "m_updated" - }, - CMD: { - 0: "stop", - 1: "start", - 2: "advent", - 3: "candles", - 4: "clock 1", - 5: "clock 2", - 6: "rainbow", - 7: "theater", - 8: "red", - 9: "blue", - 10: "green", - 11: "yellow", - 12: "orange", - 13: "white", - 14: "violet", - 15: "demo", - 16: "strobe", - 17: "clock 3", - 18: "clock 4", - 19: "clock 5", - 20: "demo 2", - 21: "clock 6", - 22: "clock 7", - 23: "standby" - - }, - DE: { - 0: "`Hallo {1}, dies ist ein privater Bot!" - "\nID {0}, {2} {3} wurde geblockt.\nDanke für den Besuch!`", - 1: "* Nicht erlaubt für diesen Bot\\! *", - 2: "Bitte triff eine Auswahl, {0}.", - 3: "*{0}* läuft bis {1}", - 4: "Bot ist einsatzbereit.", - 5: "Gerät wird neu gestartet.", - 6: "Logrotate manuell ausgeführt.", - 7: "Bot pausiert!", - 8: "Standby: {0}\nNeustart um {1}", - 9: "{0} gestoppt.", - 10: "Kompletten Bot-Prozess abgebrochen!", - - 11: "stop", - 12: "start", - 13: "advent", - 14: "kerzen", - 15: "uhr 1", - 16: "uhr 2", - 17: "regenbogen", - 18: "theater", - 19: "rot", - 20: "blau", - 21: "grün", - 22: "gelb", - 23: "orange", - 24: "weiß", - 25: "violett", - 26: "farben", - 27: "strobe", - 28: "uhr 3", - 29: "uhr 4", - 30: "uhr 5", - 31: "farben 2", - 32: "uhr 6", - 33: "uhr 7", - 34: "Bot-Update, Reboot folgt.", - 35: "Standby" - - }, - EN: { - 0: "`Hello {1}, this is a private bot!" - "\nID {0}, {2} {3} has been blocked.\nThanks for visit!`", - 1: "* Not allowed for this bot\\! *", - 2: "Please make a suitable selection, {0}!", - 3: "*{0}* runs until {1}", - 4: "Bot ready for use.", - 5: "Device rebooted.", - 6: "Logrotate executed manually.", - 7: "Bot paused.", - 8: "Standby: {0}\nRestart at {1}", - 9: "{0} stopped.", - 10: "Bot process killed!", - - 11: "stop", - 12: "start", - 13: "advent", - 14: "candles", - 15: "clock 1", - 16: "clock 2", - 17: "rainbow", - 18: "theater", - 19: "red", - 20: "blue", - 21: "green", - 22: "yellow", - 23: "orange", - 24: "white", - 25: "violet", - 26: "colors", - 27: "strobe", - 28: "clock 3", - 29: "clock 4", - 30: "clock 5", - 31: "colors 2", - 32: "clock 6", - 33: "clock 7", - 34: "Bot update, reboot shortly.", - 35: "Standby" - - }, - FR: { - 0: "`Bonjour {1}, c'est un bot privé!" - "\nL'Id {0}, {2} {3} a été bloqué.\nMerci de votre visite!`", - 1: "* Non autorisé pour ce bot\\! *", - 2: "Veuillez faire une sélection, {0}.", - 3: "*{0}* s'exécute jusqu'à {1}", - 4: "Le bot est prêt à l'emploi.", - 5: "L'appareil est redémarré.", - 6: "Logrotate exécuté manuellement.", - 7: "Bot en pause!", - 8: "Veille: {0}\nRedémarrez à {1}", - 9: "{0} arrêté.", - 10: "Processus de bot complet annulé!", - - 11: "stop", - 12: "start", - 13: "advent", - 14: "chandelles", - 15: "montre 1", - 16: "montre 2", - 17: "arc en ciel", - 18: "théâtre", - 19: "rouge", - 20: "bleu", - 21: "vert", - 22: "jaune", - 23: "orange", - 24: "blanc", - 25: "violet", - 26: "couleurs", - 27: "strobe", - 28: "montre 3", - 29: "montre 4", - 30: "montre 5", - 31: "couleurs 2", - 32: "montre 6", - 33: "montre 7", - 34: "Mise à jour, le bot est redémarré.", - 35: "Etre prêt" - - } -} - - -def __assign_texts(lng_key: str): - """ - Assignment of the texts to the messages/commands according to language - - :param lng_key: language key - :return: None (setup global assignment of texts) - """ - - text_assignments = {} - LOGGER.debug( - f"Assign {languages[lng_key].get(NAME)} texts " - f"to messages and commands.") - - # Messages - for i in range(0, len(texts[MSG]) - 1): - text_assignments[texts[MSG].get(i)] = texts[lng_key].get(i) - # Commands - for i in range(0, len(texts[CMD]) - 1): - text_assignments[texts[CMD].get(i)] = \ - texts[lng_key].get(i + len(texts[MSG]) - 1) - # Message 'Update' - text_assignments[texts[MSG].get(11)] = texts[lng_key].get(34) - # Command 'Standby' - text_assignments[texts[CMD].get(23)] = texts[lng_key].get(35) - - languages[lng_key].update(text_assignments) - - global assignment - assignment = languages - LOGGER.debug(f"{languages[lng_key].get(NAME)} text library built.") - - -def build_text_libraries(): - LOGGER.debug("Initialize text libraries of possible languages.") - for k in languages.keys(): - __assign_texts(k) - - -def set_language(lng=EN): - """ - Set chat language, default = English. - - :param lng: language key (i.e. 'de') - :return: None (set up global chat language) - """ - - if lng not in assignment: - global language - LOGGER.warning( - f"Language key \'{lng}\' not found, set default chat language!") - language = EN - else: - language = lng - LOGGER.info( - f"Chat language was set to '{(assignment[language].get(NAME))}'.") - - -def get_texts(text_type): - """ - Get text translations. - - :param text_type: messages, commands - :return: list of texts - """ - global language, assignment - translations = [] - - def text_generator(_type: str): - for key in texts[_type].keys(): - txt = assignment[language].get( - texts[_type].get(key)).title() if _type == CMD \ - else assignment[language].get(texts[_type].get(key)) - newline, space = ("\n", " ") - LOGGER.debug( - f"{key}: {texts[_type].get(key)} = " - f"{txt.replace(newline, space)}" - - ) - yield txt - - try: - for t in text_generator(text_type): - translations.append(t) - return translations - except Exception as e: - LOGGER.error(f"Error while import from translations: {e}") - - -if __name__ == '__main__': - pass diff --git a/config/settings.py b/config/settings.py index 5370cfa..f723c75 100644 --- a/config/settings.py +++ b/config/settings.py @@ -6,11 +6,6 @@ __maintainer__ = "Thomas Kaulke" __status__ = "Production" -LANGUAGE = "en" # language keys: "de", "en", "fr" - -AUTO_REBOOT_ENABLED = False -AUTO_REBOOT_TIME = "01:30" - LED_COUNT = 24 # Number of LED pixels. LED_PIN = 18 # GPIO pin connected to the pixels (must support PWM!). LED_FREQ_HZ = 800_000 # LED signal frequency in hertz (usually 800khz) @@ -22,5 +17,12 @@ LED_CUT_OFF_MORNING = 7 # Hour to adjust to day brightness LED_CUT_OFF_NIGHT = 18 # Hour to adjust to night brightness +AUTO_REBOOT_ENABLED = False +AUTO_REBOOT_TIME = "00:30" +AUTO_START = False +LANGUAGE = "German" # language keys: "German", "English", "French" +RUNNING = "Bot is running..." +TRANSLATIONS = 'dictionary.json' + if __name__ == '__main__': pass diff --git a/control/__init__.py b/control/__init__.py index 0c8620e..db7a58f 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -6,38 +6,37 @@ __maintainer__ = "Thomas Kaulke" __status__ = "Production" -from functions import dictionary_functions +from functions import dictionary_of_functions, STOP_CMD from functions.effects import clear -from .iron import * +from logger import LOGGER from .light import LightFunction from .wreath import WREATH, wreath_setup ERROR = "Any error occurred: " - -stop_flag = None +flag = None clear(WREATH) -def get_stop_flag(): - global stop_flag - return stop_flag +def stopped(): + global flag + return flag -def set_stop_flag(flag): +def set_stop_flag(_flag): """ - Set a global stop_flag for stopping while loops. + Set a global stop flag for stopping while loops. - :type flag: bool + :type _flag: bool """ - global stop_flag - stop_flag = flag - LOGGER.debug(f"Stop flag set to {stop_flag}") + global flag + flag = _flag + LOGGER.debug(f"Stop flag: {flag}") def run_thread(func_name, request_id, bot): try: - f = dictionary_functions.get(func_name) + f = dictionary_of_functions.get(func_name) LOGGER.debug(f"Init thread for function: {f}") t = LightFunction( function=f, @@ -45,6 +44,7 @@ def run_thread(func_name, request_id, bot): name=func_name, request_id=request_id, bot=bot) + LOGGER.history(func_name) t.start() set_stop_flag(False) return t @@ -58,24 +58,28 @@ def stop_threads(): if t is not None and t.is_running: t.stop() set_stop_flag(True) + LOGGER.history(STOP_CMD) except Exception as e: LOGGER.error(f"{ERROR}{e}") set_stop_flag(False) - return stop_flag + return flag def off(): - peripheral_functions.get(0)() clear(WREATH) -peripheral_functions = {0: iron.close_the_eyes, - 1: iron.open_the_eyes, - 2: iron.blink_more_often, +def _pass(): + pass + + +peripheral_functions = {0: _pass, + 1: _pass, + 2: _pass, 3: off} -"""dictionary of some functions:\n -0 - to close Iron Man's eyes,\n -1 - to open Iron Man's eyes,\n -2 - to let Iron Man blink with eyes,\n +"""dictionary of peripheral functions:\n +0 - pass,\n +1 - pass,\n +2 - pass,\n 3 - to turn off all LEDs""" diff --git a/control/iron.py b/control/iron.py deleted file mode 100644 index c857bae..0000000 --- a/control/iron.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -__author__ = "Thomas Kaulke" -__email__ = "kaulketh@gmail.com" -__maintainer__ = "Thomas Kaulke" -__status__ = "Production" - -import os -import time - -from config import IP_MASK_IRONMAN, PORT_MASK_IRONMAN -from logger import LOGGER - - -class IronMan: - logger = LOGGER - IP = IP_MASK_IRONMAN - PORT = PORT_MASK_IRONMAN - - def __init__(self): - self.__ip = IronMan.IP - self.__port = IronMan.PORT - # noinspection HttpUrlsUsage - self.__curl = f"curl --silent --output nul " \ - f"http://{self.__ip}:{self.__port}" \ - f"/?mode=" - self.__on = "1 &" - self.__off = "0 &" - IronMan.logger.debug( - f"Initialize instance of {self.__class__.__name__}") - - def open(self): - IronMan.logger.debug("Iron Man opens the eyes.") - os.system(f"{self.__curl}{self.__on}") - - def close(self): - IronMan.logger.debug("Iron Man closes the eyes.") - os.system(f"{self.__curl}{self.__off}") - - def blink(self, times: int, opened: float, closed: float): - count = times - IronMan.logger.debug(f"Iron Man {count} times blinks with eyes.") - while count > 0: - os.system(f"{self.__curl}{self.__on}") - time.sleep(opened) - os.system(f"{self.__curl}{self.__off}") - time.sleep(closed) - count -= 1 - - -def close_the_eyes(): - IronMan().close() - - -def open_the_eyes(): - IronMan().open() - - -def blink_more_often(): - IronMan().blink(20, .07, .15) - - -def blink_w_eyes(times=5, opened=.5, closed=.25): - IronMan().blink(times, opened, closed) - - -if __name__ == '__main__': - pass diff --git a/control/light.py b/control/light.py index 7b07150..2195955 100644 --- a/control/light.py +++ b/control/light.py @@ -50,13 +50,9 @@ def run(self): p = self.__process LightFunction.threads.append(self) - import control - control.peripheral_functions.get(0)() - control.peripheral_functions.get(1)() while self.__do_run: time.sleep(1) # stop - control.peripheral_functions.get(0)() p.terminate() clear(self.__wreath) self.__wreath.setBrightness(LED_BRIGHTNESS) diff --git a/control/reboot.py b/control/reboot.py index ae62abd..de622ca 100644 --- a/control/reboot.py +++ b/control/reboot.py @@ -48,9 +48,9 @@ def run(self): f"{datetime.datetime.now().second}") while not self.__time_to_reboot()[0]: - AutoReboot.log.debug( - f"reboot time not yet reached, " - f"recheck in {self.__ONE_MINUTE} seconds") + # AutoReboot.log.debug( + # f"reboot time not yet reached, " + # f"recheck in {self.__ONE_MINUTE} seconds") time.sleep(self.__ONE_MINUTE) AutoReboot.log.debug(f"reboot takes place in 1 minute") time.sleep(self.__ONE_MINUTE) diff --git a/control/service.py b/control/service.py index 3e6f902..f77f98f 100644 --- a/control/service.py +++ b/control/service.py @@ -38,17 +38,20 @@ Clock 5: Red hour hand, blue minute hand and warm yellow second 'pendulum' over all LEDs. Clock 6: Similar Clock 4, but w/o seconds and Green and Blue as major colors. Clock 7: White minute hand from LED 1 to 12 in 5 min steps (right half of circle), blue hour hand from LED 12 to 24 (left half of circle) + Red, blue, green, white, yellow, orange, violet: All LEDs at once can switched to same color. + Colors: Switching simple colors in random time periods. + Colors II: Fading over simple colors in random time periods. + Rainbow: Rainbow animation with circular fading effect. + Rainbow II: Rainbow animation with chaser, included fading effect. + Theater: Extremely colorful animation with spinning and wiping effects. + Theater II: Another colorful animation with spinning effects. Advent: Advent calendar, works in Advent time only! For every day of December will one LED flicker like a candlelight. If it is Advent Sunday it flickers red. Should be time before December but in Advent period all LEDs are working as candle light. If it is other than Advent time LEDs will circle in orange as warning! Candles: Each LED simulates candlelight. - Rainbow: Rainbow animation with circular fading effect. - Theater: Extremely colorful animation with chaser, spinning and wiping effects. Strobe: Emitting brief and rapid flashes of white light in random frequency. - Colors: Switching simple colors in random time periods. - Colors 2: Fading over simple colors in random time periods. - Red, blue, green, white, yellow, orange, violet: All LEDs at once can switched to same color. - *Service menu:* - /Reboot ... + /Reboot device + /Restart bot application (running as service) /Info Information commit/release versions on GitHub, host name, IP, memory usage, disk usage, cpu load. /Update Force update from GitHub to the latest version of master branch. /Help This menu""" @@ -58,6 +61,7 @@ class Service: c_prefix = "- " c_info = "/Info" c_reboot = "/Reboot" + c_restart = "/Restart" c_update = "/Update" c_help = "/Help" @@ -69,9 +73,10 @@ def __init__(self, command: str = None, log_msg: str = None): self.__menu_header = f"{NAME} functions:" self.__menu_dictionary = { 0: self.c_reboot, - 1: self.c_info, - 2: self.c_update, - 3: self.c_help + 1: self.c_restart, + 2: self.c_info, + 3: self.c_update, + 4: self.c_help } self.__new_line = "\n" @@ -165,6 +170,11 @@ def reboot_device(log_msg: str = None): Service("sudo reboot", log_msg).execute_os_command() +def restart_service(log_msg: str = None): + Service("sudo systemctl restart ledpibot.service", + log_msg).execute_os_command() + + def system_info(): return Service().system_info diff --git a/functions/__init__.py b/functions/__init__.py index b8d8cd1..644c924 100644 --- a/functions/__init__.py +++ b/functions/__init__.py @@ -15,32 +15,37 @@ run_clock4, run_clock5, run_clock6, run_clock7 from .colors import run_blue, run_demo, run_demo2, run_green, run_orange, \ run_red, run_stroboscope, run_violet, run_white, run_yellow -from .effects import run_rainbow, run_theater +from .effects import run_rainbow, run_rainbow_chaser, run_rainbow_cycle, \ + run_theater +STOP_CMD = commands[0] -def build_dictionary(): + +def light_animations(): try: """ Ensure right order of functions, depends on the command order - in ~.config.dictionary.py !!!! + in ~.config """ - dictionary = {} - functions = [None, None, - # index 0 and 1 not needed, direct bot commands, as last - run_advent, run_candles, run_clock1, run_clock2, - run_rainbow, run_theater, run_red, run_blue, run_green, - run_yellow, run_orange, run_white, run_violet, run_demo, - run_stroboscope, run_clock3, run_clock4, run_clock5, - run_demo2, run_clock6, run_clock7, None] + dic = {} + funcs = [None, None, + # index 0 and 1 not needed, direct bot commands, also -1 + run_advent, run_candles, run_clock1, run_clock2, + run_rainbow_cycle, run_theater, run_red, run_blue, + run_green, run_yellow, run_orange, run_white, run_violet, + run_demo, run_stroboscope, run_clock3, run_clock4, + run_clock5, run_demo2, run_clock6, run_clock7, + run_rainbow, run_rainbow_chaser, + None] LOGGER.debug("Build functions dictionary.") for i in range(len(commands)): - f = functions[i] - dictionary[commands[i]] = f + f = funcs[i] + dic[commands[i]] = f LOGGER.debug(f'Add {i}: {f}') - return dictionary + return dic except Exception as ex: LOGGER.error(f"{ex}") -dictionary_functions = build_dictionary() +dictionary_of_functions = light_animations() diff --git a/functions/advent.py b/functions/advent.py index 1b0a885..0798715 100644 --- a/functions/advent.py +++ b/functions/advent.py @@ -12,14 +12,14 @@ from rpi_ws281x import * -from functions.candles import Candle +from functions.candles import Candles from functions.effects import Effect from logger import LOGGER class Advent: logger = LOGGER - CANDLE = Candle.RED, Candle.GREEN, Candle.BLUE + CANDLE = Candles.RED, Candles.GREEN, Candles.BLUE ADVENT = 255, 30, 0 # advent RGB ZERO = (210, 70, 0) # first LED NAD = [27, 28, 29, 30] # November advent days @@ -47,9 +47,9 @@ def __init__(self, light_wreath): self.__light_wreath = light_wreath Advent.logger.debug( f"Initialize instance of {self.__class__.__name__}") - from control import get_stop_flag, peripheral_functions + from control import stopped, peripheral_functions count = 1 - while not get_stop_flag(): + while not stopped(): year = datetime.datetime.now().year if Advent.advent_period(year): self.__calendar() diff --git a/functions/candles.py b/functions/candles.py index 09445ab..127f91b 100644 --- a/functions/candles.py +++ b/functions/candles.py @@ -14,7 +14,7 @@ from logger import LOGGER -class Candle: +class Candles: logger = LOGGER RED = 255 GREEN = 150 @@ -24,25 +24,25 @@ def __init__(self, fairy_lights, led_numbers): self.__fairy_lights = fairy_lights self.__leds = led_numbers - Candle.logger.debug( + Candles.logger.debug( f"Initialize instance of {self.__class__.__name__}") - from control import get_stop_flag - while not get_stop_flag(): + from control import stopped + while not stopped(): try: for i in range(self.__leds): percent = randint(7, 10) / 100 - color = Color(int(Candle.RED * percent), - int(Candle.GREEN * percent), - int(Candle.BLUE * percent)) + color = Color(int(Candles.RED * percent), + int(Candles.GREEN * percent), + int(Candles.BLUE * percent)) self.__fairy_lights.setPixelColor(i, color) self.__fairy_lights.show() time.sleep(randint(13, 15) / 100) except KeyboardInterrupt: - Candle.logger.warning("KeyboardInterrupt") + Candles.logger.warning("KeyboardInterrupt") exit() except Exception as e: - Candle.logger.error(f"Any error occurs: {e}") + Candles.logger.error(f"Any error occurs: {e}") exit() finally: import control @@ -50,7 +50,7 @@ def __init__(self, fairy_lights, led_numbers): def run_candles(light_wreath): - Candle(light_wreath, light_wreath.numPixels()) + Candles(light_wreath, light_wreath.numPixels()) if __name__ == '__main__': diff --git a/functions/clocks.py b/functions/clocks.py index bd7e662..2264e46 100644 --- a/functions/clocks.py +++ b/functions/clocks.py @@ -60,8 +60,8 @@ def __init__(self, fairy_lights: Adafruit_NeoPixel, clock: int): Clock.log.debug(f"Initialize instance of {self.__class__.__name__}") Clock.log.debug(f"Call: {inspect.stack()[1].function}") - from control import get_stop_flag - while not get_stop_flag(): + from control import stopped + while not stopped(): try: self.__h_hand, self.__m_hand, self.__s_hand = self.__hands self.__clock_types.get(self.__clock_type)() @@ -75,10 +75,11 @@ def __init__(self, fairy_lights: Adafruit_NeoPixel, clock: int): @property def __hands(self): + _div = 60 / self.__fairy_lights.numPixels() now = wreath_setup(self.__fairy_lights)[0] - second_value = int(round(now.second / 2.5)) - minute_value = int(now.minute / 2.5) - hour_value = int(int(now.hour) % 12 * 2) + second_value = int(now.second / _div) + minute_value = int(now.minute / _div) + hour_value = now.hour % 12 * 2 return hour_value, minute_value, second_value def __gic(self, red, green, blue, hand_range, intensity): diff --git a/functions/colors.py b/functions/colors.py index 43dd4e0..64ad29e 100644 --- a/functions/colors.py +++ b/functions/colors.py @@ -40,9 +40,12 @@ def __init__(self, fairy_lights: Adafruit_NeoPixel, color_key=None): } self.__color = None Colorizer.log.debug(f"Init instance of {self.__class__.__name__}.") - Colorizer.log.debug(f"Call: {inspect.stack()[1].function}") if color_key is not None: + Colorizer.log.debug(f"Run color '{color_key}'") self.run(color_key, None) + else: + Colorizer.log.debug( + f"Call function '{inspect.stack()[1].function}'") @property def all_colors(self): @@ -50,8 +53,8 @@ def all_colors(self): def __function_loop(self, function): Colorizer.log.debug(f"Running loop: {inspect.stack()[1].function}") - from control import get_stop_flag - while not get_stop_flag(): + from control import stopped + while not stopped(): function() clear(self.__fairy_lights) diff --git a/functions/effects.py b/functions/effects.py index 679b846..113746f 100644 --- a/functions/effects.py +++ b/functions/effects.py @@ -140,13 +140,14 @@ def wipe_second(self, color: Color, pivot=0, back_again=True): self.__leds.show() time.sleep(wait_ms) - # TODO: create new wipe effects (wandering empty pixel) and implement + +# TODO: create new wipe effects (wandering empty pixel) and implement def __loop(wreath, effect): Effect.log.debug(inspect.stack()[1].code_context) - from control import get_stop_flag - while not get_stop_flag(): + from control import stopped + while not stopped(): try: wreath_setup(wreath) return effect @@ -171,7 +172,18 @@ def clear(wreath): def run_rainbow(wreath): - __loop(wreath, Effect(wreath).rainbow_cycle(iterations=100)) + while True: + __loop(wreath, Effect(wreath).rainbow(iterations=1)) + + +def run_rainbow_cycle(wreath): + while True: + __loop(wreath, Effect(wreath).rainbow_cycle(iterations=1)) + + +def run_rainbow_chaser(wreath): + while True: + __loop(wreath, Effect(wreath).rainbow_chaser(iterations=1)) def run_theater(wreath): @@ -188,7 +200,7 @@ def run_theater(wreath): def wipe_second(wreath, color, pivot, back_again=True): try: - Effect.wipe_second(wreath, color, pivot, back_again) + Effect(wreath).wipe_second(color, pivot, back_again) except KeyboardInterrupt: Effect.log.warning("KeyboardInterrupt") exit() diff --git a/logger/__init__.py b/logger/__init__.py index 226dc8a..83bb6e6 100644 --- a/logger/__init__.py +++ b/logger/__init__.py @@ -6,5 +6,9 @@ "LedPi", ../logs/debug.log (if enabled), ../logs/info.log, -../logs/error.log. +../logs/error.log. +../logs/history.log. """ + +HISTORY = PreconfiguredLogger.HIS_LOG +LOGS = PreconfiguredLogger.log_folder diff --git a/logger/logger.py b/logger/logger.py index 32172f3..2842593 100644 --- a/logger/logger.py +++ b/logger/logger.py @@ -38,7 +38,8 @@ class _LoggerMeta(type, Singleton): DEB_LOG = f"{LOG_FOLDER}/debug.log" INF_LOG = f"{LOG_FOLDER}/info.log" ERR_LOG = f"{LOG_FOLDER}/error.log" - MAX_BYTE = 1024 * 1024 # 1MB + HIS_LOG = f"{LOG_FOLDER}/history.log" + MAX_BYTE = 1024 * 1024 * 3 # 3MB BACK_COUNT = 5 DAT_FMT = "%Y-%m-%d %H:%M:%S" @@ -49,7 +50,10 @@ class _LoggerMeta(type, Singleton): "[thread: %(threadName)s] %(message)s" DEB_FMT = "%(asctime)s %(name)s %(levelname)s " \ "%(pathname)s %(funcName)s(lnr.%(lineno)s) %(message)s" + HIS_FMT = "%(asctime)s %(name)s %(levelname)s %(message)s" + HISTORY = 39 # before error level + # noinspection PyProtectedMember def __init__(cls, *args, **kwargs): super(_LoggerMeta, cls).__init__(*args, **kwargs) cls.__name = _LoggerMeta.NAME @@ -59,6 +63,7 @@ def __init__(cls, *args, **kwargs): cls.__deb_log_file = _LoggerMeta.DEB_LOG cls.__inf_log_file = _LoggerMeta.INF_LOG cls.__err_log_file = _LoggerMeta.ERR_LOG + cls.__his_log_file = _LoggerMeta.HIS_LOG # check if exists or create log folder try: @@ -86,10 +91,16 @@ def __init__(cls, *args, **kwargs): cls.__handler_error = logging.handlers.RotatingFileHandler( os.path.join(cls.__this_folder, cls.__err_log_file), maxBytes=_LoggerMeta.MAX_BYTE, backupCount=_LoggerMeta.BACK_COUNT) + cls.__handler_history = logging.handlers.RotatingFileHandler( + os.path.join(cls.__this_folder, cls.__his_log_file), + maxBytes=_LoggerMeta.MAX_BYTE, backupCount=_LoggerMeta.BACK_COUNT) # set handler log levels cls.__handler_info.setLevel(logging.INFO) cls.__handler_error.setLevel(logging.ERROR) + # add custom log level + logging.addLevelName(_LoggerMeta.HISTORY, 'HISTORY') + cls.__handler_history.setLevel(_LoggerMeta.HISTORY) # create formatters and setup handlers cls.__format_info = \ @@ -98,15 +109,20 @@ def __init__(cls, *args, **kwargs): cls.__format_error = \ logging.Formatter(_LoggerMeta.ERR_FMT, datefmt=_LoggerMeta.DAT_FMT) + cls.__format_history = \ + logging.Formatter(_LoggerMeta.HIS_FMT, + datefmt=_LoggerMeta.DAT_FMT) cls.__handler_info.setFormatter(cls.__format_info) cls.__handler_error.setFormatter(cls.__format_error) + cls.__handler_history.setFormatter(cls.__format_history) # instantiate logger and add handler cls.__log_instance = logging.getLogger(cls.__name) cls.__log_instance.addHandler(cls.__handler_info) cls.__log_instance.addHandler(cls.__handler_error) + cls.__log_instance.addHandler(cls.__handler_history) # if debug.log enabled if cls.__debug_log: @@ -121,10 +137,18 @@ def __init__(cls, *args, **kwargs): cls.__handler_debug.setFormatter(cls.__format_debug) cls.__log_instance.addHandler(cls.__handler_debug) + # set custom log level + cls.__log_instance.history = \ + lambda msg, *ars: cls.__log_instance._log(cls.HISTORY, msg, ars) + @property def instance(cls): return cls.__log_instance + @property + def log_folder(cls): + return cls.__log_folder + class PreconfiguredLogger(metaclass=_LoggerMeta): pass