Compare commits

..

No commits in common. "ace65fb21a3a42c7cbff6f70d76f704e520be181" and "d34a1c287b9208a4221f95be31074e88d2f3af9b" have entirely different histories.

97 changed files with 10640 additions and 20839 deletions

View File

@ -0,0 +1,54 @@
import serial
import logging
from data import read_file_one_line
# dbus configuration
CONNECTION = 'Modbus RTU'
PRODUCT_NAME = 'FIAMM 48TL Series Battery'
PRODUCT_ID = 0xB012 # assigned by victron
DEVICE_INSTANCE = 1
SERVICE_NAME_PREFIX = 'com.victronenergy.battery.'
# driver configuration
SOFTWARE_VERSION = '3.0.0'
UPDATE_INTERVAL = 2000 # milliseconds
LOG_LEVEL = logging.INFO
#LOG_LEVEL = logging.DEBUG
# battery config
V_MAX = 54.2
V_MIN = 42
R_STRING_MIN = 0.125
R_STRING_MAX = 0.250
I_MAX_PER_STRING = 15
AH_PER_STRING = 40
# modbus configuration
BASE_ADDRESS = 999
NO_OF_REGISTERS = 56
MAX_SLAVE_ADDRESS = 25
# RS 485 configuration
PARITY = serial.PARITY_ODD
TIMEOUT = 0.1 # seconds
BAUD_RATE = 115200
BYTE_SIZE = 8
STOP_BITS = 1
MODE = 'rtu'
# InnovEnergy IOT configuration
INSTALLATION_NAME = read_file_one_line('/data/innovenergy/openvpn/installation-name')
INNOVENERGY_SERVER_IP = '10.2.0.1'
INNOVENERGY_SERVER_PORT = 8134
INNOVENERGY_PROTOCOL_VERSION = '48TL200V3'

Binary file not shown.

View File

@ -0,0 +1,160 @@
import struct
import config as cfg
from data import LedState, BatteryStatus
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import Callable, List, Iterable, Union, AnyStr, Any
def read_bool(base_register, bit):
# type: (int, int) -> Callable[[BatteryStatus], bool]
# TODO: explain base register offset
register = base_register + int(bit/16)
bit = bit % 16
def get_value(status):
# type: (BatteryStatus) -> bool
value = status.modbus_data[register - cfg.BASE_ADDRESS]
return value & (1 << bit) > 0
return get_value
def read_float(register, scale_factor=1.0, offset=0.0):
# type: (int, float, float) -> Callable[[BatteryStatus], float]
def get_value(status):
# type: (BatteryStatus) -> float
value = status.modbus_data[register - cfg.BASE_ADDRESS]
if value >= 0x8000: # convert to signed int16
value -= 0x10000 # fiamm stores their integers signed AND with sign-offset @#%^&!
return (value + offset) * scale_factor
return get_value
def read_registers(register, count):
# type: (int, int) -> Callable[[BatteryStatus], List[int]]
start = register - cfg.BASE_ADDRESS
end = start + count
def get_value(status):
# type: (BatteryStatus) -> List[int]
return [x for x in status.modbus_data[start:end]]
return get_value
def comma_separated(values):
# type: (Iterable[str]) -> str
return ", ".join(set(values))
def count_bits(base_register, nb_of_registers, nb_of_bits, first_bit=0):
# type: (int, int, int, int) -> Callable[[BatteryStatus], int]
get_registers = read_registers(base_register, nb_of_registers)
end_bit = first_bit + nb_of_bits
def get_value(status):
# type: (BatteryStatus) -> int
registers = get_registers(status)
bin_registers = [bin(x)[-1:1:-1] for x in registers] # reverse the bits in each register so that bit0 is to the left
str_registers = [str(x).ljust(16, "0") for x in bin_registers] # add leading zeroes, so all registers are 16 chars long
bit_string = ''.join(str_registers) # join them, one long string of 0s and 1s
filtered_bits = bit_string[first_bit:end_bit] # take the first nb_of_bits bits starting at first_bit
return filtered_bits.count('1') # count 1s
return get_value
def read_led_state(register, led):
# type: (int, int) -> Callable[[BatteryStatus], int]
read_lo = read_bool(register, led * 2)
read_hi = read_bool(register, led * 2 + 1)
def get_value(status):
# type: (BatteryStatus) -> int
lo = read_lo(status)
hi = read_hi(status)
if hi:
if lo:
return LedState.blinking_fast
else:
return LedState.blinking_slow
else:
if lo:
return LedState.on
else:
return LedState.off
return get_value
# noinspection PyShadowingNames
def unit(unit):
# type: (unicode) -> Callable[[unicode], unicode]
def get_text(v):
# type: (unicode) -> unicode
return "{0}{1}".format(str(v), unit)
return get_text
def const(constant):
# type: (any) -> Callable[[any], any]
def get(*args):
return constant
return get
def mean(numbers):
# type: (List[Union[float,int]]) -> float
return float(sum(numbers)) / len(numbers)
def first(ts, default=None):
return next((t for t in ts), default)
def bitfields_to_str(lists):
# type: (List[List[int]]) -> str
def or_lists():
# type: () -> Iterable[int]
length = len(first(lists))
n_lists = len(lists)
for i in range(0, length):
e = 0
for l in range(0, n_lists):
e = e | lists[l][i]
yield e
hexed = [
'{0:0>4X}'.format(x)
for x in or_lists()
]
return ' '.join(hexed)
def pack_string(string):
# type: (AnyStr) -> Any
data = string.encode('UTF-8')
return struct.pack('B', len(data)) + data

View File

@ -0,0 +1,125 @@
import config as cfg
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import Callable, List, Optional, AnyStr, Union, Any
class LedState(object):
"""
from page 6 of the '48TLxxx ModBus Protocol doc'
"""
off = 0
on = 1
blinking_slow = 2
blinking_fast = 3
class LedColor(object):
green = 0
amber = 1
blue = 2
red = 3
class ServiceSignal(object):
def __init__(self, dbus_path, get_value_or_const, unit=''):
# type: (str, Union[Callable[[],Any],Any], Optional[AnyStr] )->None
self.get_value_or_const = get_value_or_const
self.dbus_path = dbus_path
self.unit = unit
@property
def value(self):
try:
return self.get_value_or_const() # callable
except:
return self.get_value_or_const # value
class BatterySignal(object):
def __init__(self, dbus_path, aggregate, get_value, unit=''):
# type: (str, Callable[[List[any]],any], Callable[[BatteryStatus],any], Optional[AnyStr] )->None
"""
A Signal holds all information necessary for the handling of a
certain datum (e.g. voltage) published by the battery.
:param dbus_path: str
object_path on DBus where the datum needs to be published
:param aggregate: Iterable[any] -> any
function that combines the values of multiple batteries into one.
e.g. sum for currents, or mean for voltages
:param get_value: (BatteryStatus) -> any
function to extract the datum from the modbus record,
"""
self.dbus_path = dbus_path
self.aggregate = aggregate
self.get_value = get_value
self.unit = unit
class Battery(object):
""" Data record to hold hardware and firmware specs of the battery """
def __init__(self, slave_address, hardware_version, firmware_version, bms_version, ampere_hours):
# type: (int, str, str, str, int) -> None
self.slave_address = slave_address
self.hardware_version = hardware_version
self.firmware_version = firmware_version
self.bms_version = bms_version
self.ampere_hours = ampere_hours
self.n_strings = int(ampere_hours/cfg.AH_PER_STRING)
self.i_max = self.n_strings * cfg.I_MAX_PER_STRING
self.v_min = cfg.V_MIN
self.v_max = cfg.V_MAX
self.r_int_min = cfg.R_STRING_MIN / self.n_strings
self.r_int_max = cfg.R_STRING_MAX / self.n_strings
def __str__(self):
return 'slave address = {0}\nhardware version = {1}\nfirmware version = {2}\nbms version = {3}\nampere hours = {4}'.format(
self.slave_address, self.hardware_version, self.firmware_version, self.bms_version, str(self.ampere_hours))
class BatteryStatus(object):
"""
record holding the current status of a battery
"""
def __init__(self, battery, modbus_data):
# type: (Battery, List[int]) -> None
self.battery = battery
self.modbus_data = modbus_data
def serialize(self):
# type: () -> str
b = self.battery
s = cfg.INNOVENERGY_PROTOCOL_VERSION + '\n'
s += cfg.INSTALLATION_NAME + '\n'
s += str(b.slave_address) + '\n'
s += b.hardware_version + '\n'
s += b.firmware_version + '\n'
s += b.bms_version + '\n'
s += str(b.ampere_hours) + '\n'
for d in self.modbus_data:
s += str(d) + '\n'
return s
def read_file_one_line(file_name):
with open(file_name, 'r') as file:
return file.read().replace('\n', '').replace('\r', '').strip()

Binary file not shown.

View File

@ -0,0 +1,354 @@
#!/usr/bin/python2 -u
# coding=utf-8
import logging
import re
import socket
import sys
import gobject
import signals
import config as cfg
from dbus.mainloop.glib import DBusGMainLoop
from pymodbus.client.sync import ModbusSerialClient as Modbus
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:
from typing import Callable, List, Iterable, NoReturn
RESET_REGISTER = 0x2087
def init_modbus(tty):
# type: (str) -> Modbus
logging.debug('initializing Modbus')
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)
def init_udp_socket():
# type: () -> socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setblocking(False)
return s
def report_slave_id(modbus, slave_address):
# type: (Modbus, int) -> str
slave = str(slave_address)
logging.debug('requesting slave id from node ' + slave)
with modbus:
request = ReportSlaveIdRequest(unit=slave_address)
response = modbus.execute(request)
if response is ExceptionResponse or issubclass(type(response), ModbusException):
raise Exception('failed to get slave id from ' + slave + ' : ' + str(response))
return response.identifier
def identify_battery(modbus, slave_address):
# type: (Modbus, int) -> Battery
logging.info('identifying battery...')
hardware_version, bms_version, ampere_hours = parse_slave_id(modbus, slave_address)
firmware_version = read_firmware_version(modbus, slave_address)
specs = Battery(
slave_address=slave_address,
hardware_version=hardware_version,
firmware_version=firmware_version,
bms_version=bms_version,
ampere_hours=ampere_hours)
logging.info('battery identified:\n{0}'.format(str(specs)))
return specs
def identify_batteries(modbus):
# type: (Modbus) -> List[Battery]
def _identify_batteries():
slave_address = 0
n_missing = -255
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
logging.info('giving up searching for further batteries')
batteries = list(_identify_batteries()) # dont be lazy!
n = len(batteries)
logging.info('found ' + str(n) + (' battery' if n == 1 else ' batteries'))
return batteries
def parse_slave_id(modbus, slave_address):
# type: (Modbus, int) -> (str, str, int)
slave_id = report_slave_id(modbus, slave_address)
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)
if match is None:
raise Exception('no known battery found')
return match.group('hw').strip(), match.group('bms').strip(), int(match.group('ah').strip())
def read_firmware_version(modbus, slave_address):
# type: (Modbus, int) -> str
logging.debug('reading firmware version')
with modbus:
response = read_modbus_registers(modbus, slave_address, base_address=1054, count=1)
register = response.registers[0]
return '{0:0>4X}'.format(register)
def read_modbus_registers(modbus, slave_address, base_address=cfg.BASE_ADDRESS, count=cfg.NO_OF_REGISTERS):
# type: (Modbus, int, int, int) -> ReadInputRegistersResponse
logging.debug('requesting modbus registers {0}-{1}'.format(base_address, base_address + count))
return modbus.read_input_registers(
address=base_address,
count=count,
unit=slave_address)
def read_battery_status(modbus, battery):
# type: (Modbus, Battery) -> BatteryStatus
"""
Read the modbus registers containing the battery's status info.
"""
logging.debug('reading battery status')
with modbus:
data = read_modbus_registers(modbus, battery.slave_address)
return BatteryStatus(battery, data.registers)
def publish_values_on_dbus(service, battery_signals, battery_statuses):
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
publish_individuals(service, battery_signals, battery_statuses)
publish_aggregates(service, battery_signals, battery_statuses)
def publish_aggregates(service, signals, battery_statuses):
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
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)
def publish_individuals(service, signals, battery_statuses):
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
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)
def publish_service_signals(service, signals):
# type: (DBusService, Iterable[ServiceSignal]) -> NoReturn
for signal in signals:
service.own_properties.set(signal.dbus_path, signal.value, signal.unit)
def upload_status_to_innovenergy(sock, statuses):
# type: (socket, Iterable[BatteryStatus]) -> bool
logging.debug('upload status')
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
def print_usage():
print ('Usage: ' + __file__ + ' <serial device>')
print ('Example: ' + __file__ + ' ttyUSB0')
def parse_cmdline_args(argv):
# type: (List[str]) -> str
if len(argv) == 0:
logging.info('missing command line argument for tty device')
print_usage()
sys.exit(1)
return argv[0]
def reset_batteries(modbus, batteries):
# type: (Modbus, Iterable[Battery]) -> NoReturn
logging.info('Resetting batteries...')
for battery in batteries:
result = modbus.write_registers(RESET_REGISTER, [1], unit=battery.slave_address)
# expecting a ModbusIOException (timeout)
# BMS can no longer reply because it is already reset
success = isinstance(result, ModbusIOException)
outcome = 'successfully' if success else 'FAILED to'
logging.info('Battery {0} {1} reset'.format(str(battery.slave_address), outcome))
logging.info('Shutting down fz-sonick driver')
exit(0)
alive = True # global alive flag, watchdog_task clears it, update_task sets it
def create_update_task(modbus, service, batteries):
# 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
global alive
logging.debug('starting update cycle')
if service.own_properties.get('/ResetBatteries').value == 1:
reset_batteries(modbus, batteries)
statuses = [read_battery_status(modbus, battery) for battery in batteries]
publish_values_on_dbus(service, _signals, statuses)
upload_status_to_innovenergy(_socket, statuses)
logging.debug('finished update cycle\n')
alive = True
return True
return update_task
def create_watchdog_task(main_loop):
# 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
return watchdog_task
def main(argv):
# type: (List[str]) -> ()
logging.basicConfig(level=cfg.LOG_LEVEL)
logging.info('starting ' + __file__)
tty = parse_cmdline_args(argv)
modbus = init_modbus(tty)
batteries = identify_batteries(modbus)
if len(batteries) <= 0:
sys.exit(2)
service = DBusService(service_name=cfg.SERVICE_NAME_PREFIX + tty)
service.own_properties.set('/ResetBatteries', value=False, writable=True) # initial value = False
main_loop = gobject.MainLoop()
service_signals = signals.init_service_signals(batteries)
publish_service_signals(service, service_signals)
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)
gobject.timeout_add(cfg.UPDATE_INTERVAL * 2, watchdog_task, priority = gobject.PRIORITY_LOW) # add watchdog first
gobject.timeout_add(cfg.UPDATE_INTERVAL, update_task, priority = gobject.PRIORITY_LOW) # call update once every update_interval
logging.info('starting gobject.MainLoop')
main_loop.run()
logging.info('gobject.MainLoop was shut down')
sys.exit(0xFF) # reaches this only on error
main(sys.argv[1:])

View File

@ -0,0 +1,202 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from traceback import print_exc
from os import _exit as os_exit
from os import statvfs
import logging
from functools import update_wrapper
import dbus
logger = logging.getLogger(__name__)
VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1)
# Use this function to make sure the code quits on an unexpected exception. Make sure to use it
# when using gobject.idle_add and also gobject.timeout_add.
# Without this, the code will just keep running, since gobject does not stop the mainloop on an
# exception.
# Example: gobject.idle_add(exit_on_error, myfunc, arg1, arg2)
def exit_on_error(func, *args, **kwargs):
try:
return func(*args, **kwargs)
except:
try:
print 'exit_on_error: there was an exception. Printing stacktrace will be tryed and then exit'
print_exc()
except:
pass
# sys.exit() is not used, since that throws an exception, which does not lead to a program
# halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230.
os_exit(1)
__vrm_portal_id = None
def get_vrm_portal_id():
# For the CCGX, the definition of the VRM Portal ID is that it is the mac address of the onboard-
# ethernet port (eth0), stripped from its colons (:) and lower case.
# nice coincidence is that this also works fine when running on your (linux) development computer.
global __vrm_portal_id
if __vrm_portal_id:
return __vrm_portal_id
# Assume we are on linux
import fcntl, socket, struct
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', 'eth0'[:15]))
__vrm_portal_id = ''.join(['%02x' % ord(char) for char in info[18:24]])
return __vrm_portal_id
# See VE.Can registers - public.docx for definition of this conversion
def convert_vreg_version_to_readable(version):
def str_to_arr(x, length):
a = []
for i in range(0, len(x), length):
a.append(x[i:i+length])
return a
x = "%x" % version
x = x.upper()
if len(x) == 5 or len(x) == 3 or len(x) == 1:
x = '0' + x
a = str_to_arr(x, 2);
# remove the first 00 if there are three bytes and it is 00
if len(a) == 3 and a[0] == '00':
a.remove(0);
# if we have two or three bytes now, and the first character is a 0, remove it
if len(a) >= 2 and a[0][0:1] == '0':
a[0] = a[0][1];
result = ''
for item in a:
result += ('.' if result != '' else '') + item
result = 'v' + result
return result
def get_free_space(path):
result = -1
try:
s = statvfs(path)
result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users
except Exception, ex:
logger.info("Error while retrieving free space for path %s: %s" % (path, ex))
return result
def get_load_averages():
c = read_file('/proc/loadavg')
return c.split(' ')[:3]
# Returns False if it cannot find a machine name. Otherwise returns the string
# containing the name
def get_machine_name():
c = read_file('/proc/device-tree/model')
if c != False:
return c.strip('\x00')
return read_file('/etc/venus/machine')
# Returns False if it cannot open the file. Otherwise returns its rstripped contents
def read_file(path):
content = False
try:
with open(path, 'r') as f:
content = f.read().rstrip()
except Exception, ex:
logger.debug("Error while reading %s: %s" % (path, ex))
return content
def wrap_dbus_value(value):
if value is None:
return VEDBUS_INVALID
if isinstance(value, float):
return dbus.Double(value, variant_level=1)
if isinstance(value, bool):
return dbus.Boolean(value, variant_level=1)
if isinstance(value, int):
return dbus.Int32(value, variant_level=1)
if isinstance(value, str):
return dbus.String(value, variant_level=1)
if isinstance(value, unicode):
return dbus.String(value, variant_level=1)
if isinstance(value, list):
if len(value) == 0:
# If the list is empty we cannot infer the type of the contents. So assume unsigned integer.
# A (signed) integer is dangerous, because an empty list of signed integers is used to encode
# an invalid value.
return dbus.Array([], signature=dbus.Signature('u'), variant_level=1)
return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1)
if isinstance(value, long):
return dbus.Int64(value, variant_level=1)
if isinstance(value, dict):
# Wrapping the keys of the dictionary causes D-Bus errors like:
# 'arguments to dbus_message_iter_open_container() were incorrect,
# assertion "(type == DBUS_TYPE_ARRAY && contained_signature &&
# *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL ||
# _dbus_check_is_valid_signature (contained_signature))" failed in file ...'
return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1)
return value
dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64)
def unwrap_dbus_value(val):
"""Converts D-Bus values back to the original type. For example if val is of type DBus.Double,
a float will be returned."""
if isinstance(val, dbus_int_types):
return int(val)
if isinstance(val, dbus.Double):
return float(val)
if isinstance(val, dbus.Array):
v = [unwrap_dbus_value(x) for x in val]
return None if len(v) == 0 else v
if isinstance(val, (dbus.Signature, dbus.String)):
return unicode(val)
# Python has no byte type, so we convert to an integer.
if isinstance(val, dbus.Byte):
return int(val)
if isinstance(val, dbus.ByteArray):
return "".join([str(x) for x in val])
if isinstance(val, (list, tuple)):
return [unwrap_dbus_value(x) for x in val]
if isinstance(val, (dbus.Dictionary, dict)):
# Do not unwrap the keys, see comment in wrap_dbus_value
return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()])
if isinstance(val, dbus.Boolean):
return bool(val)
return val
class reify(object):
""" Decorator to replace a property of an object with the calculated value,
to make it concrete. """
def __init__(self, wrapped):
self.wrapped = wrapped
update_wrapper(self, wrapped)
def __get__(self, inst, objtype=None):
if inst is None:
return self
v = self.wrapped(inst)
setattr(inst, self.wrapped.__name__, v)
return v

View File

@ -0,0 +1,496 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import dbus.service
import logging
import traceback
import os
import weakref
from ve_utils import wrap_dbus_value, unwrap_dbus_value
# vedbus contains three classes:
# VeDbusItemImport -> use this to read data from the dbus, ie import
# VeDbusItemExport -> use this to export data to the dbus (one value)
# VeDbusService -> use that to create a service and export several values to the dbus
# Code for VeDbusItemImport is copied from busitem.py and thereafter modified.
# All projects that used busitem.py need to migrate to this package. And some
# projects used to define there own equivalent of VeDbusItemExport. Better to
# use VeDbusItemExport, or even better the VeDbusService class that does it all for you.
# TODOS
# 1 check for datatypes, it works now, but not sure if all is compliant with
# com.victronenergy.BusItem interface definition. See also the files in
# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps
# something similar should also be done in VeDbusBusItemExport?
# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object?
# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking
# changes possible. Does everybody first invalidate its data before leaving the bus?
# And what about before taking one object away from the bus, instead of taking the
# whole service offline?
# They should! And after taking one value away, do we need to know that someone left
# the bus? Or we just keep that value in invalidated for ever? Result is that we can't
# see the difference anymore between an invalidated value and a value that was first on
# the bus and later not anymore. See comments above VeDbusItemImport as well.
# 9 there are probably more todos in the code below.
# Some thoughts with regards to the data types:
#
# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types
# ---
# Variants are represented by setting the variant_level keyword argument in the
# constructor of any D-Bus data type to a value greater than 0 (variant_level 1
# means a variant containing some other data type, variant_level 2 means a variant
# containing a variant containing some other data type, and so on). If a non-variant
# is passed as an argument but introspection indicates that a variant is expected,
# it'll automatically be wrapped in a variant.
# ---
#
# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass
# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera
#
# So all together that explains why we don't need to explicitly convert back and forth
# between the dbus datatypes and the standard python datatypes. Note that all datatypes
# in python are objects. Even an int is an object.
# The signature of a variant is 'v'.
# Export ourselves as a D-Bus service.
class VeDbusService(object):
def __init__(self, servicename, bus=None):
# dict containing the VeDbusItemExport objects, with their path as the key.
self._dbusobjects = {}
self._dbusnodes = {}
# dict containing the onchange callbacks, for each object. Object path is the key
self._onchangecallbacks = {}
# Connect to session bus whenever present, else use the system bus
self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus())
# make the dbus connection available to outside, could make this a true property instead, but ach..
self.dbusconn = self._dbusconn
# Register ourselves on the dbus, trigger an error if already in use (do_not_queue)
self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True)
# Add the root item that will return all items as a tree
self._dbusnodes['/'] = self._create_tree_export(self._dbusconn, '/', self._get_tree_dict)
logging.info("registered ourselves on D-Bus as %s" % servicename)
def _get_tree_dict(self, path, get_text=False):
logging.debug("_get_tree_dict called for %s" % path)
r = {}
px = path
if not px.endswith('/'):
px += '/'
for p, item in self._dbusobjects.items():
if p.startswith(px):
v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value())
r[p[len(px):]] = v
logging.debug(r)
return r
# To force immediate deregistering of this dbus service and all its object paths, explicitly
# call __del__().
def __del__(self):
for node in self._dbusnodes.values():
node.__del__()
self._dbusnodes.clear()
for item in self._dbusobjects.values():
item.__del__()
self._dbusobjects.clear()
if self._dbusname:
self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code
self._dbusname = None
# @param callbackonchange function that will be called when this value is changed. First parameter will
# be the path of the object, second the new value. This callback should return
# True to accept the change, False to reject it.
def add_path(self, path, value, description="", writeable=False,
onchangecallback=None, gettextcallback=None):
if onchangecallback is not None:
self._onchangecallbacks[path] = onchangecallback
item = VeDbusItemExport(
self._dbusconn, path, value, description, writeable,
self._value_changed, gettextcallback, deletecallback=self._item_deleted)
spl = path.split('/')
for i in range(2, len(spl)):
subPath = '/'.join(spl[:i])
if subPath not in self._dbusnodes and subPath not in self._dbusobjects:
self._dbusnodes[subPath] = self._create_tree_export(self._dbusconn, subPath, self._get_tree_dict)
self._dbusobjects[path] = item
logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable))
# Add the mandatory paths, as per victron dbus api doc
def add_mandatory_paths(self, processname, processversion, connection,
deviceinstance, productid, productname, firmwareversion, hardwareversion, connected):
self.add_path('/Mgmt/ProcessName', processname)
self.add_path('/Mgmt/ProcessVersion', processversion)
self.add_path('/Mgmt/Connection', connection)
# Create rest of the mandatory objects
self.add_path('/DeviceInstance', deviceinstance)
self.add_path('/ProductId', productid)
self.add_path('/ProductName', productname)
self.add_path('/FirmwareVersion', firmwareversion)
self.add_path('/HardwareVersion', hardwareversion)
self.add_path('/Connected', connected)
def _create_tree_export(self, bus, objectPath, get_value_handler):
return VeDbusTreeExport(bus, objectPath, get_value_handler)
# Callback function that is called from the VeDbusItemExport objects when a value changes. This function
# maps the change-request to the onchangecallback given to us for this specific path.
def _value_changed(self, path, newvalue):
if path not in self._onchangecallbacks:
return True
return self._onchangecallbacks[path](path, newvalue)
def _item_deleted(self, path):
self._dbusobjects.pop(path)
for np in self._dbusnodes.keys():
if np != '/':
for ip in self._dbusobjects:
if ip.startswith(np + '/'):
break
else:
self._dbusnodes[np].__del__()
self._dbusnodes.pop(np)
def __getitem__(self, path):
return self._dbusobjects[path].local_get_value()
def __setitem__(self, path, newvalue):
self._dbusobjects[path].local_set_value(newvalue)
def __delitem__(self, path):
self._dbusobjects[path].__del__() # Invalidates and then removes the object path
assert path not in self._dbusobjects
def __contains__(self, path):
return path in self._dbusobjects
"""
Importing basics:
- If when we power up, the D-Bus service does not exist, or it does exist and the path does not
yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its
initial value, which VeDbusItemImport will receive and use to update local cache. And, when set,
call the eventCallback.
- If when we power up, save it
- When using get_value, know that there is no difference between services (or object paths) that don't
exist and paths that are invalid (= empty array, see above). Both will return None. In case you do
really want to know ifa path exists or not, use the exists property.
- When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals
with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged-
signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this
class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this
class.
Read when using this class:
Note that when a service leaves that D-Bus without invalidating all its exported objects first, for
example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport,
make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor,
because that takes care of all of that for you.
"""
class VeDbusItemImport(object):
## Constructor
# @param bus the bus-object (SESSION or SYSTEM).
# @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1'
# @param path the object-path, for example '/Dc/V'
# @param eventCallback function that you want to be called on a value change
# @param createSignal only set this to False if you use this function to one time read a value. When
# leaving it to True, make sure to also subscribe to the NameOwnerChanged signal
# elsewhere. See also note some 15 lines up.
def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True):
# TODO: is it necessary to store _serviceName and _path? Isn't it
# stored in the bus_getobjectsomewhere?
self._serviceName = serviceName
self._path = path
self._match = None
# TODO: _proxy is being used in settingsdevice.py, make a getter for that
self._proxy = bus.get_object(serviceName, path, introspect=False)
self.eventCallback = eventCallback
assert eventCallback is None or createsignal == True
if createsignal:
self._match = self._proxy.connect_to_signal(
"PropertiesChanged", weak_functor(self._properties_changed_handler))
# store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to
# None, same as when a value is invalid
self._cachedvalue = None
try:
v = self._proxy.GetValue()
except dbus.exceptions.DBusException:
pass
else:
self._cachedvalue = unwrap_dbus_value(v)
def __del__(self):
if self._match != None:
self._match.remove()
self._match = None
self._proxy = None
def _refreshcachedvalue(self):
self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue())
## Returns the path as a string, for example '/AC/L1/V'
@property
def path(self):
return self._path
## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1
@property
def serviceName(self):
return self._serviceName
## Returns the value of the dbus-item.
# the type will be a dbus variant, for example dbus.Int32(0, variant_level=1)
# this is not a property to keep the name consistant with the com.victronenergy.busitem interface
# returns None when the property is invalid
def get_value(self):
return self._cachedvalue
## Writes a new value to the dbus-item
def set_value(self, newvalue):
r = self._proxy.SetValue(wrap_dbus_value(newvalue))
# instead of just saving the value, go to the dbus and get it. So we have the right type etc.
if r == 0:
self._refreshcachedvalue()
return r
## Returns the text representation of the value.
# For example when the value is an enum/int GetText might return the string
# belonging to that enum value. Another example, for a voltage, GetValue
# would return a float, 12.0Volt, and GetText could return 12 VDC.
#
# Note that this depends on how the dbus-producer has implemented this.
def get_text(self):
return self._proxy.GetText()
## Returns true of object path exists, and false if it doesn't
@property
def exists(self):
# TODO: do some real check instead of this crazy thing.
r = False
try:
r = self._proxy.GetValue()
r = True
except dbus.exceptions.DBusException:
pass
return r
## callback for the trigger-event.
# @param eventCallback the event-callback-function.
@property
def eventCallback(self):
return self._eventCallback
@eventCallback.setter
def eventCallback(self, eventCallback):
self._eventCallback = eventCallback
## Is called when the value of the imported bus-item changes.
# Stores the new value in our local cache, and calls the eventCallback, if set.
def _properties_changed_handler(self, changes):
if "Value" in changes:
changes['Value'] = unwrap_dbus_value(changes['Value'])
self._cachedvalue = changes['Value']
if self._eventCallback:
# The reason behind this try/except is to prevent errors silently ending up the an error
# handler in the dbus code.
try:
self._eventCallback(self._serviceName, self._path, changes)
except:
traceback.print_exc()
os._exit(1) # sys.exit() is not used, since that also throws an exception
class VeDbusTreeExport(dbus.service.Object):
def __init__(self, bus, objectPath, get_value_handler):
dbus.service.Object.__init__(self, bus, objectPath)
self._get_value_handler = get_value_handler
logging.debug("VeDbusTreeExport %s has been created" % objectPath)
def __del__(self):
# self._get_path() will raise an exception when retrieved after the call to .remove_from_connection,
# so we need a copy.
path = self._get_path()
if path is None:
return
self.remove_from_connection()
logging.debug("VeDbusTreeExport %s has been removed" % path)
def _get_path(self):
if len(self._locations) == 0:
return None
return self._locations[0][1]
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
def GetValue(self):
value = self._get_value_handler(self._get_path())
return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1)
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
def GetText(self):
return self._get_value_handler(self._get_path(), True)
def local_get_value(self):
return self._get_value_handler(self.path)
class VeDbusItemExport(dbus.service.Object):
## Constructor of VeDbusItemExport
#
# Use this object to export (publish), values on the dbus
# Creates the dbus-object under the given dbus-service-name.
# @param bus The dbus object.
# @param objectPath The dbus-object-path.
# @param value Value to initialize ourselves with, defaults to None which means Invalid
# @param description String containing a description. Can be called over the dbus with GetDescription()
# @param writeable what would this do!? :).
# @param callback Function that will be called when someone else changes the value of this VeBusItem
# over the dbus. First parameter passed to callback will be our path, second the new
# value. This callback should return True to accept the change, False to reject it.
def __init__(self, bus, objectPath, value=None, description=None, writeable=False,
onchangecallback=None, gettextcallback=None, deletecallback=None):
dbus.service.Object.__init__(self, bus, objectPath)
self._onchangecallback = onchangecallback
self._gettextcallback = gettextcallback
self._value = value
self._description = description
self._writeable = writeable
self._deletecallback = deletecallback
# To force immediate deregistering of this dbus object, explicitly call __del__().
def __del__(self):
# self._get_path() will raise an exception when retrieved after the
# call to .remove_from_connection, so we need a copy.
path = self._get_path()
if path == None:
return
if self._deletecallback is not None:
self._deletecallback(path)
self.local_set_value(None)
self.remove_from_connection()
logging.debug("VeDbusItemExport %s has been removed" % path)
def _get_path(self):
if len(self._locations) == 0:
return None
return self._locations[0][1]
## Sets the value. And in case the value is different from what it was, a signal
# will be emitted to the dbus. This function is to be used in the python code that
# is using this class to export values to the dbus.
# set value to None to indicate that it is Invalid
def local_set_value(self, newvalue):
if self._value == newvalue:
return
self._value = newvalue
changes = {}
changes['Value'] = wrap_dbus_value(newvalue)
changes['Text'] = self.GetText()
self.PropertiesChanged(changes)
def local_get_value(self):
return self._value
# ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ====
## Dbus exported method SetValue
# Function is called over the D-Bus by other process. It will first check (via callback) if new
# value is accepted. And it is, stores it and emits a changed-signal.
# @param value The new value.
# @return completion-code When successful a 0 is return, and when not a -1 is returned.
@dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i')
def SetValue(self, newvalue):
if not self._writeable:
return 1 # NOT OK
newvalue = unwrap_dbus_value(newvalue)
if newvalue == self._value:
return 0 # OK
# call the callback given to us, and check if new value is OK.
if (self._onchangecallback is None or
(self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))):
self.local_set_value(newvalue)
return 0 # OK
return 2 # NOT OK
## Dbus exported method GetDescription
#
# Returns the a description.
# @param language A language code (e.g. ISO 639-1 en-US).
# @param length Lenght of the language string.
# @return description
@dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s')
def GetDescription(self, language, length):
return self._description if self._description is not None else 'No description given'
## Dbus exported method GetValue
# Returns the value.
# @return the value when valid, and otherwise an empty array
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
def GetValue(self):
return wrap_dbus_value(self._value)
## Dbus exported method GetText
# Returns the value as string of the dbus-object-path.
# @return text A text-value. '---' when local value is invalid
@dbus.service.method('com.victronenergy.BusItem', out_signature='s')
def GetText(self):
if self._value is None:
return '---'
# Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we
# have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from
# the application itself, as all data from the D-Bus should have been unwrapped by now.
if self._gettextcallback is None and type(self._value) == dbus.Byte:
return str(int(self._value))
if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId':
return "0x%X" % self._value
if self._gettextcallback is None:
return str(self._value)
return self._gettextcallback(self.__dbus_object_path__, self._value)
## The signal that indicates that the value has changed.
# Other processes connected to this BusItem object will have subscribed to the
# event when they want to track our state.
@dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}')
def PropertiesChanged(self, changes):
pass
## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference
## to the object which method is to be called.
## Use this object to break circular references.
class weak_functor:
def __init__(self, f):
self._r = weakref.ref(f.__self__)
self._f = weakref.ref(f.__func__)
def __call__(self, *args, **kargs):
r = self._r()
f = self._f()
if r == None or f == None:
return
f(r, *args, **kargs)

