summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2021-10-25 00:15:36 +0300
committerEvgeny Zinoviev <me@ch1p.io>2021-10-25 00:15:36 +0300
commit1815f4be372b44c1ac738e01b59eec5db538c22f (patch)
tree2f0ad47792e919b32ad44136a31211ceeb50aa14
parent3efd89fe82adc7d072d9b24e054d6c9e398879c4 (diff)
nice upgrade
-rwxr-xr-xinverter-bot233
-rw-r--r--strings.py13
2 files changed, 180 insertions, 66 deletions
diff --git a/inverter-bot b/inverter-bot
index a56e9fe..de7d99e 100755
--- a/inverter-bot
+++ b/inverter-bot
@@ -2,16 +2,18 @@
import logging, re, datetime, json
from inverterd import Format, Client as InverterClient, InverterError
-from typing import Optional
+from typing import Optional, Tuple
from argparse import ArgumentParser
from html import escape
-from pprint import pprint
-from time import sleep
+# from pprint import pprint
+# from time import sleep
from strings import lang as _
from telegram import (
Update,
ParseMode,
KeyboardButton,
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
ReplyKeyboardMarkup
)
from telegram.ext import (
@@ -19,11 +21,25 @@ from telegram.ext import (
Filters,
CommandHandler,
MessageHandler,
- CallbackContext
+ CallbackContext,
+ CallbackQueryHandler
)
from telegram.error import TimedOut
+LT = escape('<=')
+flags_map = {
+ 'buzzer': 'BUZZ',
+ 'overload_bypass': 'OLBP',
+ 'escape_to_default_screen_after_1min_timeout': 'LCDE',
+ 'overload_restart': 'OLRS',
+ 'over_temp_restart': 'OTRS',
+ 'backlight_on': 'BLON',
+ 'alarm_on_on_primary_source_interrupt': 'ALRM',
+ 'fault_code_record': 'FTCR',
+}
+
+
class InverterClientWrapper:
def __init__(self, host: str, port: str):
self._host = host
@@ -39,7 +55,10 @@ class InverterClientWrapper:
def exec(self, command: str, arguments: tuple = (), format=Format.JSON):
try:
self._inverter.format(format)
- return self._inverter.exec(command, arguments)
+ response = self._inverter.exec(command, arguments)
+ if format == Format.JSON:
+ response = json.loads(response)
+ return response
except InverterError as e:
raise e
except Exception as e:
@@ -59,24 +78,49 @@ inverter: Optional[InverterClientWrapper] = None
#
+def get_usage(command: str, arguments: dict) -> str:
+ blocks = []
+ argument_names = []
+ argument_lines = []
+ for k, v in arguments.items():
+ argument_names.append(k)
+ argument_lines.append(
+ f'<code>{k}</code>: {v}'
+ )
+
+ command = f'/{command}'
+ if argument_names:
+ command += ' ' + ' '.join(argument_names)
+
+ blocks.append(
+ '<b>Usage</b>\n'
+ f'<code>{command}</code>'
+ )
+
+ if argument_lines:
+ blocks.append(
+ '<b>Arguments</b>\n' + '\n'.join(argument_lines)
+ )
+
+ return '\n\n'.join(blocks)
+
+
def get_markup() -> ReplyKeyboardMarkup:
button = [
[
_('status'),
_('generation')
],
- [
- _('gs'),
- _('ri'),
- _('errors')
- ]
]
return ReplyKeyboardMarkup(button, one_time_keyboard=False)
-def reply(update: Update, text: str) -> None:
+def reply(update: Update, text: str, reply_markup=None) -> None:
+ if reply_markup is None:
+ reply_markup = get_markup()
+
update.message.reply_text(text,
- reply_markup=get_markup(),
+ reply_markup=reply_markup,
parse_mode=ParseMode.HTML)
@@ -101,17 +145,19 @@ def beautify_table(s):
lines = list(map(lambda line: re.sub(r'(.*?): (.*)', r'<b>\1:</b> \2', line), lines))
return '\n'.join(lines)
+
#
# command/message handlers
#
+
def start(update: Update, context: CallbackContext) -> None:
reply(update, 'Select a command on the keyboard.')
def msg_status(update: Update, context: CallbackContext) -> None:
try:
- gs = json.loads(inverter.exec('get-status'))['data']
+ gs = inverter.exec('get-status')['data']
# render response
power_direction = gs['battery_power_direction'].lower()
@@ -151,20 +197,20 @@ def msg_generation(update: Update, context: CallbackContext) -> None:
yday = today - datetime.timedelta(days=1)
yday2 = today - datetime.timedelta(days=2)
- gs = json.loads(inverter.exec('get-status'))['data']
+ gs = inverter.exec('get-status')['data']
# sleep(0.1)
- gen_today = json.loads(inverter.exec('get-day-generated', (today.year, today.month, today.day)))['data']
+ gen_today = inverter.exec('get-day-generated', (today.year, today.month, today.day))['data']
gen_yday = None
gen_yday2 = None
if yday.month == today.month:
# sleep(0.1)
- gen_yday = json.loads(inverter.exec('get-day-generated', (yday.year, yday.month, yday.day)))['data']
+ gen_yday = inverter.exec('get-day-generated', (yday.year, yday.month, yday.day))['data']
if yday2.month == today.month:
# sleep(0.1)
- gen_yday2 = json.loads(inverter.exec('get-day-generated', (yday2.year, yday2.month, yday2.day)))['data']
+ gen_yday2 = inverter.exec('get-day-generated', (yday2.year, yday2.month, yday2.day))['data']
# render response
html = '<b>Input power:</b> %s %s' % (gs['pv1_input_power']['value'], gs['pv1_input_power']['unit'])
@@ -184,50 +230,26 @@ def msg_generation(update: Update, context: CallbackContext) -> None:
handle_exc(update, e)
-def msg_gs(update: Update, context: CallbackContext) -> None:
- try:
- status = inverter.exec('get-status', format=Format.TABLE)
- reply(update, beautify_table(status))
- except Exception as e:
- handle_exc(update, e)
-
-
-def msg_ri(update: Update, context: CallbackContext) -> None:
- try:
- rated = inverter.exec('get-rated', format=Format.TABLE)
- reply(update, beautify_table(rated))
- except Exception as e:
- handle_exc(update, e)
-
-
-def msg_errors(update: Update, context: CallbackContext) -> None:
- try:
- errors = inverter.exec('get-errors', format=Format.TABLE)
- reply(update, beautify_table(errors))
- except Exception as e:
- handle_exc(update, e)
-
-
def msg_all(update: Update, context: CallbackContext) -> None:
reply(update, "Command not recognized. Please try again.")
def on_set_ac_charging_current(update: Update, context: CallbackContext) -> None:
+ allowed_values = inverter.exec('get-allowed-ac-charging-currents')['data']
+
try:
current = int(context.args[0])
- allowed_values = json.loads(inverter.exec('get-allowed-ac-charging-currents'))['data']
-
if current not in allowed_values:
- raise ValueError(f'invalid value {current}, allowed values: ' + ', '.join(map(lambda x: str(x), allowed_values)))
+ raise ValueError(f'invalid value {current}')
- response = json.loads(inverter.exec('set-max-ac-charging-current', (0, current)))
+ response = inverter.exec('set-max-ac-charging-current', (0, current))
reply(update, 'OK' if response['result'] == 'ok' else 'ERROR')
- except IndexError:
- reply(update, escape('Usage: /setacchargingcurrent <current>'))
-
- except ValueError as e:
- handle_exc(update, e)
+ except (IndexError, ValueError):
+ usage = get_usage('setgencc', {
+ 'A': 'max charging current, allowed values: ' + ', '.join(map(lambda x: str(x), allowed_values))
+ })
+ reply(update, usage)
def on_set_ac_charging_thresholds(update: Update, context: CallbackContext) -> None:
@@ -236,15 +258,17 @@ def on_set_ac_charging_thresholds(update: Update, context: CallbackContext) -> N
dv = float(context.args[1])
if 44 <= cv <= 51 and 48 <= dv <= 58:
- response = json.loads(inverter.exec('set-charging-thresholds', (cv, dv)))
+ response = inverter.exec('set-charging-thresholds', (cv, dv))
reply(update, 'OK' if response['result'] == 'ok' else 'ERROR')
else:
raise ValueError('invalid values')
except (IndexError, ValueError):
- reply(update, escape('Usage: /setacchargingthresholds CV DV\n\n'
- '44 <= CV <= 51\n'
- '48 <= DV <= 58'))
+ usage = get_usage('setgenct', {
+ 'CV': f'charging voltage 44, {LT} CV {LT} 51',
+ 'DV': f'discharging voltage, 48 {LT} DV {LT} 58'
+ })
+ reply(update, usage)
def on_set_battery_under_voltage(update: Update, context: CallbackContext) -> None:
@@ -252,14 +276,93 @@ def on_set_battery_under_voltage(update: Update, context: CallbackContext) -> No
v = float(context.args[0])
if 40.0 <= v <= 48.0:
- response = json.loads(inverter.exec('set-battery-cut-off-voltage', (v,)))
+ response = inverter.exec('set-battery-cut-off-voltage', (v,))
reply(update, 'OK' if response['result'] == 'ok' else 'ERROR')
else:
raise ValueError('invalid voltage')
except (IndexError, ValueError):
- reply(update, escape('Usage: /setbatteryundervoltage VOLTAGE\n\n'
- 'VOLTAGE must be a floating point number between 40.0 and 48.0'))
+ usage = get_usage('setbatuv', {
+ 'V': f'floating point number, 40.0 {LT} V {LT} 48.0'
+ })
+ reply(update, usage)
+
+
+def build_flags_keyboard(flags: dict) -> Tuple[str, InlineKeyboardMarkup]:
+ keyboard = []
+ for k, v in flags.items():
+ label = ('✅' if v else '❌') + ' ' + _(f'flag_{k}')
+ proto_flag = flags_map[k]
+ keyboard.append([InlineKeyboardButton(label, callback_data=f'flag_{proto_flag}')])
+
+ text = 'Press a button to toggle a flag.'
+
+ return text, InlineKeyboardMarkup(keyboard)
+
+
+def on_flags(update: Update, context: CallbackContext) -> None:
+ flags = inverter.exec('get-flags')['data']
+ text, markup = build_flags_keyboard(flags)
+ reply(update, text, reply_markup=markup)
+
+
+def on_status(update: Update, context: CallbackContext) -> None:
+ try:
+ status = inverter.exec('get-status', format=Format.TABLE)
+ reply(update, beautify_table(status))
+ except Exception as e:
+ handle_exc(update, e)
+
+
+def on_config(update: Update, context: CallbackContext) -> None:
+ try:
+ rated = inverter.exec('get-rated', format=Format.TABLE)
+ reply(update, beautify_table(rated))
+ except Exception as e:
+ handle_exc(update, e)
+
+
+def on_errors(update: Update, context: CallbackContext) -> None:
+ try:
+ errors = inverter.exec('get-errors', format=Format.TABLE)
+ reply(update, beautify_table(errors))
+ except Exception as e:
+ handle_exc(update, e)
+
+
+def on_button(update: Update, context: CallbackContext) -> None:
+ query = update.callback_query
+
+ if query.data.startswith('flag_'):
+ flag = query.data[5:]
+ found = False
+ json_key = None
+ for k, v in flags_map.items():
+ if v == flag:
+ found = True
+ json_key = k
+ break
+ if not found:
+ query.answer('unknown flag')
+ return
+
+ flags = inverter.exec('get-flags')['data']
+ cur_flag_value = flags[json_key]
+ target_flag_value = '0' if cur_flag_value else '1'
+
+ # set flag
+ response = inverter.exec('set-flag', (flag, target_flag_value))
+
+ # notify user
+ query.answer('Done' if response['result'] == 'ok' else 'failed to toggle flag')
+
+ # edit message
+ flags[json_key] = not cur_flag_value
+ text, markup = build_flags_keyboard(flags)
+ query.edit_message_text(text, reply_markup=markup)
+
+ else:
+ query.answer('unexpected callback data')
if __name__ == '__main__':
@@ -291,13 +394,17 @@ if __name__ == '__main__':
dispatcher.add_handler(CommandHandler('start', start))
dispatcher.add_handler(MessageHandler(Filters.text(_('status')) & user_filter, msg_status))
dispatcher.add_handler(MessageHandler(Filters.text(_('generation')) & user_filter, msg_generation))
- dispatcher.add_handler(MessageHandler(Filters.text(_('gs')) & user_filter, msg_gs))
- dispatcher.add_handler(MessageHandler(Filters.text(_('ri')) & user_filter, msg_ri))
- dispatcher.add_handler(MessageHandler(Filters.text(_('errors')) & user_filter, msg_errors))
- dispatcher.add_handler(CommandHandler('setacchargingcurrent', on_set_ac_charging_current))
- dispatcher.add_handler(CommandHandler('setacchargingthresholds', on_set_ac_charging_thresholds))
- dispatcher.add_handler(CommandHandler('setbatteryundervoltage', on_set_battery_under_voltage))
+ dispatcher.add_handler(CommandHandler('setgencc', on_set_ac_charging_current))
+ dispatcher.add_handler(CommandHandler('setgenct', on_set_ac_charging_thresholds))
+ dispatcher.add_handler(CommandHandler('setbatuv', on_set_battery_under_voltage))
+
+ dispatcher.add_handler(CallbackQueryHandler(on_button))
+ dispatcher.add_handler(CommandHandler('flags', on_flags))
+ dispatcher.add_handler(CommandHandler('status', on_status))
+ dispatcher.add_handler(CommandHandler('config', on_config))
+ dispatcher.add_handler(CommandHandler('errors', on_errors))
+
dispatcher.add_handler(MessageHandler(Filters.all & user_filter, msg_all))
diff --git a/strings.py b/strings.py
index 065210a..0050217 100644
--- a/strings.py
+++ b/strings.py
@@ -1,9 +1,16 @@
__strings = {
'status': 'Status',
'generation': 'Generation',
- 'gs': 'GS',
- 'ri': 'RI',
- 'errors': 'Errors'
+
+ # flags
+ 'flag_buzzer': 'Buzzer',
+ 'flag_overload_bypass': 'Overload bypass',
+ 'flag_escape_to_default_screen_after_1min_timeout': 'Reset to default LCD page after 1min timeout',
+ 'flag_overload_restart': 'Restart on overload',
+ 'flag_over_temp_restart': 'Restart on overtemp',
+ 'flag_backlight_on': 'LCD backlight',
+ 'flag_alarm_on_on_primary_source_interrupt': 'Beep on primary source interrupt',
+ 'flag_fault_code_record': 'Fault code recording',
}