Merge branch 'master' of ch1p.io:homekit

This commit is contained in:
Evgeny Zinoviev 2022-10-24 04:39:09 +03:00
commit ed8f8839af
4 changed files with 110 additions and 68 deletions

View File

@ -1,10 +1,3 @@
from .monitor import ( from .monitor import InverterMonitor
ChargingEvent,
InverterMonitor,
BatteryState,
BatteryPowerDirection,
ACMode,
ACPresentEvent
)
from .inverter_wrapper import wrapper_instance from .inverter_wrapper import wrapper_instance
from .util import beautify_table from .util import beautify_table

View File

@ -1,7 +1,7 @@
import logging import logging
import time import time
from enum import Enum, auto from .types import *
from threading import Thread from threading import Thread
from typing import Callable, Optional from typing import Callable, Optional
from .inverter_wrapper import wrapper_instance as inverter from .inverter_wrapper import wrapper_instance as inverter
@ -12,55 +12,6 @@ from ..config import config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BatteryPowerDirection(Enum):
DISCHARGING = auto()
CHARGING = auto()
DO_NOTHING = auto()
class ChargingEvent(Enum):
AC_CHARGING_UNAVAILABLE_BECAUSE_SOLAR = auto()
AC_NOT_CHARGING = auto()
AC_CHARGING_STARTED = auto()
AC_DISCONNECTED = auto()
AC_CURRENT_CHANGED = auto()
AC_MOSTLY_CHARGED = auto()
AC_CHARGING_FINISHED = auto()
UTIL_CHARGING_STARTED = auto()
UTIL_CHARGING_STOPPED = auto()
UTIL_CHARGING_STOPPED_SOLAR = auto()
class ACPresentEvent(Enum):
CONNECTED = auto()
DISCONNECTED = auto()
class ChargingState(Enum):
NOT_CHARGING = auto()
AC_BUT_SOLAR = auto()
AC_WAITING = auto()
AC_OK = auto()
AC_DONE = auto()
class CurrentChangeDirection(Enum):
UP = auto()
DOWN = auto()
class BatteryState(Enum):
NORMAL = auto()
LOW = 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
@ -94,6 +45,8 @@ class InverterMonitor(Thread):
battery_event_handler: Optional[Callable] battery_event_handler: Optional[Callable]
util_event_handler: Optional[Callable] util_event_handler: Optional[Callable]
error_handler: Optional[Callable] error_handler: Optional[Callable]
osp_change_cb: Optional[Callable]
osp: Optional[OutputSourcePriority]
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -102,12 +55,14 @@ class InverterMonitor(Thread):
self.interrupted = False self.interrupted = False
self.min_allowed_current = 0 self.min_allowed_current = 0
self.ac_mode = None self.ac_mode = None
self.osp = None
# Event handlers for the bot. # Event handlers for the bot.
self.charging_event_handler = None self.charging_event_handler = None
self.battery_event_handler = None self.battery_event_handler = None
self.util_event_handler = None self.util_event_handler = None
self.error_handler = None self.error_handler = None
self.osp_change_cb = None
# Currents list, defined in the bot config. # Currents list, defined in the bot config.
self.currents = cfg.gen_currents self.currents = cfg.gen_currents
@ -156,6 +111,12 @@ class InverterMonitor(Thread):
self.min_allowed_current = min(allowed_currents) self.min_allowed_current = min(allowed_currents)
# Reading rated configuration
rated = inverter.exec('get-rated')['data']
self.osp = OutputSourcePriority.SolarBatteryUtility \
if rated['output_source_priority'] == 'Solar-Battery-Utility' \
else OutputSourcePriority.SolarUtilityBattery
# Read data and run implemented programs every 2 seconds. # Read data and run implemented programs every 2 seconds.
while not self.interrupted: while not self.interrupted:
try: try:
@ -167,6 +128,7 @@ class InverterMonitor(Thread):
ac = gs['grid_voltage']['value'] > 0 or gs['grid_freq']['value'] > 0 ac = gs['grid_voltage']['value'] > 0 or gs['grid_freq']['value'] > 0
solar = gs['pv1_input_voltage']['value'] > 0 or gs['pv2_input_voltage']['value'] > 0 solar = gs['pv1_input_voltage']['value'] > 0 or gs['pv2_input_voltage']['value'] > 0
solar_input = gs['pv1_input_power']['value']
v = float(gs['battery_voltage']['value']) v = float(gs['battery_voltage']['value'])
load_watts = int(gs['ac_output_active_power']['value']) load_watts = int(gs['ac_output_active_power']['value'])
pd = _pd_from_string(gs['battery_power_direction']) pd = _pd_from_string(gs['battery_power_direction'])
@ -177,7 +139,7 @@ class InverterMonitor(Thread):
self.gen_charging_program(ac, solar, v, pd) self.gen_charging_program(ac, solar, v, pd)
elif self.ac_mode == ACMode.UTILITIES: elif self.ac_mode == ACMode.UTILITIES:
self.utilities_monitoring_program(ac, solar, pd) self.utilities_monitoring_program(ac, solar, v, load_watts, solar_input, 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
@ -195,6 +157,9 @@ class InverterMonitor(Thread):
def utilities_monitoring_program(self, def utilities_monitoring_program(self,
ac: bool, # whether AC is connected ac: bool, # whether AC is connected
solar: bool, # whether MPPT is active solar: bool, # whether MPPT is active
v: float, # battery voltage
load_watts: int, # load, wh
solar_input: int, # input from solar panels, wh
pd: BatteryPowerDirection # current power direction pd: BatteryPowerDirection # current power direction
): ):
pd_event_send = False pd_event_send = False
@ -204,6 +169,15 @@ class InverterMonitor(Thread):
self.charging_event_handler(ChargingEvent.UTIL_CHARGING_STOPPED_SOLAR) self.charging_event_handler(ChargingEvent.UTIL_CHARGING_STOPPED_SOLAR)
pd_event_send = True pd_event_send = True
if solar:
if v <= 48 and self.osp == OutputSourcePriority.SolarBatteryUtility:
self.osp_change_cb(OutputSourcePriority.SolarUtilityBattery, solar_input=solar_input, v=v)
self.osp = OutputSourcePriority.SolarUtilityBattery
if self.osp == OutputSourcePriority.SolarUtilityBattery and solar_input >= 900:
self.osp_change_cb(OutputSourcePriority.SolarBatteryUtility, solar_input=solar_input, v=v)
self.osp = OutputSourcePriority.SolarBatteryUtility
if self.util_ac_present is None or ac != self.util_ac_present: if self.util_ac_present is None or ac != self.util_ac_present:
self.util_event_handler(ACPresentEvent.CONNECTED if ac else ACPresentEvent.DISCONNECTED) self.util_event_handler(ACPresentEvent.CONNECTED if ac else ACPresentEvent.DISCONNECTED)
self.util_ac_present = ac self.util_ac_present = ac
@ -213,6 +187,7 @@ class InverterMonitor(Thread):
if not pd_event_send and not solar: if not pd_event_send and not solar:
if pd == BatteryPowerDirection.CHARGING: if pd == BatteryPowerDirection.CHARGING:
self.charging_event_handler(ChargingEvent.UTIL_CHARGING_STARTED) self.charging_event_handler(ChargingEvent.UTIL_CHARGING_STARTED)
elif pd == BatteryPowerDirection.DISCHARGING: elif pd == BatteryPowerDirection.DISCHARGING:
self.charging_event_handler(ChargingEvent.UTIL_CHARGING_STOPPED) self.charging_event_handler(ChargingEvent.UTIL_CHARGING_STOPPED)
@ -493,9 +468,15 @@ 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_osp_need_change_callback(self, cb: Callable):
self.osp_change_cb = cb
def set_ac_mode(self, mode: ACMode): def set_ac_mode(self, mode: ACMode):
self.ac_mode = mode self.ac_mode = mode
def notify_osp(self, osp: OutputSourcePriority):
self.osp = osp
def stop(self): def stop(self):
self.interrupted = True self.interrupted = True
@ -513,6 +494,7 @@ class InverterMonitor(Thread):
'time_now': time.time(), 'time_now': time.time(),
'next_current_enter_time': self.next_current_enter_time, 'next_current_enter_time': self.next_current_enter_time,
'ac_mode': self.ac_mode, 'ac_mode': self.ac_mode,
'osp': self.osp,
'util_ac_present': self.util_ac_present, 'util_ac_present': self.util_ac_present,
'util_pd': self.util_pd.name, 'util_pd': self.util_pd.name,
'util_solar': self.util_solar 'util_solar': self.util_solar

View File

@ -0,0 +1,55 @@
from enum import Enum, auto
class BatteryPowerDirection(Enum):
DISCHARGING = auto()
CHARGING = auto()
DO_NOTHING = auto()
class ChargingEvent(Enum):
AC_CHARGING_UNAVAILABLE_BECAUSE_SOLAR = auto()
AC_NOT_CHARGING = auto()
AC_CHARGING_STARTED = auto()
AC_DISCONNECTED = auto()
AC_CURRENT_CHANGED = auto()
AC_MOSTLY_CHARGED = auto()
AC_CHARGING_FINISHED = auto()
UTIL_CHARGING_STARTED = auto()
UTIL_CHARGING_STOPPED = auto()
UTIL_CHARGING_STOPPED_SOLAR = auto()
class ACPresentEvent(Enum):
CONNECTED = auto()
DISCONNECTED = auto()
class ChargingState(Enum):
NOT_CHARGING = auto()
AC_BUT_SOLAR = auto()
AC_WAITING = auto()
AC_OK = auto()
AC_DONE = auto()
class CurrentChangeDirection(Enum):
UP = auto()
DOWN = auto()
class BatteryState(Enum):
NORMAL = auto()
LOW = auto()
CRITICAL = auto()
class ACMode(Enum):
GENERATOR = 'generator'
UTILITIES = 'utilities'
class OutputSourcePriority(Enum):
SolarUtilityBattery = 'SUB'
SolarBatteryUtility = 'SBU'

View File

@ -4,7 +4,6 @@ import re
import datetime import datetime
import json import json
from enum import Enum
from inverterd import Format, InverterError from inverterd import Format, InverterError
from html import escape from html import escape
from typing import Optional, Tuple, Union from typing import Optional, Tuple, Union
@ -20,12 +19,14 @@ from home.bot import (
from home.inverter import ( from home.inverter import (
wrapper_instance as inverter, wrapper_instance as inverter,
beautify_table, beautify_table,
InverterMonitor, InverterMonitor,
)
from home.inverter.types import (
ChargingEvent, ChargingEvent,
ACPresentEvent, ACPresentEvent,
BatteryState, BatteryState,
ACMode ACMode,
OutputSourcePriority
) )
from home.api.types import BotType from home.api.types import BotType
from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton
@ -57,11 +58,6 @@ SETACMODE_STARTED, = range(1)
SETOSP_STARTED, = range(1) SETOSP_STARTED, = range(1)
class OutputSourcePriority(Enum):
SolarUtilityBattery = 'SUB'
SolarBatteryUtility = 'SBU'
def monitor_charging(event: ChargingEvent, **kwargs) -> None: def monitor_charging(event: ChargingEvent, **kwargs) -> None:
args = [] args = []
is_util = False is_util = False
@ -135,6 +131,18 @@ def monitor_error(error: str) -> None:
) )
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'setosp_{new_osp.value.lower()}', lang), v, solar_input),
)
def full_status(ctx: Context) -> None: def full_status(ctx: Context) -> None:
status = inverter.exec('get-status', format=Format.TABLE) status = inverter.exec('get-status', format=Format.TABLE)
ctx.reply(beautify_table(status)) ctx.reply(beautify_table(status))
@ -294,6 +302,7 @@ def setacmode(mode: ACMode):
def setosp(sp: OutputSourcePriority): def setosp(sp: OutputSourcePriority):
logger.debug(f'setosp: sp={sp}') logger.debug(f'setosp: sp={sp}')
inverter.exec('set-output-source-priority', (sp.value,)) inverter.exec('set-output-source-priority', (sp.value,))
monitor.notify_osp(sp)
# /setacmode # /setacmode
@ -554,6 +563,7 @@ class InverterBot(Wrapper):
# other notifications # other notifications
ac_mode_changed_notification='Пользователь <a href="tg://user?id=%d">%s</a> установил режим AC: <b>%s</b>.', 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_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_normal='Нормальный',
bat_state_low='Низкий', bat_state_low='Низкий',
@ -630,7 +640,8 @@ class InverterBot(Wrapper):
# other notifications # other notifications
ac_mode_changed_notification='User <a href="tg://user?id=%d">%s</a> set AC mode to <b>%s</b>.', ac_mode_changed_notification='User <a href="tg://user?id=%d">%s</a> set AC mode to <b>%s</b>.',
osp_changed_notification='Пользователь <a href="tg://user?id=%d">%s</a> set output source priority: <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_normal='Normal',
bat_state_low='Low', bat_state_low='Low',
@ -743,6 +754,7 @@ if __name__ == '__main__':
monitor.set_battery_event_handler(monitor_battery) monitor.set_battery_event_handler(monitor_battery)
monitor.set_util_event_handler(monitor_util) monitor.set_util_event_handler(monitor_util)
monitor.set_error_handler(monitor_error) monitor.set_error_handler(monitor_error)
monitor.set_osp_need_change_callback(osp_change_cb)
setacmode(getacmode()) setacmode(getacmode())