View File

@ -0,0 +1,54 @@
from logging import getLogger
from python_libs.ie_utils.mixins import Disposable, RequiresMainLoop, Record
from python_libs.ie_dbus.private.dbus_daemon import DBusDaemon
from python_libs.ie_dbus.private.own_properties import OwnProperties
from python_libs.ie_dbus.private.remote_properties import RemoteProperties
from python_libs.ie_dbus.private.ve_constants import SERVICE_PREFIX
from python_libs.ie_dbus.private.settings import Settings
_log = getLogger(__name__)
# noinspection PyUnreachableCode
if False:
from typing import Union, AnyStr, NoReturn, List
def _enforce_ve_prefix(service_name_filter):
if not service_name_filter.startswith(SERVICE_PREFIX):
raise ValueError('service_name_filter must start with ' + SERVICE_PREFIX)
SESSION_BUS = 0
SYSTEM_BUS = 1
class DBusService(Record, Disposable, RequiresMainLoop):
def __init__(self, service_name=None, device_instance=1, connection_type_or_address=SYSTEM_BUS):
# type: (str, int, Union[int, AnyStr]) -> NoReturn
service_name = service_name if service_name.startswith(SERVICE_PREFIX) else SERVICE_PREFIX + service_name
self._daemon = DBusDaemon(connection_type_or_address)
self.remote_properties = RemoteProperties(self._daemon)
self.own_properties = OwnProperties(self._daemon)
self.own_properties.set('/DeviceInstance', device_instance) # must be set before request_name, sigh
self.settings = Settings(self._daemon, self.remote_properties)
self.name = service_name
if service_name is not None:
self._bus_name = self._daemon.request_name(service_name)
_log.info('service name is ' + service_name)
_log.info('id is ' + self.bus_id)
@property
def available_services(self):
# type: () -> List[unicode]
return [s.name for s in self._daemon.services]
@property
def bus_id(self):
# type: () -> unicode
return self._daemon.bus_id

View File

@ -0,0 +1,22 @@
from logging import getLogger
from python_libs.ie_utils.mixins import Record
_log = getLogger(__name__)
# noinspection PyUnreachableCode
if False:
from typing import AnyStr
class ServiceInfo(Record):
# noinspection PyShadowingBuiltins
def __init__(self, name, id, pid, proc_name, cmd):
# type: (AnyStr, AnyStr, int, str, str) -> ServiceInfo
self.proc_name = proc_name
self.name = name
self.id = id
self.cmd = cmd
self.pid = pid

View File

@ -0,0 +1,185 @@
from logging import getLogger
from _dbus_bindings import Connection, MethodCallMessage, SignalMessage, BUS_DAEMON_NAME, \
BUS_DAEMON_PATH, BUS_DAEMON_IFACE, NAME_FLAG_DO_NOT_QUEUE, Message, HANDLER_RESULT_HANDLED
from python_libs.ie_dbus.private.dbus_types import dbus_string, dbus_uint32
from python_libs.ie_dbus.private.message_types import DBusException
from python_libs.ie_utils.mixins import Disposable
_log = getLogger(__name__)
# noinspection PyUnreachableCode
if False:
from typing import List, Optional, Iterable, Callable, Union, NoReturn, AnyStr, Any
from python_libs.ie_dbus.private.dbus_types import DbusType
class DbusConnection(Disposable):
"""
A collection of stateless functions operating on a Connection object
"""
def __init__(self, connection_type_or_address):
# type: (Union[int, AnyStr]) -> NoReturn
self._address = connection_type_or_address
# noinspection PyProtectedMember
self._connection = Connection._new_for_bus(connection_type_or_address) # it's not disposable
self.chain_disposable(self._connection.close, 'connection ' + self._connection.get_unique_name())
@property
def bus_id(self):
return self._connection.get_unique_name()
def fork(self):
return DbusConnection(self._address)
def get_ids_and_service_names(self):
# type: () -> Iterable[unicode]
# noinspection PyTypeChecker
return map(unicode, self.call_daemon_method('ListNames')[0])
def get_service_names(self):
# type: () -> Iterable[AnyStr]
return (
unicode(name)
for name
in self.get_ids_and_service_names()
if not name.startswith(':')
)
def get_service_ids(self):
# type: () -> Iterable[AnyStr]
return (
name
for name in self.get_ids_and_service_names() if name.startswith(':'))
# noinspection PyBroadException
def get_pid_of_service(self, service_name):
# type: (AnyStr) -> Optional[int]
try:
reply = self.call_daemon_method('GetConnectionUnixProcessID', dbus_string(service_name))
return int(reply[0])
except:
return None
def get_id_of_service(self, service_name):
# type: (AnyStr) -> AnyStr
reply = self.call_daemon_method('GetNameOwner', dbus_string(service_name))
return unicode(reply[0])
def call_method(self, service_name, object_path, interface, member, *args):
# type: (AnyStr, AnyStr, Optional[str], str, List[Any]) -> List[Any]
msg = MethodCallMessage(service_name, object_path, interface, member)
for arg in args:
msg.append(arg)
reply = self._connection.send_message_with_reply_and_block(msg) # with py3 we could use asyncio here
DBusException.raise_if_error_reply(reply)
return reply.get_args_list() # TODO: utf8_strings=True ?
def send_message(self, msg):
# type: (Message) -> NoReturn
self._connection.send_message(msg)
def call_daemon_method(self, method_name, *args):
# type: (AnyStr, Iterable[DbusType])-> List[any]
return self.call_method(BUS_DAEMON_NAME, BUS_DAEMON_PATH, BUS_DAEMON_IFACE, method_name, *args)
def request_name(self, service_name):
# type: (AnyStr) -> Disposable
_log.debug('requesting bus name ' + service_name)
self.call_daemon_method('RequestName', dbus_string(service_name), dbus_uint32(NAME_FLAG_DO_NOT_QUEUE))
def dispose():
self.call_daemon_method('ReleaseName', dbus_string(service_name))
return self.create_dependent_disposable(dispose, 'bus name ' + service_name)
def broadcast_signal(self, object_path, interface, member, *args):
# type: (AnyStr, AnyStr, AnyStr, List[Any]) -> NoReturn
msg = SignalMessage(object_path, interface, member)
for arg in args:
msg.append(arg)
self._connection.send_message(msg)
def add_message_callback(self, callback, filter_rule, fork=True):
# type: (Callable[[Message], NoReturn], AnyStr, Optional[bool]) -> Disposable
if fork:
return self._add_message_callback_fork(callback, filter_rule)
else:
return self._add_message_callback_no_fork(callback, filter_rule)
def _add_message_callback_no_fork(self, callback, filter_rule): # TODO: forking for incoming method calls
# type: (Callable[[Message], NoReturn], AnyStr) -> Disposable
def dispatch(_, msg):
# type: (Connection, Message) -> int
#_log.info(' ####### got message type=' + str(msg.get_type()) + ' ' + msg.get_path() + '/' + msg.get_member())
callback(msg)
#_log.debug('DONE')
return HANDLER_RESULT_HANDLED
msg_filter = self._add_message_filter(dispatch)
match = self._add_match(filter_rule)
def dispose():
match.dispose()
msg_filter.dispose()
return self.create_dependent_disposable(dispose)
def _add_message_callback_fork(self, callback, filter_rule):
# type: (Callable[[Message], NoReturn], AnyStr) -> Disposable
forked = self.fork()
_log.debug('forked connection ' + forked.bus_id)
def dispatch(_, msg):
# type: (Connection, Message) -> int
# _log.debug('got message type=' + str(msg.get_type()) + ' ' + msg.get_path() + '/' + msg.get_member())
callback(msg)
return HANDLER_RESULT_HANDLED
forked._add_message_filter(dispatch)
forked._add_match(filter_rule)
return self.create_dependent_disposable(forked)
def _add_message_filter(self, callback):
# type: (Callable[[Connection, Message], int]) -> Disposable
_log.debug('added filter on ' + self.bus_id)
self._connection.add_message_filter(callback)
def dispose():
self._connection.remove_message_filter(callback)
return self.create_dependent_disposable(dispose, 'message filter on ' + self.bus_id)
def _add_match(self, filter_rule):
# type: (AnyStr) -> Disposable
self.call_daemon_method('AddMatch', dbus_string(filter_rule))
_log.debug('added match_rule: ' + filter_rule)
def dispose():
self.call_daemon_method('RemoveMatch', dbus_string(filter_rule))
return self.create_dependent_disposable(dispose, 'Match ' + filter_rule)

View File

@ -0,0 +1,273 @@
from logging import getLogger
from _dbus_bindings import Message, ErrorMessage, BUS_DAEMON_NAME, BUS_DAEMON_PATH, BUS_DAEMON_IFACE
from python_libs.ie_dbus.private.datatypes import ServiceInfo
from python_libs.ie_dbus.private.dbus_connection import DbusConnection
from python_libs.ie_dbus.private.message_types import MatchedMessage, MessageFilter, ResolvedMessage
from python_libs.ie_utils.mixins import Disposable, RequiresMainLoop
_log = getLogger(__name__)
NONE = '<none>'
# noinspection PyUnreachableCode
if False:
from typing import Callable, List, Optional, Iterable, Union, AnyStr, NoReturn, Any, Dict
from python_libs.ie_dbus.private.dbus_types import DbusType
class DBusDaemon(Disposable, RequiresMainLoop):
_services = None # type: Dict[str, ServiceInfo]
def __init__(self, connection_type_or_address):
# type: (Union[int, AnyStr]) -> NoReturn
self._dbus = DbusConnection(connection_type_or_address)
# self._dbus.add_message_callback(lambda _: None, 'type=method_call', fork=False) # sink method calls, TODO
self._name_changed = self.subscribe_to_signal_message(
self._on_name_owner_changed,
sender_id=BUS_DAEMON_NAME,
object_path=BUS_DAEMON_PATH,
interface=BUS_DAEMON_IFACE,
member='NameOwnerChanged')
self._services = self._init_services()
@property
def bus_id(self):
# type: () -> AnyStr
return self._dbus.bus_id
@property
def services(self):
# type: () -> Iterable[ServiceInfo]
return self._services.itervalues()
def subscribe_to_signal_message(
self,
callback,
sender_id='*',
sender_name='*',
object_path='*',
interface='*',
member='*',
signature='*'):
# type: (Callable[[MatchedMessage], None], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr]) -> Disposable
message_filter = MessageFilter(
message_type='signal',
sender_id=sender_id,
sender_name=sender_name,
object_path=object_path,
interface=interface,
member=member,
signature=signature)
def dispatch(msg):
# type: (Message) -> NoReturn
resolved_msg = self._resolve_message(msg)
matched = message_filter.match_message(resolved_msg)
if matched is not None:
callback(matched)
return self._dbus.add_message_callback(dispatch, message_filter.filter_rule)
def subscribe_to_method_call_message(
self,
callback,
sender_id='*',
sender_name='*',
object_path='*',
interface='*',
member='*',
signature='*',
destination_id='*',
destination_name='*'):
# type: (Callable[[MatchedMessage], Any], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[bool]) -> Disposable
message_filter = MessageFilter(
message_type='method_call',
sender_id=sender_id,
sender_name=sender_name,
object_path=object_path,
interface=interface,
member=member,
signature=signature,
destination_id=destination_id,
destination_name=destination_name) # TODO: eavesdrop logic
def dispatch(msg):
# type: (Message) -> NoReturn
if msg.get_type() != 1:
return
resolved_msg = self._resolve_message(msg)
matched = message_filter.match_message(resolved_msg)
if matched is None:
reply = ErrorMessage(msg, 'com.victronenergy.method_call_refused', 'refused')
else:
try:
result = callback(matched)
except Exception as e:
# _log.debug('method_call threw an exception ' + str(e))
# traceback.print_exc()
reply = matched.create_error_reply(e)
else:
reply = matched.create_method_reply(result)
self._dbus.send_message(reply)
return self._dbus.add_message_callback(dispatch, message_filter.filter_rule, fork=False)
def request_name(self, service_name):
# type: (AnyStr) -> Disposable
return self._dbus.request_name(service_name)
def call_method(self, service_name, object_path, interface, member, *args):
# type: (AnyStr, AnyStr, AnyStr, AnyStr, Iterable[DbusType]) -> List[Any]
return self._dbus.call_method(service_name, object_path, interface, member, *args)
def broadcast_signal(self, object_path, interface, member, *args):
# type: (AnyStr, AnyStr, AnyStr, List[DbusType]) -> NoReturn
self._dbus.broadcast_signal(object_path, interface, member, *args)
def get_service_names_of_id(self, service_id):
# type: (str) -> List[AnyStr]
if service_id is None:
return []
return [
s.name
for s in self.services
if s.id == service_id
]
def get_id_for_service_name(self, service_name):
# type: (AnyStr) -> Optional[AnyStr]
return next((s.id for s in self.services if s.name == service_name), None)
def exists_service_with_name(self, service_name):
# type: (AnyStr) -> bool
return self.get_id_for_service_name(service_name) is not None
def _resolve_message(self, msg):
# type: (Message) -> ResolvedMessage
sender_id, sender_names = self._resolve_name(msg.get_sender())
destination_id, destination_names = self._resolve_name(msg.get_destination())
return ResolvedMessage(msg, sender_id, sender_names, destination_id, destination_names)
# noinspection PyShadowingBuiltins
def _resolve_name(self, name):
# type: (str) -> (str, List[str])
if name is None:
id = NONE
names = []
elif name.startswith(':'):
id = name
names = self.get_service_names_of_id(name)
else:
id = self.get_id_for_service_name(name)
names = [name]
return id, names
def _on_name_owner_changed(self, msg):
# type: (MatchedMessage) -> NoReturn
(name, old_id, new_id) = msg.arguments
old_id = old_id.strip()
new_id = new_id.strip()
name = name.strip()
if name.startswith(':'):
name = None
added = old_id == '' and new_id != ''
changed = old_id != '' and new_id != ''
removed = old_id != '' and new_id == ''
# 'changed' is dispatched as 'removed' followed by 'added'
if removed or changed:
self._services.pop(old_id, None)
if added or changed:
service = self._create_service(name, new_id)
self._services[new_id] = service
# noinspection PyShadowingBuiltins
def _init_services(self):
# type: () -> Dict[str, ServiceInfo]
services = dict()
names_and_ids = self._dbus.get_ids_and_service_names()
ids = set([i for i in names_and_ids if i.startswith(':')])
names = [n for n in names_and_ids if not n.startswith(':')]
for service_name in names:
service = self._create_service(service_name)
services[service.id] = service
ids.discard(service.id)
self._services = services # UGLY, because _create_service below references it.
for id in ids:
services[id] = self._create_service(id=id)
return services
def _search_service_name_by_pid(self, pid):
# type: (int) -> Optional[AnyStr]
return next((s.name for s in self.services if s.pid == pid and s.name != NONE), NONE)
# noinspection PyShadowingBuiltins
def _create_service(self, name=None, id=None):
# type: (Optional[AnyStr], Optional[AnyStr]) -> ServiceInfo
id = id or self._dbus.get_id_of_service(name)
pid = self._dbus.get_pid_of_service(id)
proc = self._get_process_name_of_pid(pid)
cmd = self._get_commandline_of_pid(pid)
name = name or self._search_service_name_by_pid(pid)
return ServiceInfo(name, id, pid, proc, cmd)
# noinspection PyBroadException
@staticmethod
def _get_process_name_of_pid(service_pid):
# type: (int) -> str
try:
with open('/proc/{0}/comm'.format(service_pid)) as proc:
return proc.read().replace('\0', ' ').rstrip()
except Exception as _:
return '<unknown>'
# noinspection PyBroadException
@staticmethod
def _get_commandline_of_pid(service_pid):
# type: (int) -> str
try:
with open('/proc/{0}/cmdline'.format(service_pid)) as proc:
return proc.read().replace('\0', ' ').rstrip()
except Exception as _:
return '<unknown>'

View File

@ -0,0 +1,139 @@
from logging import getLogger
import dbus
_log = getLogger(__name__)
# noinspection PyUnreachableCode
if False:
from typing import Any, Union, Dict
DbusString = Union[dbus.String, dbus.UTF8String, dbus.ObjectPath, dbus.Signature]
DbusInt = Union[dbus.Int16, dbus.Int32, dbus.Int64]
DbusDouble = dbus.Double
DbusBool = dbus.Boolean
DbusStringVariant = DbusString # TODO: variant_level constraint ?
DbusIntVariant = DbusInt
DbusDoubleVariant = DbusDouble
DbusBoolVariant = DbusBool
DbusValue = Union[DbusString, DbusInt, DbusDouble, DbusBool, DBUS_NONE]
DbusVariant = Union[DbusStringVariant, DbusIntVariant, DbusDoubleVariant, DbusBoolVariant, DBUS_NONE]
DbusTextDict = dbus.Dictionary
DbusVariantDict = dbus.Dictionary
DbusType = Union[DbusValue, DbusVariant, DbusVariantDict, DbusTextDict]
DBUS_NONE = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) # DEFINED by victron
MAX_INT16 = 2 ** 15 - 1
MAX_INT32 = 2 ** 31 - 1
def dbus_uint32(value):
# type: (int) -> dbus.UInt32
if value < 0:
raise Exception('cannot convert negative value to UInt32')
return dbus.UInt32(value)
def dbus_int(value):
# type: (Union[int, long]) -> Union[dbus.Int16, dbus.Int32, dbus.Int64]
abs_value = abs(value)
if abs_value < MAX_INT16:
return dbus.Int16(value)
elif abs_value < MAX_INT32:
return dbus.Int32(value)
else:
return dbus.Int64(value)
def dbus_string(value):
# type: (Union[str, unicode]) -> DbusString
if isinstance(value, unicode):
return dbus.UTF8String(value)
else:
return dbus.String(value)
def dbus_double(value):
# type: (float) -> DbusDouble
return dbus.Double(value)
def dbus_bool(value):
# type: (bool) -> DbusBool
return dbus.Boolean(value)
# VARIANTS
def dbus_int_variant(value):
# type: (Union[int, long]) -> DbusIntVariant
abs_value = abs(value)
if abs_value < MAX_INT16:
return dbus.Int16(value, variant_level=1)
elif abs_value < MAX_INT32:
return dbus.Int32(value, variant_level=1)
else:
return dbus.Int64(value, variant_level=1)
def dbus_string_variant(value):
# type: (Union[str, unicode]) -> DbusStringVariant
if isinstance(value, unicode):
return dbus.UTF8String(value, variant_level=1)
else:
return dbus.String(value, variant_level=1)
def dbus_double_variant(value):
# type: (float) -> DbusDoubleVariant
return dbus.Double(value, variant_level=1)
def dbus_bool_variant(value):
# type: (bool) -> DbusBoolVariant
return dbus.Boolean(value, variant_level=1)
def dbus_variant(value):
# type: (Any) -> DbusVariant
if value is None:
return DBUS_NONE
if isinstance(value, float):
return dbus_double_variant(value)
if isinstance(value, bool):
return dbus_bool_variant(value)
if isinstance(value, (int, long)):
return dbus_int_variant(value)
if isinstance(value, (str, unicode)):
return dbus_string_variant(value)
# TODO: container types
raise TypeError('unsupported python type: ' + str(type(value)) + ' ' + str(value))
def dbus_value(value):
# type: (Any) -> DbusVariant
if value is None:
return DBUS_NONE
if isinstance(value, float):
return dbus_double(value)
if isinstance(value, bool):
return dbus_bool(value)
if isinstance(value, (int, long)):
return dbus_int(value)
if isinstance(value, (str, unicode)):
return dbus_string_variant(value)
# TODO: container types
raise TypeError('unsupported python type: ' + str(type(value)) + ' ' + str(value))

View File

@ -0,0 +1,259 @@
from fnmatch import fnmatch as glob
from logging import getLogger
from _dbus_bindings import ErrorMessage, Message, MethodReturnMessage
from python_libs.ie_utils.mixins import Record
_log = getLogger(__name__)
# noinspection PyUnreachableCode
if False:
from typing import List, Optional, Iterable, AnyStr, NoReturn, Any
class MessageType(object):
invalid = 0
method_call = 1
method_return = 2
error = 3
signal = 4
@staticmethod
def parse(message_type):
# type: (int) -> str
if message_type == 1:
return 'method_call'
if message_type == 2:
return 'method_return'
if message_type == 3:
return 'error'
if message_type == 4:
return 'signal'
return 'invalid'
class DBusMessage(Record):
def __init__(self, msg, sender_id, destination_id):
# type: (Message, str, str) -> NoReturn
self.sender_id = sender_id
self.destination_id = destination_id
self._msg = msg
@property
def expects_reply(self):
# type: () -> bool
return not self._msg.get_no_reply()
@property
def message_type(self):
# type: () -> int
return int(self._msg.get_type())
@property
def reply_serial(self):
# type: () -> int
return int(self._msg.get_reply_serial())
@property
def object_path(self):
# type: () -> str
return str(self._msg.get_path())
@property
def interface(self):
# type: () -> str
return str(self._msg.get_interface())
@property
def arguments(self):
# type: () -> List[Any]
return self._msg.get_args_list(utf8_strings=True)
@property
def signature(self):
# type: () -> str
return str(self._msg.get_signature())
@property
def serial(self):
# type: () -> int
return int(self._msg.get_serial())
@property
def member(self):
# type: () -> str
return str(self._msg.get_member())
def create_method_reply(self, *args):
# type: (List[any]) -> MethodReturnMessage
if self.message_type != MessageType.method_call:
raise Exception('cannot create a reply for a message that is not a method call')
reply = MethodReturnMessage(self._msg)
for arg in args:
reply.append(arg)
return reply
def create_error_reply(self, exception):
# type: (Exception) -> ErrorMessage
if self.message_type != MessageType.method_call:
raise Exception('cannot create an error reply for a message that is not a method call')
return ErrorMessage(self._msg, 'com.victronenergy.' + exception.__class__.__name__, exception.message) # TODO prefix
class ResolvedMessage(DBusMessage):
def __init__(self, msg, sender_id, sender_names, destination_id, destination_names):
# type: (Message, str, List[str], str, List[str]) -> NoReturn
super(ResolvedMessage, self).__init__(msg, sender_id, destination_id)
self.sender_names = sender_names
self.destination_names = destination_names
class MatchedMessage(DBusMessage):
def __init__(self, resolved_msg, sender_name, destination_name):
# type: (ResolvedMessage, str, str) -> NoReturn
super(MatchedMessage, self).__init__(resolved_msg._msg, resolved_msg.sender_id, resolved_msg.destination_id)
self.sender_name = sender_name
self.destination_name = destination_name
class MessageFilter(Record):
def __init__(
self,
message_type='*',
sender_id='*',
sender_name='*',
object_path='*',
interface='*',
member='*',
signature='*',
destination_id='*',
destination_name='*',
eavesdrop=False):
# type: (Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[bool]) -> NoReturn
self.signature = signature
self.message_type = message_type
self.member = member
self.interface = interface
self.object_path = object_path
self.sender_id = sender_id
self.sender_name = sender_name
self.destination_id = destination_id
self.destination_name = destination_name
self.eavesdrop = eavesdrop
@staticmethod
def create_filter_rule(
message_type='*',
sender_id='*',
sender_name='*',
object_path='*',
interface='*',
member='*',
destination_id='*',
eavesdrop=False):
# type: (Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],bool) -> AnyStr
rules = []
def rule(key, value):
if '*' not in value and '?' not in value:
rules.append("%s='%s'" % (key, value))
rule('type', message_type)
rule('sender', sender_id if sender_name == '*' and sender_id != '*' else sender_name)
rule('destination', destination_id)
rule('eavesdrop', 'true' if eavesdrop else 'false')
rule('path', object_path) # TODO: endswith *, object namespace
rule('interface', interface)
rule('member', member)
return ','.join(rules)
@property
def filter_rule(self):
# type: () -> AnyStr
return self.create_filter_rule(
message_type=self.message_type,
sender_id=self.sender_id,
sender_name=self.sender_name,
object_path=self.object_path,
interface=self.interface,
member=self.member,
destination_id=self.destination_id,
eavesdrop=self.eavesdrop)
@staticmethod
def _get_matching_name(names, name_filter):
# type: (Iterable[AnyStr], AnyStr) -> Optional[AnyStr]
matching_names = (
name
for name
in names
if glob(name, name_filter)
)
return next(matching_names, None)
def match_message(self, msg):
# type: (ResolvedMessage) -> Optional[MatchedMessage]
match = \
glob(msg.object_path, self.object_path) and \
glob(msg.interface or '<none>', self.interface) and \
glob(msg.member, self.member) and \
glob(msg.signature, self.signature) and \
glob(msg.sender_id, self.sender_id) and \
glob(msg.destination_id or '<none>', self.destination_id)
if not match:
return None
sender_name = self._get_matching_name(msg.sender_names, self.sender_name)
if sender_name is None and self.sender_name != '*': # sender might not have a well known name
return None
destination_name = self._get_matching_name(msg.destination_names, self.destination_name)
if destination_name is None and self.destination_name != '*':
return None
return MatchedMessage(msg, sender_name, destination_name)
class DBusException(Exception):
def __init__(self, message):
super(Exception, self).__init__(message)
@classmethod
def raise_if_error_reply(cls, reply):
# type: (Message) -> Message
if isinstance(reply, ErrorMessage):
raise DBusException(reply.get_error_name())
else:
return reply

View File

@ -0,0 +1,177 @@
from logging import getLogger
import dbus
from python_libs.ie_dbus.private.dbus_types import dbus_variant, dbus_string
from python_libs.ie_dbus.private.dbus_daemon import DBusDaemon
from python_libs.ie_dbus.private.message_types import MatchedMessage
from python_libs.ie_dbus.private.ve_constants import GET_TEXT, INTERFACE_BUS_ITEM, PROPERTIES_CHANGED, GET_VALUE, SET_VALUE
from python_libs.ie_utils.mixins import Disposable, Record
_log = getLogger(__name__)
# noinspection PyUnreachableCode
if False:
from typing import Optional, AnyStr, NoReturn, Dict, Any
from python_libs.ie_dbus.private.dbus_types import DbusVariant, DbusString, DbusVariantDict, DbusType
class OwnProperty(Record):
def __init__(self, value, unit='', writable=False):
str_value = round(value, 2) if isinstance(value, float) else value
self.text = unicode(str_value) + unit
self.value = value
self.unit = unit
self.writable = writable
@property
def dbus_dict(self):
# type: () -> dbus.Dictionary
d = {
dbus.String('Text'): dbus_variant(self.text),
dbus.String('Value'): dbus_variant(self.value)
}
return dbus.Dictionary(d, signature='sv')
@property
def dbus_value(self):
# type: () -> DbusVariant
return dbus_variant(self.value)
@property
def dbus_text(self):
# type: () -> DbusString
return dbus_string(self.text)
def update_value(self, value):
# type: (any) -> OwnProperty
return OwnProperty(value, self.unit, self.writable)
def __iter__(self):
yield self.value
yield self.text
class OwnProperties(Disposable):
_own_properties = None # type: Dict[AnyStr, OwnProperty]
# noinspection PyProtectedMember
def __init__(self, daemon):
# type: (DBusDaemon) -> NoReturn
self._daemon = daemon
self._own_properties = dict()
self._method_call_subs = self._daemon.subscribe_to_method_call_message(self._on_method_called) # no filter whatsoever
def get(self, object_path):
# type: (AnyStr) -> OwnProperty
return self._own_properties[object_path]
def set(self, object_path, value, unit='', writable=False):
# type: (AnyStr, any, Optional[AnyStr], Optional[bool]) -> bool
prop = OwnProperty(value, unit, writable)
if object_path in self._own_properties:
if self._own_properties[object_path] == prop:
return False
self._own_properties[object_path] = prop
# object_path, interface, member, *args):
self._daemon.broadcast_signal(
object_path,
INTERFACE_BUS_ITEM,
PROPERTIES_CHANGED,
prop.dbus_dict)
return True
def _on_method_called(self, message):
# type: (MatchedMessage) -> Any
# _log.info(str(message.sender_name) + '(' + str(message.sender_id) + ') asked ' + message.member + ' ' + message.object_path)
if message.member == GET_VALUE:
return self._on_get_value_called(message)
elif message.member == GET_TEXT:
return self._on_get_text_called(message)
elif message.member == SET_VALUE:
return self._on_set_value_called(message)
def _on_set_value_called(self, message):
# type: (MatchedMessage) -> bool
path = message.object_path
if path not in self._own_properties:
raise Exception('property ' + path + ' does not exist')
prop = self._own_properties[path]
if not prop.writable:
raise Exception('property ' + path + ' is read-only')
value = message.arguments[0]
if prop.value == value:
return False
prop = prop.update_value(value)
self._own_properties[path] = prop
# object_path, interface, member, *args):
self._daemon.broadcast_signal(
path,
INTERFACE_BUS_ITEM,
PROPERTIES_CHANGED,
prop.dbus_dict)
return True
def _on_get_value_called(self, message):
# type: (MatchedMessage) -> DbusType
path = message.object_path
if path in self._own_properties:
return self._own_properties[path].dbus_value
if path.endswith('/'): # "Tree Export"
values = {
dbus.String(k.lstrip('/')): dbus_variant(p.value)
for (k, p)
in self._own_properties.iteritems()
if k.startswith(path)
}
return dbus.Dictionary(values, signature='sv', variant_level=1) # variant for tree export !!
raise Exception('property ' + path + ' does not exist')
def _on_get_text_called(self, message):
# type: (MatchedMessage) -> DbusType
path = message.object_path
if path in self._own_properties:
return self._own_properties[message.object_path].dbus_text
if path.endswith('/'): # "Tree Export"
values = {
dbus.String(k.lstrip('/')): dbus.String(p.text)
for (k, p)
in self._own_properties.iteritems()
if k.startswith(path)
}
return dbus.Dictionary(values, signature='ss', variant_level=1) # variant for tree export !!
raise Exception('property ' + path + ' does not exist')
def __contains__(self, object_path):
# type: (AnyStr) -> bool
return object_path in self._own_properties

View File

