summaryrefslogtreecommitdiff
path: root/src/inverter_bot.py
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2023-06-10 23:02:34 +0300
committerEvgeny Zinoviev <me@ch1p.io>2023-06-10 23:02:34 +0300
commitb0bf43e6a272d42a55158e657bd937cb82fc3d8d (patch)
treef1bc13253bc028abcaed9c88882f5aee384a269c /src/inverter_bot.py
parentf3b9d50496257d87757802dfb472b5ffae11962c (diff)
move files, rename home package to homekit
Diffstat (limited to 'src/inverter_bot.py')
-rwxr-xr-xsrc/inverter_bot.py943
1 files changed, 0 insertions, 943 deletions
diff --git a/src/inverter_bot.py b/src/inverter_bot.py
deleted file mode 100755
index 1dd167e..0000000
--- a/src/inverter_bot.py
+++ /dev/null
@@ -1,943 +0,0 @@
-#!/usr/bin/env python3
-import logging
-import re
-import datetime
-import json
-import itertools
-import sys
-
-from inverterd import Format, InverterError
-from html import escape
-from typing import Optional, Tuple, Union
-
-from home.util import chunks
-from home.config import config, AppConfigUnit
-from home.telegram import bot
-from home.telegram.config import TelegramBotConfig, TelegramUserListType
-from home.inverter import (
- wrapper_instance as inverter,
- beautify_table,
- InverterMonitor,
-)
-from home.inverter.types import (
- ChargingEvent,
- ACPresentEvent,
- BatteryState,
- ACMode,
- OutputSourcePriority
-)
-from home.database.inverter_time_formats import FormatDate
-from home.api.types import BotType
-from home.api import WebApiClient
-from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton
-
-
-if __name__ != '__main__':
- print(f'this script can not be imported as module', file=sys.stderr)
- sys.exit(1)
-
-
-db = None
-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',
-}
-logger = logging.getLogger(__name__)
-
-
-class InverterBotConfig(AppConfigUnit, TelegramBotConfig):
- NAME = 'inverter_bot'
-
- @classmethod
- def schema(cls) -> Optional[dict]:
- acmode_item_schema = {
- 'thresholds': {
- 'type': 'list',
- 'required': True,
- 'schema': {
- 'type': 'list',
- 'min': 40,
- 'max': 60
- },
- },
- 'initial_current': {'type': 'integer'}
- }
-
- return {
- **super(TelegramBotConfig).schema(),
- 'ac_mode': {
- 'type': 'dict',
- 'required': True,
- 'schema': {
- 'generator': acmode_item_schema,
- 'utilities': acmode_item_schema
- }
- },
- 'monitor': {
- 'type': 'dict',
- 'required': True,
- 'schema': {
- 'vlow': {'type': 'integer', 'required': True},
- 'vcrit': {'type': 'integer', 'required': True},
- 'gen_currents': {'type': 'list', 'schema': {'type': 'integer'}, 'required': True},
- 'gen_raise_intervals': {'type': 'list', 'schema': {'type': 'integer'}, 'required': True},
- 'gen_cur30_v_limit': {'type': 'float', 'required': True},
- 'gen_cur20_v_limit': {'type': 'float', 'required': True},
- 'gen_cur10_v_limit': {'type': 'float', 'required': True},
- 'gen_floating_v': {'type': 'integer', 'required': True},
- 'gen_floating_time_max': {'type': 'integer', 'required': True}
- }
- }
- }
-
-
-config.load_app(InverterBotConfig)
-
-bot.initialize()
-bot.lang.ru(
- socket="В розетке",
- status='Статус',
- generation='Генерация',
- priority='Приоритет',
- battery="АКБ",
- load="Нагрузка",
- generator="Генератор",
- utilities="Столб",
- consumption="Статистика потребления",
- settings="Настройки",
- done="Готово",
- unexpected_callback_data="Ошибка: неверные данные",
- invalid_input="Неверное значение",
- invalid_mode="Invalid mode",
-
- flags_press_button='Нажмите кнопку для переключения настройки',
- flags_fail='Не удалось установить настройку',
- flags_invalid='Неизвестная настройка',
-
- # generation
- gen_input_power='Зарядная мощность',
-
- # settings
- settings_msg="Что вы хотите настроить?",
- settings_osp='Приоритет питания нагрузки',
- settings_ac_preset="Применить шаблон режима AC",
- settings_bat_thresholds="Пороги заряда АКБ от AC",
- settings_bat_cut_off_voltage="Порог отключения АКБ",
- settings_ac_max_charging_current="Максимальный ток заряда от AC",
-
- settings_osp_msg="Установите приоритет:",
- settings_osp_sub='Solar-Utility-Battery',
- settings_osp_sbu='Solar-Battery-Utility',
-
- settings_select_bottom_threshold="Выберите нижний порог:",
- settings_select_upper_threshold="Выберите верхний порог:",
- settings_select_max_current='Выберите максимальный ток:',
- settings_enter_cutoff_voltage=f'Введите напряжение V, где 40.0 {LT} V {LT} 48.0',
-
- # time and date
- today='Сегодня',
- yday1='Вчера',
- yday2='Позавчера',
- for_7days='За 7 дней',
- for_30days='За 30 дней',
- # to_select_interval='Выбрать интервал',
-
- # consumption
- consumption_msg="Выберите тип:",
- consumption_total="Домашние приборы",
- consumption_grid="Со столба",
- consumption_select_interval='Выберите период:',
- consumption_request_sent="⏳ Запрос отправлен...",
-
- # status
- charging_at=', ',
- pd_charging='заряжается',
- pd_discharging='разряжается',
- pd_nothing='не используется',
-
- # flags
- flag_buzzer='Звуковой сигнал',
- flag_overload_bypass='Разрешить перегрузку',
- flag_escape_to_default_screen_after_1min_timeout='Возврат на главный экран через 1 минуту',
- flag_overload_restart='Перезапуск при перегрузке',
- flag_over_temp_restart='Перезапуск при перегреве',
- flag_backlight_on='Подсветка экрана',
- flag_alarm_on_on_primary_source_interrupt='Сигнал при разрыве основного источника питания',
- flag_fault_code_record='Запись кодов ошибок',
-
- # commands
- setbatuv_v=f'напряжение, 40.0 {LT} V {LT} 48.0',
- setgenct_cv=f'напряжение включения заряда, 44 {LT} CV {LT} 51',
- setgenct_dv=f'напряжение отключения заряда, 48 {LT} DV {LT} 58',
- setgencc_a='максимальный ток заряда, допустимые значения: %s',
-
- # monitor
- chrg_evt_started='✅ Начали заряжать от генератора.',
- chrg_evt_finished='✅ Зарядили. Генератор пора выключать.',
- chrg_evt_disconnected='ℹ️ Генератор отключен.',
- chrg_evt_current_changed='ℹ️ Ток заряда от генератора установлен в %d A.',
- chrg_evt_not_charging='ℹ️ Генератор подключен, но не заряжает.',
- chrg_evt_na_solar='⛔️ Генератор подключен, но аккумуляторы не заряжаются из-за подключенных панелей.',
- chrg_evt_mostly_charged='✅ Аккумуляторы более-менее заряжены, генератор пора выключать.',
- battery_level_changed='Уровень заряда АКБ: <b>%s %s</b> (<b>%0.1f V</b> при нагрузке <b>%d W</b>)',
- error_message='<b>Ошибка:</b> %s.',
-
- util_chrg_evt_started='✅ Начали заряжать от столба.',
- util_chrg_evt_stopped='ℹ️ Перестали заряжать от столба.',
- util_chrg_evt_stopped_solar='ℹ️ Перестали заряжать от столба из-за подключения панелей.',
-
- util_connected='✅️ Столб подключён.',
- util_disconnected='‼️ Столб отключён.',
-
- # other notifications
- ac_mode_changed_notification='Пользователь <a href="tg://user?id=%d">%s</a> установил режим AC: <b>%s</b>.',
- osp_changed_notification='Пользователь <a href="tg://user?id=%d">%s</a> установил приоритет источника питания нагрузки: <b>%s</b>.',
- osp_auto_changed_notification='ℹ️ Бот установил приоритет источника питания нагрузки: <b>%s</b>. Причины: напряжение АКБ %.1f V, мощность заряда с панелей %d W.',
-
- bat_state_normal='Нормальный',
- bat_state_low='Низкий',
- bat_state_critical='Критический',
-)
-
-bot.lang.en(
- socket='AC output',
- status='Status',
- generation='Generation',
- priority='Priority',
- battery="Battery",
- load="Load",
- generator="Generator",
- utilities="Utilities",
- consumption="Consumption statistics",
- settings="Settings",
- done="Done",
- unexpected_callback_data="Unexpected callback data",
- select_priortiy="Select priority:",
- invalid_input="Invalid input",
- invalid_mode="Invalid mode",
-
- flags_press_button='Press a button to toggle a flag.',
- flags_fail='Failed to toggle flag',
- flags_invalid='Invalid flag',
-
- # settings
- settings_msg='What do you want to configure?',
- settings_osp='Output source priority',
- settings_ac_preset="AC preset",
- settings_bat_thresholds="Battery charging thresholds",
- settings_bat_cut_off_voltage="Battery cut-off voltage",
- settings_ac_max_charging_current="Max AC charging current",
-
- settings_osp_msg="Select priority:",
- settings_osp_sub='Solar-Utility-Battery',
- settings_osp_sbu='Solar-Battery-Utility',
-
- settings_select_bottom_threshold="Select bottom (lower) threshold:",
- settings_select_upper_threshold="Select top (upper) threshold:",
- settings_select_max_current='Select max current:',
- settings_enter_cutoff_voltage=f'Enter voltage V (40.0 {LT} V {LT} 48.0):',
-
- # generation
- gen_input_power='Input power',
-
- # time and date
- today='Today',
- yday1='Yesterday',
- yday2='The day before yesterday',
- for_7days='7 days',
- for_30days='30 days',
- # to_select_interval='Select interval',
-
- # consumption
- consumption_msg="Select type:",
- consumption_total="Home appliances",
- consumption_grid="Consumed from grid",
- consumption_select_interval='Select period:',
- consumption_request_sent="⏳ Request sent...",
-
- # status
- charging_at=' @ ',
- pd_charging='charging',
- pd_discharging='discharging',
- pd_nothing='not used',
-
- # 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 interruption',
- flag_fault_code_record='Fault code recording',
-
- # commands
- setbatuv_v=f'floating point number, 40.0 {LT} V {LT} 48.0',
- setgenct_cv=f'charging voltage, 44 {LT} CV {LT} 51',
- setgenct_dv=f'discharging voltage, 48 {LT} DV {LT} 58',
- setgencc_a='max charging current, allowed values: %s',
-
- # monitor
- chrg_evt_started='✅ Started charging from AC.',
- chrg_evt_finished='✅ Finished charging, it\'s time to stop the generator.',
- chrg_evt_disconnected='ℹ️ AC disconnected.',
- chrg_evt_current_changed='ℹ️ AC charging current set to %d A.',
- chrg_evt_not_charging='ℹ️ AC connected but not charging.',
- chrg_evt_na_solar='⛔️ AC connected, but battery won\'t be charged due to active solar power line.',
- chrg_evt_mostly_charged='✅ The battery is mostly charged now. The generator can be turned off.',
- 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.',
-
- util_chrg_evt_started='✅ Started charging from utilities.',
- util_chrg_evt_stopped='ℹ️ Stopped charging from utilities.',
- util_chrg_evt_stopped_solar='ℹ️ Stopped charging from utilities because solar panels were connected.',
-
- util_connected='✅️ Utilities connected.',
- util_disconnected='‼️ Utilities disconnected.',
-
- # other notifications
- ac_mode_changed_notification='User <a href="tg://user?id=%d">%s</a> set AC mode to <b>%s</b>.',
- osp_changed_notification='User <a href="tg://user?id=%d">%s</a> set output source priority: <b>%s</b>.',
- osp_auto_changed_notification='Bot changed output source priority to <b>%s</b>. Reasons: battery voltage is %.1f V, solar input is %d W.',
-
- bat_state_normal='Normal',
- bat_state_low='Low',
- bat_state_critical='Critical',
-)
-
-
-def monitor_charging(event: ChargingEvent, **kwargs) -> None:
- args = []
- is_util = False
- if event == ChargingEvent.AC_CHARGING_STARTED:
- key = 'started'
- elif event == ChargingEvent.AC_CHARGING_FINISHED:
- key = 'finished'
- elif event == ChargingEvent.AC_DISCONNECTED:
- key = 'disconnected'
- elif event == ChargingEvent.AC_NOT_CHARGING:
- key = 'not_charging'
- elif event == ChargingEvent.AC_CURRENT_CHANGED:
- key = 'current_changed'
- args.append(kwargs['current'])
- elif event == ChargingEvent.AC_CHARGING_UNAVAILABLE_BECAUSE_SOLAR:
- key = 'na_solar'
- elif event == ChargingEvent.AC_MOSTLY_CHARGED:
- key = 'mostly_charged'
- elif event == ChargingEvent.UTIL_CHARGING_STARTED:
- key = 'started'
- is_util = True
- elif event == ChargingEvent.UTIL_CHARGING_STOPPED:
- key = 'stopped'
- is_util = True
- elif event == ChargingEvent.UTIL_CHARGING_STOPPED_SOLAR:
- key = 'stopped_solar'
- is_util = True
- else:
- logger.error('unknown charging event:', event)
- return
-
- key = f'chrg_evt_{key}'
- if is_util:
- key = f'util_{key}'
- bot.notify_all(
- lambda lang: bot.lang.get(key, lang, *args)
- )
-
-
-def monitor_battery(state: BatteryState, v: float, load_watts: int) -> None:
- if state == BatteryState.NORMAL:
- emoji = '✅'
- elif state == BatteryState.LOW:
- emoji = '⚠️'
- elif state == BatteryState.CRITICAL:
- emoji = '‼️'
- else:
- 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)
- )
-
-
-def monitor_util(event: ACPresentEvent):
- if event == ACPresentEvent.CONNECTED:
- key = 'connected'
- else:
- key = 'disconnected'
- key = f'util_{key}'
- 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)
- )
-
-
-def osp_change_cb(new_osp: OutputSourcePriority,
- solar_input: int,
- v: float):
-
- 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),
- )
-
-
-@bot.handler(command='status')
-def full_status(ctx: bot.Context) -> None:
- status = inverter.exec('get-status', format=Format.TABLE)
- ctx.reply(beautify_table(status))
-
-
-@bot.handler(command='config')
-def full_rated(ctx: bot.Context) -> None:
- rated = inverter.exec('get-rated', format=Format.TABLE)
- ctx.reply(beautify_table(rated))
-
-
-@bot.handler(command='errors')
-def full_errors(ctx: bot.Context) -> None:
- errors = inverter.exec('get-errors', format=Format.TABLE)
- ctx.reply(beautify_table(errors))
-
-
-@bot.handler(command='flags')
-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)
-
-
-def build_flags_keyboard(flags: dict, ctx: bot.Context) -> Tuple[str, InlineKeyboardMarkup]:
- keyboard = []
- for k, v in flags.items():
- label = ('✅' if v else '❌') + ' ' + ctx.lang(f'flag_{k}')
- proto_flag = flags_map[k]
- keyboard.append([InlineKeyboardButton(label, callback_data=f'flag_{proto_flag}')])
-
- return ctx.lang('flags_press_button'), InlineKeyboardMarkup(keyboard)
-
-
-def getacmode() -> ACMode:
- return ACMode(bot.db.get_param('ac_mode', default=ACMode.GENERATOR))
-
-
-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-charge-thresholds', (cv, dv))
- inverter.exec('set-max-ac-charge-current', (0, a))
-
-
-def setosp(sp: OutputSourcePriority):
- logger.debug(f'setosp: sp={sp}')
- inverter.exec('set-output-source-priority', (sp.value,))
- monitor.notify_osp(sp)
-
-
-class SettingsConversation(bot.conversation):
- START, OSP, AC_PRESET, BAT_THRESHOLDS_1, BAT_THRESHOLDS_2, BAT_CUT_OFF_VOLTAGE, AC_MAX_CHARGING_CURRENT = range(7)
- STATE_SEQS = [
- [START, OSP],
- [START, AC_PRESET],
- [START, BAT_THRESHOLDS_1, BAT_THRESHOLDS_2],
- [START, BAT_CUT_OFF_VOLTAGE],
- [START, AC_MAX_CHARGING_CURRENT]
- ]
-
- START_BUTTONS = bot.lang.pfx('settings_', ['ac_preset',
- 'ac_max_charging_current',
- 'bat_thresholds',
- 'bat_cut_off_voltage',
- 'osp'])
- OSP_BUTTONS = bot.lang.pfx('settings_osp_', [sp.value.lower() for sp in OutputSourcePriority])
- AC_PRESET_BUTTONS = [mode.value for mode in ACMode]
-
- RECHARGE_VOLTAGES = [44, 45, 46, 47, 48, 49, 50, 51]
- 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):
- buttons = list(chunks(list(self.START_BUTTONS), 2))
- buttons.reverse()
- return self.reply(ctx, self.START, ctx.lang('settings_msg'), buttons,
- with_cancel=True)
-
- @bot.convinput(START, messages={
- 'settings_osp': OSP,
- 'settings_ac_preset': AC_PRESET,
- 'settings_bat_thresholds': BAT_THRESHOLDS_1,
- '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):
- 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)
-
- @bot.convinput(OSP, messages=OSP_BUTTONS)
- 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()}'):
- selected_sp = sp
- break
-
- if selected_sp is None:
- raise ValueError('invalid sp')
-
- # 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,)
- )
- 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)
-
- @bot.convinput(AC_PRESET, messages=AC_PRESET_BUTTONS)
- def acpreset_input(self, ctx: bot.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
- 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,)
- )
- return self.END
-
- @bot.conventer(BAT_THRESHOLDS_1)
- 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)
-
- @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):
- v = self._parse_voltage(ctx.text)
- ctx.user_data['bat_thrsh_v1'] = v
- return self.invoke(self.BAT_THRESHOLDS_2, ctx)
-
- @bot.conventer(BAT_THRESHOLDS_2)
- 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)
-
- @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):
- 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())
- return self.END
-
- @bot.conventer(AC_MAX_CHARGING_CURRENT)
- 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)
-
- @bot.convinput(AC_MAX_CHARGING_CURRENT, regex=r'^\d+ A$')
- 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())
- 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)
-
- @bot.convinput(BAT_CUT_OFF_VOLTAGE, regex=r'^(\d{2}(\.\d{1})?)$')
- 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())
- else:
- raise ValueError('invalid voltage')
-
- return self.END
-
- def _get_allowed_ac_charge_amps(self) -> list[int]:
- l = inverter.exec('get-allowed-ac-charge-currents')['data']
- l = filter(lambda n: n <= 40, l)
- return list(l)
-
- def _parse_voltage(self, s: str) -> int:
- return int(re.match(r'^(\d{2}) V$', s).group(1))
-
- def _parse_amps(self, s: str) -> int:
- return int(re.match(r'^(\d{1,2}) A$', s).group(1))
-
-
-class ConsumptionConversation(bot.conversation):
- START, TOTAL, GRID = range(3)
- STATE_SEQS = [
- [START, TOTAL],
- [START, GRID]
- ]
-
- START_BUTTONS = bot.lang.pfx('consumption_', ['total', 'grid'])
- INTERVAL_BUTTONS = [
- ['today'],
- ['yday1'],
- ['for_7days', 'for_30days'],
- # ['to_select_interval']
- ]
- 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)
-
- @bot.convinput(START, messages={
- 'consumption_total': TOTAL,
- 'consumption_grid': GRID
- })
- 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)
-
- @bot.conventer(GRID)
- def grid_enter(self, ctx: bot.Context):
- return 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)
-
- @bot.convinput(TOTAL, messages=INTERVAL_BUTTONS_FLAT)
- def total_input(self, ctx: bot.Context):
- return 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)
-
- def _render_interval_results(self, ctx: bot.Context, state):
- # if ctx.text == ctx.lang('to_select_interval'):
- # TODO
- # pass
- #
- # else:
-
- now = datetime.datetime.now()
- s_to = now.strftime(FormatDate)
-
- if ctx.text == ctx.lang('today'):
- s_from = now.strftime(FormatDate)
- s_to = 'now'
- elif ctx.text == ctx.lang('yday1'):
- s_from = (now - datetime.timedelta(days=1)).strftime(FormatDate)
- elif ctx.text == ctx.lang('for_7days'):
- s_from = (now - datetime.timedelta(days=7)).strftime(FormatDate)
- elif ctx.text == ctx.lang('for_30days'):
- s_from = (now - datetime.timedelta(days=30)).strftime(FormatDate)
-
- # markup = InlineKeyboardMarkup([
- # [InlineKeyboardButton(ctx.lang('please_wait'), callback_data='wait')]
- # ])
-
- message = 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())
- return self.END
- except Exception as e:
- 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:
- msg = ''
- st = monitor.dump_status()
- for k, v in st.items():
- msg += k + ': ' + str(v) + '\n'
- ctx.reply(msg)
-
-
-@bot.handler(command='monsetcur')
-def monsetcur_handler(ctx: bot.Context) -> None:
- ctx.reply('not implemented yet')
-
-
-@bot.callbackhandler
-def button_callback(ctx: bot.Context) -> None:
- query = ctx.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(ctx.lang('flags_invalid'))
- 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(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)
-
- else:
- query.answer(ctx.lang('unexpected_callback_data'))
-
-
-@bot.exceptionhandler
-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())
- return True
-
-
-@bot.handler(message='status')
-def status_handler(ctx: bot.Context) -> None:
- gs = inverter.exec('get-status')['data']
- rated = inverter.exec('get-rated')['data']
-
- # render response
- power_direction = gs['battery_power_direction'].lower()
- power_direction = re.sub(r'ge$', 'ging', power_direction)
-
- charging_rate = ''
- chrg_at = ctx.lang('charging_at')
-
- if power_direction == 'charging':
- charging_rate = f'{chrg_at}%s %s' % (
- gs['battery_charge_current']['value'], gs['battery_charge_current']['unit'])
- pd_label = ctx.lang('pd_charging')
- elif power_direction == 'discharging':
- charging_rate = f'{chrg_at}%s %s' % (
- gs['battery_discharge_current']['value'], gs['battery_discharge_current']['unit'])
- pd_label = ctx.lang('pd_discharging')
- else:
- pd_label = ctx.lang('pd_nothing')
-
- html = f'<b>{ctx.lang("battery")}:</b> %s %s' % (gs['battery_voltage']['value'], gs['battery_voltage']['unit'])
- html += ' (%s%s)' % (pd_label, charging_rate)
-
- html += f'\n<b>{ctx.lang("load")}:</b> %s %s' % (gs['ac_output_active_power']['value'], gs['ac_output_active_power']['unit'])
- html += ' (%s%%)' % (gs['output_load_percent']['value'])
-
- if gs['pv1_input_power']['value'] > 0:
- html += f'\n<b>{ctx.lang("gen_input_power")}:</b> %s %s' % (gs['pv1_input_power']['value'], gs['pv1_input_power']['unit'])
-
- if gs['grid_voltage']['value'] > 0 or gs['grid_freq']['value'] > 0:
- ac_mode = getacmode()
- html += f'\n<b>{ctx.lang(ac_mode.value)}:</b> %s %s' % (gs['grid_voltage']['value'], gs['grid_voltage']['unit'])
- html += ', %s %s' % (gs['grid_freq']['value'], gs['grid_freq']['unit'])
-
- html += f'\n<b>{ctx.lang("socket")}</b>: %s %s, %s %s' % (
- gs['ac_output_voltage']['value'], gs['ac_output_voltage']['unit'],
- gs['ac_output_freq']['value'], gs['ac_output_freq']['unit']
- )
-
- html += f'\n<b>{ctx.lang("priority")}</b>: {rated["output_source_priority"]}'
-
- # send response
- ctx.reply(html)
-
-
-@bot.handler(message='generation')
-def generation_handler(ctx: bot.Context) -> None:
- today = datetime.date.today()
- yday = today - datetime.timedelta(days=1)
- yday2 = today - datetime.timedelta(days=2)
-
- gs = inverter.exec('get-status')['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:
- gen_yday = inverter.exec('get-day-generated', (yday.year, yday.month, yday.day))['data']
-
- if yday2.month == today.month:
- gen_yday2 = inverter.exec('get-day-generated', (yday2.year, yday2.month, yday2.day))['data']
-
- # render response
- html = f'<b>{ctx.lang("gen_input_power")}:</b> %s %s' % (gs['pv1_input_power']['value'], gs['pv1_input_power']['unit'])
- html += ' (%s %s)' % (gs['pv1_input_voltage']['value'], gs['pv1_input_voltage']['unit'])
-
- html += f'\n<b>{ctx.lang("today")}:</b> %s Wh' % (gen_today['wh'])
-
- if gen_yday is not None:
- html += f'\n<b>{ctx.lang("yday1")}:</b> %s Wh' % (gen_yday['wh'])
-
- if gen_yday2 is not None:
- html += f'\n<b>{ctx.lang("yday2")}:</b> %s Wh' % (gen_yday2['wh'])
-
- # send response
- ctx.reply(html)
-
-
-@bot.defaultreplymarkup
-def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
- button = [
- [ctx.lang('status'), ctx.lang('generation')],
- [ctx.lang('consumption')],
- [ctx.lang('settings')]
- ]
- return ReplyKeyboardMarkup(button, one_time_keyboard=False)
-
-
-class InverterStore(bot.BotDatabase):
- 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()
-
-
-inverter.init(host=config['inverter']['ip'], port=config['inverter']['port'])
-
-bot.set_database(InverterStore())
-bot.enable_logging(BotType.INVERTER)
-
-bot.add_conversation(SettingsConversation(enable_back=True))
-bot.add_conversation(ConsumptionConversation(enable_back=True))
-
-monitor = InverterMonitor()
-monitor.set_charging_event_handler(monitor_charging)
-monitor.set_battery_event_handler(monitor_battery)
-monitor.set_util_event_handler(monitor_util)
-monitor.set_error_handler(monitor_error)
-monitor.set_osp_need_change_callback(osp_change_cb)
-
-setacmode(getacmode())
-
-if not config.get('monitor.disabled'):
- logging.info('starting monitor')
- monitor.start()
-
-bot.run()
-
-monitor.stop()