def Init(self): try: DBusGMainLoop(set_as_default=True) serial = os.path.basename(self.args.serial) self.dbusservice = VeDbusService('com.victronenergy.windcharger.bornay_' + serial) self.__mandatory__() self.__objects_dbus__() except: log.warn("Bornay wind+ has been created before") self.__mandatory__()
def __init__(self, servicename, deviceinstance, paths, productname='Dummy product', connection='Dummy service'): self._dbusservice = VeDbusService(servicename) self._paths = paths logging.debug("%s /DeviceInstance = %d" % (servicename, deviceinstance)) # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path( '/Mgmt/ProcessVersion', 'Unkown version, and running on Python ' + platform.python_version()) self._dbusservice.add_path('/Mgmt/Connection', connection) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', deviceinstance) self._dbusservice.add_path('/ProductId', 0) self._dbusservice.add_path('/ProductName', productname) self._dbusservice.add_path('/FirmwareVersion', 0) self._dbusservice.add_path('/HardwareVersion', 0) self._dbusservice.add_path('/Connected', 1) for path, settings in self._paths.iteritems(): self._dbusservice.add_path( path, settings['initial'], writeable=True, onchangecallback=self._handlechangedvalue) gobject.timeout_add(1000, self._update)
def __init__(self, bus, base, path, gpio, settings): self.gpio = gpio self.path = path self.bus = bus self.settings = settings self._level = 0 # Remember last state self.service = VeDbusService("{}.{}.input{:02d}".format( base, self.dbus_name, gpio), bus=bus) # Add objects required by ve-api self.service.add_path('/Mgmt/ProcessName', __file__) self.service.add_path('/Mgmt/ProcessVersion', VERSION) self.service.add_path('/Mgmt/Connection', path) self.service.add_path('/DeviceInstance', gpio) self.service.add_path('/ProductId', self.product_id) self.service.add_path('/ProductName', self.product_name) self.service.add_path('/Connected', 1) # Custom name setting def _change_name(p, v): # This should fire a change event that will update product_name # below. settings['name'] = v return True self.service.add_path('/CustomName', settings['name'], writeable=True, onchangecallback=_change_name) # We'll count the pulses for all types of services self.service.add_path('/Count', value=settings['count'])
def Init(self): try: self.dbusservice = VeDbusService( 'com.victronenergy.windcharger.bornay_ttyUSB0') self.__mandatory__() self.__objects_dbus__() except: log.warn("Bornay wind+ has been created before") self.__mandatory__()
def _create_dbus_service(self): dbusservice = VeDbusService(driver['connection']) dbusservice.add_mandatory_paths(processname=__file__, processversion=softwareVersion, connection=driver['connection'], deviceinstance=driver['instance'], productid=driver['id'], productname=driver['name'], firmwareversion=driver['version'], hardwareversion=driver['version'], connected=1) return dbusservice
def _create_dbus_service(self): dbusservice = VeDbusService("com.victronenergy.generator.startstop0") dbusservice.add_mandatory_paths(processname=__file__, processversion=softwareversion, connection='generator', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) return dbusservice
def _create_dbus_service(self): dbusservice = VeDbusService('com.victronenergy.system') dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareVersion, connection='data from other dbus processes', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) return dbusservice
def create_dbus_service(): dbusservice = VeDbusService('com.victronenergy.pvinverter.envoy') dbusservice.add_mandatory_paths( processname=__file__, processversion=0.1, connection='com.victronenergy.pvinverter.envoy', deviceinstance=driver['instance'], productid=driver['id'], productname=driver['name'], firmwareversion=driver['version'], hardwareversion=driver['version'], connected=1) return dbusservice
def __init__(self, n2kfluidtype, n2ktankinstance, paths, productname='Signal K tank', connection='Signal K tank service'): self._dbus = dbusconnection() self._servicename = '%s_%s_%s_%s' % ( APPLICATION_SERVICE_NAME, SIGNALK_SERVER.replace('.', '_').replace( ':', '_'), str(n2kfluidtype), str(n2ktankinstance)) self._dbusservicename = 'com.victronenergy.tank.%s' % self._servicename self._paths = paths # Process settings and recover our VRM instance number appsettingspath = '%s/%s' % (SETTINGS_ROOT, APPLICATION_SERVICE_NAME) servicesettingspath = '%s/%s' % (SETTINGS_ROOT, self._servicename) proposedclassdeviceinstance = '%s:%s' % ('tank', n2ktankinstance) SETTINGS = { 'instance': [ servicesettingspath + '/ClassAndVrmInstance', proposedclassdeviceinstance, 0, 0 ], 'customname': [servicesettingspath + '/CustomName', '', 0, 0] } self._settings = SettingsDevice(self._dbus, SETTINGS, self._handlesettingschanged) self._dbusservice = VeDbusService(self._dbusservicename, self._dbus) self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path( '/Mgmt/ProcessVersion', VERSION + ' on Python ' + platform.python_version()) self._dbusservice.add_path('/Mgmt/Connection', 'SignalK ' + self._settings['instance']) self._dbusservice.add_path('/DeviceInstance', self._settings['instance'].split(':')[1]) self._dbusservice.add_path('/ProductId', 0) self._dbusservice.add_path('/ProductName', '') self._dbusservice.add_path('/FirmwareVersion', 0) self._dbusservice.add_path('/HardwareVersion', 0) self._dbusservice.add_path('/Connected', 1) for path, settings in self._paths.iteritems(): self._dbusservice.add_path( path, settings['initial'], writeable=True, onchangecallback=self._handlechangedvalue)
def init(self, *args): super(NetClient, self).init(*args) svcname = 'com.victronenergy.modbusclient.%s' % self.name self.svc = VeDbusService(svcname, self.dbusconn) self.svc.add_path('/Scan', False, writeable=True, onchangecallback=self.set_scan) self.svc.add_path('/ScanProgress', None, gettextcallback=percent) self.mdns = mdns.MDNS() self.mdns.start() self.mdns_check_time = 0 self.mdns_query_time = 0
def __init__(self, devname, address): bus = (dbus.SessionBus(private=True) if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus(private=True)) self._dbusname = "fr.mildred.pzemvictron2020.pzem016.%s-%d" % (devname, address) self._dbusservice = VeDbusService(self._dbusname, bus=bus) self._error_message = "" self._disconnect = 0 # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path('/Mgmt/ProcessVersion', softwareVersion) self._dbusservice.add_path( '/Mgmt/Connection', "Device %d on Modbus-RTU %s" % (address, devname)) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', address) self._dbusservice.add_path('/ProductId', address) self._dbusservice.add_path('/ProductName', "PZEM-016") self._dbusservice.add_path('/FirmwareVersion', 0) self._dbusservice.add_path('/HardwareVersion', 0) self._dbusservice.add_path('/Connected', 0) # Readings self._dbusservice.add_path('/Ac/Current', 0, gettextcallback=self._get_text) self._dbusservice.add_path('/Ac/TotalEnergy', 0, gettextcallback=self._get_text) self._dbusservice.add_path('/Ac/Power', 0, gettextcallback=self._get_text) self._dbusservice.add_path('/Ac/Voltage', 0, gettextcallback=self._get_text) self._dbusservice.add_path('/Ac/Frequency', 0, gettextcallback=self._get_text) self._dbusservice.add_path('/Ac/PowerFactor', 0, gettextcallback=self._get_text) self._dbusservice.add_path('/DeviceType', "PZEM-016") self._dbusservice.add_path('/ErrorCode', 0, gettextcallback=self._get_text) self._dbusservice.add_path('/ErrorMessage', "")
def create_dbus_service(instance): # Use a private bus, so we can have multiple services bus = dbus.Bus.get_session(private=True) if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.Bus.get_system(private=True) dbusservice = VeDbusService("com.victronenergy.generator.startstop{}".format(instance), bus=bus) dbusservice.add_mandatory_paths( processname=sys.argv[0], processversion=softwareversion, connection='generator', deviceinstance=instance, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) return dbusservice
def __init__(self, host, base, instance): self.instance = instance self.service = service = VeDbusService("{}.smappee_{:02d}".format( base, instance), bus=dbusconnection()) # Add objects required by ve-api service.add_path('/Management/ProcessName', __file__) service.add_path('/Management/ProcessVersion', VERSION) service.add_path('/Management/Connection', host) service.add_path('/DeviceInstance', instance) service.add_path('/ProductId', 0xFFFF) # 0xB012 ? service.add_path('/ProductName', "SMAPPEE current meter") service.add_path('/FirmwareVersion', None) service.add_path('/Serial', None) service.add_path('/Connected', 1) service.add_path('/Ac/Energy/Forward', None) service.add_path('/Ac/Energy/Reverse', None) service.add_path('/Ac/L1/Current', None) service.add_path('/Ac/L1/Energy/Forward', None) service.add_path('/Ac/L1/Energy/Reverse', None) service.add_path('/Ac/L1/Power', None) service.add_path('/Ac/L1/Voltage', None) service.add_path('/Ac/L2/Current', None) service.add_path('/Ac/L2/Energy/Forward', None) service.add_path('/Ac/L2/Energy/Reverse', None) service.add_path('/Ac/L2/Power', None) service.add_path('/Ac/L2/Voltage', None) service.add_path('/Ac/L3/Current', None) service.add_path('/Ac/L3/Energy/Forward', None) service.add_path('/Ac/L3/Energy/Reverse', None) service.add_path('/Ac/L3/Power', None) service.add_path('/Ac/L3/Voltage', None) service.add_path('/Ac/Power', None)
class NetClient(Client): def __init__(self, proto): Client.__init__(self, proto) self.proto = proto def new_scanner(self, full): return NetScanner(self.proto, MODBUS_TCP_PORT, MODBUS_TCP_UNIT, if_blacklist) def init(self, *args): super(NetClient, self).init(*args) svcname = 'com.victronenergy.modbusclient.%s' % self.name self.svc = VeDbusService(svcname, self.dbusconn) self.svc.add_path('/Scan', False, writeable=True, onchangecallback=self.set_scan) self.svc.add_path('/ScanProgress', None, gettextcallback=percent) self.mdns = mdns.MDNS() self.mdns.start() self.mdns_check_time = 0 self.mdns_query_time = 0 def update(self): super(NetClient, self).update() now = time.time() if now - self.mdns_query_time > MDNS_QUERY_INTERVAL: self.mdns_query_time = now self.mdns.req() if now - self.mdns_check_time > MDNS_CHECK_INTERVAL: self.mdns_check_time = now maddr = self.mdns.get_devices() if maddr: units = probe.get_units('tcp') d = [] for a in maddr: d += ['tcp:%s:%s:%d' % (a[0], a[1], u) for u in units] self.probe_devices(d, nosave=True) return True
def _evaluate_if_we_are_needed(self): if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 3: if self._dbusservice is None: logger.info('Action! Going on dbus and taking control of the relay.') relay_polarity_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.settings', path='/Settings/Relay/Polarity', eventCallback=None, createsignal=True) if not self._relay_state_import: logger.info('Getting relay from systemcalc.') try: self._relay_state_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.system', path='/Relay/0/State', eventCallback=None, createsignal=True) except dbus.exceptions.DBusException: logger.info('Systemcalc relay not available.') self._relay_state_import = None pass # As is not possible to keep the relay state during the CCGX power cycles, # set the relay polarity to normally open. if relay_polarity_import.get_value() == 1: relay_polarity_import.set_value(0) logger.info('Setting relay polarity to normally open.') # put ourselves on the dbus self._dbusservice = VeDbusService('com.victronenergy.pump.startstop0') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareversion, connection='pump', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # State: None = invalid, 0 = stopped, 1 = running self._dbusservice.add_path('/State', value=0) self._dbusservice.add_path('/AvailableTankServices', value=None) self._dbusservice.add_path('/ActiveTankService', value=None) self._update_relay() self._handleservicechange() else: if self._dbusservice is not None: self._stop_pump() self._dbusservice.__del__() self._dbusservice = None self._relay_state_import = None logger.info('Relay function is no longer set to pump startstop: made sure pump is off and going off dbus')
def __init__(self, devname, address): self.imported = {} self.bus = (dbus.SessionBus(private=True) if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus(private=True)) self._dbusservice = VeDbusService( "com.victronenergy.vebus.mock-multiplus-%s-%s" % (devname, address), bus=self.bus) self.bus.add_signal_receiver(self.dbus_name_owner_changed, signal_name='NameOwnerChanged') logging.info('Searching dbus for vebus devices...') for serviceName in self.bus.list_names(): self.scan_dbus_service(serviceName) logging.info('Finished search for vebus devices') self._error_message = "" self._disconnect = 0 # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path('/Mgmt/ProcessVersion', softwareVersion) self._dbusservice.add_path( '/Mgmt/Connection', "Mock-Multiplus spawned by %s" % (devname, )) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', address) self._dbusservice.add_path('/ProductId', address) self._dbusservice.add_path('/ProductName', "MockMultiplus") self._dbusservice.add_path('/FirmwareVersion', 0) self._dbusservice.add_path('/HardwareVersion', 0) self._dbusservice.add_path('/Connected', 0) # Readings self._dbusservice.add_path('/Energy/InverterToAcOut', 0, gettextcallback=self._get_text) self._dbusservice.add_path('/DeviceType', "MockMultiplus") self._dbusservice.add_path('/ErrorCode', 0, gettextcallback=self._get_text) self._dbusservice.add_path('/ErrorMessage', "")
def __init__(self, name, host, base, instance, cts): self.instance = instance self.cts = cts self.service = service = VeDbusService("{}.smappee_{:02d}".format( base, instance), bus=dbusconnection()) # Add objects required by ve-api service.add_path('/Management/ProcessName', __file__) service.add_path('/Management/ProcessVersion', VERSION) service.add_path('/Management/Connection', host) service.add_path('/DeviceInstance', instance) service.add_path('/ProductId', 0xFFFF) # 0xB012 ? service.add_path('/ProductName', "Smappee - {}".format(name)) service.add_path('/FirmwareVersion', None) service.add_path('/Serial', None) service.add_path('/Connected', 1) _kwh = lambda p, v: (str(v) + 'KWh') _a = lambda p, v: (str(v) + 'A') _w = lambda p, v: (str(v) + 'W') _v = lambda p, v: (str(v) + 'V') service.add_path('/Ac/Energy/Forward', None, gettextcallback=_kwh) service.add_path('/Ac/Energy/Reverse', None, gettextcallback=_kwh) service.add_path('/Ac/L1/Current', None, gettextcallback=_a) service.add_path('/Ac/L1/Energy/Forward', None, gettextcallback=_kwh) service.add_path('/Ac/L1/Energy/Reverse', None, gettextcallback=_kwh) service.add_path('/Ac/L1/Power', None, gettextcallback=_w) service.add_path('/Ac/L1/Voltage', None, gettextcallback=_v) service.add_path('/Ac/L2/Current', None, gettextcallback=_a) service.add_path('/Ac/L2/Energy/Forward', None, gettextcallback=_kwh) service.add_path('/Ac/L2/Energy/Reverse', None, gettextcallback=_kwh) service.add_path('/Ac/L2/Power', None, gettextcallback=_w) service.add_path('/Ac/L2/Voltage', None, gettextcallback=_v) service.add_path('/Ac/L3/Current', None, gettextcallback=_a) service.add_path('/Ac/L3/Energy/Forward', None, gettextcallback=_kwh) service.add_path('/Ac/L3/Energy/Reverse', None, gettextcallback=_kwh) service.add_path('/Ac/L3/Power', None, gettextcallback=_w) service.add_path('/Ac/L3/Voltage', None, gettextcallback=_v) service.add_path('/Ac/Power', None, gettextcallback=_w) # Provide debug info about what cts make up what meter service.add_path('/Debug/Cts', ','.join(str(c) for c in cts))
def __init__(self, servicename, deviceinstance, paths, productname='Dummy product', connection='Dummy service'): self._dbusservice = VeDbusService(servicename) self._paths = paths logging.debug("%s /DeviceInstance = %d" % (servicename, deviceinstance)) # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path('/Mgmt/ProcessVersion', 'Unkown version, and running on Python ' + platform.python_version()) self._dbusservice.add_path('/Mgmt/Connection', connection) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', deviceinstance) self._dbusservice.add_path('/ProductId', 0) self._dbusservice.add_path('/ProductName', productname) self._dbusservice.add_path('/FirmwareVersion', 0) self._dbusservice.add_path('/HardwareVersion', 0) self._dbusservice.add_path('/Connected', 1) for path, settings in self._paths.iteritems(): self._dbusservice.add_path( path, settings['initial'], writeable=True, onchangecallback=self._handlechangedvalue) gobject.timeout_add(1000, self._update)
def update_dbus_service(self): if (len(self._acSensors['L1']) > 0 or len(self._acSensors['L2']) > 0 or len(self._acSensors['L3']) > 0): if self._dbusService is None: pf = {0: 'input1', 1: 'output', 2: 'input2'} self._dbusService = VeDbusService('com.victronenergy.pvinverter.vebusacsensor_' + pf[self._name]) #, self._dbusConn) self._dbusService.add_path('/Position', self._name, description=None, gettextcallback=self.gettextforposition) # Create the mandatory objects, as per victron dbus api document self._dbusService.add_path('/Mgmt/ProcessName', __file__) self._dbusService.add_path('/Mgmt/ProcessVersion', softwareVersion) self._dbusService.add_path('/Mgmt/Connection', 'AC Sensor on VE.Bus device') self._dbusService.add_path('/DeviceInstance', int(self._name) + 10) self._dbusService.add_path('/ProductId', 0xA141) self._dbusService.add_path('/ProductName', self._names[self._name]) self._dbusService.add_path('/Connected', 1) logging.info('Added to D-Bus: ' + self.__str__()) self.update_values()
def _evaluate_if_we_are_needed(self): if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 1: if self._dbusservice is None: logger.info('Action! Going on dbus and taking control of the relay.') relay_polarity_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.settings', path='/Settings/Relay/Polarity', eventCallback=None, createsignal=True) # As is not possible to keep the relay state during the CCGX power cycles, # set the relay polarity to normally open. if relay_polarity_import.get_value() == 1: relay_polarity_import.set_value(0) logger.info('Setting relay polarity to normally open.') # put ourselves on the dbus self._dbusservice = VeDbusService('com.victronenergy.generator.startstop0') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareversion, connection='generator', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # State: None = invalid, 0 = stopped, 1 = running self._dbusservice.add_path('/State', value=0) # Condition that made the generator start self._dbusservice.add_path('/RunningByCondition', value='') # Runtime self._dbusservice.add_path('/Runtime', value=0, gettextcallback=self._gettext) # Today runtime self._dbusservice.add_path('/TodayRuntime', value=0, gettextcallback=self._gettext) # Test run runtime self._dbusservice.add_path('/TestRunIntervalRuntime', value=self._interval_runtime(self._settings['testruninterval']), gettextcallback=self._gettext) # Next tes trun date, values is 0 for test run disabled self._dbusservice.add_path('/NextTestRun', value=None, gettextcallback=self._gettext) # Next tes trun is needed 1, not needed 0 self._dbusservice.add_path('/SkipTestRun', value=None) # Manual start self._dbusservice.add_path('/ManualStart', value=0, writeable=True) # Manual start timer self._dbusservice.add_path('/ManualStartTimer', value=0, writeable=True) # Silent mode active self._dbusservice.add_path('/QuietHours', value=0) self._determineservices() else: if self._dbusservice is not None: self._stop_generator() self._dbusservice.__del__() self._dbusservice = None # Reset conditions for condition in self._condition_stack: self._reset_condition(self._condition_stack[condition]) logger.info('Relay function is no longer set to generator start/stop: made sure generator is off ' + 'and now going off dbus')
class DbusBatteryService: def __init__(self, servicename, deviceinstance, voltage, capacity, productname='Valence U-BMS', connection='can0'): self.minUpdateDone = 0 self.dailyResetDone = 0 self._bat = UbmsBattery(capacity=capacity, voltage=voltage, connection=connection) self._dbusservice = VeDbusService(servicename + '.socketcan_' + connection + '_di' + str(deviceinstance)) logging.debug("%s /DeviceInstance = %d" % (servicename + '.socketcan_' + connection + '_di' + str(deviceinstance), deviceinstance)) # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path( '/Mgmt/ProcessVersion', VERSION + ' running on Python ' + platform.python_version()) self._dbusservice.add_path('/Mgmt/Connection', connection) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', deviceinstance) self._dbusservice.add_path('/ProductId', 0) self._dbusservice.add_path('/ProductName', productname) self._dbusservice.add_path('/FirmwareVersion', 'unknown') self._dbusservice.add_path('/HardwareVersion', 'unknown') self._dbusservice.add_path('/Connected', 0) # Create battery specific objects self._dbusservice.add_path('/Status', 0) self._dbusservice.add_path('/Mode', 1, writeable=True, onchangecallback=self._transmit_mode) self._dbusservice.add_path('/Soh', 100) self._dbusservice.add_path('/Capacity', int(capacity)) self._dbusservice.add_path('/InstalledCapacity', int(capacity)) self._dbusservice.add_path('/Dc/0/Temperature', 25) self._dbusservice.add_path('/Info/MaxChargeCurrent', 70) self._dbusservice.add_path('/Info/MaxDischargeCurrent', 150) self._dbusservice.add_path('/Info/MaxChargeVoltage', float(voltage)) self._dbusservice.add_path('/Info/BatteryLowVoltage', 24.0) self._dbusservice.add_path('/Alarms/CellImbalance', 0) self._dbusservice.add_path('/Alarms/LowVoltage', 0) self._dbusservice.add_path('/Alarms/HighVoltage', 0) self._dbusservice.add_path('/Alarms/HighDischargeCurrent', 0) self._dbusservice.add_path('/Alarms/HighChargeCurrent', 0) self._dbusservice.add_path('/Alarms/LowSoc', 0) self._dbusservice.add_path('/Alarms/LowTemperature', 0) self._dbusservice.add_path('/Alarms/HighTemperature', 0) self._dbusservice.add_path('/Balancing', 0) self._dbusservice.add_path('/System/HasTemperature', 1) self._dbusservice.add_path('/System/NrOfBatteries', 10) self._dbusservice.add_path('/System/NrOfModulesOnline', 10) self._dbusservice.add_path('/System/NrOfModulesOffline', 0) self._dbusservice.add_path('/System/NrOfModulesBlockingDischarge', 0) self._dbusservice.add_path('/System/NrOfModulesBlockingCharge', 0) self._dbusservice.add_path('/System/NrOfBatteriesBalancing', 0) self._dbusservice.add_path('/System/BatteriesParallel', 5) self._dbusservice.add_path('/System/BatteriesSeries', 2) self._dbusservice.add_path('/System/NrOfCellsPerBattery', 4) self._dbusservice.add_path('/System/MinVoltageCellId', 'M_C_') self._dbusservice.add_path('/System/MaxVoltageCellId', 'M_C_') self._dbusservice.add_path('/System/MinCellTemperature', 10.0) self._dbusservice.add_path('/System/MaxCellTemperature', 10.0) self._dbusservice.add_path('/System/MaxPcbTemperature', 10.0) self._settings = SettingsDevice( bus=dbus.SystemBus() if (platform.machine() == 'armv7l') else dbus.SessionBus(), supportedSettings={ 'AvgDischarge': ['/Settings/Ubms/AvgerageDischarge', 0.0, 0, 0], 'TotalAhDrawn': ['/Settings/Ubms/TotalAhDrawn', 0.0, 0, 0], 'TimeLastFull': ['/Settings/Ubms/TimeLastFull', 0.0, 0, 0], 'MinCellVoltage': ['/Settings/Ubms/MinCellVoltage', 4.0, 2.0, 4.0], 'MaxCellVoltage': ['/Settings/Ubms/MaxCellVoltage', 2.0, 2.0, 4.0], 'interval': ['/Settings/Ubms/Interval', 50, 50, 200] }, eventCallback=handle_changed_setting) self._summeditems = { '/System/MaxCellVoltage': { 'gettext': '%.2F V' }, '/System/MinCellVoltage': { 'gettext': '%.2F V' }, '/Dc/0/Voltage': { 'gettext': '%.2F V' }, '/Dc/0/Current': { 'gettext': '%.1F A' }, '/Dc/0/Power': { 'gettext': '%.0F W' }, '/Soc': { 'gettext': '%.0F %%' }, '/History/TotalAhDrawn': { 'gettext': '%.0F Ah' }, '/History/DischargedEnergy': { 'gettext': '%.2F kWh' }, '/History/ChargedEnergy': { 'gettext': '%.2F kWh' }, '/History/AverageDischarge': { 'gettext': '%.2F kWh' }, '/TimeToGo': { 'gettext': '%.0F s' }, '/ConsumedAmphours': { 'gettext': '%.1F Ah' } } for path in self._summeditems.keys(): self._dbusservice.add_path(path, value=None, gettextcallback=self._gettext) self._dbusservice['/History/AverageDischarge'] = self._settings[ 'AvgDischarge'] self._dbusservice['/History/TotalAhDrawn'] = self._settings[ 'TotalAhDrawn'] self._dbusservice.add_path('/History/TimeSinceLastFullCharge', 0) self._dbusservice.add_path('/History/MinCellVoltage', self._settings['MinCellVoltage']) self._dbusservice.add_path('/History/MaxCellVoltage', self._settings['MaxCellVoltage']) self._dbusservice['/ConsumedAmphours'] = 0 logging.info( "History cell voltage min: %.3f, max: %.3f, totalAhDrawn: %d", self._settings['MinCellVoltage'], self._settings['MaxCellVoltage'], self._settings['TotalAhDrawn']) self._dbusservice['/History/ChargedEnergy'] = 0 self._dbusservice['/History/DischargedEnergy'] = 0 gobject.timeout_add(self._settings['interval'], exit_on_error, self._update) def _gettext(self, path, value): item = self._summeditems.get(path) if item is not None: return item['gettext'] % value return str(value) def _transmit_mode(self, path, value): self._bat.set_mode(value) def __del__(self): self._safe_history() logging.info('Stopping dbus_ubms') def _safe_history(self): logging.debug('Saving history to localsettings') self._settings['AvgDischarge'] = self._dbusservice[ '/History/AverageDischarge'] self._settings['TotalAhDrawn'] = self._dbusservice[ '/History/TotalAhDrawn'] self._settings['MinCellVoltage'] = self._dbusservice[ '/History/MinCellVoltage'] self._settings['MaxCellVoltage'] = self._dbusservice[ '/History/MaxCellVoltage'] def _daily_stats(self): if (self._dbusservice['/History/DischargedEnergy'] == 0): return logging.info( "Updating stats, SOC: %d, Discharged: %.2f, Charged: %.2f ", self._bat.soc, self._dbusservice['/History/DischargedEnergy'], self._dbusservice['/History/ChargedEnergy']) self._dbusservice['/History/AverageDischarge'] = ( 6 * self._dbusservice['/History/AverageDischarge'] + self._dbusservice['/History/DischargedEnergy']) / 7 #rolling week self._dbusservice['/History/ChargedEnergy'] = 0 self._dbusservice['/History/DischargedEnergy'] = 0 dt = datetime.now() - datetime.fromtimestamp( float(self._settings['TimeLastFull'])) #if full within the last 24h and more than *0% consumed, estimate actual capacity and SOH based on consumed amphours from full and SOC reported if dt.total_seconds() < 24 * 3600 and self._bat.soc < 70: self._dbusservice['/Capacity'] = int( -self._dbusservice['/ConsumedAmphours'] * 100 / (100 - self._bat.soc)) self._dbusservice['/Soh'] = int( self._dbusservice['/Capacity'] * 100 / self._dbusservice['/InstalledCapacity']) logging.info("SOH: %d, Capacity: %d ", self._dbusservice['/Soh'], self._dbusservice['/Capacity']) self.dailyResetDone = datetime.now().day def _update(self): if self._bat.updated != -1: self._dbusservice['/Connected'] = 1 else: self._dbusservice['/Connected'] = 0 # self._dbusservice['/Alarms/CellImbalance'] = (self._bat.internalErrors & 0x20)>>5 deltaCellVoltage = self._bat.maxCellVoltage - self._bat.minCellVoltage # flag cell imbalance, only log first occurence if (deltaCellVoltage > 0.25): self._dbusservice['/Alarms/CellImbalance'] = 2 if self._bat.balanced: logging.error( "Cell voltage imbalance: %.2fV, SOC: %d, @Module: %d ", deltaCellVoltage, self._bat.soc, self.moduleSoc.index(min(self.moduleSoc))) logging.info("SOC: %d ", self._bat.soc) self._bat.balanced = False elif (deltaCellVoltage >= 0.18): # warn only if not already balancing (UBMS threshold is 0.15V) if self._bat.numberOfModulesBalancing == 0: self._dbusservice['/Alarms/CellImbalance'] = 1 if self._bat.balanced: chain = itertools.chain(*self._bat.cellVoltages) flatVList = list(chain) iMax = flatVList.index(max(flatVList)) iMin = flatVList.index(min(flatVList)) logging.info( "Cell voltage imbalance: %.2fV, iMin: %d, iMax %d, SOC: %d ", deltaCellVoltage, iMin, iMax, self._bat.soc) self._bat.balanced = False else: self._dbusservice['/Alarms/CellImbalance'] = 0 self._bat.balanced = True self._dbusservice['/Alarms/LowVoltage'] = ( self._bat.voltageAndCellTAlarms & 0x10) >> 3 self._dbusservice['/Alarms/HighVoltage'] = ( self._bat.voltageAndCellTAlarms & 0x20) >> 4 self._dbusservice['/Alarms/LowSoc'] = (self._bat.voltageAndCellTAlarms & 0x08) >> 3 self._dbusservice['/Alarms/HighDischargeCurrent'] = ( self._bat.currentAndPcbTAlarms & 0x3) # flag high cell temperature alarm and high pcb temperature alarm self._dbusservice['/Alarms/HighTemperature'] = ( self._bat.voltageAndCellTAlarms & 0x6) >> 1 | (self._bat.currentAndPcbTAlarms & 0x18) >> 3 self._dbusservice['/Alarms/LowTemperature'] = (self._bat.mode & 0x60) >> 5 self._dbusservice['/Soc'] = self._bat.soc dt = datetime.now() - datetime.fromtimestamp( float(self._settings['TimeLastFull'])) self._dbusservice['/History/TimeSinceLastFullCharge'] = ( dt.seconds + dt.days * 24 * 3600) if self._bat.soc == 100 or self._bat.chargeComplete: #reset used Amphours to zero self._dbusservice['/ConsumedAmphours'] = 0 if datetime.fromtimestamp(time()).day != datetime.fromtimestamp( float(self._settings['TimeLastFull'])).day: #and if it is the first time that day also create log entry logging.info("Fully charged, Discharged: %.2f, Charged: %.2f ", self._dbusservice['/History/DischargedEnergy'], self._dbusservice['/History/ChargedEnergy']) self._settings['TimeLastFull'] = time() self._dbusservice[ '/Status'] = self._bat.mode & 0xC #self._bat.opState[self._bat.mode & 0xC] self._dbusservice['/Mode'] = self._bat.opModes[self._bat.mode & 0x3] self._dbusservice['/Balancing'] = (self._bat.mode & 0x10) >> 4 self._dbusservice['/Dc/0/Current'] = self._bat.current self._dbusservice['/Dc/0/Voltage'] = self._bat.voltage power = self._bat.voltage * self._bat.current self._dbusservice['/Dc/0/Power'] = power self._dbusservice['/Dc/0/Temperature'] = self._bat.maxCellTemperature #only update the below every 10s to reduce load # if datetime.now().second not in [0, 20, 40]: # return True chain = itertools.chain(*self._bat.cellVoltages) flatVList = list(chain) index = flatVList.index(max(flatVList)) m = index / 4 c = index % 4 self._dbusservice['/System/MaxVoltageCellId'] = 'M' + str( m + 1) + 'C' + str(c + 1) index = flatVList.index(min(flatVList)) m = index / 4 c = index % 4 self._dbusservice['/System/MinVoltageCellId'] = 'M' + str( m + 1) + 'C' + str(c + 1) self._dbusservice['/System/MaxCellVoltage'] = self._bat.maxCellVoltage if (self._bat.maxCellVoltage > self._dbusservice['/History/MaxCellVoltage']): self._dbusservice[ '/History/MaxCellVoltage'] = self._bat.maxCellVoltage logging.info("New maximum cell voltage: %f", self._bat.maxCellVoltage) self._dbusservice['/System/MinCellVoltage'] = self._bat.minCellVoltage if (0 < self._bat.minCellVoltage < self._dbusservice['/History/MinCellVoltage']): self._dbusservice[ '/History/MinCellVoltage'] = self._bat.minCellVoltage logging.info("New minimum cell voltage: %f", self._bat.minCellVoltage) self._dbusservice[ '/System/MinCellTemperature'] = self._bat.minCellTemperature self._dbusservice[ '/System/MaxCellTemperature'] = self._bat.maxCellTemperature self._dbusservice[ '/System/MaxPcbTemperature'] = self._bat.maxPcbTemperature self._dbusservice[ '/Info/MaxChargeCurrent'] = self._bat.maxChargeCurrent self._dbusservice[ '/Info/MaxDischargeCurrent'] = self._bat.maxDischargeCurrent self._dbusservice[ '/Info/MaxChargeVoltage'] = self._bat.maxChargeVoltage self._dbusservice[ '/System/NrOfModulesOnline'] = self._bat.numberOfModulesCommunicating self._dbusservice[ '/System/NrOfModulesOffline'] = self._bat.numberOfModules - self._bat.numberOfModulesCommunicating self._dbusservice[ '/System/NrOfBatteriesBalancing'] = self._bat.numberOfModulesBalancing #update energy statistics daily at 6:00, if datetime.now().hour == 6 and datetime.now( ).minute == 0 and datetime.now().day != self.dailyResetDone: self._daily_stats() now = datetime.now().time() if now.minute != self.minUpdateDone: self.minUpdateDone = now.minute if self._bat.current > 0: #charging self._dbusservice[ '/History/ChargedEnergy'] += power * 1.666667e-5 #kWh #calculate time to full, in the absense of a mutex accessing bat.current value might have changed #to 0 after check above, so try to catch a div0 try: self._dbusservice['/TimeToGo'] = ( 100 - self._bat.soc ) * self._bat.capacity * 36 / self._bat.current except: self._dbusservice[ '/TimeToGo'] = self._bat.soc * self._bat.capacity * 36 else: #discharging self._dbusservice[ '/ConsumedAmphours'] += self._bat.current * 0.016667 #Ah self._dbusservice[ '/History/TotalAhDrawn'] += self._bat.current * 0.016667 #Ah self._dbusservice[ '/History/DischargedEnergy'] += power * 1.666667e-5 #kWh #calculate time to empty try: self._dbusservice[ '/TimeToGo'] = self._bat.soc * self._bat.capacity * 36 / ( -self._bat.current) except: self._dbusservice[ '/TimeToGo'] = self._bat.soc * self._bat.capacity * 36 self._safe_history() self._bat.updated = -1 return True
class VBus(): def __init__(self): self.dbusservice = None #dbus service variable self.args = "" #extract parse argument self.init_on = 0 #variable to init the vebus service #----------------------------------------------------------------------------- # Initializes the different arguments to add. # ENTRIES: # Nothing # RETURNS: # Nothing #----------------------------------------------------------------------------- def parser_arguments(self): # Argument parsing parser = ArgumentParser(description='Wind+ with CCGX monitoring', add_help=True) parser.add_argument("-n", "--name", help="the D-Bus service you want me to claim", type=str, default="com.windcharger.bornay_ttyUSB0") parser.add_argument("-i", "--deviceinstance", help="the device instance you want me to be", type=str, default="0") parser.add_argument("-d", "--debug", help="set logging level to debug", action="store_true") parser.add_argument('-s', '--serial', default='/dev/ttyUSB0') self.args = parser.parse_args() log.info(self.args) # Init logging logging.basicConfig( level=(logging.DEBUG if self.args.debug else logging.INFO)) log.info(__file__ + " is starting up") logLevel = { 0: 'NOTSET', 10: 'DEBUG', 20: 'INFO', 30: 'WARNING', 40: 'ERROR' } log.info('Loglevel set to ' + logLevel[log.getEffectiveLevel()]) #----------------------------------------------------------------------------- # Initializes the vbus protocol. # ENTRIES: # Nothing # RETURNS: # Nothing #----------------------------------------------------------------------------- def Init(self): try: DBusGMainLoop(set_as_default=True) serial = os.path.basename(self.args.serial) self.dbusservice = VeDbusService( 'com.victronenergy.windcharger.bornay_' + serial) self.__mandatory__() self.__objects_dbus__() except: log.warn("Bornay wind+ has been created before") self.__mandatory__() #----------------------------------------------------------------------------- # Registers the mandatory instances # ENTRIES: # Nothing # RETURNS: # Nothing #----------------------------------------------------------------------------- def __mandatory__(self): try: log.info("using device instance 0") # Create the management objects, as specified in the ccgx dbus-api document self.dbusservice.add_path('/Management/ProcessName', __file__) self.dbusservice.add_path( '/Management/ProcessVersion', 'Version {} running on Python {}'.format( __version__, sys.version)) self.dbusservice.add_path('/Management/Connection', 'ModBus RTU') # Create the mandatory objects self.dbusservice.add_path('/DeviceInstance', 0) self.dbusservice.add_path('/ProductId', 0) self.dbusservice.add_path('/ProductName', 'Bornay Wind+ MPPT') self.dbusservice.add_path('/FirmwareVersion', __version__) self.dbusservice.add_path('/HardwareVersion', 1.01) self.dbusservice.add_path('/Connected', 1) except: log.warn("Mandatory Bornay wind+ has been created before") self.__objects_dbus__() #----------------------------------------------------------------------------- # Creates the different registers to save. # ENTRIES: # Nothing # RETURNS: # Nothing #----------------------------------------------------------------------------- def __objects_dbus__(self): try: # Create all the objects that we want to export to the dbus # All are initialized with the same value 0 self.dbusservice.add_path('/Mppt/StatusMEF', 0, writeable=True) self.dbusservice.add_path('/Mppt/RefMEF', 0, writeable=True) self.dbusservice.add_path('/Turbine/BatPowerLastMin', 0, writeable=True) self.dbusservice.add_path('/Turbine/BatPowerLastHour', 0, writeable=True) self.dbusservice.add_path('/Turbine/BreakerPowerLastMin', 0, writeable=True) self.dbusservice.add_path('/Turbine/WindSpeedLastMin', 0, writeable=True) self.dbusservice.add_path('/Turbine/WindSpeedLastHour', 0, writeable=True) self.dbusservice.add_path('/Mppt/Phase', 0, writeable=True) self.dbusservice.add_path('/Mppt/SinkTemp', 0, writeable=True) self.dbusservice.add_path('/Mppt/BoxTemp', 0, writeable=True) self.dbusservice.add_path('/Flags/ElevatedVoltage', 0, writeable=True) self.dbusservice.add_path('/Flags/Extrem', 0, writeable=True) self.dbusservice.add_path('/Flags/ExternSupply', 0, writeable=True) self.dbusservice.add_path('/Flags/ElevatedWind', 0, writeable=True) self.dbusservice.add_path('/Flags/FanState', 0, writeable=True) self.dbusservice.add_path('/Flags/EmergencyButton', 0, writeable=True) self.dbusservice.add_path('/Turbine/RPM', 0, writeable=True) self.dbusservice.add_path('/History/Overall/MaxRPM', 0, writeable=True) self.dbusservice.add_path('/Mppt/DuttyCycle', 0, writeable=True) self.dbusservice.add_path('/Turbine/WindSpeed', 0, writeable=True) self.dbusservice.add_path('/Turbine/VDC', 0, writeable=True) self.dbusservice.add_path('/Dc/0/Current', 0, writeable=True) self.dbusservice.add_path('/Turbine/IBrk', 0, writeable=True) self.dbusservice.add_path('/Dc/0/Power', 0, writeable=True) self.dbusservice.add_path('/Turbine/AvailablePower', 0, writeable=True) self.dbusservice.add_path('/Turbine/Stop', 0, writeable=True) self.dbusservice.add_path('/Dc/0/Voltage', 0, writeable=True) self.dbusservice.add_path('/Mppt/ChargerState', 0, writeable=True) self.dbusservice.add_path('/Turbine/EstimatedWind', 0, writeable=True) self.dbusservice.add_path('/Flags/ChargedBattery', 0, writeable=True) self.dbusservice.add_path('/Mppt/AbsortionTime', 0, writeable=True) except: log.warn("Bornay wind+ objects has been created before") #----------------------------------------------------------------------------- # Update the different registers to save in dbus. # ENTRIES: # -modbus_values: Vector who contains the bornay modbus values # RETURNS: # Nothing #----------------------------------------------------------------------------- def update_modbus_values(self, value_modbus): self.dbusservice['/Mppt/StatusMEF'] = value_modbus[0] self.dbusservice['/Mppt/RefMEF'] = value_modbus[1] self.dbusservice['/Turbine/BatPowerLastMin'] = value_modbus[2] self.dbusservice['/Turbine/BatPowerLastHour'] = value_modbus[3] self.dbusservice['/Turbine/BreakerPowerLastMin'] = value_modbus[4] self.dbusservice['/Turbine/WindSpeedLastMin'] = value_modbus[5] self.dbusservice['/Turbine/WindSpeedLastHour'] = value_modbus[6] self.dbusservice['/Mppt/Phase'] = value_modbus[7] self.dbusservice['/Mppt/SinkTemp'] = (value_modbus[8] / 10) self.dbusservice['/Mppt/BoxTemp'] = (value_modbus[9] / 10) self.dbusservice['/Flags/ElevatedVoltage'] = value_modbus[10] self.dbusservice['/Flags/Extrem'] = value_modbus[11] self.dbusservice['/Flags/ExternSupply'] = value_modbus[12] self.dbusservice['/Flags/ElevatedWind'] = value_modbus[13] self.dbusservice['/Flags/FanState'] = value_modbus[14] self.dbusservice['/Flags/EmergencyButton'] = value_modbus[15] self.dbusservice['/Turbine/RPM'] = value_modbus[16] self.dbusservice['/History/Overall/MaxRPM'] = value_modbus[17] self.dbusservice['/Mppt/DuttyCycle'] = value_modbus[18] self.dbusservice['/Turbine/WindSpeed'] = (value_modbus[19] / 100) self.dbusservice['/Turbine/VDC'] = (value_modbus[20] / 10) self.dbusservice['/Dc/0/Current'] = (value_modbus[21] / 10) self.dbusservice['/Turbine/IBrk'] = (value_modbus[22] / 10) self.dbusservice['/Dc/0/Power'] = value_modbus[23] self.dbusservice['/Turbine/AvailablePower'] = value_modbus[24] self.dbusservice['/Turbine/Stop'] = value_modbus[25] self.dbusservice['/Dc/0/Voltage'] = (value_modbus[26] / 10) self.dbusservice['/Mppt/ChargerState'] = value_modbus[27] self.dbusservice['/Turbine/EstimatedWind'] = (value_modbus[28] / 10) self.dbusservice['/Flags/ChargedBattery'] = value_modbus[29] self.dbusservice['/Mppt/AbsortionTime'] = value_modbus[30]
class DbusGenerator: def __init__(self, retries=300): self._bus = dbus.SystemBus() if ( platform.machine() == 'armv7l' or 'DBUS_SESSION_BUS_ADDRESS' not in environ) else dbus.SessionBus() self.RELAY_GPIO_FILE = '/sys/class/gpio/gpio182/value' self.HISTORY_DAYS = 30 # One second per retry self.RETRIES_ON_ERROR = retries self._testrun_soc_retries = 0 self._last_counters_check = 0 self._dbusservice = None self._starttime = 0 self._manualstarttimer = 0 self._last_runtime_update = 0 self._timer_runnning = 0 self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None self._battery_measurement_available = True self._vebusservice_high_temperature_import = None self._vebusservice_overload_import = None self._vebusservice = None self._vebusservice_available = False self._relay_state_import = None self._condition_stack = { 'batteryvoltage': { 'name': 'batteryvoltage', 'reached': False, 'boolean': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'battery' }, 'batterycurrent': { 'name': 'batterycurrent', 'reached': False, 'boolean': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'battery' }, 'acload': { 'name': 'acload', 'reached': False, 'boolean': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'vebus' }, 'inverterhightemp': { 'name': 'inverterhightemp', 'reached': False, 'boolean': True, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'vebus' }, 'inverteroverload': { 'name': 'inverteroverload', 'reached': False, 'boolean': True, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'vebus' }, 'soc': { 'name': 'soc', 'reached': False, 'boolean': False, 'timed': False, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'battery' } } # DbusMonitor expects these values to be there, even though we don need them. So just # add some dummy data. This can go away when DbusMonitor is more generic. dummy = { 'code': None, 'whenToLog': 'configChange', 'accessLevel': None } # TODO: possible improvement: don't use the DbusMonitor it all, since we are only monitoring # a set of static values which will always be available. DbusMonitor watches for services # that come and go, and takes care of automatic signal subscribtions etc. etc: all not necessary # in this use case where we have fixed services names (com.victronenergy.settings, and c # com.victronenergy.system). self._dbusmonitor = DbusMonitor({ 'com.victronenergy.settings': { # This is not our setting so do it here. not in supportedSettings '/Settings/Relay/Function': dummy, '/Settings/Relay/Polarity': dummy, '/Settings/System/TimeZone': dummy, }, 'com.victronenergy.system': { # This is not our setting so do it here. not in supportedSettings '/Ac/Consumption/Total/Power': dummy, '/Ac/PvOnOutput/Total/Power': dummy, '/Ac/PvOnGrid/Total/Power': dummy, '/Ac/PvOnGenset/Total/Power': dummy, '/Dc/Pv/Power': dummy, '/AutoSelectedBatteryMeasurement': dummy, } }, self._dbus_value_changed, self._device_added, self._device_removed) # Set timezone to user selected timezone environ['TZ'] = self._dbusmonitor.get_value( 'com.victronenergy.settings', '/Settings/System/TimeZone') # Connect to localsettings self._settings = SettingsDevice( bus=self._bus, supportedSettings={ 'autostart': ['/Settings/Generator0/AutoStartEnabled', 1, 0, 1], 'accumulateddaily': ['/Settings/Generator0/AccumulatedDaily', '', 0, 0], 'accumulatedtotal': ['/Settings/Generator0/AccumulatedTotal', 0, 0, 0], 'batterymeasurement': ['/Settings/Generator0/BatteryService', "default", 0, 0], 'minimumruntime': ['/Settings/Generator0/MinimumRuntime', 0, 0, 86400], # minutes # On permanent loss of communication: 0 = Stop, 1 = Start, 2 = keep running 'onlosscommunication': ['/Settings/Generator0/OnLossCommunication', 0, 0, 2], # Quiet hours 'quiethoursenabled': ['/Settings/Generator0/QuietHours/Enabled', 0, 0, 1], 'quiethoursstarttime': ['/Settings/Generator0/QuietHours/StartTime', 75600, 0, 86400], 'quiethoursendtime': ['/Settings/Generator0/QuietHours/EndTime', 21600, 0, 86400], # SOC 'socenabled': ['/Settings/Generator0/Soc/Enabled', 0, 0, 1], 'socstart': ['/Settings/Generator0/Soc/StartValue', 90, 0, 100], 'socstop': ['/Settings/Generator0/Soc/StopValue', 90, 0, 100], 'qh_socstart': ['/Settings/Generator0/Soc/QuietHoursStartValue', 90, 0, 100], 'qh_socstop': ['/Settings/Generator0/Soc/QuietHoursStopValue', 90, 0, 100], # Voltage 'batteryvoltageenabled': [ '/Settings/Generator0/BatteryVoltage/Enabled', 0, 0, 1 ], 'batteryvoltagestart': [ '/Settings/Generator0/BatteryVoltage/StartValue', 11.5, 0, 150 ], 'batteryvoltagestop': [ '/Settings/Generator0/BatteryVoltage/StopValue', 12.4, 0, 150 ], 'batteryvoltagestarttimer': [ '/Settings/Generator0/BatteryVoltage/StartTimer', 20, 0, 10000 ], 'batteryvoltagestoptimer': [ '/Settings/Generator0/BatteryVoltage/StopTimer', 20, 0, 10000 ], 'qh_batteryvoltagestart': [ '/Settings/Generator0/BatteryVoltage/QuietHoursStartValue', 11.9, 0, 100 ], 'qh_batteryvoltagestop': [ '/Settings/Generator0/BatteryVoltage/QuietHoursStopValue', 12.4, 0, 100 ], # Current 'batterycurrentenabled': [ '/Settings/Generator0/BatteryCurrent/Enabled', 0, 0, 1 ], 'batterycurrentstart': [ '/Settings/Generator0/BatteryCurrent/StartValue', 10.5, 0.5, 1000 ], 'batterycurrentstop': [ '/Settings/Generator0/BatteryCurrent/StopValue', 5.5, 0, 1000 ], 'batterycurrentstarttimer': [ '/Settings/Generator0/BatteryCurrent/StartTimer', 20, 0, 10000 ], 'batterycurrentstoptimer': [ '/Settings/Generator0/BatteryCurrent/StopTimer', 20, 0, 10000 ], 'qh_batterycurrentstart': [ '/Settings/Generator0/BatteryCurrent/QuietHoursStartValue', 20.5, 0, 1000 ], 'qh_batterycurrentstop': [ '/Settings/Generator0/BatteryCurrent/QuietHoursStopValue', 15.5, 0, 1000 ], # AC load 'acloadenabled': [ '/Settings/Generator0/AcLoad/Enabled', 0, 0, 1 ], 'acloadstart': [ '/Settings/Generator0/AcLoad/StartValue', 1600, 5, 100000 ], 'acloadstop': [ '/Settings/Generator0/AcLoad/StopValue', 800, 0, 100000 ], 'acloadstarttimer': [ '/Settings/Generator0/AcLoad/StartTimer', 20, 0, 10000 ], 'acloadstoptimer': [ '/Settings/Generator0/AcLoad/StopTimer', 20, 0, 10000 ], 'qh_acloadstart': [ '/Settings/Generator0/AcLoad/QuietHoursStartValue', 1900, 0, 100000 ], 'qh_acloadstop': [ '/Settings/Generator0/AcLoad/QuietHoursStopValue', 1200, 0, 100000 ], # VE.Bus high temperature 'inverterhightempenabled': [ '/Settings/Generator0/InverterHighTemp/Enabled', 0, 0, 1 ], 'inverterhightempstarttimer': [ '/Settings/Generator0/InverterHighTemp/StartTimer', 20, 0, 10000 ], 'inverterhightempstoptimer': [ '/Settings/Generator0/InverterHighTemp/StopTimer', 20, 0, 10000 ], # VE.Bus overload 'inverteroverloadenabled': [ '/Settings/Generator0/InverterOverload/Enabled', 0, 0, 1 ], 'inverteroverloadstarttimer': [ '/Settings/Generator0/InverterOverload/StartTimer', 20, 0, 10000 ], 'inverteroverloadstoptimer': [ '/Settings/Generator0/InverterOverload/StopTimer', 20, 0, 10000 ], # TestRun 'testrunenabled': [ '/Settings/Generator0/TestRun/Enabled', 0, 0, 1 ], 'testrunstartdate': [ '/Settings/Generator0/TestRun/StartDate', time.time(), 0, 10000000000.1 ], 'testrunstarttimer': [ '/Settings/Generator0/TestRun/StartTime', 54000, 0, 86400 ], 'testruninterval': [ '/Settings/Generator0/TestRun/Interval', 28, 1, 365 ], 'testrunruntime': [ '/Settings/Generator0/TestRun/Duration', 7200, 1, 86400 ], 'testrunskipruntime': [ '/Settings/Generator0/TestRun/SkipRuntime', 0, 0, 100000 ], 'testruntillbatteryfull': [ '/Settings/Generator0/TestRun/RunTillBatteryFull', 0, 0, 1 ] }, eventCallback=self._handle_changed_setting) # Whenever services come or go, we need to check if it was a service we use. Note that this # is a bit double: DbusMonitor does the same thing. But since we don't use DbusMonitor to # monitor for com.victronenergy.battery, .vebus, .charger or any other possible source of # battery data, it is necessary to monitor for changes in the available dbus services. self._bus.add_signal_receiver(self._dbus_name_owner_changed, signal_name='NameOwnerChanged') self._evaluate_if_we_are_needed() gobject.timeout_add(1000, self._handletimertick) self._update_relay() self._changed = True def _evaluate_if_we_are_needed(self): if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 1: if not self._relay_state_import: logger.info('Getting relay from systemcalc.') try: self._relay_state_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.system', path='/Relay/0/State', eventCallback=None, createsignal=True) except dbus.exceptions.DBusException: logger.info('Systemcalc relay not available.') self._relay_state_import = None pass if self._dbusservice is None: logger.info( 'Action! Going on dbus and taking control of the relay.') relay_polarity_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.settings', path='/Settings/Relay/Polarity', eventCallback=None, createsignal=True) # As is not possible to keep the relay state during the CCGX power cycles, # set the relay polarity to normally open. if relay_polarity_import.get_value() == 1: relay_polarity_import.set_value(0) logger.info('Setting relay polarity to normally open.') # put ourselves on the dbus self._dbusservice = VeDbusService( 'com.victronenergy.generator.startstop0') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareversion, connection='generator', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # State: None = invalid, 0 = stopped, 1 = running self._dbusservice.add_path('/State', value=0) # Condition that made the generator start self._dbusservice.add_path('/RunningByCondition', value='') # Runtime self._dbusservice.add_path('/Runtime', value=0, gettextcallback=self._gettext) # Today runtime self._dbusservice.add_path('/TodayRuntime', value=0, gettextcallback=self._gettext) # Test run runtime self._dbusservice.add_path( '/TestRunIntervalRuntime', value=self._interval_runtime( self._settings['testruninterval']), gettextcallback=self._gettext) # Next tes trun date, values is 0 for test run disabled self._dbusservice.add_path('/NextTestRun', value=None, gettextcallback=self._gettext) # Next tes trun is needed 1, not needed 0 self._dbusservice.add_path('/SkipTestRun', value=None) # Manual start self._dbusservice.add_path('/ManualStart', value=0, writeable=True) # Manual start timer self._dbusservice.add_path('/ManualStartTimer', value=0, writeable=True) # Silent mode active self._dbusservice.add_path('/QuietHours', value=0) self._determineservices() else: if self._dbusservice is not None: self._stop_generator() self._dbusservice.__del__() self._dbusservice = None # Reset conditions for condition in self._condition_stack: self._reset_condition(self._condition_stack[condition]) logger.info( 'Relay function is no longer set to generator start/stop: made sure generator is off ' + 'and now going off dbus') self._relay_state_import = None def _device_added(self, dbusservicename, instance): self._evaluate_if_we_are_needed() self._determineservices() def _device_removed(self, dbusservicename, instance): self._evaluate_if_we_are_needed() # Relay handling depends on systemcalc, if the service disappears restart # the relay state import if dbusservicename == "com.victronenergy.system": self._relay_state_import = None self._determineservices() def _dbus_value_changed(self, dbusServiceName, dbusPath, options, changes, deviceInstance): if dbusPath == '/AutoSelectedBatteryMeasurement' and self._settings[ 'batterymeasurement'] == 'default': self._determineservices() if dbusPath == '/Settings/Relay/Function': self._evaluate_if_we_are_needed() self._changed = True # Update relay state when polarity is changed if dbusPath == '/Settings/Relay/Polarity': self._update_relay() def _handle_changed_setting(self, setting, oldvalue, newvalue): self._changed = True self._evaluate_if_we_are_needed() if setting == 'batterymeasurement': self._determineservices() # Reset retries and valid if service changes for condition in self._condition_stack: if self._condition_stack[condition]['monitoring'] == 'battery': self._condition_stack[condition]['valid'] = True self._condition_stack[condition]['retries'] = 0 if setting == 'autostart': logger.info('Autostart function %s.' % ('enabled' if newvalue == 1 else 'disabled')) if self._dbusservice is not None and setting == 'testruninterval': self._dbusservice[ '/TestRunIntervalRuntime'] = self._interval_runtime( self._settings['testruninterval']) def _dbus_name_owner_changed(self, name, oldowner, newowner): self._determineservices() def _gettext(self, path, value): if path == '/NextTestRun': # Locale format date d = datetime.datetime.fromtimestamp(value) return d.strftime('%c') elif path in ['/Runtime', '/TestRunIntervalRuntime', '/TodayRuntime']: m, s = divmod(value, 60) h, m = divmod(m, 60) return '%dh, %dm, %ds' % (h, m, s) else: return value def _handletimertick(self): # try catch, to make sure that we kill ourselves on an error. Without this try-catch, there would # be an error written to stdout, and then the timer would not be restarted, resulting in a dead- # lock waiting for manual intervention -> not good! try: if self._dbusservice is not None: self._evaluate_startstop_conditions() self._changed = False except: self._stop_generator() import traceback traceback.print_exc() sys.exit(1) return True def _evaluate_startstop_conditions(self): # Conditions will be evaluated in this order conditions = [ 'soc', 'acload', 'batterycurrent', 'batteryvoltage', 'inverterhightemp', 'inverteroverload' ] start = False runningbycondition = None today = calendar.timegm(datetime.date.today().timetuple()) self._timer_runnning = False values = self._get_updated_values() connection_lost = False self._check_quiet_hours() # New day, register it if self._last_counters_check < today and self._dbusservice[ '/State'] == 0: self._last_counters_check = today self._update_accumulated_time() # Update current and accumulated runtime. if self._dbusservice['/State'] == 1: self._dbusservice['/Runtime'] = int(time.time() - self._starttime) # By performance reasons, accumulated runtime is only updated # once per 10s. When the generator stops is also updated. if self._dbusservice['/Runtime'] - self._last_runtime_update >= 10: self._update_accumulated_time() if self._evaluate_manual_start(): runningbycondition = 'manual' start = True # Autostart conditions will only be evaluated if the autostart functionality is enabled if self._settings['autostart'] == 1: if self._evaluate_testrun_condition(): runningbycondition = 'testrun' start = True # Evaluate value conditions for condition in conditions: start = self._evaluate_condition( self._condition_stack[condition], values[condition]) or start runningbycondition = condition if start and runningbycondition is None else runningbycondition # Connection lost is set to true if the numbear of retries of one or more enabled conditions # >= RETRIES_ON_ERROR if self._condition_stack[condition]['enabled']: connection_lost = self._condition_stack[condition][ 'retries'] >= self.RETRIES_ON_ERROR # If none condition is reached check if connection is lost and start/keep running the generator # depending on '/OnLossCommunication' setting if not start and connection_lost: # Start always if self._settings['onlosscommunication'] == 1: start = True runningbycondition = 'lossofcommunication' # Keep running if generator already started if self._dbusservice['/State'] == 1 and self._settings[ 'onlosscommunication'] == 2: start = True runningbycondition = 'lossofcommunication' if start: self._start_generator(runningbycondition) elif (self._dbusservice['/Runtime'] >= self._settings['minimumruntime'] * 60 or self._dbusservice['/RunningByCondition'] == 'manual'): self._stop_generator() def _reset_condition(self, condition): condition['reached'] = False if condition['timed']: condition['start_timer'] = 0 condition['stop_timer'] = 0 def _check_condition(self, condition, value): name = condition['name'] if self._settings[name + 'enabled'] == 0: if condition['enabled']: condition['enabled'] = False logger.info('Disabling (%s) condition' % name) condition['retries'] = 0 condition['valid'] = True self._reset_condition(condition) return False elif not condition['enabled']: condition['enabled'] = True logger.info('Enabling (%s) condition' % name) if (condition['monitoring'] == 'battery') and (self._settings['batterymeasurement'] == 'nobattery'): return False if value is None and condition['valid']: if condition['retries'] >= self.RETRIES_ON_ERROR: logger.info( 'Error getting (%s) value, skipping evaluation till get a valid value' % name) self._reset_condition(condition) self._comunnication_lost = True condition['valid'] = False else: condition['retries'] += 1 if condition['retries'] == 1 or (condition['retries'] % 10) == 0: logger.info('Error getting (%s) value, retrying(#%i)' % (name, condition['retries'])) return False elif value is not None and not condition['valid']: logger.info('Success getting (%s) value, resuming evaluation' % name) condition['valid'] = True condition['retries'] = 0 # Reset retries if value is valid if value is not None: condition['retries'] = 0 return condition['valid'] def _evaluate_condition(self, condition, value): name = condition['name'] setting = ('qh_' if self._dbusservice['/QuietHours'] == 1 else '') + name startvalue = self._settings[setting + 'start'] if not condition['boolean'] else 1 stopvalue = self._settings[setting + 'stop'] if not condition['boolean'] else 0 # Check if the condition has to be evaluated if not self._check_condition(condition, value): # If generator is started by this condition and value is invalid # wait till RETRIES_ON_ERROR to skip the condition if condition['reached'] and condition[ 'retries'] <= self.RETRIES_ON_ERROR: return True return False # As this is a generic evaluation method, we need to know how to compare the values # first check if start value should be greater than stop value and then compare start_is_greater = startvalue > stopvalue # When the condition is already reached only the stop value can set it to False start = condition['reached'] or ( value >= startvalue if start_is_greater else value <= startvalue) stop = value <= stopvalue if start_is_greater else value >= stopvalue # Timed conditions must start/stop after the condition has been reached for a minimum # time. if condition['timed']: if not condition['reached'] and start: condition['start_timer'] += time.time( ) if condition['start_timer'] == 0 else 0 start = time.time( ) - condition['start_timer'] >= self._settings[name + 'starttimer'] condition['stop_timer'] *= int(not start) self._timer_runnning = True else: condition['start_timer'] = 0 if condition['reached'] and stop: condition['stop_timer'] += time.time( ) if condition['stop_timer'] == 0 else 0 stop = time.time() - condition['stop_timer'] >= self._settings[ name + 'stoptimer'] condition['stop_timer'] *= int(not stop) self._timer_runnning = True else: condition['stop_timer'] = 0 condition['reached'] = start and not stop return condition['reached'] def _evaluate_manual_start(self): if self._dbusservice['/ManualStart'] == 0: if self._dbusservice['/RunningByCondition'] == 'manual': self._dbusservice['/ManualStartTimer'] = 0 return False start = True # If /ManualStartTimer has a value greater than zero will use it to set a stop timer. # If no timer is set, the generator will not stop until the user stops it manually. # Once started by manual start, each evaluation the timer is decreased if self._dbusservice['/ManualStartTimer'] != 0: self._manualstarttimer += time.time( ) if self._manualstarttimer == 0 else 0 self._dbusservice['/ManualStartTimer'] -= int(time.time()) - int( self._manualstarttimer) self._manualstarttimer = time.time() start = self._dbusservice['/ManualStartTimer'] > 0 self._dbusservice['/ManualStart'] = int(start) # Reset if timer is finished self._manualstarttimer *= int(start) self._dbusservice['/ManualStartTimer'] *= int(start) return start def _evaluate_testrun_condition(self): if self._settings['testrunenabled'] == 0: self._dbusservice['/SkipTestRun'] = None self._dbusservice['/NextTestRun'] = None return False today = datetime.date.today() runtillbatteryfull = self._settings['testruntillbatteryfull'] == 1 soc = self._get_updated_values()['soc'] batteryisfull = runtillbatteryfull and soc == 100 try: startdate = datetime.date.fromtimestamp( self._settings['testrunstartdate']) starttime = time.mktime( today.timetuple()) + self._settings['testrunstarttimer'] except ValueError: logger.debug('Invalid dates, skipping testrun') return False # If start date is in the future set as NextTestRun and stop evaluating if startdate > today: self._dbusservice['/NextTestRun'] = time.mktime( startdate.timetuple()) return False start = False # If the accumulated runtime during the tes trun interval is greater than '/TestRunIntervalRuntime' # the tes trun must be skipped needed = (self._settings['testrunskipruntime'] > self._dbusservice['/TestRunIntervalRuntime'] or self._settings['testrunskipruntime'] == 0) self._dbusservice['/SkipTestRun'] = int(not needed) interval = self._settings['testruninterval'] stoptime = (starttime + self._settings['testrunruntime'] ) if not runtillbatteryfull else (starttime + 60) elapseddays = (today - startdate).days mod = elapseddays % interval start = (not bool(mod) and (time.time() >= starttime) and (time.time() <= stoptime)) if runtillbatteryfull: if soc is not None: self._testrun_soc_retries = 0 start = (start or self._dbusservice['/RunningByCondition'] == 'testrun') and not batteryisfull elif self._dbusservice['/RunningByCondition'] == 'testrun': if self._testrun_soc_retries < self.RETRIES_ON_ERROR: self._testrun_soc_retries += 1 start = True if (self._testrun_soc_retries % 10) == 0: logger.info( 'Test run failed to get SOC value, retrying(#%i)' % self._testrun_soc_retries) else: logger.info( 'Failed to get SOC after %i retries, terminating test run condition' % self._testrun_soc_retries) start = False else: start = False if not bool(mod) and (time.time() <= stoptime): self._dbusservice['/NextTestRun'] = starttime else: self._dbusservice['/NextTestRun'] = ( time.mktime( (today + datetime.timedelta(days=interval - mod)).timetuple()) + self._settings['testrunstarttimer']) return start and needed def _check_quiet_hours(self): active = False if self._settings['quiethoursenabled'] == 1: # Seconds after today 00:00 timeinseconds = time.time() - time.mktime( datetime.date.today().timetuple()) quiethoursstart = self._settings['quiethoursstarttime'] quiethoursend = self._settings['quiethoursendtime'] # Check if the current time is between the start time and end time if quiethoursstart < quiethoursend: active = quiethoursstart <= timeinseconds and timeinseconds < quiethoursend else: # End time is lower than start time, example Start: 21:00, end: 08:00 active = not (quiethoursend < timeinseconds and timeinseconds < quiethoursstart) if self._dbusservice['/QuietHours'] == 0 and active: logger.info('Entering to quiet mode') elif self._dbusservice['/QuietHours'] == 1 and not active: logger.info('Leaving secondary quiet mode') self._dbusservice['/QuietHours'] = int(active) return active def _update_accumulated_time(self): seconds = self._dbusservice['/Runtime'] accumulated = seconds - self._last_runtime_update self._settings['accumulatedtotal'] = int( self._settings['accumulatedtotal']) + accumulated # Using calendar to get timestamp in UTC, not local time today_date = str(calendar.timegm(datetime.date.today().timetuple())) # If something goes wrong getting the json string create a new one try: accumulated_days = json.loads(self._settings['accumulateddaily']) except ValueError: accumulated_days = {today_date: 0} if (today_date in accumulated_days): accumulated_days[today_date] += accumulated else: accumulated_days[today_date] = accumulated self._last_runtime_update = seconds # Keep the historical with a maximum of HISTORY_DAYS while len(accumulated_days) > self.HISTORY_DAYS: accumulated_days.pop(min(accumulated_days.keys()), None) # Upadate settings self._settings['accumulateddaily'] = json.dumps(accumulated_days, sort_keys=True) self._dbusservice['/TodayRuntime'] = self._interval_runtime(0) self._dbusservice['/TestRunIntervalRuntime'] = self._interval_runtime( self._settings['testruninterval']) def _interval_runtime(self, days): summ = 0 try: daily_record = json.loads(self._settings['accumulateddaily']) except ValueError: return 0 for i in range(days + 1): previous_day = calendar.timegm( (datetime.date.today() - datetime.timedelta(days=i)).timetuple()) if str(previous_day) in daily_record.keys(): summ += daily_record[str(previous_day)] if str( previous_day) in daily_record.keys() else 0 return summ def _get_updated_values(self): values = { 'batteryvoltage': (self._battery_measurement_voltage_import.get_value() if self._battery_measurement_voltage_import else None), 'batterycurrent': (self._battery_measurement_current_import.get_value() if self._battery_measurement_current_import else None), 'soc': self._battery_measurement_soc_import.get_value() if self._battery_measurement_soc_import else None, 'acload': self._dbusmonitor.get_value('com.victronenergy.system', '/Ac/Consumption/Total/Power'), 'inverterhightemp': (self._vebusservice_high_temperature_import.get_value() if self._vebusservice_high_temperature_import else None), 'inverteroverload': (self._vebusservice_overload_import.get_value() if self._vebusservice_overload_import else None) } if values['batterycurrent']: values['batterycurrent'] *= -1 return values def _determineservices(self): # batterymeasurement is either 'default' or 'com_victronenergy_battery_288/Dc/0'. # In case it is set to default, we use the AutoSelected battery measurement, given by # SystemCalc. batterymeasurement = None batteryservicename = None newbatteryservice = None batteryprefix = "" selectedbattery = self._settings['batterymeasurement'] vebusservice = None if selectedbattery == 'default': batterymeasurement = self._dbusmonitor.get_value( 'com.victronenergy.system', '/AutoSelectedBatteryMeasurement') elif len(selectedbattery.split( "/", 1)) == 2: # Only very basic sanity checking.. batterymeasurement = self._settings['batterymeasurement'] elif selectedbattery == 'nobattery': batterymeasurement = None else: # Exception: unexpected value for batterymeasurement pass if batterymeasurement: batteryprefix = "/" + batterymeasurement.split("/", 1)[1] # Get the current battery servicename if self._battery_measurement_voltage_import: oldservice = ( self._battery_measurement_voltage_import.serviceName + self._battery_measurement_voltage_import.path.replace( "/Voltage", "")) else: oldservice = None if batterymeasurement: try: batteryservicename = VeDbusItemImport( bus=self._bus, serviceName="com.victronenergy.system", path='/ServiceMapping/' + batterymeasurement.split("/", 1)[0], eventCallback=None, createsignal=False) if batteryservicename.get_value(): newbatteryservice = batteryservicename.get_value( ) + batteryprefix except dbus.exceptions.DBusException: pass else: newbatteryservice = None if batteryservicename and batteryservicename.get_value(): self._battery_measurement_available = True logger.info( 'Battery service we need (%s) found! Using it for generator start/stop' % batterymeasurement) try: self._battery_measurement_voltage_import = VeDbusItemImport( bus=self._bus, serviceName=batteryservicename.get_value(), path=batteryprefix + '/Voltage', eventCallback=None, createsignal=True) self._battery_measurement_current_import = VeDbusItemImport( bus=self._bus, serviceName=batteryservicename.get_value(), path=batteryprefix + '/Current', eventCallback=None, createsignal=True) # Exception caused by Matthijs :), we forgot to batteryprefix the Soc during the big path-change... self._battery_measurement_soc_import = VeDbusItemImport( bus=self._bus, serviceName=batteryservicename.get_value(), path='/Soc', eventCallback=None, createsignal=True) except Exception: logger.debug('Error getting battery service!') self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None elif selectedbattery == 'nobattery' and self._battery_measurement_available: logger.info( 'Battery monitoring disabled! Stop evaluating related conditions' ) self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None self._battery_measurement_available = False elif batteryservicename and batteryservicename.get_value( ) is None and self._battery_measurement_available: logger.info( 'Battery service we need (%s) is not available! Stop evaluating related conditions' % batterymeasurement) self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None self._battery_measurement_available = False # Get the default VE.Bus service and import high temperature and overload warnings try: vebusservice = VeDbusItemImport( bus=self._bus, serviceName="com.victronenergy.system", path='/VebusService', eventCallback=None, createsignal=False) if vebusservice.get_value() and ( vebusservice.get_value() != self._vebusservice or not self._vebusservice_available): self._vebusservice = vebusservice.get_value() self._vebusservice_available = True logger.info( 'Vebus service (%s) found! Using it for generator start/stop' % vebusservice.get_value()) self._vebusservice_high_temperature_import = VeDbusItemImport( bus=self._bus, serviceName=vebusservice.get_value(), path='/Alarms/HighTemperature', eventCallback=None, createsignal=True) self._vebusservice_overload_import = VeDbusItemImport( bus=self._bus, serviceName=vebusservice.get_value(), path='/Alarms/Overload', eventCallback=None, createsignal=True) except Exception: logger.info('Error getting Vebus service!') self._vebusservice_available = False self._vebusservice_high_temperature_import = None self._vebusservice_overload_import = None logger.info( 'Vebus service (%s) dissapeared! Stop evaluating related conditions' % self._vebusservice) # Trigger an immediate check of system status self._changed = True def _start_generator(self, condition): if not self._relay_state_import: logger.info( "Relay import not available, can't start generator by %s condition" % condition) return systemcalc_relay_state = 0 state = self._dbusservice['/State'] try: systemcalc_relay_state = self._relay_state_import.get_value() except dbus.exceptions.DBusException: logger.info('Error getting relay state') # This function will start the generator in the case generator not # already running. When differs, the RunningByCondition is updated if state == 0 or systemcalc_relay_state != state: self._dbusservice['/State'] = 1 self._update_relay() self._starttime = time.time() logger.info('Starting generator by %s condition' % condition) elif self._dbusservice['/RunningByCondition'] != condition: logger.info( 'Generator previously running by %s condition is now running by %s condition' % (self._dbusservice['/RunningByCondition'], condition)) self._dbusservice['/RunningByCondition'] = condition def _stop_generator(self): if not self._relay_state_import: logger.info("Relay import not available, can't stop generator") return systemcalc_relay_state = 1 state = self._dbusservice['/State'] try: systemcalc_relay_state = self._relay_state_import.get_value() except dbus.exceptions.DBusException: logger.info('Error getting relay state') if state == 1 or systemcalc_relay_state != state: self._dbusservice['/State'] = 0 self._update_relay() logger.info('Stopping generator that was running by %s condition' % str(self._dbusservice['/RunningByCondition'])) self._dbusservice['/RunningByCondition'] = '' self._update_accumulated_time() self._starttime = 0 self._dbusservice['/Runtime'] = 0 self._dbusservice['/ManualStartTimer'] = 0 self._manualstarttimer = 0 self._last_runtime_update = 0 def _update_relay(self): if not self._relay_state_import: logger.info("Relay import not available") return # Relay polarity 0 = NO, 1 = NC polarity = bool( self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Polarity')) w = int(not polarity) if bool( self._dbusservice['/State']) else int(polarity) try: self._relay_state_import.set_value(dbus.Int32(w, variant_level=1)) except dbus.exceptions.DBusException: logger.info('Error setting relay state')
def setup_vedbus(self, instance): self._dbusservice = VeDbusService("com.victronenergy.battery." + self.port[self.port.rfind('/') + 1:]) logger.debug("%s /DeviceInstance = %d" % ("com.victronenergy.battery." + self.port[self.port.rfind('/') + 1:], instance)) # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path('/Mgmt/ProcessVersion', 'Python ' + platform.python_version()) self._dbusservice.add_path('/Mgmt/Connection', 'Serial ' + self.port) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', instance) self._dbusservice.add_path('/ProductId', 0x0) self._dbusservice.add_path('/ProductName', 'SerialBattery (LTT)') self._dbusservice.add_path('/FirmwareVersion', self.version) self._dbusservice.add_path('/HardwareVersion', self.hardware_version) self._dbusservice.add_path('/Connected', 1) # Create static battery info self._dbusservice.add_path('/Info/BatteryLowVoltage', self.min_battery_voltage, writeable=True) self._dbusservice.add_path('/Info/MaxChargeVoltage', self.max_battery_voltage, writeable=True) self._dbusservice.add_path('/Info/MaxChargeCurrent', MAX_BATTERY_CURRENT, writeable=True) self._dbusservice.add_path('/Info/MaxDischargeCurrent', MAX_BATTERY_DISCHARGE_CURRENT, writeable=True) self._dbusservice.add_path('/System/NrOfCellsPerBattery', self.cell_count, writeable=True) self._dbusservice.add_path('/System/NrOfModulesOnline', 1, writeable=True) self._dbusservice.add_path('/System/NrOfModulesOffline', None, writeable=True) self._dbusservice.add_path('/System/NrOfModulesBlockingCharge', None, writeable=True) self._dbusservice.add_path('/System/NrOfModulesBlockingDischarge', None, writeable=True) # Not used at this stage # self._dbusservice.add_path('/System/MinTemperatureCellId', None, writeable=True) # self._dbusservice.add_path('/System/MaxTemperatureCellId', None, writeable=True) self._dbusservice.add_path('/Capacity', self.capacity, writeable=True) # Create SOC, DC and System items self._dbusservice.add_path('/Soc', None, writeable=True) self._dbusservice.add_path('/Dc/0/Voltage', None, writeable=True) self._dbusservice.add_path('/Dc/0/Current', None, writeable=True) self._dbusservice.add_path('/Dc/0/Power', None, writeable=True) self._dbusservice.add_path('/Dc/0/Temperature', 21.0, writeable=True) # Create battery extras self._dbusservice.add_path('/System/MinCellTemperature', None, writeable=True) self._dbusservice.add_path('/System/MaxCellTemperature', None, writeable=True) self._dbusservice.add_path('/System/MaxCellVoltage', 0.0, writeable=True) self._dbusservice.add_path('/System/MaxVoltageCellId', '', writeable=True) self._dbusservice.add_path('/System/MinCellVoltage', 0.0, writeable=True) self._dbusservice.add_path('/System/MinVoltageCellId', '', writeable=True) self._dbusservice.add_path('/History/ChargeCycles', 0, writeable=True) self._dbusservice.add_path('/Balancing', 0, writeable=True) self._dbusservice.add_path('/Io/AllowToCharge', 0, writeable=True) self._dbusservice.add_path('/Io/AllowToDischarge', 0, writeable=True) # Create the alarms self._dbusservice.add_path('/Alarms/LowVoltage', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighVoltage', 0, writeable=True) self._dbusservice.add_path('/Alarms/LowSoc', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighChargeCurrent', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighDischargeCurrent', 0, writeable=True) self._dbusservice.add_path('/Alarms/CellImbalance', 0, writeable=True) self._dbusservice.add_path('/Alarms/InternalFailure', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighChargeTemperature', 0, writeable=True) self._dbusservice.add_path('/Alarms/LowChargeTemperature', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighTemperature', 0, writeable=True) self._dbusservice.add_path('/Alarms/LowTemperature', 0, writeable=True)
def __init__(self, dbusmonitor_gen=None, dbusservice_gen=None, settings_device_gen=None): self.STATE_IDLE = 0 self.STATE_CHARGING = 1 self.STATE_DISCHARGING = 2 self.BATSERVICE_DEFAULT = 'default' self.BATSERVICE_NOBATTERY = 'nobattery' # Why this dummy? Because DbusMonitor expects these values to be there, even though we don't # need them. So just add some dummy data. This can go away when DbusMonitor is more generic. dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} dbus_tree = { 'com.victronenergy.solarcharger': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy}, 'com.victronenergy.pvinverter': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Ac/L1/Power': dummy, '/Ac/L2/Power': dummy, '/Ac/L3/Power': dummy, '/Position': dummy, '/ProductId': dummy}, 'com.victronenergy.battery': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy, '/Dc/0/Power': dummy, '/Soc': dummy, '/TimeToGo': dummy, '/ConsumedAmphours': dummy}, 'com.victronenergy.vebus' : { '/Ac/ActiveIn/ActiveInput': dummy, '/Ac/ActiveIn/L1/P': dummy, '/Ac/ActiveIn/L2/P': dummy, '/Ac/ActiveIn/L3/P': dummy, '/Ac/Out/L1/P': dummy, '/Ac/Out/L2/P': dummy, '/Ac/Out/L3/P': dummy, '/Hub4/AcPowerSetpoint': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy, '/Dc/0/Power': dummy, '/Soc': dummy}, 'com.victronenergy.charger': { '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy}, 'com.victronenergy.grid' : { '/ProductName': dummy, '/Mgmt/Connection': dummy, '/ProductId' : dummy, '/DeviceType' : dummy, '/Ac/L1/Power': dummy, '/Ac/L2/Power': dummy, '/Ac/L3/Power': dummy}, 'com.victronenergy.genset' : { '/ProductName': dummy, '/Mgmt/Connection': dummy, '/ProductId' : dummy, '/DeviceType' : dummy, '/Ac/L1/Power': dummy, '/Ac/L2/Power': dummy, '/Ac/L3/Power': dummy}, 'com.victronenergy.settings' : { '/Settings/SystemSetup/AcInput1' : dummy, '/Settings/SystemSetup/AcInput2' : dummy} } if dbusmonitor_gen is None: self._dbusmonitor = DbusMonitor(dbus_tree, self._dbus_value_changed, self._device_added, self._device_removed) else: self._dbusmonitor = dbusmonitor_gen(dbus_tree) # Connect to localsettings supported_settings = { 'batteryservice': ['/Settings/SystemSetup/BatteryService', self.BATSERVICE_DEFAULT, 0, 0], 'hasdcsystem': ['/Settings/SystemSetup/HasDcSystem', 0, 0, 1], 'writevebussoc': ['/Settings/SystemSetup/WriteVebusSoc', 0, 0, 1]} if settings_device_gen is None: self._settings = SettingsDevice( bus=dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus(), supportedSettings=supported_settings, eventCallback=self._handlechangedsetting) else: self._settings = settings_device_gen(supported_settings, self._handlechangedsetting) # put ourselves on the dbus if dbusservice_gen is None: self._dbusservice = VeDbusService('com.victronenergy.system') else: self._dbusservice = dbusservice_gen('com.victronenergy.system') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareVersion, connection='data from other dbus processes', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # At this moment, VRM portal ID is the MAC address of the CCGX. Anyhow, it should be string uniquely # identifying the CCGX. self._dbusservice.add_path('/Serial', value=get_vrm_portal_id()) self._dbusservice.add_path( '/AvailableBatteryServices', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/AvailableBatteryMeasurements', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/AutoSelectedBatteryService', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/AutoSelectedBatteryMeasurement', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/ActiveBatteryService', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/PvInvertersProductIds', value=None) self._summeditems = { '/Ac/Grid/L1/Power': {'gettext': '%.0F W'}, '/Ac/Grid/L2/Power': {'gettext': '%.0F W'}, '/Ac/Grid/L3/Power': {'gettext': '%.0F W'}, '/Ac/Grid/Total/Power': {'gettext': '%.0F W'}, '/Ac/Grid/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/Grid/ProductId': {'gettext': '%s'}, '/Ac/Grid/DeviceType': {'gettext': '%s'}, '/Ac/Genset/L1/Power': {'gettext': '%.0F W'}, '/Ac/Genset/L2/Power': {'gettext': '%.0F W'}, '/Ac/Genset/L3/Power': {'gettext': '%.0F W'}, '/Ac/Genset/Total/Power': {'gettext': '%.0F W'}, '/Ac/Genset/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/Genset/ProductId': {'gettext': '%s'}, '/Ac/Genset/DeviceType': {'gettext': '%s'}, '/Ac/Consumption/L1/Power': {'gettext': '%.0F W'}, '/Ac/Consumption/L2/Power': {'gettext': '%.0F W'}, '/Ac/Consumption/L3/Power': {'gettext': '%.0F W'}, '/Ac/Consumption/Total/Power': {'gettext': '%.0F W'}, '/Ac/Consumption/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/L1/Power': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/L2/Power': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/L3/Power': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/Total/Power': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/L1/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/L2/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/L3/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/Total/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/PvOnGenset/L1/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGenset/L2/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGenset/L3/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGenset/NumberOfPhases': {'gettext': '%d'}, '/Ac/PvOnGenset/Total/Power': {'gettext': '%.0F W'}, '/Dc/Pv/Power': {'gettext': '%.0F W'}, '/Dc/Pv/Current': {'gettext': '%.1F A'}, '/Dc/Battery/Voltage': {'gettext': '%.2F V'}, '/Dc/Battery/Current': {'gettext': '%.1F A'}, '/Dc/Battery/Power': {'gettext': '%.0F W'}, '/Dc/Battery/Soc': {'gettext': '%.0F %%'}, '/Dc/Battery/State': {'gettext': '%s'}, '/Dc/Battery/TimeToGo': {'gettext': '%.0F s'}, '/Dc/Battery/ConsumedAmphours': {'gettext': '%.1F Ah'}, '/Dc/Charger/Power': {'gettext': '%.0F %%'}, '/Dc/Vebus/Current': {'gettext': '%.1F A'}, '/Dc/Vebus/Power': {'gettext': '%.0F W'}, '/Dc/System/Power': {'gettext': '%.0F W'}, '/Hub': {'gettext': '%s'}, '/Ac/ActiveIn/Source': {'gettext': '%s'}, '/VebusService': {'gettext': '%s'} } for path in self._summeditems.keys(): self._dbusservice.add_path(path, value=None, gettextcallback=self._gettext) self._batteryservice = None self._determinebatteryservice() if self._batteryservice is None: logger.info("Battery service initialized to None (setting == %s)" % self._settings['batteryservice']) self._changed = True for service, instance in self._dbusmonitor.get_service_list().items(): self._device_added(service, instance, do_service_change=False) self._handleservicechange() self._updatevalues() self._writeVebusSocCounter = 9 gobject.timeout_add(1000, exit_on_error, self._handletimertick)
def main(argv): global dbusObjects print __file__ + " starting up" # Have a mainloop, so we can send/receive asynchronous calls to and from dbus DBusGMainLoop(set_as_default=True) # Put ourselves on to the dbus dbusservice = VeDbusService('com.victronenergy.example') # Most simple and short way to add an object with an initial value of 5. dbusservice.add_path('/Position', value=5) # Most advanced wayt to add a path dbusservice.add_path('/RPM', value=100, description='RPM setpoint', writeable=True, onchangecallback=validate_new_value, gettextcallback=get_text_for_rpm) # You can access the paths as if the dbusservice is a dictionary print('/Position value is %s' % dbusservice['/Position']) # Same for changing it dbusservice['/Position'] = 10 print('/Position value is now %s' % dbusservice['/Position']) # To invalidate a value (see com.victronenergy.BusItem specs for definition of invalid), set to None dbusservice['/Position'] = None dbusservice.add_path('/String', 'this is a string') dbusservice.add_path('/Int', 0) dbusservice.add_path('/NegativeInt', -10) dbusservice.add_path('/Float', 1.5) print( 'try changing our RPM by executing the following command from a terminal\n' ) print( 'dbus-send --print-reply --dest=com.victronenergy.example /RPM com.victronenergy.BusItem.SetValue int32:1200' ) print( 'Reply will be <> 0 for values > 1000: not accepted. And reply will be 0 for values < 1000: accepted.' ) mainloop = gobject.MainLoop() mainloop.run()
def main(): global dbusservice global kwhdeltas # Argument parsing parser = argparse.ArgumentParser( description= 'kwhcounters aggregrates information from vebus system, solar chargers, etc. and' + 'calculates system kWh\n' ) parser.add_argument("-d", "--debug", help="set logging level to debug", action="store_true") args = parser.parse_args() # Init logging logging.basicConfig(level=(logging.DEBUG if args.debug else logging.INFO)) logging.info("%s v%s is starting up" % (__file__, softwareversion)) logLevel = {0: 'NOTSET', 10: 'DEBUG', 20: 'INFO', 30: 'WARNING', 40: 'ERROR'} logging.info('Loglevel set to ' + logLevel[logging.getLogger().getEffectiveLevel()]) # Have a mainloop, so we can send/receive asynchronous calls to and from dbus DBusGMainLoop(set_as_default=True) # Publish ourselves on the dbus dbusservice = VeDbusService("com.victronenergy.kwhcounters.s0") dbusservice.add_path('/GridToBattery', value=None) dbusservice.add_path('/GridToConsumers', value=None) dbusservice.add_path('/GensetToConsumers', value=None) dbusservice.add_path('/GensetToBattery', value=None) dbusservice.add_path('/PvToBattery', value=None) dbusservice.add_path('/PvToGrid', value=None) dbusservice.add_path('/PvToConsumers', value=None) dbusservice.add_path('/BatteryToConsumers', value=None) dbusservice.add_path('/BatteryToGrid', value=None) kwhdeltas = KwhDeltas() gobject.timeout_add(1000, kwhdeltas.getdeltas) # Start and run the mainloop logging.info("Starting mainloop, responding on only events from now on. Press ctrl-Z to see the deltas") mainloop = gobject.MainLoop() mainloop.run()
parser.add_argument("-d", "--debug", help="set logging level to debug", action="store_true") args = parser.parse_args() # Init logging logging.basicConfig(level=(logging.DEBUG if args.debug else logging.INFO)) logging.info(__file__ + " is starting up") logLevel = {0: 'NOTSET', 10: 'DEBUG', 20: 'INFO', 30: 'WARNING', 40: 'ERROR'} logging.info('Loglevel set to ' + logLevel[logging.getLogger().getEffectiveLevel()]) # Have a mainloop, so we can send/receive asynchronous calls to and from dbus DBusGMainLoop(set_as_default=True) dbusservice = VeDbusService(args.name) logging.info("using device instance %s" % args.deviceinstance) # Create the management objects, as specified in the ccgx dbus-api document dbusservice.add_path('/Management/ProcessName', __file__) dbusservice.add_path('/Management/ProcessVersion', 'Unkown version, and running on Python ' + platform.python_version()) dbusservice.add_path('/Management/Connection', 'Data taken from mk2dbus') # Create the mandatory objects dbusservice.add_path('/DeviceInstance', args.deviceinstance) dbusservice.add_path('/ProductId', 0) dbusservice.add_path('/ProductName', 'PV Inverter on Output') dbusservice.add_path('/FirmwareVersion', 0) dbusservice.add_path('/HardwareVersion', 0) dbusservice.add_path('/Connected', 1)
def main(): global mainloop global start start = datetime.now() parser = ArgumentParser(description=NAME, add_help=True) parser.add_argument('-d', '--debug', help='enable debug logging', action='store_true') parser.add_argument('-s', '--serial', help='tty') args = parser.parse_args() logging.basicConfig(format='%(levelname)-8s %(message)s', level=(logging.DEBUG if args.debug else logging.INFO)) logLevel = { 0: 'NOTSET', 10: 'DEBUG', 20: 'INFO', 30: 'WARNING', 40: 'ERROR', } log.info('Loglevel set to ' + logLevel[log.getEffectiveLevel()]) if not args.serial: log.error('No serial port specified, see -h') exit(1) rate = 19200 gpio_base = find_gpio_base(os.path.basename(args.serial)) if gpio_base == None: log.error('GPIO not found') exit(1) log.info('Starting %s %s on %s at %d bps, GPIO base %d' % (NAME, VERSION, args.serial, rate, gpio_base)) gobject.threads_init() dbus.mainloop.glib.threads_init() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) mainloop = gobject.MainLoop() svc = VeDbusService('com.victronenergy.hiber') svc.add_path('/Model', None) svc.add_path('/ModemNumber', None) svc.add_path('/Firmware', None) svc.add_path('/NextAlarm', None) svc.add_path('/NextPass', None) hiber = Hiber(svc, args.serial, rate, gpio_base) hiber.start() gobject.timeout_add(WAKEUP_INTERVAL * 1000, hiber.update_modem) gobject.timeout_add(5000, hiber.update_watchdog) mainloop.run() quit(1)
class DbusGenerator: def __init__(self): self.RELAY_GPIO_FILE = '/sys/class/gpio/gpio182/value' self.SERVICE_NOBATTERY = 'nobattery' self.SERVICE_NOVEBUS = 'novebus' self.HISTORY_DAYS = 30 self._last_counters_check = 0 self._dbusservice = None self._batteryservice = None self._vebusservice = None self._starttime = 0 self._manualstarttimer = 0 self._last_runtime_update = 0 self.timer_runnning = 0 self._condition_stack = { 'batteryvoltage': { 'name': 'batteryvoltage', 'reached': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False }, 'batterycurrent': { 'name': 'batterycurrent', 'reached': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False }, 'acload': { 'name': 'acload', 'reached': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False }, 'soc': { 'name': 'soc', 'reached': False, 'timed': False, 'valid': True, 'enabled': False } } # DbusMonitor expects these values to be there, even though we don need them. So just # add some dummy data. This can go away when DbusMonitor is more generic. dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} self._dbusmonitor = DbusMonitor({ 'com.victronenergy.vebus': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/State': dummy, '/Ac/Out/P': dummy, '/Dc/I': dummy, '/Dc/V': dummy, '/Soc': dummy }, 'com.victronenergy.battery': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/V': dummy, '/Dc/0/I': dummy, '/Dc/0/P': dummy, '/Soc': dummy }, 'com.victronenergy.settings': { # This is not our setting so do it here. not in supportedSettings '/Settings/Relay/Function': dummy, '/Settings/Relay/Polarity': dummy, '/Settings/System/TimeZone': dummy} }, self._dbus_value_changed, self._device_added, self._device_removed) # Set timezone to user selected timezone environ['TZ'] = self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/System/TimeZone') # Connect to localsettings self._settings = SettingsDevice( bus=dbus.SystemBus() if (platform.machine() == 'armv7l') else dbus.SessionBus(), supportedSettings={ 'autostart': ['/Settings/Generator/AutoStart', 0, 0, 1], 'accumulateddaily': ['/Settings/Generator/AccumulatedDaily', '', 0, 0], 'accumulatedtotal': ['/Settings/Generator/AccumulatedTotal', 0, 0, 0], 'batteryservice': ['/Settings/Generator/BatteryService', self.SERVICE_NOBATTERY, 0, 0], 'vebusservice': ['/Settings/Generator/VebusService', self.SERVICE_NOVEBUS, 0, 0], # Silent mode 'silentmodeenabled': ['/Settings/Generator/SilentMode/Enabled', 0, 0, 1], 'silentmodestarttimer': ['/Settings/Generator/SilentMode/StartTime', 0, 0, 86400], 'silentmodeendtime': ['/Settings/Generator/SilentMode/EndTime', 0, 0, 86400], # SOC 'socenabled': ['/Settings/Generator/Soc/Enabled', 0, 0, 1], 'socstart': ['/Settings/Generator/Soc/StartValue', 90, 0, 100], 'socstop': ['/Settings/Generator/Soc/StopValue', 90, 0, 100], 'em_socstart': ['/Settings/Generator/Soc/EmergencyStartValue', 90, 0, 100], 'em_socstop': ['/Settings/Generator/Soc/EmergencyStopValue', 90, 0, 100], # Voltage 'batteryvoltageenabled': ['/Settings/Generator/BatteryVoltage/Enabled', 0, 0, 1], 'batteryvoltagestart': ['/Settings/Generator/BatteryVoltage/StartValue', 11.5, 0, 150], 'batteryvoltagestop': ['/Settings/Generator/BatteryVoltage/StopValue', 12.4, 0, 150], 'batteryvoltagestarttimer': ['/Settings/Generator/BatteryVoltage/StartTimer', 20, 0, 10000], 'batteryvoltagestoptimer': ['/Settings/Generator/BatteryVoltage/StopTimer', 20, 0, 10000], 'em_batteryvoltagestart': ['/Settings/Generator/BatteryVoltage/EmergencyStartValue', 11.9, 0, 100], 'em_batteryvoltagestop': ['/Settings/Generator/BatteryVoltage/EmergencyStopValue', 12.4, 0, 100], # Current 'batterycurrentenabled': ['/Settings/Generator/BatteryCurrent/Enabled', 0, 0, 1], 'batterycurrentstart': ['/Settings/Generator/BatteryCurrent/StartValue', 10.5, 0.5, 1000], 'batterycurrentstop': ['/Settings/Generator/BatteryCurrent/StopValue', 5.5, 0, 1000], 'batterycurrentstarttimer': ['/Settings/Generator/BatteryCurrent/StartTimer', 20, 0, 10000], 'batterycurrentstoptimer': ['/Settings/Generator/BatteryCurrent/StopTimer', 20, 0, 10000], 'em_batterycurrentstart': ['/Settings/Generator/BatteryCurrent/EmergencyStartValue', 20.5, 0, 1000], 'em_batterycurrentstop': ['/Settings/Generator/BatteryCurrent/EmergencyStopValue', 15.5, 0, 1000], # AC load 'acloadenabled': ['/Settings/Generator/AcLoad/Enabled', 0, 0, 1], 'acloadstart': ['/Settings/Generator/AcLoad/StartValue', 1600, 5, 100000], 'acloadstop': ['/Settings/Generator/AcLoad/StopValue', 800, 0, 100000], 'acloadstarttimer': ['/Settings/Generator/AcLoad/StartTimer', 20, 0, 10000], 'acloadstoptimer': ['/Settings/Generator/AcLoad/StopTimer', 20, 0, 10000], 'em_acloadstart': ['/Settings/Generator/AcLoad/EmergencyStartValue', 1900, 0, 100000], 'em_acloadstop': ['/Settings/Generator/AcLoad/EmergencyStopValue', 1200, 0, 100000], # Maintenance 'maintenanceenabled': ['/Settings/Generator/Maintenance/Enabled', 0, 0, 1], 'maintenancestartdate': ['/Settings/Generator/Maintenance/StartDate', time.time(), 0, 10000000000.1], 'maintenancestarttimer': ['/Settings/Generator/Maintenance/StartTime', 54000, 0, 86400], 'maintenanceinterval': ['/Settings/Generator/Maintenance/Interval', 28, 1, 365], 'maintenanceruntime': ['/Settings/Generator/Maintenance/Duration', 7200, 1, 86400], 'maintenanceskipruntime': ['/Settings/Generator/Maintenance/SkipRuntime', 0, 0, 100000] }, eventCallback=self._handle_changed_setting) self._evaluate_if_we_are_needed() gobject.timeout_add(1000, self._handletimertick) self._changed = True def _evaluate_if_we_are_needed(self): if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 1: if self._dbusservice is None: logger.info('Action! Going on dbus and taking control of the relay.') # put ourselves on the dbus self._dbusservice = VeDbusService('com.victronenergy.generator.startstop0') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareversion, connection='generator', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # State: None = invalid, 0 = stopped, 1 = running self._dbusservice.add_path('/State', value=0) # Condition that made the generator start self._dbusservice.add_path('/RunningByCondition', value='') # Runtime self._dbusservice.add_path('/Runtime', value=0, gettextcallback=self._gettext) # Today runtime self._dbusservice.add_path('/TodayRuntime', value=0, gettextcallback=self._gettext) # Maintenance runtime self._dbusservice.add_path('/MaintenanceIntervalRuntime', value=self._interval_runtime(self._settings['maintenanceinterval']), gettextcallback=self._gettext) # Next maintenance date, values is 0 for maintenande disabled self._dbusservice.add_path('/NextMaintenance', value=None, gettextcallback=self._gettext) # Next maintenance is needed 1, not needed 0 self._dbusservice.add_path('/SkipMaintenance', value=None) # Manual start self._dbusservice.add_path('/ManualStart', value=0, writeable=True) # Manual start timer self._dbusservice.add_path('/ManualStartTimer', value=0, writeable=True) # Silent mode active self._dbusservice.add_path('/SilentMode', value=0) # Battery services self._dbusservice.add_path('/AvailableBatteryServices', value=None) # Vebus services self._dbusservice.add_path('/AvailableVebusServices', value=None) # As the user can select the vebus service and is not yet possible to get the servie name from the gui # we need to provide it self._dbusservice.add_path('/VebusServiceName', value=None) self._determineservices() self._batteryservice = None self._vebusservice = None self._populate_services_list() self._determineservices() if self._batteryservice is not None: logger.info('Battery service we need (%s) found! Using it for generator start/stop' % self._get_service_path(self._settings['batteryservice'])) elif self._vebusservice is not None: logger.info('VE.Bus service we need (%s) found! Using it for generator start/stop' % self._get_service_path(self._settings['vebusservice'])) else: self._populate_services_list() self._determineservices() else: if self._dbusservice is not None: self._stop_generator() self._batteryservice = None self._vebusservice = None self._dbusservice.__del__() self._dbusservice = None # Reset conditions for condition in self._condition_stack: self._reset_condition(self._condition_stack[condition]) logger.info('Relay function is no longer set to generator start/stop: made sure generator is off ' + 'and now going off dbus') def _device_added(self, dbusservicename, instance): self._evaluate_if_we_are_needed() def _device_removed(self, dbusservicename, instance): self._evaluate_if_we_are_needed() def _dbus_value_changed(self, dbusServiceName, dbusPath, options, changes, deviceInstance): self._evaluate_if_we_are_needed() self._changed = True # Update relay state when polarity is changed if dbusPath == '/Settings/Relay/Polarity': self._update_relay() def _handle_changed_setting(self, setting, oldvalue, newvalue): self._changed = True self._evaluate_if_we_are_needed() if setting == 'Polarity': self._update_relay() if self._dbusservice is not None and setting == 'maintenanceinterval': self._dbusservice['/MaintenanceIntervalRuntime'] = self._interval_runtime( self._settings['maintenanceinterval']) def _gettext(self, path, value): if path == '/NextMaintenance': # Locale format date d = datetime.datetime.fromtimestamp(value) return d.strftime('%c') elif path in ['/Runtime', '/MaintenanceIntervalRuntime', '/TodayRuntime']: m, s = divmod(value, 60) h, m = divmod(m, 60) return '%dh, %dm, %ds' % (h, m, s) else: return value def _handletimertick(self): # try catch, to make sure that we kill ourselves on an error. Without this try-catch, there would # be an error written to stdout, and then the timer would not be restarted, resulting in a dead- # lock waiting for manual intervention -> not good! # To keep accuracy, conditions will forced to be evaluated each second when the generator or a timer is running try: if self._dbusservice is not None and (self._changed or self._dbusservice['/State'] == 1 or self._dbusservice['/ManualStart'] == 1 or self.timer_runnning): self._evaluate_startstop_conditions() self._changed = False except: self._stop_generator() import traceback traceback.print_exc() sys.exit(1) return True def _evaluate_startstop_conditions(self): # Conditions will be evaluated in this order conditions = ['soc', 'acload', 'batterycurrent', 'batteryvoltage'] start = False runningbycondition = None today = calendar.timegm(datetime.date.today().timetuple()) self.timer_runnning = False values = self._get_updated_values() self._check_silent_mode() # New day, register it if self._last_counters_check < today and self._dbusservice['/State'] == 0: self._last_counters_check = today self._update_accumulated_time() # Update current and accumulated runtime. if self._dbusservice['/State'] == 1: self._dbusservice['/Runtime'] = int(time.time() - self._starttime) # By performance reasons, accumulated runtime is onle updated # once per 10s. When the generator stops is also updated. if self._dbusservice['/Runtime'] - self._last_runtime_update >= 10: self._update_accumulated_time() if self._evaluate_manual_start(): runningbycondition = 'manual' start = True # Evaluate value conditions for condition in conditions: start = self._evaluate_condition(self._condition_stack[condition], values[condition]) or start runningbycondition = condition if start and runningbycondition is None else runningbycondition if self._evaluate_maintenance_condition() and not start: runningbycondition = 'maintenance' start = True if start: self._start_generator(runningbycondition) else: self._stop_generator() def _reset_condition(self, condition): condition['reached'] = False if condition['timed']: condition['start_timer'] = 0 condition['stop_timer'] = 0 def _check_condition(self, condition, value): name = condition['name'] if self._settings[name + 'enabled'] == 0: if condition['enabled']: condition['enabled'] = False logger.info('Disabling (%s) condition' % name) self._reset_condition(condition) return False elif not condition['enabled']: condition['enabled'] = True logger.info('Enabling (%s) condition' % name) if value is None and condition['valid']: logger.info('Error getting (%s) value, skipping evaluation till get a valid value' % name) self._reset_condition(condition) condition['valid'] = False return False elif value is not None and not condition['valid']: logger.info('Success getting (%s) value, resuming evaluation' % name) condition['valid'] = True return condition['valid'] def _evaluate_condition(self, condition, value): name = condition['name'] setting = ('em_' if self._dbusservice['/SilentMode'] == 1 else '') + name startvalue = self._settings[setting + 'start'] stopvalue = self._settings[setting + 'stop'] # Check if the have to be evaluated if not self._check_condition(condition, value): return False # As this is a generic evaluation method, we need to know how to compare the values # first check if start value should be greater than stop value and then compare start_is_greater = startvalue > stopvalue # When the condition is already reached only the stop value can set it to False start = condition['reached'] or (value >= startvalue if start_is_greater else value <= startvalue) stop = value <= stopvalue if start_is_greater else value >= stopvalue # Timed conditions must start/stop after the condition has been reached for a minimum # time. if condition['timed']: if not condition['reached'] and start: condition['start_timer'] += time.time() if condition['start_timer'] == 0 else 0 start = time.time() - condition['start_timer'] >= self._settings[name + 'starttimer'] condition['stop_timer'] *= int(not start) self.timer_runnning = True else: condition['start_timer'] = 0 if condition['reached'] and stop: condition['stop_timer'] += time.time() if condition['stop_timer'] == 0 else 0 stop = time.time() - condition['stop_timer'] >= self._settings[name + 'stoptimer'] condition['stop_timer'] *= int(not stop) self.timer_runnning = True else: condition['stop_timer'] = 0 condition['reached'] = start and not stop return condition['reached'] def _evaluate_manual_start(self): if self._dbusservice['/ManualStart'] == 0: return False start = True # If /ManualStartTimer has a value greater than zero will use it to set a stop timer. # If no timer is set, the generator will not stop until the user stops it manually. # Once started by manual start, each evaluation the timer is decreased if self._dbusservice['/ManualStartTimer'] != 0: self._manualstarttimer += time.time() if self._manualstarttimer == 0 else 0 self._dbusservice['/ManualStartTimer'] -= int(time.time()) - int(self._manualstarttimer) self._manualstarttimer = time.time() start = self._dbusservice['/ManualStartTimer'] > 0 self._dbusservice['/ManualStart'] = int(start) # Reset if timer is finished self._manualstarttimer *= int(start) self._dbusservice['/ManualStartTimer'] *= int(start) return start def _evaluate_maintenance_condition(self): if self._settings['maintenanceenabled'] == 0: self._dbusservice['/SkipMaintenance'] = None self._dbusservice['/NextMaintenance'] = None return False today = datetime.date.today() try: startdate = datetime.date.fromtimestamp(self._settings['maintenancestartdate']) starttime = time.mktime(today.timetuple()) + self._settings['maintenancestarttimer'] except ValueError: logger.debug('Invalid dates, skipping maintenance') return False # If start date is in the future set as NextMaintenance and stop evaluating if startdate > today: self._dbusservice['/NextMaintenance'] = time.mktime(startdate.timetuple()) return False start = False # If the accumulated runtime during the maintenance interval is greater than '/MaintenanceIntervalRuntime' # the maintenance must be skipped needed = (self._settings['maintenanceskipruntime'] > self._dbusservice['/MaintenanceIntervalRuntime'] or self._settings['maintenanceskipruntime'] == 0) self._dbusservice['/SkipMaintenance'] = int(not needed) interval = self._settings['maintenanceinterval'] stoptime = starttime + self._settings['maintenanceruntime'] elapseddays = (today - startdate).days mod = elapseddays % interval start = (not bool(mod) and (time.time() >= starttime) and (time.time() <= stoptime)) if not bool(mod) and (time.time() <= stoptime): self._dbusservice['/NextMaintenance'] = starttime else: self._dbusservice['/NextMaintenance'] = (time.mktime((today + datetime.timedelta(days=interval - mod)).timetuple()) + self._settings['maintenancestarttimer']) return start and needed def _check_silent_mode(self): active = False if self._settings['silentmodeenabled'] == 1: # Seconds after today 00:00 timeinseconds = time.time() - time.mktime(datetime.date.today().timetuple()) silentmodestart = self._settings['silentmodestarttimer'] silentmodeend = self._settings['silentmodeendtime'] # Check if the current time is between the start time and end time if silentmodestart < silentmodeend: active = silentmodestart <= timeinseconds and timeinseconds < silentmodeend else: # End time is lower than start time, example Start: 21:00, end: 08:00 active = not (silentmodeend < timeinseconds and timeinseconds < silentmodestart) if self._dbusservice['/SilentMode'] == 0 and active: logger.info('Entering silent mode, only emergency values will be evaluated') elif self._dbusservice['/SilentMode'] == 1 and not active: logger.info('Leaving silent mode') self._dbusservice['/SilentMode'] = int(active) return active def _update_accumulated_time(self): seconds = self._dbusservice['/Runtime'] accumulated = seconds - self._last_runtime_update self._settings['accumulatedtotal'] = int(self._settings['accumulatedtotal']) + accumulated # Using calendar to get timestamp in UTC, not local time today_date = str(calendar.timegm(datetime.date.today().timetuple())) # If something goes wrong getting the json string create a new one try: accumulated_days = json.loads(self._settings['accumulateddaily']) except ValueError: accumulated_days = {today_date: 0} if (today_date in accumulated_days): accumulated_days[today_date] += accumulated else: accumulated_days[today_date] = accumulated self._last_runtime_update = seconds # Keep the historical with a maximum of HISTORY_DAYS while len(accumulated_days) > self.HISTORY_DAYS: accumulated_days.pop(min(accumulated_days.keys()), None) # Upadate settings self._settings['accumulateddaily'] = json.dumps(accumulated_days, sort_keys=True) self._dbusservice['/TodayRuntime'] = self._interval_runtime(0) self._dbusservice['/MaintenanceIntervalRuntime'] = self._interval_runtime(self._settings['maintenanceinterval']) def _interval_runtime(self, days): summ = 0 try: daily_record = json.loads(self._settings['accumulateddaily']) except ValueError: return 0 for i in range(days + 1): previous_day = calendar.timegm((datetime.date.today() - datetime.timedelta(days=i)).timetuple()) if str(previous_day) in daily_record.keys(): summ += daily_record[str(previous_day)] if str(previous_day) in daily_record.keys() else 0 return summ def _get_updated_values(self): values = { 'batteryvoltage': None, 'batterycurrent': None, 'soc': None, 'acload': None } # Update values from battery monitor if self._batteryservice is not None: batteryservicetype = self._batteryservice.split('.')[2] values['soc'] = self._dbusmonitor.get_value(self._batteryservice, '/Soc') if batteryservicetype == 'battery': values['batteryvoltage'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/V') values['batterycurrent'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/I') * -1 elif batteryservicetype == 'vebus': values['batteryvoltage'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/V') values['batterycurrent'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/I') * -1 if self._vebusservice is not None: values['acload'] = self._dbusmonitor.get_value(self._vebusservice, '/Ac/Out/P') return values def _populate_services_list(self): vebusservices = self._dbusmonitor.get_service_list('com.victronenergy.vebus') batteryservices = self._dbusmonitor.get_service_list('com.victronenergy.battery') self._remove_unconnected_services(vebusservices) # User can set a vebus as battery monitor, add the option batteryservices.update(vebusservices) vebus = {self.SERVICE_NOVEBUS: 'None'} battery = {self.SERVICE_NOBATTERY: 'None'} for servicename, instance in vebusservices.items(): key = '%s/%s' % ('.'.join(servicename.split('.')[0:3]), instance) vebus[key] = self._get_readable_service_name(servicename) for servicename, instance in batteryservices.items(): key = '%s/%s' % ('.'.join(servicename.split('.')[0:3]), instance) battery[key] = self._get_readable_service_name(servicename) self._dbusservice['/AvailableBatteryServices'] = json.dumps(battery) self._dbusservice['/AvailableVebusServices'] = json.dumps(vebus) def _determineservices(self): vebusservice = self._settings['vebusservice'] batteryservice = self._settings['batteryservice'] if batteryservice != self.SERVICE_NOBATTERY and batteryservice != '': self._batteryservice = self._get_service_path(batteryservice) else: self._batteryservice = None if vebusservice != self.SERVICE_NOVEBUS and vebusservice != '': self._vebusservice = self._get_service_path(vebusservice) self._dbusservice['/VebusServiceName'] = self._vebusservice else: self._vebusservice = None self._changed = True def _get_readable_service_name(self, servicename): return (self._dbusmonitor.get_value(servicename, '/ProductName') + ' on ' + self._dbusmonitor.get_value(servicename, '/Mgmt/Connection')) def _remove_unconnected_services(self, services): # Workaround: because com.victronenergy.vebus is available even when there is no vebus product # connected. Remove any that is not connected. For this, we use /State since mandatory path # /Connected is not implemented in mk2dbus. for servicename in services.keys(): if ((servicename.split('.')[2] == 'vebus' and self._dbusmonitor.get_value(servicename, '/State') is None) or self._dbusmonitor.get_value(servicename, '/Connected') != 1 or self._dbusmonitor.get_value(servicename, '/ProductName') is None or self._dbusmonitor.get_value(servicename, '/Mgmt/Connection') is None): del services[servicename] def _get_service_path(self, service): s = service.split('/') assert len(s) == 2, 'The setting (%s) is invalid!' % service serviceclass = s[0] instance = int(s[1]) services = self._dbusmonitor.get_service_list(classfilter=serviceclass) if instance not in services.values(): # Once chosen battery monitor does not exist. Don't auto change the setting (it might come # back). And also don't autoselect another. servicepath = None else: # According to https://www.python.org/dev/peps/pep-3106/, dict.keys() and dict.values() # always have the same order. servicepath = services.keys()[services.values().index(instance)] return servicepath def _start_generator(self, condition): # This function will start the generator in the case generator not # already running. When differs, the RunningByCondition is updated if self._dbusservice['/State'] == 0: self._dbusservice['/State'] = 1 self._update_relay() self._starttime = time.time() logger.info('Starting generator by %s condition' % condition) elif self._dbusservice['/RunningByCondition'] != condition: logger.info('Generator previously running by %s condition is now running by %s condition' % (self._dbusservice['/RunningByCondition'], condition)) self._dbusservice['/RunningByCondition'] = condition def _stop_generator(self): if self._dbusservice['/State'] == 1: self._dbusservice['/State'] = 0 self._update_relay() logger.info('Stopping generator that was running by %s condition' % str(self._dbusservice['/RunningByCondition'])) self._dbusservice['/RunningByCondition'] = '' self._update_accumulated_time() self._starttime = 0 self._dbusservice['/Runtime'] = 0 self._dbusservice['/ManualStartTimer'] = 0 self._manualstarttimer = 0 self._last_runtime_update = 0 def _update_relay(self): # Relay polarity 0 = NO, 1 = NC polarity = bool(self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Polarity')) w = int(not polarity) if bool(self._dbusservice['/State']) else int(polarity) try: f = open(self.RELAY_GPIO_FILE, 'w') f.write(str(w)) f.close() except IOError: logger.info('Error writting to the relay GPIO file!: %s' % self.RELAY_GPIO_FILE)
def main(argv): global dbusObjects print __file__ + " starting up" # Have a mainloop, so we can send/receive asynchronous calls to and from dbus DBusGMainLoop(set_as_default=True) # Put ourselves on to the dbus dbusservice = VeDbusService("com.victronenergy.example") # Most simple and short way to add an object with an initial value of 5. dbusservice.add_path("/Position", value=5) # Most advanced wayt to add a path dbusservice.add_path( "/RPM", value=100, description="RPM setpoint", writeable=True, onchangecallback=validate_new_value, gettextcallback=get_text_for_rpm, ) # You can access the paths as if the dbusservice is a dictionary print ("/Position value is %s" % dbusservice["/Position"]) # Same for changing it dbusservice["/Position"] = 10 print ("/Position value is now %s" % dbusservice["/Position"]) # To invalidate a value (see com.victronenergy.BusItem specs for definition of invalid), set to None dbusservice["/Position"] = None dbusservice.add_path("/String", "this is a string") dbusservice.add_path("/Int", 0) dbusservice.add_path("/NegativeInt", -10) dbusservice.add_path("/Float", 1.5) print ("try changing our RPM by executing the following command from a terminal\n") print ( "dbus-send --print-reply --dest=com.victronenergy.example /RPM com.victronenergy.BusItem.SetValue int32:1200" ) print ("Reply will be <> 0 for values > 1000: not accepted. And reply will be 0 for values < 1000: accepted.") mainloop = gobject.MainLoop() mainloop.run()
def __init__(self, servicename, deviceinstance, voltage, capacity, productname='Valence U-BMS', connection='can0'): self.minUpdateDone = 0 self.dailyResetDone = 0 self._bat = UbmsBattery(capacity=capacity, voltage=voltage, connection=connection) self._dbusservice = VeDbusService(servicename + '.socketcan_' + connection + '_di' + str(deviceinstance)) logging.debug("%s /DeviceInstance = %d" % (servicename + '.socketcan_' + connection + '_di' + str(deviceinstance), deviceinstance)) # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path( '/Mgmt/ProcessVersion', VERSION + ' running on Python ' + platform.python_version()) self._dbusservice.add_path('/Mgmt/Connection', connection) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', deviceinstance) self._dbusservice.add_path('/ProductId', 0) self._dbusservice.add_path('/ProductName', productname) self._dbusservice.add_path('/FirmwareVersion', 'unknown') self._dbusservice.add_path('/HardwareVersion', 'unknown') self._dbusservice.add_path('/Connected', 0) # Create battery specific objects self._dbusservice.add_path('/Status', 0) self._dbusservice.add_path('/Mode', 1, writeable=True, onchangecallback=self._transmit_mode) self._dbusservice.add_path('/Soh', 100) self._dbusservice.add_path('/Capacity', int(capacity)) self._dbusservice.add_path('/InstalledCapacity', int(capacity)) self._dbusservice.add_path('/Dc/0/Temperature', 25) self._dbusservice.add_path('/Info/MaxChargeCurrent', 70) self._dbusservice.add_path('/Info/MaxDischargeCurrent', 150) self._dbusservice.add_path('/Info/MaxChargeVoltage', float(voltage)) self._dbusservice.add_path('/Info/BatteryLowVoltage', 24.0) self._dbusservice.add_path('/Alarms/CellImbalance', 0) self._dbusservice.add_path('/Alarms/LowVoltage', 0) self._dbusservice.add_path('/Alarms/HighVoltage', 0) self._dbusservice.add_path('/Alarms/HighDischargeCurrent', 0) self._dbusservice.add_path('/Alarms/HighChargeCurrent', 0) self._dbusservice.add_path('/Alarms/LowSoc', 0) self._dbusservice.add_path('/Alarms/LowTemperature', 0) self._dbusservice.add_path('/Alarms/HighTemperature', 0) self._dbusservice.add_path('/Balancing', 0) self._dbusservice.add_path('/System/HasTemperature', 1) self._dbusservice.add_path('/System/NrOfBatteries', 10) self._dbusservice.add_path('/System/NrOfModulesOnline', 10) self._dbusservice.add_path('/System/NrOfModulesOffline', 0) self._dbusservice.add_path('/System/NrOfModulesBlockingDischarge', 0) self._dbusservice.add_path('/System/NrOfModulesBlockingCharge', 0) self._dbusservice.add_path('/System/NrOfBatteriesBalancing', 0) self._dbusservice.add_path('/System/BatteriesParallel', 5) self._dbusservice.add_path('/System/BatteriesSeries', 2) self._dbusservice.add_path('/System/NrOfCellsPerBattery', 4) self._dbusservice.add_path('/System/MinVoltageCellId', 'M_C_') self._dbusservice.add_path('/System/MaxVoltageCellId', 'M_C_') self._dbusservice.add_path('/System/MinCellTemperature', 10.0) self._dbusservice.add_path('/System/MaxCellTemperature', 10.0) self._dbusservice.add_path('/System/MaxPcbTemperature', 10.0) self._settings = SettingsDevice( bus=dbus.SystemBus() if (platform.machine() == 'armv7l') else dbus.SessionBus(), supportedSettings={ 'AvgDischarge': ['/Settings/Ubms/AvgerageDischarge', 0.0, 0, 0], 'TotalAhDrawn': ['/Settings/Ubms/TotalAhDrawn', 0.0, 0, 0], 'TimeLastFull': ['/Settings/Ubms/TimeLastFull', 0.0, 0, 0], 'MinCellVoltage': ['/Settings/Ubms/MinCellVoltage', 4.0, 2.0, 4.0], 'MaxCellVoltage': ['/Settings/Ubms/MaxCellVoltage', 2.0, 2.0, 4.0], 'interval': ['/Settings/Ubms/Interval', 50, 50, 200] }, eventCallback=handle_changed_setting) self._summeditems = { '/System/MaxCellVoltage': { 'gettext': '%.2F V' }, '/System/MinCellVoltage': { 'gettext': '%.2F V' }, '/Dc/0/Voltage': { 'gettext': '%.2F V' }, '/Dc/0/Current': { 'gettext': '%.1F A' }, '/Dc/0/Power': { 'gettext': '%.0F W' }, '/Soc': { 'gettext': '%.0F %%' }, '/History/TotalAhDrawn': { 'gettext': '%.0F Ah' }, '/History/DischargedEnergy': { 'gettext': '%.2F kWh' }, '/History/ChargedEnergy': { 'gettext': '%.2F kWh' }, '/History/AverageDischarge': { 'gettext': '%.2F kWh' }, '/TimeToGo': { 'gettext': '%.0F s' }, '/ConsumedAmphours': { 'gettext': '%.1F Ah' } } for path in self._summeditems.keys(): self._dbusservice.add_path(path, value=None, gettextcallback=self._gettext) self._dbusservice['/History/AverageDischarge'] = self._settings[ 'AvgDischarge'] self._dbusservice['/History/TotalAhDrawn'] = self._settings[ 'TotalAhDrawn'] self._dbusservice.add_path('/History/TimeSinceLastFullCharge', 0) self._dbusservice.add_path('/History/MinCellVoltage', self._settings['MinCellVoltage']) self._dbusservice.add_path('/History/MaxCellVoltage', self._settings['MaxCellVoltage']) self._dbusservice['/ConsumedAmphours'] = 0 logging.info( "History cell voltage min: %.3f, max: %.3f, totalAhDrawn: %d", self._settings['MinCellVoltage'], self._settings['MaxCellVoltage'], self._settings['TotalAhDrawn']) self._dbusservice['/History/ChargedEnergy'] = 0 self._dbusservice['/History/DischargedEnergy'] = 0 gobject.timeout_add(self._settings['interval'], exit_on_error, self._update)
class DbusGenerator: def __init__(self): self._bus = dbus.SystemBus() if (platform.machine() == 'armv7l') else dbus.SessionBus() self.RELAY_GPIO_FILE = '/sys/class/gpio/gpio182/value' self.HISTORY_DAYS = 30 # One second per retry self.RETRIES_ON_ERROR = 300 self._testrun_soc_retries = 0 self._last_counters_check = 0 self._dbusservice = None self._starttime = 0 self._manualstarttimer = 0 self._last_runtime_update = 0 self._timer_runnning = 0 self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None self._battery_measurement_available = True self._vebusservice_high_temperature_import = None self._vebusservice_overload_import = None self._vebusservice = None self._vebusservice_available = False self._condition_stack = { 'batteryvoltage': { 'name': 'batteryvoltage', 'reached': False, 'boolean': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'battery' }, 'batterycurrent': { 'name': 'batterycurrent', 'reached': False, 'boolean': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'battery' }, 'acload': { 'name': 'acload', 'reached': False, 'boolean': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'vebus' }, 'inverterhightemp': { 'name': 'inverterhightemp', 'reached': False, 'boolean': True, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'monitoring': 'vebus' }, 'inverteroverload': { 'name': 'inverteroverload', 'reached': False, 'boolean': True, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'vebus' }, 'soc': { 'name': 'soc', 'reached': False, 'boolean': False, 'timed': False, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'battery' } } # DbusMonitor expects these values to be there, even though we don need them. So just # add some dummy data. This can go away when DbusMonitor is more generic. dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} # TODO: possible improvement: don't use the DbusMonitor it all, since we are only monitoring # a set of static values which will always be available. DbusMonitor watches for services # that come and go, and takes care of automatic signal subscribtions etc. etc: all not necessary # in this use case where we have fixed services names (com.victronenergy.settings, and c # com.victronenergy.system). self._dbusmonitor = DbusMonitor({ 'com.victronenergy.settings': { # This is not our setting so do it here. not in supportedSettings '/Settings/Relay/Function': dummy, '/Settings/Relay/Polarity': dummy, '/Settings/System/TimeZone': dummy, }, 'com.victronenergy.system': { # This is not our setting so do it here. not in supportedSettings '/Ac/Consumption/Total/Power': dummy, '/Ac/PvOnOutput/Total/Power': dummy, '/Ac/PvOnGrid/Total/Power': dummy, '/Ac/PvOnGenset/Total/Power': dummy, '/Dc/Pv/Power': dummy, '/AutoSelectedBatteryMeasurement': dummy, } }, self._dbus_value_changed, self._device_added, self._device_removed) # Set timezone to user selected timezone environ['TZ'] = self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/System/TimeZone') # Connect to localsettings self._settings = SettingsDevice( bus=self._bus, supportedSettings={ 'autostart': ['/Settings/Generator0/AutoStartEnabled', 1, 0, 1], 'accumulateddaily': ['/Settings/Generator0/AccumulatedDaily', '', 0, 0], 'accumulatedtotal': ['/Settings/Generator0/AccumulatedTotal', 0, 0, 0], 'batterymeasurement': ['/Settings/Generator0/BatteryService', "default", 0, 0], 'minimumruntime': ['/Settings/Generator0/MinimumRuntime', 0, 0, 86400], # minutes # On permanent loss of communication: 0 = Stop, 1 = Start, 2 = keep running 'onlosscommunication': ['/Settings/Generator0/OnLossCommunication', 0, 0, 2], # Quiet hours 'quiethoursenabled': ['/Settings/Generator0/QuietHours/Enabled', 0, 0, 1], 'quiethoursstarttime': ['/Settings/Generator0/QuietHours/StartTime', 75600, 0, 86400], 'quiethoursendtime': ['/Settings/Generator0/QuietHours/EndTime', 21600, 0, 86400], # SOC 'socenabled': ['/Settings/Generator0/Soc/Enabled', 0, 0, 1], 'socstart': ['/Settings/Generator0/Soc/StartValue', 90, 0, 100], 'socstop': ['/Settings/Generator0/Soc/StopValue', 90, 0, 100], 'qh_socstart': ['/Settings/Generator0/Soc/QuietHoursStartValue', 90, 0, 100], 'qh_socstop': ['/Settings/Generator0/Soc/QuietHoursStopValue', 90, 0, 100], # Voltage 'batteryvoltageenabled': ['/Settings/Generator0/BatteryVoltage/Enabled', 0, 0, 1], 'batteryvoltagestart': ['/Settings/Generator0/BatteryVoltage/StartValue', 11.5, 0, 150], 'batteryvoltagestop': ['/Settings/Generator0/BatteryVoltage/StopValue', 12.4, 0, 150], 'batteryvoltagestarttimer': ['/Settings/Generator0/BatteryVoltage/StartTimer', 20, 0, 10000], 'batteryvoltagestoptimer': ['/Settings/Generator0/BatteryVoltage/StopTimer', 20, 0, 10000], 'qh_batteryvoltagestart': ['/Settings/Generator0/BatteryVoltage/QuietHoursStartValue', 11.9, 0, 100], 'qh_batteryvoltagestop': ['/Settings/Generator0/BatteryVoltage/QuietHoursStopValue', 12.4, 0, 100], # Current 'batterycurrentenabled': ['/Settings/Generator0/BatteryCurrent/Enabled', 0, 0, 1], 'batterycurrentstart': ['/Settings/Generator0/BatteryCurrent/StartValue', 10.5, 0.5, 1000], 'batterycurrentstop': ['/Settings/Generator0/BatteryCurrent/StopValue', 5.5, 0, 1000], 'batterycurrentstarttimer': ['/Settings/Generator0/BatteryCurrent/StartTimer', 20, 0, 10000], 'batterycurrentstoptimer': ['/Settings/Generator0/BatteryCurrent/StopTimer', 20, 0, 10000], 'qh_batterycurrentstart': ['/Settings/Generator0/BatteryCurrent/QuietHoursStartValue', 20.5, 0, 1000], 'qh_batterycurrentstop': ['/Settings/Generator0/BatteryCurrent/QuietHoursStopValue', 15.5, 0, 1000], # AC load 'acloadenabled': ['/Settings/Generator0/AcLoad/Enabled', 0, 0, 1], 'acloadstart': ['/Settings/Generator0/AcLoad/StartValue', 1600, 5, 100000], 'acloadstop': ['/Settings/Generator0/AcLoad/StopValue', 800, 0, 100000], 'acloadstarttimer': ['/Settings/Generator0/AcLoad/StartTimer', 20, 0, 10000], 'acloadstoptimer': ['/Settings/Generator0/AcLoad/StopTimer', 20, 0, 10000], 'qh_acloadstart': ['/Settings/Generator0/AcLoad/QuietHoursStartValue', 1900, 0, 100000], 'qh_acloadstop': ['/Settings/Generator0/AcLoad/QuietHoursStopValue', 1200, 0, 100000], # VE.Bus high temperature 'inverterhightempenabled': ['/Settings/Generator0/InverterHighTemp/Enabled', 0, 0, 1], 'inverterhightempstarttimer': ['/Settings/Generator0/InverterHighTemp/StartTimer', 20, 0, 10000], 'inverterhightempstoptimer': ['/Settings/Generator0/InverterHighTemp/StopTimer', 20, 0, 10000], # VE.Bus overload 'inverteroverloadenabled': ['/Settings/Generator0/InverterOverload/Enabled', 0, 0, 1], 'inverteroverloadstarttimer': ['/Settings/Generator0/InverterOverload/StartTimer', 20, 0, 10000], 'inverteroverloadstoptimer': ['/Settings/Generator0/InverterOverload/StopTimer', 20, 0, 10000], # TestRun 'testrunenabled': ['/Settings/Generator0/TestRun/Enabled', 0, 0, 1], 'testrunstartdate': ['/Settings/Generator0/TestRun/StartDate', time.time(), 0, 10000000000.1], 'testrunstarttimer': ['/Settings/Generator0/TestRun/StartTime', 54000, 0, 86400], 'testruninterval': ['/Settings/Generator0/TestRun/Interval', 28, 1, 365], 'testrunruntime': ['/Settings/Generator0/TestRun/Duration', 7200, 1, 86400], 'testrunskipruntime': ['/Settings/Generator0/TestRun/SkipRuntime', 0, 0, 100000], 'testruntillbatteryfull': ['/Settings/Generator0/TestRun/RunTillBatteryFull', 0, 0, 1] }, eventCallback=self._handle_changed_setting) # Whenever services come or go, we need to check if it was a service we use. Note that this # is a bit double: DbusMonitor does the same thing. But since we don't use DbusMonitor to # monitor for com.victronenergy.battery, .vebus, .charger or any other possible source of # battery data, it is necessary to monitor for changes in the available dbus services. self._bus.add_signal_receiver(self._dbus_name_owner_changed, signal_name='NameOwnerChanged') self._evaluate_if_we_are_needed() gobject.timeout_add(1000, self._handletimertick) self._update_relay() self._changed = True def _evaluate_if_we_are_needed(self): if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 1: if self._dbusservice is None: logger.info('Action! Going on dbus and taking control of the relay.') relay_polarity_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.settings', path='/Settings/Relay/Polarity', eventCallback=None, createsignal=True) # As is not possible to keep the relay state during the CCGX power cycles, # set the relay polarity to normally open. if relay_polarity_import.get_value() == 1: relay_polarity_import.set_value(0) logger.info('Setting relay polarity to normally open.') # put ourselves on the dbus self._dbusservice = VeDbusService('com.victronenergy.generator.startstop0') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareversion, connection='generator', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # State: None = invalid, 0 = stopped, 1 = running self._dbusservice.add_path('/State', value=0) # Condition that made the generator start self._dbusservice.add_path('/RunningByCondition', value='') # Runtime self._dbusservice.add_path('/Runtime', value=0, gettextcallback=self._gettext) # Today runtime self._dbusservice.add_path('/TodayRuntime', value=0, gettextcallback=self._gettext) # Test run runtime self._dbusservice.add_path('/TestRunIntervalRuntime', value=self._interval_runtime(self._settings['testruninterval']), gettextcallback=self._gettext) # Next tes trun date, values is 0 for test run disabled self._dbusservice.add_path('/NextTestRun', value=None, gettextcallback=self._gettext) # Next tes trun is needed 1, not needed 0 self._dbusservice.add_path('/SkipTestRun', value=None) # Manual start self._dbusservice.add_path('/ManualStart', value=0, writeable=True) # Manual start timer self._dbusservice.add_path('/ManualStartTimer', value=0, writeable=True) # Silent mode active self._dbusservice.add_path('/QuietHours', value=0) self._determineservices() else: if self._dbusservice is not None: self._stop_generator() self._dbusservice.__del__() self._dbusservice = None # Reset conditions for condition in self._condition_stack: self._reset_condition(self._condition_stack[condition]) logger.info('Relay function is no longer set to generator start/stop: made sure generator is off ' + 'and now going off dbus') def _device_added(self, dbusservicename, instance): self._evaluate_if_we_are_needed() self._determineservices() def _device_removed(self, dbusservicename, instance): self._evaluate_if_we_are_needed() self._determineservices() def _dbus_value_changed(self, dbusServiceName, dbusPath, options, changes, deviceInstance): if dbusPath == '/AutoSelectedBatteryMeasurement' and self._settings['batterymeasurement'] == 'default': self._determineservices() if dbusPath == '/Settings/Relay/Function': self._evaluate_if_we_are_needed() self._changed = True # Update relay state when polarity is changed if dbusPath == '/Settings/Relay/Polarity': self._update_relay() def _handle_changed_setting(self, setting, oldvalue, newvalue): self._changed = True self._evaluate_if_we_are_needed() if setting == 'batterymeasurement': self._determineservices() # Reset retries and valid if service changes for condition in self._condition_stack: if self._condition_stack[condition]['monitoring'] == 'battery': self._condition_stack[condition]['valid'] = True self._condition_stack[condition]['retries'] = 0 if setting == 'autostart': logger.info('Autostart function %s.' % ('enabled' if newvalue == 1 else 'disabled')) if self._dbusservice is not None and setting == 'testruninterval': self._dbusservice['/TestRunIntervalRuntime'] = self._interval_runtime( self._settings['testruninterval']) def _dbus_name_owner_changed(self, name, oldowner, newowner): self._determineservices() def _gettext(self, path, value): if path == '/NextTestRun': # Locale format date d = datetime.datetime.fromtimestamp(value) return d.strftime('%c') elif path in ['/Runtime', '/TestRunIntervalRuntime', '/TodayRuntime']: m, s = divmod(value, 60) h, m = divmod(m, 60) return '%dh, %dm, %ds' % (h, m, s) else: return value def _handletimertick(self): # try catch, to make sure that we kill ourselves on an error. Without this try-catch, there would # be an error written to stdout, and then the timer would not be restarted, resulting in a dead- # lock waiting for manual intervention -> not good! try: if self._dbusservice is not None: self._evaluate_startstop_conditions() self._changed = False except: self._stop_generator() import traceback traceback.print_exc() sys.exit(1) return True def _evaluate_startstop_conditions(self): # Conditions will be evaluated in this order conditions = ['soc', 'acload', 'batterycurrent', 'batteryvoltage', 'inverterhightemp', 'inverteroverload'] start = False runningbycondition = None today = calendar.timegm(datetime.date.today().timetuple()) self._timer_runnning = False values = self._get_updated_values() connection_lost = False self._check_quiet_hours() # New day, register it if self._last_counters_check < today and self._dbusservice['/State'] == 0: self._last_counters_check = today self._update_accumulated_time() # Update current and accumulated runtime. if self._dbusservice['/State'] == 1: self._dbusservice['/Runtime'] = int(time.time() - self._starttime) # By performance reasons, accumulated runtime is only updated # once per 10s. When the generator stops is also updated. if self._dbusservice['/Runtime'] - self._last_runtime_update >= 10: self._update_accumulated_time() if self._evaluate_manual_start(): runningbycondition = 'manual' start = True # Autostart conditions will only be evaluated if the autostart functionality is enabled if self._settings['autostart'] == 1: if self._evaluate_testrun_condition(): runningbycondition = 'testrun' start = True # Evaluate value conditions for condition in conditions: start = self._evaluate_condition(self._condition_stack[condition], values[condition]) or start runningbycondition = condition if start and runningbycondition is None else runningbycondition # Connection lost is set to true if the numbear of retries of one or more enabled conditions # >= RETRIES_ON_ERROR if self._condition_stack[condition]['enabled']: connection_lost = self._condition_stack[condition]['retries'] >= self.RETRIES_ON_ERROR # If none condition is reached check if connection is lost and start/keep running the generator # depending on '/OnLossCommunication' setting if not start and connection_lost: # Start always if self._settings['onlosscommunication'] == 1: start = True runningbycondition = 'lossofcommunication' # Keep running if generator already started if self._dbusservice['/State'] == 1 and self._settings['onlosscommunication'] == 2: start = True runningbycondition = 'lossofcommunication' if start: self._start_generator(runningbycondition) elif (self._dbusservice['/Runtime'] >= self._settings['minimumruntime'] * 60 or self._dbusservice['/RunningByCondition'] == 'manual'): self._stop_generator() def _reset_condition(self, condition): condition['reached'] = False if condition['timed']: condition['start_timer'] = 0 condition['stop_timer'] = 0 def _check_condition(self, condition, value): name = condition['name'] if self._settings[name + 'enabled'] == 0: if condition['enabled']: condition['enabled'] = False logger.info('Disabling (%s) condition' % name) condition['retries'] = 0 condition['valid'] = True self._reset_condition(condition) return False elif not condition['enabled']: condition['enabled'] = True logger.info('Enabling (%s) condition' % name) if (condition['monitoring'] == 'battery') and (self._settings['batterymeasurement'] == 'nobattery'): return False if value is None and condition['valid']: if condition['retries'] >= self.RETRIES_ON_ERROR: logger.info('Error getting (%s) value, skipping evaluation till get a valid value' % name) self._reset_condition(condition) self._comunnication_lost = True condition['valid'] = False else: condition['retries'] += 1 if condition['retries'] == 1 or (condition['retries'] % 10) == 0: logger.info('Error getting (%s) value, retrying(#%i)' % (name, condition['retries'])) return False elif value is not None and not condition['valid']: logger.info('Success getting (%s) value, resuming evaluation' % name) condition['valid'] = True condition['retries'] = 0 # Reset retries if value is valid if value is not None: condition['retries'] = 0 return condition['valid'] def _evaluate_condition(self, condition, value): name = condition['name'] setting = ('qh_' if self._dbusservice['/QuietHours'] == 1 else '') + name startvalue = self._settings[setting + 'start'] if not condition['boolean'] else 1 stopvalue = self._settings[setting + 'stop'] if not condition['boolean'] else 0 # Check if the condition has to be evaluated if not self._check_condition(condition, value): # If generator is started by this condition and value is invalid # wait till RETRIES_ON_ERROR to skip the condition if condition['reached'] and condition['retries'] <= self.RETRIES_ON_ERROR: return True return False # As this is a generic evaluation method, we need to know how to compare the values # first check if start value should be greater than stop value and then compare start_is_greater = startvalue > stopvalue # When the condition is already reached only the stop value can set it to False start = condition['reached'] or (value >= startvalue if start_is_greater else value <= startvalue) stop = value <= stopvalue if start_is_greater else value >= stopvalue # Timed conditions must start/stop after the condition has been reached for a minimum # time. if condition['timed']: if not condition['reached'] and start: condition['start_timer'] += time.time() if condition['start_timer'] == 0 else 0 start = time.time() - condition['start_timer'] >= self._settings[name + 'starttimer'] condition['stop_timer'] *= int(not start) self._timer_runnning = True else: condition['start_timer'] = 0 if condition['reached'] and stop: condition['stop_timer'] += time.time() if condition['stop_timer'] == 0 else 0 stop = time.time() - condition['stop_timer'] >= self._settings[name + 'stoptimer'] condition['stop_timer'] *= int(not stop) self._timer_runnning = True else: condition['stop_timer'] = 0 condition['reached'] = start and not stop return condition['reached'] def _evaluate_manual_start(self): if self._dbusservice['/ManualStart'] == 0: if self._dbusservice['/RunningByCondition'] == 'manual': self._dbusservice['/ManualStartTimer'] = 0 return False start = True # If /ManualStartTimer has a value greater than zero will use it to set a stop timer. # If no timer is set, the generator will not stop until the user stops it manually. # Once started by manual start, each evaluation the timer is decreased if self._dbusservice['/ManualStartTimer'] != 0: self._manualstarttimer += time.time() if self._manualstarttimer == 0 else 0 self._dbusservice['/ManualStartTimer'] -= int(time.time()) - int(self._manualstarttimer) self._manualstarttimer = time.time() start = self._dbusservice['/ManualStartTimer'] > 0 self._dbusservice['/ManualStart'] = int(start) # Reset if timer is finished self._manualstarttimer *= int(start) self._dbusservice['/ManualStartTimer'] *= int(start) return start def _evaluate_testrun_condition(self): if self._settings['testrunenabled'] == 0: self._dbusservice['/SkipTestRun'] = None self._dbusservice['/NextTestRun'] = None return False today = datetime.date.today() runtillbatteryfull = self._settings['testruntillbatteryfull'] == 1 soc = self._get_updated_values()['soc'] batteryisfull = runtillbatteryfull and soc == 100 try: startdate = datetime.date.fromtimestamp(self._settings['testrunstartdate']) starttime = time.mktime(today.timetuple()) + self._settings['testrunstarttimer'] except ValueError: logger.debug('Invalid dates, skipping testrun') return False # If start date is in the future set as NextTestRun and stop evaluating if startdate > today: self._dbusservice['/NextTestRun'] = time.mktime(startdate.timetuple()) return False start = False # If the accumulated runtime during the tes trun interval is greater than '/TestRunIntervalRuntime' # the tes trun must be skipped needed = (self._settings['testrunskipruntime'] > self._dbusservice['/TestRunIntervalRuntime'] or self._settings['testrunskipruntime'] == 0) self._dbusservice['/SkipTestRun'] = int(not needed) interval = self._settings['testruninterval'] stoptime = (starttime + self._settings['testrunruntime']) if not runtillbatteryfull else (starttime + 60) elapseddays = (today - startdate).days mod = elapseddays % interval start = (not bool(mod) and (time.time() >= starttime) and (time.time() <= stoptime)) if runtillbatteryfull: if soc is not None: self._testrun_soc_retries = 0 start = (start or self._dbusservice['/RunningByCondition'] == 'testrun') and not batteryisfull elif self._dbusservice['/RunningByCondition'] == 'testrun': if self._testrun_soc_retries < self.RETRIES_ON_ERROR: self._testrun_soc_retries += 1 start = True if (self._testrun_soc_retries % 10) == 0: logger.info('Test run failed to get SOC value, retrying(#%i)' % self._testrun_soc_retries) else: logger.info('Failed to get SOC after %i retries, terminating test run condition' % self._testrun_soc_retries) start = False else: start = False if not bool(mod) and (time.time() <= stoptime): self._dbusservice['/NextTestRun'] = starttime else: self._dbusservice['/NextTestRun'] = (time.mktime((today + datetime.timedelta(days=interval - mod)).timetuple()) + self._settings['testrunstarttimer']) return start and needed def _check_quiet_hours(self): active = False if self._settings['quiethoursenabled'] == 1: # Seconds after today 00:00 timeinseconds = time.time() - time.mktime(datetime.date.today().timetuple()) quiethoursstart = self._settings['quiethoursstarttime'] quiethoursend = self._settings['quiethoursendtime'] # Check if the current time is between the start time and end time if quiethoursstart < quiethoursend: active = quiethoursstart <= timeinseconds and timeinseconds < quiethoursend else: # End time is lower than start time, example Start: 21:00, end: 08:00 active = not (quiethoursend < timeinseconds and timeinseconds < quiethoursstart) if self._dbusservice['/QuietHours'] == 0 and active: logger.info('Entering to quiet mode') elif self._dbusservice['/QuietHours'] == 1 and not active: logger.info('Leaving secondary quiet mode') self._dbusservice['/QuietHours'] = int(active) return active def _update_accumulated_time(self): seconds = self._dbusservice['/Runtime'] accumulated = seconds - self._last_runtime_update self._settings['accumulatedtotal'] = int(self._settings['accumulatedtotal']) + accumulated # Using calendar to get timestamp in UTC, not local time today_date = str(calendar.timegm(datetime.date.today().timetuple())) # If something goes wrong getting the json string create a new one try: accumulated_days = json.loads(self._settings['accumulateddaily']) except ValueError: accumulated_days = {today_date: 0} if (today_date in accumulated_days): accumulated_days[today_date] += accumulated else: accumulated_days[today_date] = accumulated self._last_runtime_update = seconds # Keep the historical with a maximum of HISTORY_DAYS while len(accumulated_days) > self.HISTORY_DAYS: accumulated_days.pop(min(accumulated_days.keys()), None) # Upadate settings self._settings['accumulateddaily'] = json.dumps(accumulated_days, sort_keys=True) self._dbusservice['/TodayRuntime'] = self._interval_runtime(0) self._dbusservice['/TestRunIntervalRuntime'] = self._interval_runtime(self._settings['testruninterval']) def _interval_runtime(self, days): summ = 0 try: daily_record = json.loads(self._settings['accumulateddaily']) except ValueError: return 0 for i in range(days + 1): previous_day = calendar.timegm((datetime.date.today() - datetime.timedelta(days=i)).timetuple()) if str(previous_day) in daily_record.keys(): summ += daily_record[str(previous_day)] if str(previous_day) in daily_record.keys() else 0 return summ def _get_updated_values(self): values = { 'batteryvoltage': (self._battery_measurement_voltage_import.get_value() if self._battery_measurement_voltage_import else None), 'batterycurrent': (self._battery_measurement_current_import.get_value() if self._battery_measurement_current_import else None), 'soc': self._battery_measurement_soc_import.get_value() if self._battery_measurement_soc_import else None, 'acload': self._dbusmonitor.get_value('com.victronenergy.system', '/Ac/Consumption/Total/Power'), 'inverterhightemp': (self._vebusservice_high_temperature_import.get_value() if self._vebusservice_high_temperature_import else None), 'inverteroverload': (self._vebusservice_overload_import.get_value() if self._vebusservice_overload_import else None) } if values['batterycurrent']: values['batterycurrent'] *= -1 return values def _determineservices(self): # batterymeasurement is either 'default' or 'com_victronenergy_battery_288/Dc/0'. # In case it is set to default, we use the AutoSelected battery measurement, given by # SystemCalc. batterymeasurement = None batteryservicename = None newbatteryservice = None batteryprefix = "" selectedbattery = self._settings['batterymeasurement'] vebusservice = None if selectedbattery == 'default': batterymeasurement = self._dbusmonitor.get_value('com.victronenergy.system', '/AutoSelectedBatteryMeasurement') elif len(selectedbattery.split("/", 1)) == 2: # Only very basic sanity checking.. batterymeasurement = self._settings['batterymeasurement'] elif selectedbattery == 'nobattery': batterymeasurement = None else: # Exception: unexpected value for batterymeasurement pass if batterymeasurement: batteryprefix = "/" + batterymeasurement.split("/", 1)[1] # Get the current battery servicename if self._battery_measurement_voltage_import: oldservice = (self._battery_measurement_voltage_import.serviceName + self._battery_measurement_voltage_import.path.replace("/Voltage", "")) else: oldservice = None if batterymeasurement: batteryservicename = VeDbusItemImport( bus=self._bus, serviceName="com.victronenergy.system", path='/ServiceMapping/' + batterymeasurement.split("/", 1)[0], eventCallback=None, createsignal=False) if batteryservicename.get_value(): newbatteryservice = batteryservicename.get_value() + batteryprefix else: newbatteryservice = None if batteryservicename and batteryservicename.get_value(): self._battery_measurement_available = True logger.info('Battery service we need (%s) found! Using it for generator start/stop' % batterymeasurement) try: self._battery_measurement_voltage_import = VeDbusItemImport( bus=self._bus, serviceName=batteryservicename.get_value(), path=batteryprefix + '/Voltage', eventCallback=None, createsignal=True) self._battery_measurement_current_import = VeDbusItemImport( bus=self._bus, serviceName=batteryservicename.get_value(), path=batteryprefix + '/Current', eventCallback=None, createsignal=True) # Exception caused by Matthijs :), we forgot to batteryprefix the Soc during the big path-change... self._battery_measurement_soc_import = VeDbusItemImport( bus=self._bus, serviceName=batteryservicename.get_value(), path='/Soc', eventCallback=None, createsignal=True) except Exception: logger.debug('Error getting battery service!') self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None elif selectedbattery == 'nobattery' and self._battery_measurement_available: logger.info('Battery monitoring disabled! Stop evaluating related conditions') self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None self._battery_measurement_available = False elif batteryservicename and batteryservicename.get_value() is None and self._battery_measurement_available: logger.info('Battery service we need (%s) is not available! Stop evaluating related conditions' % batterymeasurement) self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None self._battery_measurement_available = False # Get the default VE.Bus service and import high temperature and overload warnings vebusservice = VeDbusItemImport( bus=self._bus, serviceName="com.victronenergy.system", path='/VebusService', eventCallback=None, createsignal=False) if vebusservice.get_value() and (vebusservice.get_value() != self._vebusservice or not self._vebusservice_available): self._vebusservice = vebusservice.get_value() self._vebusservice_available = True logger.info('Vebus service (%s) found! Using it for generator start/stop' % vebusservice.get_value()) try: self._vebusservice_high_temperature_import = VeDbusItemImport( bus=self._bus, serviceName=vebusservice.get_value(), path='/Alarms/HighTemperature', eventCallback=None, createsignal=True) self._vebusservice_overload_import = VeDbusItemImport( bus=self._bus, serviceName=vebusservice.get_value(), path='/Alarms/Overload', eventCallback=None, createsignal=True) except Exception: logger.info('Error getting Vebus service!') self._vebusservice_available = False self._vebusservice_high_temperature_import = None self._vebusservice_overload_import = None elif not vebusservice.get_value() and self._vebusservice_available: logger.info('Vebus service (%s) dissapeared! Stop evaluating related conditions' % self._vebusservice) self._vebusservice_available = False self._vebusservice_high_temperature_import = None self._vebusservice_overload_import = None # Trigger an immediate check of system status self._changed = True def _start_generator(self, condition): # This function will start the generator in the case generator not # already running. When differs, the RunningByCondition is updated if self._dbusservice['/State'] == 0: self._dbusservice['/State'] = 1 self._update_relay() self._starttime = time.time() logger.info('Starting generator by %s condition' % condition) elif self._dbusservice['/RunningByCondition'] != condition: logger.info('Generator previously running by %s condition is now running by %s condition' % (self._dbusservice['/RunningByCondition'], condition)) self._dbusservice['/RunningByCondition'] = condition def _stop_generator(self): if self._dbusservice['/State'] == 1: self._dbusservice['/State'] = 0 self._update_relay() logger.info('Stopping generator that was running by %s condition' % str(self._dbusservice['/RunningByCondition'])) self._dbusservice['/RunningByCondition'] = '' self._update_accumulated_time() self._starttime = 0 self._dbusservice['/Runtime'] = 0 self._dbusservice['/ManualStartTimer'] = 0 self._manualstarttimer = 0 self._last_runtime_update = 0 def _update_relay(self): # Relay polarity 0 = NO, 1 = NC polarity = bool(self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Polarity')) w = int(not polarity) if bool(self._dbusservice['/State']) else int(polarity) try: f = open(self.RELAY_GPIO_FILE, 'w') f.write(str(w)) f.close() except IOError: logger.info('Error writting to the relay GPIO file!: %s' % self.RELAY_GPIO_FILE)
class DbusGpsService: def __init__(self, servicename, deviceinstance, paths, productname='SIM868', connection='GPS'): self._dbusservice = VeDbusService(servicename) self._paths = paths logging.debug("%s /DeviceInstance = %d" % (servicename, deviceinstance)) # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path( '/Mgmt/ProcessVersion', 'Unkown version, and running on Python ' + platform.python_version()) self._dbusservice.add_path('/Mgmt/Connection', connection) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', deviceinstance) self._dbusservice.add_path('/ProductId', 0) self._dbusservice.add_path('/ProductName', productname) self._dbusservice.add_path('/FirmwareVersion', 0) self._dbusservice.add_path('/HardwareVersion', 0) self._dbusservice.add_path('/Connected', 1) for path, settings in self._paths.iteritems(): self._dbusservice.add_path( path, settings['initial'], writeable=True, onchangecallback=self._handlechangedvalue) serial.Serial() self.ser = serial.Serial("/dev/ttyUSB0", 115200) self.ser.timeout = 0 self.ser.flushInput() init_gps(self.ser) gobject.timeout_add(1000, self.update_position) def update_position(self): num_waiting = self.ser.inWaiting() while num_waiting > 0: try: line = self.ser.readline() msg = pynmea2.parse(line) if type(msg) is pynmea2.GGA: logging.info("lat: {}; lon: {}".format( msg.latitude, msg.longitude)) self._dbusservice["/Position/Latitude"] = msg.latitude self._dbusservice["/Position/Longtitude"] = msg.longitude except serial.SerialException as e: logging.error('Device error: {}'.format(e)) break except pynmea2.ParseError as e: logging.warning('Parse error: {}'.format(e)) continue num_waiting = self.ser.inWaiting() return True def _update(self): for path, settings in self._paths.iteritems(): if 'update' in settings: self._dbusservice[ path] = self._dbusservice[path] + settings['update'] logging.debug("%s: %s" % (path, self._dbusservice[path])) return True def _handlechangedvalue(self, path, value): logging.debug("someone else updated %s to %s" % (path, value)) return True # accept the change
class PinHandler(object, metaclass=HandlerMaker): product_id = 0xFFFF _product_name = 'Generic GPIO' dbus_name = "digital" def __init__(self, bus, base, path, gpio, settings): self.gpio = gpio self.path = path self.bus = bus self.settings = settings self._level = 0 # Remember last state self.service = VeDbusService("{}.{}.input{:02d}".format( base, self.dbus_name, gpio), bus=bus) # Add objects required by ve-api self.service.add_path('/Mgmt/ProcessName', __file__) self.service.add_path('/Mgmt/ProcessVersion', VERSION) self.service.add_path('/Mgmt/Connection', path) self.service.add_path('/DeviceInstance', gpio) self.service.add_path('/ProductId', self.product_id) self.service.add_path('/ProductName', self.product_name) self.service.add_path('/Connected', 1) # Custom name setting def _change_name(p, v): # This should fire a change event that will update product_name # below. settings['name'] = v return True self.service.add_path('/CustomName', settings['name'], writeable=True, onchangecallback=_change_name) # We'll count the pulses for all types of services self.service.add_path('/Count', value=settings['count']) @property def product_name(self): return self.settings['name'] or self._product_name @product_name.setter def product_name(self, v): # Some pin types don't have an associated service (Disabled pins for # example) if self.service is not None: self.service['/ProductName'] = v or self._product_name def deactivate(self): self.save_count() self.service.__del__() del self.service self.service = None @property def level(self): return self._level @level.setter def level(self, l): self._level = int(bool(l)) def toggle(self, level): # Only increment Count on rising edge. if level and level != self._level: self.service['/Count'] = (self.service['/Count'] + 1) % MAXCOUNT self._level = level def refresh(self): """ Toggle state to last remembered state. This is called if settings are changed so the Service can recalculate paths. """ self.toggle(self._level) def save_count(self): if self.service is not None: self.settings['count'] = self.count @property def active(self): return self.service is not None @property def count(self): return self.service['/Count'] @count.setter def count(self, v): self.service['/Count'] = v @classmethod def createHandler(cls, _type, *args, **kwargs): if _type in cls.handlers: return cls.handlers[_type](*args, **kwargs) return None
def _evaluate_if_we_are_needed(self): if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 1: if self._dbusservice is None: logger.info('Action! Going on dbus and taking control of the relay.') # put ourselves on the dbus self._dbusservice = VeDbusService('com.victronenergy.generator.startstop0') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareversion, connection='generator', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # State: None = invalid, 0 = stopped, 1 = running self._dbusservice.add_path('/State', value=0) # Condition that made the generator start self._dbusservice.add_path('/RunningByCondition', value='') # Runtime self._dbusservice.add_path('/Runtime', value=0, gettextcallback=self._gettext) # Today runtime self._dbusservice.add_path('/TodayRuntime', value=0, gettextcallback=self._gettext) # Maintenance runtime self._dbusservice.add_path('/MaintenanceIntervalRuntime', value=self._interval_runtime(self._settings['maintenanceinterval']), gettextcallback=self._gettext) # Next maintenance date, values is 0 for maintenande disabled self._dbusservice.add_path('/NextMaintenance', value=None, gettextcallback=self._gettext) # Next maintenance is needed 1, not needed 0 self._dbusservice.add_path('/SkipMaintenance', value=None) # Manual start self._dbusservice.add_path('/ManualStart', value=0, writeable=True) # Manual start timer self._dbusservice.add_path('/ManualStartTimer', value=0, writeable=True) # Silent mode active self._dbusservice.add_path('/SilentMode', value=0) # Battery services self._dbusservice.add_path('/AvailableBatteryServices', value=None) # Vebus services self._dbusservice.add_path('/AvailableVebusServices', value=None) # As the user can select the vebus service and is not yet possible to get the servie name from the gui # we need to provide it self._dbusservice.add_path('/VebusServiceName', value=None) self._determineservices() self._batteryservice = None self._vebusservice = None self._populate_services_list() self._determineservices() if self._batteryservice is not None: logger.info('Battery service we need (%s) found! Using it for generator start/stop' % self._get_service_path(self._settings['batteryservice'])) elif self._vebusservice is not None: logger.info('VE.Bus service we need (%s) found! Using it for generator start/stop' % self._get_service_path(self._settings['vebusservice'])) else: self._populate_services_list() self._determineservices() else: if self._dbusservice is not None: self._stop_generator() self._batteryservice = None self._vebusservice = None self._dbusservice.__del__() self._dbusservice = None # Reset conditions for condition in self._condition_stack: self._reset_condition(self._condition_stack[condition]) logger.info('Relay function is no longer set to generator start/stop: made sure generator is off ' + 'and now going off dbus')
class SystemCalc: def __init__(self, dbusmonitor_gen=None, dbusservice_gen=None, settings_device_gen=None): self.STATE_IDLE = 0 self.STATE_CHARGING = 1 self.STATE_DISCHARGING = 2 self.BATSERVICE_DEFAULT = 'default' self.BATSERVICE_NOBATTERY = 'nobattery' # Why this dummy? Because DbusMonitor expects these values to be there, even though we don't # need them. So just add some dummy data. This can go away when DbusMonitor is more generic. dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} dbus_tree = { 'com.victronenergy.solarcharger': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy}, 'com.victronenergy.pvinverter': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Ac/L1/Power': dummy, '/Ac/L2/Power': dummy, '/Ac/L3/Power': dummy, '/Position': dummy, '/ProductId': dummy}, 'com.victronenergy.battery': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy, '/Dc/0/Power': dummy, '/Soc': dummy, '/TimeToGo': dummy, '/ConsumedAmphours': dummy}, 'com.victronenergy.vebus' : { '/Ac/ActiveIn/ActiveInput': dummy, '/Ac/ActiveIn/L1/P': dummy, '/Ac/ActiveIn/L2/P': dummy, '/Ac/ActiveIn/L3/P': dummy, '/Ac/Out/L1/P': dummy, '/Ac/Out/L2/P': dummy, '/Ac/Out/L3/P': dummy, '/Hub4/AcPowerSetpoint': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy, '/Dc/0/Power': dummy, '/Soc': dummy}, 'com.victronenergy.charger': { '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy}, 'com.victronenergy.grid' : { '/ProductName': dummy, '/Mgmt/Connection': dummy, '/ProductId' : dummy, '/DeviceType' : dummy, '/Ac/L1/Power': dummy, '/Ac/L2/Power': dummy, '/Ac/L3/Power': dummy}, 'com.victronenergy.genset' : { '/ProductName': dummy, '/Mgmt/Connection': dummy, '/ProductId' : dummy, '/DeviceType' : dummy, '/Ac/L1/Power': dummy, '/Ac/L2/Power': dummy, '/Ac/L3/Power': dummy}, 'com.victronenergy.settings' : { '/Settings/SystemSetup/AcInput1' : dummy, '/Settings/SystemSetup/AcInput2' : dummy} } if dbusmonitor_gen is None: self._dbusmonitor = DbusMonitor(dbus_tree, self._dbus_value_changed, self._device_added, self._device_removed) else: self._dbusmonitor = dbusmonitor_gen(dbus_tree) # Connect to localsettings supported_settings = { 'batteryservice': ['/Settings/SystemSetup/BatteryService', self.BATSERVICE_DEFAULT, 0, 0], 'hasdcsystem': ['/Settings/SystemSetup/HasDcSystem', 0, 0, 1], 'writevebussoc': ['/Settings/SystemSetup/WriteVebusSoc', 0, 0, 1]} if settings_device_gen is None: self._settings = SettingsDevice( bus=dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus(), supportedSettings=supported_settings, eventCallback=self._handlechangedsetting) else: self._settings = settings_device_gen(supported_settings, self._handlechangedsetting) # put ourselves on the dbus if dbusservice_gen is None: self._dbusservice = VeDbusService('com.victronenergy.system') else: self._dbusservice = dbusservice_gen('com.victronenergy.system') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareVersion, connection='data from other dbus processes', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # At this moment, VRM portal ID is the MAC address of the CCGX. Anyhow, it should be string uniquely # identifying the CCGX. self._dbusservice.add_path('/Serial', value=get_vrm_portal_id()) self._dbusservice.add_path( '/AvailableBatteryServices', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/AvailableBatteryMeasurements', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/AutoSelectedBatteryService', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/AutoSelectedBatteryMeasurement', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/ActiveBatteryService', value=None, gettextcallback=self._gettext) self._dbusservice.add_path( '/PvInvertersProductIds', value=None) self._summeditems = { '/Ac/Grid/L1/Power': {'gettext': '%.0F W'}, '/Ac/Grid/L2/Power': {'gettext': '%.0F W'}, '/Ac/Grid/L3/Power': {'gettext': '%.0F W'}, '/Ac/Grid/Total/Power': {'gettext': '%.0F W'}, '/Ac/Grid/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/Grid/ProductId': {'gettext': '%s'}, '/Ac/Grid/DeviceType': {'gettext': '%s'}, '/Ac/Genset/L1/Power': {'gettext': '%.0F W'}, '/Ac/Genset/L2/Power': {'gettext': '%.0F W'}, '/Ac/Genset/L3/Power': {'gettext': '%.0F W'}, '/Ac/Genset/Total/Power': {'gettext': '%.0F W'}, '/Ac/Genset/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/Genset/ProductId': {'gettext': '%s'}, '/Ac/Genset/DeviceType': {'gettext': '%s'}, '/Ac/Consumption/L1/Power': {'gettext': '%.0F W'}, '/Ac/Consumption/L2/Power': {'gettext': '%.0F W'}, '/Ac/Consumption/L3/Power': {'gettext': '%.0F W'}, '/Ac/Consumption/Total/Power': {'gettext': '%.0F W'}, '/Ac/Consumption/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/L1/Power': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/L2/Power': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/L3/Power': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/Total/Power': {'gettext': '%.0F W'}, '/Ac/PvOnOutput/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/L1/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/L2/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/L3/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/Total/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGrid/NumberOfPhases': {'gettext': '%.0F W'}, '/Ac/PvOnGenset/L1/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGenset/L2/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGenset/L3/Power': {'gettext': '%.0F W'}, '/Ac/PvOnGenset/NumberOfPhases': {'gettext': '%d'}, '/Ac/PvOnGenset/Total/Power': {'gettext': '%.0F W'}, '/Dc/Pv/Power': {'gettext': '%.0F W'}, '/Dc/Pv/Current': {'gettext': '%.1F A'}, '/Dc/Battery/Voltage': {'gettext': '%.2F V'}, '/Dc/Battery/Current': {'gettext': '%.1F A'}, '/Dc/Battery/Power': {'gettext': '%.0F W'}, '/Dc/Battery/Soc': {'gettext': '%.0F %%'}, '/Dc/Battery/State': {'gettext': '%s'}, '/Dc/Battery/TimeToGo': {'gettext': '%.0F s'}, '/Dc/Battery/ConsumedAmphours': {'gettext': '%.1F Ah'}, '/Dc/Charger/Power': {'gettext': '%.0F %%'}, '/Dc/Vebus/Current': {'gettext': '%.1F A'}, '/Dc/Vebus/Power': {'gettext': '%.0F W'}, '/Dc/System/Power': {'gettext': '%.0F W'}, '/Hub': {'gettext': '%s'}, '/Ac/ActiveIn/Source': {'gettext': '%s'}, '/VebusService': {'gettext': '%s'} } for path in self._summeditems.keys(): self._dbusservice.add_path(path, value=None, gettextcallback=self._gettext) self._batteryservice = None self._determinebatteryservice() if self._batteryservice is None: logger.info("Battery service initialized to None (setting == %s)" % self._settings['batteryservice']) self._changed = True for service, instance in self._dbusmonitor.get_service_list().items(): self._device_added(service, instance, do_service_change=False) self._handleservicechange() self._updatevalues() self._writeVebusSocCounter = 9 gobject.timeout_add(1000, exit_on_error, self._handletimertick) def _handlechangedsetting(self, setting, oldvalue, newvalue): self._determinebatteryservice() self._changed = True def _determinebatteryservice(self): auto_battery_service = self._autoselect_battery_service() auto_battery_measurement = None if auto_battery_service is not None: services = self._dbusmonitor.get_service_list() if auto_battery_service in services: auto_battery_measurement = \ self._get_instance_service_name(auto_battery_service, services[auto_battery_service]) auto_battery_measurement = auto_battery_measurement.replace('.', '_').replace('/', '_') + '/Dc/0' self._dbusservice['/AutoSelectedBatteryMeasurement'] = auto_battery_measurement if self._settings['batteryservice'] == self.BATSERVICE_DEFAULT: newbatteryservice = auto_battery_service self._dbusservice['/AutoSelectedBatteryService'] = ( 'No battery monitor found' if newbatteryservice is None else self._get_readable_service_name(newbatteryservice)) elif self._settings['batteryservice'] == self.BATSERVICE_NOBATTERY: self._dbusservice['/AutoSelectedBatteryService'] = None newbatteryservice = None else: self._dbusservice['/AutoSelectedBatteryService'] = None s = self._settings['batteryservice'].split('/') if len(s) != 2: logger.error("The battery setting (%s) is invalid!" % self._settings['batteryservice']) serviceclass = s[0] instance = int(s[1]) if len(s) == 2 else None services = self._dbusmonitor.get_service_list(classfilter=serviceclass) if instance not in services.values(): # Once chosen battery monitor does not exist. Don't auto change the setting (it might come # back). And also don't autoselect another. newbatteryservice = None else: # According to https://www.python.org/dev/peps/pep-3106/, dict.keys() and dict.values() # always have the same order. newbatteryservice = services.keys()[services.values().index(instance)] if newbatteryservice != self._batteryservice: services = self._dbusmonitor.get_service_list() instance = services.get(newbatteryservice, None) if instance is None: battery_service = None else: battery_service = self._get_instance_service_name(newbatteryservice, instance) self._dbusservice['/ActiveBatteryService'] = battery_service logger.info("Battery service, setting == %s, changed from %s to %s (%s)" % (self._settings['batteryservice'], self._batteryservice, newbatteryservice, instance)) self._batteryservice = newbatteryservice def _autoselect_battery_service(self): # Default setting business logic: # first try to use a battery service (BMV or Lynx Shunt VE.Can). If there is more than one battery # service, use the first one (sort alphabetical). If no battery service is available, check if there # are not Solar chargers and no normal chargers. If they are not there, assume this is a hub-2, # hub-3 or hub-4 system and use VE.Bus SOC. battery_service = self._get_first_service('com.victronenergy.battery') if battery_service is not None: return battery_service if len(self._dbusmonitor.get_service_list('com.victronenergy.solarcharger')) > 0: return None if len(self._dbusmonitor.get_service_list('com.victronenergy.charger')) > 0: return None vebus_service = self._get_first_service('com.victronenergy.vebus') return vebus_service # will be None when no vebus service found # Called on a one second timer def _handletimertick(self): if self._changed: self._updatevalues() self._changed = False self._writeVebusSocCounter += 1 if self._writeVebusSocCounter >= 10: self._writeVebusSoc() self._writeVebusSocCounter = 0 return True # keep timer running def _writeVebusSoc(self): # ==== COPY BATTERY SOC TO VEBUS ==== if self._settings['writevebussoc'] and self._dbusservice['/VebusService'] and self._dbusservice['/Dc/Battery/Soc'] and \ self._batteryservice.split('.')[2] != 'vebus': logger.debug("writing this soc to vebus: %d", self._dbusservice['/Dc/Battery/Soc']) self._dbusmonitor.get_item(self._dbusservice['/VebusService'], '/Soc').set_value(self._dbusservice['/Dc/Battery/Soc']) def _updatepvinverterspidlist(self): # Create list of connected pv inverters id's pvinverters = self._dbusmonitor.get_service_list('com.victronenergy.pvinverter') productids = [] for pvinverter in pvinverters: pid = self._dbusmonitor.get_value(pvinverter, '/ProductId') if pid not in productids: productids.append(pid) self._dbusservice['/PvInvertersProductIds'] = productids if productids else None def _updatevalues(self): # ==== PREPARATIONS ==== # Determine values used in logic below vebusses = self._dbusmonitor.get_service_list('com.victronenergy.vebus') vebuspower = 0 for vebus in vebusses: v = self._dbusmonitor.get_value(vebus, '/Dc/0/Voltage') i = self._dbusmonitor.get_value(vebus, '/Dc/0/Current') if v is not None and i is not None: vebuspower += v * i # ==== PVINVERTERS ==== pvinverters = self._dbusmonitor.get_service_list('com.victronenergy.pvinverter') newvalues = {} pos = {0: '/Ac/PvOnGrid', 1: '/Ac/PvOnOutput', 2: '/Ac/PvOnGenset'} total = {0: None, 1: None, 2: None} for pvinverter in pvinverters: # Position will be None if PV inverter service has just been removed (after retrieving the # service list). position = pos.get(self._dbusmonitor.get_value(pvinverter, '/Position')) if position is not None: for phase in range(1, 4): power = self._dbusmonitor.get_value(pvinverter, '/Ac/L%s/Power' % phase) if power is not None: path = '%s/L%s/Power' % (position, phase) newvalues[path] = _safeadd(newvalues.get(path), power) for path in pos.values(): self._compute_phase_totals(path, newvalues) # ==== SOLARCHARGERS ==== solarchargers = self._dbusmonitor.get_service_list('com.victronenergy.solarcharger') solarcharger_batteryvoltage = None for solarcharger in solarchargers: v = self._dbusmonitor.get_value(solarcharger, '/Dc/0/Voltage') if v is None: continue i = self._dbusmonitor.get_value(solarcharger, '/Dc/0/Current') if i is None: continue if '/Dc/Pv/Power' not in newvalues: newvalues['/Dc/Pv/Power'] = v * i newvalues['/Dc/Pv/Current'] = i solarcharger_batteryvoltage = v else: newvalues['/Dc/Pv/Power'] += v * i newvalues['/Dc/Pv/Current'] += i # ==== CHARGERS ==== chargers = self._dbusmonitor.get_service_list('com.victronenergy.charger') charger_batteryvoltage = None for charger in chargers: # Assume the battery connected to output 0 is the main battery v = self._dbusmonitor.get_value(charger, '/Dc/0/Voltage') if v is None: continue charger_batteryvoltage = v i = self._dbusmonitor.get_value(charger, '/Dc/0/Current') if i is None: continue if '/Dc/Charger/Power' not in newvalues: newvalues['/Dc/Charger/Power'] = v * i else: newvalues['/Dc/Charger/Power'] += v * i # ==== BATTERY ==== if self._batteryservice is not None: batteryservicetype = self._batteryservice.split('.')[2] # either 'battery' or 'vebus' newvalues['/Dc/Battery/Soc'] = self._dbusmonitor.get_value(self._batteryservice,'/Soc') newvalues['/Dc/Battery/TimeToGo'] = self._dbusmonitor.get_value(self._batteryservice,'/TimeToGo') newvalues['/Dc/Battery/ConsumedAmphours'] = self._dbusmonitor.get_value(self._batteryservice,'/ConsumedAmphours') if batteryservicetype == 'battery': newvalues['/Dc/Battery/Voltage'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Voltage') newvalues['/Dc/Battery/Current'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Current') newvalues['/Dc/Battery/Power'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Power') elif batteryservicetype == 'vebus': newvalues['/Dc/Battery/Voltage'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Voltage') newvalues['/Dc/Battery/Current'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Current') if newvalues['/Dc/Battery/Voltage'] is not None and newvalues['/Dc/Battery/Current'] is not None: newvalues['/Dc/Battery/Power'] = ( newvalues['/Dc/Battery/Voltage'] * newvalues['/Dc/Battery/Current']) p = newvalues.get('/Dc/Battery/Power', None) if p is not None: if p > 30: newvalues['/Dc/Battery/State'] = self.STATE_CHARGING elif p < -30: newvalues['/Dc/Battery/State'] = self.STATE_DISCHARGING else: newvalues['/Dc/Battery/State'] = self.STATE_IDLE else: batteryservicetype = None if solarcharger_batteryvoltage is not None: newvalues['/Dc/Battery/Voltage'] = solarcharger_batteryvoltage elif charger_batteryvoltage is not None: newvalues['/Dc/Battery/Voltage'] = charger_batteryvoltage else: # CCGX-connected system consists of only a Multi, but it is not user-selected, nor # auto-selected as the battery-monitor, probably because there are other loads or chargers. # In that case, at least use its reported battery voltage. vebusses = self._dbusmonitor.get_service_list('com.victronenergy.vebus') for vebus in vebusses: v = self._dbusmonitor.get_value(vebus, '/Dc/0/Voltage') if v is not None: newvalues['/Dc/Battery/Voltage'] = v if self._settings['hasdcsystem'] == 0 and '/Dc/Battery/Voltage' in newvalues: # No unmonitored DC loads or chargers, and also no battery monitor: derive battery watts # and amps from vebus, solarchargers and chargers. assert '/Dc/Battery/Power' not in newvalues assert '/Dc/Battery/Current' not in newvalues p = newvalues.get('/Dc/Pv/Power', 0) + newvalues.get('/Dc/Charger/Power', 0) + vebuspower voltage = newvalues['/Dc/Battery/Voltage'] newvalues['/Dc/Battery/Current'] = p / voltage if voltage > 0 else None newvalues['/Dc/Battery/Power'] = p # ==== SYSTEM ==== if self._settings['hasdcsystem'] == 1 and batteryservicetype == 'battery': # Calculate power being generated/consumed by not measured devices in the network. # /Dc/System: positive: consuming power # VE.Bus: Positive: current flowing from the Multi to the dc system or battery # Solarcharger & other chargers: positive: charging # battery: Positive: charging battery. # battery = solarcharger + charger + ve.bus - system battery_power = newvalues.get('/Dc/Battery/Power') if battery_power is not None: dc_pv_power = newvalues.get('/Dc/Pv/Power', 0) charger_power = newvalues.get('/Dc/Charger/Power', 0) newvalues['/Dc/System/Power'] = dc_pv_power + charger_power + vebuspower - battery_power # ==== Vebus ==== # Assume there's only 1 multi service present on the D-Bus multi_path = self._get_first_service('com.victronenergy.vebus') if multi_path is not None: dc_current = self._dbusmonitor.get_value(multi_path, '/Dc/0/Current') newvalues['/Dc/Vebus/Current'] = dc_current dc_power = self._dbusmonitor.get_value(multi_path, '/Dc/0/Power') # Just in case /Dc/0/Power is not available if dc_power == None and dc_current is not None: dc_voltage = self._dbusmonitor.get_value(multi_path, '/Dc/0/Voltage') if dc_voltage is not None: dc_power = dc_voltage * dc_current # Note that there is also vebuspower, which is the total DC power summed over all multis. # However, this value cannot be combined with /Dc/Multi/Current, because it does not make sense # to add the Dc currents of all multis if they do not share the same DC voltage. newvalues['/Dc/Vebus/Power'] = dc_power newvalues['/VebusService'] = multi_path # ===== AC IN SOURCE ===== ac_in_source = None active_input = self._dbusmonitor.get_value(multi_path, '/Ac/ActiveIn/ActiveInput') if active_input is not None: settings_path = '/Settings/SystemSetup/AcInput%s' % (active_input + 1) ac_in_source = self._dbusmonitor.get_value('com.victronenergy.settings', settings_path) newvalues['/Ac/ActiveIn/Source'] = ac_in_source # ===== HUB MODE ===== # The code below should be executed after PV inverter data has been updated, because we need the # PV inverter total power to update the consumption. hub = None if self._dbusmonitor.get_value(multi_path, '/Hub4/AcPowerSetpoint') is not None: hub = 4 elif newvalues.get('/Dc/Pv/Power', None) is not None: hub = 1 elif newvalues.get('/Ac/PvOnOutput/Total/Power', None) is not None: hub = 2 elif newvalues.get('/Ac/PvOnGrid/Total/Power', None) is not None or \ newvalues.get('/Ac/PvOnGenset/Total/Power', None) is not None: hub = 3 newvalues['/Hub'] = hub # ===== GRID METERS & CONSUMPTION ==== consumption = { "L1" : None, "L2" : None, "L3" : None } for device_type in ['Grid', 'Genset']: servicename = 'com.victronenergy.%s' % device_type.lower() em_service = self._get_first_service(servicename) uses_active_input = False if multi_path is not None: # If a grid meter is present we use values from it. If not, we look at the multi. If it has # AcIn1 or AcIn2 connected to the grid, we use those values. # com.victronenergy.grid.??? indicates presence of an energy meter used as grid meter. # com.victronenergy.vebus.???/Ac/ActiveIn/ActiveInput: decides which whether we look at AcIn1 # or AcIn2 as possible grid connection. if ac_in_source is not None: uses_active_input = ac_in_source > 0 and (ac_in_source == 2) == (device_type == 'Genset') for phase in consumption: p = None pvpower = newvalues.get('/Ac/PvOn%s/%s/Power' % (device_type, phase)) if em_service is not None: p = self._dbusmonitor.get_value(em_service, '/Ac/%s/Power' % phase) # Compute consumption between energy meter and multi (meter power - multi AC in) and # add an optional PV inverter on input to the mix. c = consumption[phase] if uses_active_input: ac_in = self._dbusmonitor.get_value(multi_path, '/Ac/ActiveIn/%s/P' % phase) if ac_in is not None: c = _safeadd(c, -ac_in) # If there's any power coming from a PV inverter in the inactive AC in (which is unlikely), # it will still be used, because there may also be a load in the same ACIn consuming # power, or the power could be fed back to the net. c = _safeadd(c, p, pvpower) consumption[phase] = None if c is None else max(0, c) else: if uses_active_input: p = self._dbusmonitor.get_value(multi_path, '/Ac/ActiveIn/%s/P' % phase) # No relevant energy meter present. Assume there is no load between the grid and the multi. # There may be a PV inverter present though (Hub-3 setup). if pvpower != None: p = _safeadd(p, -pvpower) newvalues['/Ac/%s/%s/Power' % (device_type, phase)] = p self._compute_phase_totals('/Ac/%s' % device_type, newvalues) if em_service is not None: newvalues['/Ac/%s/ProductId' % device_type] = self._dbusmonitor.get_value(em_service, '/ProductId') newvalues['/Ac/%s/DeviceType' % device_type] = self._dbusmonitor.get_value(em_service, '/DeviceType') for phase in consumption: c = consumption[phase] pvpower = newvalues.get('/Ac/PvOnOutput/%s/Power' % phase) c = _safeadd(c, pvpower) if multi_path is not None: ac_out = self._dbusmonitor.get_value(multi_path, '/Ac/Out/%s/P' % phase) c = _safeadd(c, ac_out) newvalues['/Ac/Consumption/%s/Power' % phase] = None if c is None else max(0, c) self._compute_phase_totals('/Ac/Consumption', newvalues) # TODO EV Add Multi DeviceType & ProductID. Unfortunately, the com.victronenergy.vebus.??? tree does # not contain a /ProductId entry. # ==== UPDATE DBUS ITEMS ==== for path in self._summeditems.keys(): # Why the None? Because we want to invalidate things we don't have anymore. self._dbusservice[path] = newvalues.get(path, None) def _handleservicechange(self): # Update the available battery monitor services, used to populate the dropdown in the settings. # Below code makes a dictionary. The key is [dbuserviceclass]/[deviceinstance]. For example # "battery/245". The value is the name to show to the user in the dropdown. The full dbus- # servicename, ie 'com.victronenergy.vebus.ttyO1' is not used, since the last part of that is not # fixed. dbus-serviceclass name and the device instance are already fixed, so best to use those. services = self._dbusmonitor.get_service_list('com.victronenergy.vebus') services.update(self._dbusmonitor.get_service_list('com.victronenergy.battery')) ul = {self.BATSERVICE_DEFAULT: 'Automatic', self.BATSERVICE_NOBATTERY: 'No battery monitor'} for servicename, instance in services.items(): key = self._get_instance_service_name(servicename, instance) ul[key] = self._get_readable_service_name(servicename) self._dbusservice['/AvailableBatteryServices'] = json.dumps(ul) ul = {self.BATSERVICE_DEFAULT: 'Automatic', self.BATSERVICE_NOBATTERY: 'No battery monitor'} # For later: for device supporting multiple Dc measurement we should add entries for /Dc/1 etc as # well. for servicename, instance in services.items(): key = self._get_instance_service_name(servicename, instance).replace('.', '_').replace('/', '_') + '/Dc/0' ul[key] = self._get_readable_service_name(servicename) self._dbusservice['/AvailableBatteryMeasurements'] = dbus.Dictionary(ul, signature='sv') self._determinebatteryservice() self._updatepvinverterspidlist() self._changed = True def _get_readable_service_name(self, servicename): return (self._dbusmonitor.get_value(servicename, '/ProductName') + ' on ' + self._dbusmonitor.get_value(servicename, '/Mgmt/Connection')) def _get_instance_service_name(self, service, instance): return '%s/%s' % ('.'.join(service.split('.')[0:3]), instance) def _get_service_mapping_path(self, service, instance): sn = self._get_instance_service_name(service, instance).replace('.', '_').replace('/', '_') return '/ServiceMapping/%s' % sn def _dbus_value_changed(self, dbusServiceName, dbusPath, dict, changes, deviceInstance): self._changed = True # Workaround because com.victronenergy.vebus is available even when there is no vebus product # connected. if dbusPath in ['/ProductName', '/Mgmt/Connection']: self._handleservicechange() def _device_added(self, service, instance, do_service_change=True): path = self._get_service_mapping_path(service, instance) if path in self._dbusservice: self._dbusservice[path] = service else: self._dbusservice.add_path(path, service) if do_service_change: self._handleservicechange() def _device_removed(self, service, instance): path = self._get_service_mapping_path(service, instance) if path in self._dbusservice: del self._dbusservice[path] self._handleservicechange() def _gettext(self, path, value): if path == '/Dc/Battery/State': state = {self.STATE_IDLE: 'Idle', self.STATE_CHARGING: 'Charging', self.STATE_DISCHARGING: 'Discharging'} return state[value] item = self._summeditems.get(path) if item is not None: return item['gettext'] % value return value def _compute_phase_totals(self, path, newvalues): total_power = None number_of_phases = None for phase in range(1, 4): p = newvalues.get('%s/L%s/Power' % (path, phase)) total_power = _safeadd(total_power, p) if p is not None: number_of_phases = phase newvalues[path + '/Total/Power'] = total_power newvalues[path + '/NumberOfPhases'] = number_of_phases def _get_first_service(self, classfilter=None): services = self._dbusmonitor.get_service_list(classfilter=classfilter) if len(services) == 0: return None return sorted(services.keys())[0]
parser.add_argument("-d", "--debug", help="set logging level to debug", action="store_true") args = parser.parse_args() # Init logging logging.basicConfig(level=(logging.DEBUG if args.debug else logging.INFO)) logging.info(__file__ + " is starting up") logLevel = {0: 'NOTSET', 10: 'DEBUG', 20: 'INFO', 30: 'WARNING', 40: 'ERROR'} logging.info('Loglevel set to ' + logLevel[logging.getLogger().getEffectiveLevel()]) # Have a mainloop, so we can send/receive asynchronous calls to and from dbus DBusGMainLoop(set_as_default=True) dbusservice = VeDbusService(args.name) logging.info("using device instance %s" % args.deviceinstance) # Create the management objects, as specified in the ccgx dbus-api document dbusservice.add_path('/Management/ProcessName', __file__) dbusservice.add_path('/Management/ProcessVersion', 'Unkown version, and running on Python ' + platform.python_version()) dbusservice.add_path('/Management/Connection', 'Data taken from mk2dbus') # Create the mandatory objects dbusservice.add_path('/DeviceInstance', args.deviceinstance) dbusservice.add_path('/ProductId', 0) dbusservice.add_path('/ProductName', 'vebus device with ac sensors') dbusservice.add_path('/FirmwareVersion', 0) dbusservice.add_path('/HardwareVersion', 0) dbusservice.add_path('/Connected', 1)
class Battery: hardware_version = "" voltage = 0 current = 0 capacity_remain = 0 capacity = 0 cycles = 0 production = "" protection = Protection() version = 0 soc = 0 charge_fet = True discharge_fet = True cell_count = 0 temp_censors = 0 temp1 = 0 temp2 = 0 cells = [] control_charging = False control_voltage = 0 control_current = 0 control_previous_total = 0 control_previous_max = 0 control_discharge_current = 0 control_charge_current = 0 control_allow_charge = True max_battery_voltage = 0 min_battery_voltage = 0 def __init__(self, port): self.port = port # degree_sign = u'\N{DEGREE SIGN}' command_general = b"\xDD\xA5\x03\x00\xFF\xFD\x77" command_cell = b"\xDD\xA5\x04\x00\xFF\xFC\x77" command_hardware = b"\xDD\xA5\x05\x00\xFF\xFB\x77" zero_char = chr(48) def to_protection_bits(self, byte_data): tmp = bin(byte_data)[2:].rjust(13, self.zero_char) self.protection.voltage_high_cell = self.is_bit_set(tmp[12]) self.protection.voltage_low_cell = self.is_bit_set(tmp[11]) self.protection.voltage_high = self.is_bit_set(tmp[10]) self.protection.voltage_low = self.is_bit_set(tmp[9]) self.protection.temp_high_charge = self.is_bit_set(tmp[8]) self.protection.temp_low_charge = self.is_bit_set(tmp[7]) self.protection.temp_high_discharge = self.is_bit_set(tmp[6]) self.protection.temp_low_discharge = self.is_bit_set(tmp[5]) self.protection.current_over = self.is_bit_set(tmp[4]) self.protection.current_under = self.is_bit_set(tmp[3]) self.protection.short = self.is_bit_set(tmp[2]) self.protection.IC_inspection = self.is_bit_set(tmp[1]) self.protection.software_lock = self.is_bit_set(tmp[0]) def to_temp(self, sensor, value): # Keep the temp value between -20 and 100 to handle sensor issues or no data. # The BMS should have already protected before those limits have been reached. if sensor == 1: self.temp1 = min(max(value, -20), 100) if sensor == 2: self.temp2 = min(max(value, -20), 100) def is_bit_set(self, tmp): return False if tmp == self.zero_char else True def to_cell_bits(self, byte_data, byte_data_high): # clear the list for c in self.cells: self.cells.remove(c) # get up to the first 16 cells tmp = bin(byte_data)[2:].rjust(min(self.cell_count, 16), self.zero_char) for bit in reversed(tmp): self.cells.append(Cell(self.is_bit_set(bit))) # get any cells above 16 if self.cell_count > 16: tmp = bin(byte_data_high)[2:].rjust(self.cell_count - 16, self.zero_char) for bit in reversed(tmp): self.cells.append(Cell(self.is_bit_set(bit))) def to_fet_bits(self, byte_data): tmp = bin(byte_data)[2:].rjust(2, self.zero_char) self.charge_fet = self.is_bit_set(tmp[1]) self.discharge_fet = self.is_bit_set(tmp[0]) def log_battery_data(self): logger.debug("voltage {0}V current {1}A".format( self.voltage, self.current)) logger.debug(" capacity {0}Ah of {1}Ah SOC {2}%".format( self.capacity_remain, self.capacity, self.soc)) for c in range(self.cell_count): cell = str(c + 1) balance = "B" if self.cells[c].balance else " " cell_volt = str(self.cells[c].voltage) logger.debug("C[" + cell.rjust(2, self.zero_char) + "] " + balance + cell_volt + "V") def publish_battery_dbus(self): # Update SOC, DC and System items self._dbusservice['/System/NrOfCellsPerBattery'] = self.cell_count self._dbusservice['/Soc'] = round(self.soc, 2) self._dbusservice['/Dc/0/Voltage'] = round(self.voltage, 2) self._dbusservice['/Dc/0/Current'] = round(self.current, 2) self._dbusservice['/Dc/0/Power'] = round(self.voltage * self.current, 2) self._dbusservice['/Dc/0/Temperature'] = round( (float(self.temp1) + float(self.temp2)) / 2, 2) # Update battery extras self._dbusservice['/History/ChargeCycles'] = self.cycles self._dbusservice['/Io/AllowToCharge'] = 1 if self.charge_fet else 0 self._dbusservice[ '/Io/AllowToDischarge'] = 1 if self.discharge_fet else 0 self._dbusservice['/System/MinCellTemperature'] = min( self.temp1, self.temp2) self._dbusservice['/System/MaxCellTemperature'] = max( self.temp1, self.temp2) # Updates from cells max_voltage = 0 max_cell = '' min_voltage = 99 min_cell = '' balance = False total_voltage = 0 for c in range(self.cell_count): total_voltage += self.cells[c].voltage if max_voltage < self.cells[c].voltage: max_voltage = self.cells[c].voltage max_cell = c if min_voltage > self.cells[c].voltage: min_voltage = self.cells[c].voltage min_cell = c if self.cells[c].balance: balance = True self._dbusservice['/System/MaxCellVoltage'] = max_voltage self._dbusservice['/System/MaxVoltageCellId'] = 'C' + str(max_cell + 1) self._dbusservice['/System/MinCellVoltage'] = min_voltage self._dbusservice['/System/MinVoltageCellId'] = 'C' + str(min_cell + 1) self._dbusservice['/Balancing'] = 1 if balance else 0 self.manage_charge_current() # self.manage_control_charging(max_voltage, min_voltage, total_voltage, balance) # Update the alarms self._dbusservice[ '/Alarms/LowVoltage'] = 2 if self.protection.voltage_low else 0 self._dbusservice[ '/Alarms/HighVoltage'] = 2 if self.protection.voltage_high else 0 self._dbusservice[ '/Alarms/LowSoc'] = 2 if self.soc < 10 else 1 if self.soc < 20 else 0 self._dbusservice[ '/Alarms/HighChargeCurrent'] = 1 if self.protection.current_over else 0 self._dbusservice[ '/Alarms/HighDischargeCurrent'] = 1 if self.protection.current_under else 0 self._dbusservice['/Alarms/CellImbalance'] = 2 if self.protection.voltage_low_cell \ or self.protection.voltage_high_cell else 0 self._dbusservice['/Alarms/InternalFailure'] = 2 if self.protection.short \ or self.protection.IC_inspection \ or self.protection.software_lock else 0 self._dbusservice[ '/Alarms/HighChargeTemperature'] = 1 if self.protection.temp_high_charge else 0 self._dbusservice[ '/Alarms/LowChargeTemperature'] = 1 if self.protection.temp_low_charge else 0 self._dbusservice[ '/Alarms/HighTemperature'] = 1 if self.protection.temp_high_discharge else 0 self._dbusservice[ '/Alarms/LowTemperature'] = 1 if self.protection.temp_low_discharge else 0 logging.debug("logged to dbus ", round(self.voltage / 100, 2), round(self.current / 100, 2), round(self.soc, 2)) def manage_charge_current(self): # Start with the current values charge_current = self.control_charge_current discharge_current = self.control_discharge_current allow_charge = self.control_allow_charge # Change depending on the SOC values if self.soc > 99: allow_charge = False else: allow_charge = True # Change depending on the SOC values if self.soc >= 100: charge_current = 0 elif 98 < self.soc < 100: charge_current = 1 elif 95 < self.soc <= 97: charge_current = 4 elif 91 < self.soc <= 95: charge_current = MAX_BATTERY_CURRENT / 2 else: charge_current = MAX_BATTERY_CURRENT # Change depending on the SOC values if self.soc <= 20: discharge_current = 5 elif 20 < self.soc <= 30: discharge_current = MAX_BATTERY_DISCHARGE_CURRENT / 4 elif 30 < self.soc <= 35: discharge_current = MAX_BATTERY_DISCHARGE_CURRENT / 2 else: discharge_current = MAX_BATTERY_DISCHARGE_CURRENT # Update the dbus values if they changed if charge_current != self.control_charge_current: self.control_charge_current = charge_current self._dbusservice[ '/Info/MaxChargeCurrent'] = self.control_charge_current if discharge_current != self.control_discharge_current: self.control_discharge_current = discharge_current self._dbusservice[ '/Info/MaxDischargeCurrent'] = self.control_discharge_current if allow_charge != self.control_allow_charge: if allow_charge and self.charge_fet: self._dbusservice['/Io/AllowToCharge'] = 1 self._dbusservice['/System/NrOfModulesBlockingCharge'] = 0 else: self._dbusservice['/Io/AllowToCharge'] = 0 self._dbusservice['/System/NrOfModulesBlockingCharge'] = 1 def manage_control_charging(self, max_voltage, min_voltage, total_voltage, balance): # Nothing to do if we cannot charge if not self.charge_fet or self.current < 0: if self.control_charging: self.control_charging = False self._dbusservice[ '/Info/MaxChargeVoltage'] = self.max_battery_voltage self._dbusservice[ '/Info/MaxChargeCurrent'] = MAX_BATTERY_CURRENT logger.info(">STOP< control charging") return if max_voltage > 3.50: logger.info( ">CHECK< control charging min {0} max {1} tot {2} Bal {3}". format(min_voltage, max_voltage, total_voltage, 1 if balance else 0)) if not self.control_charging and max_voltage > 3.50 and min_voltage < 3.45: # should we start self.control_charging = True self.control_previous_total = total_voltage self.control_current = min(MAX_BATTERY_CURRENT, 0.5) self.control_voltage = min(self.max_battery_voltage, total_voltage - 1) self.control_previous_max = max_voltage self._dbusservice['/Info/MaxChargeVoltage'] = self.control_voltage self._dbusservice['/Info/MaxChargeCurrent'] = self.control_current logger.info(">START< control charging {0}A {1}V".format( self.control_current, self.control_voltage)) else: # If all cells are low then we can stop control if self.control_charging and max_voltage < 3.45: self.control_charging = False self._dbusservice[ '/Info/MaxChargeVoltage'] = self.max_battery_voltage self._dbusservice[ '/Info/MaxChargeCurrent'] = MAX_BATTERY_CURRENT logger.info(">STOP< control charging") return if self.control_charging and max_voltage > 3.64: self._dbusservice['/Info/MaxChargeVoltage'] = min( self.max_battery_voltage, 48) self._dbusservice['/Info/MaxChargeCurrent'] = min( MAX_BATTERY_CURRENT, 0.001) logger.info(">STOP< No Balancing!") return # Limit the charge voltage if a few cells get too high if max_voltage > (self.control_previous_max + 0.01): # Still to high self.control_current -= 0.005 self.control_voltage -= 0.01 self.control_current = max(0.2, self.control_current) self.control_voltage = max(self.max_battery_voltage - 4, self.control_voltage) self.control_previous_total = total_voltage self._dbusservice[ '/Info/MaxChargeVoltage'] = self.control_voltage self._dbusservice[ '/Info/MaxChargeCurrent'] = self.control_current logger.info(">DOWN< control charging {0}A {1}V".format( self.control_current, self.control_voltage)) return else: if total_voltage < (self.control_previous_total - 0.03): # To low self.control_current += 0.005 self.control_voltage += 0.01 self.control_current = min(MAX_BATTERY_CURRENT, self.control_current) self.control_voltage = min(self.max_battery_voltage, self.control_voltage) self.control_previous_total = total_voltage self._dbusservice[ '/Info/MaxChargeVoltage'] = self.control_voltage self._dbusservice[ '/Info/MaxChargeCurrent'] = self.control_current logger.info(">UP< control charging {0}A {1}V".format( self.control_current, self.control_voltage)) return def read_gen_data(self): gen_data = read_serial_data(self.command_general, self.port) # check if connect success if gen_data is False: return gen_data voltage, current, capacity_remain, capacity, self.cycles, self.production, balance, \ balance2, protection, version, self.soc, fet, self.cell_count, self.temp_censors, temp1, temp2 \ = unpack_from('>HhHHHHhHHBBBBBHH', gen_data) self.voltage = voltage / 100 self.current = current / 100 self.capacity_remain = capacity_remain / 100 self.capacity = capacity / 100 self.to_temp(1, (temp1 - 2731) / 10) self.to_temp(2, (temp2 - 2731) / 10) self.to_cell_bits(balance, balance2) self.version = float( str(version >> 4 & 0x0F) + "." + str(version & 0x0F)) self.to_fet_bits(fet) self.to_protection_bits(protection) self.max_battery_voltage = MAX_CELL_VOLTAGE * self.cell_count self.min_battery_voltage = MIN_CELL_VOLTAGE * self.cell_count def read_cell_data(self): cell_data = read_serial_data(self.command_cell, self.port) # check if connect success if cell_data is False or len(cell_data) < self.cell_count * 2: return cell_data for c in range(self.cell_count): self.cells[c].voltage = unpack_from('>H', cell_data, c * 2)[0] / 1000 def read_hardware_data(self): hardware_data = read_serial_data(self.command_hardware, self.port) # check if connection success if hardware_data is False: return hardware_data self.hardware_version = unpack_from( '>' + str(len(hardware_data)) + 's', hardware_data)[0] logger.info(self.hardware_version) return True def publish_battery(self, loop): try: self.read_gen_data() self.read_cell_data() # self.log_battery_data() self.publish_battery_dbus() except: traceback.print_exc() loop.quit() def setup_vedbus(self, instance): self._dbusservice = VeDbusService("com.victronenergy.battery." + self.port[self.port.rfind('/') + 1:]) logger.debug("%s /DeviceInstance = %d" % ("com.victronenergy.battery." + self.port[self.port.rfind('/') + 1:], instance)) # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path('/Mgmt/ProcessVersion', 'Python ' + platform.python_version()) self._dbusservice.add_path('/Mgmt/Connection', 'Serial ' + self.port) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', instance) self._dbusservice.add_path('/ProductId', 0x0) self._dbusservice.add_path('/ProductName', 'SerialBattery (LTT)') self._dbusservice.add_path('/FirmwareVersion', self.version) self._dbusservice.add_path('/HardwareVersion', self.hardware_version) self._dbusservice.add_path('/Connected', 1) # Create static battery info self._dbusservice.add_path('/Info/BatteryLowVoltage', self.min_battery_voltage, writeable=True) self._dbusservice.add_path('/Info/MaxChargeVoltage', self.max_battery_voltage, writeable=True) self._dbusservice.add_path('/Info/MaxChargeCurrent', MAX_BATTERY_CURRENT, writeable=True) self._dbusservice.add_path('/Info/MaxDischargeCurrent', MAX_BATTERY_DISCHARGE_CURRENT, writeable=True) self._dbusservice.add_path('/System/NrOfCellsPerBattery', self.cell_count, writeable=True) self._dbusservice.add_path('/System/NrOfModulesOnline', 1, writeable=True) self._dbusservice.add_path('/System/NrOfModulesOffline', None, writeable=True) self._dbusservice.add_path('/System/NrOfModulesBlockingCharge', None, writeable=True) self._dbusservice.add_path('/System/NrOfModulesBlockingDischarge', None, writeable=True) # Not used at this stage # self._dbusservice.add_path('/System/MinTemperatureCellId', None, writeable=True) # self._dbusservice.add_path('/System/MaxTemperatureCellId', None, writeable=True) self._dbusservice.add_path('/Capacity', self.capacity, writeable=True) # Create SOC, DC and System items self._dbusservice.add_path('/Soc', None, writeable=True) self._dbusservice.add_path('/Dc/0/Voltage', None, writeable=True) self._dbusservice.add_path('/Dc/0/Current', None, writeable=True) self._dbusservice.add_path('/Dc/0/Power', None, writeable=True) self._dbusservice.add_path('/Dc/0/Temperature', 21.0, writeable=True) # Create battery extras self._dbusservice.add_path('/System/MinCellTemperature', None, writeable=True) self._dbusservice.add_path('/System/MaxCellTemperature', None, writeable=True) self._dbusservice.add_path('/System/MaxCellVoltage', 0.0, writeable=True) self._dbusservice.add_path('/System/MaxVoltageCellId', '', writeable=True) self._dbusservice.add_path('/System/MinCellVoltage', 0.0, writeable=True) self._dbusservice.add_path('/System/MinVoltageCellId', '', writeable=True) self._dbusservice.add_path('/History/ChargeCycles', 0, writeable=True) self._dbusservice.add_path('/Balancing', 0, writeable=True) self._dbusservice.add_path('/Io/AllowToCharge', 0, writeable=True) self._dbusservice.add_path('/Io/AllowToDischarge', 0, writeable=True) # Create the alarms self._dbusservice.add_path('/Alarms/LowVoltage', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighVoltage', 0, writeable=True) self._dbusservice.add_path('/Alarms/LowSoc', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighChargeCurrent', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighDischargeCurrent', 0, writeable=True) self._dbusservice.add_path('/Alarms/CellImbalance', 0, writeable=True) self._dbusservice.add_path('/Alarms/InternalFailure', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighChargeTemperature', 0, writeable=True) self._dbusservice.add_path('/Alarms/LowChargeTemperature', 0, writeable=True) self._dbusservice.add_path('/Alarms/HighTemperature', 0, writeable=True) self._dbusservice.add_path('/Alarms/LowTemperature', 0, writeable=True)
class AcDevice(object): def __init__(self, position): # Dictionary containing the AC Sensors per phase. This is the source of the data self._acSensors = {'L1': [], 'L2': [], 'L3': []} # Type and position (numbering is equal to numbering in VE.Bus Assistant): self._names = {0: 'PV Inverter on input 1', 1: 'PV Inverter on output', 2: 'PV Inverter on input 2'} self._name = position self._dbusService = None def __str__(self): return self._names[self._name] + ' containing ' + \ str(len(self._acSensors['L1'])) + ' AC-sensors on L1, ' + \ str(len(self._acSensors['L2'])) + ' AC-sensors on L2, ' + \ str(len(self._acSensors['L3'])) + ' AC-sensors on L3' # add_ac_sensor function is called to add dbusitems that represent power for a certain phase def add_ac_sensor(self, acsensor, phase): acsensor.set_eventcallback(self.value_has_changed) self._acSensors[phase].append(acsensor) def value_has_changed(self, dbusName, dbusObjectPath, changes): # decouple, and process update in the mainloop idle_add(self.update_values) # iterates through all sensor dbusItems, and recalculates our values. Adds objects to exported # dbus values if necessary. def update_values(self): if not self._dbusService: return totals = {'I': 0, 'P': 0, 'E': 0} for phase in ['L1', 'L2', 'L3']: pre = '/Ac/' + phase if len(self._acSensors[phase]) == 0: if (pre + '/Power') in self._dbusService: self._dbusService[pre + '/Power'] = None self._dbusService[pre + '/Energy/Forward'] = None self._dbusService[pre + '/Voltage'] = None self._dbusService[pre + '/Current'] = None else: phaseTotals = {'I': 0, 'P': 0, 'E': 0} for o in self._acSensors[phase]: phaseTotals['I'] += float(o['current'].get_value() or 0) phaseTotals['P'] += float(o['power'].get_value() or 0) phaseTotals['E'] += float(o['energycounter'].get_value() or 0) voltage = float(o['voltage'].get_value() or 0) # just take the last voltage if (pre + '/Power') not in self._dbusService: # This phase hasn't been added yet, adding it now self._dbusService.add_path(pre + '/Voltage', voltage, gettextcallback=self.gettextforV) self._dbusService.add_path(pre + '/Current', phaseTotals['I'], gettextcallback=self.gettextforA) self._dbusService.add_path(pre + '/Power', phaseTotals['P'], gettextcallback=self.gettextforW) self._dbusService.add_path(pre + '/Energy/Forward', phaseTotals['E'], gettextcallback=self.gettextforkWh) else: self._dbusService[pre + '/Voltage'] = voltage self._dbusService[pre + '/Current'] = phaseTotals['I'] self._dbusService[pre + '/Power'] = phaseTotals['P'] self._dbusService[pre + '/Energy/Forward'] = phaseTotals['E'] totals['I'] += phaseTotals['I'] totals['P'] += phaseTotals['P'] totals['E'] += phaseTotals['E'] #logging.debug( # self._names[self._name] + '. Phase ' + phase + ' recalculated: %0.2fV, %0.2fA, %0.4fW and %0.4f kWh' % # (voltage, phaseTotals['I'], phaseTotals['P'], phaseTotals['E'])) # TODO, why doesn't the application crash on an exception? I want it to crash, also on exceptions # in threads. #raise Exception ("exit Exception!") if '/Ac/Current' not in self._dbusService: self._dbusService.add_path('/Ac/Current', totals['I'], gettextcallback=self.gettextforA) self._dbusService.add_path('/Ac/Power', totals['P'], gettextcallback=self.gettextforW) self._dbusService.add_path('/Ac/Energy/Forward', totals['E'], gettextcallback=self.gettextforkWh) else: self._dbusService['/Ac/Current'] = totals['I'] self._dbusService['/Ac/Power'] = totals['P'] self._dbusService['/Ac/Energy/Forward'] = totals['E'] # Call this function after you have added AC sensors to this class. Code will check if we have any, # and if yes, add ourselves to the dbus. def update_dbus_service(self): if (len(self._acSensors['L1']) > 0 or len(self._acSensors['L2']) > 0 or len(self._acSensors['L3']) > 0): if self._dbusService is None: pf = {0: 'input1', 1: 'output', 2: 'input2'} self._dbusService = VeDbusService('com.victronenergy.pvinverter.vebusacsensor_' + pf[self._name]) #, self._dbusConn) self._dbusService.add_path('/Position', self._name, description=None, gettextcallback=self.gettextforposition) # Create the mandatory objects, as per victron dbus api document self._dbusService.add_path('/Mgmt/ProcessName', __file__) self._dbusService.add_path('/Mgmt/ProcessVersion', softwareVersion) self._dbusService.add_path('/Mgmt/Connection', 'AC Sensor on VE.Bus device') self._dbusService.add_path('/DeviceInstance', int(self._name) + 10) self._dbusService.add_path('/ProductId', 0xA141) self._dbusService.add_path('/ProductName', self._names[self._name]) self._dbusService.add_path('/Connected', 1) logging.info('Added to D-Bus: ' + self.__str__()) self.update_values() # Apparantly some service from which we imported AC Sensors has gone offline. Remove those sensors # from our repo. def remove_ac_sensors_imported_from(self, serviceBeingRemoved): logging.debug( '%s: Checking if we have sensors from %s, and removing them' % (self._names[self._name], serviceBeingRemoved)) for phase in ['L1', 'L2', 'L3']: self._acSensors[phase][:] = [x for x in self._acSensors[phase] if not x['power'].serviceName == serviceBeingRemoved] if self._dbusService is None: return if (not self._acSensors['L1'] and not self._acSensors['L2'] and not self._acSensors['L3']): # No sensors left for us, clean up self._dbusService.__del__() # explicitly call __del__(), instead of waiting for gc self._dbusService = None logging.info("Removed from D-Bus: %s" % self.__str__()) else: # Still some sensors left for us, update values self.update_values() def gettextforkWh(self, path, value): return ("%.3FkWh" % (float(value) / 1000.0)) def gettextforW(self, path, value): return ("%.0FW" % (float(value))) def gettextforV(self, path, value): return ("%.0FV" % (float(value))) def gettextforA(self, path, value): return ("%.0FA" % (float(value))) def gettextforposition(self, path, value): return self._names[value]
def _evaluate_if_we_are_needed(self): if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 1: if not self._relay_state_import: logger.info('Getting relay from systemcalc.') try: self._relay_state_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.system', path='/Relay/0/State', eventCallback=None, createsignal=True) except dbus.exceptions.DBusException: logger.info('Systemcalc relay not available.') self._relay_state_import = None pass if self._dbusservice is None: logger.info( 'Action! Going on dbus and taking control of the relay.') relay_polarity_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.settings', path='/Settings/Relay/Polarity', eventCallback=None, createsignal=True) # As is not possible to keep the relay state during the CCGX power cycles, # set the relay polarity to normally open. if relay_polarity_import.get_value() == 1: relay_polarity_import.set_value(0) logger.info('Setting relay polarity to normally open.') # put ourselves on the dbus self._dbusservice = VeDbusService( 'com.victronenergy.generator.startstop0') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareversion, connection='generator', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # State: None = invalid, 0 = stopped, 1 = running self._dbusservice.add_path('/State', value=0) # Condition that made the generator start self._dbusservice.add_path('/RunningByCondition', value='') # Runtime self._dbusservice.add_path('/Runtime', value=0, gettextcallback=self._gettext) # Today runtime self._dbusservice.add_path('/TodayRuntime', value=0, gettextcallback=self._gettext) # Test run runtime self._dbusservice.add_path( '/TestRunIntervalRuntime', value=self._interval_runtime( self._settings['testruninterval']), gettextcallback=self._gettext) # Next tes trun date, values is 0 for test run disabled self._dbusservice.add_path('/NextTestRun', value=None, gettextcallback=self._gettext) # Next tes trun is needed 1, not needed 0 self._dbusservice.add_path('/SkipTestRun', value=None) # Manual start self._dbusservice.add_path('/ManualStart', value=0, writeable=True) # Manual start timer self._dbusservice.add_path('/ManualStartTimer', value=0, writeable=True) # Silent mode active self._dbusservice.add_path('/QuietHours', value=0) self._determineservices() else: if self._dbusservice is not None: self._stop_generator() self._dbusservice.__del__() self._dbusservice = None # Reset conditions for condition in self._condition_stack: self._reset_condition(self._condition_stack[condition]) logger.info( 'Relay function is no longer set to generator start/stop: made sure generator is off ' + 'and now going off dbus') self._relay_state_import = None
class DbusPump: def __init__(self, retries=300): self._bus = dbus.SystemBus() if (platform.machine() == 'armv7l') else dbus.SessionBus() self.RELAY_GPIO_FILE = '/sys/class/gpio/gpio182/value' self.HISTORY_DAYS = 30 # One second per retry self.RETRIES_ON_ERROR = retries self._current_retries = 0 self.TANKSERVICE_DEFAULT = 'default' self.TANKSERVICE_NOTANK = 'notanksensor' self._dbusservice = None self._tankservice = self.TANKSERVICE_NOTANK self._valid_tank_level = True self._relay_state_import = None # DbusMonitor expects these values to be there, even though we don need them. So just # add some dummy data. This can go away when DbusMonitor is more generic. dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} # TODO: possible improvement: don't use the DbusMonitor it all, since we are only monitoring # a set of static values which will always be available. DbusMonitor watches for services # that come and go, and takes care of automatic signal subscribtions etc. etc: all not necessary # in this use case where we have fixed services names (com.victronenergy.settings, and c # com.victronenergy.system). self._dbusmonitor = DbusMonitor({ 'com.victronenergy.settings': { # This is not our setting so do it here. not in supportedSettings '/Settings/Relay/Function': dummy, '/Settings/Relay/Polarity': dummy }, 'com.victronenergy.tank': { # This is not our setting so do it here. not in supportedSettings '/Level': dummy, '/FluidType': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy } }, self._dbus_value_changed, self._device_added, self._device_removed) # Connect to localsettings self._settings = SettingsDevice( bus=self._bus, supportedSettings={ 'tankservice': ['/Settings/Pump0/TankService', self.TANKSERVICE_NOTANK, 0, 1], 'autostart': ['/Settings/Pump0/AutoStartEnabled', 1, 0, 1], 'startvalue': ['/Settings/Pump0/StartValue', 50, 0, 100], 'stopvalue': ['/Settings/Pump0/StopValue', 80, 0, 100], 'mode': ['/Settings/Pump0/Mode', 0, 0, 100] # Auto = 0, On = 1, Off = 2 }, eventCallback=self._handle_changed_setting) # Whenever services come or go, we need to check if it was a service we use. Note that this # is a bit double: DbusMonitor does the same thing. But since we don't use DbusMonitor to # monitor for com.victronenergy.battery, .vebus, .charger or any other possible source of # battery data, it is necessary to monitor for changes in the available dbus services. self._bus.add_signal_receiver(self._dbus_name_owner_changed, signal_name='NameOwnerChanged') self._evaluate_if_we_are_needed() gobject.timeout_add(1000, self._handletimertick) self._changed = True def _evaluate_if_we_are_needed(self): if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 3: if self._dbusservice is None: logger.info('Action! Going on dbus and taking control of the relay.') relay_polarity_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.settings', path='/Settings/Relay/Polarity', eventCallback=None, createsignal=True) if not self._relay_state_import: logger.info('Getting relay from systemcalc.') try: self._relay_state_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.system', path='/Relay/0/State', eventCallback=None, createsignal=True) except dbus.exceptions.DBusException: logger.info('Systemcalc relay not available.') self._relay_state_import = None pass # As is not possible to keep the relay state during the CCGX power cycles, # set the relay polarity to normally open. if relay_polarity_import.get_value() == 1: relay_polarity_import.set_value(0) logger.info('Setting relay polarity to normally open.') # put ourselves on the dbus self._dbusservice = VeDbusService('com.victronenergy.pump.startstop0') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareversion, connection='pump', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # State: None = invalid, 0 = stopped, 1 = running self._dbusservice.add_path('/State', value=0) self._dbusservice.add_path('/AvailableTankServices', value=None) self._dbusservice.add_path('/ActiveTankService', value=None) self._update_relay() self._handleservicechange() else: if self._dbusservice is not None: self._stop_pump() self._dbusservice.__del__() self._dbusservice = None self._relay_state_import = None logger.info('Relay function is no longer set to pump startstop: made sure pump is off and going off dbus') def _device_added(self, dbusservicename, instance): self._handleservicechange() self._evaluate_if_we_are_needed() def _device_removed(self, dbusservicename, instance): self._handleservicechange() # Relay handling depends on systemcalc, if the service disappears restart # the relay state import if dbusservicename == "com.victronenergy.system": self._relay_state_import = None self._evaluate_if_we_are_needed() def _dbus_value_changed(self, dbusServiceName, dbusPath, options, changes, deviceInstance): if dbusPath == '/Settings/Relay/Function': self._evaluate_if_we_are_needed() self._changed = True # Update relay state when polarity changes if dbusPath == '/Settings/Relay/Polarity': self._update_relay() def _handle_changed_setting(self, setting, oldvalue, newvalue): self._changed = True self._evaluate_if_we_are_needed() if setting == "tankservice": self._handleservicechange() if setting == 'autostart': logger.info('Autostart function %s.' % ('enabled' if newvalue == 1 else 'disabled')) def _dbus_name_owner_changed(self, name, oldowner, newowner): return True def _handletimertick(self): # try catch, to make sure that we kill ourselves on an error. Without this try-catch, there would # be an error written to stdout, and then the timer would not be restarted, resulting in a dead- # lock waiting for manual intervention -> not good! try: if self._dbusservice is not None: self._evaluate_startstop_conditions() self._changed = False except: self._stop_pump() import traceback traceback.print_exc() sys.exit(1) return True def _evaluate_startstop_conditions(self): if self._settings['tankservice'] == self.TANKSERVICE_NOTANK: self._stop_pump() return value = self._dbusmonitor.get_value(self._tankservice, "/Level") startvalue = self._settings['startvalue'] stopvalue = self._settings['stopvalue'] started = self._dbusservice['/State'] == 1 mode = self._settings['mode'] # On mode if mode == 1: if not started: self._start_pump() self._current_retries = 0 return # Off mode if mode == 2: if started: self._stop_pump() self._current_retries = 0 return # Auto mode, in case of an invalid reading start the retrying mechanism if started and value is None and mode == 0: # Keep the pump running during RETRIES_ON_ERROR(default 300) retries if started and self._current_retries < self.RETRIES_ON_ERROR: self._current_retries += 1 logger.info("Unable to get tank level, retrying (%i)" % self._current_retries) return # Stop the pump after RETRIES_ON_ERROR(default 300) retries logger.info("Unable to get tank level after %i retries, stopping pump." % self._current_retries) self._stop_pump() return if self._current_retries > 0 and value is not None: logger.info("Tank level successfuly obtained after %i retries." % self._current_retries) self._current_retries = 0 # Tank level not valid, check if is the first invalid reading # and print a log message if value is None: if self._valid_tank_level: self._valid_tank_level = False logger.info("Unable to get tank level, skipping evaluation.") return # Valid reading after a previous invalid one elif value is not None and not self._valid_tank_level: self._valid_tank_level = True logger.info("Tank level successfuly obtained, resuming evaluation.") start_is_greater = startvalue > stopvalue start = started or (value >= startvalue if start_is_greater else value <= startvalue) stop = value <= stopvalue if start_is_greater else value >= stopvalue if start and not stop: self._start_pump() else: self._stop_pump() def _determinetankservice(self): s = self._settings['tankservice'].split('/') if len(s) != 2: logger.error("The tank setting (%s) is invalid!" % self._settings['tankservice']) serviceclass = s[0] instance = int(s[1]) if len(s) == 2 else None services = self._dbusmonitor.get_service_list(classfilter=serviceclass) if instance not in services.values(): # Once chosen tank does not exist. Don't auto change the setting (it might come # back). And also don't autoselect another. newtankservice = None else: # According to https://www.python.org/dev/peps/pep-3106/, dict.keys() and dict.values() # always have the same order. newtankservice = services.keys()[services.values().index(instance)] if newtankservice != self._tankservice: services = self._dbusmonitor.get_service_list() instance = services.get(newtankservice, None) if instance is None: tank_service = None else: tank_service = self._get_instance_service_name(newtankservice, instance) self._dbusservice['/ActiveTankService'] = newtankservice logger.info("Tank service, setting == %s, changed from %s to %s (%s)" % (self._settings['tankservice'], self._tankservice, newtankservice, instance)) self._tankservice = newtankservice def _handleservicechange(self): services = self._get_connected_service_list('com.victronenergy.tank') ul = {self.TANKSERVICE_NOTANK: 'No tank sensor'} for servicename, instance in services.items(): key = self._get_instance_service_name(servicename, instance) ul[key] = self._get_readable_service_name(servicename) self._dbusservice['/AvailableTankServices'] = dbus.Dictionary(ul, signature='sv') self._determinetankservice() def _get_readable_service_name(self, servicename): fluidType = ['Fuel', 'Fresh water', 'Waste water', 'Live well', 'Oil', 'Black water'] fluid = fluidType[self._dbusmonitor.get_value(servicename, '/FluidType')] return (fluid + ' on ' + self._dbusmonitor.get_value(servicename, '/Mgmt/Connection')) def _get_instance_service_name(self, service, instance): return '%s/%s' % ('.'.join(service.split('.')[0:3]), instance) def _get_connected_service_list(self, classfilter=None): services = self._dbusmonitor.get_service_list(classfilter=classfilter) return services def _start_pump(self): if not self._relay_state_import: logger.info("Relay import not available, can't start pump by %s condition" % condition) return systemcalc_relay_state = 0 state = self._dbusservice['/State'] try: systemcalc_relay_state = self._relay_state_import.get_value() except dbus.exceptions.DBusException: logger.info('Error getting relay state') # This function will start the pump in the case the pump not # already running. if state == 0 or systemcalc_relay_state != state: self._dbusservice['/State'] = 1 self._update_relay() self._starttime = time.time() logger.info('Starting pump') def _stop_pump(self): if not self._relay_state_import: logger.info("Relay import not available, can't stop the pump") return systemcalc_relay_state = 1 state = self._dbusservice['/State'] try: systemcalc_relay_state = self._relay_state_import.get_value() except dbus.exceptions.DBusException: logger.info('Error getting relay state') if state == 1 or systemcalc_relay_state != state: self._dbusservice['/State'] = 0 logger.info('Stopping pump') self._update_relay() def _update_relay(self): if not self._relay_state_import: logger.info("Relay import not available") return # Relay polarity 0 = NO, 1 = NC polarity = bool(self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Polarity')) w = int(not polarity) if bool(self._dbusservice['/State']) else int(polarity) try: self._relay_state_import.set_value(dbus.Int32(w, variant_level=1)) except dbus.exceptions.DBusException: logger.info('Error setting relay state')
def new_service(base, type, physical, logical, id, instance, settingId=False): self = VeDbusService("{}.{}.{}_id{:02d}".format(base, type, physical, id), dbusconnection()) # physical is the physical connection # logical is the logical connection to allign with the numbering of the console display # Create the management objects, as specified in the ccgx dbus-api document self.add_path('/Mgmt/ProcessName', __file__) self.add_path( '/Mgmt/ProcessVersion', 'Unkown version, and running on Python ' + platform.python_version()) self.add_path('/Mgmt/Connection', logical) # Create the mandatory objects, note these may need to be customised after object creation self.add_path('/DeviceInstance', instance) self.add_path('/ProductId', 0) self.add_path('/ProductName', '') self.add_path('/FirmwareVersion', 0) self.add_path('/HardwareVersion', 0) self.add_path('/Connected', 0) # Mark devices as disconnected until they are confirmed # Create device type specific objects set values to empty until connected if settingId: setting = "/Settings/" + type.capitalize() + "/" + str(settingId) else: print("no setting required") setting = "" if type == 'temperature': self.add_path('/Temperature', []) self.add_path('/Status', 0) if settingId: addSetting(setting, '/TemperatureType', self) addSetting(setting, '/CustomName', self) self.add_path( '/TemperatureType', 0, writeable=True, onchangecallback=lambda x, y: handle_changed_value(setting, x, y)) self.add_path( '/CustomName', '', writeable=True, onchangecallback=lambda x, y: handle_changed_value(setting, x, y)) self.add_path('/Function', 1, writeable=True) if 'adc' in physical: if settingId: addSetting(setting, '/Scale', self) addSetting(setting, '/Offset', self) self.add_path('/Scale', 1.0, writeable=True, onchangecallback=lambda x, y: handle_changed_value( setting, x, y)) self.add_path('/Offset', 0, writeable=True, onchangecallback=lambda x, y: handle_changed_value( setting, x, y)) if type == 'humidity': self.add_path('/Humidity', []) self.add_path('/Status', 0) return self
class DbusDummyService: def __init__(self, servicename, deviceinstance, paths, productname='Fronius Smart Meter', connection='Fronius Smart Meter service'): self._dbusservice = VeDbusService(servicename) self._paths = paths logging.debug("%s /DeviceInstance = %d" % (servicename, deviceinstance)) # Create the management objects, as specified in the ccgx dbus-api document self._dbusservice.add_path('/Mgmt/ProcessName', __file__) self._dbusservice.add_path('/Mgmt/ProcessVersion', 'Unkown version, and running on Python ' + platform.python_version()) self._dbusservice.add_path('/Mgmt/Connection', connection) # Create the mandatory objects self._dbusservice.add_path('/DeviceInstance', deviceinstance) self._dbusservice.add_path('/ProductId', 16) # value used in ac_sensor_bridge.cpp of dbus-cgwacs self._dbusservice.add_path('/ProductName', productname) self._dbusservice.add_path('/FirmwareVersion', 0.1) self._dbusservice.add_path('/HardwareVersion', 0) self._dbusservice.add_path('/Connected', 1) for path, settings in self._paths.iteritems(): self._dbusservice.add_path( path, settings['initial'], writeable=True, onchangecallback=self._handlechangedvalue) gobject.timeout_add(200, self._update) # pause 200ms before the next request def _update(self): URL = "http://10.194.65.143/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0&DataCollection=MeterRealtimeData" meter_r = requests.get(url = URL) meter_data = meter_r.json() MeterConsumption = meter_data['Body']['Data']['PowerReal_P_Sum'] self._dbusservice['/Ac/Power'] = MeterConsumption # positive: consumption, negative: feed into grid self._dbusservice['/Ac/L1/Voltage'] = meter_data['Body']['Data']['Voltage_AC_Phase_1'] self._dbusservice['/Ac/L2/Voltage'] = meter_data['Body']['Data']['Voltage_AC_Phase_2'] self._dbusservice['/Ac/L3/Voltage'] = meter_data['Body']['Data']['Voltage_AC_Phase_3'] self._dbusservice['/Ac/L1/Current'] = meter_data['Body']['Data']['Current_AC_Phase_1'] self._dbusservice['/Ac/L2/Current'] = meter_data['Body']['Data']['Current_AC_Phase_2'] self._dbusservice['/Ac/L3/Current'] = meter_data['Body']['Data']['Current_AC_Phase_3'] self._dbusservice['/Ac/L1/Power'] = meter_data['Body']['Data']['PowerReal_P_Phase_1'] self._dbusservice['/Ac/L2/Power'] = meter_data['Body']['Data']['PowerReal_P_Phase_2'] self._dbusservice['/Ac/L3/Power'] = meter_data['Body']['Data']['PowerReal_P_Phase_3'] self._dbusservice['/Ac/Energy/Forward'] = meter_data['Body']['Data']['EnergyReal_WAC_Sum_Consumed'] self._dbusservice['/Ac/Energy/Reverse'] = meter_data['Body']['Data']['EnergyReal_WAC_Sum_Produced'] logging.info("House Consumption: %s" % (MeterConsumption)) return True def _handlechangedvalue(self, path, value): logging.debug("someone else updated %s to %s" % (path, value)) return True # accept the change
parser.add_argument("-d", "--debug", help="set logging level to debug", action="store_true") args = parser.parse_args() # Init logging logging.basicConfig(level=(logging.DEBUG if args.debug else logging.INFO)) logging.info(__file__ + " is starting up") logLevel = {0: 'NOTSET', 10: 'DEBUG', 20: 'INFO', 30: 'WARNING', 40: 'ERROR'} logging.info('Loglevel set to ' + logLevel[logging.getLogger().getEffectiveLevel()]) # Have a mainloop, so we can send/receive asynchronous calls to and from dbus DBusGMainLoop(set_as_default=True) dbusservice = VeDbusService(args.name) logging.info("using device instance %s" % args.deviceinstance) # Create the management objects, as specified in the ccgx dbus-api document dbusservice.add_path('/Management/ProcessName', __file__) dbusservice.add_path('/Management/ProcessVersion', 'Unkown version, and running on Python ' + platform.python_version()) dbusservice.add_path('/Management/Connection', 'dummy data') # Create the mandatory objects dbusservice.add_path('/DeviceInstance', args.deviceinstance) dbusservice.add_path('/ProductId', 0) dbusservice.add_path('/ProductName', 'Dummy battery') dbusservice.add_path('/FirmwareVersion', 0) dbusservice.add_path('/HardwareVersion', 0) dbusservice.add_path('/Connected', 1)
def __init__(self, dev, connection, instance, serial, product, firmwarev, pversion): #VERSION = '0.1' print(__file__ + " starting up") # instance = 50 + 0 # Have a mainloop, so we can send/receive asynchronous calls to and from dbus #DBusGMainLoop(set_as_default=True) #Put ourselves on to the dbus self.dbusservice = VeDbusService('com.victronenergy.pvinverter.' + dev) # Most simple and short way to add an object with an initial value of 5. # dbusservice.add_path('/Ac/Power', value=1000, description='Total power', writeable=False) # dbusservice.add_path('/DeviceType', value=1000, description='Total power', writeable=False) # Add objects required by ve-api self.dbusservice.add_path('/Mgmt/ProcessName', __file__) self.dbusservice.add_path('/Mgmt/ProcessVersion', pversion) self.dbusservice.add_path('/Mgmt/Connection', connection) # todo self.dbusservice.add_path('/DeviceInstance', instance) self.dbusservice.add_path('/ProductId', 0xFFFF) # 0xB012 ? self.dbusservice.add_path('/ProductName', product) #self.dbusservice.add_path('/CustomName', "PLC Mec meter") self.dbusservice.add_path('/FirmwareVersion', firmwarev) self.dbusservice.add_path('/Serial', serial) self.dbusservice.add_path('/Connected', 1, writeable=True) self.dbusservice.add_path('/ErrorCode', '(0) No Error') self.dbusservice.add_path('/Position', 0) _kwh = lambda p, v: (str(v) + 'KWh') _a = lambda p, v: (str(v) + 'A') _w = lambda p, v: (str(v) + 'W') _v = lambda p, v: (str(v) + 'V') _s = lambda p, v: (str(v) + 's') _x = lambda p, v: (str(v)) self.dbusservice.add_path('/Ac/Energy/Forward', None, gettextcallback=_kwh) self.dbusservice.add_path('/Ac/L1/Current', None, gettextcallback=_a) self.dbusservice.add_path('/Ac/L1/Energy/Forward', None, gettextcallback=_kwh) self.dbusservice.add_path('/Ac/L1/Power', None, gettextcallback=_w) self.dbusservice.add_path('/Ac/L1/Voltage', None, gettextcallback=_v) self.dbusservice.add_path('/Ac/L2/Current', None, gettextcallback=_a) self.dbusservice.add_path('/Ac/L2/Energy/Forward', None, gettextcallback=_kwh) self.dbusservice.add_path('/Ac/L2/Power', None, gettextcallback=_w) self.dbusservice.add_path('/Ac/L2/Voltage', None, gettextcallback=_v) self.dbusservice.add_path('/Ac/L3/Current', None, gettextcallback=_a) self.dbusservice.add_path('/Ac/L3/Energy/Forward', None, gettextcallback=_kwh) self.dbusservice.add_path('/Ac/L3/Power', None, gettextcallback=_w) self.dbusservice.add_path('/Ac/L3/Voltage', None, gettextcallback=_v) self.dbusservice.add_path('/Ac/Power', None, gettextcallback=_w) self.dbusservice.add_path('/Ac/Current', None, gettextcallback=_a) self.dbusservice.add_path('/Ac/Voltage', None, gettextcallback=_v) self.dbusservice.add_path('/stats/connection_ok', 0, gettextcallback=_x, writeable=True) self.dbusservice.add_path('/stats/connection_error', 0, gettextcallback=_x, writeable=True) self.dbusservice.add_path('/stats/parse_error', 0, gettextcallback=_x, writeable=True) self.dbusservice.add_path('/stats/repeated_values', 0, gettextcallback=_x, writeable=True) self.dbusservice.add_path('/stats/last_connection_errors', 0, gettextcallback=_x, writeable=True) self.dbusservice.add_path('/stats/last_repeated_values', 0, gettextcallback=_x, writeable=True) self.dbusservice.add_path('/stats/reconnect', 0, gettextcallback=_x) self.dbusservice.add_path('/Mgmt/intervall', 1, gettextcallback=_s, writeable=True)