@ -0,0 +1,166 @@
from logging import getLogger
from python_libs.ie_dbus.private.dbus_types import dbus_variant
from python_libs.ie_utils.mixins import Disposable, Record
from python_libs.ie_dbus.private.dbus_daemon import DBusDaemon
from python_libs.ie_dbus.private.message_types import MatchedMessage
from python_libs.ie_dbus.private.ve_constants import GET_TEXT, INTERFACE_BUS_ITEM, PROPERTIES_CHANGED, GET_VALUE, SERVICE_PREFIX, SET_VALUE
_log = getLogger(__name__)
_UNKNOWN_TEXT = '<UNKNOWN_TEXT>'
# noinspection PyUnreachableCode
if False:
from typing import List, AnyStr, NoReturn, Dict, Any
class RemoteProperty(Record):
def __init__(self, value, text):
self.text = text
self.value = value
@staticmethod
def from_dbus_dict(dbus_dict):
value = dbus_dict['Value']
text = dbus_dict['Text']
return RemoteProperty(value, text)
class RemoteProperties(Disposable):
_remote_properties = None # type: Dict[AnyStr, RemoteProperty]
def __init__(self, daemon):
# type: (DBusDaemon) -> NoReturn
self._daemon = daemon
self._remote_properties = dict()
# noinspection PyBroadException
def available_properties(self, service_name):
# type: (unicode) -> List[unicode]
if not self._daemon.exists_service_with_name(service_name):
return []
try:
paths = self._call_remote(service_name=service_name, object_path='/', member=GET_TEXT)[0].keys()
except Exception as _:
return []
else:
return ['/' + str(path) for path in paths]
def exists(self, combined_path):
# type: (AnyStr) -> bool
service_name, object_path, combined_path = self._parse_combined_path(combined_path)
return object_path in self.available_properties(service_name)
def get(self, combined_path):
# type: (AnyStr) -> RemoteProperty
service_name, object_path, combined_path = self._parse_combined_path(combined_path)
if combined_path in self._remote_properties:
cached = self._remote_properties[combined_path]
# a cached prop might have an unknown text, because its value has been written before,
# but it has never read or updated via property-changed
if cached.text != _UNKNOWN_TEXT:
return cached
text = self._get_text(service_name, object_path)
self._remote_properties[combined_path] = RemoteProperty(cached.value, text)
return self._remote_properties[combined_path]
prop = self._get_property(service_name, object_path)
self._remote_properties[combined_path] = prop
self._subscribe_to_property_changed(service_name, object_path)
return prop
def set(self, combined_path, value):
# type: (AnyStr, any) -> bool
service_name, object_path, combined_path = self._parse_combined_path(combined_path)
if combined_path in self._remote_properties:
if self._remote_properties[combined_path].value == value:
return False # property already has the requested value => nothing to do
else:
self._subscribe_to_property_changed(service_name, object_path)
result = self._call_remote(service_name, object_path, SET_VALUE, dbus_variant(value))[0]
if result != 0:
raise Exception(service_name + ' refused to set value of ' + object_path + ' to ' + str(value))
self._remote_properties[combined_path] = RemoteProperty(value, _UNKNOWN_TEXT)
return True
def _subscribe_to_property_changed(self, service_name, object_path):
# type: (unicode, unicode) -> NoReturn
def callback(msg):
# type: (MatchedMessage) -> NoReturn
prop = RemoteProperty.from_dbus_dict(msg.arguments[0])
key = msg.sender_name+msg.object_path
self._remote_properties[key] = prop
signal = self._daemon.subscribe_to_signal_message(
callback=callback,
sender_name=service_name,
object_path=object_path,
interface=INTERFACE_BUS_ITEM, # TODO: <- this could be removed to make it more robust, in theory
member=PROPERTIES_CHANGED) # TODO: OTOH, don't fix if it is not broken
self.chain_disposable(signal, 'signal subscription on ' + self._daemon.bus_id + ' ' + service_name + object_path)
def _get_value(self, service_name, object_path):
# type: (unicode, unicode) -> any
return self._call_remote(service_name, object_path, GET_VALUE)[0]
def _get_text(self, service_name, object_path):
# type: (unicode, unicode) -> unicode
result = self._call_remote(service_name, object_path, GET_TEXT)[0]
return unicode(result)
def _get_property(self, service_name, object_path):
# type: (unicode, unicode) -> RemoteProperty
value = self._get_value(service_name, object_path)
text = self._get_text(service_name, object_path)
return RemoteProperty(value, text)
def _call_remote(self, service_name, object_path, member, *args):
# type: (unicode, unicode, unicode, List[Any]) -> List[Any]
return self._daemon.call_method(service_name, object_path, INTERFACE_BUS_ITEM, member, *args)
def _parse_combined_path(self, combined_path):
# type: (str) -> (unicode,unicode,unicode)
service_name, object_path = combined_path.lstrip('/').split('/', 1)
if service_name == '':
raise Exception('Failed to parse service name. \ncombined_path must be of the form "service_name/path/to/property"')
if object_path == '':
raise Exception('Failed to parse object path. \ncombined_path must be of the form "service_name/path/to/property"')
service_name = service_name if service_name.startswith(SERVICE_PREFIX) else SERVICE_PREFIX + service_name
if not self._daemon.exists_service_with_name(service_name):
raise Exception('there is no service with the name "' + service_name + '" on the bus')
object_path = '/' + object_path
return unicode(service_name), unicode(object_path), unicode(service_name + object_path)

View File

@ -0,0 +1,89 @@
from logging import getLogger
from python_libs.ie_dbus.private.dbus_types import dbus_string, dbus_int_variant, dbus_string_variant, dbus_double_variant, dbus_variant
from python_libs.ie_utils.mixins import Record
from python_libs.ie_dbus.private.dbus_daemon import DBusDaemon
from python_libs.ie_dbus.private.remote_properties import RemoteProperties
from python_libs.ie_dbus.private.ve_constants import SETTINGS_SERVICE, SETTINGS_INTERFACE, SETTINGS_PREFIX
_log = getLogger(__name__)
# noinspection PyUnreachableCode
if False:
from typing import Union, NoReturn, Optional, AnyStr
def prepend_settings_prefix(path):
# type: (AnyStr) -> any
path = '/' + path.lstrip('/')
path = path if path.startswith(SETTINGS_PREFIX) else SETTINGS_PREFIX + path
return path
class Settings(Record):
# noinspection PyProtectedMember
def __init__(self, daemon, remote_properties):
# type: (DBusDaemon, RemoteProperties) -> NoReturn
self._daemon = daemon
self._remote_properties = remote_properties
# noinspection PyShadowingBuiltins
def add_setting(self, path, default_value, min=None, max=None, silent=False):
# type: (AnyStr, Union[unicode, int, float], Union[int, float, None], Union[int, float, None], Optional[bool]) -> NoReturn
path = prepend_settings_prefix(path)
if isinstance(default_value, int):
item_type = 'i'
elif isinstance(default_value, float):
item_type = 'f'
elif isinstance(default_value, (str, unicode)):
item_type = 's'
else:
raise Exception('Unsupported Settings Type')
reply = self._daemon.call_method(
SETTINGS_SERVICE, # service_name
'/', # object_path
SETTINGS_INTERFACE, # interface
'AddSilentSetting' if silent else 'AddSetting', # member,
dbus_string(''), # "group", not used
dbus_string(path),
dbus_variant(default_value),
dbus_string(item_type),
dbus_int_variant(min or 0),
dbus_int_variant(max or 0))
if reply[0] != 0:
raise Exception('failed to add setting ' + path)
def exists(self, path):
# type: (unicode) -> bool
path = prepend_settings_prefix(path)
return path in self.available_settings
def get(self, path):
# type: (unicode) -> Union[unicode, int, float]
path = prepend_settings_prefix(path)
return self._remote_properties.get(SETTINGS_SERVICE + path).value
def set(self, path, value):
# type: (unicode, Union[unicode, int, float]) -> NoReturn
path = prepend_settings_prefix(path)
self._remote_properties.set(SETTINGS_SERVICE + path, value)
@property
def available_settings(self):
# type: () -> [unicode]
return self._remote_properties.available_properties(SETTINGS_SERVICE)
def __contains__(self, path):
# type: (unicode) -> bool
return self.exists(path)

View File

@ -0,0 +1,11 @@
SERVICE_PREFIX = 'com.victronenergy.'
VE_SERVICE_FILTER = SERVICE_PREFIX + '*'
INTERFACE_BUS_ITEM = SERVICE_PREFIX + 'BusItem'
PROPERTIES_CHANGED = 'PropertiesChanged'
GET_VALUE = 'GetValue'
SET_VALUE = 'SetValue'
GET_TEXT = 'GetText'
SETTINGS_SERVICE = 'com.victronenergy.settings'
SETTINGS_INTERFACE = 'com.victronenergy.Settings'
SETTINGS_PREFIX = '/Settings'

View File

@ -0,0 +1,73 @@
from logging import getLogger
# noinspection PyUnreachableCode
if False:
from typing import NoReturn, Optional
_log = getLogger(__name__)
class MovingAverageFilter(object):
def __init__(self, length=30, initial_value=0):
# type: (int, float) -> NoReturn
self.value = initial_value
self.length = length
def update(self, value, length=None):
# type: (float, int) -> float
if length is not None:
self.length = length
self.value = (self.value * self.length + value) / (self.length + 1)
_log.debug('real value: ' + str(value) + ', filtered value: ' + str(self.value))
return self.value
class DebounceFilter(object):
def __init__(self, initial_state=None, max_inertia=10):
# type: (Optional[bool], Optional[int]) -> NoReturn
self._max_inertia = max_inertia
self._inertia = max_inertia
self._state = initial_state
def reset(self, state=None, max_inertia=None):
# type: (Optional[bool], Optional[int]) -> bool
self._max_inertia = max_inertia or self._max_inertia
self._inertia = self._max_inertia
self._state = state or self._state
_log.debug('debounce filter reset: state={0}, inertia={1}'.format(self._state, self._inertia))
return self._state
def flip(self):
# type: () -> bool
self._state = not self._state
self._inertia = self._max_inertia
return self._state
def update(self, new_state, max_inertia=None):
# type: (bool, int) -> bool
if max_inertia is not None and max_inertia != self._max_inertia:
return self.reset(new_state, max_inertia)
if new_state != self._state:
if self._inertia > 0:
self._inertia = self._inertia - 1
else:
self.flip()
else:
self._inertia = min(self._inertia + 1, self._max_inertia)
_log.debug('debounce filter update: state={0}, inertia={1}'.format(self._state, self._inertia))
return self._state

View File

@ -0,0 +1,30 @@
from logging import getLogger
import traceback
import gobject
# noinspection PyUnreachableCode
if False:
from typing import Callable, NoReturn
_log = getLogger(__name__)
def run_on_main_loop(update_action, update_period):
# type: (Callable[[],NoReturn], int) -> NoReturn
main_loop = gobject.MainLoop()
def update(*args, **kwargs):
try:
update_action()
return True
except Exception as e:
_log.error(e.message)
traceback.print_exc()
main_loop.quit()
return False
gobject.timeout_add(update_period, update)
main_loop.run()

View File

@ -0,0 +1,115 @@
from logging import getLogger
from _dbus_glib_bindings import DBusGMainLoop
# noinspection PyUnreachableCode
if False:
from typing import Callable, NoReturn, List, AnyStr, Optional, Union
_log = getLogger(__name__)
def nop(*_args):
pass
def memoize(fn):
attr_name = '_memoized_' + fn.__name__
def _memoized(self):
if not hasattr(self, attr_name):
setattr(self, attr_name, fn(self))
return getattr(self, attr_name)
return _memoized
# noinspection PyAttributeOutsideInit
class Disposable(object):
_dispose_actions = None # type: List[Callable[[],NoReturn]]
def __enter__(self):
return self
def __exit__(self, typ, value, tb):
self.dispose()
def dispose(self):
# type: () -> NoReturn
while self._dispose_actions:
dispose = self._dispose_actions.pop()
dispose()
for k, v in self.__dict__.iteritems():
if isinstance(v, Disposable) and v._dispose_actions:
_log.debug('disposing ' + type(self).__name__ + '.' + k)
v.dispose()
def chain_disposable(self, dispose, message=None):
# type: (Union[Callable[[],None],Disposable], Optional[AnyStr]) -> NoReturn
if self._dispose_actions is None:
self._dispose_actions = []
if isinstance(dispose, Disposable):
dispose = dispose.dispose
if message is None:
self._dispose_actions.append(dispose)
return
def dispose_with_log_msg():
_log.debug('disposing ' + message)
dispose()
# _log.debug('new disposable ' + message)
self._dispose_actions.append(dispose_with_log_msg)
@classmethod
def create(cls, dispose_action, message=None):
# type: (Union[Callable[[],None],Disposable], Optional[AnyStr]) -> Disposable
disposable = Disposable()
disposable.chain_disposable(dispose_action, message)
return disposable
def create_dependent_disposable(self, dispose_action, message=None):
# type: (Union[Callable[[],None],Disposable], Optional[AnyStr]) -> Disposable
disposable = Disposable.create(dispose_action, message)
self.chain_disposable(disposable)
return disposable
class Record(object):
@memoize
def __str__(self):
return self.__class__.__name__ + ' ' + unicode(vars(self))
def __repr__(self):
return self.__str__()
@memoize
def __hash__(self):
return self.__str__().__hash__()
def __eq__(self, other):
# TODO: improve, iterable vars are not correctly handled
return str(other) == str(self)
# make readonly
def __setattr__(self, key, value):
# type: (str, any) -> NoReturn
if not key.startswith('_') and hasattr(self, key): # disallow redefining
raise ValueError(key + ' is read-only' + str(dir()))
super(Record, self).__setattr__(key, value)
class RequiresMainLoop(object):
main_loop = DBusGMainLoop(set_as_default=True) # initialized only once for all subclasses that need it

View File

@ -0,0 +1,44 @@
from logging import getLogger
import re
# noinspection PyUnreachableCode
if False:
from typing import Dict
_log = getLogger(__name__)
def make2way(dic):
# type: (Dict) -> Dict
for k, v in dic.items():
dic[v] = k
return dic
def invert_dict(src_dic):
# type: (Dict) -> Dict
dic = dict()
for k, v in src_dic.items():
dic[v] = k
return dic
def enum_file_name_of(path):
# type: (str) -> Dict[int,str]
"""
This is kinda hacky, but it works :)
The enum file must contain a single enum however!
"""
path = path[0:-1] if path.endswith('.pyc') else path
pattern = re.compile(r"^\s*(\w+)\s*=\s*(\d+)", re.M)
with open(path, "r") as f:
return {
int(m[1]): m[0]
for m
in pattern.findall(f.read())
}

View File

@ -0,0 +1,30 @@
# Copyright 2019 Ram Rachum and collaborators.
# This program is distributed under the MIT license.
'''
PySnooper - Never use print for debugging again
Usage:
import pysnooper
@pysnooper.snoop()
def your_function(x):
...
A log will be written to stderr showing the lines executed and variables
changed in the decorated function.
For more information, see https://github.com/cool-RR/PySnooper
'''
from .tracer import Tracer as snoop
from .variables import Attrs, Exploding, Indices, Keys
import collections
__VersionInfo = collections.namedtuple('VersionInfo',
('major', 'minor', 'micro'))
__version__ = '0.4.0'
__version_info__ = __VersionInfo(*(map(int, __version__.split('.'))))
del collections, __VersionInfo # Avoid polluting the namespace

View File

@ -0,0 +1,95 @@
# Copyright 2019 Ram Rachum and collaborators.
# This program is distributed under the MIT license.
'''Python 2/3 compatibility'''
import abc
import os
import inspect
import sys
import datetime as datetime_module
PY3 = (sys.version_info[0] == 3)
PY2 = not PY3
if hasattr(abc, 'ABC'):
ABC = abc.ABC
else:
class ABC(object):
"""Helper class that provides a standard way to create an ABC using
inheritance.
"""
__metaclass__ = abc.ABCMeta
__slots__ = ()
if hasattr(os, 'PathLike'):
PathLike = os.PathLike
else:
class PathLike(ABC):
"""Abstract base class for implementing the file system path protocol."""
@abc.abstractmethod
def __fspath__(self):
"""Return the file system path representation of the object."""
raise NotImplementedError
@classmethod
def __subclasshook__(cls, subclass):
return (
hasattr(subclass, '__fspath__') or
# Make a concession for older `pathlib` versions:g
(hasattr(subclass, 'open') and
'path' in subclass.__name__.lower())
)
try:
iscoroutinefunction = inspect.iscoroutinefunction
except AttributeError:
iscoroutinefunction = lambda whatever: False # Lolz
try:
isasyncgenfunction = inspect.isasyncgenfunction
except AttributeError:
isasyncgenfunction = lambda whatever: False # Lolz
if PY3:
string_types = (str,)
text_type = str
else:
string_types = (basestring,)
text_type = unicode
try:
from collections import abc as collections_abc
except ImportError: # Python 2.7
import collections as collections_abc
if sys.version_info[:2] >= (3, 6):
time_isoformat = datetime_module.time.isoformat
else:
def time_isoformat(time, timespec='microseconds'):
assert isinstance(time, datetime_module.time)
if timespec != 'microseconds':
raise NotImplementedError
result = '{:02d}:{:02d}:{:02d}.{:06d}'.format(
time.hour, time.minute, time.second, time.microsecond
)
assert len(result) == 15
return result
def timedelta_format(timedelta):
time = (datetime_module.datetime.min + timedelta).time()
return time_isoformat(time, timespec='microseconds')
def timedelta_parse(s):
hours, minutes, seconds, microseconds = map(
int,
s.replace('.', ':').split(':')
)
return datetime_module.timedelta(hours=hours, minutes=minutes,
seconds=seconds,
microseconds=microseconds)

View File

@ -0,0 +1,498 @@
# Copyright 2019 Ram Rachum and collaborators.
# This program is distributed under the MIT license.
import functools
import inspect
import opcode
import os
import sys
import re
import collections
import datetime as datetime_module
import itertools
import threading
import traceback
from .variables import CommonVariable, Exploding, BaseVariable
from . import utils, pycompat
if pycompat.PY2:
from io import open
ipython_filename_pattern = re.compile('^<ipython-input-([0-9]+)-.*>$')
def get_local_reprs(frame, watch=(), custom_repr=(), max_length=None, normalize=False):
code = frame.f_code
vars_order = (code.co_varnames + code.co_cellvars + code.co_freevars +
tuple(frame.f_locals.keys()))
result_items = [(key, utils.get_shortish_repr(value, custom_repr,
max_length, normalize))
for key, value in frame.f_locals.items()]
result_items.sort(key=lambda key_value: vars_order.index(key_value[0]))
result = collections.OrderedDict(result_items)
for variable in watch:
result.update(sorted(variable.items(frame, normalize)))
return result
class UnavailableSource(object):
def __getitem__(self, i):
return u'SOURCE IS UNAVAILABLE'
source_and_path_cache = {}
def get_path_and_source_from_frame(frame):
globs = frame.f_globals or {}
module_name = globs.get('__name__')
file_name = frame.f_code.co_filename
cache_key = (module_name, file_name)
try:
return source_and_path_cache[cache_key]
except KeyError:
pass
loader = globs.get('__loader__')
source = None
if hasattr(loader, 'get_source'):
try:
source = loader.get_source(module_name)
except ImportError:
pass
if source is not None:
source = source.splitlines()
if source is None:
ipython_filename_match = ipython_filename_pattern.match(file_name)
if ipython_filename_match:
entry_number = int(ipython_filename_match.group(1))
try:
import IPython
ipython_shell = IPython.get_ipython()
((_, _, source_chunk),) = ipython_shell.history_manager. \
get_range(0, entry_number, entry_number + 1)
source = source_chunk.splitlines()
except Exception:
pass
else:
try:
with open(file_name, 'rb') as fp:
source = fp.read().splitlines()
except utils.file_reading_errors:
pass
if not source:
# We used to check `if source is None` but I found a rare bug where it
# was empty, but not `None`, so now we check `if not source`.
source = UnavailableSource()
# If we just read the source from a file, or if the loader did not
# apply tokenize.detect_encoding to decode the source into a
# string, then we should do that ourselves.
if isinstance(source[0], bytes):
encoding = 'utf-8'
for line in source[:2]:
# File coding may be specified. Match pattern from PEP-263
# (https://www.python.org/dev/peps/pep-0263/)
match = re.search(br'coding[:=]\s*([-\w.]+)', line)
if match:
encoding = match.group(1).decode('ascii')
break
source = [pycompat.text_type(sline, encoding, 'replace') for sline in
source]
result = (file_name, source)
source_and_path_cache[cache_key] = result
return result
def get_write_function(output, overwrite):
is_path = isinstance(output, (pycompat.PathLike, str))
if overwrite and not is_path:
raise Exception('`overwrite=True` can only be used when writing '
'content to file.')
if output is None:
def write(s):
stderr = sys.stderr
try:
stderr.write(s)
except UnicodeEncodeError:
# God damn Python 2
stderr.write(utils.shitcode(s))
elif is_path:
return FileWriter(output, overwrite).write
elif callable(output):
write = output
else:
assert isinstance(output, utils.WritableStream)
def write(s):
output.write(s)
return write
class FileWriter(object):
def __init__(self, path, overwrite):
self.path = pycompat.text_type(path)
self.overwrite = overwrite
def write(self, s):
with open(self.path, 'w' if self.overwrite else 'a',
encoding='utf-8') as output_file:
output_file.write(s)
self.overwrite = False
thread_global = threading.local()
DISABLED = bool(os.getenv('PYSNOOPER_DISABLED', ''))
class Tracer:
'''
Snoop on the function, writing everything it's doing to stderr.
This is useful for debugging.
When you decorate a function with `@pysnooper.snoop()`
or wrap a block of code in `with pysnooper.snoop():`, you'll get a log of
every line that ran in the function and a play-by-play of every local
variable that changed.
If stderr is not easily accessible for you, you can redirect the output to
a file::
@pysnooper.snoop('/my/log/file.log')
See values of some expressions that aren't local variables::
@pysnooper.snoop(watch=('foo.bar', 'self.x["whatever"]'))
Expand values to see all their attributes or items of lists/dictionaries:
@pysnooper.snoop(watch_explode=('foo', 'self'))
(see Advanced Usage in the README for more control)
Show snoop lines for functions that your function calls::
@pysnooper.snoop(depth=2)
Start all snoop lines with a prefix, to grep for them easily::
@pysnooper.snoop(prefix='ZZZ ')
On multi-threaded apps identify which thread are snooped in output::
@pysnooper.snoop(thread_info=True)
Customize how values are represented as strings::
@pysnooper.snoop(custom_repr=((type1, custom_repr_func1),
(condition2, custom_repr_func2), ...))
Variables and exceptions get truncated to 100 characters by default. You
can customize that:
@pysnooper.snoop(max_variable_length=200)
You can also use `max_variable_length=None` to never truncate them.
Show timestamps relative to start time rather than wall time::
@pysnooper.snoop(relative_time=True)
'''
def __init__(self, output=None, watch=(), watch_explode=(), depth=1,
prefix='', overwrite=False, thread_info=False, custom_repr=(),
max_variable_length=100, normalize=False, relative_time=False):
self._write = get_write_function(output, overwrite)
self.watch = [
v if isinstance(v, BaseVariable) else CommonVariable(v)
for v in utils.ensure_tuple(watch)
] + [
v if isinstance(v, BaseVariable) else Exploding(v)
for v in utils.ensure_tuple(watch_explode)
]
self.frame_to_local_reprs = {}
self.start_times = {}
self.depth = depth
self.prefix = prefix
self.thread_info = thread_info
self.thread_info_padding = 0
assert self.depth >= 1
self.target_codes = set()
self.target_frames = set()
self.thread_local = threading.local()
if len(custom_repr) == 2 and not all(isinstance(x,
pycompat.collections_abc.Iterable) for x in custom_repr):
custom_repr = (custom_repr,)
self.custom_repr = custom_repr
self.last_source_path = None
self.max_variable_length = max_variable_length
self.normalize = normalize
self.relative_time = relative_time
def __call__(self, function_or_class):
if DISABLED:
return function_or_class
if inspect.isclass(function_or_class):
return self._wrap_class(function_or_class)
else:
return self._wrap_function(function_or_class)
def _wrap_class(self, cls):
for attr_name, attr in cls.__dict__.items():
# Coroutines are functions, but snooping them is not supported
# at the moment
if pycompat.iscoroutinefunction(attr):
continue
if inspect.isfunction(attr):
setattr(cls, attr_name, self._wrap_function(attr))
return cls
def _wrap_function(self, function):
self.target_codes.add(function.__code__)
@functools.wraps(function)
def simple_wrapper(*args, **kwargs):
with self:
return function(*args, **kwargs)
@functools.wraps(function)
def generator_wrapper(*args, **kwargs):
gen = function(*args, **kwargs)
method, incoming = gen.send, None
while True:
with self:
try:
outgoing = method(incoming)
except StopIteration:
return
try:
method, incoming = gen.send, (yield outgoing)
except Exception as e:
method, incoming = gen.throw, e
if pycompat.iscoroutinefunction(function):
raise NotImplementedError
if pycompat.isasyncgenfunction(function):
raise NotImplementedError
elif inspect.isgeneratorfunction(function):
return generator_wrapper
else:
return simple_wrapper
def write(self, s):
s = u'{self.prefix}{s}\n'.format(**locals())
self._write(s)
def __enter__(self):
if DISABLED:
return
calling_frame = inspect.currentframe().f_back
if not self._is_internal_frame(calling_frame):
calling_frame.f_trace = self.trace
self.target_frames.add(calling_frame)
stack = self.thread_local.__dict__.setdefault(
'original_trace_functions', []
)
stack.append(sys.gettrace())
self.start_times[calling_frame] = datetime_module.datetime.now()
sys.settrace(self.trace)
def __exit__(self, exc_type, exc_value, exc_traceback):
if DISABLED:
return
stack = self.thread_local.original_trace_functions
sys.settrace(stack.pop())
calling_frame = inspect.currentframe().f_back
self.target_frames.discard(calling_frame)
self.frame_to_local_reprs.pop(calling_frame, None)
### Writing elapsed time: #############################################
# #
start_time = self.start_times.pop(calling_frame)
duration = datetime_module.datetime.now() - start_time
elapsed_time_string = pycompat.timedelta_format(duration)
indent = ' ' * 4 * (thread_global.depth + 1)
self.write(
'{indent}Elapsed time: {elapsed_time_string}'.format(**locals())
)
# #
### Finished writing elapsed time. ####################################
def _is_internal_frame(self, frame):
return frame.f_code.co_filename == Tracer.__enter__.__code__.co_filename
def set_thread_info_padding(self, thread_info):
current_thread_len = len(thread_info)
self.thread_info_padding = max(self.thread_info_padding,
current_thread_len)
return thread_info.ljust(self.thread_info_padding)
def trace(self, frame, event, arg):
### Checking whether we should trace this line: #######################
# #
# We should trace this line either if it's in the decorated function,
# or the user asked to go a few levels deeper and we're within that
# number of levels deeper.
if not (frame.f_code in self.target_codes or frame in self.target_frames):
if self.depth == 1:
# We did the most common and quickest check above, because the
# trace function runs so incredibly often, therefore it's
# crucial to hyper-optimize it for the common case.
return None
elif self._is_internal_frame(frame):
return None
else:
_frame_candidate = frame
for i in range(1, self.depth):
_frame_candidate = _frame_candidate.f_back
if _frame_candidate is None:
return None
elif _frame_candidate.f_code in self.target_codes or _frame_candidate in self.target_frames:
break
else:
return None
thread_global.__dict__.setdefault('depth', -1)
if event == 'call':
thread_global.depth += 1
indent = ' ' * 4 * thread_global.depth
# #
### Finished checking whether we should trace this line. ##############
### Making timestamp: #################################################
# #
if self.normalize:
timestamp = ' ' * 15
elif self.relative_time:
try:
start_time = self.start_times[frame]
except KeyError:
start_time = self.start_times[frame] = \
datetime_module.datetime.now()
duration = datetime_module.datetime.now() - start_time
timestamp = pycompat.timedelta_format(duration)
else:
timestamp = pycompat.time_isoformat(
datetime_module.datetime.now().time(),
timespec='microseconds'
)
# #
### Finished making timestamp. ########################################
line_no = frame.f_lineno
source_path, source = get_path_and_source_from_frame(frame)
source_path = source_path if not self.normalize else os.path.basename(source_path)
if self.last_source_path != source_path:
self.write(u'{indent}Source path:... {source_path}'.
format(**locals()))
self.last_source_path = source_path
source_line = source[line_no - 1]
thread_info = ""
if self.thread_info:
if self.normalize:
raise NotImplementedError("normalize is not supported with "
"thread_info")
current_thread = threading.current_thread()
thread_info = "{ident}-{name} ".format(
ident=current_thread.ident, name=current_thread.getName())
thread_info = self.set_thread_info_padding(thread_info)
### Reporting newish and modified variables: ##########################
# #
old_local_reprs = self.frame_to_local_reprs.get(frame, {})
self.frame_to_local_reprs[frame] = local_reprs = \
get_local_reprs(frame,
watch=self.watch, custom_repr=self.custom_repr,
max_length=self.max_variable_length,
normalize=self.normalize,
)
newish_string = ('Starting var:.. ' if event == 'call' else
'New var:....... ')
for name, value_repr in local_reprs.items():
if name not in old_local_reprs:
self.write('{indent}{newish_string}{name} = {value_repr}'.format(
**locals()))
elif old_local_reprs[name] != value_repr:
self.write('{indent}Modified var:.. {name} = {value_repr}'.format(
**locals()))
# #
### Finished newish and modified variables. ###########################
### Dealing with misplaced function definition: #######################
# #
if event == 'call' and source_line.lstrip().startswith('@'):
# If a function decorator is found, skip lines until an actual
# function definition is found.
for candidate_line_no in itertools.count(line_no):
try:
candidate_source_line = source[candidate_line_no - 1]
except IndexError:
# End of source file reached without finding a function
# definition. Fall back to original source line.
break
if candidate_source_line.lstrip().startswith('def'):
# Found the def line!
line_no = candidate_line_no
source_line = candidate_source_line
break
# #
### Finished dealing with misplaced function definition. ##############
# If a call ends due to an exception, we still get a 'return' event
# with arg = None. This seems to be the only way to tell the difference
# https://stackoverflow.com/a/12800909/2482744
code_byte = frame.f_code.co_code[frame.f_lasti]
if not isinstance(code_byte, int):
code_byte = ord(code_byte)
ended_by_exception = (
event == 'return'
and arg is None
and (opcode.opname[code_byte]
not in ('RETURN_VALUE', 'YIELD_VALUE'))
)
if ended_by_exception:
self.write('{indent}Call ended by exception'.
format(**locals()))
else:
self.write(u'{indent}{timestamp} {thread_info}{event:9} '
u'{line_no:4} {source_line}'.format(**locals()))
if event == 'return':
self.frame_to_local_reprs.pop(frame, None)
self.start_times.pop(frame, None)
thread_global.depth -= 1
if not ended_by_exception:
return_value_repr = utils.get_shortish_repr(arg,
custom_repr=self.custom_repr,
max_length=self.max_variable_length,
normalize=self.normalize,
)
self.write('{indent}Return value:.. {return_value_repr}'.
format(**locals()))
if event == 'exception':
exception = '\n'.join(traceback.format_exception_only(*arg[:2])).strip()
if self.max_variable_length:
exception = utils.truncate(exception, self.max_variable_length)
self.write('{indent}{exception}'.
format(**locals()))
return self.trace

View File

@ -0,0 +1,98 @@
# Copyright 2019 Ram Rachum and collaborators.
# This program is distributed under the MIT license.
import abc
import re
import sys
from .pycompat import ABC, string_types, collections_abc
def _check_methods(C, *methods):
mro = C.__mro__
for method in methods:
for B in mro:
if method in B.__dict__:
if B.__dict__[method] is None:
return NotImplemented
break
else:
return NotImplemented
return True
class WritableStream(ABC):
@abc.abstractmethod
def write(self, s):
pass
@classmethod
def __subclasshook__(cls, C):
if cls is WritableStream:
return _check_methods(C, 'write')
return NotImplemented
file_reading_errors = (
IOError,
OSError,
ValueError # IronPython weirdness.
)
def shitcode(s):
return ''.join(
(c if (0 < ord(c) < 256) else '?') for c in s
)
def get_repr_function(item, custom_repr):
for condition, action in custom_repr:
if isinstance(condition, type):
condition = lambda x, y=condition: isinstance(x, y)
if condition(item):
return action
return repr
DEFAULT_REPR_RE = re.compile(r' at 0x[a-f0-9A-F]{4,}')
def normalize_repr(item_repr):
"""Remove memory address (0x...) from a default python repr"""
return DEFAULT_REPR_RE.sub('', item_repr)
def get_shortish_repr(item, custom_repr=(), max_length=None, normalize=False):
repr_function = get_repr_function(item, custom_repr)
try:
r = repr_function(item)
except Exception:
r = 'REPR FAILED'
r = r.replace('\r', '').replace('\n', '')
if normalize:
r = normalize_repr(r)
if max_length:
r = truncate(r, max_length)
return r
def truncate(string, max_length):
if (max_length is None) or (len(string) <= max_length):
return string
else:
left = (max_length - 3) // 2
right = max_length - 3 - left
return u'{}...{}'.format(string[:left], string[-right:])
def ensure_tuple(x):
if isinstance(x, collections_abc.Iterable) and \
not isinstance(x, string_types):
return tuple(x)
else:
return (x,)

View File

