diff options
-rw-r--r-- | doc/inverter_bot.md | 9 | ||||
-rw-r--r-- | src/home/bot/__init__.py | 4 | ||||
-rw-r--r-- | src/home/bot/wrapper.py | 15 | ||||
-rw-r--r-- | src/home/inverter/__init__.py | 3 | ||||
-rw-r--r-- | src/home/inverter/monitor.py | 13 | ||||
-rwxr-xr-x | src/inverter_bot.py | 159 |
6 files changed, 185 insertions, 18 deletions
diff --git a/doc/inverter_bot.md b/doc/inverter_bot.md index c9b299c..79e07d9 100644 --- a/doc/inverter_bot.md +++ b/doc/inverter_bot.md @@ -14,6 +14,14 @@ notify_users = [ 1, 2 ] host = "127.0.0.1" port = 8305 +[ac_mode.generator] +thresholds = [51, 58] +initial_current = 2 + +[ac_mode.utilities] +thresholds = [48, 54] +initial_current = 40 + [monitor] vlow = 47 vcrit = 45 @@ -71,6 +79,7 @@ calcwadv - Advanced watts usage calculator setbatuv - Set battery under voltage setgencc - Set AC charging current setgenct - Set AC charging thresholds +setacmode - Set AC input mode monstatus - Monitor: dump state monsetcur - Monitor: set charging currents ```
\ No newline at end of file diff --git a/src/home/bot/__init__.py b/src/home/bot/__init__.py index 0d93af3..41ad78e 100644 --- a/src/home/bot/__init__.py +++ b/src/home/bot/__init__.py @@ -1,6 +1,6 @@ from .reporting import ReportingHelper from .lang import LangPack -from .wrapper import Wrapper, Context, text_filter, handlermethod +from .wrapper import Wrapper, Context, text_filter, handlermethod, IgnoreMarkup from .store import Store from .errors import * -from .util import command_usage, user_any_name
\ No newline at end of file +from .util import command_usage, user_any_name diff --git a/src/home/bot/wrapper.py b/src/home/bot/wrapper.py index df7175e..98946ed 100644 --- a/src/home/bot/wrapper.py +++ b/src/home/bot/wrapper.py @@ -35,7 +35,7 @@ languages = { 'en': 'English', 'ru': 'Русский' } -LANG_STARTED = range(1) +LANG_STARTED, = range(1) user_filter: Optional[BaseFilter] = None @@ -47,7 +47,7 @@ def default_langpack() -> LangPack: cancel="Cancel", select_language="Select language on the keyboard.", invalid_language="Invalid language. Please try again.", - language_saved='Saved.', + saved='Saved.', ) lang.ru( start_message="Выберите команду на клавиатуре.", @@ -55,7 +55,7 @@ def default_langpack() -> LangPack: cancel="Отмена", select_language="Выберите язык на клавиатуре.", invalid_language="Неверный язык. Пожалуйста, попробуйте снова", - language_saved="Настройки сохранены." + saved="Настройки сохранены." ) return lang @@ -183,11 +183,12 @@ class Wrapper: lang: LangPack reporting: Optional[ReportingHelper] - def __init__(self): + def __init__(self, + store: Optional[Store] = None): self.updater = Updater(config['bot']['token'], request_kwargs={'read_timeout': 6, 'connect_timeout': 7}) self.lang = default_langpack() - self.store = Store() + self.store = store if store else Store() self.reporting = None init_user_filter() @@ -346,11 +347,11 @@ class Wrapper: break if lang is None: - ValueError('could not find the language') + raise ValueError('could not find the language') self.store.set_user_lang(ctx.user_id, lang) - ctx.reply(ctx.lang('language_saved'), markup=IgnoreMarkup()) + ctx.reply(ctx.lang('saved'), markup=IgnoreMarkup()) self.start(ctx) return ConversationHandler.END diff --git a/src/home/inverter/__init__.py b/src/home/inverter/__init__.py index b184580..374bc7b 100644 --- a/src/home/inverter/__init__.py +++ b/src/home/inverter/__init__.py @@ -2,7 +2,8 @@ from .monitor import ( ChargingEvent, InverterMonitor, BatteryState, - BatteryPowerDirection + BatteryPowerDirection, + ACMode ) from .inverter_wrapper import wrapper_instance from .util import beautify_table diff --git a/src/home/inverter/monitor.py b/src/home/inverter/monitor.py index 3835365..8d3220e 100644 --- a/src/home/inverter/monitor.py +++ b/src/home/inverter/monitor.py @@ -47,6 +47,11 @@ class BatteryState(Enum): CRITICAL = auto() +class ACMode(Enum): + GENERATOR = 'generator' + UTILITIES = 'utilities' + + def _pd_from_string(pd: str) -> BatteryPowerDirection: if pd == 'Discharge': return BatteryPowerDirection.DISCHARGING @@ -72,7 +77,6 @@ TODO: - поддержать возможность бесшовного перезапуска бота, когда монитор понимает, что зарядка уже идет, и он не запускает программу с начала, а продолжает с уже существующей позиции. Уведомления при этом можно не присылать совсем, либо прислать какое-то одно приложение, в духе "программа была перезапущена" -- баг: при отключении генератора бот не присылает никаких уведомлений, а должен """ @@ -87,6 +91,7 @@ class InverterMonitor(Thread): self.interrupted = False self.min_allowed_current = 0 + self.ac_mode = None # Event handlers for the bot. self.charging_event_handler = None @@ -152,7 +157,8 @@ class InverterMonitor(Thread): logger.debug(f'got status: ac={ac}, solar={solar}, v={v}, pd={pd}') - self.gen_charging_program(ac, solar, v, pd) + if self.ac_mode == ACMode.GENERATOR: + self.gen_charging_program(ac, solar, v, pd) if not ac or pd != BatteryPowerDirection.CHARGING: # if AC is disconnected or not charging, run the low voltage checking program @@ -440,6 +446,9 @@ class InverterMonitor(Thread): def set_error_handler(self, handler: Callable): self.error_handler = handler + def set_ac_mode(self, mode: ACMode): + self.ac_mode = mode + def stop(self): self.interrupted = True diff --git a/src/inverter_bot.py b/src/inverter_bot.py index 5ad5e33..c7801b4 100755 --- a/src/inverter_bot.py +++ b/src/inverter_bot.py @@ -6,9 +6,16 @@ import json from inverterd import Format, InverterError from html import escape -from typing import Optional, Tuple +from typing import Optional, Tuple, Union from home.config import config -from home.bot import Wrapper, Context, text_filter, command_usage +from home.bot import ( + Wrapper, + Context, + text_filter, + command_usage, + Store, + IgnoreMarkup +) from home.inverter import ( wrapper_instance as inverter, beautify_table, @@ -16,10 +23,17 @@ from home.inverter import ( InverterMonitor, ChargingEvent, BatteryState, + ACMode ) from home.api.types import BotType from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton -from telegram.ext import MessageHandler, CommandHandler, CallbackQueryHandler +from telegram.ext import ( + MessageHandler, + CommandHandler, + CallbackQueryHandler, + ConversationHandler, + Filters +) monitor: Optional[InverterMonitor] = None bot: Optional[Wrapper] = None @@ -225,6 +239,76 @@ def setgenct(ctx: Context) -> None: }, language=ctx.user_lang)) +SETACMODE_STARTED, = range(1) + + +def setacmode(mode: ACMode): + monitor.set_ac_mode(mode) + + cv, dv = config['ac_mode'][str(mode.value)]['thresholds'] + a = config['ac_mode'][str(mode.value)]['initial_current'] + + logger.debug(f'setacmode: mode={mode}, cv={cv}, dv={dv}, a={a}') + + inverter.exec('set-charging-thresholds', (cv, dv)) + inverter.exec('set-max-ac-charging-current', (0, a)) + + +def setacmode_start(ctx: Context) -> None: + if monitor.active_current is not None: + raise RuntimeError('generator charging program is active') + + buttons = [] + for mode in ACMode: + buttons.append(ctx.lang(str(mode.value))) + markup = ReplyKeyboardMarkup([buttons, [ctx.lang('cancel')]], one_time_keyboard=False) + + ctx.reply(ctx.lang('select_ac_mode'), markup=markup) + return SETACMODE_STARTED + + +def setacmode_input(ctx: Context): + if monitor.active_current is not None: + raise RuntimeError('generator charging program is active') + + if ctx.text == ctx.lang('utilities'): + newmode = ACMode.UTILITIES + elif ctx.text == ctx.lang('generator'): + newmode = ACMode.GENERATOR + else: + raise ValueError('invalid mode') + + # apply the mode + setacmode(newmode) + + # save + db.set_param('ac_mode', str(newmode.value)) + + # reply to user + ctx.reply(ctx.lang('saved'), markup=IgnoreMarkup()) + + # notify other users + bot.notify_all( + lambda lang: bot.lang.get('ac_mode_changed_notification', lang, + ctx.user.id, ctx.user.name, + bot.lang.get(str(newmode.value), lang)), + exclude=(ctx.user_id,) + ) + + bot.start(ctx) + return ConversationHandler.END + + +def setacmode_invalid(ctx: Context): + ctx.reply(ctx.lang('invalid_mode'), markup=IgnoreMarkup()) + return SETACMODE_STARTED + + +def setacmode_cancel(ctx: Context): + bot.start(ctx) + return ConversationHandler.END + + def setbatuv(ctx: Context) -> None: try: v = float(ctx.args[0]) @@ -297,8 +381,8 @@ def button_callback(ctx: Context) -> None: class InverterBot(Wrapper): - def __init__(self): - super().__init__() + def __init__(self, **kwargs): + super().__init__(**kwargs) self.lang.ru( status='Статус', @@ -306,8 +390,11 @@ class InverterBot(Wrapper): battery="АКБ", load="Нагрузка", generator="Генератор", + utilities="Столб", done="Готово", unexpected_callback_data="Ошибка: неверные данные", + select_ac_mode="Выберите режим:", + invalid_input="Неверное значение", flags_press_button='Нажмите кнопку для переключения настройки', flags_fail='Не удалось установить настройку', @@ -352,6 +439,9 @@ class InverterBot(Wrapper): battery_level_changed='Уровень заряда АКБ: <b>%s %s</b> (<b>%0.1f V</b> при нагрузке <b>%d W</b>)', error_message='<b>Ошибка:</b> %s.', + # other notifications + ac_mode_changed_notification='Пользователь <a href="tg://user?id=%d">%s</a> установил режим A/C: <b>%s</b>.', + bat_state_normal='Нормальный', bat_state_low='Низкий', bat_state_critical='Критический', @@ -363,8 +453,11 @@ class InverterBot(Wrapper): battery="Battery", load="Load", generator="Generator", + utilities="Utilities", done="Done", unexpected_callback_data="Unexpected callback data", + select_ac_mode="Select AC input mode:", + invalid_input="Invalid input", flags_press_button='Press a button to toggle a flag.', flags_fail='Failed to toggle flag', @@ -409,6 +502,9 @@ class InverterBot(Wrapper): battery_level_changed='Battery level: <b>%s</b> (<b>%0.1f V</b> under <b>%d W</b> load)', error_message='<b>Error:</b> %s.', + # other notifications + ac_mode_changed_notification='User <a href="tg://user?id=%d">%s</a> set A/C mode to <b>%s</b>.', + bat_state_normal='Normal', bat_state_low='Low', bat_state_critical='Critical', @@ -432,6 +528,22 @@ class InverterBot(Wrapper): self.add_handler(CallbackQueryHandler(self.wrap(button_callback))) + def run(self): + cancel_filter = Filters.text(self.lang.all('cancel')) + + self.add_handler(ConversationHandler( + entry_points=[CommandHandler('setacmode', self.wrap(setacmode_start), self.user_filter)], + states={ + SETACMODE_STARTED: [ + *[MessageHandler(text_filter(self.lang.all(mode.value)), self.wrap(setacmode_input)) for mode in ACMode], + MessageHandler(self.user_filter & ~cancel_filter, self.wrap(setacmode_invalid)) + ] + }, + fallbacks=[MessageHandler(self.user_filter & cancel_filter, self.wrap(setacmode_cancel))] + )) + + super().run() + def markup(self, ctx: Optional[Context]) -> Optional[ReplyKeyboardMarkup]: button = [ [ctx.lang('status'), ctx.lang('generation')] @@ -449,18 +561,53 @@ class InverterBot(Wrapper): return True +class InverterStore(Store): + SCHEMA = 2 + + def schema_init(self, version: int) -> None: + super().schema_init(version) + + if version < 2: + cursor = self.cursor() + cursor.execute("""CREATE TABLE IF NOT EXISTS params ( + id TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL + )""") + cursor.execute("CREATE INDEX param_id_idx ON params (id)") + self.commit() + + def get_param(self, key: str, default=None): + cursor = self.cursor() + cursor.execute('SELECT value FROM params WHERE id=?', (key,)) + row = cursor.fetchone() + + return default if row is None else row[0] + + def set_param(self, key: str, value: Union[str, int, float]): + cursor = self.cursor() + cursor.execute('REPLACE INTO params (id, value) VALUES (?, ?)', (key, str(value))) + self.commit() + + +db: Optional[InverterStore] = None + + if __name__ == '__main__': config.load('inverter_bot') inverter.init(host=config['inverter']['ip'], port=config['inverter']['port']) + db = InverterStore() + monitor = InverterMonitor() monitor.set_charging_event_handler(monitor_charging) monitor.set_battery_event_handler(monitor_battery) monitor.set_error_handler(monitor_error) monitor.start() - bot = InverterBot() + setacmode(ACMode(db.get_param('ac_mode', default=ACMode.GENERATOR))) + + bot = InverterBot(store=db) bot.enable_logging(BotType.INVERTER) bot.run() |