aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2023-06-11 03:20:01 +0300
committerEvgeny Zinoviev <me@ch1p.io>2023-06-11 03:20:01 +0300
commit0109d6c01db94822757cd7cb84034dd6f4d6cea8 (patch)
treec5551dc0839d45251dff06001341375f209d9ba6
parent975d2bc6ed6d588187fea4bb538e04ac30cbd989 (diff)
inverter bot: migrate to PTB 20 (not tested yet)
-rwxr-xr-xbin/inverter_bot.py243
-rw-r--r--include/py/homekit/inverter/monitor.py2
-rw-r--r--include/py/homekit/telegram/bot.py20
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