diff options
-rwxr-xr-x | bin/inverter_bot.py | 243 | ||||
-rw-r--r-- | include/py/homekit/inverter/monitor.py | 2 | ||||
-rw-r--r-- | include/py/homekit/telegram/bot.py | 20 |
3 files changed, 142 insertions, 123 deletions
diff --git a/bin/inverter_bot.py b/bin/inverter_bot.py index 7da21aa..032f513 100755 --- a/bin/inverter_bot.py +++ b/bin/inverter_bot.py @@ -5,6 +5,7 @@ import datetime import json import itertools import sys +import asyncio import __py_include from inverterd import Format, InverterError @@ -347,8 +348,11 @@ def monitor_charging(event: ChargingEvent, **kwargs) -> None: key = f'chrg_evt_{key}' if is_util: key = f'util_{key}' - bot.notify_all( - lambda lang: bot.lang.get(key, lang, *args) + + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get(key, lang, *args) + ) ) @@ -363,9 +367,11 @@ def monitor_battery(state: BatteryState, v: float, load_watts: int) -> None: logger.error('unknown battery state:', state) return - bot.notify_all( - lambda lang: bot.lang.get('battery_level_changed', lang, - emoji, bot.lang.get(f'bat_state_{state.name.lower()}', lang), v, load_watts) + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get('battery_level_changed', lang, + emoji, bot.lang.get(f'bat_state_{state.name.lower()}', lang), v, load_watts) + ) ) @@ -375,14 +381,18 @@ def monitor_util(event: ACPresentEvent): else: key = 'disconnected' key = f'util_{key}' - bot.notify_all( - lambda lang: bot.lang.get(key, lang) + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get(key, lang) + ) ) def monitor_error(error: str) -> None: - bot.notify_all( - lambda lang: bot.lang.get('error_message', lang, error) + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get('error_message', lang, error) + ) ) @@ -392,35 +402,37 @@ def osp_change_cb(new_osp: OutputSourcePriority, setosp(new_osp) - bot.notify_all( - lambda lang: bot.lang.get('osp_auto_changed_notification', lang, - bot.lang.get(f'settings_osp_{new_osp.value.lower()}', lang), v, solar_input), + asyncio.ensure_future( + bot.notify_all( + lambda lang: bot.lang.get('osp_auto_changed_notification', lang, + bot.lang.get(f'settings_osp_{new_osp.value.lower()}', lang), v, solar_input), + ) ) @bot.handler(command='status') -def full_status(ctx: bot.Context) -> None: +async def full_status(ctx: bot.Context) -> None: status = inverter.exec('get-status', format=Format.TABLE) - ctx.reply(beautify_table(status)) + await ctx.reply(beautify_table(status)) @bot.handler(command='config') -def full_rated(ctx: bot.Context) -> None: +async def full_rated(ctx: bot.Context) -> None: rated = inverter.exec('get-rated', format=Format.TABLE) - ctx.reply(beautify_table(rated)) + await ctx.reply(beautify_table(rated)) @bot.handler(command='errors') -def full_errors(ctx: bot.Context) -> None: +async def full_errors(ctx: bot.Context) -> None: errors = inverter.exec('get-errors', format=Format.TABLE) - ctx.reply(beautify_table(errors)) + await ctx.reply(beautify_table(errors)) @bot.handler(command='flags') -def flags_handler(ctx: bot.Context) -> None: +async def flags_handler(ctx: bot.Context) -> None: flags = inverter.exec('get-flags')['data'] text, markup = build_flags_keyboard(flags, ctx) - ctx.reply(text, markup=markup) + await ctx.reply(text, markup=markup) def build_flags_keyboard(flags: dict, ctx: bot.Context) -> Tuple[str, InlineKeyboardMarkup]: @@ -477,11 +489,11 @@ class SettingsConversation(bot.conversation): REDISCHARGE_VOLTAGES = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58] @bot.conventer(START, message='settings') - def start_enter(self, ctx: bot.Context): + async def start_enter(self, ctx: bot.Context): buttons = list(chunks(list(self.START_BUTTONS), 2)) buttons.reverse() - return self.reply(ctx, self.START, ctx.lang('settings_msg'), buttons, - with_cancel=True) + return await self.reply(ctx, self.START, ctx.lang('settings_msg'), buttons, + with_cancel=True) @bot.convinput(START, messages={ 'settings_osp': OSP, @@ -490,16 +502,16 @@ class SettingsConversation(bot.conversation): 'settings_bat_cut_off_voltage': BAT_CUT_OFF_VOLTAGE, 'settings_ac_max_charging_current': AC_MAX_CHARGING_CURRENT }) - def start_input(self, ctx: bot.Context): + async def start_input(self, ctx: bot.Context): pass @bot.conventer(OSP) - def osp_enter(self, ctx: bot.Context): - return self.reply(ctx, self.OSP, ctx.lang('settings_osp_msg'), self.OSP_BUTTONS, - with_back=True) + async def osp_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.OSP, ctx.lang('settings_osp_msg'), self.OSP_BUTTONS, + with_back=True) @bot.convinput(OSP, messages=OSP_BUTTONS) - def osp_input(self, ctx: bot.Context): + async def osp_input(self, ctx: bot.Context): selected_sp = None for sp in OutputSourcePriority: if ctx.text == ctx.lang(f'settings_osp_{sp.value.lower()}'): @@ -512,25 +524,28 @@ class SettingsConversation(bot.conversation): # apply the mode setosp(selected_sp) - # reply to user - ctx.reply(ctx.lang('saved'), markup=bot.IgnoreMarkup()) - - # notify other users - bot.notify_all( - lambda lang: bot.lang.get('osp_changed_notification', lang, - ctx.user.id, ctx.user.name, - bot.lang.get(f'settings_osp_{selected_sp.value.lower()}', lang)), - exclude=(ctx.user_id,) + await asyncio.gather( + # reply to user + ctx.reply(ctx.lang('saved'), markup=bot.IgnoreMarkup()), + + # notify other users + bot.notify_all( + lambda lang: bot.lang.get('osp_changed_notification', lang, + ctx.user.id, ctx.user.name, + bot.lang.get(f'settings_osp_{selected_sp.value.lower()}', lang)), + exclude=(ctx.user_id,) + ) ) + return self.END @bot.conventer(AC_PRESET) - def acpreset_enter(self, ctx: bot.Context): - return self.reply(ctx, self.AC_PRESET, ctx.lang('settings_ac_preset_msg'), self.AC_PRESET_BUTTONS, - with_back=True) + async def acpreset_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.AC_PRESET, ctx.lang('settings_ac_preset_msg'), self.AC_PRESET_BUTTONS, + with_back=True) @bot.convinput(AC_PRESET, messages=AC_PRESET_BUTTONS) - def acpreset_input(self, ctx: bot.Context): + async def acpreset_input(self, ctx: bot.Context): if monitor.active_current is not None: raise RuntimeError('generator charging program is active') @@ -547,85 +562,88 @@ class SettingsConversation(bot.conversation): # save bot.db.set_param('ac_mode', str(newmode.value)) - # reply to user - ctx.reply(ctx.lang('saved'), markup=bot.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,) + await asyncio.gather( + # reply to user + ctx.reply(ctx.lang('saved'), markup=bot.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,) + ) ) + return self.END @bot.conventer(BAT_THRESHOLDS_1) - def thresholds1_enter(self, ctx: bot.Context): + async def thresholds1_enter(self, ctx: bot.Context): buttons = list(map(lambda v: f'{v} V', self.RECHARGE_VOLTAGES)) buttons = chunks(buttons, 4) - return self.reply(ctx, self.BAT_THRESHOLDS_1, ctx.lang('settings_select_bottom_threshold'), buttons, - with_back=True, buttons_lang_completed=True) + return await self.reply(ctx, self.BAT_THRESHOLDS_1, ctx.lang('settings_select_bottom_threshold'), buttons, + with_back=True, buttons_lang_completed=True) @bot.convinput(BAT_THRESHOLDS_1, messages=list(map(lambda n: f'{n} V', RECHARGE_VOLTAGES)), messages_lang_completed=True) - def thresholds1_input(self, ctx: bot.Context): + async def thresholds1_input(self, ctx: bot.Context): v = self._parse_voltage(ctx.text) ctx.user_data['bat_thrsh_v1'] = v - return self.invoke(self.BAT_THRESHOLDS_2, ctx) + return await self.invoke(self.BAT_THRESHOLDS_2, ctx) @bot.conventer(BAT_THRESHOLDS_2) - def thresholds2_enter(self, ctx: bot.Context): + async def thresholds2_enter(self, ctx: bot.Context): buttons = list(map(lambda v: f'{v} V', self.REDISCHARGE_VOLTAGES)) buttons = chunks(buttons, 4) - return self.reply(ctx, self.BAT_THRESHOLDS_2, ctx.lang('settings_select_upper_threshold'), buttons, - with_back=True, buttons_lang_completed=True) + return await self.reply(ctx, self.BAT_THRESHOLDS_2, ctx.lang('settings_select_upper_threshold'), buttons, + with_back=True, buttons_lang_completed=True) @bot.convinput(BAT_THRESHOLDS_2, messages=list(map(lambda n: f'{n} V', REDISCHARGE_VOLTAGES)), messages_lang_completed=True) - def thresholds2_input(self, ctx: bot.Context): + async def thresholds2_input(self, ctx: bot.Context): v2 = v = self._parse_voltage(ctx.text) v1 = ctx.user_data['bat_thrsh_v1'] del ctx.user_data['bat_thrsh_v1'] response = inverter.exec('set-charge-thresholds', (v1, v2)) - ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', - markup=bot.IgnoreMarkup()) + await ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', + markup=bot.IgnoreMarkup()) return self.END @bot.conventer(AC_MAX_CHARGING_CURRENT) - def ac_max_enter(self, ctx: bot.Context): + async def ac_max_enter(self, ctx: bot.Context): buttons = self._get_allowed_ac_charge_amps() buttons = map(lambda n: f'{n} A', buttons) buttons = [list(buttons)] - return self.reply(ctx, self.AC_MAX_CHARGING_CURRENT, ctx.lang('settings_select_max_current'), buttons, - with_back=True, buttons_lang_completed=True) + return await self.reply(ctx, self.AC_MAX_CHARGING_CURRENT, ctx.lang('settings_select_max_current'), buttons, + with_back=True, buttons_lang_completed=True) @bot.convinput(AC_MAX_CHARGING_CURRENT, regex=r'^\d+ A$') - def ac_max_input(self, ctx: bot.Context): + async def ac_max_input(self, ctx: bot.Context): a = self._parse_amps(ctx.text) allowed = self._get_allowed_ac_charge_amps() if a not in allowed: raise ValueError('input is not allowed') response = inverter.exec('set-max-ac-charge-current', (0, a)) - ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', - markup=bot.IgnoreMarkup()) + await ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', + markup=bot.IgnoreMarkup()) return self.END @bot.conventer(BAT_CUT_OFF_VOLTAGE) - def cutoff_enter(self, ctx: bot.Context): - return self.reply(ctx, self.BAT_CUT_OFF_VOLTAGE, ctx.lang('settings_enter_cutoff_voltage'), None, - with_back=True) + async def cutoff_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.BAT_CUT_OFF_VOLTAGE, ctx.lang('settings_enter_cutoff_voltage'), None, + with_back=True) @bot.convinput(BAT_CUT_OFF_VOLTAGE, regex=r'^(\d{2}(\.\d{1})?)$') - def cutoff_input(self, ctx: bot.Context): + async def cutoff_input(self, ctx: bot.Context): v = float(ctx.text) if 40.0 <= v <= 48.0: response = inverter.exec('set-battery-cutoff-voltage', (v,)) - ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', - markup=bot.IgnoreMarkup()) + await ctx.reply(ctx.lang('saved') if response['result'] == 'ok' else 'ERROR', + markup=bot.IgnoreMarkup()) else: raise ValueError('invalid voltage') @@ -660,38 +678,38 @@ class ConsumptionConversation(bot.conversation): INTERVAL_BUTTONS_FLAT = list(itertools.chain.from_iterable(INTERVAL_BUTTONS)) @bot.conventer(START, message='consumption') - def start_enter(self, ctx: bot.Context): - return self.reply(ctx, self.START, ctx.lang('consumption_msg'), [self.START_BUTTONS], - with_cancel=True) + async def start_enter(self, ctx: bot.Context): + return await self.reply(ctx, self.START, ctx.lang('consumption_msg'), [self.START_BUTTONS], + with_cancel=True) @bot.convinput(START, messages={ 'consumption_total': TOTAL, 'consumption_grid': GRID }) - def start_input(self, ctx: bot.Context): + async def start_input(self, ctx: bot.Context): pass @bot.conventer(TOTAL) - def total_enter(self, ctx: bot.Context): - return self._render_interval_btns(ctx, self.TOTAL) + async def total_enter(self, ctx: bot.Context): + return await self._render_interval_btns(ctx, self.TOTAL) @bot.conventer(GRID) - def grid_enter(self, ctx: bot.Context): - return self._render_interval_btns(ctx, self.GRID) + async def grid_enter(self, ctx: bot.Context): + return await self._render_interval_btns(ctx, self.GRID) - def _render_interval_btns(self, ctx: bot.Context, state): - return self.reply(ctx, state, ctx.lang('consumption_select_interval'), self.INTERVAL_BUTTONS, - with_back=True) + async def _render_interval_btns(self, ctx: bot.Context, state): + return await self.reply(ctx, state, ctx.lang('consumption_select_interval'), self.INTERVAL_BUTTONS, + with_back=True) @bot.convinput(TOTAL, messages=INTERVAL_BUTTONS_FLAT) - def total_input(self, ctx: bot.Context): - return self._render_interval_results(ctx, self.TOTAL) + async def total_input(self, ctx: bot.Context): + return await self._render_interval_results(ctx, self.TOTAL) @bot.convinput(GRID, messages=INTERVAL_BUTTONS_FLAT) - def grid_input(self, ctx: bot.Context): - return self._render_interval_results(ctx, self.GRID) + async def grid_input(self, ctx: bot.Context): + return await self._render_interval_results(ctx, self.GRID) - def _render_interval_results(self, ctx: bot.Context, state): + async def _render_interval_results(self, ctx: bot.Context, state): # if ctx.text == ctx.lang('to_select_interval'): # TODO # pass @@ -715,41 +733,43 @@ class ConsumptionConversation(bot.conversation): # [InlineKeyboardButton(ctx.lang('please_wait'), callback_data='wait')] # ]) - message = ctx.reply(ctx.lang('consumption_request_sent'), - markup=bot.IgnoreMarkup()) + message = await ctx.reply(ctx.lang('consumption_request_sent'), + markup=bot.IgnoreMarkup()) api = WebApiClient(timeout=60) method = 'inverter_get_consumed_energy' if state == self.TOTAL else 'inverter_get_grid_consumed_energy' try: wh = getattr(api, method)(s_from, s_to) - bot.delete_message(message.chat_id, message.message_id) - ctx.reply('%.2f Wh' % (wh,), - markup=bot.IgnoreMarkup()) + await bot.delete_message(message.chat_id, message.message_id) + await ctx.reply('%.2f Wh' % (wh,), + markup=bot.IgnoreMarkup()) return self.END except Exception as e: - bot.delete_message(message.chat_id, message.message_id) - ctx.reply_exc(e) + await asyncio.gather( + bot.delete_message(message.chat_id, message.message_id), + ctx.reply_exc(e) + ) # other # ----- @bot.handler(command='monstatus') -def monstatus_handler(ctx: bot.Context) -> None: +async def monstatus_handler(ctx: bot.Context) -> None: msg = '' st = monitor.dump_status() for k, v in st.items(): msg += k + ': ' + str(v) + '\n' - ctx.reply(msg) + await ctx.reply(msg) @bot.handler(command='monsetcur') -def monsetcur_handler(ctx: bot.Context) -> None: - ctx.reply('not implemented yet') +async def monsetcur_handler(ctx: bot.Context) -> None: + await ctx.reply('not implemented yet') @bot.callbackhandler -def button_callback(ctx: bot.Context) -> None: +async def button_callback(ctx: bot.Context) -> None: query = ctx.callback_query if query.data.startswith('flag_'): @@ -762,7 +782,7 @@ def button_callback(ctx: bot.Context) -> None: json_key = k break if not found: - query.answer(ctx.lang('flags_invalid')) + await query.answer(ctx.lang('flags_invalid')) return flags = inverter.exec('get-flags')['data'] @@ -773,32 +793,31 @@ def button_callback(ctx: bot.Context) -> None: response = inverter.exec('set-flag', (flag, target_flag_value)) # notify user - query.answer(ctx.lang('done') if response['result'] == 'ok' else ctx.lang('flags_fail')) + await query.answer(ctx.lang('done') if response['result'] == 'ok' else ctx.lang('flags_fail')) # edit message flags[json_key] = not cur_flag_value text, markup = build_flags_keyboard(flags, ctx) - query.edit_message_text(text, reply_markup=markup) + await query.edit_message_text(text, reply_markup=markup) else: - query.answer(ctx.lang('unexpected_callback_data')) + await query.answer(ctx.lang('unexpected_callback_data')) @bot.exceptionhandler -def exception_handler(e: Exception, ctx: bot.Context) -> Optional[bool]: +async def exception_handler(e: Exception, ctx: bot.Context) -> Optional[bool]: if isinstance(e, InverterError): try: err = json.loads(str(e))['message'] except json.decoder.JSONDecodeError: err = str(e) err = re.sub(r'((?:.*)?error:) (.*)', r'<b>\1</b> \2', err) - ctx.reply(err, - markup=bot.IgnoreMarkup()) + await ctx.reply(err, markup=bot.IgnoreMarkup()) return True @bot.handler(message='status') -def status_handler(ctx: bot.Context) -> None: +async def status_handler(ctx: bot.Context) -> None: gs = inverter.exec('get-status')['data'] rated = inverter.exec('get-rated')['data'] @@ -842,11 +861,11 @@ def status_handler(ctx: bot.Context) -> None: html += f'\n<b>{ctx.lang("priority")}</b>: {rated["output_source_priority"]}' # send response - ctx.reply(html) + await ctx.reply(html) @bot.handler(message='generation') -def generation_handler(ctx: bot.Context) -> None: +async def generation_handler(ctx: bot.Context) -> None: today = datetime.date.today() yday = today - datetime.timedelta(days=1) yday2 = today - datetime.timedelta(days=2) @@ -876,7 +895,7 @@ def generation_handler(ctx: bot.Context) -> None: html += f'\n<b>{ctx.lang("yday2")}:</b> %s Wh' % (gen_yday2['wh']) # send response - ctx.reply(html) + await ctx.reply(html) @bot.defaultreplymarkup diff --git a/include/py/homekit/inverter/monitor.py b/include/py/homekit/inverter/monitor.py index 86f75ac..5955d92 100644 --- a/include/py/homekit/inverter/monitor.py +++ b/include/py/homekit/inverter/monitor.py @@ -25,7 +25,7 @@ def _pd_from_string(pd: str) -> BatteryPowerDirection: class MonitorConfig: def __getattr__(self, item): - return config['monitor'][item] + return config.app_config['monitor'][item] cfg = MonitorConfig() diff --git a/include/py/homekit/telegram/bot.py b/include/py/homekit/telegram/bot.py index 8a78c6f..2efd9e4 100644 --- a/include/py/homekit/telegram/bot.py +++ b/include/py/homekit/telegram/bot.py @@ -274,7 +274,7 @@ class conversation: continue cd = f.__dict__['_conv_data'] if cd['enter'] and cd['state'] == state: - return cd['orig_f'](self, ctx) + return await cd['orig_f'](self, ctx) raise RuntimeError(f'invoke: failed to find method for state {state}') @@ -362,14 +362,14 @@ class conversation: # buttons.insert(0, [ctx.lang('back')]) buttons.append([ctx.lang('back')]) - def reply(self, - ctx: Context, - state: Union[int, Enum], - text: str, - buttons: Optional[list], - with_cancel=False, - with_back=False, - buttons_lang_completed=False): + async def reply(self, + ctx: Context, + state: Union[int, Enum], + text: str, + buttons: Optional[list], + with_cancel=False, + with_back=False, + buttons_lang_completed=False): if buttons: new_buttons = [] @@ -400,7 +400,7 @@ class conversation: self.add_back_button(ctx, new_buttons) markup = ReplyKeyboardMarkup(new_buttons, one_time_keyboard=True) if new_buttons else IgnoreMarkup() - ctx.reply(text, markup=markup) + await ctx.reply(text, markup=markup) self.set_user_state(ctx.user_id, state) return state |