Innovenergy_trunk/firmware/opt/victronenergy/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py

376 lines
10 KiB
Python
Raw Normal View History

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