inverter: a/c input mode switching

This commit is contained in:
Evgeny Zinoviev 2022-08-30 15:57:58 +03:00
parent 4ed7e6859a
commit 765bb8d8c4
6 changed files with 185 additions and 18 deletions

View File

@ -14,6 +14,14 @@ notify_users = [ 1, 2 ]
host = "127.0.0.1" host = "127.0.0.1"
port = 8305 port = 8305
[ac_mode.generator]
thresholds = [51, 58]
initial_current = 2
[ac_mode.utilities]
thresholds = [48, 54]
initial_current = 40
[monitor] [monitor]
vlow = 47 vlow = 47
vcrit = 45 vcrit = 45
@ -71,6 +79,7 @@ calcwadv - Advanced watts usage calculator
setbatuv - Set battery under voltage setbatuv - Set battery under voltage
setgencc - Set AC charging current setgencc - Set AC charging current
setgenct - Set AC charging thresholds setgenct - Set AC charging thresholds
setacmode - Set AC input mode
monstatus - Monitor: dump state monstatus - Monitor: dump state
monsetcur - Monitor: set charging currents monsetcur - Monitor: set charging currents
``` ```

View File

@ -1,6 +1,6 @@
from .reporting import ReportingHelper from .reporting import ReportingHelper
from .lang import LangPack 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 .store import Store
from .errors import * from .errors import *
from .util import command_usage, user_any_name from .util import command_usage, user_any_name

View File

@ -35,7 +35,7 @@ languages = {
'en': 'English', 'en': 'English',
'ru': 'Русский' 'ru': 'Русский'
} }
LANG_STARTED = range(1) LANG_STARTED, = range(1)
user_filter: Optional[BaseFilter] = None user_filter: Optional[BaseFilter] = None
@ -47,7 +47,7 @@ def default_langpack() -> LangPack:
cancel="Cancel", cancel="Cancel",
select_language="Select language on the keyboard.", select_language="Select language on the keyboard.",
invalid_language="Invalid language. Please try again.", invalid_language="Invalid language. Please try again.",
language_saved='Saved.', saved='Saved.',
) )
lang.ru( lang.ru(
start_message="Выберите команду на клавиатуре.", start_message="Выберите команду на клавиатуре.",
@ -55,7 +55,7 @@ def default_langpack() -> LangPack:
cancel="Отмена", cancel="Отмена",
select_language="Выберите язык на клавиатуре.", select_language="Выберите язык на клавиатуре.",
invalid_language="Неверный язык. Пожалуйста, попробуйте снова", invalid_language="Неверный язык. Пожалуйста, попробуйте снова",
language_saved="Настройки сохранены." saved="Настройки сохранены."
) )
return lang return lang
@ -183,11 +183,12 @@ class Wrapper:
lang: LangPack lang: LangPack
reporting: Optional[ReportingHelper] reporting: Optional[ReportingHelper]
def __init__(self): def __init__(self,
store: Optional[Store] = None):
self.updater = Updater(config['bot']['token'], self.updater = Updater(config['bot']['token'],
request_kwargs={'read_timeout': 6, 'connect_timeout': 7}) request_kwargs={'read_timeout': 6, 'connect_timeout': 7})
self.lang = default_langpack() self.lang = default_langpack()
self.store = Store() self.store = store if store else Store()
self.reporting = None self.reporting = None
init_user_filter() init_user_filter()
@ -346,11 +347,11 @@ class Wrapper:
break break
if lang is None: 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) 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) self.start(ctx)
return ConversationHandler.END return ConversationHandler.END

View File

@ -2,7 +2,8 @@ from .monitor import (
ChargingEvent, ChargingEvent,
InverterMonitor, InverterMonitor,
BatteryState, BatteryState,
BatteryPowerDirection BatteryPowerDirection,
ACMode
) )
from .inverter_wrapper import wrapper_instance from .inverter_wrapper import wrapper_instance
from .util import beautify_table from .util import beautify_table

View File

@ -47,6 +47,11 @@ class BatteryState(Enum):
CRITICAL = auto() CRITICAL = auto()
class ACMode(Enum):
GENERATOR = 'generator'
UTILITIES = 'utilities'
def _pd_from_string(pd: str) -> BatteryPowerDirection: def _pd_from_string(pd: str) -> BatteryPowerDirection:
if pd == 'Discharge': if pd == 'Discharge':
return BatteryPowerDirection.DISCHARGING return BatteryPowerDirection.DISCHARGING
@ -72,7 +77,6 @@ TODO:
- поддержать возможность бесшовного перезапуска бота, когда монитор понимает, что зарядка уже идет, и он - поддержать возможность бесшовного перезапуска бота, когда монитор понимает, что зарядка уже идет, и он
не запускает программу с начала, а продолжает с уже существующей позиции. Уведомления при этом можно не не запускает программу с начала, а продолжает с уже существующей позиции. Уведомления при этом можно не
присылать совсем, либо прислать какое-то одно приложение, в духе "программа была перезапущена" присылать совсем, либо прислать какое-то одно приложение, в духе "программа была перезапущена"
- баг: при отключении генератора бот не присылает никаких уведомлений, а должен
""" """
@ -87,6 +91,7 @@ class InverterMonitor(Thread):
self.interrupted = False self.interrupted = False
self.min_allowed_current = 0 self.min_allowed_current = 0
self.ac_mode = None
# Event handlers for the bot. # Event handlers for the bot.
self.charging_event_handler = None 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}') 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 not ac or pd != BatteryPowerDirection.CHARGING:
# if AC is disconnected or not charging, run the low voltage checking program # 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): def set_error_handler(self, handler: Callable):
self.error_handler = handler self.error_handler = handler
def set_ac_mode(self, mode: ACMode):
self.ac_mode = mode
def stop(self): def stop(self):
self.interrupted = True self.interrupted = True

