From a62e151a53ba82dd314343528652a62c127dd05b Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Thu, 6 May 2021 02:32:37 +0300 Subject: add systemd service example --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 752e17e..361ba29 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,33 @@ use_sudo=0 Only users in `admins` are allowed to use the bot. +## Launching with systemd + +Create a service file `/etc/systemd/system/inverter-bot.service` with the following content (changing stuff like paths): + +```systemd +[Unit] +Description=inverter bot +After=network.target + +[Service] +User=user +Group=user +Restart=on-failure +ExecStart=python3 /home/user/inverter-bot/main.py +WorkingDirectory=/home/user/inverter-bot + +[Install] +WantedBy=multi-user.target +``` + +Then enable and start the service: +``` +systemctl daemon-reload +systemctl enable inverter-bot +systemctl start inverter-bot +``` + ## License BSD-2c -- cgit v1.2.3 From db7911c40872dda0623b9d48182529920a5bd313 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Thu, 6 May 2021 23:11:52 +0300 Subject: change license to MIT --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 361ba29..9f7f33c 100644 --- a/README.md +++ b/README.md @@ -54,4 +54,4 @@ systemctl start inverter-bot ## License -BSD-2c +MIT \ No newline at end of file -- cgit v1.2.3 From a9f26cf874a74cd3cd90f4185e537609fffdb815 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Thu, 6 May 2021 23:13:27 +0300 Subject: add license file --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3517954 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2021 Evgeny Zinoviev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. -- cgit v1.2.3 From b804a5d7e7dab93bf6056e8969a5dd4d4c52f452 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 8 May 2021 01:53:47 +0300 Subject: migrate from isv to inverterd --- README.md | 43 +++++----- configstore.py | 51 ------------ inverter-bot | 236 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ isv.py | 42 ---------- main.py | 187 ------------------------------------------- requirements.txt | 3 +- test.py | 35 +++++++++ 7 files changed, 298 insertions(+), 299 deletions(-) delete mode 100644 configstore.py create mode 100755 inverter-bot delete mode 100644 isv.py delete mode 100644 main.py create mode 100644 test.py diff --git a/README.md b/README.md index 9f7f33c..abc44a0 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,40 @@ # inverter-bot -This is a Telegram bot for querying information from an InfiniSolar V family of hybrid solar inverters, in particular -inverters supported by **isv** utility, which is an older version of **infinisolarctl** from **infinisolar-tools** -package. +This is a Telegram bot for querying information from an InfiniSolar V family of hybrid solar inverters. It supports querying general status, such as battery voltage or power usage, printing amounts of energy generated in the last days, dumping status or rated information and more. It requires Python 3.6+ or so. -## Configuration +## Requirements -Configuration is stored in `config.ini` file in `~/.config/inverter-bot`. +- **`inverterd`** from [inverter-tools](https://github.com/gch1p/inverter-tools) +- **[`inverterd`](https://pypi.org/project/inverterd/)** python library +- Python 3.6+ or so -Config example: -``` -token=YOUR_TOKEN -admins= - 123456 ; admin id - 000123 ; another admin id -isv_bin=/path/to/isv -use_sudo=0 -``` +## Configuration + +The bot accepts following parameters: -Only users in `admins` are allowed to use the bot. +* ``--token`` — your telegram bot token (required) +* ``--users-whitelist`` — space-separated list of IDs of users who are allowed + to use the bot (required) +* ``--inverterd-host`` (default is `127.0.0.1`) +* ``--inverterd-port`` (default is `8305`) ## Launching with systemd -Create a service file `/etc/systemd/system/inverter-bot.service` with the following content (changing stuff like paths): +This is tested on Debian 10. Something might differ on other systems. + +Create environment configuration file `/etc/default/inverter-bot`: +``` +TOKEN="YOUR_TOKEN" +USERS="ID ID ID ..." +OPTS="" # here you can pass other options such as --inverterd-host +``` + +Create systemd service file `/etc/systemd/system/inverter-bot.service` with the following content (changing stuff like paths): ```systemd [Unit] @@ -35,10 +42,11 @@ Description=inverter bot After=network.target [Service] +EnvironmentFile=/etc/default/inverter-bot User=user Group=user Restart=on-failure -ExecStart=python3 /home/user/inverter-bot/main.py +ExecStart=python3 /home/user/inverter-bot/inverter-bot --token $TOKEN --users-whitelist $USERS $PARAMS WorkingDirectory=/home/user/inverter-bot [Install] @@ -47,7 +55,6 @@ WantedBy=multi-user.target Then enable and start the service: ``` -systemctl daemon-reload systemctl enable inverter-bot systemctl start inverter-bot ``` diff --git a/configstore.py b/configstore.py deleted file mode 100644 index fcaa5c8..0000000 --- a/configstore.py +++ /dev/null @@ -1,51 +0,0 @@ -import os, re -from configparser import ConfigParser - -CONFIG_DIR = os.environ['HOME'] + '/.config/inverter-bot' -CONFIG_FILE = 'config.ini' - -__config__ = None - - -def _get_config_path() -> str: - return "%s/%s" % (CONFIG_DIR, CONFIG_FILE) - - -def get_config(): - global __config__ - if __config__ is not None: - return __config__['root'] - - if not os.path.exists(CONFIG_DIR): - raise IOError("%s directory not found" % CONFIG_DIR) - - if not os.path.isdir(CONFIG_DIR): - raise IOError("%s is not a directory" % CONFIG_DIR) - - config_path = _get_config_path() - if not os.path.isfile(config_path): - raise IOError("%s file not found" % config_path) - - __config__ = ConfigParser() - with open(config_path) as config_content: - __config__.read_string("[root]\n" + config_content.read()) - - return __config__['root'] - - -def get_token() -> str: - return get_config()['token'] - - -def get_admins() -> tuple: - config = get_config() - return tuple([int(s) for s in re.findall(r'\b\d+\b', config['admins'], flags=re.MULTILINE)]) - - -def get_isv_bin() -> str: - return get_config()['isv_bin'] - - -def use_sudo() -> bool: - config = get_config() - return 'use_sudo' in config and config['use_sudo'] == '1' \ No newline at end of file diff --git a/inverter-bot b/inverter-bot new file mode 100755 index 0000000..921b185 --- /dev/null +++ b/inverter-bot @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +import logging, re, datetime, json + +from inverterd import Format, Client as InverterClient, InverterError +from typing import Optional +from argparse import ArgumentParser +from html import escape +from pprint import pprint +from time import sleep +from strings import lang as _ +from telegram import ( + Update, + ParseMode, + KeyboardButton, + ReplyKeyboardMarkup +) +from telegram.ext import ( + Updater, + Filters, + CommandHandler, + MessageHandler, + CallbackContext +) + + +class InverterClientWrapper: + def __init__(self, host: str, port: str): + self._host = host + self._port = port + self._inverter = None + + self.create() + + def create(self): + self._inverter = InverterClient(host=self._host, port=self._port) + self._inverter.connect() + + def exec(self, command: str, arguments: tuple = (), format=Format.JSON): + try: + self._inverter.format(format) + return self._inverter.exec(command, arguments) + except InverterError as e: + raise e + except Exception as e: + # silently try to reconnect + try: + self.create() + except Exception: + pass + raise e + + +inverter: Optional[InverterClientWrapper] = None + + +# +# helpers +# + + +def get_markup() -> ReplyKeyboardMarkup: + button = [ + [ + _('status'), + _('generation') + ], + [ + _('gs'), + _('ri'), + _('errors') + ] + ] + return ReplyKeyboardMarkup(button, one_time_keyboard=False) + + +def reply(update: Update, text: str) -> None: + update.message.reply_text(text, + reply_markup=get_markup(), + parse_mode=ParseMode.HTML) + + +# +# 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'] + + # render response + power_direction = gs['battery_power_direction'].lower() + power_direction = re.sub(r'ge$', 'ging', power_direction) + + charging_rate = '' + if power_direction == 'charging': + charging_rate = ' @ %s %s' % ( + gs['battery_charging_current']['value'], gs['battery_charging_current']['unit']) + elif power_direction == 'discharging': + charging_rate = ' @ %s %s' % ( + gs['battery_discharging_current']['value'], gs['battery_discharging_current']['unit']) + + html = 'Battery: %s %s' % (gs['battery_voltage']['value'], gs['battery_voltage']['unit']) + html += ' (%s%s, ' % (gs['battery_capacity']['value'], gs['battery_capacity']['unit']) + html += '%s%s)' % (power_direction, charging_rate) + + html += '\nLoad: %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 += '\nInput power: %s%s' % (gs['pv1_input_power']['value'], gs['pv1_input_power']['unit']) + + if gs['grid_voltage']['value'] > 0 or gs['grid_freq']['value'] > 0: + html += '\nGenerator: %s %s' % (gs['grid_voltage']['unit'], gs['grid_voltage']['value']) + html += ', %s %s' % (gs['grid_freq']['value'], gs['grid_freq']['unit']) + + # send response + reply(update, html) + except Exception as e: + logging.exception(str(e)) + reply(update, 'exception: ' + str(e)) + + +def msg_generation(update: Update, context: CallbackContext) -> None: + try: + today = datetime.date.today() + yday = today - datetime.timedelta(days=1) + yday2 = today - datetime.timedelta(days=2) + + gs = json.loads(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_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'] + + if yday2.month == today.month: + # sleep(0.1) + gen_yday2 = json.loads(inverter.exec('get-day-generated', (yday2.year, yday2.month, yday2.day)))['data'] + + # render response + html = 'Input power: %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 += '\nToday: %s Wh' % (gen_today['wh']) + + if gen_yday is not None: + html += '\nYesterday: %s Wh' % (gen_yday['wh']) + + if gen_yday2 is not None: + html += '\nThe day before yesterday: %s Wh' % (gen_yday2['wh']) + + # send response + reply(update, html) + except Exception as e: + logging.exception(str(e)) + reply(update, 'exception: ' + str(e)) + + +def msg_gs(update: Update, context: CallbackContext) -> None: + try: + status = inverter.exec('get-status', format=Format.TABLE) + reply(update, status) + except Exception as e: + logging.exception(str(e)) + reply(update, 'exception: ' + str(e)) + + +def msg_ri(update: Update, context: CallbackContext) -> None: + try: + rated = inverter.exec('get-rated', format=Format.TABLE) + reply(update, rated) + except Exception as e: + logging.exception(str(e)) + reply(update, 'exception: ' + str(e)) + + +def msg_errors(update: Update, context: CallbackContext) -> None: + try: + errors = inverter.exec('get-errors', format=Format.TABLE) + reply(update, errors) + except Exception as e: + logging.exception(str(e)) + reply(update, 'exception: ' + str(e)) + + +def msg_all(update: Update, context: CallbackContext) -> None: + reply(update, "Command not recognized. Please try again.") + + +if __name__ == '__main__': + # command-line arguments + parser = ArgumentParser() + parser.add_argument('--token', required=True, type=str, + help='Telegram bot token') + parser.add_argument('--users-whitelist', nargs='+', + help='ID of users allowed to use the bot') + parser.add_argument('--inverterd-host', default='127.0.0.1', type=str) + parser.add_argument('--inverterd-port', default=8305, type=int) + args = parser.parse_args() + + whitelist = list(map(lambda x: int(x), args.users_whitelist)) + + # connect to inverterd + inverter = InverterClientWrapper(host=args.inverterd_host, port=args.inverterd_port) + + # configure logging + logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO) + + # configure bot + updater = Updater(args.token, request_kwargs={'read_timeout': 6, 'connect_timeout': 7}) + dispatcher = updater.dispatcher + + user_filter = Filters.user(whitelist) + + 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(MessageHandler(Filters.all & user_filter, msg_all)) + + # start the bot + updater.start_polling() + + # run the bot until the user presses Ctrl-C or the process receives SIGINT, SIGTERM or SIGABRT + updater.idle() diff --git a/isv.py b/isv.py deleted file mode 100644 index ab4268b..0000000 --- a/isv.py +++ /dev/null @@ -1,42 +0,0 @@ -import subprocess -import configstore -import json - - -def __run(argv: list, fmt='json-w-units'): - argv.insert(0, configstore.get_isv_bin()) - if configstore.use_sudo(): - argv.insert(0, 'sudo') - argv.append('--format') - argv.append(fmt) - - result = subprocess.run(argv, capture_output=True) - if result.returncode != 0: - raise ChildProcessError("isv returned %d: %s" % (result.returncode, result.stderr)) - - return json.loads(result.stdout) if 'json' in fmt else result.stdout.decode('utf-8') - - -def general_status(as_table=False): - kwargs = {} - if as_table: - kwargs['fmt'] = 'table' - return __run(['--get-general-status'], **kwargs) - - -def day_generated(y: int, m: int, d: int): - return __run(['--get-day-generated', str(y), str(m), str(d)]) - - -def rated_information(as_table=False): - kwargs = {} - if as_table: - kwargs['fmt'] = 'table' - return __run(['--get-rated-information'], **kwargs) - - -def faults(as_table=False): - kwargs = {} - if as_table: - kwargs['fmt'] = 'table' - return __run(['--get-faults-warnings'], **kwargs) diff --git a/main.py b/main.py deleted file mode 100644 index 1441728..0000000 --- a/main.py +++ /dev/null @@ -1,187 +0,0 @@ -import logging -import re -import datetime -import isv -import configstore - -from html import escape -from time import sleep -from strings import lang as _ -from telegram import ( - Update, - ParseMode, - KeyboardButton, - ReplyKeyboardMarkup -) -from telegram.ext import ( - Updater, - Filters, - CommandHandler, - MessageHandler, - CallbackContext -) - - -# -# helpers -# - -def get_markup() -> ReplyKeyboardMarkup: - button = [ - [ - _('status'), - _('generation') - ], - [ - _('gs'), - _('ri'), - _('errors') - ] - ] - return ReplyKeyboardMarkup(button, one_time_keyboard=False) - - -def reply(update: Update, text: str) -> None: - update.message.reply_text(text, - reply_markup=get_markup(), - parse_mode=ParseMode.HTML) - - -# -# 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 = isv.general_status() - - # render response - power_direction = gs['battery_power_direction'].lower() - power_direction = re.sub(r'ge$', 'ging', power_direction) - - charging_rate = '' - if power_direction == 'charging': - charging_rate = ' @ %s %s' % tuple(gs['battery_charging_current']) - elif power_direction == 'discharging': - charging_rate = ' @ %s %s' % tuple(gs['battery_discharge_current']) - - html = 'Battery: %s %s' % tuple(gs['battery_voltage']) - html += ' (%s%s, ' % tuple(gs['battery_capacity']) - html += '%s%s)' % (power_direction, charging_rate) - - html += '\nLoad: %s %s' % tuple(gs['ac_output_active_power']) - html += ' (%s%%)' % (gs['output_load_percent'][0]) - - if gs['pv1_input_power'][0] > 0: - html += '\nInput power: %s%s' % tuple(gs['pv1_input_power']) - - if gs['grid_voltage'][0] > 0 or gs['grid_freq'][0] > 0: - html += '\nGenerator: %s %s' % tuple(gs['grid_voltage']) - html += ', %s %s' % tuple(gs['grid_freq']) - - # send response - reply(update, html) - except Exception as e: - logging.exception(str(e)) - reply(update, 'exception: ' + str(e)) - - -def msg_generation(update: Update, context: CallbackContext) -> None: - try: - today = datetime.date.today() - yday = today - datetime.timedelta(days=1) - yday2 = today - datetime.timedelta(days=2) - - gs = isv.general_status() - sleep(0.1) - - gen_today = isv.day_generated(today.year, today.month, today.day) - gen_yday = None - gen_yday2 = None - - if yday.month == today.month: - sleep(0.1) - gen_yday = isv.day_generated(yday.year, yday.month, yday.day) - - if yday2.month == today.month: - sleep(0.1) - gen_yday2 = isv.day_generated(yday2.year, yday2.month, yday2.day) - - # render response - html = 'Input power: %s %s' % tuple(gs['pv1_input_power']) - html += ' (%s %s)' % tuple(gs['pv1_input_voltage']) - - html += '\nToday: %s Wh' % (gen_today['wh']) - - if gen_yday is not None: - html += '\nYesterday: %s Wh' % (gen_yday['wh']) - - if gen_yday2 is not None: - html += '\nThe day before yesterday: %s Wh' % (gen_yday2['wh']) - - # send response - reply(update, html) - except Exception as e: - logging.exception(str(e)) - reply(update, 'exception: ' + str(e)) - - -def msg_gs(update: Update, context: CallbackContext) -> None: - try: - status = isv.general_status(as_table=True) - reply(update, status) - except Exception as e: - logging.exception(str(e)) - reply(update, 'exception: ' + str(e)) - - -def msg_ri(update: Update, context: CallbackContext) -> None: - try: - rated = isv.rated_information(as_table=True) - reply(update, rated) - except Exception as e: - logging.exception(str(e)) - reply(update, 'exception: ' + str(e)) - - -def msg_errors(update: Update, context: CallbackContext) -> None: - try: - faults = isv.faults(as_table=True) - reply(update, faults) - except Exception as e: - logging.exception(str(e)) - reply(update, 'exception: ' + str(e)) - - -def msg_all(update: Update, context: CallbackContext) -> None: - reply(update, "Command not recognized. Please try again.") - - -if __name__ == '__main__': - config = configstore.get_config() - - logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - level=logging.INFO) - - updater = Updater(configstore.get_token(), request_kwargs={'read_timeout': 6, 'connect_timeout': 7}) - dispatcher = updater.dispatcher - - user_filter = Filters.user(configstore.get_admins()) - - 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(MessageHandler(Filters.all & user_filter, msg_all)) - - # start the bot - updater.start_polling() - - # run the bot until the user presses Ctrl-C or the process receives SIGINT, SIGTERM or SIGABRT - updater.idle() diff --git a/requirements.txt b/requirements.txt index ccd058e..41f958c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -python-telegram-bot~=13.1 \ No newline at end of file +python-telegram-bot~=13.1 +inverterd~=1.0.2 \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..79ac277 --- /dev/null +++ b/test.py @@ -0,0 +1,35 @@ +import json, re +from pprint import pprint + + +s = '{"result":"ok","data":{"grid_voltage":{"unit":"V","value":0.0},"grid_freq":{"unit":"Hz","value":0.0},"ac_output_voltage":{"unit":"V","value":230.0},"ac_output_freq":{"unit":"Hz","value":50.0},"ac_output_apparent_power":{"unit":"VA","value":115},"ac_output_active_power":{"unit":"Wh","value":18},"output_load_percent":{"unit":"%","value":2},"battery_voltage":{"unit":"V","value":50.0},"battery_voltage_scc":{"unit":"V","value":0.0},"battery_voltage_scc2":{"unit":"V","value":0.0},"battery_discharging_current":{"unit":"A","value":0},"battery_charging_current":{"unit":"A","value":0},"battery_capacity":{"unit":"%","value":78},"inverter_heat_sink_temp":{"unit":"°C","value":19},"mppt1_charger_temp":{"unit":"°C","value":0},"mppt2_charger_temp":{"unit":"°C","value":0},"pv1_input_power":{"unit":"Wh","value":1000},"pv2_input_power":{"unit":"Wh","value":0},"pv1_input_voltage":{"unit":"V","value":0.0},"pv2_input_voltage":{"unit":"V","value":0.0},"settings_values_changed":"Custom","mppt1_charger_status":"Abnormal","mppt2_charger_status":"Abnormal","load_connected":"Connected","battery_power_direction":"Discharge","dc_ac_power_direction":"DC/AC","line_power_direction":"Do nothing","local_parallel_id":0}}' + +if __name__ == '__main__': + gs = json.loads(s)['data'] + # pprint(gs) + + # render response + power_direction = gs['battery_power_direction'].lower() + power_direction = re.sub(r'ge$', 'ging', power_direction) + + charging_rate = '' + if power_direction == 'charging': + charging_rate = ' @ %s %s' % (gs['battery_charging_current']['value'], gs['battery_charging_current']['unit']) + elif power_direction == 'discharging': + charging_rate = ' @ %s %s' % (gs['battery_discharging_current']['value'], gs['battery_discharging_current']['unit']) + + html = 'Battery: %s %s' % (gs['battery_voltage']['value'], gs['battery_voltage']['unit']) + html += ' (%s%s, ' % (gs['battery_capacity']['value'], gs['battery_capacity']['unit']) + html += '%s%s)' % (power_direction, charging_rate) + + html += '\nLoad: %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 += '\nInput power: %s%s' % (gs['pv1_input_power']['value'], gs['pv1_input_power']['unit']) + + if gs['grid_voltage']['value'] > 0 or gs['grid_freq']['value'] > 0: + html += '\nGenerator: %s %s' % (gs['grid_voltage']['unit'], gs['grid_voltage']['value']) + html += ', %s %s' % (gs['grid_freq']['value'], gs['grid_freq']['unit']) + + print(html) \ No newline at end of file -- cgit v1.2.3