@ -0,0 +1,133 @@
import itertools
import abc
try:
from collections.abc import Mapping, Sequence
except ImportError:
from collections import Mapping, Sequence
from copy import deepcopy
from . import utils
from . import pycompat
def needs_parentheses(source):
def code(s):
return compile(s, '<variable>', 'eval').co_code
return code('{}.x'.format(source)) != code('({}).x'.format(source))
class BaseVariable(pycompat.ABC):
def __init__(self, source, exclude=()):
self.source = source
self.exclude = utils.ensure_tuple(exclude)
self.code = compile(source, '<variable>', 'eval')
if needs_parentheses(source):
self.unambiguous_source = '({})'.format(source)
else:
self.unambiguous_source = source
def items(self, frame, normalize=False):
try:
main_value = eval(self.code, frame.f_globals or {}, frame.f_locals)
except Exception:
return ()
return self._items(main_value, normalize)
@abc.abstractmethod
def _items(self, key, normalize=False):
raise NotImplementedError
@property
def _fingerprint(self):
return (type(self), self.source, self.exclude)
def __hash__(self):
return hash(self._fingerprint)
def __eq__(self, other):
return (isinstance(other, BaseVariable) and
self._fingerprint == other._fingerprint)
class CommonVariable(BaseVariable):
def _items(self, main_value, normalize=False):
result = [(self.source, utils.get_shortish_repr(main_value, normalize=normalize))]
for key in self._safe_keys(main_value):
try:
if key in self.exclude:
continue
value = self._get_value(main_value, key)
except Exception:
continue
result.append((
'{}{}'.format(self.unambiguous_source, self._format_key(key)),
utils.get_shortish_repr(value)
))
return result
def _safe_keys(self, main_value):
try:
for key in self._keys(main_value):
yield key
except Exception:
pass
def _keys(self, main_value):
return ()
def _format_key(self, key):
raise NotImplementedError
def _get_value(self, main_value, key):
raise NotImplementedError
class Attrs(CommonVariable):
def _keys(self, main_value):
return itertools.chain(
getattr(main_value, '__dict__', ()),
getattr(main_value, '__slots__', ())
)
def _format_key(self, key):
return '.' + key
def _get_value(self, main_value, key):
return getattr(main_value, key)
class Keys(CommonVariable):
def _keys(self, main_value):
return main_value.keys()
def _format_key(self, key):
return '[{}]'.format(utils.get_shortish_repr(key))
def _get_value(self, main_value, key):
return main_value[key]
class Indices(Keys):
_slice = slice(None)
def _keys(self, main_value):
return range(len(main_value))[self._slice]
def __getitem__(self, item):
assert isinstance(item, slice)
result = deepcopy(self)
result._slice = item
return result
class Exploding(BaseVariable):
def _items(self, main_value, normalize=False):
if isinstance(main_value, Mapping):
cls = Keys
elif isinstance(main_value, Sequence):
cls = Indices
else:
cls = Attrs
return cls(self.source, self.exclude)._items(main_value, normalize)

View File

@ -0,0 +1,3 @@
#!/bin/sh
exec 2>&1
exec multilog t s25000 n4 /var/log/dbus-fzsonick-48tl.TTY

View File

@ -0,0 +1,4 @@
#!/bin/sh
exec 2>&1
exec softlimit -d 100000000 -s 1000000 -a 100000000 /opt/innovenergy/dbus-fzsonick-48tl/start.sh TTY

View File

@ -0,0 +1,214 @@
# coding=utf-8
import config as cfg
from convert import mean, read_float, read_led_state, read_bool, count_bits, comma_separated
from data import BatterySignal, Battery, LedColor, ServiceSignal, BatteryStatus, LedState
# noinspection PyUnreachableCode
if False:
from typing import List, Iterable
def init_service_signals(batteries):
# type: (List[Battery]) -> Iterable[ServiceSignal]
n_batteries = len(batteries)
product_name = cfg.PRODUCT_NAME + ' x' + str(n_batteries)
return [
ServiceSignal('/NbOfBatteries', n_batteries), # TODO: nb of operational batteries
ServiceSignal('/Mgmt/ProcessName', __file__),
ServiceSignal('/Mgmt/ProcessVersion', cfg.SOFTWARE_VERSION),
ServiceSignal('/Mgmt/Connection', cfg.CONNECTION),
ServiceSignal('/DeviceInstance', cfg.DEVICE_INSTANCE),
ServiceSignal('/ProductName', product_name),
ServiceSignal('/ProductId', cfg.PRODUCT_ID),
ServiceSignal('/Connected', 1)
]
def init_battery_signals():
# type: () -> Iterable[BatterySignal]
read_voltage = read_float(register=999, scale_factor=0.01, offset=0)
read_current = read_float(register=1000, scale_factor=0.01, offset=-10000)
read_led_amber = read_led_state(register=1004, led=LedColor.amber)
read_led_green = read_led_state(register=1004, led=LedColor.green)
read_led_blue = read_led_state(register=1004, led=LedColor.blue)
read_led_red = read_led_state(register=1004, led=LedColor.red)
def read_power(status):
# type: (BatteryStatus) -> int
return int(read_current(status) * read_voltage(status))
def calc_power_limit_imposed_by_voltage_limit(v, i, v_limit, r_int):
# type: (float, float, float, float) -> float
dv = v_limit - v
di = dv / r_int
p_limit = v_limit * (i + di)
return p_limit
def calc_power_limit_imposed_by_current_limit(v, i, i_limit, r_int):
# type: (float, float, float, float) -> float
di = i_limit - i
dv = di * r_int
p_limit = i_limit * (v + dv)
return p_limit
def calc_max_charge_power(bs):
# type: (BatteryStatus) -> int
b = bs.battery
v = read_voltage(bs)
i = read_current(bs)
p_limits = [
calc_power_limit_imposed_by_voltage_limit(v, i, b.v_max, b.r_int_min),
calc_power_limit_imposed_by_voltage_limit(v, i, b.v_max, b.r_int_max),
calc_power_limit_imposed_by_current_limit(v, i, b.i_max, b.r_int_min),
calc_power_limit_imposed_by_current_limit(v, i, b.i_max, b.r_int_max),
]
p_limit = min(p_limits) # p_limit is normally positive here (signed)
p_limit = max(p_limit, 0) # charge power must not become negative
return int(p_limit)
def calc_max_discharge_power(bs):
# type: (BatteryStatus) -> float
b = bs.battery
v = read_voltage(bs)
i = read_current(bs)
p_limits = [
calc_power_limit_imposed_by_voltage_limit(v, i, b.v_min, b.r_int_min),
calc_power_limit_imposed_by_voltage_limit(v, i, b.v_min, b.r_int_max),
calc_power_limit_imposed_by_current_limit(v, i, -b.i_max, b.r_int_min),
calc_power_limit_imposed_by_current_limit(v, i, -b.i_max, b.r_int_max),
]
p_limit = max(p_limits) # p_limit is normally negative here (signed)
p_limit = min(p_limit, 0) # discharge power must not become positive
return int(-p_limit) # make unsigned!
def read_battery_cold(status):
return \
read_led_green(status) >= LedState.blinking_slow and \
read_led_blue(status) >= LedState.blinking_slow
def read_soc(status):
soc = read_float(register=1053, scale_factor=0.1, offset=0)(status)
# if the SOC is 100 but EOC is not yet reached, report 99.9 instead of 100
if soc > 99.9 and not read_eoc_reached(status):
return 99.9
if soc >= 99.9 and read_eoc_reached(status):
return 100
return soc
def read_eoc_reached(status):
return \
read_led_green(status) == LedState.on and \
read_led_amber(status) == LedState.off and \
read_led_blue(status) == LedState.off
return [
BatterySignal('/Dc/0/Voltage', mean, get_value=read_voltage, unit='V'),
BatterySignal('/Dc/0/Current', sum, get_value=read_current, unit='A'),
BatterySignal('/Dc/0/Power', sum, get_value=read_power, unit='W'),
BatterySignal('/BussVoltage', mean, read_float(register=1001, scale_factor=0.01, offset=0), unit='V'),
BatterySignal('/Soc', mean, read_soc, unit='%'),
BatterySignal('/Dc/0/Temperature', mean, read_float(register=1003, scale_factor=0.1, offset=-400), unit='C'),
BatterySignal('/NumberOfWarningFlags', sum, count_bits(base_register=1005, nb_of_registers=3, nb_of_bits=47)),
BatterySignal('/WarningFlags/TaM1', any, read_bool(base_register=1005, bit=1)),
BatterySignal('/WarningFlags/TbM1', any, read_bool(base_register=1005, bit=4)),
BatterySignal('/WarningFlags/VBm1', any, read_bool(base_register=1005, bit=6)),
BatterySignal('/WarningFlags/VBM1', any, read_bool(base_register=1005, bit=8)),
BatterySignal('/WarningFlags/IDM1', any, read_bool(base_register=1005, bit=10)),
BatterySignal('/WarningFlags/vsM1', any, read_bool(base_register=1005, bit=24)),
BatterySignal('/WarningFlags/iCM1', any, read_bool(base_register=1005, bit=26)),
BatterySignal('/WarningFlags/iDM1', any, read_bool(base_register=1005, bit=28)),
BatterySignal('/WarningFlags/MID1', any, read_bool(base_register=1005, bit=30)),
BatterySignal('/WarningFlags/BLPW', any, read_bool(base_register=1005, bit=32)),
BatterySignal('/WarningFlags/Ah_W', any, read_bool(base_register=1005, bit=35)),
BatterySignal('/WarningFlags/MPMM', any, read_bool(base_register=1005, bit=38)),
BatterySignal('/WarningFlags/TCMM', any, read_bool(base_register=1005, bit=39)),
BatterySignal('/WarningFlags/TCdi', any, read_bool(base_register=1005, bit=40)),
BatterySignal('/WarningFlags/WMTO', any, read_bool(base_register=1005, bit=41)),
BatterySignal('/WarningFlags/bit44', any, read_bool(base_register=1005, bit=44)),
BatterySignal('/WarningFlags/CELL1', any, read_bool(base_register=1005, bit=46)),
BatterySignal('/WarningFlags/bit47WarningDummy', any, read_bool(base_register=1005, bit=47)),
BatterySignal('/NumberOfAlarmFlags', sum, count_bits(base_register=1009, nb_of_registers=3, nb_of_bits=47)),
BatterySignal('/AlarmFlags/Tam', any, read_bool(base_register=1009, bit=0)),
BatterySignal('/AlarmFlags/TaM2', any, read_bool(base_register=1009, bit=2)),
BatterySignal('/AlarmFlags/Tbm', any, read_bool(base_register=1009, bit=3)),
BatterySignal('/AlarmFlags/TbM2', any, read_bool(base_register=1009, bit=5)),
BatterySignal('/AlarmFlags/VBm2', any, read_bool(base_register=1009, bit=7)),
BatterySignal('/AlarmFlags/IDM2', any, read_bool(base_register=1009, bit=11)),
BatterySignal('/AlarmFlags/ISOB', any, read_bool(base_register=1009, bit=12)),
BatterySignal('/AlarmFlags/MSWE', any, read_bool(base_register=1009, bit=13)),
BatterySignal('/AlarmFlags/FUSE', any, read_bool(base_register=1009, bit=14)),
BatterySignal('/AlarmFlags/HTRE', any, read_bool(base_register=1009, bit=15)),
BatterySignal('/AlarmFlags/TCPE', any, read_bool(base_register=1009, bit=16)),
BatterySignal('/AlarmFlags/STRE', any, read_bool(base_register=1009, bit=17)),
BatterySignal('/AlarmFlags/CME', any, read_bool(base_register=1009, bit=18)),
BatterySignal('/AlarmFlags/HWFL', any, read_bool(base_register=1009, bit=19)),
BatterySignal('/AlarmFlags/HWEM', any, read_bool(base_register=1009, bit=20)),
BatterySignal('/AlarmFlags/ThM', any, read_bool(base_register=1009, bit=21)),
BatterySignal('/AlarmFlags/vsm1', any, read_bool(base_register=1009, bit=22)),
BatterySignal('/AlarmFlags/vsm2', any, read_bool(base_register=1009, bit=23)),
BatterySignal('/AlarmFlags/vsM2', any, read_bool(base_register=1009, bit=25)),
BatterySignal('/AlarmFlags/iCM2', any, read_bool(base_register=1009, bit=27)),
BatterySignal('/AlarmFlags/iDM2', any, read_bool(base_register=1009, bit=29)),
BatterySignal('/AlarmFlags/MID2', any, read_bool(base_register=1009, bit=31)),
BatterySignal('/AlarmFlags/CCBF', any, read_bool(base_register=1009, bit=33)),
BatterySignal('/AlarmFlags/AhFL', any, read_bool(base_register=1009, bit=34)),
BatterySignal('/AlarmFlags/TbCM', any, read_bool(base_register=1009, bit=36)),
BatterySignal('/AlarmFlags/BRNF', any, read_bool(base_register=1009, bit=37)),
BatterySignal('/AlarmFlags/HTFS', any, read_bool(base_register=1009, bit=42)),
BatterySignal('/AlarmFlags/DATA', any, read_bool(base_register=1009, bit=43)),
BatterySignal('/AlarmFlags/CELL2', any, read_bool(base_register=1009, bit=45)),
BatterySignal('/AlarmFlags/bit47AlarmDummy', any, read_bool(base_register=1009, bit=47)),
BatterySignal('/LedStatus/Red', max, read_led_red),
BatterySignal('/LedStatus/Blue', max, read_led_blue),
BatterySignal('/LedStatus/Green', max, read_led_green),
BatterySignal('/LedStatus/Amber', max, read_led_amber),
BatterySignal('/IoStatus/MainSwitchClosed', any, read_bool(base_register=1013, bit=0)),
BatterySignal('/IoStatus/AlarmOutActive', any, read_bool(base_register=1013, bit=1)),
BatterySignal('/IoStatus/InternalFanActive', any, read_bool(base_register=1013, bit=2)),
BatterySignal('/IoStatus/VoltMeasurementAllowed', any, read_bool(base_register=1013, bit=3)),
BatterySignal('/IoStatus/AuxRelay', any, read_bool(base_register=1013, bit=4)),
BatterySignal('/IoStatus/RemoteState', any, read_bool(base_register=1013, bit=5)),
BatterySignal('/IoStatus/HeaterOn', any, read_bool(base_register=1013, bit=6)),
BatterySignal('/IoStatus/EocReached', min, read_eoc_reached),
BatterySignal('/IoStatus/BatteryCold', any, read_battery_cold),
# see protocol doc page 7
BatterySignal('/Info/MaxDischargeCurrent', sum, lambda bs: bs.battery.i_max, unit='A'),
BatterySignal('/Info/MaxChargeCurrent', sum, lambda bs: bs.battery.i_max, unit='A'),
BatterySignal('/Info/MaxChargeVoltage', min, lambda bs: bs.battery.v_max, unit='V'),
BatterySignal('/Info/MinDischargeVoltage', max, lambda bs: bs.battery.v_min, unit='V'),
BatterySignal('/Info/BatteryLowVoltage' , max, lambda bs: bs.battery.v_min-2, unit='V'),
BatterySignal('/Info/NumberOfStrings', sum, lambda bs: bs.battery.n_strings),
BatterySignal('/Info/MaxChargePower', sum, calc_max_charge_power),
BatterySignal('/Info/MaxDischargePower', sum, calc_max_discharge_power),
BatterySignal('/FirmwareVersion', comma_separated, lambda bs: bs.battery.firmware_version),
BatterySignal('/HardwareVersion', comma_separated, lambda bs: bs.battery.hardware_version),
BatterySignal('/BmsVersion', comma_separated, lambda bs: bs.battery.bms_version)
]

View File

@ -0,0 +1,7 @@
#!/bin/bash
. /opt/victronenergy/serial-starter/run-service.sh
app="/opt/innovenergy/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py"
args="$tty"
start $args

View File

@ -13,9 +13,9 @@ DEVICE_INSTANCE = 1
SERVICE_NAME_PREFIX = 'com.victronenergy.battery.' SERVICE_NAME_PREFIX = 'com.victronenergy.battery.'
#s3 configuration #s3 configuration
S3BUCKET = "2-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" S3BUCKET = "13-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
S3KEY = "EXO5b2e35442791260eaaa7bdc8" S3KEY = "EXOcca50b894afa583d8d380dd1"
S3SECRET = "XFFOVzenDiEQoLPmhK6ML9RfQfsAMhrAs25MfJxi-24" S3SECRET = "7fmdIN1WL8WL9k-20YjLZC5liH2qCwYrGP31Y4dityk"
# driver configuration # driver configuration

View File

@ -90,25 +90,6 @@ def read_bitmap(register):
return get_value return get_value
def read_limb_string(register):
# type: (int) -> Callable[[BatteryStatus], bitmap]
def get_value(status):
# type: (BatteryStatus) -> bitmap
value = status.modbus_data[register - cfg.BASE_ADDRESS]
string1_disabled = int((value & 0b00001) != 0)
string2_disabled = int((value & 0b00010) != 0)
string3_disabled = int((value & 0b00100) != 0)
string4_disabled = int((value & 0b01000) != 0)
string5_disabled = int((value & 0b10000) != 0)
n_limb_strings = string1_disabled+string2_disabled+string3_disabled+string4_disabled+string5_disabled
if n_limb_strings>=2:
return True
else:
return False
return get_value
def append_unit(unit): def append_unit(unit):
# type: (unicode) -> Callable[[unicode], unicode] # type: (unicode) -> Callable[[unicode], unicode]

View File

@ -111,6 +111,14 @@ class S3config:
).decode() ).decode()
return f"AWS {s3_key}:{signature}" return f"AWS {s3_key}:{signature}"
@staticmethod
def _create_authorization(method, bucket, s3_path, date, s3_key, s3_secret, content_type="", md5_hash=""):
payload = f"{method}\n{md5_hash}\n{content_type}\n{date}\n/{bucket.strip('/')}/{s3_path.strip('/')}"
signature = base64.b64encode(
hmac.new(s3_secret.encode(), payload.encode(), hashlib.sha1).digest()
).decode()
return f"AWS {s3_key}:{signature}"
def read_csv_as_string(file_path): def read_csv_as_string(file_path):
""" """
Reads a CSV file from the given path and returns its content as a single string. Reads a CSV file from the given path and returns its content as a single string.
@ -131,22 +139,21 @@ CSV_DIR = "/data/csv_files/"
# Define the path to the file containing the installation name # Define the path to the file containing the installation name
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name' INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime # trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode # noinspection PyUnreachableCode
if False: if False:
from typing import Callable from typing import Callable
def interpret_limb_bitmap(bitmap_value): def interpret_limb_bitmap(bitmap_value):
# The bit for string 1 also monitors all 5 strings: 0000 0000 means All 5 strings activated. 0000 0001 means string 1 disabled. # The bit for string 1 also monitors all 5 strings: 0000 0000 means All 5 strings activated. 0000 0001 means string 1 disabled.
string1_disabled = int((bitmap_value & 0b00001) != 0) string1_disabled = int((bitmap_value & 0b00001) != 0)
string2_disabled = int((bitmap_value & 0b00010) != 0) string2_disabled = int((bitmap_value & 0b00010) != 0)
string3_disabled = int((bitmap_value & 0b00100) != 0) string3_disabled = int((bitmap_value & 0b00100) != 0)
string4_disabled = int((bitmap_value & 0b01000) != 0) string4_disabled = int((bitmap_value & 0b01000) != 0)
string5_disabled = int((bitmap_value & 0b10000) != 0) string5_disabled = int((bitmap_value & 0b10000) != 0)
n_limb_strings = string1_disabled+string2_disabled+string3_disabled+string4_disabled+string5_disabled
n_limb_strings = string1_disabled+string2_disabled+string3_disabled+string4_disabled+string5_disabled return n_limb_strings
return n_limb_strings
def calc_power_limit_imposed_by_voltage_limit(v, i, v_limit, r_int): def calc_power_limit_imposed_by_voltage_limit(v, i, v_limit, r_int):
# type: (float, float, float, float) -> float # type: (float, float, float, float) -> float
@ -733,39 +740,34 @@ def update_state_from_dictionaries(current_warnings, current_alarms, node_number
"Alarms": [] "Alarms": []
} }
alarms_number_list = [] alarms_number_list = []
for node_number in node_numbers: for node_number in node_numbers:
cnt = 0 cnt = 0
for i, alarm_value in enumerate(current_alarms.values()): for alarm_name, alarm_value in current_alarms.items():
if int(list(current_alarms.keys())[i].split("/")[3]) == int(node_number): if str(node_number) in alarm_name and alarm_value:
if alarm_value: cnt+=1
cnt+=1
alarms_number_list.append(cnt) alarms_number_list.append(cnt)
warnings_number_list = [] warnings_number_list = []
for node_number in node_numbers: for node_number in node_numbers:
cnt = 0 cnt = 0
for i, warning_value in enumerate(current_warnings.values()): for warning_name, warning_value in current_warnings.items():
if int(list(current_warnings.keys())[i].split("/")[3]) == int(node_number): if str(node_number) in warning_name and warning_value:
if warning_value: cnt+=1
cnt+=1
warnings_number_list.append(cnt) warnings_number_list.append(cnt)
# Evaluate alarms # Evaluate alarms
if any(changed_alarms.values()): if any(changed_alarms.values()):
for i, changed_alarm in enumerate(changed_alarms.values()): for i, changed_alarm in enumerate(changed_alarms.values()):
if changed_alarm and list(current_alarms.values())[i]: if changed_alarm and list(current_alarms.values())[i]:
description = list(current_alarms.keys())[i].split("/")[-1] status_message["Alarms"].append(AlarmOrWarning(list(current_alarms.keys())[i],"System").to_dict())
device_created = "Battery node " + list(current_alarms.keys())[i].split("/")[3]
status_message["Alarms"].append(AlarmOrWarning(description, device_created).to_dict())
if any(changed_warnings.values()): if any(changed_warnings.values()):
for i, changed_warning in enumerate(changed_warnings.values()): for i, changed_warning in enumerate(changed_warnings.values()):
if changed_warning and list(current_warnings.values())[i]: if changed_warning and list(current_warnings.values())[i]:
description = list(current_warnings.keys())[i].split("/")[-1] status_message["Warnings"].append(AlarmOrWarning(list(current_warnings.keys())[i],"System").to_dict())
device_created = "Battery node " + list(current_warnings.keys())[i].split("/")[3]
status_message["Warnings"].append(AlarmOrWarning(description, device_created).to_dict())
if any(current_alarms.values()): if any(current_alarms.values()):
status_message["Status"]=2 status_message["Status"]=2
@ -845,10 +847,44 @@ def read_warning_and_alarm_flags():
CsvSignal('/Battery/Devices/AlarmFlags/LMPA', c.read_bool(register=1005, bit=45)), CsvSignal('/Battery/Devices/AlarmFlags/LMPA', c.read_bool(register=1005, bit=45)),
CsvSignal('/Battery/Devices/AlarmFlags/HEBT', c.read_bool(register=1005, bit=46)), CsvSignal('/Battery/Devices/AlarmFlags/HEBT', c.read_bool(register=1005, bit=46)),
CsvSignal('/Battery/Devices/AlarmFlags/CURM', c.read_bool(register=1005, bit=48)), CsvSignal('/Battery/Devices/AlarmFlags/CURM', c.read_bool(register=1005, bit=48)),
CsvSignal('/Battery/Devices/AlarmFlags/NeedToReplaceBattery',c.read_limb_string(1059)),
] ]
import random
'''def update_for_testing(modbus, batteries, dbus, signals, csv_signals):
global ALLOW
logging.debug('starting testing update cycle')
warning_signals, alarm_signals = read_warning_and_alarm_flags()
current_warnings = {}
current_alarms = {}
statuses = [read_battery_status(modbus, battery) for battery in batteries]
node_numbers = [battery.slave_address for battery in batteries]
if ALLOW:
any_warning_active = False
any_alarm_active = False
for i, node in enumerate(node_numbers):
for s in warning_signals:
signal_name = insert_id(s.name, i+1)
value = s.get_value(statuses[i])
current_warnings[signal_name] = value
if ALLOW and value:
any_warning_active = True
for s in alarm_signals:
signal_name = insert_id(s.name, i+1)
value = random.choice([True, False])
current_alarms[signal_name] = value
if ALLOW and value:
any_alarm_active = True
print(update_state_from_dictionaries(current_warnings, current_alarms))
publish_values(dbus, signals, statuses)
create_csv_files(csv_signals, statuses, node_numbers)
logging.debug('finished update cycle\n')
return True'''
start_time = time.time()
def update(modbus, batteries, dbus, signals, csv_signals): def update(modbus, batteries, dbus, signals, csv_signals):
global start_time
# type: (Modbus, Iterable[Battery], DBus, Iterable[Signal]) -> bool # type: (Modbus, Iterable[Battery], DBus, Iterable[Signal]) -> bool
""" """
Main update function Main update function
@ -867,17 +903,21 @@ def update(modbus, batteries, dbus, signals, csv_signals):
# Iterate over each node and signal to create rows in the new format # Iterate over each node and signal to create rows in the new format
for i, node in enumerate(node_numbers): for i, node in enumerate(node_numbers):
for s in warnings_signals: for s in warnings_signals:
signal_name = insert_id(s.name, node) signal_name = insert_id(s.name, i+1)
value = s.get_value(statuses[i]) value = s.get_value(statuses[i])
current_warnings[signal_name] = value current_warnings[signal_name] = value
for s in alarm_signals: for s in alarm_signals:
signal_name = insert_id(s.name, node) signal_name = insert_id(s.name, i+1)
value = s.get_value(statuses[i]) value = s.get_value(statuses[i])
current_alarms[signal_name] = value current_alarms[signal_name] = value
#print(update_state_from_dictionaries(current_warnings, current_alarms)) #print(update_state_from_dictionaries(current_warnings, current_alarms))
status_message, alarms_number_list, warnings_number_list = update_state_from_dictionaries(current_warnings, current_alarms, node_numbers) status_message, alarms_number_list, warnings_number_list = update_state_from_dictionaries(current_warnings, current_alarms, node_numbers)
publish_values(dbus, signals, statuses) publish_values(dbus, signals, statuses)
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list) elapsed_time = time.time() - start_time
if elapsed_time >= 30:
create_csv_files(csv_signals, statuses, node_numbers, alarms_number_list, warnings_number_list)
start_time = time.time()
print(f"Elapsed time: {elapsed_time:.2f} seconds")
logging.debug('finished update cycle\n') logging.debug('finished update cycle\n')
return True return True
@ -942,7 +982,7 @@ def get_installation_name(file_path):
return file.read().strip() return file.read().strip()
def manage_csv_files(directory_path, max_files=20): def manage_csv_files(directory_path, max_files=20):
csv_files = [f for f in os.listdir(directory_path)] csv_files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))]
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x))) csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
# Remove oldest files if exceeds maximum # Remove oldest files if exceeds maximum
while len(csv_files) > max_files: while len(csv_files) > max_files:

Binary file not shown.

View File

@ -0,0 +1,51 @@
import serial
import logging
# dbus configuration
FIRMWARE_VERSION = 1 # value returned by getValue (getText returns string value reported by battery)
HARDWARE_VERSION = 1 # value returned by getValue (getText returns string value reported by battery)
CONNECTION = 'Modbus RTU'
PRODUCT_NAME = 'FZS 48TL200'
PRODUCT_ID = 0xB012 # assigned by victron
DEVICE_INSTANCE = 1
SERVICE_NAME_PREFIX = 'com.victronenergy.battery.'
# driver configuration
SOFTWARE_VERSION = '3.0.3'
UPDATE_INTERVAL = 2000 # milliseconds
#LOG_LEVEL = logging.INFO
LOG_LEVEL = logging.DEBUG
# modbus configuration
BASE_ADDRESS = 999
#NO_OF_REGISTERS = 63
NO_OF_REGISTERS = 64
MAX_SLAVE_ADDRESS = 10
# RS 485 configuration
PARITY = serial.PARITY_ODD
TIMEOUT = 0.1 # seconds
BAUD_RATE = 115200
BYTE_SIZE = 8
STOP_BITS = 1
MODE = 'rtu'
# battery configuration
MAX_CHARGE_VOLTAGE = 58
I_MAX_PER_STRING = 15
NUM_OF_STRING_PER_BATTERY = 5
AH_PER_STRING = 40
V_MAX = 54.2
R_STRING_MIN = 0.125
R_STRING_MAX = 0.250

View File

@ -0,0 +1,119 @@
from collections import Iterable
from decimal import *
import config as cfg
from data import LedState, BatteryStatus
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import Callable
def read_bool(register, bit):
# type: (int, int) -> Callable[[BatteryStatus], bool]
def get_value(status):
# type: (BatteryStatus) -> bool
value = status.modbus_data[register - cfg.BASE_ADDRESS]
return value & (1 << bit) > 0
return get_value
def read_float(register, scale_factor=1.0, offset=0.0, places=2):
# type: (int, float, float) -> Callable[[BatteryStatus], float]
def get_value(status):
# type: (BatteryStatus) -> float
value = status.modbus_data[register - cfg.BASE_ADDRESS]
if value >= 0x8000: # convert to signed int16
value -= 0x10000 # fiamm stores their integers signed AND with sign-offset @#%^&!
result = (value+offset)*scale_factor
return round(result,places)
return get_value
def read_hex_string(register, count):
# type: (int, int) -> Callable[[BatteryStatus], str]
"""
reads count consecutive modbus registers from start_address,
and returns a hex representation of it:
e.g. for count=4: DEAD BEEF DEAD BEEF.
"""
start = register - cfg.BASE_ADDRESS
end = start + count
def get_value(status):
# type: (BatteryStatus) -> str
return ' '.join(['{0:0>4X}'.format(x) for x in status.modbus_data[start:end]])
return get_value
def read_led_state(register, led):
# type: (int, int) -> Callable[[BatteryStatus], int]
read_lo = read_bool(register, led * 2)
read_hi = read_bool(register, led * 2 + 1)
def get_value(status):
# type: (BatteryStatus) -> int
lo = read_lo(status)
hi = read_hi(status)
if hi:
if lo:
return LedState.blinking_fast
else:
return LedState.blinking_slow
else:
if lo:
return LedState.on
else:
return LedState.off
return get_value
def read_bitmap(register):
# type: (int) -> Callable[[BatteryStatus], bitmap]
def get_value(status):
# type: (BatteryStatus) -> bitmap
value = status.modbus_data[register - cfg.BASE_ADDRESS]
return value
return get_value
def append_unit(unit):
# type: (unicode) -> Callable[[unicode], unicode]
def get_text(v):
# type: (unicode) -> unicode
return "{0}{1}".format(str(v), unit)
return get_text
def mean(numbers):
# type: (Iterable[float] | Iterable[int]) -> float
return float("{:.2f}".format(float(sum(numbers)) / len(numbers)))
def ssum(numbers):
# type: (Iterable[float] | Iterable[int]) -> float
return float("{:.2f}".format(float(sum(numbers))))
def first(ts):
return next(t for t in ts)
def return_in_list(ts):
return ts

View File

@ -0,0 +1,63 @@
import config as cfg
from collections import Iterable
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import Callable
class LedState(object):
"""
from page 6 of the '48TLxxx ModBus Protocol doc'
"""
off = 0
on = 1
blinking_slow = 2
blinking_fast = 3
class LedColor(object):
green = 0
amber = 1
blue = 2
red = 3
class CsvSignal(object):
def __init__(self, name, get_value, get_text = None):
self.name = name
self.get_value = get_value if callable(get_value) else lambda _: get_value
self.get_text = get_text
if get_text is None:
self.get_text = ""
class Battery(object):
""" Data record to hold hardware and firmware specs of the battery """
def __init__(self, slave_address, hardware_version, firmware_version, bms_version, ampere_hours):
# type: (int, str, str, str, int) -> None
self.slave_address = slave_address
self.hardware_version = hardware_version
self.firmware_version = firmware_version
self.bms_version = bms_version
self.ampere_hours = ampere_hours
def __str__(self):
return 'slave address = {0}\nhardware version = {1}\nfirmware version = {2}\nbms version = {3}\nampere hours = {4}'.format(
self.slave_address, self.hardware_version, self.firmware_version, self.bms_version, str(self.ampere_hours))
class BatteryStatus(object):
"""
record holding the current status of a battery
"""
def __init__(self, battery, modbus_data):
# type: (Battery, list[int]) -> None
self.battery = battery
self.modbus_data = modbus_data

View File

