extarct s3 data with decompression

This commit is contained in:
kostas 2024-05-30 13:19:58 +02:00
parent 2e8dab4bbe
commit bac3e17224
1 changed files with 191 additions and 212 deletions

View File

@ -5,9 +5,7 @@ import logging
import re import re
import socket import socket
import sys import sys
import typing import gobject
from gi.repository import GLib as glib
import signals import signals
import config as cfg import config as cfg
@ -25,351 +23,332 @@ from python_libs.ie_dbus.dbus_service import DBusService
if False: if False:
from typing import Callable, List, Iterable, NoReturn from typing import Callable, List, Iterable, NoReturn
RESET_REGISTER = 0x2087
SETTINGS_SERVICE_PREFIX = 'com.victronenergy.settings'
INVERTER_SERVICE_PREFIX = 'com.victronenergy.vebus.'
RESET_REGISTER = 0x2087
def init_modbus(tty): def init_modbus(tty):
# type: (str) -> Modbus # type: (str) -> Modbus
logging.debug('initializing Modbus') logging.debug('initializing Modbus')
return Modbus( return Modbus(
port='/dev/' + tty, port='/dev/' + tty,
method=cfg.MODE, method=cfg.MODE,
baudrate=cfg.BAUD_RATE, baudrate=cfg.BAUD_RATE,
stopbits=cfg.STOP_BITS, stopbits=cfg.STOP_BITS,
bytesize=cfg.BYTE_SIZE, bytesize=cfg.BYTE_SIZE,
timeout=cfg.TIMEOUT, timeout=cfg.TIMEOUT,
parity=cfg.PARITY) parity=cfg.PARITY)
def init_udp_socket(): def init_udp_socket():
# type: () -> socket # type: () -> socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setblocking(False) s.setblocking(False)
return s return s
def report_slave_id(modbus, slave_address): def report_slave_id(modbus, slave_address):
# type: (Modbus, int) -> str # type: (Modbus, int) -> str
slave = str(slave_address) slave = str(slave_address)
logging.debug('requesting slave id from node ' + slave) logging.debug('requesting slave id from node ' + slave)
with modbus: with modbus:
request = ReportSlaveIdRequest(unit=slave_address) request = ReportSlaveIdRequest(unit=slave_address)
response = modbus.execute(request) response = modbus.execute(request)
if response is ExceptionResponse or issubclass(type(response), ModbusException): if response is ExceptionResponse or issubclass(type(response), ModbusException):
raise Exception('failed to get slave id from ' + slave + ' : ' + str(response)) raise Exception('failed to get slave id from ' + slave + ' : ' + str(response))
return response.identifier return response.identifier
def identify_battery(modbus, slave_address): def identify_battery(modbus, slave_address):
# type: (Modbus, int) -> Battery # type: (Modbus, int) -> Battery
logging.info('identifying battery...') logging.info('identifying battery...')
hardware_version, bms_version, ampere_hours = parse_slave_id(modbus, slave_address) hardware_version, bms_version, ampere_hours = parse_slave_id(modbus, slave_address)
firmware_version = read_firmware_version(modbus, slave_address) firmware_version = read_firmware_version(modbus, slave_address)
specs = Battery( specs = Battery(
slave_address=slave_address, slave_address=slave_address,
hardware_version=hardware_version, hardware_version=hardware_version,
firmware_version=firmware_version, firmware_version=firmware_version,
bms_version=bms_version, bms_version=bms_version,
ampere_hours=ampere_hours) ampere_hours=ampere_hours)
logging.info('battery identified:\n{0}'.format(str(specs))) logging.info('battery identified:\n{0}'.format(str(specs)))
return specs return specs
def identify_batteries(modbus): def identify_batteries(modbus):
# type: (Modbus) -> List[Battery] # type: (Modbus) -> List[Battery]
def _identify_batteries(): def _identify_batteries():
slave_address = 0 slave_address = 0
n_missing = -255 n_missing = -255
while n_missing < 3: while n_missing < 3:
slave_address += 1 slave_address += 1
try: try:
yield identify_battery(modbus, slave_address) yield identify_battery(modbus, slave_address)
n_missing = 0 n_missing = 0
except Exception as e: except Exception as e:
logging.info('failed to identify battery at {0} : {1}'.format(str(slave_address), str(e))) logging.info('failed to identify battery at {0} : {1}'.format(str(slave_address), str(e)))
n_missing += 1 n_missing += 1
logging.info('giving up searching for further batteries') logging.info('giving up searching for further batteries')
batteries = list(_identify_batteries()) # dont be lazy! batteries = list(_identify_batteries()) # dont be lazy!
n = len(batteries) n = len(batteries)
logging.info('found ' + str(n) + (' battery' if n == 1 else ' batteries')) logging.info('found ' + str(n) + (' battery' if n == 1 else ' batteries'))
return batteries return batteries
def parse_slave_id(modbus, slave_address): def parse_slave_id(modbus, slave_address):
# type: (Modbus, int) -> (str, str, int) # type: (Modbus, int) -> (str, str, int)
slave_id = report_slave_id(modbus, slave_address) slave_id = report_slave_id(modbus, slave_address)
sid = re.sub(r'[^\x20-\x7E]', '', slave_id) # remove weird special chars sid = re.sub(r'[^\x20-\x7E]', '', slave_id) # remove weird special chars
match = re.match('(?P<hw>48TL(?P<ah>[0-9]+)) *(?P<bms>.*)', sid) match = re.match('(?P<hw>48TL(?P<ah>[0-9]+)) *(?P<bms>.*)', sid)
if match is None: if match is None:
raise Exception('no known battery found') raise Exception('no known battery found')
return match.group('hw').strip(), match.group('bms').strip(), int(match.group('ah').strip()) return match.group('hw').strip(), match.group('bms').strip(), int(match.group('ah').strip())
def read_firmware_version(modbus, slave_address): def read_firmware_version(modbus, slave_address):
# type: (Modbus, int) -> str # type: (Modbus, int) -> str
logging.debug('reading firmware version') logging.debug('reading firmware version')
with modbus: with modbus:
response = read_modbus_registers(modbus, slave_address, base_address=1054, count=1) response = read_modbus_registers(modbus, slave_address, base_address=1054, count=1)
register = response.registers[0] register = response.registers[0]
return '{0:0>4X}'.format(register) return '{0:0>4X}'.format(register)
def read_modbus_registers(modbus, slave_address, base_address=cfg.BASE_ADDRESS, count=cfg.NO_OF_REGISTERS): def read_modbus_registers(modbus, slave_address, base_address=cfg.BASE_ADDRESS, count=cfg.NO_OF_REGISTERS):
# type: (Modbus, int, int, int) -> ReadInputRegistersResponse # type: (Modbus, int, int, int) -> ReadInputRegistersResponse
logging.debug('requesting modbus registers {0}-{1}'.format(base_address, base_address + count)) logging.debug('requesting modbus registers {0}-{1}'.format(base_address, base_address + count))
return modbus.read_input_registers( return modbus.read_input_registers(
address=base_address, address=base_address,
count=count, count=count,
unit=slave_address) unit=slave_address)
def read_battery_status(modbus, battery): def read_battery_status(modbus, battery):
# type: (Modbus, Battery) -> BatteryStatus # type: (Modbus, Battery) -> BatteryStatus
""" """
Read the modbus registers containing the battery's status info. Read the modbus registers containing the battery's status info.
""" """
logging.debug('reading battery status') logging.debug('reading battery status')
with modbus: with modbus:
data = read_modbus_registers(modbus, battery.slave_address) data = read_modbus_registers(modbus, battery.slave_address)
return BatteryStatus(battery, data.registers) return BatteryStatus(battery, data.registers)
def publish_values_on_dbus(service, battery_signals, battery_statuses): def publish_values_on_dbus(service, battery_signals, battery_statuses):
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> () # type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
publish_individuals(service, battery_signals, battery_statuses) publish_individuals(service, battery_signals, battery_statuses)
publish_aggregates(service, battery_signals, battery_statuses) publish_aggregates(service, battery_signals, battery_statuses)
def publish_aggregates(service, signals, battery_statuses): def publish_aggregates(service, signals, battery_statuses):
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> () # type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
for s in signals: for s in signals:
if s.aggregate is None: if s.aggregate is None:
continue continue
values = [s.get_value(battery_status) for battery_status in battery_statuses] values = [s.get_value(battery_status) for battery_status in battery_statuses]
value = s.aggregate(values) value = s.aggregate(values)
service.own_properties.set(s.dbus_path, value, s.unit)
service.own_properties.set(s.dbus_path, value, s.unit)
def publish_individuals(service, signals, battery_statuses): def publish_individuals(service, signals, battery_statuses):
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> () # type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
for signal in signals: for signal in signals:
for battery_status in battery_statuses: for battery_status in battery_statuses:
address = battery_status.battery.slave_address address = battery_status.battery.slave_address
dbus_path = '/_Battery/' + str(address) + signal.dbus_path dbus_path = '/_Battery/' + str(address) + signal.dbus_path
value = signal.get_value(battery_status) value = signal.get_value(battery_status)
service.own_properties.set(dbus_path, value, signal.unit) service.own_properties.set(dbus_path, value, signal.unit)
def publish_service_signals(service, signals): def publish_service_signals(service, signals):
# type: (DBusService, Iterable[ServiceSignal]) -> NoReturn # type: (DBusService, Iterable[ServiceSignal]) -> NoReturn
for signal in signals: for signal in signals:
service.own_properties.set(signal.dbus_path, signal.value, signal.unit) service.own_properties.set(signal.dbus_path, signal.value, signal.unit)
def upload_status_to_innovenergy(sock, statuses): def upload_status_to_innovenergy(sock, statuses):
# type: (socket, Iterable[BatteryStatus]) -> bool # type: (socket, Iterable[BatteryStatus]) -> bool
logging.debug('upload status') logging.debug('upload status')
try: try:
for s in statuses: for s in statuses:
sock.sendto(s.serialize(), (cfg.INNOVENERGY_SERVER_IP, cfg.INNOVENERGY_SERVER_PORT)) sock.sendto(s.serialize(), (cfg.INNOVENERGY_SERVER_IP, cfg.INNOVENERGY_SERVER_PORT))
except: except:
logging.debug('FAILED') logging.debug('FAILED')
return False return False
else: else:
return True return True
def print_usage(): def print_usage():
print ('Usage: ' + __file__ + ' <serial device>') print ('Usage: ' + __file__ + ' <serial device>')
print ('Example: ' + __file__ + ' ttyUSB0') print ('Example: ' + __file__ + ' ttyUSB0')
def parse_cmdline_args(argv): def parse_cmdline_args(argv):
# type: (List[str]) -> str # type: (List[str]) -> str
if len(argv) == 0: if len(argv) == 0:
logging.info('missing command line argument for tty device') logging.info('missing command line argument for tty device')
print_usage() print_usage()
sys.exit(1) sys.exit(1)
return argv[0] return argv[0]
def reset_batteries(modbus, batteries): def reset_batteries(modbus, batteries):
# type: (Modbus, Iterable[Battery]) -> NoReturn # type: (Modbus, Iterable[Battery]) -> NoReturn
logging.info('Resetting batteries...') logging.info('Resetting batteries...')
for battery in batteries: for battery in batteries:
result = modbus.write_registers(RESET_REGISTER, [1], unit=battery.slave_address) result = modbus.write_registers(RESET_REGISTER, [1], unit=battery.slave_address)
# expecting a ModbusIOException (timeout) # expecting a ModbusIOException (timeout)
# BMS can no longer reply because it is already reset # BMS can no longer reply because it is already reset
success = isinstance(result, ModbusIOException) success = isinstance(result, ModbusIOException)
outcome = 'successfully' if success else 'FAILED to' outcome = 'successfully' if success else 'FAILED to'
logging.info('Battery {0} {1} reset'.format(str(battery.slave_address), outcome)) logging.info('Battery {0} {1} reset'.format(str(battery.slave_address), outcome))
logging.info('Shutting down fz-sonick driver') logging.info('Shutting down fz-sonick driver')
exit(0) exit(0)
alive = True # global alive flag, watchdog_task clears it, update_task sets it alive = True # global alive flag, watchdog_task clears it, update_task sets it
def create_update_task(modbus, service, batteries): def create_update_task(modbus, service, batteries):
# type: (Modbus, DBusService, Iterable[Battery]) -> Callable[[],bool] # type: (Modbus, DBusService, Iterable[Battery]) -> Callable[[],bool]
""" """
Creates an update task which runs the main update function Creates an update task which runs the main update function
and resets the alive flag and resets the alive flag
""" """
_socket = init_udp_socket() _socket = init_udp_socket()
_signals = signals.init_battery_signals() _signals = signals.init_battery_signals()
def update_task(): def update_task():
# type: () -> bool # type: () -> bool
global alive global alive
logging.debug('starting update cycle') logging.debug('starting update cycle')
# Checking if we have excess power and if so charge batteries more if service.own_properties.get('/ResetBatteries').value == 1:
reset_batteries(modbus, batteries)
target = service.remote_properties.get(get_service(SETTINGS_SERVICE_PREFIX) + '/Settings/CGwacs/AcPowerSetPoint').value or 0 statuses = [read_battery_status(modbus, battery) for battery in batteries]
actual = service.remote_properties.get(get_service(INVERTER_SERVICE_PREFIX) + '/Ac/Out/P').value or 0
if actual>target: publish_values_on_dbus(service, _signals, statuses)
service.own_properties.set('/Info/MaxChargeCurrent').value = min([battery.i_max for battery in batteries]) upload_status_to_innovenergy(_socket, statuses)
if service.own_properties.get('/ResetBatteries').value == 1: logging.debug('finished update cycle\n')
reset_batteries(modbus, batteries)
statuses = [read_battery_status(modbus, battery) for battery in batteries] alive = True
publish_values_on_dbus(service, _signals, statuses) return True
upload_status_to_innovenergy(_socket, statuses)
logging.debug('finished update cycle\n') return update_task
alive = True
return True
return update_task
def create_watchdog_task(main_loop): def create_watchdog_task(main_loop):
# type: (DBusGMainLoop) -> Callable[[],bool] # type: (DBusGMainLoop) -> Callable[[],bool]
""" """
Creates a Watchdog task that monitors the alive flag. Creates a Watchdog task that monitors the alive flag.
The watchdog kills the main loop if the alive flag is not periodically reset by the update task. The watchdog kills the main loop if the alive flag is not periodically reset by the update task.
Who watches the watchdog? Who watches the watchdog?
""" """
def watchdog_task():
# type: () -> bool
def watchdog_task(): global alive
# type: () -> bool
global alive if alive:
logging.debug('watchdog_task: update_task is alive')
alive = False
return True
else:
logging.info('watchdog_task: killing main loop because update_task is no longer alive')
main_loop.quit()
return False
if alive: return watchdog_task
logging.debug('watchdog_task: update_task is alive')
alive = False
return True
else:
logging.info('watchdog_task: killing main loop because update_task is no longer alive')
main_loop.quit()
return False
return watchdog_task
def get_service(self, prefix: str) -> Optional[unicode]:
service = next((s for s in self.available_services if s.startswith(prefix)), None)
if service is None:
raise Exception('no service matching ' + prefix + '* available')
return service
def main(argv): def main(argv):
# type: (List[str]) -> () # type: (List[str]) -> ()
logging.basicConfig(level=cfg.LOG_LEVEL) logging.basicConfig(level=cfg.LOG_LEVEL)
logging.info('starting ' + __file__) logging.info('starting ' + __file__)
tty = parse_cmdline_args(argv) tty = parse_cmdline_args(argv)
modbus = init_modbus(tty) modbus = init_modbus(tty)
batteries = identify_batteries(modbus) batteries = identify_batteries(modbus)
if len(batteries) <= 0: if len(batteries) <= 0:
sys.exit(2) sys.exit(2)
service = DBusService(service_name=cfg.SERVICE_NAME_PREFIX + tty) service = DBusService(service_name=cfg.SERVICE_NAME_PREFIX + tty)
service.own_properties.set('/ResetBatteries', value=False, writable=True) # initial value = False service.own_properties.set('/ResetBatteries', value=False, writable=True) # initial value = False
main_loop = GLib.MainLoop() main_loop = gobject.MainLoop()
service_signals = signals.init_service_signals(batteries) service_signals = signals.init_service_signals(batteries)
publish_service_signals(service, service_signals) publish_service_signals(service, service_signals)
update_task = create_update_task(modbus, service, batteries) update_task = create_update_task(modbus, service, batteries)
update_task() # run it right away, so that all props are initialized before anyone can ask update_task() # run it right away, so that all props are initialized before anyone can ask
watchdog_task = create_watchdog_task(main_loop) watchdog_task = create_watchdog_task(main_loop)
GLib.timeout_add(cfg.UPDATE_INTERVAL * 2, watchdog_task, priority = GLib.PRIORITY_LOW) # add watchdog first gobject.timeout_add(cfg.UPDATE_INTERVAL * 2, watchdog_task, priority = gobject.PRIORITY_LOW) # add watchdog first
GLib.timeout_add(cfg.UPDATE_INTERVAL, update_task, priority = GLib.PRIORITY_LOW) # call update once every update_interval gobject.timeout_add(cfg.UPDATE_INTERVAL, update_task, priority = gobject.PRIORITY_LOW) # call update once every update_interval
logging.info('starting gobject.MainLoop') logging.info('starting gobject.MainLoop')
main_loop.run() main_loop.run()
logging.info('gobject.MainLoop was shut down') logging.info('gobject.MainLoop was shut down')
sys.exit(0xFF) # reaches this only on error sys.exit(0xFF) # reaches this only on error
main(sys.argv[1:]) main(sys.argv[1:])