def __init__(self,mqqthost,nodeid,service,base_topic='emonhub'): self.d = DbusMonitor(datalist.vrmtree, self._value_changed_on_dbus) """ self.ttl = 300 # seconds to keep sending the updates. If no keepAlive received in this # time - stop sending self.lastKeepAliveRcd = int(time.time()) - self.ttl # initialised to when this code launches """ self.ttm = 0 # seconds to gather data before sending, 0 - send immediate, NN - gather # for NN seconds self._last_publish = int(time.time() - 60) # Just an initialisation value, so the first # ttm timeout is _now_ self._gathered_data_timer = None self._gathered_data = {} self._mqtt = mqtt.Client(client_id=get_vrm_portal_id(), clean_session=True, userdata=None) self._mqtt.loop_start() # creates new thread and runs Mqtt.loop_forever() in it. self._mqtt.on_connect = self._on_connect self._mqtt.on_message = self._on_message self._mqtt.connect_async(mqqthost, port=1883, keepalive=60, bind_address="") self._service_name = service self._nodeid = nodeid #node ide expected by emonhub . need to modigy emonhub.conf accordingly self._topic = base_topic message = '{basetopic}/tx/{nodeid}/values' self._publishPath = message.format(basetopic=self._topic,nodeid=self._nodeid)
def __init__(self): self._dbusservice = None self._batteryservice = None self._settings = SettingsDevice( bus=dbus.SystemBus() if (platform.machine() == 'armv7l') else dbus.SessionBus(), supportedSettings={ 'batteryinstance': ['/Settings/Generator/BatteryInstance', 0, 0, 1000], 'running': ['/Settings/Generator/Running', 0, 0, 1], 'autostopsoc': ['/Settings/Generator/AutoStopSOC', 90, 0, 100], 'autostartsoc': ['/Settings/Generator/AutoStartSOC', 10, 0, 100], 'autostartcurrent': ['/Settings/Generator/AutoStartCurrent', 0, 0, 500] }, eventCallback=self._handle_changed_setting) # 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.battery': { '/Dc/0/I': dummy, '/Soc': dummy}, 'com.victronenergy.settings': { '/Settings/Relay/Function': dummy} # This is not our setting so do it here. not in supportedSettings }, self._dbus_value_changed, self._device_added, self._device_removed) self._evaluate_if_we_are_needed()
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
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]
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
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)
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, '/ProductId': 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, '/Connected': dummy, '/Hub4/AcPowerSetpoint': dummy, '/ProductId': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Mode': dummy, '/State': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy, '/Dc/0/Power': dummy, '/Soc': dummy }, 'com.victronenergy.charger': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy }, 'com.victronenergy.grid': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/ProductId': dummy, '/DeviceType': dummy, '/Ac/L1/Power': dummy, '/Ac/L2/Power': dummy, '/Ac/L3/Power': dummy }, 'com.victronenergy.genset': { '/Connected': dummy, '/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(), gettextcallback=lambda x: str(x)) self._dbusservice.add_path('/Relay/0/State', value=None, writeable=True, onchangecallback=lambda p, v: exit_on_error( self._on_relay_state_changed, p, v)) self._dbusservice.add_path('/AvailableBatteryServices', value=None, gettextcallback=self._gettext) self._dbusservice.add_path('/AvailableBatteryMeasurements', value=None) 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._dbusservice.add_path('/Dc/Battery/Alarms/CircuitBreakerTripped', 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() self._supervised = {} self._lg_battery = None 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() try: self._relay_file_read = open(relayGpioFile, 'rt') self._relay_file_write = open(relayGpioFile, 'wt') self._update_relay_state() gobject.timeout_add(5000, exit_on_error, self._update_relay_state) except IOError: self._relay_file_read = None self._relay_file_write = None logging.warn('Could not open %s (relay)' % relayGpioFile) self._writeVebusSocCounter = 9 gobject.timeout_add(1000, exit_on_error, self._handletimertick) gobject.timeout_add(60000, exit_on_error, self._process_supervised)
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, '/ProductId': 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, '/Connected': dummy, '/Hub4/AcPowerSetpoint': dummy, '/ProductId': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Mode': dummy, '/State': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy, '/Dc/0/Power': dummy, '/Soc': dummy }, 'com.victronenergy.charger': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/Dc/0/Voltage': dummy, '/Dc/0/Current': dummy }, 'com.victronenergy.grid': { '/Connected': dummy, '/ProductName': dummy, '/Mgmt/Connection': dummy, '/ProductId': dummy, '/DeviceType': dummy, '/Ac/L1/Power': dummy, '/Ac/L2/Power': dummy, '/Ac/L3/Power': dummy }, 'com.victronenergy.genset': { '/Connected': dummy, '/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(), gettextcallback=lambda x: str(x)) self._dbusservice.add_path('/Relay/0/State', value=None, writeable=True, onchangecallback=lambda p, v: exit_on_error( self._on_relay_state_changed, p, v)) self._dbusservice.add_path('/AvailableBatteryServices', value=None, gettextcallback=self._gettext) self._dbusservice.add_path('/AvailableBatteryMeasurements', value=None) 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._dbusservice.add_path('/Dc/Battery/Alarms/CircuitBreakerTripped', 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() self._supervised = {} self._lg_battery = None 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() try: self._relay_file_read = open(relayGpioFile, 'rt') self._relay_file_write = open(relayGpioFile, 'wt') self._update_relay_state() gobject.timeout_add(5000, exit_on_error, self._update_relay_state) except IOError: self._relay_file_read = None self._relay_file_write = None logging.warn('Could not open %s (relay)' % relayGpioFile) self._writeVebusSocCounter = 9 gobject.timeout_add(1000, exit_on_error, self._handletimertick) gobject.timeout_add(60000, exit_on_error, self._process_supervised) 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, just use a random one. 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. batteries = self._get_connected_service_list( 'com.victronenergy.battery') if len(batteries) > 0: return sorted(batteries)[0] # Pick a random battery service if self._get_first_connected_service( 'com.victronenergy.solarcharger') is not None: return None if self._get_first_connected_service( 'com.victronenergy.charger') is not None: return None vebus_services = self._get_first_connected_service( 'com.victronenergy.vebus') if vebus_services is None: return None return vebus_services[0] # 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 is not None and pid not in productids: productids.append(pid) self._dbusservice['/PvInvertersProductIds'] = dbus.Array(productids, signature='i') 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 = self._get_first_connected_service('com.victronenergy.vebus') multi_path = None if multi is not None: multi_path = multi[0] 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() energy_meter = self._get_first_connected_service(servicename) em_service = None if energy_meter is None else energy_meter[0] 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 = None 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] = _safeadd(consumption[phase], _safemax(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) product_id = None device_type_id = None if em_service is not None: product_id = self._dbusmonitor.get_value( em_service, '/ProductId') device_type_id = self._dbusmonitor.get_value( em_service, '/DeviceType') if product_id is None and uses_active_input: product_id = self._dbusmonitor.get_value( multi_path, '/ProductId') newvalues['/Ac/%s/ProductId' % device_type] = product_id newvalues['/Ac/%s/DeviceType' % device_type] = device_type_id for phase in consumption: c = newvalues.get('/Ac/PvOnOutput/%s/Power' % phase) 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] = _safeadd( consumption[phase], _safemax(0, c)) self._compute_phase_totals('/Ac/Consumption', newvalues) self._check_lg_battery(multi_path) # ==== 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._get_connected_service_list('com.victronenergy.vebus') services.update( self._get_connected_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 _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 _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 ['/Connected', '/ProductName', '/Mgmt/Connection'] or (dbusPath == '/State' and dbusServiceName.split('.')[0:3] == ['com', 'victronenergy', 'vebus'])): 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() service_type = service.split('.')[2] if service_type == 'battery' or service_type == 'solarcharger': try: proxy = self._dbusmonitor.dbusConn.get_object(service, '/ProductId', introspect=False) method = proxy.get_dbus_method('GetValue') self._supervised[service] = method except dbus.DBusException: pass if service_type == 'battery' and self._dbusmonitor.get_value( service, '/ProductId') == 0xB004: logging.info('LG battery service appeared: %s' % service) self._lg_battery = service self._lg_voltage_buffer = [] self._dbusservice['/Dc/Battery/Alarms/CircuitBreakerTripped'] = 0 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() if service in self._supervised: del self._supervised[service] if service == self._lg_battery: logging.info('LG battery service disappeared: %s' % service) self._lg_battery = None self._lg_voltage_buffer = None self._dbusservice[ '/Dc/Battery/Alarms/CircuitBreakerTripped'] = None 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 str(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_connected_service_list(self, classfilter=None): services = self._dbusmonitor.get_service_list(classfilter=classfilter) self._remove_unconnected_services(services) return services def _get_first_connected_service(self, classfilter=None): services = self._get_connected_service_list(classfilter=classfilter) if len(services) == 0: return None return services.items()[0] def _process_supervised(self): for service, method in self._supervised.items(): # Do an async call. If the owner of the service does not answer, we do not want to wait for # the timeout here. method.call_async(error_handler=lambda x: exit_on_error( self._supervise_failed, service, x)) return True def _supervise_failed(self, service, error): try: if error.get_dbus_name() != 'org.freedesktop.DBus.Error.NoReply': logging.info('Ignoring supervise error from %s: %s' % (service, error)) return logging.error('%s is not responding to D-Bus requests' % service) proxy = self._dbusmonitor.dbusConn.get_object( 'org.freedesktop.DBus', '/', introspect=False) pid = proxy.GetConnectionUnixProcessID(service) if pid is not None and pid > 1: logging.error('killing owner of %s (pid=%s)' % (service, pid)) os.kill(pid, signal.SIGKILL) except (OSError, dbus.exceptions.DBusException): print_exc() def _update_relay_state(self): state = None try: self._relay_file_read.seek(0) state = int(self._relay_file_read.read().strip()) except (IOError, ValueError): print_exc() self._dbusservice['/Relay/0/State'] = state return True def _on_relay_state_changed(self, path, value): if self._relay_file_write is None: return False try: v = int(value) if v < 0 or v > 1: return False self._relay_file_write.write(str(v)) self._relay_file_write.flush() return True except (IOError, ValueError): print_exc() return False def _check_lg_battery(self, multi_path): if self._lg_battery is None or multi_path is None: return battery_current = self._dbusmonitor.get_value(self._lg_battery, '/Dc/0/Current') if battery_current is None or abs(battery_current) > 0.01: if len(self._lg_voltage_buffer) > 0: logging.debug('LG voltage buffer reset') self._lg_voltage_buffer = [] return vebus_voltage = self._dbusmonitor.get_value(multi_path, '/Dc/0/Voltage') if vebus_voltage is None: return self._lg_voltage_buffer.append(float(vebus_voltage)) if len(self._lg_voltage_buffer) > 40: self._lg_voltage_buffer = self._lg_voltage_buffer[-40:] elif len(self._lg_voltage_buffer) < 20: return min_voltage = min(self._lg_voltage_buffer) max_voltage = max(self._lg_voltage_buffer) battery_voltage = self._dbusmonitor.get_value(self._lg_battery, '/Dc/0/Voltage') logging.debug('LG battery current V=%s I=%s' % (battery_voltage, battery_current)) if min_voltage < 0.9 * battery_voltage or max_voltage > 1.1 * battery_voltage: logging.error( 'LG shutdown detected V=%s I=%s %s' % (battery_voltage, battery_current, self._lg_voltage_buffer)) item = self._dbusmonitor.get_item(multi_path, '/Mode') if item is None: logging.error('Cannot switch off vebus device') else: self._dbusservice[ '/Dc/Battery/Alarms/CircuitBreakerTripped'] = 2 item.set_value(dbus.Int32(4, variant_level=1)) self._lg_voltage_buffer = []
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
# callback that gets called every time a dbus value has changed def _dbus_value_changed(dbusServiceName, dbusPath, dict, changes, deviceInstance): pass # 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.system': { '/Dc/Battery/Soc': dummy, } } dbusmonitor = DbusMonitor(dbus_tree, valueChangedCallback=_dbus_value_changed) # connect and register to dbus driver = { 'name': "Enphase Envoy", 'servicename': "enphase_envoy", 'instance': 263, 'id': 126, 'version': 478, } def create_dbus_service(): dbusservice = VeDbusService('com.victronenergy.pvinverter.envoy') dbusservice.add_mandatory_paths( processname=__file__,
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
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)
class DbusMqtt: def __init__(self,mqqthost,nodeid,service,base_topic='emonhub'): self.d = DbusMonitor(datalist.vrmtree, self._value_changed_on_dbus) """ self.ttl = 300 # seconds to keep sending the updates. If no keepAlive received in this # time - stop sending self.lastKeepAliveRcd = int(time.time()) - self.ttl # initialised to when this code launches """ self.ttm = 0 # seconds to gather data before sending, 0 - send immediate, NN - gather # for NN seconds self._last_publish = int(time.time() - 60) # Just an initialisation value, so the first # ttm timeout is _now_ self._gathered_data_timer = None self._gathered_data = {} self._mqtt = mqtt.Client(client_id=get_vrm_portal_id(), clean_session=True, userdata=None) self._mqtt.loop_start() # creates new thread and runs Mqtt.loop_forever() in it. self._mqtt.on_connect = self._on_connect self._mqtt.on_message = self._on_message self._mqtt.connect_async(mqqthost, port=1883, keepalive=60, bind_address="") self._service_name = service self._nodeid = nodeid #node ide expected by emonhub . need to modigy emonhub.conf accordingly self._topic = base_topic message = '{basetopic}/tx/{nodeid}/values' self._publishPath = message.format(basetopic=self._topic,nodeid=self._nodeid) def get_service_values(self): """ Hackish way to send BMV data on dbus to MQTT for EMON """ current_vals = self.d.get_values_for_service(['onIntervalAlways'],self._service_name) payload = [] #Create String that Emon will like for val in current_vals: payload.append(str(current_vals[val])) #Emon Expects , between data point objects payload_str = ", ".join(payload) logger.debug('Sending payload data %s ' % (payload_str)) #publish away self._publish(payload_str,self._topic) # servicename: for example com.victronenergy.dbus.ttyO1 # path: for example /Ac/ActiveIn/L1/V # props: the dictionary containing the properties from the vrmTree # changes: the changes, a tuple with GetText() and GetValue() # instance: the deviceInstance def _value_changed_on_dbus(self, servicename, path, props, changes, instance): # if not self.someone_watching(): # return self._gathered_data[props["code"] + str(instance)] = { 'code': props["code"], 'instance': instance, 'value': str(changes['Value'])} if self.ttm: # != 0, ie. gather before sending logger.debug('Got data from DBUS checking ttm') if self._marshall_says_go(): self._publish(str(self._gathered_data),self._topic) elif self._gathered_data_timer is None: # Set timer, to make sure that this data will not reside in this queue for longer # than ttm-time self._gathered_data_timer = gobject.timeout_add( self.ttm * 1000, exit_on_error, self._publish) else: # send immediate logger.debug('collected data : %s'%self._gathered_data) logger.debug('sending data') self._publish(str(self._gathered_data),self._topic) def _on_message(self, client, userdata, msg): logger.debug('message! userdata: %s, message %s' % (userdata, msg.topic+" "+str(msg.payload))) def _on_connect(self, client, userdata, flags, rc): """ RC definition: 0: Connection successful 1: Connection refused - incorrect protocol version 2: Connection refused - invalid client identifier 3: Connection refused - server unavailable 4: Connection refused - bad username or password 5: Connection refused - not authorised 6-255: Currently unused. """ logger.debug('connected! client=%s, userdata=%s, flags=%s, rc=%s' % (client, userdata, flags, rc)) # Subscribing in on_connect() means that if we lose the connection and # reconnect then subscriptions will be renewed. # client.subscribe("$SYS/#") def _someone_watching(self): return True # (self.lastKeepAliveRcd + self.ttl) > int(time.time()) def _marshall_says_go(self): return (self._last_publish + self.ttm) < int(time.time()) def _publish(self,payload,topic): self._last_publish = int(time.time()) """ message = {'dataUpdate': self.gatheredData.values()} self.gatheredData = {} if self.gatheredDataTimer is not None: gobject.source_remove(self.gatheredDataTimer) self.gatheredDataTimer = None logger.debug("Sending dataUpdate, fired by timer is %s" % firedbytimer) send_to_pubnub('livefeed', message) return False # let gobject know that it is not necessary to fire us again. """ topic = self._publishPath logger.debug('publishing on topic "%s", data "%s"' % (topic, payload)) self._mqtt.publish(topic, payload=payload, qos=0, retain=False)
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
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 __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)
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')
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 _create_dbus_monitor(self, *args, **kwargs): return DbusMonitor(*args, **kwargs)
class DbusGenerator: def __init__(self): self._dbusservice = None self._batteryservice = None self._settings = SettingsDevice( bus=dbus.SystemBus() if (platform.machine() == 'armv7l') else dbus.SessionBus(), supportedSettings={ 'batteryinstance': ['/Settings/Generator/BatteryInstance', 0, 0, 1000], 'running': ['/Settings/Generator/Running', 0, 0, 1], 'autostopsoc': ['/Settings/Generator/AutoStopSOC', 90, 0, 100], 'autostartsoc': ['/Settings/Generator/AutoStartSOC', 10, 0, 100], 'autostartcurrent': ['/Settings/Generator/AutoStartCurrent', 0, 0, 500] }, eventCallback=self._handle_changed_setting) # 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.battery': { '/Dc/0/I': dummy, '/Soc': dummy}, 'com.victronenergy.settings': { '/Settings/Relay/Function': dummy} # This is not our setting so do it here. not in supportedSettings }, self._dbus_value_changed, self._device_added, self._device_removed) self._evaluate_if_we_are_needed() # Call this function on startup, when settings change or services (dis)appear def _evaluate_if_we_are_needed(self): # 0 == Alarm relay, 1 == Generator start/stop # Don't touch the relay when it is not ours! if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 1: forcebatterymessage = False # todo, this can probably be coded cleaner.. if self._dbusservice is None: logging.info("Action! Going on dbus and taking control of the relay.") forcebatterymessage = True # self._dbusservice = VeDbusService('com.victronenergy.generator.startstop0') # self._dbusservice.add_mandatory_paths( # processname=__file__, # processversion='v%s, on Python %s' % (softwareversion, platform.python_version()), # connection='CCGX relay and Bat. instance %s' % self._settings['batteryinstance'], # deviceinstance=0, # productid=0, # productname='Genset start/stop', # firmwareversion=0, # hardwareversion=0, # connected=1) # Create our own paths # State: None = invalid, 0 = stopped, 1 = running # self._dbusservice.add_path('/State', None) # Error: None = invalid, 0 = no error, 1 = no battery instance to use, genset stopped # TODO, do we want an error for invalid soc?, genset stopped (? or do some other logic?) # self._dbusservice.add_path('/Error', None) # Is our battery instance available? batteries = self._dbusmonitor.get_service_list('com.victronenergy.battery') if self._settings['batteryinstance'] in batteries: if self._batteryservice is None or forcebatterymessage: logging.info("Battery instance we need (%s) found! Using it for genset start/stop" % self._settings['batteryinstance']) # self._dbusservice['/Error'] = 0 self._batteryservice = batteries[self._settings['batteryinstance']] else: if self._batteryservice is not None or forcebatterymessage: logging.info("Battery instance (%s) not on the dbus (anymore), in case the genset was \ running, it will be stopped now" % self._settings['batteryinstance']) # self._dbusservice['/Error'] = 1 # Genset will be stopped by _evaluate_startstop_conditions(), since soc will be None self._batteryservice = None self._evaluate_startstop_conditions() else: if self._dbusservice is not None: # First stop the genset, so this is also signalled via the dbus self.stop_genset() # self._dbusService.__del__() self._dbusService = None self._batteryservice = None logging.info("Relay function is no longer set to genset start/stop: made sure genset 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): logging.debug("value changed! %s %s %s" % (dbusServiceName, dbusPath, changes)) if dbusServiceName == self._batteryservice and deviceInstance == self._settings['batteryinstance']: self._evaluate_startstop_conditions() elif dbusServiceName == 'com.victronenergy.settings': self._evaluate_if_we_are_needed() def _handle_changed_setting(self, setting, oldvalue, newvalue): logging.info("handle changed setting called") self._evaluate_startstop_conditions() def _evaluate_startstop_conditions(self): global genlaststate logging.info("soc: %s" % (self.battery_soc())) if self.battery_soc() is None: logging.warning("invalid soc! stopping genset!") self.stop_genset() return if (genlaststate != self._settings['running']): if (self._settings['running'] == 0): logging.info("manual stop") self.stop_genset() genlaststate = 0 return else: if (self.battery_soc() < self._settings['autostopsoc']): logging.info("manual start") self.start_genset() genlaststate = 1 return else: self.stop_genset() genlaststate = 0 return return return # check that the setting is not 0=disabled and then check the values if (self._settings['autostartsoc'] != 0 and self.battery_soc() <= self._settings['autostartsoc']): self.start_genset() # check that the setting is not 0=disabled and then check the values if (self._settings['autostopsoc'] != 0 and self.battery_soc() >= self._settings['autostopsoc']): self.stop_genset() def battery_soc(self): soc = self._dbusmonitor.get_value(self._batteryservice, '/Soc') return int(soc) if soc is not None else None def battery_current(self): current = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/I') return int(current) if current is not None else None def start_genset(self): global genlaststate logging.info("Turning on relay and starting genset") relayfile = file(relayFile, 'w') relayfile.write('1\n') relayfile.close() system("/usr/bin/dbus -y com.victronenergy.settings /Settings/Generator/Running SetValue 1") genlaststate = 1 def stop_genset(self): global genlaststate logging.info("Turning off relay and stopping genset") relayfile = file(relayFile, 'w') relayfile.write('0\n') relayfile.close() system("/usr/bin/dbus -y com.victronenergy.settings /Settings/Generator/Running SetValue 0") genlaststate = 0