@ -0,0 +1,731 @@
#! /usr/bin/python3 -u
import re
import sys
import logging
from gi.repository import GLib
import config as cfg
import convert as c
from pymodbus.register_read_message import ReadInputRegistersResponse
from pymodbus.client.sync import ModbusSerialClient as Modbus
from pymodbus.other_message import ReportSlaveIdRequest
from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ExceptionResponse
from dbus.mainloop.glib import DBusGMainLoop
from data import BatteryStatus, Battery, LedColor, CsvSignal, LedState
from collections import Iterable
from os import path
app_dir = path.dirname(path.realpath(__file__))
sys.path.insert(1, path.join(app_dir, 'ext', 'velib_python'))
#from vedbus import VeDbusService as DBus
import time
import os
import csv
import requests
import hmac
import hashlib
import base64
from datetime import datetime
import io
class S3config:
def __init__(self):
self.bucket = "1-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
self.region = "sos-ch-dk-2"
self.provider = "exo.io"
self.key = "EXOcc0e47a4c4d492888ff5a7f2"
self.secret = "79QG4unMh7MeVacMnXr5xGxEyAlWZDIdM-dg_nXFFr4"
self.content_type = "text/plain; charset=utf-8"
@property
def host(self):
return f"{self.bucket}.{self.region}.{self.provider}"
@property
def url(self):
return f"https://{self.host}"
def create_put_request(self, s3_path, data):
headers = self._create_request("PUT", s3_path)
url = f"{self.url}/{s3_path}"
response = requests.put(url, headers=headers, data=data)
return response
def _create_request(self, method, s3_path):
date = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
auth = self._create_authorization(method, self.bucket, s3_path, date, self.key, self.secret, self.content_type)
headers = {
"Host": self.host,
"Date": date,
"Authorization": auth,
"Content-Type": self.content_type
}
return headers
@staticmethod
def _create_authorization(method, bucket, s3_path, date, s3_key, s3_secret, content_type="", md5_hash=""):
payload = f"{method}\n{md5_hash}\n{content_type}\n{date}\n/{bucket.strip('/')}/{s3_path.strip('/')}"
signature = base64.b64encode(
hmac.new(s3_secret.encode(), payload.encode(), hashlib.sha1).digest()
).decode()
return f"AWS {s3_key}:{signature}"
def read_csv_as_string(file_path):
"""
Reads a CSV file from the given path and returns its content as a single string.
"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
return file.read()
except FileNotFoundError:
print(f"Error: The file {file_path} does not exist.")
return None
except IOError as e:
print(f"IO error occurred: {str(e)}")
return None
CSV_DIR = "/data/csv_files_service/"
#CSV_DIR = "csv_files/"
# Define the path to the file containing the installation name
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import Callable
def interpret_limb_bitmap(bitmap_value):
# The bit for string 1 also monitors all 5 strings: 0000 0000 means All 5 strings activated. 0000 0001 means string 1 disabled.
string1_disabled = int((bitmap_value & 0b00001) != 0)
string2_disabled = int((bitmap_value & 0b00010) != 0)
string3_disabled = int((bitmap_value & 0b00100) != 0)
string4_disabled = int((bitmap_value & 0b01000) != 0)
string5_disabled = int((bitmap_value & 0b10000) != 0)
n_limb_strings = string1_disabled+string2_disabled+string3_disabled+string4_disabled+string5_disabled
return n_limb_strings
def create_csv_signals(firmware_version):
def read_power(status):
return int(read_current(status) * read_voltage(status))
read_voltage = c.read_float(register=999, scale_factor=0.01, offset=0, places=2)
read_current = c.read_float(register=1000, scale_factor=0.01, offset=-10000, places=2)
read_limb_bitmap = c.read_bitmap(1059)
def string1_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b00001) != 0)
def string2_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b00010) != 0)
def string3_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b00100) != 0)
def string4_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b01000) != 0)
def string5_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b10000) != 0)
def limp_strings_value(status):
return interpret_limb_bitmap(read_limb_bitmap(status))
def calc_power_limit_imposed_by_voltage_limit(v, i, v_limit, r_int):
# type: (float, float, float, float) -> float
dv = v_limit - v
di = dv / r_int
p_limit = v_limit * (i + di)
return p_limit
def calc_power_limit_imposed_by_current_limit(v, i, i_limit, r_int):
# type: (float, float, float, float) -> float
di = i_limit - i
dv = di * r_int
p_limit = i_limit * (v + dv)
return p_limit
def calc_max_charge_power(status):
# type: (BatteryStatus) -> int
n_strings = cfg.NUM_OF_STRING_PER_BATTERY-limp_strings_value(status)
i_max = n_strings * cfg.I_MAX_PER_STRING
v_max = cfg.V_MAX
r_int_min = cfg.R_STRING_MIN / n_strings
r_int_max = cfg.R_STRING_MAX / n_strings
v = read_voltage(status)
i = read_current(status)
p_limits = [
calc_power_limit_imposed_by_voltage_limit(v, i, v_max,r_int_min),
calc_power_limit_imposed_by_voltage_limit(v, i, v_max, r_int_max),
calc_power_limit_imposed_by_current_limit(v, i, i_max, r_int_min),
calc_power_limit_imposed_by_current_limit(v, i, i_max, r_int_max),
]
p_limit = min(p_limits) # p_limit is normally positive here (signed)
p_limit = max(p_limit, 0) # charge power must not become negative
return int(p_limit)
def calc_max_discharge_power(status):
n_strings = cfg.NUM_OF_STRING_PER_BATTERY-limp_strings_value(status)
max_discharge_current = n_strings*cfg.I_MAX_PER_STRING
return int(max_discharge_current*read_voltage(status))
def return_led_state_blue(status):
led_state = c.read_led_state(register=1004, led=LedColor.blue)(status)
if led_state == LedState.blinking_fast or led_state == LedState.blinking_slow:
return "Blinking"
elif led_state == LedState.on:
return "On"
elif led_state == LedState.off:
return "Off"
return "Unknown"
def return_led_state_red(status):
led_state = c.read_led_state(register=1004, led=LedColor.red)(status)
if led_state == LedState.blinking_fast or led_state == LedState.blinking_slow:
return "Blinking"
elif led_state == LedState.on:
return "On"
elif led_state == LedState.off:
return "Off"
return "Unknown"
def return_led_state_green(status):
led_state = c.read_led_state(register=1004, led=LedColor.green)(status)
if led_state == LedState.blinking_fast or led_state == LedState.blinking_slow:
return "Blinking"
elif led_state == LedState.on:
return "On"
elif led_state == LedState.off:
return "Off"
return "Unknown"
def return_led_state_amber(status):
led_state = c.read_led_state(register=1004, led=LedColor.amber)(status)
if led_state == LedState.blinking_fast or led_state == LedState.blinking_slow:
return "Blinking"
elif led_state == LedState.on:
return "On"
elif led_state == LedState.off:
return "Off"
return "Unknown"
total_current = c.read_float(register=1062, scale_factor=0.01, offset=-10000, places=1)
def read_total_current(status):
return total_current(status)
def read_heating_current(status):
return total_current(status) - read_current(status)
def read_heating_power(status):
return read_voltage(status) * read_heating_current(status)
soc_ah = c.read_float(register=1002, scale_factor=0.1, offset=-10000, places=1)
def read_soc_ah(status):
return soc_ah(status)
def hex_string_to_ascii(hex_string):
# Ensure the hex_string is correctly formatted without spaces
hex_string = hex_string.replace(" ", "")
# Convert every two characters (a byte) in the hex string to ASCII
ascii_string = ''.join([chr(int(hex_string[i:i+2], 16)) for i in range(0, len(hex_string), 2)])
return ascii_string
battery_status_reader = c.read_hex_string(1060,2)
def read_eoc_reached(status):
battery_status_string = battery_status_reader(status)
#if hex_string_to_ascii(battery_status_string) == "EOC_":
#return True
#return False
return hex_string_to_ascii(battery_status_string) == "EOC_"
def read_serial_number(status):
serial_regs = [1055, 1056, 1057, 1058]
serial_parts = []
for reg in serial_regs:
# reading each register as a single hex value
hex_value_fun = c.read_hex_string(reg, 1)
hex_value = hex_value_fun(status)
# append without spaces and leading zeros stripped if any
serial_parts.append(hex_value.replace(' ', ''))
# concatenate all parts to form the full serial number
serial_number = ''.join(serial_parts).rstrip('0')
return serial_number
return [
CsvSignal('/Battery/Devices/FwVersion', firmware_version),
CsvSignal('/Battery/Devices/Dc/Power', read_power, 'W'),
CsvSignal('/Battery/Devices/Dc/Voltage', read_voltage, 'V'),
CsvSignal('/Battery/Devices/Soc', c.read_float(register=1053, scale_factor=0.1, offset=0, places=1), '%'),
CsvSignal('/Battery/Devices/Temperatures/Cells/Average', c.read_float(register=1003, scale_factor=0.1, offset=-400, places=1), 'C'),
CsvSignal('/Battery/Devices/Dc/Current', read_current, 'A'),
CsvSignal('/Battery/Devices/BusCurrent', read_total_current, 'A'),
CsvSignal('/Battery/Devices/CellsCurrent', read_current, 'A'),
CsvSignal('/Battery/Devices/HeatingCurrent', read_heating_current, 'A'),
CsvSignal('/Battery/Devices/HeatingPower', read_heating_power, 'W'),
CsvSignal('/Battery/Devices/SOCAh', read_soc_ah),
CsvSignal('/Battery/Devices/Leds/Blue', return_led_state_blue),
CsvSignal('/Battery/Devices/Leds/Red', return_led_state_red),
CsvSignal('/Battery/Devices/Leds/Green', return_led_state_green),
CsvSignal('/Battery/Devices/Leds/Amber', return_led_state_amber),
CsvSignal('/Battery/Devices/BatteryStrings/String1Active', string1_disabled),
CsvSignal('/Battery/Devices/BatteryStrings/String2Active', string2_disabled),
CsvSignal('/Battery/Devices/BatteryStrings/String3Active', string3_disabled),
CsvSignal('/Battery/Devices/BatteryStrings/String4Active', string4_disabled),
CsvSignal('/Battery/Devices/BatteryStrings/String5Active', string5_disabled),
CsvSignal('/Battery/Devices/IoStatus/ConnectedToDcBus', c.read_bool(register=1013, bit=0)),
CsvSignal('/Battery/Devices/IoStatus/AlarmOutActive', c.read_bool(register=1013, bit=1)),
CsvSignal('/Battery/Devices/IoStatus/InternalFanActive', c.read_bool(register=1013, bit=2)),
CsvSignal('/Battery/Devices/IoStatus/VoltMeasurementAllowed', c.read_bool(register=1013, bit=3)),
CsvSignal('/Battery/Devices/IoStatus/AuxRelayBus', c.read_bool(register=1013, bit=4)),
CsvSignal('/Battery/Devices/IoStatus/RemoteStateActive', c.read_bool(register=1013, bit=5)),
CsvSignal('/Battery/Devices/IoStatus/RiscActive', c.read_bool(register=1013, bit=6)),
CsvSignal('/Battery/Devices/Eoc', read_eoc_reached),
CsvSignal('/Battery/Devices/SerialNumber', read_serial_number),
CsvSignal('/Battery/Devices/TimeSinceTOC', c.read_float(register=1052)),
CsvSignal('/Battery/Devices/MaxChargePower', calc_max_charge_power),
CsvSignal('/Battery/Devices/MaxDischargePower', calc_max_discharge_power),
# Warnings
CsvSignal('/Battery/Devices/WarningFlags/TaM1', c.read_bool(register=1005, bit=1)),
CsvSignal('/Battery/Devices/WarningFlags/TbM1', c.read_bool(register=1005, bit=4)),
CsvSignal('/Battery/Devices/WarningFlags/VBm1', c.read_bool(register=1005, bit=6)),
CsvSignal('/Battery/Devices/WarningFlags/VBM1', c.read_bool(register=1005, bit=8)),
CsvSignal('/Battery/Devices/WarningFlags/IDM1', c.read_bool(register=1005, bit=10)),
CsvSignal('/Battery/Devices/WarningFlags/vsm1', c.read_bool(register=1005, bit=22)),
CsvSignal('/Battery/Devices/WarningFlags/vsM1', c.read_bool(register=1005, bit=24)),
CsvSignal('/Battery/Devices/WarningFlags/iCM1', c.read_bool(register=1005, bit=26)),
CsvSignal('/Battery/Devices/WarningFlags/iDM1', c.read_bool(register=1005, bit=28)),
CsvSignal('/Battery/Devices/WarningFlags/MID1', c.read_bool(register=1005, bit=30)),
CsvSignal('/Battery/Devices/WarningFlags/BLPW', c.read_bool(register=1005, bit=32)),
CsvSignal('/Battery/Devices/WarningFlags/CCBF', c.read_bool(register=1005, bit=33)),
CsvSignal('/Battery/Devices/WarningFlags/Ah_W', c.read_bool(register=1005, bit=35)),
CsvSignal('/Battery/Devices/WarningFlags/MPMM', c.read_bool(register=1005, bit=38)),
CsvSignal('/Battery/Devices/WarningFlags/TCdi', c.read_bool(register=1005, bit=40)),
CsvSignal('/Battery/Devices/WarningFlags/LMPW', c.read_bool(register=1005, bit=44)),
CsvSignal('/Battery/Devices/WarningFlags/TOCW', c.read_bool(register=1005, bit=47)),
CsvSignal('/Battery/Devices/WarningFlags/BUSL', c.read_bool(register=1005, bit=49)),
# Alarms
CsvSignal('/Battery/Devices/AlarmFlags/Tam', c.read_bool(register=1005, bit=0)),
CsvSignal('/Battery/Devices/AlarmFlags/TaM2', c.read_bool(register=1005, bit=2)),
CsvSignal('/Battery/Devices/AlarmFlags/Tbm', c.read_bool(register=1005, bit=3)),
CsvSignal('/Battery/Devices/AlarmFlags/TbM2', c.read_bool(register=1005, bit=5)),
CsvSignal('/Battery/Devices/AlarmFlags/VBm2', c.read_bool(register=1005, bit=7)),
CsvSignal('/Battery/Devices/AlarmFlags/VBM2', c.read_bool(register=1005, bit=9)),
CsvSignal('/Battery/Devices/AlarmFlags/IDM2', c.read_bool(register=1005, bit=11)),
CsvSignal('/Battery/Devices/AlarmFlags/ISOB', c.read_bool(register=1005, bit=12)),
CsvSignal('/Battery/Devices/AlarmFlags/MSWE', c.read_bool(register=1005, bit=13)),
CsvSignal('/Battery/Devices/AlarmFlags/FUSE', c.read_bool(register=1005, bit=14)),
CsvSignal('/Battery/Devices/AlarmFlags/HTRE', c.read_bool(register=1005, bit=15)),
CsvSignal('/Battery/Devices/AlarmFlags/TCPE', c.read_bool(register=1005, bit=16)),
CsvSignal('/Battery/Devices/AlarmFlags/STRE', c.read_bool(register=1005, bit=17)),
CsvSignal('/Battery/Devices/AlarmFlags/CME', c.read_bool(register=1005, bit=18)),
CsvSignal('/Battery/Devices/AlarmFlags/HWFL', c.read_bool(register=1005, bit=19)),
CsvSignal('/Battery/Devices/AlarmFlags/HWEM', c.read_bool(register=1005, bit=20)),
CsvSignal('/Battery/Devices/AlarmFlags/ThM', c.read_bool(register=1005, bit=21)),
CsvSignal('/Battery/Devices/AlarmFlags/vsm2', c.read_bool(register=1005, bit=23)),
CsvSignal('/Battery/Devices/AlarmFlags/vsM2', c.read_bool(register=1005, bit=25)),
CsvSignal('/Battery/Devices/AlarmFlags/iCM2', c.read_bool(register=1005, bit=27)),
CsvSignal('/Battery/Devices/AlarmFlags/iDM2', c.read_bool(register=1005, bit=29)),
CsvSignal('/Battery/Devices/AlarmFlags/MID2', c.read_bool(register=1005, bit=31)),
CsvSignal('/Battery/Devices/AlarmFlags/HTFS', c.read_bool(register=1005, bit=42)),
CsvSignal('/Battery/Devices/AlarmFlags/DATA', c.read_bool(register=1005, bit=43)),
CsvSignal('/Battery/Devices/AlarmFlags/LMPA', c.read_bool(register=1005, bit=45)),
CsvSignal('/Battery/Devices/AlarmFlags/HEBT', c.read_bool(register=1005, bit=46)),
CsvSignal('/Battery/Devices/AlarmFlags/CURM', c.read_bool(register=1005, bit=48)),
]
def init_modbus(tty):
# type: (str) -> Modbus
logging.debug('initializing Modbus')
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)
def read_modbus_registers(modbus, slave_address, base_address=cfg.BASE_ADDRESS, count=cfg.NO_OF_REGISTERS):
# type: (Modbus, int) -> ReadInputRegistersResponse
logging.debug('requesting modbus registers {0}-{1}'.format(base_address, base_address + count))
return modbus.read_input_registers(
address=base_address,
count=count,
unit=slave_address)
def read_firmware_version(modbus, slave_address):
# type: (Modbus, int) -> str
logging.debug('reading firmware version')
try:
modbus.connect()
response = read_modbus_registers(modbus, slave_address, base_address=1054, count=1)
register = response.registers[0]
return '{0:0>4X}'.format(register)
finally:
modbus.close() # close in any case
def init_main_loop():
# type: () -> DBusGMainLoop
logging.debug('initializing DBusGMainLoop Loop')
DBusGMainLoop(set_as_default=True)
return GLib.MainLoop()
def report_slave_id(modbus, slave_address):
# type: (Modbus, int) -> str
slave = str(slave_address)
logging.debug('requesting slave id from node ' + slave)
try:
modbus.connect()
request = ReportSlaveIdRequest(unit=slave_address)
response = modbus.execute(request)
if response is ExceptionResponse or issubclass(type(response), ModbusException):
raise Exception('failed to get slave id from ' + slave + ' : ' + str(response))
return response.identifier
finally:
modbus.close()
def parse_slave_id(modbus, slave_address):
# type: (Modbus, int) -> (str, str, int)
slave_id = report_slave_id(modbus, slave_address)
sid = re.sub(b'[^\x20-\x7E]', b'', slave_id) # remove weird special chars
match = re.match('(?P<hw>48TL(?P<ah>\d+)) *(?P<bms>.*)', sid.decode('ascii'))
if match is None:
raise Exception('no known battery found')
return match.group('hw'), match.group('bms'), int(match.group('ah'))
def identify_battery(modbus, slave_address):
# type: (Modbus, int) -> Battery
logging.info('identifying battery...')
hardware_version, bms_version, ampere_hours = parse_slave_id(modbus, slave_address)
firmware_version = read_firmware_version(modbus, slave_address)
specs = Battery(
slave_address=slave_address,
hardware_version=hardware_version,
firmware_version=firmware_version,
bms_version=bms_version,
ampere_hours=ampere_hours)
logging.info('battery identified:\n{0}'.format(str(specs)))
return specs
def identify_batteries(modbus):
# type: (Modbus) -> list[Battery]
def _identify_batteries():
address_range = range(1, cfg.MAX_SLAVE_ADDRESS + 1)
for slave_address in address_range:
try:
yield identify_battery(modbus, slave_address)
except Exception as e:
logging.info('failed to identify battery at {0} : {1}'.format(str(slave_address), str(e)))
return list(_identify_batteries()) # force that lazy iterable!
def read_modbus_registers(modbus, slave_address, base_address=cfg.BASE_ADDRESS, count=cfg.NO_OF_REGISTERS):
# type: (Modbus, int) -> ReadInputRegistersResponse
logging.debug('requesting modbus registers {0}-{1}'.format(base_address, base_address + count))
return modbus.read_input_registers(
address=base_address,
count=count,
unit=slave_address)
def read_battery_status(modbus, battery):
# type: (Modbus, Battery) -> BatteryStatus
"""
Read the modbus registers containing the battery's status info.
"""
logging.debug('reading battery status')
try:
modbus.connect()
data = read_modbus_registers(modbus, battery.slave_address)
return BatteryStatus(battery, data.registers)
finally:
modbus.close() # close in any case
def get_installation_name(file_path):
with open(file_path, 'r') as file:
return file.read().strip()
def manage_csv_files(directory_path, max_files=20):
csv_files = [f for f in os.listdir(directory_path)]
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
# Remove oldest files if exceeds maximum
while len(csv_files) > max_files:
file_to_delete = os.path.join(directory_path, csv_files.pop(0))
os.remove(file_to_delete)
def serialize_for_csv(value):
if isinstance(value, (dict, list, tuple)):
return json.dumps(value, ensure_ascii=False)
return str(value)
def insert_id(path, id_number):
parts = path.split("/")
insert_position = parts.index("Devices") + 1
parts.insert(insert_position, str(id_number))
return "/".join(parts)
def create_csv_files(signals, statuses, node_numbers):
timestamp = int(time.time())
if timestamp % 2 != 0:
timestamp -= 1
# Create CSV directory if it doesn't exist
if not os.path.exists(CSV_DIR):
os.makedirs(CSV_DIR)
#installation_name = get_installation_name(INSTALLATION_NAME_FILE)
csv_filename = f"{timestamp}.csv"
csv_path = os.path.join(CSV_DIR, csv_filename)
# Append values to the CSV file
with open(csv_path, 'a', newline='') as csvfile:
csv_writer = csv.writer(csvfile, delimiter=';')
# Add a special row for the nodes configuration
nodes_config_path = "/Config/Devices/BatteryNodes"
nodes_list = ",".join(str(node) for node in node_numbers)
config_row = [nodes_config_path, nodes_list, ""]
csv_writer.writerow(config_row)
# Iterate over each node and signal to create rows in the new format
for i, node in enumerate(node_numbers):
for s in signals:
signal_name = insert_id(s.name, i+1)
#value = serialize_for_csv(s.get_value(statuses[i]))
value = s.get_value(statuses[i])
row_values = [signal_name, value, s.get_text]
csv_writer.writerow(row_values)
# Manage CSV files, keep a limited number of files
# Create the CSV as a string
csv_data = read_csv_as_string(csv_path)
# Create an S3config instance
s3_config = S3config()
response = s3_config.create_put_request(csv_filename, csv_data)
if response.status_code == 200:
os.remove(csv_path)
print("Success")
else:
failed_dir = os.path.join(CSV_DIR, "failed")
if not os.path.exists(failed_dir):
os.makedirs(failed_dir)
failed_path = os.path.join(failed_dir, csv_filename)
os.rename(csv_path, failed_path)
print("Uploading failed")
manage_csv_files(failed_dir, 10)
manage_csv_files(CSV_DIR)
def update(modbus, batteries, csv_signals):
# type: (Modbus, Iterable[Battery], DBus, Iterable[Signal]) -> bool
"""
Main update function
1. requests status record each battery via modbus,
2. parses the data using Signal.get_value
3. aggregates the data from all batteries into one datum using Signal.aggregate
4. publishes the data on the dbus
"""
logging.debug('starting update cycle')
statuses = [read_battery_status(modbus, battery) for battery in batteries]
node_numbers = [battery.slave_address for battery in batteries]
create_csv_files(csv_signals, statuses, node_numbers)
logging.debug('finished update cycle\n')
return True
def print_usage():
print ('Usage: ' + __file__ + ' <serial device>')
print ('Example: ' + __file__ + ' ttyUSB0')
def parse_cmdline_args(argv):
# type: (list[str]) -> str
if len(argv) == 0:
logging.info('missing command line argument for tty device')
print_usage()
sys.exit(1)
return argv[0]
alive = True # global alive flag, watchdog_task clears it, update_task sets it
def create_update_task(modbus, batteries, csv_signals, main_loop):
# type: (Modbus, DBus, Iterable[Battery], Iterable[Signal], DBusGMainLoop) -> Callable[[],bool]
"""
Creates an update task which runs the main update function
and resets the alive flag
"""
def update_task():
# type: () -> bool
global alive
alive = update(modbus, batteries, csv_signals)
if not alive:
logging.info('update_task: quitting main loop because of error')
main_loop.quit()
return alive
return update_task
def create_watchdog_task(main_loop):
# 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
return watchdog_task
def main(argv):
# type: (list[str]) -> ()
print("PAME")
logging.basicConfig(level=cfg.LOG_LEVEL)
logging.info('starting ' + __file__)
tty = parse_cmdline_args(argv)
modbus = init_modbus(tty)
batteries = identify_batteries(modbus)
n = len(batteries)
logging.info('found ' + str(n) + (' battery' if n == 1 else ' batteries'))
if n <= 0:
sys.exit(2)
bat = c.first(batteries) # report hw and fw version of first battery found
csv_signals = create_csv_signals(bat.firmware_version)
main_loop = init_main_loop() # must run before init_dbus because gobject does some global magic
# we do not use dbus this time. we only want modbus
update_task = create_update_task(modbus, batteries, csv_signals, main_loop)
watchdog_task = create_watchdog_task(main_loop)
GLib.timeout_add(cfg.UPDATE_INTERVAL * 2, watchdog_task) # add watchdog first
GLib.timeout_add(cfg.UPDATE_INTERVAL, update_task) # call update once every update_interval
logging.info('starting GLib.MainLoop')
main_loop.run()
logging.info('GLib.MainLoop was shut down')
sys.exit(0xFF) # reaches this only on error
if __name__ == "__main__":
main(sys.argv[1:])

View File

@ -0,0 +1,7 @@
#!/bin/bash
. /opt/victronenergy/serial-starter/run-service.sh
app=/opt/victronenergy/dbus-csv-files/dbus-csv-files.py
args="$tty"
start $args

View File

@ -0,0 +1,51 @@
import serial
import logging
# dbus configuration
FIRMWARE_VERSION = 1 # value returned by getValue (getText returns string value reported by battery)
HARDWARE_VERSION = 1 # value returned by getValue (getText returns string value reported by battery)
CONNECTION = 'Modbus RTU'
PRODUCT_NAME = 'FZS 48TL200'
PRODUCT_ID = 0xB012 # assigned by victron
DEVICE_INSTANCE = 1
SERVICE_NAME_PREFIX = 'com.victronenergy.battery.'
# driver configuration
SOFTWARE_VERSION = '3.0.3'
UPDATE_INTERVAL = 2000 # milliseconds
#LOG_LEVEL = logging.INFO
LOG_LEVEL = logging.DEBUG
# modbus configuration
BASE_ADDRESS = 999
#NO_OF_REGISTERS = 63
NO_OF_REGISTERS = 64
MAX_SLAVE_ADDRESS = 10
# RS 485 configuration
PARITY = serial.PARITY_ODD
TIMEOUT = 0.1 # seconds
BAUD_RATE = 115200
BYTE_SIZE = 8
STOP_BITS = 1
MODE = 'rtu'
# battery configuration
MAX_CHARGE_VOLTAGE = 58
I_MAX_PER_STRING = 15
NUM_OF_STRING_PER_BATTERY = 5
AH_PER_STRING = 40
V_MAX = 54.2
R_STRING_MIN = 0.125
R_STRING_MAX = 0.250

View File

@ -0,0 +1,119 @@
from collections import Iterable
from decimal import *
import config as cfg
from data import LedState, BatteryStatus
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import Callable
def read_bool(register, bit):
# type: (int, int) -> Callable[[BatteryStatus], bool]
def get_value(status):
# type: (BatteryStatus) -> bool
value = status.modbus_data[register - cfg.BASE_ADDRESS]
return value & (1 << bit) > 0
return get_value
def read_float(register, scale_factor=1.0, offset=0.0, places=2):
# type: (int, float, float) -> Callable[[BatteryStatus], float]
def get_value(status):
# type: (BatteryStatus) -> float
value = status.modbus_data[register - cfg.BASE_ADDRESS]
if value >= 0x8000: # convert to signed int16
value -= 0x10000 # fiamm stores their integers signed AND with sign-offset @#%^&!
result = (value+offset)*scale_factor
return round(result,places)
return get_value
def read_hex_string(register, count):
# type: (int, int) -> Callable[[BatteryStatus], str]
"""
reads count consecutive modbus registers from start_address,
and returns a hex representation of it:
e.g. for count=4: DEAD BEEF DEAD BEEF.
"""
start = register - cfg.BASE_ADDRESS
end = start + count
def get_value(status):
# type: (BatteryStatus) -> str
return ' '.join(['{0:0>4X}'.format(x) for x in status.modbus_data[start:end]])
return get_value
def read_led_state(register, led):
# type: (int, int) -> Callable[[BatteryStatus], int]
read_lo = read_bool(register, led * 2)
read_hi = read_bool(register, led * 2 + 1)
def get_value(status):
# type: (BatteryStatus) -> int
lo = read_lo(status)
hi = read_hi(status)
if hi:
if lo:
return LedState.blinking_fast
else:
return LedState.blinking_slow
else:
if lo:
return LedState.on
else:
return LedState.off
return get_value
def read_bitmap(register):
# type: (int) -> Callable[[BatteryStatus], bitmap]
def get_value(status):
# type: (BatteryStatus) -> bitmap
value = status.modbus_data[register - cfg.BASE_ADDRESS]
return value
return get_value
def append_unit(unit):
# type: (unicode) -> Callable[[unicode], unicode]
def get_text(v):
# type: (unicode) -> unicode
return "{0}{1}".format(str(v), unit)
return get_text
def mean(numbers):
# type: (Iterable[float] | Iterable[int]) -> float
return float("{:.2f}".format(float(sum(numbers)) / len(numbers)))
def ssum(numbers):
# type: (Iterable[float] | Iterable[int]) -> float
return float("{:.2f}".format(float(sum(numbers))))
def first(ts):
return next(t for t in ts)
def return_in_list(ts):
return ts

View File

@ -0,0 +1,97 @@
import config as cfg
from collections import Iterable
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import Callable
class LedState(object):
"""
from page 6 of the '48TLxxx ModBus Protocol doc'
"""
off = 0
on = 1
blinking_slow = 2
blinking_fast = 3
class LedColor(object):
green = 0
amber = 1
blue = 2
red = 3
class CsvSignal(object):
def __init__(self, name, get_value, get_text = None):
self.name = name
self.get_value = get_value if callable(get_value) else lambda _: get_value
self.get_text = get_text
if get_text is None:
self.get_text = ""
class Signal(object):
def __init__(self, dbus_path, aggregate, get_value, get_text=None):
# type: (str, Callable[[Iterable[object]],object], Callable[[BatteryStatus],object] | object, Callable[[object],unicode] | object)->None
"""
A Signal holds all information necessary for the handling of a
certain datum (e.g. voltage) published by the battery.
:param dbus_path: str
object_path on DBus where the datum needs to be published
:param aggregate: Iterable[object] -> object
function that combines the values of multiple batteries into one.
e.g. sum for currents, or mean for voltages
:param get_value: (BatteryStatus) -> object
function to extract the datum from the modbus record,
alternatively: a constant
:param get_text: (object) -> unicode [optional]
function to render datum to text, needed by DBus
alternatively: a constant
"""
self.dbus_path = dbus_path
self.aggregate = aggregate
self.get_value = get_value if callable(get_value) else lambda _: get_value
self.get_text = get_text if callable(get_text) else lambda _: str(get_text)
# if no 'get_text' provided use 'default_text' if available, otherwise str()
if get_text is None:
self.get_text = str
class Battery(object):
""" Data record to hold hardware and firmware specs of the battery """
def __init__(self, slave_address, hardware_version, firmware_version, bms_version, ampere_hours):
# type: (int, str, str, str, int) -> None
self.slave_address = slave_address
self.hardware_version = hardware_version
self.firmware_version = firmware_version
self.bms_version = bms_version
self.ampere_hours = ampere_hours
def __str__(self):
return 'slave address = {0}\nhardware version = {1}\nfirmware version = {2}\nbms version = {3}\nampere hours = {4}'.format(
self.slave_address, self.hardware_version, self.firmware_version, self.bms_version, str(self.ampere_hours))
class BatteryStatus(object):
"""
record holding the current status of a battery
"""
def __init__(self, battery, modbus_data):
# type: (Battery, list[int]) -> None
self.battery = battery
self.modbus_data = modbus_data

View File

@ -0,0 +1,980 @@
#!/usr/bin/python3 -u
# coding=utf-8
import re
import sys
import logging
from gi.repository import GLib
import config as cfg
import convert as c
from pymodbus.register_read_message import ReadInputRegistersResponse
from pymodbus.client.sync import ModbusSerialClient as Modbus
from pymodbus.other_message import ReportSlaveIdRequest
from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ExceptionResponse
from dbus.mainloop.glib import DBusGMainLoop
from data import BatteryStatus, Signal, Battery, LedColor, CsvSignal, LedState
from collections import Iterable
from os import path
app_dir = path.dirname(path.realpath(__file__))
sys.path.insert(1, path.join(app_dir, 'ext', 'velib_python'))
from vedbus import VeDbusService as DBus
import time
import os
import csv
import requests
import hmac
import hashlib
import base64
from datetime import datetime
import io
class S3config:
def __init__(self):
self.bucket = "1-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
self.region = "sos-ch-dk-2"
self.provider = "exo.io"
self.key = "EXOcc0e47a4c4d492888ff5a7f2"
self.secret = "79QG4unMh7MeVacMnXr5xGxEyAlWZDIdM-dg_nXFFr4"
self.content_type = "text/plain; charset=utf-8"
@property
def host(self):
return f"{self.bucket}.{self.region}.{self.provider}"
@property
def url(self):
return f"https://{self.host}"
def create_put_request(self, s3_path, data):
headers = self._create_request("PUT", s3_path)
url = f"{self.url}/{s3_path}"
response = requests.put(url, headers=headers, data=data)
return response
def _create_request(self, method, s3_path):
date = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
auth = self._create_authorization(method, self.bucket, s3_path, date, self.key, self.secret, self.content_type)
headers = {
"Host": self.host,
"Date": date,
"Authorization": auth,
"Content-Type": self.content_type
}
return headers
@staticmethod
def _create_authorization(method, bucket, s3_path, date, s3_key, s3_secret, content_type="", md5_hash=""):
payload = f"{method}\n{md5_hash}\n{content_type}\n{date}\n/{bucket.strip('/')}/{s3_path.strip('/')}"
signature = base64.b64encode(
hmac.new(s3_secret.encode(), payload.encode(), hashlib.sha1).digest()
).decode()
return f"AWS {s3_key}:{signature}"
def read_csv_as_string(file_path):
"""
Reads a CSV file from the given path and returns its content as a single string.
"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
return file.read()
except FileNotFoundError:
print(f"Error: The file {file_path} does not exist.")
return None
except IOError as e:
print(f"IO error occurred: {str(e)}")
return None
CSV_DIR = "/data/csv_files/"
#CSV_DIR = "csv_files/"
# Define the path to the file containing the installation name
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import Callable
def interpret_limb_bitmap(bitmap_value):
# The bit for string 1 also monitors all 5 strings: 0000 0000 means All 5 strings activated. 0000 0001 means string 1 disabled.
string1_disabled = int((bitmap_value & 0b00001) != 0)
string2_disabled = int((bitmap_value & 0b00010) != 0)
string3_disabled = int((bitmap_value & 0b00100) != 0)
string4_disabled = int((bitmap_value & 0b01000) != 0)
string5_disabled = int((bitmap_value & 0b10000) != 0)
n_limb_strings = string1_disabled+string2_disabled+string3_disabled+string4_disabled+string5_disabled
return n_limb_strings
def create_csv_signals(firmware_version):
def read_power(status):
return int(read_current(status) * read_voltage(status))
read_voltage = c.read_float(register=999, scale_factor=0.01, offset=0, places=2)
read_current = c.read_float(register=1000, scale_factor=0.01, offset=-10000, places=2)
read_limb_bitmap = c.read_bitmap(1059)
def string1_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b00001) != 0)
def string2_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b00010) != 0)
def string3_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b00100) != 0)
def string4_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b01000) != 0)
def string5_disabled(status):
bitmap_value = read_limb_bitmap(status)
return int((bitmap_value & 0b10000) != 0)
def limp_strings_value(status):
return interpret_limb_bitmap(read_limb_bitmap(status))
def calc_power_limit_imposed_by_voltage_limit(v, i, v_limit, r_int):
# type: (float, float, float, float) -> float
dv = v_limit - v
di = dv / r_int
p_limit = v_limit * (i + di)
return p_limit
def calc_power_limit_imposed_by_current_limit(v, i, i_limit, r_int):
# type: (float, float, float, float) -> float
di = i_limit - i
dv = di * r_int
p_limit = i_limit * (v + dv)
return p_limit
def calc_max_charge_power(status):
# type: (BatteryStatus) -> int
n_strings = cfg.NUM_OF_STRING_PER_BATTERY-limp_strings_value(status)
i_max = n_strings * cfg.I_MAX_PER_STRING
v_max = cfg.V_MAX
r_int_min = cfg.R_STRING_MIN / n_strings
r_int_max = cfg.R_STRING_MAX / n_strings
v = read_voltage(status)
i = read_current(status)
p_limits = [
calc_power_limit_imposed_by_voltage_limit(v, i, v_max,r_int_min),
calc_power_limit_imposed_by_voltage_limit(v, i, v_max, r_int_max),
calc_power_limit_imposed_by_current_limit(v, i, i_max, r_int_min),
calc_power_limit_imposed_by_current_limit(v, i, i_max, r_int_max),
]
p_limit = min(p_limits) # p_limit is normally positive here (signed)
p_limit = max(p_limit, 0) # charge power must not become negative
return int(p_limit)
def calc_max_discharge_power(status):
n_strings = cfg.NUM_OF_STRING_PER_BATTERY-limp_strings_value(status)
max_discharge_current = n_strings*cfg.I_MAX_PER_STRING
return int(max_discharge_current*read_voltage(status))
def return_led_state_blue(status):
led_state = c.read_led_state(register=1004, led=LedColor.blue)(status)
if led_state == LedState.blinking_fast or led_state == LedState.blinking_slow:
return "Blinking"
elif led_state == LedState.on:
return "On"
elif led_state == LedState.off:
return "Off"
return "Unknown"
def return_led_state_red(status):
led_state = c.read_led_state(register=1004, led=LedColor.red)(status)
if led_state == LedState.blinking_fast or led_state == LedState.blinking_slow:
return "Blinking"
elif led_state == LedState.on:
return "On"
elif led_state == LedState.off:
return "Off"
return "Unknown"
def return_led_state_green(status):
led_state = c.read_led_state(register=1004, led=LedColor.green)(status)
if led_state == LedState.blinking_fast or led_state == LedState.blinking_slow:
return "Blinking"
elif led_state == LedState.on:
return "On"
elif led_state == LedState.off:
return "Off"
return "Unknown"
def return_led_state_amber(status):
led_state = c.read_led_state(register=1004, led=LedColor.amber)(status)
if led_state == LedState.blinking_fast or led_state == LedState.blinking_slow:
return "Blinking"
elif led_state == LedState.on:
return "On"
elif led_state == LedState.off:
return "Off"
return "Unknown"
total_current = c.read_float(register=1062, scale_factor=0.01, offset=-10000, places=1)
def read_total_current(status):
return total_current(status)
def read_heating_current(status):
return total_current(status) - read_current(status)
def read_heating_power(status):
return read_voltage(status) * read_heating_current(status)
soc_ah = c.read_float(register=1002, scale_factor=0.1, offset=-10000, places=1)
def read_soc_ah(status):
return soc_ah(status)
def hex_string_to_ascii(hex_string):
# Ensure the hex_string is correctly formatted without spaces
hex_string = hex_string.replace(" ", "")
# Convert every two characters (a byte) in the hex string to ASCII
ascii_string = ''.join([chr(int(hex_string[i:i+2], 16)) for i in range(0, len(hex_string), 2)])
return ascii_string
battery_status_reader = c.read_hex_string(1060,2)
def read_eoc_reached(status):
battery_status_string = battery_status_reader(status)
#if hex_string_to_ascii(battery_status_string) == "EOC_":
#return True
#return False
return hex_string_to_ascii(battery_status_string) == "EOC_"
def read_serial_number(status):
serial_regs = [1055, 1056, 1057, 1058]
serial_parts = []
for reg in serial_regs:
# reading each register as a single hex value
hex_value_fun = c.read_hex_string(reg, 1)
hex_value = hex_value_fun(status)
# append without spaces and leading zeros stripped if any
serial_parts.append(hex_value.replace(' ', ''))
# concatenate all parts to form the full serial number
serial_number = ''.join(serial_parts).rstrip('0')
return serial_number
return [
CsvSignal('/Battery/Devices/FwVersion', firmware_version),
CsvSignal('/Battery/Devices/Dc/Power', read_power, 'W'),
CsvSignal('/Battery/Devices/Dc/Voltage', read_voltage, 'V'),
CsvSignal('/Battery/Devices/Soc', c.read_float(register=1053, scale_factor=0.1, offset=0, places=1), '%'),
CsvSignal('/Battery/Devices/Temperatures/Cells/Average', c.read_float(register=1003, scale_factor=0.1, offset=-400, places=1), 'C'),
CsvSignal('/Battery/Devices/Dc/Current', read_current, 'A'),
CsvSignal('/Battery/Devices/BusCurrent', read_total_current, 'A'),
CsvSignal('/Battery/Devices/CellsCurrent', read_current, 'A'),
CsvSignal('/Battery/Devices/HeatingCurrent', read_heating_current, 'A'),
CsvSignal('/Battery/Devices/HeatingPower', read_heating_power, 'W'),
CsvSignal('/Battery/Devices/SOCAh', read_soc_ah),
CsvSignal('/Battery/Devices/Leds/Blue', return_led_state_blue),
CsvSignal('/Battery/Devices/Leds/Red', return_led_state_red),
CsvSignal('/Battery/Devices/Leds/Green', return_led_state_green),
CsvSignal('/Battery/Devices/Leds/Amber', return_led_state_amber),
CsvSignal('/Battery/Devices/BatteryStrings/String1Active', string1_disabled),
CsvSignal('/Battery/Devices/BatteryStrings/String2Active', string2_disabled),
CsvSignal('/Battery/Devices/BatteryStrings/String3Active', string3_disabled),
CsvSignal('/Battery/Devices/BatteryStrings/String4Active', string4_disabled),
CsvSignal('/Battery/Devices/BatteryStrings/String5Active', string5_disabled),
CsvSignal('/Battery/Devices/IoStatus/ConnectedToDcBus', c.read_bool(register=1013, bit=0)),
CsvSignal('/Battery/Devices/IoStatus/AlarmOutActive', c.read_bool(register=1013, bit=1)),
CsvSignal('/Battery/Devices/IoStatus/InternalFanActive', c.read_bool(register=1013, bit=2)),
CsvSignal('/Battery/Devices/IoStatus/VoltMeasurementAllowed', c.read_bool(register=1013, bit=3)),
CsvSignal('/Battery/Devices/IoStatus/AuxRelayBus', c.read_bool(register=1013, bit=4)),
CsvSignal('/Battery/Devices/IoStatus/RemoteStateActive', c.read_bool(register=1013, bit=5)),
CsvSignal('/Battery/Devices/IoStatus/RiscActive', c.read_bool(register=1013, bit=6)),
CsvSignal('/Battery/Devices/Eoc', read_eoc_reached),
CsvSignal('/Battery/Devices/SerialNumber', read_serial_number),
CsvSignal('/Battery/Devices/TimeSinceTOC', c.read_float(register=1052)),
CsvSignal('/Battery/Devices/MaxChargePower', calc_max_charge_power),
CsvSignal('/Battery/Devices/MaxDischargePower', calc_max_discharge_power),
# Warnings
CsvSignal('/Battery/Devices/WarningFlags/TaM1', c.read_bool(register=1005, bit=1)),
CsvSignal('/Battery/Devices/WarningFlags/TbM1', c.read_bool(register=1005, bit=4)),
CsvSignal('/Battery/Devices/WarningFlags/VBm1', c.read_bool(register=1005, bit=6)),
CsvSignal('/Battery/Devices/WarningFlags/VBM1', c.read_bool(register=1005, bit=8)),
CsvSignal('/Battery/Devices/WarningFlags/IDM1', c.read_bool(register=1005, bit=10)),
CsvSignal('/Battery/Devices/WarningFlags/vsm1', c.read_bool(register=1005, bit=22)),
CsvSignal('/Battery/Devices/WarningFlags/vsM1', c.read_bool(register=1005, bit=24)),
CsvSignal('/Battery/Devices/WarningFlags/iCM1', c.read_bool(register=1005, bit=26)),
CsvSignal('/Battery/Devices/WarningFlags/iDM1', c.read_bool(register=1005, bit=28)),
CsvSignal('/Battery/Devices/WarningFlags/MID1', c.read_bool(register=1005, bit=30)),
CsvSignal('/Battery/Devices/WarningFlags/BLPW', c.read_bool(register=1005, bit=32)),
CsvSignal('/Battery/Devices/WarningFlags/CCBF', c.read_bool(register=1005, bit=33)),
CsvSignal('/Battery/Devices/WarningFlags/Ah_W', c.read_bool(register=1005, bit=35)),
CsvSignal('/Battery/Devices/WarningFlags/MPMM', c.read_bool(register=1005, bit=38)),
CsvSignal('/Battery/Devices/WarningFlags/TCdi', c.read_bool(register=1005, bit=40)),
CsvSignal('/Battery/Devices/WarningFlags/LMPW', c.read_bool(register=1005, bit=44)),
CsvSignal('/Battery/Devices/WarningFlags/TOCW', c.read_bool(register=1005, bit=47)),
CsvSignal('/Battery/Devices/WarningFlags/BUSL', c.read_bool(register=1005, bit=49)),
# Alarms
CsvSignal('/Battery/Devices/AlarmFlags/Tam', c.read_bool(register=1005, bit=0)),
CsvSignal('/Battery/Devices/AlarmFlags/TaM2', c.read_bool(register=1005, bit=2)),
CsvSignal('/Battery/Devices/AlarmFlags/Tbm', c.read_bool(register=1005, bit=3)),
CsvSignal('/Battery/Devices/AlarmFlags/TbM2', c.read_bool(register=1005, bit=5)),
CsvSignal('/Battery/Devices/AlarmFlags/VBm2', c.read_bool(register=1005, bit=7)),
CsvSignal('/Battery/Devices/AlarmFlags/VBM2', c.read_bool(register=1005, bit=9)),
CsvSignal('/Battery/Devices/AlarmFlags/IDM2', c.read_bool(register=1005, bit=11)),
CsvSignal('/Battery/Devices/AlarmFlags/ISOB', c.read_bool(register=1005, bit=12)),
CsvSignal('/Battery/Devices/AlarmFlags/MSWE', c.read_bool(register=1005, bit=13)),
CsvSignal('/Battery/Devices/AlarmFlags/FUSE', c.read_bool(register=1005, bit=14)),
CsvSignal('/Battery/Devices/AlarmFlags/HTRE', c.read_bool(register=1005, bit=15)),
CsvSignal('/Battery/Devices/AlarmFlags/TCPE', c.read_bool(register=1005, bit=16)),
CsvSignal('/Battery/Devices/AlarmFlags/STRE', c.read_bool(register=1005, bit=17)),
CsvSignal('/Battery/Devices/AlarmFlags/CME', c.read_bool(register=1005, bit=18)),
CsvSignal('/Battery/Devices/AlarmFlags/HWFL', c.read_bool(register=1005, bit=19)),
CsvSignal('/Battery/Devices/AlarmFlags/HWEM', c.read_bool(register=1005, bit=20)),
CsvSignal('/Battery/Devices/AlarmFlags/ThM', c.read_bool(register=1005, bit=21)),
CsvSignal('/Battery/Devices/AlarmFlags/vsm2', c.read_bool(register=1005, bit=23)),
CsvSignal('/Battery/Devices/AlarmFlags/vsM2', c.read_bool(register=1005, bit=25)),
CsvSignal('/Battery/Devices/AlarmFlags/iCM2', c.read_bool(register=1005, bit=27)),
CsvSignal('/Battery/Devices/AlarmFlags/iDM2', c.read_bool(register=1005, bit=29)),
CsvSignal('/Battery/Devices/AlarmFlags/MID2', c.read_bool(register=1005, bit=31)),
CsvSignal('/Battery/Devices/AlarmFlags/HTFS', c.read_bool(register=1005, bit=42)),
CsvSignal('/Battery/Devices/AlarmFlags/DATA', c.read_bool(register=1005, bit=43)),
CsvSignal('/Battery/Devices/AlarmFlags/LMPA', c.read_bool(register=1005, bit=45)),
CsvSignal('/Battery/Devices/AlarmFlags/HEBT', c.read_bool(register=1005, bit=46)),
CsvSignal('/Battery/Devices/AlarmFlags/CURM', c.read_bool(register=1005, bit=48)),
]
def init_signals(hardware_version, firmware_version, n_batteries):
# type: (str,str,int) -> Iterable[Signal]
"""
A Signal holds all information necessary for the handling of a
certain datum (e.g. voltage) published by the battery.
Signal(dbus_path, aggregate, get_value, get_text = str)
dbus_path: str
object_path on DBus where the datum needs to be published
aggregate: Iterable[object] -> object
function that combines the values of multiple batteries into one.
e.g. sum for currents, or mean for voltages
get_value: (BatteryStatus) -> object [optional]
function to extract the datum from the modbus record,
alternatively: a constant
get_text: (object) -> unicode [optional]
function to render datum to text, needed by DBus
alternatively: a constant
The conversion functions use the same parameters (e.g scale_factor, offset)
as described in the document 'T48TLxxx ModBus Protocol Rev.7.1' which can
be found in the /doc folder
"""
product_id_hex = '0x{0:04x}'.format(cfg.PRODUCT_ID)
read_voltage = c.read_float(register=999, scale_factor=0.01, offset=0, places=2)
read_current = c.read_float(register=1000, scale_factor=0.01, offset=-10000, places=2)
def read_power(status):
return int(read_current(status) * read_voltage(status))
read_limb_bitmap = c.read_bitmap(1059)
def limp_strings_value(status):
return interpret_limb_bitmap(read_limb_bitmap(status))
def max_discharge_current(status):
return (cfg.NUM_OF_STRING_PER_BATTERY-limp_strings_value(status))*cfg.I_MAX_PER_STRING
def max_charge_current(status):
return status.battery.ampere_hours/2
def calc_power_limit_imposed_by_voltage_limit(v, i, v_limit, r_int):
# type: (float, float, float, float) -> float
dv = v_limit - v
di = dv / r_int
p_limit = v_limit * (i + di)
return p_limit
def calc_power_limit_imposed_by_current_limit(v, i, i_limit, r_int):
# type: (float, float, float, float) -> float
di = i_limit - i
dv = di * r_int
p_limit = i_limit * (v + dv)
return p_limit
def calc_max_charge_power(status):
# type: (BatteryStatus) -> int
n_strings = cfg.NUM_OF_STRING_PER_BATTERY-limp_strings_value(status)
i_max = n_strings * cfg.I_MAX_PER_STRING
v_max = cfg.V_MAX
r_int_min = cfg.R_STRING_MIN / n_strings
r_int_max = cfg.R_STRING_MAX / n_strings
v = read_voltage(status)
i = read_current(status)
p_limits = [
calc_power_limit_imposed_by_voltage_limit(v, i, v_max,r_int_min),
calc_power_limit_imposed_by_voltage_limit(v, i, v_max, r_int_max),
calc_power_limit_imposed_by_current_limit(v, i, i_max, r_int_min),
calc_power_limit_imposed_by_current_limit(v, i, i_max, r_int_max),
]
p_limit = min(p_limits) # p_limit is normally positive here (signed)
p_limit = max(p_limit, 0) # charge power must not become negative
return int(p_limit)
product_name = cfg.PRODUCT_NAME
if n_batteries > 1:
product_name = cfg.PRODUCT_NAME + ' x' + str(n_batteries)
return [
# Node Red related dbus paths
Signal('/TimeToTOCRequest', min, c.read_float(register=1052)),
Signal('/NumOfLimbStrings', c.return_in_list, get_value=limp_strings_value),
Signal('/NumOfBatteries', max, get_value=n_batteries),
Signal('/Dc/0/Voltage', c.mean, get_value=read_voltage, get_text=c.append_unit('V')),
Signal('/Dc/0/Current', c.ssum, get_value=read_current, get_text=c.append_unit('A')),
Signal('/Dc/0/Power', c.ssum, get_value=read_power, get_text=c.append_unit('W')),
Signal('/BussVoltage', c.mean, c.read_float(register=1001, scale_factor=0.01, offset=0, places=2), c.append_unit('V')),
Signal('/Soc', c.mean, c.read_float(register=1053, scale_factor=0.1, offset=0, places=1), c.append_unit('%')),
Signal('/LowestSoc', min, c.read_float(register=1053, scale_factor=0.1, offset=0, places=1), c.append_unit('%')),
Signal('/Dc/0/Temperature', c.mean, c.read_float(register=1003, scale_factor=0.1, offset=-400, places=1), c.append_unit(u'°C')),
Signal('/Dc/0/LowestTemperature', min, c.read_float(register=1003, scale_factor=0.1, offset=-400, places=1), c.append_unit(u'°C')),
# Charge/Discharge current, voltage and power
Signal('/Info/MaxDischargeCurrent', c.ssum, max_discharge_current,c.append_unit('A')),
Signal('/Info/MaxChargeCurrent', c.ssum, max_charge_current, c.append_unit('A')),
Signal('/Info/MaxChargeVoltage', min, cfg.MAX_CHARGE_VOLTAGE, c.append_unit('V')),
Signal('/Info/MaxChargePower', c.ssum, calc_max_charge_power),
# Victron mandatory dbus paths
Signal('/Mgmt/ProcessName', c.first, __file__),
Signal('/Mgmt/ProcessVersion', c.first, cfg.SOFTWARE_VERSION),
Signal('/Mgmt/Connection', c.first, cfg.CONNECTION),
Signal('/DeviceInstance', c.first, cfg.DEVICE_INSTANCE),
Signal('/ProductName', c.first, product_name),
Signal('/ProductId', c.first, cfg.PRODUCT_ID, product_id_hex),
Signal('/Connected', c.first, 1),
#Signal('/FirmwareVersion', c.first, cfg.FIRMWARE_VERSION, firmware_version),
Signal('/FirmwareVersion', c.return_in_list, firmware_version),
Signal('/HardwareVersion', c.first, cfg.HARDWARE_VERSION, hardware_version),
## Diagnostics
Signal('/Diagnostics/BmsVersion', c.first, lambda s: s.battery.bms_version),
# Warnings
#Signal('/Diagnostics/WarningFlags', c.first, c.read_hex_string(register=1005, count=4)),
Signal('/WarningFlags/TaM1', c.return_in_list, c.read_bool(register=1005, bit=1)),
Signal('/WarningFlags/TbM1', c.return_in_list, c.read_bool(register=1005, bit=4)),
Signal('/WarningFlags/VBm1', c.return_in_list, c.read_bool(register=1005, bit=6)),
Signal('/WarningFlags/VBM1', c.return_in_list, c.read_bool(register=1005, bit=8)),
Signal('/WarningFlags/IDM1', c.return_in_list, c.read_bool(register=1005, bit=10)),
Signal('/WarningFlags/vsm1', c.return_in_list, c.read_bool(register=1005, bit=22)),
Signal('/WarningFlags/vsM1', c.return_in_list, c.read_bool(register=1005, bit=24)),
Signal('/WarningFlags/iCM1', c.return_in_list, c.read_bool(register=1005, bit=26)),
Signal('/WarningFlags/iDM1', c.return_in_list, c.read_bool(register=1005, bit=28)),
Signal('/WarningFlags/MID1', c.return_in_list, c.read_bool(register=1005, bit=30)),
Signal('/WarningFlags/BLPW', c.return_in_list, c.read_bool(register=1005, bit=32)),
Signal('/WarningFlags/CCBF', c.return_in_list, c.read_bool(register=1005, bit=33)),
Signal('/WarningFlags/Ah_W', c.return_in_list, c.read_bool(register=1005, bit=35)),
Signal('/WarningFlags/MPMM', c.return_in_list, c.read_bool(register=1005, bit=38)),
#Signal('/WarningFlags/TCMM', c.return_in_list, c.read_bool(register=1005, bit=39)),
Signal('/WarningFlags/TCdi', c.return_in_list, c.read_bool(register=1005, bit=40)),
Signal('/WarningFlags/LMPW', c.return_in_list, c.read_bool(register=1005, bit=44)),
Signal('/WarningFlags/TOCW', c.return_in_list, c.read_bool(register=1005, bit=47)),
Signal('/WarningFlags/BUSL', c.return_in_list, c.read_bool(register=1005, bit=49)),
# Alarms
#Signal('/Diagnostics/AlarmFlags', c.first, c.read_hex_string(register=1009, count=4)),
Signal('/AlarmFlags/Tam', c.return_in_list, c.read_bool(register=1005, bit=0)),
Signal('/AlarmFlags/TaM2', c.return_in_list, c.read_bool(register=1005, bit=2)),
Signal('/AlarmFlags/Tbm', c.return_in_list, c.read_bool(register=1005, bit=3)),
Signal('/AlarmFlags/TbM2', c.return_in_list, c.read_bool(register=1005, bit=5)),
Signal('/AlarmFlags/VBm2', c.return_in_list, c.read_bool(register=1005, bit=7)),
Signal('/AlarmFlags/VBM2', c.return_in_list, c.read_bool(register=1005, bit=9)),
Signal('/AlarmFlags/IDM2', c.return_in_list, c.read_bool(register=1005, bit=11)),
Signal('/AlarmFlags/ISOB', c.return_in_list, c.read_bool(register=1005, bit=12)),
Signal('/AlarmFlags/MSWE', c.return_in_list, c.read_bool(register=1005, bit=13)),
Signal('/AlarmFlags/FUSE', c.return_in_list, c.read_bool(register=1005, bit=14)),
Signal('/AlarmFlags/HTRE', c.return_in_list, c.read_bool(register=1005, bit=15)),
Signal('/AlarmFlags/TCPE', c.return_in_list, c.read_bool(register=1005, bit=16)),
Signal('/AlarmFlags/STRE', c.return_in_list, c.read_bool(register=1005, bit=17)),
Signal('/AlarmFlags/CME', c.return_in_list, c.read_bool(register=1005, bit=18)),
Signal('/AlarmFlags/HWFL', c.return_in_list, c.read_bool(register=1005, bit=19)),
Signal('/AlarmFlags/HWEM', c.return_in_list, c.read_bool(register=1005, bit=20)),
Signal('/AlarmFlags/ThM', c.return_in_list, c.read_bool(register=1005, bit=21)),
Signal('/AlarmFlags/vsm2', c.return_in_list, c.read_bool(register=1005, bit=23)),
Signal('/AlarmFlags/vsM2', c.return_in_list, c.read_bool(register=1005, bit=25)),
Signal('/AlarmFlags/iCM2', c.return_in_list, c.read_bool(register=1005, bit=27)),
Signal('/AlarmFlags/iDM2', c.return_in_list, c.read_bool(register=1005, bit=29)),
Signal('/AlarmFlags/MID2', c.return_in_list, c.read_bool(register=1005, bit=31)),
#Signal('/AlarmFlags/TcBM', c.return_in_list, c.read_bool(register=1005, bit=36)),
#Signal('/AlarmFlags/BRNF', c.return_in_list, c.read_bool(register=1005, bit=37)),
Signal('/AlarmFlags/HTFS', c.return_in_list, c.read_bool(register=1005, bit=42)),
Signal('/AlarmFlags/DATA', c.return_in_list, c.read_bool(register=1005, bit=43)),
Signal('/AlarmFlags/LMPA', c.return_in_list, c.read_bool(register=1005, bit=45)),
Signal('/AlarmFlags/HEBT', c.return_in_list, c.read_bool(register=1005, bit=46)),
Signal('/AlarmFlags/CURM', c.return_in_list, c.read_bool(register=1005, bit=48)),
# LedStatus
Signal('/Diagnostics/LedStatus/Red', c.first, c.read_led_state(register=1004, led=LedColor.red)),
Signal('/Diagnostics/LedStatus/Blue', c.first, c.read_led_state(register=1004, led=LedColor.blue)),
Signal('/Diagnostics/LedStatus/Green', c.first, c.read_led_state(register=1004, led=LedColor.green)),
Signal('/Diagnostics/LedStatus/Amber', c.first, c.read_led_state(register=1004, led=LedColor.amber)),
# IO Status
Signal('/Diagnostics/IoStatus/MainSwitchClosed', c.return_in_list, c.read_bool(register=1013, bit=0)),
Signal('/Diagnostics/IoStatus/AlarmOutActive', c.return_in_list, c.read_bool(register=1013, bit=1)),
Signal('/Diagnostics/IoStatus/InternalFanActive', c.return_in_list, c.read_bool(register=1013, bit=2)),
Signal('/Diagnostics/IoStatus/VoltMeasurementAllowed', c.return_in_list, c.read_bool(register=1013, bit=3)),
Signal('/Diagnostics/IoStatus/AuxRelay', c.return_in_list, c.read_bool(register=1013, bit=4)),
Signal('/Diagnostics/IoStatus/RemoteState', c.return_in_list, c.read_bool(register=1013, bit=5)),
Signal('/Diagnostics/IoStatus/RiscOn', c.return_in_list, c.read_bool(register=1013, bit=6)),
]
def init_modbus(tty):
# type: (str) -> Modbus
logging.debug('initializing Modbus')
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)
def init_dbus(tty, signals):
# type: (str, Iterable[Signal]) -> DBus
logging.debug('initializing DBus service')
dbus = DBus(servicename=cfg.SERVICE_NAME_PREFIX + tty)
logging.debug('initializing DBus paths')
for signal in signals:
init_dbus_path(dbus, signal)
return dbus
# noinspection PyBroadException
def try_get_value(sig):
# type: (Signal) -> object
try:
return sig.get_value(None)
except:
return None
def init_dbus_path(dbus, sig):
# type: (DBus, Signal) -> ()
dbus.add_path(
sig.dbus_path,
try_get_value(sig),
gettextcallback=lambda _, v: sig.get_text(v))
def init_main_loop():
# type: () -> DBusGMainLoop
logging.debug('initializing DBusGMainLoop Loop')
DBusGMainLoop(set_as_default=True)
return GLib.MainLoop()
def report_slave_id(modbus, slave_address):
# type: (Modbus, int) -> str
slave = str(slave_address)
logging.debug('requesting slave id from node ' + slave)
try:
modbus.connect()
request = ReportSlaveIdRequest(unit=slave_address)
response = modbus.execute(request)
if response is ExceptionResponse or issubclass(type(response), ModbusException):
raise Exception('failed to get slave id from ' + slave + ' : ' + str(response))
return response.identifier
finally:
modbus.close()
def identify_battery(modbus, slave_address):
# type: (Modbus, int) -> Battery
logging.info('identifying battery...')
hardware_version, bms_version, ampere_hours = parse_slave_id(modbus, slave_address)
firmware_version = read_firmware_version(modbus, slave_address)
specs = Battery(
slave_address=slave_address,
hardware_version=hardware_version,
firmware_version=firmware_version,
bms_version=bms_version,
ampere_hours=ampere_hours)
logging.info('battery identified:\n{0}'.format(str(specs)))
return specs
def identify_batteries(modbus):
# type: (Modbus) -> list[Battery]
def _identify_batteries():
address_range = range(1, cfg.MAX_SLAVE_ADDRESS + 1)
for slave_address in address_range:
try:
yield identify_battery(modbus, slave_address)
except Exception as e:
logging.info('failed to identify battery at {0} : {1}'.format(str(slave_address), str(e)))
return list(_identify_batteries()) # force that lazy iterable!
def parse_slave_id(modbus, slave_address):
# type: (Modbus, int) -> (str, str, int)
slave_id = report_slave_id(modbus, slave_address)
sid = re.sub(b'[^\x20-\x7E]', b'', slave_id) # remove weird special chars
match = re.match('(?P<hw>48TL(?P<ah>\d+)) *(?P<bms>.*)', sid.decode('ascii'))
if match is None:
raise Exception('no known battery found')
return match.group('hw'), match.group('bms'), int(match.group('ah'))
def read_firmware_version(modbus, slave_address):
# type: (Modbus, int) -> str
logging.debug('reading firmware version')
try:
modbus.connect()
response = read_modbus_registers(modbus, slave_address, base_address=1054, count=1)
register = response.registers[0]
return '{0:0>4X}'.format(register)
finally:
modbus.close() # close in any case
def read_modbus_registers(modbus, slave_address, base_address=cfg.BASE_ADDRESS, count=cfg.NO_OF_REGISTERS):
# type: (Modbus, int) -> ReadInputRegistersResponse
logging.debug('requesting modbus registers {0}-{1}'.format(base_address, base_address + count))
return modbus.read_input_registers(
address=base_address,
count=count,
unit=slave_address)
def read_battery_status(modbus, battery):
# type: (Modbus, Battery) -> BatteryStatus
"""
Read the modbus registers containing the battery's status info.
"""
logging.debug('reading battery status')
try:
modbus.connect()
data = read_modbus_registers(modbus, battery.slave_address)
return BatteryStatus(battery, data.registers)
finally:
modbus.close() # close in any case
def publish_values(dbus, signals, statuses):
# type: (DBus, Iterable[Signal], Iterable[BatteryStatus]) -> ()
for s in signals:
values = [s.get_value(status) for status in statuses]
with dbus as srv:
srv[s.dbus_path] = s.aggregate(values)
def update(modbus, batteries, dbus, signals, csv_signals):
# type: (Modbus, Iterable[Battery], DBus, Iterable[Signal]) -> bool
"""
Main update function
1. requests status record each battery via modbus,
2. parses the data using Signal.get_value
3. aggregates the data from all batteries into one datum using Signal.aggregate
4. publishes the data on the dbus
"""
logging.debug('starting update cycle')
statuses = [read_battery_status(modbus, battery) for battery in batteries]
node_numbers = [battery.slave_address for battery in batteries]
publish_values(dbus, signals, statuses)
create_csv_files(csv_signals, statuses, node_numbers)
logging.debug('finished update cycle\n')
return True
def print_usage():
print ('Usage: ' + __file__ + ' <serial device>')
print ('Example: ' + __file__ + ' ttyUSB0')
def parse_cmdline_args(argv):
# type: (list[str]) -> str
if len(argv) == 0:
logging.info('missing command line argument for tty device')
print_usage()
sys.exit(1)
return argv[0]
alive = True # global alive flag, watchdog_task clears it, update_task sets it
def create_update_task(modbus, dbus, batteries, signals, csv_signals, main_loop):
# type: (Modbus, DBus, Iterable[Battery], Iterable[Signal], DBusGMainLoop) -> Callable[[],bool]
"""
Creates an update task which runs the main update function
and resets the alive flag
"""
def update_task():
# type: () -> bool
global alive
alive = update(modbus, batteries, dbus, signals, csv_signals)
if not alive:
logging.info('update_task: quitting main loop because of error')
main_loop.quit()
return alive
return update_task
def create_watchdog_task(main_loop):
# 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
return watchdog_task
def get_installation_name(file_path):
with open(file_path, 'r') as file:
return file.read().strip()
def manage_csv_files(directory_path, max_files=20):
csv_files = [f for f in os.listdir(directory_path)]
csv_files.sort(key=lambda x: os.path.getctime(os.path.join(directory_path, x)))
# Remove oldest files if exceeds maximum
while len(csv_files) > max_files:
file_to_delete = os.path.join(directory_path, csv_files.pop(0))
os.remove(file_to_delete)
def serialize_for_csv(value):
if isinstance(value, (dict, list, tuple)):
return json.dumps(value, ensure_ascii=False)
return str(value)
def insert_id(path, id_number):
parts = path.split("/")
insert_position = parts.index("Devices") + 1
parts.insert(insert_position, str(id_number))
return "/".join(parts)
def create_csv_files(signals, statuses, node_numbers):
timestamp = int(time.time())
if timestamp % 2 != 0:
timestamp -= 1
# Create CSV directory if it doesn't exist
if not os.path.exists(CSV_DIR):
os.makedirs(CSV_DIR)
#installation_name = get_installation_name(INSTALLATION_NAME_FILE)
csv_filename = f"{timestamp}.csv"
csv_path = os.path.join(CSV_DIR, csv_filename)
# Append values to the CSV file
with open(csv_path, 'a', newline='') as csvfile:
csv_writer = csv.writer(csvfile, delimiter=';')
# Add a special row for the nodes configuration
nodes_config_path = "/Config/Devices/BatteryNodes"
nodes_list = ",".join(str(node) for node in node_numbers)
config_row = [nodes_config_path, nodes_list, ""]
csv_writer.writerow(config_row)
# Iterate over each node and signal to create rows in the new format
for i, node in enumerate(node_numbers):
for s in signals:
signal_name = insert_id(s.name, i+1)
#value = serialize_for_csv(s.get_value(statuses[i]))
value = s.get_value(statuses[i])
row_values = [signal_name, value, s.get_text]
csv_writer.writerow(row_values)
# Manage CSV files, keep a limited number of files
# Create the CSV as a string
csv_data = read_csv_as_string(csv_path)
# Create an S3config instance
s3_config = S3config()
response = s3_config.create_put_request(csv_filename, csv_data)
if response.status_code == 200:
os.remove(csv_path)
print("Success")
else:
failed_dir = os.path.join(CSV_DIR, "failed")
if not os.path.exists(failed_dir):
os.makedirs(failed_dir)
failed_path = os.path.join(failed_dir, csv_filename)
os.rename(csv_path, failed_path)
print("Uploading failed")
manage_csv_files(failed_dir, 10)
manage_csv_files(CSV_DIR)
def main(argv):
# type: (list[str]) -> ()
logging.basicConfig(level=cfg.LOG_LEVEL)
logging.info('starting ' + __file__)
tty = parse_cmdline_args(argv)
modbus = init_modbus(tty)
batteries = identify_batteries(modbus)
n = len(batteries)
logging.info('found ' + str(n) + (' battery' if n == 1 else ' batteries'))
if n <= 0:
sys.exit(2)
bat = c.first(batteries) # report hw and fw version of first battery found
signals = init_signals(bat.hardware_version, bat.firmware_version, n)
csv_signals = create_csv_signals(bat.firmware_version)
main_loop = init_main_loop() # must run before init_dbus because gobject does some global magic
dbus = init_dbus(tty, signals)
update_task = create_update_task(modbus, dbus, batteries, signals, csv_signals, main_loop)
watchdog_task = create_watchdog_task(main_loop)
GLib.timeout_add(cfg.UPDATE_INTERVAL * 2, watchdog_task) # add watchdog first
GLib.timeout_add(cfg.UPDATE_INTERVAL, update_task) # call update once every update_interval
logging.info('starting GLib.MainLoop')
main_loop.run()
logging.info('GLib.MainLoop was shut down')
sys.exit(0xFF) # reaches this only on error
if __name__ == "__main__":
main(sys.argv[1:])

