class PinHandler(object, metaclass=HandlerMaker): product_id = 0xFFFF _product_name = 'Generic GPIO' dbus_name = "digital" def __init__(self, bus, base, path, gpio, settings): self.gpio = gpio self.path = path self.bus = bus self.settings = settings self._level = 0 # Remember last state self.service = VeDbusService("{}.{}.input{:02d}".format( base, self.dbus_name, gpio), bus=bus) # Add objects required by ve-api self.service.add_path('/Mgmt/ProcessName', __file__) self.service.add_path('/Mgmt/ProcessVersion', VERSION) self.service.add_path('/Mgmt/Connection', path) self.service.add_path('/DeviceInstance', gpio) self.service.add_path('/ProductId', self.product_id) self.service.add_path('/ProductName', self.product_name) self.service.add_path('/Connected', 1) # Custom name setting def _change_name(p, v): # This should fire a change event that will update product_name # below. settings['name'] = v return True self.service.add_path('/CustomName', settings['name'], writeable=True, onchangecallback=_change_name) # We'll count the pulses for all types of services self.service.add_path('/Count', value=settings['count']) @property def product_name(self): return self.settings['name'] or self._product_name @product_name.setter def product_name(self, v): # Some pin types don't have an associated service (Disabled pins for # example) if self.service is not None: self.service['/ProductName'] = v or self._product_name def deactivate(self): self.save_count() self.service.__del__() del self.service self.service = None @property def level(self): return self._level @level.setter def level(self, l): self._level = int(bool(l)) def toggle(self, level): # Only increment Count on rising edge. if level and level != self._level: self.service['/Count'] = (self.service['/Count'] + 1) % MAXCOUNT self._level = level def refresh(self): """ Toggle state to last remembered state. This is called if settings are changed so the Service can recalculate paths. """ self.toggle(self._level) def save_count(self): if self.service is not None: self.settings['count'] = self.count @property def active(self): return self.service is not None @property def count(self): return self.service['/Count'] @count.setter def count(self, v): self.service['/Count'] = v @classmethod def createHandler(cls, _type, *args, **kwargs): if _type in cls.handlers: return cls.handlers[_type](*args, **kwargs) return None
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 DbusGenerator: def __init__(self): self._bus = dbus.SystemBus() if (platform.machine() == 'armv7l') else dbus.SessionBus() self.RELAY_GPIO_FILE = '/sys/class/gpio/gpio182/value' self.HISTORY_DAYS = 30 # One second per retry self.RETRIES_ON_ERROR = 300 self._testrun_soc_retries = 0 self._last_counters_check = 0 self._dbusservice = None self._starttime = 0 self._manualstarttimer = 0 self._last_runtime_update = 0 self._timer_runnning = 0 self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None self._battery_measurement_available = True self._vebusservice_high_temperature_import = None self._vebusservice_overload_import = None self._vebusservice = None self._vebusservice_available = False self._condition_stack = { 'batteryvoltage': { 'name': 'batteryvoltage', 'reached': False, 'boolean': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'battery' }, 'batterycurrent': { 'name': 'batterycurrent', 'reached': False, 'boolean': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'battery' }, 'acload': { 'name': 'acload', 'reached': False, 'boolean': False, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'vebus' }, 'inverterhightemp': { 'name': 'inverterhightemp', 'reached': False, 'boolean': True, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'monitoring': 'vebus' }, 'inverteroverload': { 'name': 'inverteroverload', 'reached': False, 'boolean': True, 'timed': True, 'start_timer': 0, 'stop_timer': 0, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'vebus' }, 'soc': { 'name': 'soc', 'reached': False, 'boolean': False, 'timed': False, 'valid': True, 'enabled': False, 'retries': 0, 'monitoring': 'battery' } } # DbusMonitor expects these values to be there, even though we don need them. So just # add some dummy data. This can go away when DbusMonitor is more generic. dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} # TODO: possible improvement: don't use the DbusMonitor it all, since we are only monitoring # a set of static values which will always be available. DbusMonitor watches for services # that come and go, and takes care of automatic signal subscribtions etc. etc: all not necessary # in this use case where we have fixed services names (com.victronenergy.settings, and c # com.victronenergy.system). self._dbusmonitor = DbusMonitor({ 'com.victronenergy.settings': { # This is not our setting so do it here. not in supportedSettings '/Settings/Relay/Function': dummy, '/Settings/Relay/Polarity': dummy, '/Settings/System/TimeZone': dummy, }, 'com.victronenergy.system': { # This is not our setting so do it here. not in supportedSettings '/Ac/Consumption/Total/Power': dummy, '/Ac/PvOnOutput/Total/Power': dummy, '/Ac/PvOnGrid/Total/Power': dummy, '/Ac/PvOnGenset/Total/Power': dummy, '/Dc/Pv/Power': dummy, '/AutoSelectedBatteryMeasurement': dummy, } }, self._dbus_value_changed, self._device_added, self._device_removed) # Set timezone to user selected timezone environ['TZ'] = self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/System/TimeZone') # Connect to localsettings self._settings = SettingsDevice( bus=self._bus, supportedSettings={ 'autostart': ['/Settings/Generator0/AutoStartEnabled', 1, 0, 1], 'accumulateddaily': ['/Settings/Generator0/AccumulatedDaily', '', 0, 0], 'accumulatedtotal': ['/Settings/Generator0/AccumulatedTotal', 0, 0, 0], 'batterymeasurement': ['/Settings/Generator0/BatteryService', "default", 0, 0], 'minimumruntime': ['/Settings/Generator0/MinimumRuntime', 0, 0, 86400], # minutes # On permanent loss of communication: 0 = Stop, 1 = Start, 2 = keep running 'onlosscommunication': ['/Settings/Generator0/OnLossCommunication', 0, 0, 2], # Quiet hours 'quiethoursenabled': ['/Settings/Generator0/QuietHours/Enabled', 0, 0, 1], 'quiethoursstarttime': ['/Settings/Generator0/QuietHours/StartTime', 75600, 0, 86400], 'quiethoursendtime': ['/Settings/Generator0/QuietHours/EndTime', 21600, 0, 86400], # SOC 'socenabled': ['/Settings/Generator0/Soc/Enabled', 0, 0, 1], 'socstart': ['/Settings/Generator0/Soc/StartValue', 90, 0, 100], 'socstop': ['/Settings/Generator0/Soc/StopValue', 90, 0, 100], 'qh_socstart': ['/Settings/Generator0/Soc/QuietHoursStartValue', 90, 0, 100], 'qh_socstop': ['/Settings/Generator0/Soc/QuietHoursStopValue', 90, 0, 100], # Voltage 'batteryvoltageenabled': ['/Settings/Generator0/BatteryVoltage/Enabled', 0, 0, 1], 'batteryvoltagestart': ['/Settings/Generator0/BatteryVoltage/StartValue', 11.5, 0, 150], 'batteryvoltagestop': ['/Settings/Generator0/BatteryVoltage/StopValue', 12.4, 0, 150], 'batteryvoltagestarttimer': ['/Settings/Generator0/BatteryVoltage/StartTimer', 20, 0, 10000], 'batteryvoltagestoptimer': ['/Settings/Generator0/BatteryVoltage/StopTimer', 20, 0, 10000], 'qh_batteryvoltagestart': ['/Settings/Generator0/BatteryVoltage/QuietHoursStartValue', 11.9, 0, 100], 'qh_batteryvoltagestop': ['/Settings/Generator0/BatteryVoltage/QuietHoursStopValue', 12.4, 0, 100], # Current 'batterycurrentenabled': ['/Settings/Generator0/BatteryCurrent/Enabled', 0, 0, 1], 'batterycurrentstart': ['/Settings/Generator0/BatteryCurrent/StartValue', 10.5, 0.5, 1000], 'batterycurrentstop': ['/Settings/Generator0/BatteryCurrent/StopValue', 5.5, 0, 1000], 'batterycurrentstarttimer': ['/Settings/Generator0/BatteryCurrent/StartTimer', 20, 0, 10000], 'batterycurrentstoptimer': ['/Settings/Generator0/BatteryCurrent/StopTimer', 20, 0, 10000], 'qh_batterycurrentstart': ['/Settings/Generator0/BatteryCurrent/QuietHoursStartValue', 20.5, 0, 1000], 'qh_batterycurrentstop': ['/Settings/Generator0/BatteryCurrent/QuietHoursStopValue', 15.5, 0, 1000], # AC load 'acloadenabled': ['/Settings/Generator0/AcLoad/Enabled', 0, 0, 1], 'acloadstart': ['/Settings/Generator0/AcLoad/StartValue', 1600, 5, 100000], 'acloadstop': ['/Settings/Generator0/AcLoad/StopValue', 800, 0, 100000], 'acloadstarttimer': ['/Settings/Generator0/AcLoad/StartTimer', 20, 0, 10000], 'acloadstoptimer': ['/Settings/Generator0/AcLoad/StopTimer', 20, 0, 10000], 'qh_acloadstart': ['/Settings/Generator0/AcLoad/QuietHoursStartValue', 1900, 0, 100000], 'qh_acloadstop': ['/Settings/Generator0/AcLoad/QuietHoursStopValue', 1200, 0, 100000], # VE.Bus high temperature 'inverterhightempenabled': ['/Settings/Generator0/InverterHighTemp/Enabled', 0, 0, 1], 'inverterhightempstarttimer': ['/Settings/Generator0/InverterHighTemp/StartTimer', 20, 0, 10000], 'inverterhightempstoptimer': ['/Settings/Generator0/InverterHighTemp/StopTimer', 20, 0, 10000], # VE.Bus overload 'inverteroverloadenabled': ['/Settings/Generator0/InverterOverload/Enabled', 0, 0, 1], 'inverteroverloadstarttimer': ['/Settings/Generator0/InverterOverload/StartTimer', 20, 0, 10000], 'inverteroverloadstoptimer': ['/Settings/Generator0/InverterOverload/StopTimer', 20, 0, 10000], # TestRun 'testrunenabled': ['/Settings/Generator0/TestRun/Enabled', 0, 0, 1], 'testrunstartdate': ['/Settings/Generator0/TestRun/StartDate', time.time(), 0, 10000000000.1], 'testrunstarttimer': ['/Settings/Generator0/TestRun/StartTime', 54000, 0, 86400], 'testruninterval': ['/Settings/Generator0/TestRun/Interval', 28, 1, 365], 'testrunruntime': ['/Settings/Generator0/TestRun/Duration', 7200, 1, 86400], 'testrunskipruntime': ['/Settings/Generator0/TestRun/SkipRuntime', 0, 0, 100000], 'testruntillbatteryfull': ['/Settings/Generator0/TestRun/RunTillBatteryFull', 0, 0, 1] }, eventCallback=self._handle_changed_setting) # Whenever services come or go, we need to check if it was a service we use. Note that this # is a bit double: DbusMonitor does the same thing. But since we don't use DbusMonitor to # monitor for com.victronenergy.battery, .vebus, .charger or any other possible source of # battery data, it is necessary to monitor for changes in the available dbus services. self._bus.add_signal_receiver(self._dbus_name_owner_changed, signal_name='NameOwnerChanged') self._evaluate_if_we_are_needed() gobject.timeout_add(1000, self._handletimertick) self._update_relay() self._changed = True def _evaluate_if_we_are_needed(self): if self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Function') == 1: if self._dbusservice is None: logger.info('Action! Going on dbus and taking control of the relay.') relay_polarity_import = VeDbusItemImport( bus=self._bus, serviceName='com.victronenergy.settings', path='/Settings/Relay/Polarity', eventCallback=None, createsignal=True) # As is not possible to keep the relay state during the CCGX power cycles, # set the relay polarity to normally open. if relay_polarity_import.get_value() == 1: relay_polarity_import.set_value(0) logger.info('Setting relay polarity to normally open.') # put ourselves on the dbus self._dbusservice = VeDbusService('com.victronenergy.generator.startstop0') self._dbusservice.add_mandatory_paths( processname=__file__, processversion=softwareversion, connection='generator', deviceinstance=0, productid=None, productname=None, firmwareversion=None, hardwareversion=None, connected=1) # State: None = invalid, 0 = stopped, 1 = running self._dbusservice.add_path('/State', value=0) # Condition that made the generator start self._dbusservice.add_path('/RunningByCondition', value='') # Runtime self._dbusservice.add_path('/Runtime', value=0, gettextcallback=self._gettext) # Today runtime self._dbusservice.add_path('/TodayRuntime', value=0, gettextcallback=self._gettext) # Test run runtime self._dbusservice.add_path('/TestRunIntervalRuntime', value=self._interval_runtime(self._settings['testruninterval']), gettextcallback=self._gettext) # Next tes trun date, values is 0 for test run disabled self._dbusservice.add_path('/NextTestRun', value=None, gettextcallback=self._gettext) # Next tes trun is needed 1, not needed 0 self._dbusservice.add_path('/SkipTestRun', value=None) # Manual start self._dbusservice.add_path('/ManualStart', value=0, writeable=True) # Manual start timer self._dbusservice.add_path('/ManualStartTimer', value=0, writeable=True) # Silent mode active self._dbusservice.add_path('/QuietHours', value=0) self._determineservices() else: if self._dbusservice is not None: self._stop_generator() self._dbusservice.__del__() self._dbusservice = None # Reset conditions for condition in self._condition_stack: self._reset_condition(self._condition_stack[condition]) logger.info('Relay function is no longer set to generator start/stop: made sure generator is off ' + 'and now going off dbus') def _device_added(self, dbusservicename, instance): self._evaluate_if_we_are_needed() self._determineservices() def _device_removed(self, dbusservicename, instance): self._evaluate_if_we_are_needed() self._determineservices() def _dbus_value_changed(self, dbusServiceName, dbusPath, options, changes, deviceInstance): if dbusPath == '/AutoSelectedBatteryMeasurement' and self._settings['batterymeasurement'] == 'default': self._determineservices() if dbusPath == '/Settings/Relay/Function': self._evaluate_if_we_are_needed() self._changed = True # Update relay state when polarity is changed if dbusPath == '/Settings/Relay/Polarity': self._update_relay() def _handle_changed_setting(self, setting, oldvalue, newvalue): self._changed = True self._evaluate_if_we_are_needed() if setting == 'batterymeasurement': self._determineservices() # Reset retries and valid if service changes for condition in self._condition_stack: if self._condition_stack[condition]['monitoring'] == 'battery': self._condition_stack[condition]['valid'] = True self._condition_stack[condition]['retries'] = 0 if setting == 'autostart': logger.info('Autostart function %s.' % ('enabled' if newvalue == 1 else 'disabled')) if self._dbusservice is not None and setting == 'testruninterval': self._dbusservice['/TestRunIntervalRuntime'] = self._interval_runtime( self._settings['testruninterval']) def _dbus_name_owner_changed(self, name, oldowner, newowner): self._determineservices() def _gettext(self, path, value): if path == '/NextTestRun': # Locale format date d = datetime.datetime.fromtimestamp(value) return d.strftime('%c') elif path in ['/Runtime', '/TestRunIntervalRuntime', '/TodayRuntime']: m, s = divmod(value, 60) h, m = divmod(m, 60) return '%dh, %dm, %ds' % (h, m, s) else: return value def _handletimertick(self): # try catch, to make sure that we kill ourselves on an error. Without this try-catch, there would # be an error written to stdout, and then the timer would not be restarted, resulting in a dead- # lock waiting for manual intervention -> not good! try: if self._dbusservice is not None: self._evaluate_startstop_conditions() self._changed = False except: self._stop_generator() import traceback traceback.print_exc() sys.exit(1) return True def _evaluate_startstop_conditions(self): # Conditions will be evaluated in this order conditions = ['soc', 'acload', 'batterycurrent', 'batteryvoltage', 'inverterhightemp', 'inverteroverload'] start = False runningbycondition = None today = calendar.timegm(datetime.date.today().timetuple()) self._timer_runnning = False values = self._get_updated_values() connection_lost = False self._check_quiet_hours() # New day, register it if self._last_counters_check < today and self._dbusservice['/State'] == 0: self._last_counters_check = today self._update_accumulated_time() # Update current and accumulated runtime. if self._dbusservice['/State'] == 1: self._dbusservice['/Runtime'] = int(time.time() - self._starttime) # By performance reasons, accumulated runtime is only updated # once per 10s. When the generator stops is also updated. if self._dbusservice['/Runtime'] - self._last_runtime_update >= 10: self._update_accumulated_time() if self._evaluate_manual_start(): runningbycondition = 'manual' start = True # Autostart conditions will only be evaluated if the autostart functionality is enabled if self._settings['autostart'] == 1: if self._evaluate_testrun_condition(): runningbycondition = 'testrun' start = True # Evaluate value conditions for condition in conditions: start = self._evaluate_condition(self._condition_stack[condition], values[condition]) or start runningbycondition = condition if start and runningbycondition is None else runningbycondition # Connection lost is set to true if the numbear of retries of one or more enabled conditions # >= RETRIES_ON_ERROR if self._condition_stack[condition]['enabled']: connection_lost = self._condition_stack[condition]['retries'] >= self.RETRIES_ON_ERROR # If none condition is reached check if connection is lost and start/keep running the generator # depending on '/OnLossCommunication' setting if not start and connection_lost: # Start always if self._settings['onlosscommunication'] == 1: start = True runningbycondition = 'lossofcommunication' # Keep running if generator already started if self._dbusservice['/State'] == 1 and self._settings['onlosscommunication'] == 2: start = True runningbycondition = 'lossofcommunication' if start: self._start_generator(runningbycondition) elif (self._dbusservice['/Runtime'] >= self._settings['minimumruntime'] * 60 or self._dbusservice['/RunningByCondition'] == 'manual'): self._stop_generator() def _reset_condition(self, condition): condition['reached'] = False if condition['timed']: condition['start_timer'] = 0 condition['stop_timer'] = 0 def _check_condition(self, condition, value): name = condition['name'] if self._settings[name + 'enabled'] == 0: if condition['enabled']: condition['enabled'] = False logger.info('Disabling (%s) condition' % name) condition['retries'] = 0 condition['valid'] = True self._reset_condition(condition) return False elif not condition['enabled']: condition['enabled'] = True logger.info('Enabling (%s) condition' % name) if (condition['monitoring'] == 'battery') and (self._settings['batterymeasurement'] == 'nobattery'): return False if value is None and condition['valid']: if condition['retries'] >= self.RETRIES_ON_ERROR: logger.info('Error getting (%s) value, skipping evaluation till get a valid value' % name) self._reset_condition(condition) self._comunnication_lost = True condition['valid'] = False else: condition['retries'] += 1 if condition['retries'] == 1 or (condition['retries'] % 10) == 0: logger.info('Error getting (%s) value, retrying(#%i)' % (name, condition['retries'])) return False elif value is not None and not condition['valid']: logger.info('Success getting (%s) value, resuming evaluation' % name) condition['valid'] = True condition['retries'] = 0 # Reset retries if value is valid if value is not None: condition['retries'] = 0 return condition['valid'] def _evaluate_condition(self, condition, value): name = condition['name'] setting = ('qh_' if self._dbusservice['/QuietHours'] == 1 else '') + name startvalue = self._settings[setting + 'start'] if not condition['boolean'] else 1 stopvalue = self._settings[setting + 'stop'] if not condition['boolean'] else 0 # Check if the condition has to be evaluated if not self._check_condition(condition, value): # If generator is started by this condition and value is invalid # wait till RETRIES_ON_ERROR to skip the condition if condition['reached'] and condition['retries'] <= self.RETRIES_ON_ERROR: return True return False # As this is a generic evaluation method, we need to know how to compare the values # first check if start value should be greater than stop value and then compare start_is_greater = startvalue > stopvalue # When the condition is already reached only the stop value can set it to False start = condition['reached'] or (value >= startvalue if start_is_greater else value <= startvalue) stop = value <= stopvalue if start_is_greater else value >= stopvalue # Timed conditions must start/stop after the condition has been reached for a minimum # time. if condition['timed']: if not condition['reached'] and start: condition['start_timer'] += time.time() if condition['start_timer'] == 0 else 0 start = time.time() - condition['start_timer'] >= self._settings[name + 'starttimer'] condition['stop_timer'] *= int(not start) self._timer_runnning = True else: condition['start_timer'] = 0 if condition['reached'] and stop: condition['stop_timer'] += time.time() if condition['stop_timer'] == 0 else 0 stop = time.time() - condition['stop_timer'] >= self._settings[name + 'stoptimer'] condition['stop_timer'] *= int(not stop) self._timer_runnning = True else: condition['stop_timer'] = 0 condition['reached'] = start and not stop return condition['reached'] def _evaluate_manual_start(self): if self._dbusservice['/ManualStart'] == 0: if self._dbusservice['/RunningByCondition'] == 'manual': self._dbusservice['/ManualStartTimer'] = 0 return False start = True # If /ManualStartTimer has a value greater than zero will use it to set a stop timer. # If no timer is set, the generator will not stop until the user stops it manually. # Once started by manual start, each evaluation the timer is decreased if self._dbusservice['/ManualStartTimer'] != 0: self._manualstarttimer += time.time() if self._manualstarttimer == 0 else 0 self._dbusservice['/ManualStartTimer'] -= int(time.time()) - int(self._manualstarttimer) self._manualstarttimer = time.time() start = self._dbusservice['/ManualStartTimer'] > 0 self._dbusservice['/ManualStart'] = int(start) # Reset if timer is finished self._manualstarttimer *= int(start) self._dbusservice['/ManualStartTimer'] *= int(start) return start def _evaluate_testrun_condition(self): if self._settings['testrunenabled'] == 0: self._dbusservice['/SkipTestRun'] = None self._dbusservice['/NextTestRun'] = None return False today = datetime.date.today() runtillbatteryfull = self._settings['testruntillbatteryfull'] == 1 soc = self._get_updated_values()['soc'] batteryisfull = runtillbatteryfull and soc == 100 try: startdate = datetime.date.fromtimestamp(self._settings['testrunstartdate']) starttime = time.mktime(today.timetuple()) + self._settings['testrunstarttimer'] except ValueError: logger.debug('Invalid dates, skipping testrun') return False # If start date is in the future set as NextTestRun and stop evaluating if startdate > today: self._dbusservice['/NextTestRun'] = time.mktime(startdate.timetuple()) return False start = False # If the accumulated runtime during the tes trun interval is greater than '/TestRunIntervalRuntime' # the tes trun must be skipped needed = (self._settings['testrunskipruntime'] > self._dbusservice['/TestRunIntervalRuntime'] or self._settings['testrunskipruntime'] == 0) self._dbusservice['/SkipTestRun'] = int(not needed) interval = self._settings['testruninterval'] stoptime = (starttime + self._settings['testrunruntime']) if not runtillbatteryfull else (starttime + 60) elapseddays = (today - startdate).days mod = elapseddays % interval start = (not bool(mod) and (time.time() >= starttime) and (time.time() <= stoptime)) if runtillbatteryfull: if soc is not None: self._testrun_soc_retries = 0 start = (start or self._dbusservice['/RunningByCondition'] == 'testrun') and not batteryisfull elif self._dbusservice['/RunningByCondition'] == 'testrun': if self._testrun_soc_retries < self.RETRIES_ON_ERROR: self._testrun_soc_retries += 1 start = True if (self._testrun_soc_retries % 10) == 0: logger.info('Test run failed to get SOC value, retrying(#%i)' % self._testrun_soc_retries) else: logger.info('Failed to get SOC after %i retries, terminating test run condition' % self._testrun_soc_retries) start = False else: start = False if not bool(mod) and (time.time() <= stoptime): self._dbusservice['/NextTestRun'] = starttime else: self._dbusservice['/NextTestRun'] = (time.mktime((today + datetime.timedelta(days=interval - mod)).timetuple()) + self._settings['testrunstarttimer']) return start and needed def _check_quiet_hours(self): active = False if self._settings['quiethoursenabled'] == 1: # Seconds after today 00:00 timeinseconds = time.time() - time.mktime(datetime.date.today().timetuple()) quiethoursstart = self._settings['quiethoursstarttime'] quiethoursend = self._settings['quiethoursendtime'] # Check if the current time is between the start time and end time if quiethoursstart < quiethoursend: active = quiethoursstart <= timeinseconds and timeinseconds < quiethoursend else: # End time is lower than start time, example Start: 21:00, end: 08:00 active = not (quiethoursend < timeinseconds and timeinseconds < quiethoursstart) if self._dbusservice['/QuietHours'] == 0 and active: logger.info('Entering to quiet mode') elif self._dbusservice['/QuietHours'] == 1 and not active: logger.info('Leaving secondary quiet mode') self._dbusservice['/QuietHours'] = int(active) return active def _update_accumulated_time(self): seconds = self._dbusservice['/Runtime'] accumulated = seconds - self._last_runtime_update self._settings['accumulatedtotal'] = int(self._settings['accumulatedtotal']) + accumulated # Using calendar to get timestamp in UTC, not local time today_date = str(calendar.timegm(datetime.date.today().timetuple())) # If something goes wrong getting the json string create a new one try: accumulated_days = json.loads(self._settings['accumulateddaily']) except ValueError: accumulated_days = {today_date: 0} if (today_date in accumulated_days): accumulated_days[today_date] += accumulated else: accumulated_days[today_date] = accumulated self._last_runtime_update = seconds # Keep the historical with a maximum of HISTORY_DAYS while len(accumulated_days) > self.HISTORY_DAYS: accumulated_days.pop(min(accumulated_days.keys()), None) # Upadate settings self._settings['accumulateddaily'] = json.dumps(accumulated_days, sort_keys=True) self._dbusservice['/TodayRuntime'] = self._interval_runtime(0) self._dbusservice['/TestRunIntervalRuntime'] = self._interval_runtime(self._settings['testruninterval']) def _interval_runtime(self, days): summ = 0 try: daily_record = json.loads(self._settings['accumulateddaily']) except ValueError: return 0 for i in range(days + 1): previous_day = calendar.timegm((datetime.date.today() - datetime.timedelta(days=i)).timetuple()) if str(previous_day) in daily_record.keys(): summ += daily_record[str(previous_day)] if str(previous_day) in daily_record.keys() else 0 return summ def _get_updated_values(self): values = { 'batteryvoltage': (self._battery_measurement_voltage_import.get_value() if self._battery_measurement_voltage_import else None), 'batterycurrent': (self._battery_measurement_current_import.get_value() if self._battery_measurement_current_import else None), 'soc': self._battery_measurement_soc_import.get_value() if self._battery_measurement_soc_import else None, 'acload': self._dbusmonitor.get_value('com.victronenergy.system', '/Ac/Consumption/Total/Power'), 'inverterhightemp': (self._vebusservice_high_temperature_import.get_value() if self._vebusservice_high_temperature_import else None), 'inverteroverload': (self._vebusservice_overload_import.get_value() if self._vebusservice_overload_import else None) } if values['batterycurrent']: values['batterycurrent'] *= -1 return values def _determineservices(self): # batterymeasurement is either 'default' or 'com_victronenergy_battery_288/Dc/0'. # In case it is set to default, we use the AutoSelected battery measurement, given by # SystemCalc. batterymeasurement = None batteryservicename = None newbatteryservice = None batteryprefix = "" selectedbattery = self._settings['batterymeasurement'] vebusservice = None if selectedbattery == 'default': batterymeasurement = self._dbusmonitor.get_value('com.victronenergy.system', '/AutoSelectedBatteryMeasurement') elif len(selectedbattery.split("/", 1)) == 2: # Only very basic sanity checking.. batterymeasurement = self._settings['batterymeasurement'] elif selectedbattery == 'nobattery': batterymeasurement = None else: # Exception: unexpected value for batterymeasurement pass if batterymeasurement: batteryprefix = "/" + batterymeasurement.split("/", 1)[1] # Get the current battery servicename if self._battery_measurement_voltage_import: oldservice = (self._battery_measurement_voltage_import.serviceName + self._battery_measurement_voltage_import.path.replace("/Voltage", "")) else: oldservice = None if batterymeasurement: batteryservicename = VeDbusItemImport( bus=self._bus, serviceName="com.victronenergy.system", path='/ServiceMapping/' + batterymeasurement.split("/", 1)[0], eventCallback=None, createsignal=False) if batteryservicename.get_value(): newbatteryservice = batteryservicename.get_value() + batteryprefix else: newbatteryservice = None if batteryservicename and batteryservicename.get_value(): self._battery_measurement_available = True logger.info('Battery service we need (%s) found! Using it for generator start/stop' % batterymeasurement) try: self._battery_measurement_voltage_import = VeDbusItemImport( bus=self._bus, serviceName=batteryservicename.get_value(), path=batteryprefix + '/Voltage', eventCallback=None, createsignal=True) self._battery_measurement_current_import = VeDbusItemImport( bus=self._bus, serviceName=batteryservicename.get_value(), path=batteryprefix + '/Current', eventCallback=None, createsignal=True) # Exception caused by Matthijs :), we forgot to batteryprefix the Soc during the big path-change... self._battery_measurement_soc_import = VeDbusItemImport( bus=self._bus, serviceName=batteryservicename.get_value(), path='/Soc', eventCallback=None, createsignal=True) except Exception: logger.debug('Error getting battery service!') self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None elif selectedbattery == 'nobattery' and self._battery_measurement_available: logger.info('Battery monitoring disabled! Stop evaluating related conditions') self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None self._battery_measurement_available = False elif batteryservicename and batteryservicename.get_value() is None and self._battery_measurement_available: logger.info('Battery service we need (%s) is not available! Stop evaluating related conditions' % batterymeasurement) self._battery_measurement_voltage_import = None self._battery_measurement_current_import = None self._battery_measurement_soc_import = None self._battery_measurement_available = False # Get the default VE.Bus service and import high temperature and overload warnings vebusservice = VeDbusItemImport( bus=self._bus, serviceName="com.victronenergy.system", path='/VebusService', eventCallback=None, createsignal=False) if vebusservice.get_value() and (vebusservice.get_value() != self._vebusservice or not self._vebusservice_available): self._vebusservice = vebusservice.get_value() self._vebusservice_available = True logger.info('Vebus service (%s) found! Using it for generator start/stop' % vebusservice.get_value()) try: self._vebusservice_high_temperature_import = VeDbusItemImport( bus=self._bus, serviceName=vebusservice.get_value(), path='/Alarms/HighTemperature', eventCallback=None, createsignal=True) self._vebusservice_overload_import = VeDbusItemImport( bus=self._bus, serviceName=vebusservice.get_value(), path='/Alarms/Overload', eventCallback=None, createsignal=True) except Exception: logger.info('Error getting Vebus service!') self._vebusservice_available = False self._vebusservice_high_temperature_import = None self._vebusservice_overload_import = None elif not vebusservice.get_value() and self._vebusservice_available: logger.info('Vebus service (%s) dissapeared! Stop evaluating related conditions' % self._vebusservice) self._vebusservice_available = False self._vebusservice_high_temperature_import = None self._vebusservice_overload_import = None # Trigger an immediate check of system status self._changed = True def _start_generator(self, condition): # This function will start the generator in the case generator not # already running. When differs, the RunningByCondition is updated if self._dbusservice['/State'] == 0: self._dbusservice['/State'] = 1 self._update_relay() self._starttime = time.time() logger.info('Starting generator by %s condition' % condition) elif self._dbusservice['/RunningByCondition'] != condition: logger.info('Generator previously running by %s condition is now running by %s condition' % (self._dbusservice['/RunningByCondition'], condition)) self._dbusservice['/RunningByCondition'] = condition def _stop_generator(self): if self._dbusservice['/State'] == 1: self._dbusservice['/State'] = 0 self._update_relay() logger.info('Stopping generator that was running by %s condition' % str(self._dbusservice['/RunningByCondition'])) self._dbusservice['/RunningByCondition'] = '' self._update_accumulated_time() self._starttime = 0 self._dbusservice['/Runtime'] = 0 self._dbusservice['/ManualStartTimer'] = 0 self._manualstarttimer = 0 self._last_runtime_update = 0 def _update_relay(self): # Relay polarity 0 = NO, 1 = NC polarity = bool(self._dbusmonitor.get_value('com.victronenergy.settings', '/Settings/Relay/Polarity')) w = int(not polarity) if bool(self._dbusservice['/State']) else int(polarity) try: f = open(self.RELAY_GPIO_FILE, 'w') f.write(str(w)) f.close() except IOError: logger.info('Error writting to the relay GPIO file!: %s' % self.RELAY_GPIO_FILE)
class 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')
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 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 AcDevice(object): def __init__(self, position): # Dictionary containing the AC Sensors per phase. This is the source of the data self._acSensors = {'L1': [], 'L2': [], 'L3': []} # Type and position (numbering is equal to numbering in VE.Bus Assistant): self._names = {0: 'PV Inverter on input 1', 1: 'PV Inverter on output', 2: 'PV Inverter on input 2'} self._name = position self._dbusService = None def __str__(self): return self._names[self._name] + ' containing ' + \ str(len(self._acSensors['L1'])) + ' AC-sensors on L1, ' + \ str(len(self._acSensors['L2'])) + ' AC-sensors on L2, ' + \ str(len(self._acSensors['L3'])) + ' AC-sensors on L3' # add_ac_sensor function is called to add dbusitems that represent power for a certain phase def add_ac_sensor(self, acsensor, phase): acsensor.set_eventcallback(self.value_has_changed) self._acSensors[phase].append(acsensor) def value_has_changed(self, dbusName, dbusObjectPath, changes): # decouple, and process update in the mainloop idle_add(self.update_values) # iterates through all sensor dbusItems, and recalculates our values. Adds objects to exported # dbus values if necessary. def update_values(self): if not self._dbusService: return totals = {'I': 0, 'P': 0, 'E': 0} for phase in ['L1', 'L2', 'L3']: pre = '/Ac/' + phase if len(self._acSensors[phase]) == 0: if (pre + '/Power') in self._dbusService: self._dbusService[pre + '/Power'] = None self._dbusService[pre + '/Energy/Forward'] = None self._dbusService[pre + '/Voltage'] = None self._dbusService[pre + '/Current'] = None else: phaseTotals = {'I': 0, 'P': 0, 'E': 0} for o in self._acSensors[phase]: phaseTotals['I'] += float(o['current'].get_value() or 0) phaseTotals['P'] += float(o['power'].get_value() or 0) phaseTotals['E'] += float(o['energycounter'].get_value() or 0) voltage = float(o['voltage'].get_value() or 0) # just take the last voltage if (pre + '/Power') not in self._dbusService: # This phase hasn't been added yet, adding it now self._dbusService.add_path(pre + '/Voltage', voltage, gettextcallback=self.gettextforV) self._dbusService.add_path(pre + '/Current', phaseTotals['I'], gettextcallback=self.gettextforA) self._dbusService.add_path(pre + '/Power', phaseTotals['P'], gettextcallback=self.gettextforW) self._dbusService.add_path(pre + '/Energy/Forward', phaseTotals['E'], gettextcallback=self.gettextforkWh) else: self._dbusService[pre + '/Voltage'] = voltage self._dbusService[pre + '/Current'] = phaseTotals['I'] self._dbusService[pre + '/Power'] = phaseTotals['P'] self._dbusService[pre + '/Energy/Forward'] = phaseTotals['E'] totals['I'] += phaseTotals['I'] totals['P'] += phaseTotals['P'] totals['E'] += phaseTotals['E'] #logging.debug( # self._names[self._name] + '. Phase ' + phase + ' recalculated: %0.2fV, %0.2fA, %0.4fW and %0.4f kWh' % # (voltage, phaseTotals['I'], phaseTotals['P'], phaseTotals['E'])) # TODO, why doesn't the application crash on an exception? I want it to crash, also on exceptions # in threads. #raise Exception ("exit Exception!") if '/Ac/Current' not in self._dbusService: self._dbusService.add_path('/Ac/Current', totals['I'], gettextcallback=self.gettextforA) self._dbusService.add_path('/Ac/Power', totals['P'], gettextcallback=self.gettextforW) self._dbusService.add_path('/Ac/Energy/Forward', totals['E'], gettextcallback=self.gettextforkWh) else: self._dbusService['/Ac/Current'] = totals['I'] self._dbusService['/Ac/Power'] = totals['P'] self._dbusService['/Ac/Energy/Forward'] = totals['E'] # Call this function after you have added AC sensors to this class. Code will check if we have any, # and if yes, add ourselves to the dbus. def update_dbus_service(self): if (len(self._acSensors['L1']) > 0 or len(self._acSensors['L2']) > 0 or len(self._acSensors['L3']) > 0): if self._dbusService is None: pf = {0: 'input1', 1: 'output', 2: 'input2'} self._dbusService = VeDbusService('com.victronenergy.pvinverter.vebusacsensor_' + pf[self._name]) #, self._dbusConn) self._dbusService.add_path('/Position', self._name, description=None, gettextcallback=self.gettextforposition) # Create the mandatory objects, as per victron dbus api document self._dbusService.add_path('/Mgmt/ProcessName', __file__) self._dbusService.add_path('/Mgmt/ProcessVersion', softwareVersion) self._dbusService.add_path('/Mgmt/Connection', 'AC Sensor on VE.Bus device') self._dbusService.add_path('/DeviceInstance', int(self._name) + 10) self._dbusService.add_path('/ProductId', 0xA141) self._dbusService.add_path('/ProductName', self._names[self._name]) self._dbusService.add_path('/Connected', 1) logging.info('Added to D-Bus: ' + self.__str__()) self.update_values() # Apparantly some service from which we imported AC Sensors has gone offline. Remove those sensors # from our repo. def remove_ac_sensors_imported_from(self, serviceBeingRemoved): logging.debug( '%s: Checking if we have sensors from %s, and removing them' % (self._names[self._name], serviceBeingRemoved)) for phase in ['L1', 'L2', 'L3']: self._acSensors[phase][:] = [x for x in self._acSensors[phase] if not x['power'].serviceName == serviceBeingRemoved] if self._dbusService is None: return if (not self._acSensors['L1'] and not self._acSensors['L2'] and not self._acSensors['L3']): # No sensors left for us, clean up self._dbusService.__del__() # explicitly call __del__(), instead of waiting for gc self._dbusService = None logging.info("Removed from D-Bus: %s" % self.__str__()) else: # Still some sensors left for us, update values self.update_values() def gettextforkWh(self, path, value): return ("%.3FkWh" % (float(value) / 1000.0)) def gettextforW(self, path, value): return ("%.0FW" % (float(value))) def gettextforV(self, path, value): return ("%.0FV" % (float(value))) def gettextforA(self, path, value): return ("%.0FA" % (float(value))) def gettextforposition(self, path, value): return self._names[value]