inverter: a/c input mode switching
This commit is contained in:
parent
4ed7e6859a
commit
765bb8d8c4
@ -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
|
||||
```
|
@ -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
|
@ -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
|
||||
|
@ -2,7 +2,8 @@ from .monitor import (
|
||||
ChargingEvent,
|
||||
InverterMonitor,
|
||||
BatteryState,
|
||||
BatteryPowerDirection
|
||||
BatteryPowerDirection,
|
||||
ACMode
|
||||
)
|
||||
from .inverter_wrapper import wrapper_instance
|
||||
from .util import beautify_table
|
||||
|
@ -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,6 +157,7 @@ class InverterMonitor(Thread):
|
||||
|
||||
logger.debug(f'got status: ac={ac}, solar={solar}, v={v}, pd={pd}')
|
||||
|
||||
if self.ac_mode == ACMode.GENERATOR:
|
||||
self.gen_charging_program(ac, solar, v, pd)
|
||||
|
||||
if not ac or pd != BatteryPowerDirection.CHARGING:
|
||||
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user