View File

@ -0,0 +1,276 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from traceback import print_exc
from os import _exit as os_exit
from os import statvfs
from subprocess import check_output, CalledProcessError
import logging
import dbus
logger = logging.getLogger(__name__)
VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1)
class NoVrmPortalIdError(Exception):
pass
# Use this function to make sure the code quits on an unexpected exception. Make sure to use it
# when using GLib.idle_add and also GLib.timeout_add.
# Without this, the code will just keep running, since GLib does not stop the mainloop on an
# exception.
# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2)
def exit_on_error(func, *args, **kwargs):
try:
return func(*args, **kwargs)
except:
try:
print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit')
print_exc()
except:
pass
# sys.exit() is not used, since that throws an exception, which does not lead to a program
# halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230.
os_exit(1)
__vrm_portal_id = None
def get_vrm_portal_id():
# The original definition of the VRM Portal ID is that it is the mac
# address of the onboard- ethernet port (eth0), stripped from its colons
# (:) and lower case. This may however differ between platforms. On Venus
# the task is therefore deferred to /sbin/get-unique-id so that a
# platform specific method can be easily defined.
#
# If /sbin/get-unique-id does not exist, then use the ethernet address
# of eth0. This also handles the case where velib_python is used as a
# package install on a Raspberry Pi.
#
# On a Linux host where the network interface may not be eth0, you can set
# the VRM_IFACE environment variable to the correct name.
global __vrm_portal_id
if __vrm_portal_id:
return __vrm_portal_id
portal_id = None
# First try the method that works if we don't have a data partition. This
# will fail when the current user is not root.
try:
portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip()
if not portal_id:
raise NoVrmPortalIdError("get-unique-id returned blank")
__vrm_portal_id = portal_id
return portal_id
except CalledProcessError:
# get-unique-id returned non-zero
raise NoVrmPortalIdError("get-unique-id returned non-zero")
except OSError:
# File doesn't exist, use fallback
pass
# Fall back to getting our id using a syscall. Assume we are on linux.
# Allow the user to override what interface is used using an environment
# variable.
import fcntl, socket, struct, os
iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii')
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15]))
except IOError:
raise NoVrmPortalIdError("ioctl failed for eth0")
__vrm_portal_id = info[18:24].hex()
return __vrm_portal_id
# See VE.Can registers - public.docx for definition of this conversion
def convert_vreg_version_to_readable(version):
def str_to_arr(x, length):
a = []
for i in range(0, len(x), length):
a.append(x[i:i+length])
return a
x = "%x" % version
x = x.upper()
if len(x) == 5 or len(x) == 3 or len(x) == 1:
x = '0' + x
a = str_to_arr(x, 2);
# remove the first 00 if there are three bytes and it is 00
if len(a) == 3 and a[0] == '00':
a.remove(0);
# if we have two or three bytes now, and the first character is a 0, remove it
if len(a) >= 2 and a[0][0:1] == '0':
a[0] = a[0][1];
result = ''
for item in a:
result += ('.' if result != '' else '') + item
result = 'v' + result
return result
def get_free_space(path):
result = -1
try:
s = statvfs(path)
result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users
except Exception as ex:
logger.info("Error while retrieving free space for path %s: %s" % (path, ex))
return result
def _get_sysfs_machine_name():
try:
with open('/sys/firmware/devicetree/base/model', 'r') as f:
return f.read().rstrip('\x00')
except IOError:
pass
return None
# Returns None if it cannot find a machine name. Otherwise returns the string
# containing the name
def get_machine_name():
# First try calling the venus utility script
try:
return check_output("/usr/bin/product-name").strip().decode('UTF-8')
except (CalledProcessError, OSError):
pass
# Fall back to sysfs
name = _get_sysfs_machine_name()
if name is not None:
return name
# Fall back to venus build machine name
try:
with open('/etc/venus/machine', 'r', encoding='UTF-8') as f:
return f.read().strip()
except IOError:
pass
return None
def get_product_id():
""" Find the machine ID and return it. """
# First try calling the venus utility script
try:
return check_output("/usr/bin/product-id").strip().decode('UTF-8')
except (CalledProcessError, OSError):
pass
# Fall back machine name mechanism
name = _get_sysfs_machine_name()
return {
'Color Control GX': 'C001',
'Venus GX': 'C002',
'Octo GX': 'C006',
'EasySolar-II': 'C007',
'MultiPlus-II': 'C008',
'Maxi GX': 'C009',
'Cerbo GX': 'C00A'
}.get(name, 'C003') # C003 is Generic
# Returns False if it cannot open the file. Otherwise returns its rstripped contents
def read_file(path):
content = False
try:
with open(path, 'r') as f:
content = f.read().rstrip()
except Exception as ex:
logger.debug("Error while reading %s: %s" % (path, ex))
return content
def wrap_dbus_value(value):
if value is None:
return VEDBUS_INVALID
if isinstance(value, float):
return dbus.Double(value, variant_level=1)
if isinstance(value, bool):
return dbus.Boolean(value, variant_level=1)
if isinstance(value, int):
try:
return dbus.Int32(value, variant_level=1)
except OverflowError:
return dbus.Int64(value, variant_level=1)
if isinstance(value, str):
return dbus.String(value, variant_level=1)
if isinstance(value, list):
if len(value) == 0:
# If the list is empty we cannot infer the type of the contents. So assume unsigned integer.
# A (signed) integer is dangerous, because an empty list of signed integers is used to encode
# an invalid value.
return dbus.Array([], signature=dbus.Signature('u'), variant_level=1)
return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1)
if isinstance(value, dict):
# Wrapping the keys of the dictionary causes D-Bus errors like:
# 'arguments to dbus_message_iter_open_container() were incorrect,
# assertion "(type == DBUS_TYPE_ARRAY && contained_signature &&
# *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL ||
# _dbus_check_is_valid_signature (contained_signature))" failed in file ...'
return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1)
return value
dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64)
def unwrap_dbus_value(val):
"""Converts D-Bus values back to the original type. For example if val is of type DBus.Double,
a float will be returned."""
if isinstance(val, dbus_int_types):
return int(val)
if isinstance(val, dbus.Double):
return float(val)
if isinstance(val, dbus.Array):
v = [unwrap_dbus_value(x) for x in val]
return None if len(v) == 0 else v
if isinstance(val, (dbus.Signature, dbus.String)):
return str(val)
# Python has no byte type, so we convert to an integer.
if isinstance(val, dbus.Byte):
return int(val)
if isinstance(val, dbus.ByteArray):
return "".join([bytes(x) for x in val])
if isinstance(val, (list, tuple)):
return [unwrap_dbus_value(x) for x in val]
if isinstance(val, (dbus.Dictionary, dict)):
# Do not unwrap the keys, see comment in wrap_dbus_value
return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()])
if isinstance(val, dbus.Boolean):
return bool(val)
return val
# When supported, only name owner changes for the the given namespace are reported. This
# prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily.
def add_name_owner_changed_receiver(dbus, name_owner_changed, namespace="com.victronenergy"):
# support for arg0namespace is submitted upstream, but not included at the time of
# writing, Venus OS does support it, so try if it works.
if namespace is None:
dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')
else:
try:
dbus.add_signal_receiver(name_owner_changed,
signal_name='NameOwnerChanged', arg0namespace=namespace)
except TypeError:
dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')

