summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/inverter_bot.md9
-rw-r--r--src/home/bot/__init__.py4
-rw-r--r--src/home/bot/wrapper.py15
-rw-r--r--src/home/inverter/__init__.py3
-rw-r--r--src/home/inverter/monitor.py13
-rwxr-xr-xsrc/inverter_bot.py159
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()