View File

@ -6,9 +6,16 @@ import json
from inverterd import Format, InverterError from inverterd import Format, InverterError
from html import escape from html import escape
from typing import Optional, Tuple from typing import Optional, Tuple, Union
from home.config import config 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 ( from home.inverter import (
wrapper_instance as inverter, wrapper_instance as inverter,
beautify_table, beautify_table,
@ -16,10 +23,17 @@ from home.inverter import (
InverterMonitor, InverterMonitor,
ChargingEvent, ChargingEvent,
BatteryState, BatteryState,
ACMode
) )
from home.api.types import BotType from home.api.types import BotType
from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton 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 monitor: Optional[InverterMonitor] = None
bot: Optional[Wrapper] = None bot: Optional[Wrapper] = None
@ -225,6 +239,76 @@ def setgenct(ctx: Context) -> None:
}, language=ctx.user_lang)) }, 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: def setbatuv(ctx: Context) -> None:
try: try:
v = float(ctx.args[0]) v = float(ctx.args[0])
@ -297,8 +381,8 @@ def button_callback(ctx: Context) -> None:
class InverterBot(Wrapper): class InverterBot(Wrapper):
def __init__(self): def __init__(self, **kwargs):
super().__init__() super().__init__(**kwargs)
self.lang.ru( self.lang.ru(
status='Статус', status='Статус',
@ -306,8 +390,11 @@ class InverterBot(Wrapper):
battery="АКБ", battery="АКБ",
load="Нагрузка", load="Нагрузка",
generator="Генератор", generator="Генератор",
utilities="Столб",
done="Готово", done="Готово",
unexpected_callback_data="Ошибка: неверные данные", unexpected_callback_data="Ошибка: неверные данные",
select_ac_mode="Выберите режим:",
invalid_input="Неверное значение",
flags_press_button='Нажмите кнопку для переключения настройки', flags_press_button='Нажмите кнопку для переключения настройки',
flags_fail='Не удалось установить настройку', 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>)', battery_level_changed='Уровень заряда АКБ: <b>%s %s</b> (<b>%0.1f V</b> при нагрузке <b>%d W</b>)',
error_message='<b>Ошибка:</b> %s.', 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_normal='Нормальный',
bat_state_low='Низкий', bat_state_low='Низкий',
bat_state_critical='Критический', bat_state_critical='Критический',
@ -363,8 +453,11 @@ class InverterBot(Wrapper):
battery="Battery", battery="Battery",
load="Load", load="Load",
generator="Generator", generator="Generator",
utilities="Utilities",
done="Done", done="Done",
unexpected_callback_data="Unexpected callback data", 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_press_button='Press a button to toggle a flag.',
flags_fail='Failed to toggle 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)', 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.', 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_normal='Normal',
bat_state_low='Low', bat_state_low='Low',
bat_state_critical='Critical', bat_state_critical='Critical',
@ -432,6 +528,22 @@ class InverterBot(Wrapper):
self.add_handler(CallbackQueryHandler(self.wrap(button_callback))) 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]: def markup(self, ctx: Optional[Context]) -> Optional[ReplyKeyboardMarkup]:
button = [ button = [
[ctx.lang('status'), ctx.lang('generation')] [ctx.lang('status'), ctx.lang('generation')]
@ -449,18 +561,53 @@ class InverterBot(Wrapper):
return True 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__': if __name__ == '__main__':
config.load('inverter_bot') config.load('inverter_bot')
inverter.init(host=config['inverter']['ip'], port=config['inverter']['port']) inverter.init(host=config['inverter']['ip'], port=config['inverter']['port'])
db = InverterStore()
monitor = InverterMonitor() monitor = InverterMonitor()
monitor.set_charging_event_handler(monitor_charging) monitor.set_charging_event_handler(monitor_charging)
monitor.set_battery_event_handler(monitor_battery) monitor.set_battery_event_handler(monitor_battery)
monitor.set_error_handler(monitor_error) monitor.set_error_handler(monitor_error)
monitor.start() monitor.start()
bot = InverterBot() setacmode(ACMode(db.get_param('ac_mode', default=ACMode.GENERATOR)))
bot = InverterBot(store=db)
bot.enable_logging(BotType.INVERTER) bot.enable_logging(BotType.INVERTER)
bot.run() bot.run()