View File

@ -0,0 +1,614 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import dbus.service
import logging
import traceback
import os
import weakref
from collections import defaultdict
from ve_utils import wrap_dbus_value, unwrap_dbus_value
# vedbus contains three classes:
# VeDbusItemImport -> use this to read data from the dbus, ie import
# VeDbusItemExport -> use this to export data to the dbus (one value)
# VeDbusService -> use that to create a service and export several values to the dbus
# Code for VeDbusItemImport is copied from busitem.py and thereafter modified.
# All projects that used busitem.py need to migrate to this package. And some
# projects used to define there own equivalent of VeDbusItemExport. Better to
# use VeDbusItemExport, or even better the VeDbusService class that does it all for you.
# TODOS
# 1 check for datatypes, it works now, but not sure if all is compliant with
# com.victronenergy.BusItem interface definition. See also the files in
# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps
# something similar should also be done in VeDbusBusItemExport?
# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object?
# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking
# changes possible. Does everybody first invalidate its data before leaving the bus?
# And what about before taking one object away from the bus, instead of taking the
# whole service offline?
# They should! And after taking one value away, do we need to know that someone left
# the bus? Or we just keep that value in invalidated for ever? Result is that we can't
# see the difference anymore between an invalidated value and a value that was first on
# the bus and later not anymore. See comments above VeDbusItemImport as well.
# 9 there are probably more todos in the code below.
# Some thoughts with regards to the data types:
#
# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types
# ---
# Variants are represented by setting the variant_level keyword argument in the
# constructor of any D-Bus data type to a value greater than 0 (variant_level 1
# means a variant containing some other data type, variant_level 2 means a variant
# containing a variant containing some other data type, and so on). If a non-variant
# is passed as an argument but introspection indicates that a variant is expected,
# it'll automatically be wrapped in a variant.
# ---
#
# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass
# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera
#
# So all together that explains why we don't need to explicitly convert back and forth
# between the dbus datatypes and the standard python datatypes. Note that all datatypes
# in python are objects. Even an int is an object.
# The signature of a variant is 'v'.
# Export ourselves as a D-Bus service.
class VeDbusService(object):
def __init__(self, servicename, bus=None):
# dict containing the VeDbusItemExport objects, with their path as the key.
self._dbusobjects = {}
self._dbusnodes = {}
self._ratelimiters = []
self._dbusname = None
# dict containing the onchange callbacks, for each object. Object path is the key
self._onchangecallbacks = {}
# Connect to session bus whenever present, else use the system bus
self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus())
# make the dbus connection available to outside, could make this a true property instead, but ach..
self.dbusconn = self._dbusconn
# Register ourselves on the dbus, trigger an error if already in use (do_not_queue)
self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True)
# Add the root item that will return all items as a tree
self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self)
logging.info("registered ourselves on D-Bus as %s" % servicename)
# To force immediate deregistering of this dbus service and all its object paths, explicitly
# call __del__().
def __del__(self):
for node in list(self._dbusnodes.values()):
node.__del__()
self._dbusnodes.clear()
for item in list(self._dbusobjects.values()):
item.__del__()
self._dbusobjects.clear()
if self._dbusname:
self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code
self._dbusname = None
def get_name(self):
return self._dbusname.get_name()
# @param callbackonchange function that will be called when this value is changed. First parameter will
# be the path of the object, second the new value. This callback should return
# True to accept the change, False to reject it.
def add_path(self, path, value, description="", writeable=False,
onchangecallback=None, gettextcallback=None, valuetype=None, itemtype=None):
if onchangecallback is not None:
self._onchangecallbacks[path] = onchangecallback
itemtype = itemtype or VeDbusItemExport
item = itemtype(self._dbusconn, path, value, description, writeable,
self._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype)
spl = path.split('/')
for i in range(2, len(spl)):
subPath = '/'.join(spl[:i])
if subPath not in self._dbusnodes and subPath not in self._dbusobjects:
self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self)
self._dbusobjects[path] = item
logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable))
# Add the mandatory paths, as per victron dbus api doc
def add_mandatory_paths(self, processname, processversion, connection,
deviceinstance, productid, productname, firmwareversion, hardwareversion, connected):
self.add_path('/Mgmt/ProcessName', processname)
self.add_path('/Mgmt/ProcessVersion', processversion)
self.add_path('/Mgmt/Connection', connection)
# Create rest of the mandatory objects
self.add_path('/DeviceInstance', deviceinstance)
self.add_path('/ProductId', productid)
self.add_path('/ProductName', productname)
self.add_path('/FirmwareVersion', firmwareversion)
self.add_path('/HardwareVersion', hardwareversion)
self.add_path('/Connected', connected)
# Callback function that is called from the VeDbusItemExport objects when a value changes. This function
# maps the change-request to the onchangecallback given to us for this specific path.
def _value_changed(self, path, newvalue):
if path not in self._onchangecallbacks:
return True
return self._onchangecallbacks[path](path, newvalue)
def _item_deleted(self, path):
self._dbusobjects.pop(path)
for np in list(self._dbusnodes.keys()):
if np != '/':
for ip in self._dbusobjects:
if ip.startswith(np + '/'):
break
else:
self._dbusnodes[np].__del__()
self._dbusnodes.pop(np)
def __getitem__(self, path):
return self._dbusobjects[path].local_get_value()
def __setitem__(self, path, newvalue):
self._dbusobjects[path].local_set_value(newvalue)
def __delitem__(self, path):
self._dbusobjects[path].__del__() # Invalidates and then removes the object path
assert path not in self._dbusobjects
def __contains__(self, path):
return path in self._dbusobjects
def __enter__(self):
l = ServiceContext(self)
self._ratelimiters.append(l)
return l
def __exit__(self, *exc):
# pop off the top one and flush it. If with statements are nested
# then each exit flushes its own part.
if self._ratelimiters:
self._ratelimiters.pop().flush()
class ServiceContext(object):
def __init__(self, parent):
self.parent = parent
self.changes = {}
def __getitem__(self, path):
return self.parent[path]
def __setitem__(self, path, newvalue):
c = self.parent._dbusobjects[path]._local_set_value(newvalue)
if c is not None:
self.changes[path] = c
def flush(self):
if self.changes:
self.parent._dbusnodes['/'].ItemsChanged(self.changes)
class TrackerDict(defaultdict):
""" Same as defaultdict, but passes the key to default_factory. """
def __missing__(self, key):
self[key] = x = self.default_factory(key)
return x
class VeDbusRootTracker(object):
""" This tracks the root of a dbus path and listens for PropertiesChanged
signals. When a signal arrives, parse it and unpack the key/value changes
into traditional events, then pass it to the original eventCallback
method. """
def __init__(self, bus, serviceName):
self.importers = defaultdict(weakref.WeakSet)
self.serviceName = serviceName
self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal(
"ItemsChanged", weak_functor(self._items_changed_handler))
def __del__(self):
self._match.remove()
self._match = None
def add(self, i):
self.importers[i.path].add(i)
def _items_changed_handler(self, items):
if not isinstance(items, dict):
return
for path, changes in items.items():
try:
v = changes['Value']
except KeyError:
continue
try:
t = changes['Text']
except KeyError:
t = str(unwrap_dbus_value(v))
for i in self.importers.get(path, ()):
i._properties_changed_handler({'Value': v, 'Text': t})
"""
Importing basics:
- If when we power up, the D-Bus service does not exist, or it does exist and the path does not
yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its
initial value, which VeDbusItemImport will receive and use to update local cache. And, when set,
call the eventCallback.
- If when we power up, save it
- When using get_value, know that there is no difference between services (or object paths) that don't
exist and paths that are invalid (= empty array, see above). Both will return None. In case you do
really want to know ifa path exists or not, use the exists property.
- When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals
with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged-
signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this
class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this
class.
Read when using this class:
Note that when a service leaves that D-Bus without invalidating all its exported objects first, for
example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport,
make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor,
because that takes care of all of that for you.
"""
class VeDbusItemImport(object):
def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True):
instance = object.__new__(cls)
# If signal tracking should be done, also add to root tracker
if createsignal:
if "_roots" not in cls.__dict__:
cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k))
return instance
## Constructor
# @param bus the bus-object (SESSION or SYSTEM).
# @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1'
# @param path the object-path, for example '/Dc/V'
# @param eventCallback function that you want to be called on a value change
# @param createSignal only set this to False if you use this function to one time read a value. When
# leaving it to True, make sure to also subscribe to the NameOwnerChanged signal
# elsewhere. See also note some 15 lines up.
def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True):
# TODO: is it necessary to store _serviceName and _path? Isn't it
# stored in the bus_getobjectsomewhere?
self._serviceName = serviceName
self._path = path
self._match = None
# TODO: _proxy is being used in settingsdevice.py, make a getter for that
self._proxy = bus.get_object(serviceName, path, introspect=False)
self.eventCallback = eventCallback
assert eventCallback is None or createsignal == True
if createsignal:
self._match = self._proxy.connect_to_signal(
"PropertiesChanged", weak_functor(self._properties_changed_handler))
self._roots[serviceName].add(self)
# store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to
# None, same as when a value is invalid
self._cachedvalue = None
try:
v = self._proxy.GetValue()
except dbus.exceptions.DBusException:
pass
else:
self._cachedvalue = unwrap_dbus_value(v)
def __del__(self):
if self._match is not None:
self._match.remove()
self._match = None
self._proxy = None
def _refreshcachedvalue(self):
self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue())
## Returns the path as a string, for example '/AC/L1/V'
@property
def path(self):
return self._path
## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1
@property
def serviceName(self):
return self._serviceName
## Returns the value of the dbus-item.
# the type will be a dbus variant, for example dbus.Int32(0, variant_level=1)
# this is not a property to keep the name consistant with the com.victronenergy.busitem interface
# returns None when the property is invalid
def get_value(self):
return self._cachedvalue
## Writes a new value to the dbus-item
def set_value(self, newvalue):
r = self._proxy.SetValue(wrap_dbus_value(newvalue))
# instead of just saving the value, go to the dbus and get it. So we have the right type etc.
if r == 0:
self._refreshcachedvalue()
return r
## Resets the item to its default value
def set_default(self):
self._proxy.SetDefault()
self._refreshcachedvalue()
## Returns the text representation of the value.
# For example when the value is an enum/int GetText might return the string
# belonging to that enum value. Another example, for a voltage, GetValue
# would return a float, 12.0Volt, and GetText could return 12 VDC.
#
# Note that this depends on how the dbus-producer has implemented this.
def get_text(self):
return self._proxy.GetText()
## Returns true of object path exists, and false if it doesn't
@property
def exists(self):
# TODO: do some real check instead of this crazy thing.
r = False
try:
r = self._proxy.GetValue()
r = True
except dbus.exceptions.DBusException:
pass
return r
## callback for the trigger-event.
# @param eventCallback the event-callback-function.
@property
def eventCallback(self):
return self._eventCallback
@eventCallback.setter
def eventCallback(self, eventCallback):
self._eventCallback = eventCallback
## Is called when the value of the imported bus-item changes.
# Stores the new value in our local cache, and calls the eventCallback, if set.
def _properties_changed_handler(self, changes):
if "Value" in changes:
changes['Value'] = unwrap_dbus_value(changes['Value'])
self._cachedvalue = changes['Value']
if self._eventCallback:
# The reason behind this try/except is to prevent errors silently ending up the an error
# handler in the dbus code.
try:
self._eventCallback(self._serviceName, self._path, changes)
except:
traceback.print_exc()
os._exit(1) # sys.exit() is not used, since that also throws an exception
class VeDbusTreeExport(dbus.service.Object):
def __init__(self, bus, objectPath, service):
dbus.service.Object.__init__(self, bus, objectPath)
self._service = service
logging.debug("VeDbusTreeExport %s has been created" % objectPath)
def __del__(self):
# self._get_path() will raise an exception when retrieved after the call to .remove_from_connection,
# so we need a copy.
path = self._get_path()
if path is None:
return
self.remove_from_connection()
logging.debug("VeDbusTreeExport %s has been removed" % path)
def _get_path(self):
if len(self._locations) == 0:
return None
return self._locations[0][1]
def _get_value_handler(self, path, get_text=False):
logging.debug("_get_value_handler called for %s" % path)
r = {}
px = path
if not px.endswith('/'):
px += '/'
for p, item in self._service._dbusobjects.items():
if p.startswith(px):
v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value())
r[p[len(px):]] = v
logging.debug(r)
return r
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
def GetValue(self):
value = self._get_value_handler(self._get_path())
return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1)
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
def GetText(self):
return self._get_value_handler(self._get_path(), True)
def local_get_value(self):
return self._get_value_handler(self.path)
class VeDbusRootExport(VeDbusTreeExport):
@dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}')
def ItemsChanged(self, changes):
pass
@dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}')
def GetItems(self):
return {
path: {
'Value': wrap_dbus_value(item.local_get_value()),
'Text': item.GetText() }
for path, item in self._service._dbusobjects.items()
}
class VeDbusItemExport(dbus.service.Object):
## Constructor of VeDbusItemExport
#
# Use this object to export (publish), values on the dbus
# Creates the dbus-object under the given dbus-service-name.
# @param bus The dbus object.
# @param objectPath The dbus-object-path.
# @param value Value to initialize ourselves with, defaults to None which means Invalid
# @param description String containing a description. Can be called over the dbus with GetDescription()
# @param writeable what would this do!? :).
# @param callback Function that will be called when someone else changes the value of this VeBusItem
# over the dbus. First parameter passed to callback will be our path, second the new
# value. This callback should return True to accept the change, False to reject it.
def __init__(self, bus, objectPath, value=None, description=None, writeable=False,
onchangecallback=None, gettextcallback=None, deletecallback=None,
valuetype=None):
dbus.service.Object.__init__(self, bus, objectPath)
self._onchangecallback = onchangecallback
self._gettextcallback = gettextcallback
self._value = value
self._description = description
self._writeable = writeable
self._deletecallback = deletecallback
self._type = valuetype
# To force immediate deregistering of this dbus object, explicitly call __del__().
def __del__(self):
# self._get_path() will raise an exception when retrieved after the
# call to .remove_from_connection, so we need a copy.
path = self._get_path()
if path == None:
return
if self._deletecallback is not None:
self._deletecallback(path)
self.remove_from_connection()
logging.debug("VeDbusItemExport %s has been removed" % path)
def _get_path(self):
if len(self._locations) == 0:
return None
return self._locations[0][1]
## Sets the value. And in case the value is different from what it was, a signal
# will be emitted to the dbus. This function is to be used in the python code that
# is using this class to export values to the dbus.
# set value to None to indicate that it is Invalid
def local_set_value(self, newvalue):
changes = self._local_set_value(newvalue)
if changes is not None:
self.PropertiesChanged(changes)
def _local_set_value(self, newvalue):
if self._value == newvalue:
return None
self._value = newvalue
return {
'Value': wrap_dbus_value(newvalue),
'Text': self.GetText()
}
def local_get_value(self):
return self._value
# ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ====
## Dbus exported method SetValue
# Function is called over the D-Bus by other process. It will first check (via callback) if new
# value is accepted. And it is, stores it and emits a changed-signal.
# @param value The new value.
# @return completion-code When successful a 0 is return, and when not a -1 is returned.
@dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i')
def SetValue(self, newvalue):
if not self._writeable:
return 1 # NOT OK
newvalue = unwrap_dbus_value(newvalue)
# If value type is enforced, cast it. If the type can be coerced
# python will do it for us. This allows ints to become floats,
# or bools to become ints. Additionally also allow None, so that
# a path may be invalidated.
if self._type is not None and newvalue is not None:
try:
newvalue = self._type(newvalue)
except (ValueError, TypeError):
return 1 # NOT OK
if newvalue == self._value:
return 0 # OK
# call the callback given to us, and check if new value is OK.
if (self._onchangecallback is None or
(self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))):
self.local_set_value(newvalue)
return 0 # OK
return 2 # NOT OK
## Dbus exported method GetDescription
#
# Returns the a description.
# @param language A language code (e.g. ISO 639-1 en-US).
# @param length Lenght of the language string.
# @return description
@dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s')
def GetDescription(self, language, length):
return self._description if self._description is not None else 'No description given'
## Dbus exported method GetValue
# Returns the value.
# @return the value when valid, and otherwise an empty array
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
def GetValue(self):
return wrap_dbus_value(self._value)
## Dbus exported method GetText
# Returns the value as string of the dbus-object-path.
# @return text A text-value. '---' when local value is invalid
@dbus.service.method('com.victronenergy.BusItem', out_signature='s')
def GetText(self):
if self._value is None:
return '---'
# Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we
# have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from
# the application itself, as all data from the D-Bus should have been unwrapped by now.
if self._gettextcallback is None and type(self._value) == dbus.Byte:
return str(int(self._value))
if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId':
return "0x%X" % self._value
if self._gettextcallback is None:
return str(self._value)
return self._gettextcallback(self.__dbus_object_path__, self._value)
## The signal that indicates that the value has changed.
# Other processes connected to this BusItem object will have subscribed to the
# event when they want to track our state.
@dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}')
def PropertiesChanged(self, changes):
pass
## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference
## to the object which method is to be called.
## Use this object to break circular references.
class weak_functor:
def __init__(self, f):
self._r = weakref.ref(f.__self__)
self._f = weakref.ref(f.__func__)
def __call__(self, *args, **kargs):
r = self._r()
f = self._f()
if r == None or f == None:
return
f(r, *args, **kargs)

View File

@ -0,0 +1,7 @@
#!/bin/bash
. /opt/victronenergy/serial-starter/run-service.sh
app=/opt/victronenergy/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py
args="$tty"
start $args

View File

@ -1,311 +0,0 @@
using CliWrap;
using HandlebarsDotNet;
using InnovEnergy.App.VrmGrabber.Database;
using InnovEnergy.Lib.Utils;
using Microsoft.AspNetCore.Mvc;
using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation;
namespace InnovEnergy.App.VrmGrabber;
public record InstallationToHtmlInterface(
String Name,
String Ip,
Int64 Vrm,
String Identifier,
String Serial,
String EscapedName,
String Online,
String LastSeen,
String NumBatteries,
String BatteryVersion,
String BatteryUpdateStatus,
String ServerIp = "10.2.0.1", //TODO MAKE ME DYNAMIC
String FirmwareVersion = "AF09", //Todo automatically grab newest version?
String NodeRedFiles = "NodeRedFiles"
);
[Controller]
public class Controller : ControllerBase
{
//Todo automatically grab newest version?
private const String FirmwareVersion = "AF09";
[HttpGet]
[Route("/")]
[Produces("text/html")]
public ActionResult Index()
{
const String source = @"<head>
<style>
tbody {
background-color: #e4f0f5;
}
tbody tr:nth-child(odd) {
background-color: #ECE9E9;
}
th, td { /* cell */
padding: 0.75rem;
font-size: 0.9375rem;
}
th { /* header cell */
font-weight: 700;
text-align: left;
color: #272838;
border-bottom: 2px solid #EB9486;
position: sticky;
top: 0;
background-color: #F9F8F8;
}
table {
border-collapse: collapse;
width: 100%;
border: 2px solid rgb(200, 200, 200);
letter-spacing: 1px;
font-family: sans-serif;
font-size: 0.8rem;
position: absolute; top: 0; bottom: 0; left: 0; right: 0;
}
thead th {
border: 1px solid rgb(190, 190, 190);
padding: 5px 10px;
position: sticky;
position: -webkit-sticky;
top: 0px;
background: white;
z-index: 999;
}
td {
text-align: left;
}
#managerTable {
overflow: hidden;
}</style></head>
<div id='managerTable'>
<table>
<tbody>
<tr>
<th>Name This site is updated once per day!</th>
<th>Gui</th>
<th>VRM</th>
<th>Grafana</th>
<th>Identifier</th>
<th>Last Seen</th>
<th>Serial</th>
<th>#Batteries</th>
<th>Firmware-Version</th>
<th>Update</th>
<th>Last Update Status</th>
<th>Upload Node Red Files</th>
</tr>
{{#inst}}
{{> installations}}
{{/inst}}
</tbody>
</table>
<div id='managerTable'>";
const String partialSource = @"<tr><td>{{Name}}</td>
<td><a target='_blank' href=http://{{Ip}}>{{online}} {{Ip}}</a></td>
<td><a target='_blank' href=https://vrm.victronenergy.com/installation/{{Vrm}}/dashboard>VRM</a></td>
<td><a target='_blank' href='https://salidomo.innovenergy.ch/d/ENkNRQXmk/installation?refresh=5s&orgId=1&var-Installation={{EscapedName}}&kiosk=tv'>Grafana</a></td>
<td>{{Identifier}}</td>
<td>{{LastSeen}}</td>
<td>{{Serial}}</td>
<td>{{NumBatteries}}</td>
<td>{{BatteryVersion}}</td>
<td><a target='_blank' href=http://{{ServerIp}}/UpdateBatteryFirmware/{{Ip}}>⬆️{{FirmwareVersion}}</a></td>
<td>{{BatteryUpdateStatus}}</td>
<td><a target='_blank' href=http://{{ServerIp}}/UploadNodeRedFiles/{{Ip}}>⬆️{{NodeRedFiles}}</a></td>
</tr>";
var installationsInDb = Db.Installations.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ToList();
if (installationsInDb.Count == 0) return new ContentResult
{
ContentType = "text/html",
Content = "<p>Please wait page is still loading</p>"
};
Handlebars.RegisterTemplate("installations", partialSource);
var template = Handlebars.Compile(source);
var installsForHtml = installationsInDb.Select(i => new InstallationToHtmlInterface(
i.Name,
i.Ip,
i.Vrm,
i.Identifier,
i.Serial,
i.EscapedName,
i.Online,
DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(i.LastSeen)).ToString(),
i.NumberOfBatteries,
i.BatteryFirmwareVersion,
i.BatteryUpdateStatus));
var data = new
{
inst = installsForHtml,
};
var result = template(data);
return new ContentResult
{
ContentType = "text/html",
Content = result
};
}
[HttpGet("UpdateBatteryFirmware/{installationIp}")]
public async Task<String> UpdateBatteryFirmware(String installationIp)
{
//We need the DeviceName of the battery (ttyUSB?)
var pathToBattery = await Db.ExecuteBufferedAsyncCommandOnIp(installationIp, "dbus-send --system --dest=com.victronenergy.system --type=method_call --print-reply /ServiceMapping/com_victronenergy_battery_1 com.victronenergy.BusItem.GetText");
var split = pathToBattery.Split('"');
var split2 = pathToBattery.Split(' ');
if (split.Length < 2 || split2.Length < 1)
{
Console.WriteLine(pathToBattery + " Split failed ");
return "Update failed";
}
if (split[1] == "Failed" || split2[0] == "Error") return "Update failed";
await SendNewBatteryFirmware(installationIp);
var batteryTtyName = split[1].Split(".").Last();
var localCommand = "echo start";
var installation = Db.Installations.First(installation => installation.Ip == installationIp);
installation.BatteryUpdateStatus = "Running";
Db.Update(installation: installation);
var batteryIdsResult = await Db.ExecuteBufferedAsyncCommandOnIp(installationIp, $"dbus-send --system --dest=com.victronenergy.battery.{batteryTtyName} --type=method_call --print-reply / com.victronenergy.BusItem.GetText | grep -E -o '_Battery/[0-9]+/' | grep -E -o '[0-9]+'| sort -u");
var batteryIds = batteryIdsResult.Split("\n").ToList();
batteryIds.Pop();
foreach (var batteryId in batteryIds)
{
localCommand = localCommand.Append(
$" && /opt/innovenergy/scripts/upload-bms-firmware {batteryTtyName} {batteryId} /opt/innovenergy/bms-firmware/{FirmwareVersion}.bin");
}
#pragma warning disable CS4014
// Console.WriteLine(localCommand);
Db.ExecuteBufferedAsyncCommandOnIp(installationIp, localCommand)
.ContinueWith(async t =>
{
Console.WriteLine(t.Result);
installation.BatteryUpdateStatus = "Complete";
// installation.BatteryFirmwareVersion = FirmwareVersion;
Db.Update(installation: installation);
var vrmInst = await FindVrmInstallationByIp(installation.Ip!);
await UpdateVrmTagsToNewFirmware(installationIp);
await Db.UpdateAlarms(vrmInst);
});
#pragma warning restore CS4014
return "Battery update is successfully initiated, it will take around 15 minutes to complete! You can close this page now.";
}
private static async Task UpdateVrmTagsToNewFirmware(String installationIp)
{
var vrmInstallation = await FindVrmInstallationByIp(installationIp);
var tags = await vrmInstallation.GetTags();
async void RemoveTag(String t) => await vrmInstallation.RemoveTags(t);
tags.Where(tag => tag.StartsWith("FM-"))
.Do(RemoveTag);
await vrmInstallation.AddTags("FM-" + FirmwareVersion);
}
private static async Task<VrmInstallation> FindVrmInstallationByIp(String installationIp)
{
var installationId = Db.Installations.Where(i => i.Ip == installationIp).Select(i => i.Vrm).First();
var vrmAccount = await Db.GetVrmAccount();
return await vrmAccount.GetInstallation(installationId!);
}
private static async Task SendNewBatteryFirmware(String installationIp)
{
await Cli.Wrap("rsync")
.WithArguments($@"-r --relative bms-firmware/{FirmwareVersion}.bin")
.AppendArgument($@"root@{installationIp}:/opt/innovenergy")
.ExecuteAsync();
}
// [HttpGet(nameof(GetInstallation))]
// [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
// public Object GetInstallation(UInt64 serialNumber)
// {
// var instList = Db.InstallationsAndDetails.Values.ToList();
// foreach (var detailList in instList.Select((value, index) => new { Value = value, Index = index}))
// {
// if (detailList.Value.All(detail => detail.Json["idSite"]?.GetValue<UInt64>() != serialNumber)) continue;
// var retour = Db.InstallationsAndDetails.Keys.ToList()[detailList.Index].Json;
// retour["details"] = JsonSerializer.Deserialize<JsonArray>(JsonSerializer.Serialize(detailList.Value.Select(d => d.Json).ToArray()));
// return retour;
// }
//
// return new NotFoundResult();
// }
// remove the original ones????????
[HttpPost("UploadNodeRedFiles/{installationIp}")]
public async Task<IActionResult> UploadNodeRedFiles(String installationIp)
{
// Define the mapping of files to remote locations
var fileLocationMappings = new Dictionary<string, string>
{
{ "flows.json", "/opt/data/nodered/.node-red/" },
{ "settings-user.js", "/opt/data/nodered/.node-red/" },
{ "rc.local", "/data/" },
{ "dbus-fzsonick-48tl", "/data/"}
};
var nodeRedFilesFolder = Path.Combine(Directory.GetCurrentDirectory(), "NodeRedFiles");
if (!Directory.Exists(nodeRedFilesFolder))
{
return BadRequest("NodeRedFiles folder does not exist.");
}
var tasks = fileLocationMappings.Select(async mapping =>
{
var fileName = mapping.Key;
var remoteLocation = mapping.Value;
var filePath = Path.Combine(nodeRedFilesFolder, fileName);
if (!System.IO.File.Exists(filePath))
{
throw new FileNotFoundException($"File {fileName} not found in {nodeRedFilesFolder}.");
}
// Execute the SCP command to upload the file
await Cli.Wrap("rsync")
.WithArguments($@"-r {filePath}")
.AppendArgument($@"root@{installationIp}:{remoteLocation}")
.ExecuteAsync();
});
try
{
await Task.WhenAll(tasks);
return Ok("All files uploaded successfully.");
}
catch (Exception ex)
{
return StatusCode(500, $"An error occurred while uploading files: {ex.Message}");
}
}
}

View File

@ -1,25 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VrmGrabber", "VrmGrabber.csproj", "{A3BDD9AD-F065-444E-9C2E-F777810E3BF9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A3BDD9AD-F065-444E-9C2E-F777810E3BF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A3BDD9AD-F065-444E-9C2E-F777810E3BF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A3BDD9AD-F065-444E-9C2E-F777810E3BF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A3BDD9AD-F065-444E-9C2E-F777810E3BF9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {274A19A1-A0B6-4EAF-BCF6-475F7C511EF3}
EndGlobalSection
EndGlobal

View File

@ -1,302 +0,0 @@
#!/usr/bin/python2 -u
# coding=utf-8
import os
import struct
from time import sleep
import serial
from os import system
from pymodbus.client.sync import ModbusSerialClient as Modbus
from pymodbus.exceptions import ModbusIOException
from pymodbus.pdu import ModbusResponse
from os.path import dirname, abspath
from sys import path, argv, exit
path.append(dirname(dirname(abspath(__file__))))
PAGE_SIZE = 0x100
HALF_PAGE = PAGE_SIZE / 2
WRITE_ENABLE = [1]
SERIAL_STARTER_DIR = '/opt/victronenergy/serial-starter/'
FIRMWARE_VERSION_REGISTER = 1054
ERASE_FLASH_REGISTER = 0x2084
RESET_REGISTER = 0x2087
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import List, NoReturn, Iterable, Optional
class LockTTY(object):
def __init__(self, tty):
# type: (str) -> None
self.tty = tty
def __enter__(self):
system(SERIAL_STARTER_DIR + 'stop-tty.sh ' + self.tty)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
system(SERIAL_STARTER_DIR + 'start-tty.sh ' + self.tty)
def calc_stm32_crc_round(crc, data):
# type: (int, int) -> int
crc = crc ^ data
for _ in range(32):
xor = (crc & 0x80000000) != 0
crc = (crc & 0x7FFFFFFF) << 1 # clear bit 31 because python ints have "infinite" bits
if xor:
crc = crc ^ 0x04C11DB7
return crc
def calc_stm32_crc(data):
# type: (Iterable[int]) -> int
crc = 0xFFFFFFFF
for dw in data:
crc = calc_stm32_crc_round(crc, dw)
return crc
def init_modbus(tty):
# type: (str) -> Modbus
return Modbus(
port='/dev/' + tty,
method='rtu',
baudrate=115200,
stopbits=1,
bytesize=8,
timeout=0.15, # seconds
parity=serial.PARITY_ODD)
def failed(response):
# type: (ModbusResponse) -> bool
return response.function_code > 0x80
def clear_flash(modbus, slave_address):
# type: (Modbus, int) -> bool
print ('erasing flash...')
write_response = modbus.write_registers(address=0x2084, values=[1], unit=slave_address)
if failed(write_response):
print('erasing flash FAILED')
return False
flash_countdown = 17
while flash_countdown > 0:
read_response = modbus.read_holding_registers(address=0x2085, count=1, unit=slave_address)
if failed(read_response):
print('erasing flash FAILED')
return False
if read_response.registers[0] != flash_countdown:
flash_countdown = read_response.registers[0]
msg = str(100 * (16 - flash_countdown) / 16) + '%'
print '\r{0} '.format(msg),
print('done!')
return True
# noinspection PyShadowingBuiltins
def bytes_to_words(bytes):
# type: (str) -> List[int]
return list(struct.unpack('>' + len(bytes)/2 * 'H', bytes))
def send_half_page_1(modbus, slave_address, data, page):
# type: (Modbus, int, str, int) -> NoReturn
first_half = [page] + bytes_to_words(data[:HALF_PAGE])
write_first_half = modbus.write_registers(0x2000, first_half, unit=slave_address)
if failed(write_first_half):
raise Exception("Failed to write page " + str(page))
def send_half_page_2(modbus, slave_address, data, page):
# type: (Modbus, int, str, int) -> NoReturn
registers = bytes_to_words(data[HALF_PAGE:]) + calc_crc(page, data) + WRITE_ENABLE
result = modbus.write_registers(0x2041, registers, unit=slave_address)
if failed(result):
raise Exception("Failed to write page " + str(page))
def get_fw_name(fw_path):
# type: (str) -> str
return fw_path.split('/')[-1].split('.')[0]
def upload_fw(modbus, slave_id, fw_path, fw_name):
# type: (Modbus, int, str, str) -> NoReturn
with open(fw_path, "rb") as f:
size = os.fstat(f.fileno()).st_size
n_pages = size / PAGE_SIZE
print 'uploading firmware ' + fw_name + ' to BMS ...'
for page in range(0, n_pages):
page_data = f.read(PAGE_SIZE)
msg = "page " + str(page + 1) + '/' + str(n_pages) + ' ' + str(100 * page / n_pages + 1) + '%'
print '\r{0} '.format(msg),
if is_page_empty(page_data):
continue
send_half_page_1(modbus, slave_id, page_data, page)
send_half_page_2(modbus, slave_id, page_data, page)
def is_page_empty(page):
# type: (str) -> bool
return page.count('\xff') == len(page)
def reset_bms(modbus, slave_id):
# type: (Modbus, int) -> bool
print ('resetting BMS...')
result = modbus.write_registers(RESET_REGISTER, [1], unit=slave_id)
# expecting a ModbusIOException (timeout)
# BMS can no longer reply because it is already reset
success = isinstance(result, ModbusIOException)
if success:
print('done')
else:
print('FAILED to reset battery!')
return success
def calc_crc(page, data):
# type: (int, str) -> List[int]
crc = calc_stm32_crc([page] + bytes_to_words(data))
crc_bytes = struct.pack('>L', crc)
return bytes_to_words(crc_bytes)
def identify_battery(modbus, slave_id):
# type: (Modbus, int) -> Optional[str]
target = 'battery #' + str(slave_id) + ' at ' + modbus.port
try:
print('contacting ' + target + ' ...')
response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, unit=slave_id)
fw = '{0:0>4X}'.format(response.registers[0])
print('found battery with firmware ' + fw)
return fw
except:
print('failed to communicate with ' + target + ' !')
return None
def print_usage():
print ('Usage: ' + __file__ + ' <serial device> <battery id> <firmware>')
print ('Example: ' + __file__ + ' ttyUSB0 2 A08C.bin')
def parse_cmdline_args(argv):
# type: (List[str]) -> (str, str, str, str)
def fail_with(msg):
print(msg)
print_usage()
exit(1)
if len(argv) < 1:
fail_with('missing argument for tty device')
if len(argv) < 2:
fail_with('missing argument for battery ID')
if len(argv) < 3:
fail_with('missing argument for firmware')
return argv[0], int(argv[1]), argv[2], get_fw_name(argv[2])
def verify_firmware(modbus, battery_id, fw_name):
# type: (Modbus, int, str) -> NoReturn
fw_verify = identify_battery(modbus, battery_id)
if fw_verify == fw_name:
print 'SUCCESS'
else:
print 'FAILED to verify uploaded firmware!'
if fw_verify is not None:
print 'expected firmware version ' + fw_name + ' but got ' + fw_verify
def wait_for_bms_reboot():
# type: () -> NoReturn
# wait 20s for the battery to reboot
print 'waiting for BMS to reboot...'
for t in range(20, 0, -1):
print '\r{0} '.format(t),
sleep(1)
print '0'
def main(argv):
# type: (List[str]) -> NoReturn
tty, battery_id, fw_path, fw_name = parse_cmdline_args(argv)
with LockTTY(tty), init_modbus(tty) as modbus:
if identify_battery(modbus, battery_id) is None:
return
clear_flash(modbus, battery_id)
upload_fw(modbus, battery_id, fw_path, fw_name)
if not reset_bms(modbus, battery_id):
return
wait_for_bms_reboot()
verify_firmware(modbus, battery_id, fw_name)
main(argv[1:])

View File

@ -1,303 +0,0 @@
#!/usr/bin/python2 -u
# coding=utf-8
import os
import struct
from time import sleep
import serial
from os import system
from pymodbus.client.sync import ModbusSerialClient as Modbus
from pymodbus.exceptions import ModbusIOException
from pymodbus.pdu import ModbusResponse
from os.path import dirname, abspath
from sys import path, argv, exit
path.append(dirname(dirname(abspath(__file__))))
PAGE_SIZE = 0x100
HALF_PAGE = PAGE_SIZE / 2
WRITE_ENABLE = [1]
SERIAL_STARTER_DIR = '/opt/victronenergy/serial-starter/'
FIRMWARE_VERSION_REGISTER = 1054
ERASE_FLASH_REGISTER = 0x2084
RESET_REGISTER = 0x2087
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
from typing import List, NoReturn, Iterable, Optional
class LockTTY(object):
def __init__(self, tty):
# type: (str) -> None
self.tty = tty
def __enter__(self):
system(SERIAL_STARTER_DIR + 'stop-tty.sh ' + self.tty)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
system(SERIAL_STARTER_DIR + 'start-tty.sh ' + self.tty)
def calc_stm32_crc_round(crc, data):
# type: (int, int) -> int
crc = crc ^ data
for _ in range(32):
xor = (crc & 0x80000000) != 0
crc = (crc & 0x7FFFFFFF) << 1 # clear bit 31 because python ints have "infinite" bits
if xor:
crc = crc ^ 0x04C11DB7
return crc
def calc_stm32_crc(data):
# type: (Iterable[int]) -> int
crc = 0xFFFFFFFF
for dw in data:
crc = calc_stm32_crc_round(crc, dw)
return crc
def init_modbus(tty):
# type: (str) -> Modbus
return Modbus(
port='/dev/' + tty,
method='rtu',
baudrate=115200,
stopbits=1,
bytesize=8,
timeout=0.15, # seconds
parity=serial.PARITY_ODD)
def failed(response):
# type: (ModbusResponse) -> bool
# Todo 'ModbusIOException' object has no attribute 'function_code'
return response.function_code > 0x80
def clear_flash(modbus, slave_address):
# type: (Modbus, int) -> bool
print ('erasing flash...')
write_response = modbus.write_registers(address=0x2084, values=[1], unit=slave_address)
if failed(write_response):
print('erasing flash FAILED')
return False
flash_countdown = 17
while flash_countdown > 0:
read_response = modbus.read_holding_registers(address=0x2085, count=1, unit=slave_address)
if failed(read_response):
print('erasing flash FAILED')
return False
if read_response.registers[0] != flash_countdown:
flash_countdown = read_response.registers[0]
msg = str(100 * (16 - flash_countdown) / 16) + '%'
print('\r{0} '.format(msg), end=' ')
print('done!')
return True
# noinspection PyShadowingBuiltins
def bytes_to_words(bytes):
# type: (str) -> List[int]
return list(struct.unpack('>' + len(bytes)/2 * 'H', bytes))
def send_half_page_1(modbus, slave_address, data, page):
# type: (Modbus, int, str, int) -> NoReturn
first_half = [page] + bytes_to_words(data[:HALF_PAGE])
write_first_half = modbus.write_registers(0x2000, first_half, unit=slave_address)
if failed(write_first_half):
raise Exception("Failed to write page " + str(page))
def send_half_page_2(modbus, slave_address, data, page):
# type: (Modbus, int, str, int) -> NoReturn
registers = bytes_to_words(data[HALF_PAGE:]) + calc_crc(page, data) + WRITE_ENABLE
result = modbus.write_registers(0x2041, registers, unit=slave_address)
if failed(result):
raise Exception("Failed to write page " + str(page))
def get_fw_name(fw_path):
# type: (str) -> str
return fw_path.split('/')[-1].split('.')[0]
def upload_fw(modbus, slave_id, fw_path, fw_name):
# type: (Modbus, int, str, str) -> NoReturn
with open(fw_path, "rb") as f:
size = os.fstat(f.fileno()).st_size
n_pages = size / PAGE_SIZE
print('uploading firmware ' + fw_name + ' to BMS ...')
for page in range(0, n_pages):
page_data = f.read(PAGE_SIZE)
msg = "page " + str(page + 1) + '/' + str(n_pages) + ' ' + str(100 * page / n_pages + 1) + '%'
print('\r{0} '.format(msg), end=' ')
if is_page_empty(page_data):
continue
send_half_page_1(modbus, slave_id, page_data, page)
send_half_page_2(modbus, slave_id, page_data, page)
def is_page_empty(page):
# type: (str) -> bool
return page.count('\xff') == len(page)
def reset_bms(modbus, slave_id):
# type: (Modbus, int) -> bool
print ('resetting BMS...')
result = modbus.write_registers(RESET_REGISTER, [1], unit=slave_id)
# expecting a ModbusIOException (timeout)
# BMS can no longer reply because it is already reset
success = isinstance(result, ModbusIOException)
if success:
print('done')
else:
print('FAILED to reset battery!')
return success
def calc_crc(page, data):
# type: (int, str) -> List[int]
crc = calc_stm32_crc([page] + bytes_to_words(data))
crc_bytes = struct.pack('>L', crc)
return bytes_to_words(crc_bytes)
def identify_battery(modbus, slave_id):
# type: (Modbus, int) -> Optional[str]
target = 'battery #' + str(slave_id) + ' at ' + modbus.port
try:
print(('contacting ' + target + ' ...'))
response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, unit=slave_id)
fw = '{0:0>4X}'.format(response.registers[0])
print(('found battery with firmware ' + fw))
return fw
except:
print(('failed to communicate with ' + target + ' !'))
return None
def print_usage():
print(('Usage: ' + __file__ + ' <serial device> <battery id> <firmware>'))
print(('Example: ' + __file__ + ' ttyUSB0 2 A08C.bin'))
def parse_cmdline_args(argv):
# type: (List[str]) -> (str, str, str, str)
def fail_with(msg):
print(msg)
print_usage()
exit(1)
if len(argv) < 1:
fail_with('missing argument for tty device')
if len(argv) < 2:
fail_with('missing argument for battery ID')
if len(argv) < 3:
fail_with('missing argument for firmware')
return argv[0], int(argv[1]), argv[2], get_fw_name(argv[2])
def verify_firmware(modbus, battery_id, fw_name):
# type: (Modbus, int, str) -> NoReturn
fw_verify = identify_battery(modbus, battery_id)
if fw_verify == fw_name:
print('SUCCESS')
else:
print('FAILED to verify uploaded firmware!')
if fw_verify is not None:
print('expected firmware version ' + fw_name + ' but got ' + fw_verify)
def wait_for_bms_reboot():
# type: () -> NoReturn
# wait 20s for the battery to reboot
print('waiting for BMS to reboot...')
for t in range(20, 0, -1):
print('\r{0} '.format(t), end=' ')
sleep(1)
print('0')
def main(argv):
# type: (List[str]) -> NoReturn
tty, battery_id, fw_path, fw_name = parse_cmdline_args(argv)
with LockTTY(tty), init_modbus(tty) as modbus:
if identify_battery(modbus, battery_id) is None:
return
clear_flash(modbus, battery_id)
upload_fw(modbus, battery_id, fw_path, fw_name)
if not reset_bms(modbus, battery_id):
return
wait_for_bms_reboot()
verify_firmware(modbus, battery_id, fw_name)
main(argv[1:])

View File

@ -3978,8 +3978,7 @@ var require_node_domexception = __commonJS({
var import_node_fs, import_node_domexception, stat, BlobDataItem; var import_node_fs, import_node_domexception, stat, BlobDataItem;
var init_from = __esm({ var init_from = __esm({
"node_modules/fetch-blob/from.js"() { "node_modules/fetch-blob/from.js"() {
// import_node_fs = require("node:fs"); import_node_fs = require("node:fs");
import_node_fs = require("fs");
import_node_domexception = __toESM(require_node_domexception(), 1); import_node_domexception = __toESM(require_node_domexception(), 1);
init_file(); init_file();
init_fetch_blob(); init_fetch_blob();
@ -13684,18 +13683,11 @@ var require_linq = __commonJS({
}); });
// node_modules/node-fetch/src/index.js // node_modules/node-fetch/src/index.js
// var import_node_http2 = __toESM(require("node:http"), 1); var import_node_http2 = __toESM(require("node:http"), 1);
// var import_node_https = __toESM(require("node:https"), 1); var import_node_https = __toESM(require("node:https"), 1);
// var import_node_zlib = __toESM(require("node:zlib"), 1); var import_node_zlib = __toESM(require("node:zlib"), 1);
// var import_node_stream2 = __toESM(require("node:stream"), 1); var import_node_stream2 = __toESM(require("node:stream"), 1);
// var import_node_buffer2 = require("node:buffer"); var import_node_buffer2 = require("node:buffer");
var import_node_http2 = __toESM(require("http"), 1);
var import_node_https = __toESM(require("https"), 1);
var import_node_zlib = __toESM(require("zlib"), 1);
var import_node_stream2 = __toESM(require("stream"), 1);
var import_node_buffer2 = require("buffer");
// node_modules/data-uri-to-buffer/dist/index.js // node_modules/data-uri-to-buffer/dist/index.js
function dataUriToBuffer(uri) { function dataUriToBuffer(uri) {
@ -13737,13 +13729,9 @@ function dataUriToBuffer(uri) {
var dist_default = dataUriToBuffer; var dist_default = dataUriToBuffer;
// node_modules/node-fetch/src/body.js // node_modules/node-fetch/src/body.js
// var import_node_stream = __toESM(require("node:stream"), 1); var import_node_stream = __toESM(require("node:stream"), 1);
// var import_node_util = require("node:util"); var import_node_util = require("node:util");
// var import_node_buffer = require("node:buffer"); var import_node_buffer = require("node:buffer");
var import_node_stream = __toESM(require("stream"), 1);
var import_node_util = require("util");
var import_node_buffer = require("buffer");
init_fetch_blob(); init_fetch_blob();
init_esm_min(); init_esm_min();
@ -14003,10 +13991,8 @@ var writeToStream = async (dest, { body }) => {
}; };
// node_modules/node-fetch/src/headers.js // node_modules/node-fetch/src/headers.js
// var import_node_util2 = require("node:util"); var import_node_util2 = require("node:util");
// var import_node_http = __toESM(require("node:http"), 1); var import_node_http = __toESM(require("node:http"), 1);
var import_node_util2 = require("util");
var import_node_http = __toESM(require("http"), 1);
var validateHeaderName = typeof import_node_http.default.validateHeaderName === "function" ? import_node_http.default.validateHeaderName : (name) => { var validateHeaderName = typeof import_node_http.default.validateHeaderName === "function" ? import_node_http.default.validateHeaderName : (name) => {
if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) { if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) {
const error2 = new TypeError(`Header name must be a valid HTTP token [${name}]`); const error2 = new TypeError(`Header name must be a valid HTTP token [${name}]`);
@ -14259,10 +14245,8 @@ Object.defineProperties(Response.prototype, {
}); });
// node_modules/node-fetch/src/request.js // node_modules/node-fetch/src/request.js
// var import_node_url = require("node:url"); var import_node_url = require("node:url");
// var import_node_util3 = require("node:util"); var import_node_util3 = require("node:util");
var import_node_url = require("url");
var import_node_util3 = require("util");
// node_modules/node-fetch/src/utils/get-search.js // node_modules/node-fetch/src/utils/get-search.js
var getSearch = (parsedURL) => { var getSearch = (parsedURL) => {
@ -14275,8 +14259,7 @@ var getSearch = (parsedURL) => {
}; };
// node_modules/node-fetch/src/utils/referrer.js // node_modules/node-fetch/src/utils/referrer.js
// var import_node_net = require("node:net"); var import_node_net = require("node:net");
var import_node_net = require("net");
function stripURLForUseAsAReferrer(url, originOnly = false) { function stripURLForUseAsAReferrer(url, originOnly = false) {
if (url == null) { if (url == null) {
return "no-referrer"; return "no-referrer";
@ -15060,29 +15043,20 @@ async function getAllDataFromVrm() {
const nbMppts = devices.count((d) => d.name === "Solar Charger"); const nbMppts = devices.count((d) => d.name === "Solar Charger");
return { return {
name: installation.name, name: installation.name,
// inverter: inverter?.productName ?? "unknown", inverter: inverter?.productName ?? "unknown",
inverter: (inverter && inverter.productName) ? inverter.productName : "unknown", inverterFw: inverter?.firmwareVersion ?? "unknown",
// inverterFw: inverter?.firmwareVersion ?? "unknown",
inverterFw: (inverter && inverter.firmwareVersion) ? inverter.firmwareVersion : "unknown",
identifier: installation.identifier, identifier: installation.identifier,
hasMains: installation.hasMains > 0, hasMains: installation.hasMains > 0,
hasGenerator: installation.hasGenerator > 0, hasGenerator: installation.hasGenerator > 0,
nbMppts, nbMppts,
nbPvInverters, nbPvInverters,
// firmware: gateway?.firmwareVersion ?? "unknown", firmware: gateway?.firmwareVersion ?? "unknown",
// autoUpdate: gateway?.autoUpdate ?? "unknown", autoUpdate: gateway?.autoUpdate ?? "unknown",
// updateTo: gateway?.updateTo ?? "unknown", updateTo: gateway?.updateTo ?? "unknown",
// lastConnection: gateway?.lastConnection ?? 0, lastConnection: gateway?.lastConnection ?? 0,
// lastPowerUpOrRestart: gateway?.lastPowerUpOrRestart ?? 0, lastPowerUpOrRestart: gateway?.lastPowerUpOrRestart ?? 0,
// machineSerialNumber: gateway?.machineSerialNumber ?? "unknown", machineSerialNumber: gateway?.machineSerialNumber ?? "unknown",
// controllerType: gateway?.productName ?? "unknown", controllerType: gateway?.productName ?? "unknown",
firmware: (gateway && gateway.firmwareVersion) ? gateway.firmwareVersion : "unknown",
autoUpdate: (gateway && gateway.autoUpdate) ? gateway.autoUpdate : "unknown",
updateTo: (gateway && gateway.updateTo) ? gateway.updateTo : "unknown",
lastConnection: (gateway && gateway.lastConnection) ? gateway.lastConnection : 0,
lastPowerUpOrRestart: (gateway && gateway.lastPowerUpOrRestart) ? gateway.lastPowerUpOrRestart : 0,
machineSerialNumber: (gateway && gateway.machineSerialNumber) ? gateway.machineSerialNumber : "unknown",
controllerType: (gateway && gateway.productName) ? gateway.productName : "unknown",
vrmLink: `vrm.victronenergy.com/installation/${installation.idSite}`, vrmLink: `vrm.victronenergy.com/installation/${installation.idSite}`,
accessLevel: installation.accessLevel, accessLevel: installation.accessLevel,
syscreated: installation.syscreated, syscreated: installation.syscreated,
@ -15094,29 +15068,12 @@ async function getAllDataFromVrm() {
}; };
} }
} }
// function getVpnIpFromHttp(vpnName) {
// return fetch(`${vpnIp}/vpn/${vpnName}`).then((r2) => r2.text()).then((t2) => t2.match(rxIp)?.firstOrDefault()?.replace("ifconfig-push", "").trim() ?? "");
// }
function getVpnIpFromHttp(vpnName) { function getVpnIpFromHttp(vpnName) {
return fetch(`${vpnIp}/vpn/${vpnName}`) return fetch(`${vpnIp}/vpn/${vpnName}`).then((r2) => r2.text()).then((t2) => t2.match(rxIp)?.firstOrDefault()?.replace("ifconfig-push", "").trim() ?? "");
.then((r2) => r2.text())
.then((t2) => {
const match = t2.match(rxIp);
return match && match.length > 0 ? match[0].replace("ifconfig-push", "").trim() : "";
});
} }
// function getVpnIpFromFs(vpnName) {
// return import_fs3.default.readFileSync(`${ccdDir}/${vpnName}`, "utf-8").match(rxIp)?.firstOrDefault()?.replace("ifconfig-push", "").trim() ?? "";
// }
const fs = require('fs');
function getVpnIpFromFs(vpnName) { function getVpnIpFromFs(vpnName) {
const fileContent = fs.readFileSync(`${ccdDir}/${vpnName}`, "utf-8"); return import_fs3.default.readFileSync(`${ccdDir}/${vpnName}`, "utf-8").match(rxIp)?.firstOrDefault()?.replace("ifconfig-push", "").trim() ?? "";
const match = fileContent.match(rxIp);
return match && match.length > 0 ? match[0].replace("ifconfig-push", "").trim() : "";
} }
function getVpnOnlineStatusFromHttp() { function getVpnOnlineStatusFromHttp() {
return fetch(`${vpnIp}/vpnstatus.txt`).then((r2) => r2.text()).then((s2) => s2.split("\n")); return fetch(`${vpnIp}/vpnstatus.txt`).then((r2) => r2.text()).then((s2) => s2.split("\n"));
} }
@ -15250,9 +15207,7 @@ var usage = {
"default value": "if 'field' is omitted it defaults to 'name'" "default value": "if 'field' is omitted it defaults to 'name'"
}; };
function handleRequest(request) { function handleRequest(request) {
// const url = request?.url; const url = request?.url;
const url = request && request.url;
if (isUndefined(url)) if (isUndefined(url))
return []; return [];
if (url === "/") if (url === "/")
@ -15261,12 +15216,10 @@ function handleRequest(request) {
const [installations, field] = filterInstallations(where5); const [installations, field] = filterInstallations(where5);
if (select6 === "*") if (select6 === "*")
return installations.toArray(); return installations.toArray();
// if ((select6 ?? field) === "name") if ((select6 ?? field) === "name")
if ((select6 || field) === "name")
return installations.select((i2) => i2.name).toArray(); return installations.select((i2) => i2.name).toArray();
const record = {}; const record = {};
// installations.toArray().forEach((i2) => record[i2.name] = getField(i2, select6 ?? field)); installations.toArray().forEach((i2) => record[i2.name] = getField(i2, select6 ?? field));
installations.toArray().forEach((i2) => record[i2.name] = getField(i2, select6 !== null && select6 !== undefined ? select6 : field));
return record; return record;
} }
function serve(request, response) { function serve(request, response) {

View File

@ -1,12 +1,13 @@
{ {
"name": "VrmSync", "name": "IeApi",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"esbuild": "^0.14.23", "esbuild": "^0.14.23",
"linq-to-typescript": "^10.0.0" "linq-to-typescript": "^10.0.0",
"rxjs": "^7.5.5"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
@ -412,13 +413,12 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.3", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.0.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -1121,11 +1121,10 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -1328,7 +1327,6 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -1390,6 +1388,18 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge2": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -1456,11 +1466,10 @@
} }
}, },
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "3.3.2", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.0.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "integrity": "sha512-8xeimMwMItMw8hRrOl3C9/xzU49HV/yE6ORew/l+dxWimO5A4Ra8ld2rerlJvc/O7et5Z1zrWsPX43v1QBjCxw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"data-uri-to-buffer": "^4.0.0", "data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4", "fetch-blob": "^3.1.4",
@ -1658,12 +1667,27 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/rxjs/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.2", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true, "dev": true,
"license": "ISC", "dependencies": {
"lru-cache": "^6.0.0"
},
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@ -1748,7 +1772,6 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@ -1854,11 +1877,10 @@
} }
}, },
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -1868,6 +1890,12 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true "dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
} }
}, },
"dependencies": { "dependencies": {
@ -2133,12 +2161,12 @@
} }
}, },
"braces": { "braces": {
"version": "3.0.3", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true, "dev": true,
"requires": { "requires": {
"fill-range": "^7.1.1" "fill-range": "^7.0.1"
} }
}, },
"callsites": { "callsites": {
@ -2562,9 +2590,9 @@
} }
}, },
"fill-range": { "fill-range": {
"version": "7.1.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
@ -2766,6 +2794,15 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
},
"merge2": { "merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -2810,9 +2847,9 @@
"dev": true "dev": true
}, },
"node-fetch": { "node-fetch": {
"version": "3.3.2", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.0.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "integrity": "sha512-8xeimMwMItMw8hRrOl3C9/xzU49HV/yE6ORew/l+dxWimO5A4Ra8ld2rerlJvc/O7et5Z1zrWsPX43v1QBjCxw==",
"dev": true, "dev": true,
"requires": { "requires": {
"data-uri-to-buffer": "^4.0.0", "data-uri-to-buffer": "^4.0.0",
@ -2930,11 +2967,29 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"semver": { "semver": {
"version": "7.6.2", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true "dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}, },
"shebang-command": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
@ -3063,9 +3118,9 @@
} }
}, },
"word-wrap": { "word-wrap": {
"version": "1.2.5", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true "dev": true
}, },
"wrappy": { "wrappy": {
@ -3073,6 +3128,12 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true "dev": true
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
} }
} }
} }

File diff suppressed because it is too large Load Diff