Esempio n. 1
0
 def _create_dbus_service(self):
     dbusservice = VeDbusService("com.victronenergy.generator.startstop0")
     dbusservice.add_mandatory_paths(processname=__file__,
                                     processversion=softwareversion,
                                     connection='generator',
                                     deviceinstance=0,
                                     productid=None,
                                     productname=None,
                                     firmwareversion=None,
                                     hardwareversion=None,
                                     connected=1)
     return dbusservice
Esempio n. 2
0
 def _create_dbus_service(self):
     dbusservice = VeDbusService(driver['connection'])
     dbusservice.add_mandatory_paths(processname=__file__,
                                     processversion=softwareVersion,
                                     connection=driver['connection'],
                                     deviceinstance=driver['instance'],
                                     productid=driver['id'],
                                     productname=driver['name'],
                                     firmwareversion=driver['version'],
                                     hardwareversion=driver['version'],
                                     connected=1)
     return dbusservice
	def _create_dbus_service(self):
		dbusservice = VeDbusService('com.victronenergy.system')
		dbusservice.add_mandatory_paths(
			processname=__file__,
			processversion=softwareVersion,
			connection='data from other dbus processes',
			deviceinstance=0,
			productid=None,
			productname=None,
			firmwareversion=None,
			hardwareversion=None,
			connected=1)
		return dbusservice
 def _create_dbus_service(self):
     dbusservice = VeDbusService('com.victronenergy.system')
     dbusservice.add_mandatory_paths(
         processname=__file__,
         processversion=softwareVersion,
         connection='data from other dbus processes',
         deviceinstance=0,
         productid=None,
         productname=None,
         firmwareversion=None,
         hardwareversion=None,
         connected=1)
     return dbusservice
Esempio n. 5
0
def create_dbus_service():
    dbusservice = VeDbusService('com.victronenergy.pvinverter.envoy')
    dbusservice.add_mandatory_paths(
        processname=__file__,
        processversion=0.1,
        connection='com.victronenergy.pvinverter.envoy',
        deviceinstance=driver['instance'],
        productid=driver['id'],
        productname=driver['name'],
        firmwareversion=driver['version'],
        hardwareversion=driver['version'],
        connected=1)

    return dbusservice
Esempio n. 6
0
def create_dbus_service(instance):
	# Use a private bus, so we can have multiple services
	bus = dbus.Bus.get_session(private=True) if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.Bus.get_system(private=True)

	dbusservice = VeDbusService("com.victronenergy.generator.startstop{}".format(instance), bus=bus)
	dbusservice.add_mandatory_paths(
		processname=sys.argv[0],
		processversion=softwareversion,
		connection='generator',
		deviceinstance=instance,
		productid=None,
		productname=None,
		firmwareversion=None,
		hardwareversion=None,
		connected=1)
	return dbusservice
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 SystemCalc:
	def __init__(self, dbusmonitor_gen=None, dbusservice_gen=None, settings_device_gen=None):
		self.STATE_IDLE = 0
		self.STATE_CHARGING = 1
		self.STATE_DISCHARGING = 2

		self.BATSERVICE_DEFAULT = 'default'
		self.BATSERVICE_NOBATTERY = 'nobattery'

		# Why this dummy? Because DbusMonitor expects these values to be there, even though we don't
		# need them. So just add some dummy data. This can go away when DbusMonitor is more generic.
		dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None}
		dbus_tree = {
			'com.victronenergy.solarcharger': {
				'/Connected': dummy,
				'/ProductName': dummy,
				'/Mgmt/Connection': dummy,
				'/Dc/0/Voltage': dummy,
				'/Dc/0/Current': dummy},
			'com.victronenergy.pvinverter': {
				'/Connected': dummy,
				'/ProductName': dummy,
				'/Mgmt/Connection': dummy,
				'/Ac/L1/Power': dummy,
				'/Ac/L2/Power': dummy,
				'/Ac/L3/Power': dummy,
				'/Position': dummy,
				'/ProductId': dummy},
			'com.victronenergy.battery': {
				'/Connected': dummy,
				'/ProductName': dummy,
				'/Mgmt/Connection': dummy,
				'/Dc/0/Voltage': dummy,
				'/Dc/0/Current': dummy,
				'/Dc/0/Power': dummy,
				'/Soc': dummy,
				'/TimeToGo': dummy,
				'/ConsumedAmphours': dummy},
			'com.victronenergy.vebus' : {
				'/Ac/ActiveIn/ActiveInput': dummy,
				'/Ac/ActiveIn/L1/P': dummy,
				'/Ac/ActiveIn/L2/P': dummy,
				'/Ac/ActiveIn/L3/P': dummy,
				'/Ac/Out/L1/P': dummy,
				'/Ac/Out/L2/P': dummy,
				'/Ac/Out/L3/P': dummy,
				'/Hub4/AcPowerSetpoint': dummy,
				'/ProductName': dummy,
				'/Mgmt/Connection': dummy,
				'/Dc/0/Voltage': dummy,
				'/Dc/0/Current': dummy,
				'/Dc/0/Power': dummy,
				'/Soc': dummy},
			'com.victronenergy.charger': {
				'/ProductName': dummy,
				'/Mgmt/Connection': dummy,
				'/Dc/0/Voltage': dummy,
				'/Dc/0/Current': dummy},
			'com.victronenergy.grid' : {
				'/ProductName': dummy,
				'/Mgmt/Connection': dummy,
				'/ProductId' : dummy,
				'/DeviceType' : dummy,
				'/Ac/L1/Power': dummy,
				'/Ac/L2/Power': dummy,
				'/Ac/L3/Power': dummy},
			'com.victronenergy.genset' : {
				'/ProductName': dummy,
				'/Mgmt/Connection': dummy,
				'/ProductId' : dummy,
				'/DeviceType' : dummy,
				'/Ac/L1/Power': dummy,
				'/Ac/L2/Power': dummy,
				'/Ac/L3/Power': dummy},
			'com.victronenergy.settings' : {
				'/Settings/SystemSetup/AcInput1' : dummy,
				'/Settings/SystemSetup/AcInput2' : dummy}
		}

		if dbusmonitor_gen is None:
			self._dbusmonitor = DbusMonitor(dbus_tree, self._dbus_value_changed, self._device_added, self._device_removed)
		else:
			self._dbusmonitor = dbusmonitor_gen(dbus_tree)

		# Connect to localsettings
		supported_settings = {
			'batteryservice': ['/Settings/SystemSetup/BatteryService', self.BATSERVICE_DEFAULT, 0, 0],
			'hasdcsystem': ['/Settings/SystemSetup/HasDcSystem', 0, 0, 1],
			'writevebussoc': ['/Settings/SystemSetup/WriteVebusSoc', 0, 0, 1]}

		if settings_device_gen is None:
			self._settings = SettingsDevice(
				bus=dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus(),
				supportedSettings=supported_settings,
				eventCallback=self._handlechangedsetting)
		else:
			self._settings = settings_device_gen(supported_settings, self._handlechangedsetting)

		# put ourselves on the dbus
		if dbusservice_gen is None:
			self._dbusservice = VeDbusService('com.victronenergy.system')
		else:
			self._dbusservice = dbusservice_gen('com.victronenergy.system')

		self._dbusservice.add_mandatory_paths(
			processname=__file__,
			processversion=softwareVersion,
			connection='data from other dbus processes',
			deviceinstance=0,
			productid=None,
			productname=None,
			firmwareversion=None,
			hardwareversion=None,
			connected=1)

		# At this moment, VRM portal ID is the MAC address of the CCGX. Anyhow, it should be string uniquely
		# identifying the CCGX.
		self._dbusservice.add_path('/Serial', value=get_vrm_portal_id())

		self._dbusservice.add_path(
			'/AvailableBatteryServices', value=None, gettextcallback=self._gettext)
		self._dbusservice.add_path(
			'/AvailableBatteryMeasurements', value=None, gettextcallback=self._gettext)
		self._dbusservice.add_path(
			'/AutoSelectedBatteryService', value=None, gettextcallback=self._gettext)
		self._dbusservice.add_path(
			'/AutoSelectedBatteryMeasurement', value=None, gettextcallback=self._gettext)
		self._dbusservice.add_path(
			'/ActiveBatteryService', value=None, gettextcallback=self._gettext)
		self._dbusservice.add_path(
			'/PvInvertersProductIds', value=None)
		self._summeditems = {
			'/Ac/Grid/L1/Power': {'gettext': '%.0F W'},
			'/Ac/Grid/L2/Power': {'gettext': '%.0F W'},
			'/Ac/Grid/L3/Power': {'gettext': '%.0F W'},
			'/Ac/Grid/Total/Power': {'gettext': '%.0F W'},
			'/Ac/Grid/NumberOfPhases': {'gettext': '%.0F W'},
			'/Ac/Grid/ProductId': {'gettext': '%s'},
			'/Ac/Grid/DeviceType': {'gettext': '%s'},
			'/Ac/Genset/L1/Power': {'gettext': '%.0F W'},
			'/Ac/Genset/L2/Power': {'gettext': '%.0F W'},
			'/Ac/Genset/L3/Power': {'gettext': '%.0F W'},
			'/Ac/Genset/Total/Power': {'gettext': '%.0F W'},
			'/Ac/Genset/NumberOfPhases': {'gettext': '%.0F W'},
			'/Ac/Genset/ProductId': {'gettext': '%s'},
			'/Ac/Genset/DeviceType': {'gettext': '%s'},
			'/Ac/Consumption/L1/Power': {'gettext': '%.0F W'},
			'/Ac/Consumption/L2/Power': {'gettext': '%.0F W'},
			'/Ac/Consumption/L3/Power': {'gettext': '%.0F W'},
			'/Ac/Consumption/Total/Power': {'gettext': '%.0F W'},
			'/Ac/Consumption/NumberOfPhases': {'gettext': '%.0F W'},
			'/Ac/PvOnOutput/L1/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnOutput/L2/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnOutput/L3/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnOutput/Total/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnOutput/NumberOfPhases': {'gettext': '%.0F W'},
			'/Ac/PvOnGrid/L1/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnGrid/L2/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnGrid/L3/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnGrid/Total/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnGrid/NumberOfPhases': {'gettext': '%.0F W'},
			'/Ac/PvOnGenset/L1/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnGenset/L2/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnGenset/L3/Power': {'gettext': '%.0F W'},
			'/Ac/PvOnGenset/NumberOfPhases': {'gettext': '%d'},
			'/Ac/PvOnGenset/Total/Power': {'gettext': '%.0F W'},
			'/Dc/Pv/Power': {'gettext': '%.0F W'},
			'/Dc/Pv/Current': {'gettext': '%.1F A'},
			'/Dc/Battery/Voltage': {'gettext': '%.2F V'},
			'/Dc/Battery/Current': {'gettext': '%.1F A'},
			'/Dc/Battery/Power': {'gettext': '%.0F W'},
			'/Dc/Battery/Soc': {'gettext': '%.0F %%'},
			'/Dc/Battery/State': {'gettext': '%s'},
			'/Dc/Battery/TimeToGo': {'gettext': '%.0F s'},
			'/Dc/Battery/ConsumedAmphours': {'gettext': '%.1F Ah'},
			'/Dc/Charger/Power': {'gettext': '%.0F %%'},
			'/Dc/Vebus/Current': {'gettext': '%.1F A'},
			'/Dc/Vebus/Power': {'gettext': '%.0F W'},
			'/Dc/System/Power': {'gettext': '%.0F W'},
			'/Hub': {'gettext': '%s'},
			'/Ac/ActiveIn/Source': {'gettext': '%s'},
			'/VebusService': {'gettext': '%s'}
		}

		for path in self._summeditems.keys():
			self._dbusservice.add_path(path, value=None, gettextcallback=self._gettext)

		self._batteryservice = None
		self._determinebatteryservice()

		if self._batteryservice is None:
			logger.info("Battery service initialized to None (setting == %s)" %
				self._settings['batteryservice'])

		self._changed = True
		for service, instance in self._dbusmonitor.get_service_list().items():
			self._device_added(service, instance, do_service_change=False)

		self._handleservicechange()
		self._updatevalues()

		self._writeVebusSocCounter = 9
		gobject.timeout_add(1000, exit_on_error, self._handletimertick)

	def _handlechangedsetting(self, setting, oldvalue, newvalue):
		self._determinebatteryservice()
		self._changed = True

	def _determinebatteryservice(self):
		auto_battery_service = self._autoselect_battery_service()
		auto_battery_measurement = None
		if auto_battery_service is not None:
			services = self._dbusmonitor.get_service_list()
			if auto_battery_service in services:
				auto_battery_measurement = \
					self._get_instance_service_name(auto_battery_service, services[auto_battery_service])
				auto_battery_measurement = auto_battery_measurement.replace('.', '_').replace('/', '_') + '/Dc/0'
		self._dbusservice['/AutoSelectedBatteryMeasurement'] = auto_battery_measurement

		if self._settings['batteryservice'] == self.BATSERVICE_DEFAULT:
			newbatteryservice = auto_battery_service
			self._dbusservice['/AutoSelectedBatteryService'] = (
				'No battery monitor found' if newbatteryservice is None else
				self._get_readable_service_name(newbatteryservice))

		elif self._settings['batteryservice'] == self.BATSERVICE_NOBATTERY:
			self._dbusservice['/AutoSelectedBatteryService'] = None
			newbatteryservice = None

		else:
			self._dbusservice['/AutoSelectedBatteryService'] = None

			s = self._settings['batteryservice'].split('/')
			if len(s) != 2:
				logger.error("The battery setting (%s) is invalid!" % self._settings['batteryservice'])
			serviceclass = s[0]
			instance = int(s[1]) if len(s) == 2 else None
			services = self._dbusmonitor.get_service_list(classfilter=serviceclass)
			if instance not in services.values():
				# Once chosen battery monitor does not exist. Don't auto change the setting (it might come
				# back). And also don't autoselect another.
				newbatteryservice = None
			else:
				# According to https://www.python.org/dev/peps/pep-3106/, dict.keys() and dict.values()
				# always have the same order.
				newbatteryservice = services.keys()[services.values().index(instance)]

		if newbatteryservice != self._batteryservice:
			services = self._dbusmonitor.get_service_list()
			instance = services.get(newbatteryservice, None)
			if instance is None:
				battery_service = None
			else:
				battery_service = self._get_instance_service_name(newbatteryservice, instance)
			self._dbusservice['/ActiveBatteryService'] = battery_service
			logger.info("Battery service, setting == %s, changed from %s to %s (%s)" %
				(self._settings['batteryservice'], self._batteryservice, newbatteryservice, instance))
			self._batteryservice = newbatteryservice

	def _autoselect_battery_service(self):
		# Default setting business logic:
		# first try to use a battery service (BMV or Lynx Shunt VE.Can). If there is more than one battery
		# service, use the first one (sort alphabetical). If no battery service is available, check if there
		# are not Solar chargers and no normal chargers. If they are not there, assume this is a hub-2,
		# hub-3 or hub-4 system and use VE.Bus SOC.
		battery_service = self._get_first_service('com.victronenergy.battery')
		if battery_service is not None:
			return battery_service

		if len(self._dbusmonitor.get_service_list('com.victronenergy.solarcharger')) > 0:
			return None

		if len(self._dbusmonitor.get_service_list('com.victronenergy.charger')) > 0:
			return None

		vebus_service = self._get_first_service('com.victronenergy.vebus')

		return vebus_service  # will be None when no vebus service found

	# Called on a one second timer
	def _handletimertick(self):
		if self._changed:
			self._updatevalues()
		self._changed = False

		self._writeVebusSocCounter += 1
		if self._writeVebusSocCounter >= 10:
			self._writeVebusSoc()
			self._writeVebusSocCounter = 0

		return True  # keep timer running

	def _writeVebusSoc(self):
		# ==== COPY BATTERY SOC TO VEBUS ====
		if self._settings['writevebussoc'] and self._dbusservice['/VebusService'] and self._dbusservice['/Dc/Battery/Soc'] and \
			self._batteryservice.split('.')[2] != 'vebus':

			logger.debug("writing this soc to vebus: %d", self._dbusservice['/Dc/Battery/Soc'])
			self._dbusmonitor.get_item(self._dbusservice['/VebusService'], '/Soc').set_value(self._dbusservice['/Dc/Battery/Soc'])

	def _updatepvinverterspidlist(self):
		# Create list of connected pv inverters id's
		pvinverters = self._dbusmonitor.get_service_list('com.victronenergy.pvinverter')
		productids = []

		for pvinverter in pvinverters:
			pid = self._dbusmonitor.get_value(pvinverter, '/ProductId')
			if pid not in productids:
				productids.append(pid)
		self._dbusservice['/PvInvertersProductIds'] = productids if productids else None

	def _updatevalues(self):
		# ==== PREPARATIONS ====
		# Determine values used in logic below
		vebusses = self._dbusmonitor.get_service_list('com.victronenergy.vebus')
		vebuspower = 0
		for vebus in vebusses:
			v = self._dbusmonitor.get_value(vebus, '/Dc/0/Voltage')
			i = self._dbusmonitor.get_value(vebus, '/Dc/0/Current')
			if v is not None and i is not None:
				vebuspower += v * i

		# ==== PVINVERTERS ====
		pvinverters = self._dbusmonitor.get_service_list('com.victronenergy.pvinverter')
		newvalues = {}
		pos = {0: '/Ac/PvOnGrid', 1: '/Ac/PvOnOutput', 2: '/Ac/PvOnGenset'}
		total = {0: None, 1: None, 2: None}
		for pvinverter in pvinverters:
			# Position will be None if PV inverter service has just been removed (after retrieving the
			# service list).
			position = pos.get(self._dbusmonitor.get_value(pvinverter, '/Position'))
			if position is not None:
				for phase in range(1, 4):
					power = self._dbusmonitor.get_value(pvinverter, '/Ac/L%s/Power' % phase)
					if power is not None:
						path = '%s/L%s/Power' % (position, phase)
						newvalues[path] = _safeadd(newvalues.get(path), power)

		for path in pos.values():
			self._compute_phase_totals(path, newvalues)

		# ==== SOLARCHARGERS ====
		solarchargers = self._dbusmonitor.get_service_list('com.victronenergy.solarcharger')
		solarcharger_batteryvoltage = None
		for solarcharger in solarchargers:
			v = self._dbusmonitor.get_value(solarcharger, '/Dc/0/Voltage')
			if v is None:
				continue
			i = self._dbusmonitor.get_value(solarcharger, '/Dc/0/Current')
			if i is None:
				continue

			if '/Dc/Pv/Power' not in newvalues:
				newvalues['/Dc/Pv/Power'] = v * i
				newvalues['/Dc/Pv/Current'] = i
				solarcharger_batteryvoltage = v
			else:
				newvalues['/Dc/Pv/Power'] += v * i
				newvalues['/Dc/Pv/Current'] += i

		# ==== CHARGERS ====
		chargers = self._dbusmonitor.get_service_list('com.victronenergy.charger')
		charger_batteryvoltage = None
		for charger in chargers:
			# Assume the battery connected to output 0 is the main battery
			v = self._dbusmonitor.get_value(charger, '/Dc/0/Voltage')
			if v is None:
				continue

			charger_batteryvoltage = v

			i = self._dbusmonitor.get_value(charger, '/Dc/0/Current')
			if i is None:
				continue

			if '/Dc/Charger/Power' not in newvalues:
				newvalues['/Dc/Charger/Power'] = v * i
			else:
				newvalues['/Dc/Charger/Power'] += v * i

		# ==== BATTERY ====
		if self._batteryservice is not None:
			batteryservicetype = self._batteryservice.split('.')[2]  # either 'battery' or 'vebus'
			newvalues['/Dc/Battery/Soc'] = self._dbusmonitor.get_value(self._batteryservice,'/Soc')
			newvalues['/Dc/Battery/TimeToGo'] = self._dbusmonitor.get_value(self._batteryservice,'/TimeToGo')
			newvalues['/Dc/Battery/ConsumedAmphours'] = self._dbusmonitor.get_value(self._batteryservice,'/ConsumedAmphours')

			if batteryservicetype == 'battery':
				newvalues['/Dc/Battery/Voltage'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Voltage')
				newvalues['/Dc/Battery/Current'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Current')
				newvalues['/Dc/Battery/Power'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Power')

			elif batteryservicetype == 'vebus':
				newvalues['/Dc/Battery/Voltage'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Voltage')
				newvalues['/Dc/Battery/Current'] = self._dbusmonitor.get_value(self._batteryservice, '/Dc/0/Current')
				if newvalues['/Dc/Battery/Voltage'] is not None and newvalues['/Dc/Battery/Current'] is not None:
					newvalues['/Dc/Battery/Power'] = (
						newvalues['/Dc/Battery/Voltage'] * newvalues['/Dc/Battery/Current'])

			p = newvalues.get('/Dc/Battery/Power', None)
			if p is not None:
				if p > 30:
					newvalues['/Dc/Battery/State'] = self.STATE_CHARGING
				elif p < -30:
					newvalues['/Dc/Battery/State'] = self.STATE_DISCHARGING
				else:
					newvalues['/Dc/Battery/State'] = self.STATE_IDLE

		else:
			batteryservicetype = None
			if solarcharger_batteryvoltage is not None:
				newvalues['/Dc/Battery/Voltage'] = solarcharger_batteryvoltage
			elif charger_batteryvoltage is not None:
				newvalues['/Dc/Battery/Voltage'] = charger_batteryvoltage
			else:
				# CCGX-connected system consists of only a Multi, but it is not user-selected, nor
				# auto-selected as the battery-monitor, probably because there are other loads or chargers.
				# In that case, at least use its reported battery voltage.
				vebusses = self._dbusmonitor.get_service_list('com.victronenergy.vebus')
				for vebus in vebusses:
					v = self._dbusmonitor.get_value(vebus, '/Dc/0/Voltage')
					if v is not None:
						newvalues['/Dc/Battery/Voltage'] = v

			if self._settings['hasdcsystem'] == 0 and '/Dc/Battery/Voltage' in newvalues:
				# No unmonitored DC loads or chargers, and also no battery monitor: derive battery watts
				# and amps from vebus, solarchargers and chargers.
				assert '/Dc/Battery/Power' not in newvalues
				assert '/Dc/Battery/Current' not in newvalues
				p = newvalues.get('/Dc/Pv/Power', 0) + newvalues.get('/Dc/Charger/Power', 0) + vebuspower
				voltage = newvalues['/Dc/Battery/Voltage']
				newvalues['/Dc/Battery/Current'] = p / voltage if voltage > 0 else None
				newvalues['/Dc/Battery/Power'] = p

		# ==== SYSTEM ====
		if self._settings['hasdcsystem'] == 1 and batteryservicetype == 'battery':
			# Calculate power being generated/consumed by not measured devices in the network.
			# /Dc/System: positive: consuming power
			# VE.Bus: Positive: current flowing from the Multi to the dc system or battery
			# Solarcharger & other chargers: positive: charging
			# battery: Positive: charging battery.
			# battery = solarcharger + charger + ve.bus - system

			battery_power = newvalues.get('/Dc/Battery/Power')
			if battery_power is not None:
				dc_pv_power = newvalues.get('/Dc/Pv/Power', 0)
				charger_power = newvalues.get('/Dc/Charger/Power', 0)
				newvalues['/Dc/System/Power'] = dc_pv_power + charger_power + vebuspower - battery_power

		# ==== Vebus ====
		# Assume there's only 1 multi service present on the D-Bus
		multi_path = self._get_first_service('com.victronenergy.vebus')
		if multi_path is not None:
			dc_current = self._dbusmonitor.get_value(multi_path, '/Dc/0/Current')
			newvalues['/Dc/Vebus/Current'] = dc_current
			dc_power = self._dbusmonitor.get_value(multi_path, '/Dc/0/Power')
			# Just in case /Dc/0/Power is not available
			if dc_power == None and dc_current is not None:
				dc_voltage = self._dbusmonitor.get_value(multi_path, '/Dc/0/Voltage')
				if dc_voltage is not None:
					dc_power = dc_voltage * dc_current
			# Note that there is also vebuspower, which is the total DC power summed over all multis.
			# However, this value cannot be combined with /Dc/Multi/Current, because it does not make sense
			# to add the Dc currents of all multis if they do not share the same DC voltage.
			newvalues['/Dc/Vebus/Power'] = dc_power

		newvalues['/VebusService'] = multi_path

		# ===== AC IN SOURCE =====
		ac_in_source = None
		active_input = self._dbusmonitor.get_value(multi_path, '/Ac/ActiveIn/ActiveInput')
		if active_input is not None:
			settings_path = '/Settings/SystemSetup/AcInput%s' % (active_input + 1)
			ac_in_source = self._dbusmonitor.get_value('com.victronenergy.settings', settings_path)
		newvalues['/Ac/ActiveIn/Source'] = ac_in_source

		# ===== HUB MODE =====
		# The code below should be executed after PV inverter data has been updated, because we need the
		# PV inverter total power to update the consumption.
		hub = None
		if self._dbusmonitor.get_value(multi_path, '/Hub4/AcPowerSetpoint') is not None:
			hub = 4
		elif newvalues.get('/Dc/Pv/Power', None) is not None:
			hub = 1
		elif newvalues.get('/Ac/PvOnOutput/Total/Power', None) is not None:
			hub = 2
		elif newvalues.get('/Ac/PvOnGrid/Total/Power', None) is not None or \
			newvalues.get('/Ac/PvOnGenset/Total/Power', None) is not None:
			hub = 3
		newvalues['/Hub'] = hub

		# ===== GRID METERS & CONSUMPTION ====
		consumption = { "L1" : None, "L2" : None, "L3" : None }
		for device_type in ['Grid', 'Genset']:
			servicename = 'com.victronenergy.%s' % device_type.lower()
			em_service = self._get_first_service(servicename)
			uses_active_input = False
			if multi_path is not None:
				# If a grid meter is present we use values from it. If not, we look at the multi. If it has
				# AcIn1 or AcIn2 connected to the grid, we use those values.
				# com.victronenergy.grid.??? indicates presence of an energy meter used as grid meter.
				# com.victronenergy.vebus.???/Ac/ActiveIn/ActiveInput: decides which whether we look at AcIn1
				# or AcIn2 as possible grid connection.
				if ac_in_source is not None:
					uses_active_input = ac_in_source > 0 and (ac_in_source == 2) == (device_type == 'Genset')
			for phase in consumption:
				p = None
				pvpower = newvalues.get('/Ac/PvOn%s/%s/Power' % (device_type, phase))
				if em_service is not None:
					p = self._dbusmonitor.get_value(em_service, '/Ac/%s/Power' % phase)
					# Compute consumption between energy meter and multi (meter power - multi AC in) and
					# add an optional PV inverter on input to the mix.
					c = consumption[phase]
					if uses_active_input:
						ac_in = self._dbusmonitor.get_value(multi_path, '/Ac/ActiveIn/%s/P' % phase)
						if ac_in is not None:
							c = _safeadd(c, -ac_in)
					# If there's any power coming from a PV inverter in the inactive AC in (which is unlikely),
					# it will still be used, because there may also be a load in the same ACIn consuming
					# power, or the power could be fed back to the net.
					c = _safeadd(c, p, pvpower)
					consumption[phase] = None if c is None else max(0, c)
				else:
					if uses_active_input:
						p = self._dbusmonitor.get_value(multi_path, '/Ac/ActiveIn/%s/P' % phase)
					# No relevant energy meter present. Assume there is no load between the grid and the multi.
					# There may be a PV inverter present though (Hub-3 setup).
					if pvpower != None:
						p = _safeadd(p, -pvpower)
				newvalues['/Ac/%s/%s/Power' % (device_type, phase)] = p
			self._compute_phase_totals('/Ac/%s' % device_type, newvalues)
			if em_service is not None:
				newvalues['/Ac/%s/ProductId' % device_type] = self._dbusmonitor.get_value(em_service, '/ProductId')
				newvalues['/Ac/%s/DeviceType' % device_type] = self._dbusmonitor.get_value(em_service, '/DeviceType')
		for phase in consumption:
			c = consumption[phase]
			pvpower = newvalues.get('/Ac/PvOnOutput/%s/Power' % phase)
			c = _safeadd(c, pvpower)
			if multi_path is not None:
				ac_out = self._dbusmonitor.get_value(multi_path, '/Ac/Out/%s/P' % phase)
				c = _safeadd(c, ac_out)
			newvalues['/Ac/Consumption/%s/Power' % phase] = None if c is None else max(0, c)
		self._compute_phase_totals('/Ac/Consumption', newvalues)
		# TODO EV Add Multi DeviceType & ProductID. Unfortunately, the com.victronenergy.vebus.??? tree does
		# not contain a /ProductId entry.

		# ==== UPDATE DBUS ITEMS ====
		for path in self._summeditems.keys():
			# Why the None? Because we want to invalidate things we don't have anymore.
			self._dbusservice[path] = newvalues.get(path, None)

	def _handleservicechange(self):
		# Update the available battery monitor services, used to populate the dropdown in the settings.
		# Below code makes a dictionary. The key is [dbuserviceclass]/[deviceinstance]. For example
		# "battery/245". The value is the name to show to the user in the dropdown. The full dbus-
		# servicename, ie 'com.victronenergy.vebus.ttyO1' is not used, since the last part of that is not
		# fixed. dbus-serviceclass name and the device instance are already fixed, so best to use those.

		services = self._dbusmonitor.get_service_list('com.victronenergy.vebus')
		services.update(self._dbusmonitor.get_service_list('com.victronenergy.battery'))

		ul = {self.BATSERVICE_DEFAULT: 'Automatic', self.BATSERVICE_NOBATTERY: 'No battery monitor'}
		for servicename, instance in services.items():
			key = self._get_instance_service_name(servicename, instance)
			ul[key] = self._get_readable_service_name(servicename)
		self._dbusservice['/AvailableBatteryServices'] = json.dumps(ul)

		ul = {self.BATSERVICE_DEFAULT: 'Automatic', self.BATSERVICE_NOBATTERY: 'No battery monitor'}
		# For later: for device supporting multiple Dc measurement we should add entries for /Dc/1 etc as
		# well.
		for servicename, instance in services.items():
			key = self._get_instance_service_name(servicename, instance).replace('.', '_').replace('/', '_') + '/Dc/0'
			ul[key] = self._get_readable_service_name(servicename)
		self._dbusservice['/AvailableBatteryMeasurements'] = dbus.Dictionary(ul, signature='sv')

		self._determinebatteryservice()
		self._updatepvinverterspidlist()

		self._changed = True

	def _get_readable_service_name(self, servicename):
		return (self._dbusmonitor.get_value(servicename, '/ProductName') + ' on ' +
						self._dbusmonitor.get_value(servicename, '/Mgmt/Connection'))

	def _get_instance_service_name(self, service, instance):
		return '%s/%s' % ('.'.join(service.split('.')[0:3]), instance)

	def _get_service_mapping_path(self, service, instance):
		sn = self._get_instance_service_name(service, instance).replace('.', '_').replace('/', '_')
		return '/ServiceMapping/%s' % sn

	def _dbus_value_changed(self, dbusServiceName, dbusPath, dict, changes, deviceInstance):
		self._changed = True

		# Workaround because com.victronenergy.vebus is available even when there is no vebus product
		# connected.
		if dbusPath in ['/ProductName', '/Mgmt/Connection']:
			self._handleservicechange()

	def _device_added(self, service, instance, do_service_change=True):
		path = self._get_service_mapping_path(service, instance)
		if path in self._dbusservice:
			self._dbusservice[path] = service
		else:
			self._dbusservice.add_path(path, service)

		if do_service_change:
			self._handleservicechange()

	def _device_removed(self, service, instance):
		path = self._get_service_mapping_path(service, instance)
		if path in self._dbusservice:
			del self._dbusservice[path]
		self._handleservicechange()

	def _gettext(self, path, value):
		if path == '/Dc/Battery/State':
			state = {self.STATE_IDLE: 'Idle', self.STATE_CHARGING: 'Charging',
				self.STATE_DISCHARGING: 'Discharging'}
			return state[value]
		item = self._summeditems.get(path)
		if item is not None:
			return item['gettext'] % value
		return value

	def _compute_phase_totals(self, path, newvalues):
		total_power = None
		number_of_phases = None
		for phase in range(1, 4):
			p = newvalues.get('%s/L%s/Power' % (path, phase))
			total_power = _safeadd(total_power, p)
			if p is not None:
				number_of_phases = phase
		newvalues[path + '/Total/Power'] = total_power
		newvalues[path + '/NumberOfPhases'] = number_of_phases

	def _get_first_service(self, classfilter=None):
		services = self._dbusmonitor.get_service_list(classfilter=classfilter)
		if len(services) == 0:
			return None
		return sorted(services.keys())[0]
Esempio n. 9
0
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)
Esempio n. 10
0
class SystemCalc:
    def __init__(self,
                 dbusmonitor_gen=None,
                 dbusservice_gen=None,
                 settings_device_gen=None):
        self.STATE_IDLE = 0
        self.STATE_CHARGING = 1
        self.STATE_DISCHARGING = 2

        self.BATSERVICE_DEFAULT = 'default'
        self.BATSERVICE_NOBATTERY = 'nobattery'

        # Why this dummy? Because DbusMonitor expects these values to be there, even though we don't
        # need them. So just add some dummy data. This can go away when DbusMonitor is more generic.
        dummy = {
            'code': None,
            'whenToLog': 'configChange',
            'accessLevel': None
        }
        dbus_tree = {
            'com.victronenergy.solarcharger': {
                '/Connected': dummy,
                '/ProductName': dummy,
                '/Mgmt/Connection': dummy,
                '/Dc/0/Voltage': dummy,
                '/Dc/0/Current': dummy
            },
            'com.victronenergy.pvinverter': {
                '/Connected': dummy,
                '/ProductName': dummy,
                '/Mgmt/Connection': dummy,
                '/Ac/L1/Power': dummy,
                '/Ac/L2/Power': dummy,
                '/Ac/L3/Power': dummy,
                '/Position': dummy,
                '/ProductId': dummy
            },
            'com.victronenergy.battery': {
                '/Connected': dummy,
                '/ProductName': dummy,
                '/Mgmt/Connection': dummy,
                '/Dc/0/Voltage': dummy,
                '/Dc/0/Current': dummy,
                '/Dc/0/Power': dummy,
                '/Soc': dummy,
                '/TimeToGo': dummy,
                '/ConsumedAmphours': dummy,
                '/ProductId': dummy
            },
            'com.victronenergy.vebus': {
                '/Ac/ActiveIn/ActiveInput': dummy,
                '/Ac/ActiveIn/L1/P': dummy,
                '/Ac/ActiveIn/L2/P': dummy,
                '/Ac/ActiveIn/L3/P': dummy,
                '/Ac/Out/L1/P': dummy,
                '/Ac/Out/L2/P': dummy,
                '/Ac/Out/L3/P': dummy,
                '/Connected': dummy,
                '/Hub4/AcPowerSetpoint': dummy,
                '/ProductId': dummy,
                '/ProductName': dummy,
                '/Mgmt/Connection': dummy,
                '/Mode': dummy,
                '/State': dummy,
                '/Dc/0/Voltage': dummy,
                '/Dc/0/Current': dummy,
                '/Dc/0/Power': dummy,
                '/Soc': dummy
            },
            'com.victronenergy.charger': {
                '/Connected': dummy,
                '/ProductName': dummy,
                '/Mgmt/Connection': dummy,
                '/Dc/0/Voltage': dummy,
                '/Dc/0/Current': dummy
            },
            'com.victronenergy.grid': {
                '/Connected': dummy,
                '/ProductName': dummy,
                '/Mgmt/Connection': dummy,
                '/ProductId': dummy,
                '/DeviceType': dummy,
                '/Ac/L1/Power': dummy,
                '/Ac/L2/Power': dummy,
                '/Ac/L3/Power': dummy
            },
            'com.victronenergy.genset': {
                '/Connected': dummy,
                '/ProductName': dummy,
                '/Mgmt/Connection': dummy,
                '/ProductId': dummy,
                '/DeviceType': dummy,
                '/Ac/L1/Power': dummy,
                '/Ac/L2/Power': dummy,
                '/Ac/L3/Power': dummy
            },
            'com.victronenergy.settings': {
                '/Settings/SystemSetup/AcInput1': dummy,
                '/Settings/SystemSetup/AcInput2': dummy
            }
        }

        if dbusmonitor_gen is None:
            self._dbusmonitor = DbusMonitor(dbus_tree,
                                            self._dbus_value_changed,
                                            self._device_added,
                                            self._device_removed)
        else:
            self._dbusmonitor = dbusmonitor_gen(dbus_tree)

        # Connect to localsettings
        supported_settings = {
            'batteryservice': [
                '/Settings/SystemSetup/BatteryService',
                self.BATSERVICE_DEFAULT, 0, 0
            ],
            'hasdcsystem': ['/Settings/SystemSetup/HasDcSystem', 0, 0, 1],
            'writevebussoc': ['/Settings/SystemSetup/WriteVebusSoc', 0, 0, 1]
        }

        if settings_device_gen is None:
            self._settings = SettingsDevice(
                bus=dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS'
                in os.environ else dbus.SystemBus(),
                supportedSettings=supported_settings,
                eventCallback=self._handlechangedsetting)
        else:
            self._settings = settings_device_gen(supported_settings,
                                                 self._handlechangedsetting)

        # put ourselves on the dbus
        if dbusservice_gen is None:
            self._dbusservice = VeDbusService('com.victronenergy.system')
        else:
            self._dbusservice = dbusservice_gen('com.victronenergy.system')

        self._dbusservice.add_mandatory_paths(
            processname=__file__,
            processversion=softwareVersion,
            connection='data from other dbus processes',
            deviceinstance=0,
            productid=None,
            productname=None,
            firmwareversion=None,
            hardwareversion=None,
            connected=1)

        # At this moment, VRM portal ID is the MAC address of the CCGX. Anyhow, it should be string uniquely
        # identifying the CCGX.
        self._dbusservice.add_path('/Serial',
                                   value=get_vrm_portal_id(),
                                   gettextcallback=lambda x: str(x))
        self._dbusservice.add_path('/Relay/0/State',
                                   value=None,
                                   writeable=True,
                                   onchangecallback=lambda p, v: exit_on_error(
                                       self._on_relay_state_changed, p, v))

        self._dbusservice.add_path('/AvailableBatteryServices',
                                   value=None,
                                   gettextcallback=self._gettext)
        self._dbusservice.add_path('/AvailableBatteryMeasurements', value=None)
        self._dbusservice.add_path('/AutoSelectedBatteryService',
                                   value=None,
                                   gettextcallback=self._gettext)
        self._dbusservice.add_path('/AutoSelectedBatteryMeasurement',
                                   value=None,
                                   gettextcallback=self._gettext)
        self._dbusservice.add_path('/ActiveBatteryService',
                                   value=None,
                                   gettextcallback=self._gettext)
        self._dbusservice.add_path('/PvInvertersProductIds', value=None)
        self._dbusservice.add_path('/Dc/Battery/Alarms/CircuitBreakerTripped',
                                   value=None)
        self._summeditems = {
            '/Ac/Grid/L1/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Grid/L2/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Grid/L3/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Grid/Total/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Grid/NumberOfPhases': {
                'gettext': '%.0F W'
            },
            '/Ac/Grid/ProductId': {
                'gettext': '%s'
            },
            '/Ac/Grid/DeviceType': {
                'gettext': '%s'
            },
            '/Ac/Genset/L1/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Genset/L2/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Genset/L3/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Genset/Total/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Genset/NumberOfPhases': {
                'gettext': '%.0F W'
            },
            '/Ac/Genset/ProductId': {
                'gettext': '%s'
            },
            '/Ac/Genset/DeviceType': {
                'gettext': '%s'
            },
            '/Ac/Consumption/L1/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Consumption/L2/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Consumption/L3/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Consumption/Total/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/Consumption/NumberOfPhases': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnOutput/L1/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnOutput/L2/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnOutput/L3/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnOutput/Total/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnOutput/NumberOfPhases': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnGrid/L1/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnGrid/L2/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnGrid/L3/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnGrid/Total/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnGrid/NumberOfPhases': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnGenset/L1/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnGenset/L2/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnGenset/L3/Power': {
                'gettext': '%.0F W'
            },
            '/Ac/PvOnGenset/NumberOfPhases': {
                'gettext': '%d'
            },
            '/Ac/PvOnGenset/Total/Power': {
                'gettext': '%.0F W'
            },
            '/Dc/Pv/Power': {
                'gettext': '%.0F W'
            },
            '/Dc/Pv/Current': {
                'gettext': '%.1F A'
            },
            '/Dc/Battery/Voltage': {
                'gettext': '%.2F V'
            },
            '/Dc/Battery/Current': {
                'gettext': '%.1F A'
            },
            '/Dc/Battery/Power': {
                'gettext': '%.0F W'
            },
            '/Dc/Battery/Soc': {
                'gettext': '%.0F %%'
            },
            '/Dc/Battery/State': {
                'gettext': '%s'
            },
            '/Dc/Battery/TimeToGo': {
                'gettext': '%.0F s'
            },
            '/Dc/Battery/ConsumedAmphours': {
                'gettext': '%.1F Ah'
            },
            '/Dc/Charger/Power': {
                'gettext': '%.0F %%'
            },
            '/Dc/Vebus/Current': {
                'gettext': '%.1F A'
            },
            '/Dc/Vebus/Power': {
                'gettext': '%.0F W'
            },
            '/Dc/System/Power': {
                'gettext': '%.0F W'
            },
            '/Hub': {
                'gettext': '%s'
            },
            '/Ac/ActiveIn/Source': {
                'gettext': '%s'
            },
            '/VebusService': {
                'gettext': '%s'
            }
        }

        for path in self._summeditems.keys():
            self._dbusservice.add_path(path,
                                       value=None,
                                       gettextcallback=self._gettext)

        self._batteryservice = None
        self._determinebatteryservice()

        self._supervised = {}
        self._lg_battery = None

        if self._batteryservice is None:
            logger.info("Battery service initialized to None (setting == %s)" %
                        self._settings['batteryservice'])

        self._changed = True
        for service, instance in self._dbusmonitor.get_service_list().items():
            self._device_added(service, instance, do_service_change=False)

        self._handleservicechange()
        self._updatevalues()
        try:
            self._relay_file_read = open(relayGpioFile, 'rt')
            self._relay_file_write = open(relayGpioFile, 'wt')
            self._update_relay_state()
            gobject.timeout_add(5000, exit_on_error, self._update_relay_state)
        except IOError:
            self._relay_file_read = None
            self._relay_file_write = None
            logging.warn('Could not open %s (relay)' % relayGpioFile)

        self._writeVebusSocCounter = 9
        gobject.timeout_add(1000, exit_on_error, self._handletimertick)
        gobject.timeout_add(60000, exit_on_error, self._process_supervised)

    def _handlechangedsetting(self, setting, oldvalue, newvalue):
        self._determinebatteryservice()
        self._changed = True

    def _determinebatteryservice(self):
        auto_battery_service = self._autoselect_battery_service()
        auto_battery_measurement = None
        if auto_battery_service is not None:
            services = self._dbusmonitor.get_service_list()
            if auto_battery_service in services:
                auto_battery_measurement = \
                 self._get_instance_service_name(auto_battery_service, services[auto_battery_service])
                auto_battery_measurement = auto_battery_measurement.replace(
                    '.', '_').replace('/', '_') + '/Dc/0'
        self._dbusservice[
            '/AutoSelectedBatteryMeasurement'] = auto_battery_measurement

        if self._settings['batteryservice'] == self.BATSERVICE_DEFAULT:
            newbatteryservice = auto_battery_service
            self._dbusservice['/AutoSelectedBatteryService'] = (
                'No battery monitor found' if newbatteryservice is None else
                self._get_readable_service_name(newbatteryservice))

        elif self._settings['batteryservice'] == self.BATSERVICE_NOBATTERY:
            self._dbusservice['/AutoSelectedBatteryService'] = None
            newbatteryservice = None

        else:
            self._dbusservice['/AutoSelectedBatteryService'] = None

            s = self._settings['batteryservice'].split('/')
            if len(s) != 2:
                logger.error("The battery setting (%s) is invalid!" %
                             self._settings['batteryservice'])
            serviceclass = s[0]
            instance = int(s[1]) if len(s) == 2 else None
            services = self._dbusmonitor.get_service_list(
                classfilter=serviceclass)
            if instance not in services.values():
                # Once chosen battery monitor does not exist. Don't auto change the setting (it might come
                # back). And also don't autoselect another.
                newbatteryservice = None
            else:
                # According to https://www.python.org/dev/peps/pep-3106/, dict.keys() and dict.values()
                # always have the same order.
                newbatteryservice = services.keys()[services.values().index(
                    instance)]

        if newbatteryservice != self._batteryservice:
            services = self._dbusmonitor.get_service_list()
            instance = services.get(newbatteryservice, None)
            if instance is None:
                battery_service = None
            else:
                battery_service = self._get_instance_service_name(
                    newbatteryservice, instance)
            self._dbusservice['/ActiveBatteryService'] = battery_service
            logger.info(
                "Battery service, setting == %s, changed from %s to %s (%s)" %
                (self._settings['batteryservice'], self._batteryservice,
                 newbatteryservice, instance))
            self._batteryservice = newbatteryservice

    def _autoselect_battery_service(self):
        # Default setting business logic:
        # first try to use a battery service (BMV or Lynx Shunt VE.Can). If there
        # is more than one battery service, just use a random one. If no battery service is
        # available, check if there are not Solar chargers and no normal chargers. If they are not
        # there, assume this is a hub-2, hub-3 or hub-4 system and use VE.Bus SOC.
        batteries = self._get_connected_service_list(
            'com.victronenergy.battery')

        if len(batteries) > 0:
            return sorted(batteries)[0]  # Pick a random battery service

        if self._get_first_connected_service(
                'com.victronenergy.solarcharger') is not None:
            return None

        if self._get_first_connected_service(
                'com.victronenergy.charger') is not None:
            return None

        vebus_services = self._get_first_connected_service(
            'com.victronenergy.vebus')
        if vebus_services is None:
            return None
        return vebus_services[0]

    # Called on a one second timer
    def _handletimertick(self):
        if self._changed:
            self._updatevalues()
        self._changed = False

        self._writeVebusSocCounter += 1
        if self._writeVebusSocCounter >= 10:
            self._writeVebusSoc()
            self._writeVebusSocCounter = 0

        return True  # keep timer running

    def _writeVebusSoc(self):
        # ==== COPY BATTERY SOC TO VEBUS ====
        if self._settings['writevebussoc'] and self._dbusservice['/VebusService'] and self._dbusservice['/Dc/Battery/Soc'] and \
         self._batteryservice.split('.')[2] != 'vebus':

            logger.debug("writing this soc to vebus: %d",
                         self._dbusservice['/Dc/Battery/Soc'])
            self._dbusmonitor.get_item(
                self._dbusservice['/VebusService'],
                '/Soc').set_value(self._dbusservice['/Dc/Battery/Soc'])

    def _updatepvinverterspidlist(self):
        # Create list of connected pv inverters id's
        pvinverters = self._dbusmonitor.get_service_list(
            'com.victronenergy.pvinverter')
        productids = []

        for pvinverter in pvinverters:
            pid = self._dbusmonitor.get_value(pvinverter, '/ProductId')
            if pid is not None and pid not in productids:
                productids.append(pid)
        self._dbusservice['/PvInvertersProductIds'] = dbus.Array(productids,
                                                                 signature='i')

    def _updatevalues(self):
        # ==== PREPARATIONS ====
        # Determine values used in logic below
        vebusses = self._dbusmonitor.get_service_list(
            'com.victronenergy.vebus')
        vebuspower = 0
        for vebus in vebusses:
            v = self._dbusmonitor.get_value(vebus, '/Dc/0/Voltage')
            i = self._dbusmonitor.get_value(vebus, '/Dc/0/Current')
            if v is not None and i is not None:
                vebuspower += v * i

        # ==== PVINVERTERS ====
        pvinverters = self._dbusmonitor.get_service_list(
            'com.victronenergy.pvinverter')
        newvalues = {}
        pos = {0: '/Ac/PvOnGrid', 1: '/Ac/PvOnOutput', 2: '/Ac/PvOnGenset'}
        total = {0: None, 1: None, 2: None}
        for pvinverter in pvinverters:
            # Position will be None if PV inverter service has just been removed (after retrieving the
            # service list).
            position = pos.get(
                self._dbusmonitor.get_value(pvinverter, '/Position'))
            if position is not None:
                for phase in range(1, 4):
                    power = self._dbusmonitor.get_value(
                        pvinverter, '/Ac/L%s/Power' % phase)
                    if power is not None:
                        path = '%s/L%s/Power' % (position, phase)
                        newvalues[path] = _safeadd(newvalues.get(path), power)

        for path in pos.values():
            self._compute_phase_totals(path, newvalues)

        # ==== SOLARCHARGERS ====
        solarchargers = self._dbusmonitor.get_service_list(
            'com.victronenergy.solarcharger')
        solarcharger_batteryvoltage = None
        for solarcharger in solarchargers:
            v = self._dbusmonitor.get_value(solarcharger, '/Dc/0/Voltage')
            if v is None:
                continue
            i = self._dbusmonitor.get_value(solarcharger, '/Dc/0/Current')
            if i is None:
                continue

            if '/Dc/Pv/Power' not in newvalues:
                newvalues['/Dc/Pv/Power'] = v * i
                newvalues['/Dc/Pv/Current'] = i
                solarcharger_batteryvoltage = v
            else:
                newvalues['/Dc/Pv/Power'] += v * i
                newvalues['/Dc/Pv/Current'] += i

        # ==== CHARGERS ====
        chargers = self._dbusmonitor.get_service_list(
            'com.victronenergy.charger')
        charger_batteryvoltage = None
        for charger in chargers:
            # Assume the battery connected to output 0 is the main battery
            v = self._dbusmonitor.get_value(charger, '/Dc/0/Voltage')
            if v is None:
                continue

            charger_batteryvoltage = v

            i = self._dbusmonitor.get_value(charger, '/Dc/0/Current')
            if i is None:
                continue

            if '/Dc/Charger/Power' not in newvalues:
                newvalues['/Dc/Charger/Power'] = v * i
            else:
                newvalues['/Dc/Charger/Power'] += v * i

        # ==== BATTERY ====
        if self._batteryservice is not None:
            batteryservicetype = self._batteryservice.split('.')[
                2]  # either 'battery' or 'vebus'
            newvalues['/Dc/Battery/Soc'] = self._dbusmonitor.get_value(
                self._batteryservice, '/Soc')
            newvalues['/Dc/Battery/TimeToGo'] = self._dbusmonitor.get_value(
                self._batteryservice, '/TimeToGo')
            newvalues[
                '/Dc/Battery/ConsumedAmphours'] = self._dbusmonitor.get_value(
                    self._batteryservice, '/ConsumedAmphours')

            if batteryservicetype == 'battery':
                newvalues['/Dc/Battery/Voltage'] = self._dbusmonitor.get_value(
                    self._batteryservice, '/Dc/0/Voltage')
                newvalues['/Dc/Battery/Current'] = self._dbusmonitor.get_value(
                    self._batteryservice, '/Dc/0/Current')
                newvalues['/Dc/Battery/Power'] = self._dbusmonitor.get_value(
                    self._batteryservice, '/Dc/0/Power')

            elif batteryservicetype == 'vebus':
                newvalues['/Dc/Battery/Voltage'] = self._dbusmonitor.get_value(
                    self._batteryservice, '/Dc/0/Voltage')
                newvalues['/Dc/Battery/Current'] = self._dbusmonitor.get_value(
                    self._batteryservice, '/Dc/0/Current')
                if newvalues['/Dc/Battery/Voltage'] is not None and newvalues[
                        '/Dc/Battery/Current'] is not None:
                    newvalues['/Dc/Battery/Power'] = (
                        newvalues['/Dc/Battery/Voltage'] *
                        newvalues['/Dc/Battery/Current'])

            p = newvalues.get('/Dc/Battery/Power', None)
            if p is not None:
                if p > 30:
                    newvalues['/Dc/Battery/State'] = self.STATE_CHARGING
                elif p < -30:
                    newvalues['/Dc/Battery/State'] = self.STATE_DISCHARGING
                else:
                    newvalues['/Dc/Battery/State'] = self.STATE_IDLE

        else:
            batteryservicetype = None
            if solarcharger_batteryvoltage is not None:
                newvalues['/Dc/Battery/Voltage'] = solarcharger_batteryvoltage
            elif charger_batteryvoltage is not None:
                newvalues['/Dc/Battery/Voltage'] = charger_batteryvoltage
            else:
                # CCGX-connected system consists of only a Multi, but it is not user-selected, nor
                # auto-selected as the battery-monitor, probably because there are other loads or chargers.
                # In that case, at least use its reported battery voltage.
                vebusses = self._dbusmonitor.get_service_list(
                    'com.victronenergy.vebus')
                for vebus in vebusses:
                    v = self._dbusmonitor.get_value(vebus, '/Dc/0/Voltage')
                    if v is not None:
                        newvalues['/Dc/Battery/Voltage'] = v

            if self._settings[
                    'hasdcsystem'] == 0 and '/Dc/Battery/Voltage' in newvalues:
                # No unmonitored DC loads or chargers, and also no battery monitor: derive battery watts
                # and amps from vebus, solarchargers and chargers.
                assert '/Dc/Battery/Power' not in newvalues
                assert '/Dc/Battery/Current' not in newvalues
                p = newvalues.get('/Dc/Pv/Power', 0) + newvalues.get(
                    '/Dc/Charger/Power', 0) + vebuspower
                voltage = newvalues['/Dc/Battery/Voltage']
                newvalues[
                    '/Dc/Battery/Current'] = p / voltage if voltage > 0 else None
                newvalues['/Dc/Battery/Power'] = p

        # ==== SYSTEM ====
        if self._settings[
                'hasdcsystem'] == 1 and batteryservicetype == 'battery':
            # Calculate power being generated/consumed by not measured devices in the network.
            # /Dc/System: positive: consuming power
            # VE.Bus: Positive: current flowing from the Multi to the dc system or battery
            # Solarcharger & other chargers: positive: charging
            # battery: Positive: charging battery.
            # battery = solarcharger + charger + ve.bus - system

            battery_power = newvalues.get('/Dc/Battery/Power')
            if battery_power is not None:
                dc_pv_power = newvalues.get('/Dc/Pv/Power', 0)
                charger_power = newvalues.get('/Dc/Charger/Power', 0)
                newvalues[
                    '/Dc/System/Power'] = dc_pv_power + charger_power + vebuspower - battery_power

        # ==== Vebus ====
        # Assume there's only 1 multi service present on the D-Bus
        multi = self._get_first_connected_service('com.victronenergy.vebus')
        multi_path = None
        if multi is not None:
            multi_path = multi[0]
            dc_current = self._dbusmonitor.get_value(multi_path,
                                                     '/Dc/0/Current')
            newvalues['/Dc/Vebus/Current'] = dc_current
            dc_power = self._dbusmonitor.get_value(multi_path, '/Dc/0/Power')
            # Just in case /Dc/0/Power is not available
            if dc_power == None and dc_current is not None:
                dc_voltage = self._dbusmonitor.get_value(
                    multi_path, '/Dc/0/Voltage')
                if dc_voltage is not None:
                    dc_power = dc_voltage * dc_current
            # Note that there is also vebuspower, which is the total DC power summed over all multis.
            # However, this value cannot be combined with /Dc/Multi/Current, because it does not make sense
            # to add the Dc currents of all multis if they do not share the same DC voltage.
            newvalues['/Dc/Vebus/Power'] = dc_power

        newvalues['/VebusService'] = multi_path

        # ===== AC IN SOURCE =====
        ac_in_source = None
        active_input = self._dbusmonitor.get_value(multi_path,
                                                   '/Ac/ActiveIn/ActiveInput')
        if active_input is not None:
            settings_path = '/Settings/SystemSetup/AcInput%s' % (active_input +
                                                                 1)
            ac_in_source = self._dbusmonitor.get_value(
                'com.victronenergy.settings', settings_path)
        newvalues['/Ac/ActiveIn/Source'] = ac_in_source

        # ===== HUB MODE =====
        # The code below should be executed after PV inverter data has been updated, because we need the
        # PV inverter total power to update the consumption.
        hub = None
        if self._dbusmonitor.get_value(multi_path,
                                       '/Hub4/AcPowerSetpoint') is not None:
            hub = 4
        elif newvalues.get('/Dc/Pv/Power', None) is not None:
            hub = 1
        elif newvalues.get('/Ac/PvOnOutput/Total/Power', None) is not None:
            hub = 2
        elif newvalues.get('/Ac/PvOnGrid/Total/Power', None) is not None or \
         newvalues.get('/Ac/PvOnGenset/Total/Power', None) is not None:
            hub = 3
        newvalues['/Hub'] = hub

        # ===== GRID METERS & CONSUMPTION ====
        consumption = {"L1": None, "L2": None, "L3": None}
        for device_type in ['Grid', 'Genset']:
            servicename = 'com.victronenergy.%s' % device_type.lower()
            energy_meter = self._get_first_connected_service(servicename)
            em_service = None if energy_meter is None else energy_meter[0]
            uses_active_input = False
            if multi_path is not None:
                # If a grid meter is present we use values from it. If not, we look at the multi. If it has
                # AcIn1 or AcIn2 connected to the grid, we use those values.
                # com.victronenergy.grid.??? indicates presence of an energy meter used as grid meter.
                # com.victronenergy.vebus.???/Ac/ActiveIn/ActiveInput: decides which whether we look at AcIn1
                # or AcIn2 as possible grid connection.
                if ac_in_source is not None:
                    uses_active_input = ac_in_source > 0 and (
                        ac_in_source == 2) == (device_type == 'Genset')
            for phase in consumption:
                p = None
                pvpower = newvalues.get('/Ac/PvOn%s/%s/Power' %
                                        (device_type, phase))
                if em_service is not None:
                    p = self._dbusmonitor.get_value(em_service,
                                                    '/Ac/%s/Power' % phase)
                    # Compute consumption between energy meter and multi (meter power - multi AC in) and
                    # add an optional PV inverter on input to the mix.
                    c = None
                    if uses_active_input:
                        ac_in = self._dbusmonitor.get_value(
                            multi_path, '/Ac/ActiveIn/%s/P' % phase)
                        if ac_in is not None:
                            c = _safeadd(c, -ac_in)
                    # If there's any power coming from a PV inverter in the inactive AC in (which is unlikely),
                    # it will still be used, because there may also be a load in the same ACIn consuming
                    # power, or the power could be fed back to the net.
                    c = _safeadd(c, p, pvpower)
                    consumption[phase] = _safeadd(consumption[phase],
                                                  _safemax(0, c))
                else:
                    if uses_active_input:
                        p = self._dbusmonitor.get_value(
                            multi_path, '/Ac/ActiveIn/%s/P' % phase)
                    # No relevant energy meter present. Assume there is no load between the grid and the multi.
                    # There may be a PV inverter present though (Hub-3 setup).
                    if pvpower != None:
                        p = _safeadd(p, -pvpower)
                newvalues['/Ac/%s/%s/Power' % (device_type, phase)] = p
            self._compute_phase_totals('/Ac/%s' % device_type, newvalues)
            product_id = None
            device_type_id = None
            if em_service is not None:
                product_id = self._dbusmonitor.get_value(
                    em_service, '/ProductId')
                device_type_id = self._dbusmonitor.get_value(
                    em_service, '/DeviceType')
            if product_id is None and uses_active_input:
                product_id = self._dbusmonitor.get_value(
                    multi_path, '/ProductId')
            newvalues['/Ac/%s/ProductId' % device_type] = product_id
            newvalues['/Ac/%s/DeviceType' % device_type] = device_type_id
        for phase in consumption:
            c = newvalues.get('/Ac/PvOnOutput/%s/Power' % phase)
            if multi_path is not None:
                ac_out = self._dbusmonitor.get_value(multi_path,
                                                     '/Ac/Out/%s/P' % phase)
                c = _safeadd(c, ac_out)
            newvalues['/Ac/Consumption/%s/Power' % phase] = _safeadd(
                consumption[phase], _safemax(0, c))
        self._compute_phase_totals('/Ac/Consumption', newvalues)

        self._check_lg_battery(multi_path)

        # ==== UPDATE DBUS ITEMS ====
        for path in self._summeditems.keys():
            # Why the None? Because we want to invalidate things we don't have anymore.
            self._dbusservice[path] = newvalues.get(path, None)

    def _handleservicechange(self):
        # Update the available battery monitor services, used to populate the dropdown in the settings.
        # Below code makes a dictionary. The key is [dbuserviceclass]/[deviceinstance]. For example
        # "battery/245". The value is the name to show to the user in the dropdown. The full dbus-
        # servicename, ie 'com.victronenergy.vebus.ttyO1' is not used, since the last part of that is not
        # fixed. dbus-serviceclass name and the device instance are already fixed, so best to use those.

        services = self._get_connected_service_list('com.victronenergy.vebus')
        services.update(
            self._get_connected_service_list('com.victronenergy.battery'))

        ul = {
            self.BATSERVICE_DEFAULT: 'Automatic',
            self.BATSERVICE_NOBATTERY: 'No battery monitor'
        }
        for servicename, instance in services.items():
            key = self._get_instance_service_name(servicename, instance)
            ul[key] = self._get_readable_service_name(servicename)
        self._dbusservice['/AvailableBatteryServices'] = json.dumps(ul)

        ul = {
            self.BATSERVICE_DEFAULT: 'Automatic',
            self.BATSERVICE_NOBATTERY: 'No battery monitor'
        }
        # For later: for device supporting multiple Dc measurement we should add entries for /Dc/1 etc as
        # well.
        for servicename, instance in services.items():
            key = self._get_instance_service_name(
                servicename, instance).replace('.', '_').replace('/',
                                                                 '_') + '/Dc/0'
            ul[key] = self._get_readable_service_name(servicename)
        self._dbusservice['/AvailableBatteryMeasurements'] = dbus.Dictionary(
            ul, signature='sv')

        self._determinebatteryservice()
        self._updatepvinverterspidlist()

        self._changed = True

    def _get_readable_service_name(self, servicename):
        return (self._dbusmonitor.get_value(servicename, '/ProductName') +
                ' on ' +
                self._dbusmonitor.get_value(servicename, '/Mgmt/Connection'))

    def _get_instance_service_name(self, service, instance):
        return '%s/%s' % ('.'.join(service.split('.')[0:3]), instance)

    def _get_service_mapping_path(self, service, instance):
        sn = self._get_instance_service_name(service, instance).replace(
            '.', '_').replace('/', '_')
        return '/ServiceMapping/%s' % sn

    def _remove_unconnected_services(self, services):
        # Workaround: because com.victronenergy.vebus is available even when there is no vebus product
        # connected. Remove any that is not connected. For this, we use /State since mandatory path
        # /Connected is not implemented in mk2dbus.
        for servicename in services.keys():
            if ((servicename.split('.')[2] == 'vebus' and
                 self._dbusmonitor.get_value(servicename, '/State') is None) or
                    self._dbusmonitor.get_value(servicename, '/Connected') != 1
                    or self._dbusmonitor.get_value(servicename, '/ProductName')
                    is None or self._dbusmonitor.get_value(
                        servicename, '/Mgmt/Connection') is None):
                del services[servicename]

    def _dbus_value_changed(self, dbusServiceName, dbusPath, dict, changes,
                            deviceInstance):
        self._changed = True

        # Workaround because com.victronenergy.vebus is available even when there is no vebus product
        # connected.
        if (dbusPath in ['/Connected', '/ProductName', '/Mgmt/Connection']
                or (dbusPath == '/State' and dbusServiceName.split('.')[0:3]
                    == ['com', 'victronenergy', 'vebus'])):
            self._handleservicechange()

    def _device_added(self, service, instance, do_service_change=True):
        path = self._get_service_mapping_path(service, instance)
        if path in self._dbusservice:
            self._dbusservice[path] = service
        else:
            self._dbusservice.add_path(path, service)

        if do_service_change:
            self._handleservicechange()

        service_type = service.split('.')[2]
        if service_type == 'battery' or service_type == 'solarcharger':
            try:
                proxy = self._dbusmonitor.dbusConn.get_object(service,
                                                              '/ProductId',
                                                              introspect=False)
                method = proxy.get_dbus_method('GetValue')
                self._supervised[service] = method
            except dbus.DBusException:
                pass

        if service_type == 'battery' and self._dbusmonitor.get_value(
                service, '/ProductId') == 0xB004:
            logging.info('LG battery service appeared: %s' % service)
            self._lg_battery = service
            self._lg_voltage_buffer = []
            self._dbusservice['/Dc/Battery/Alarms/CircuitBreakerTripped'] = 0

    def _device_removed(self, service, instance):
        path = self._get_service_mapping_path(service, instance)
        if path in self._dbusservice:
            del self._dbusservice[path]
        self._handleservicechange()
        if service in self._supervised:
            del self._supervised[service]

        if service == self._lg_battery:
            logging.info('LG battery service disappeared: %s' % service)
            self._lg_battery = None
            self._lg_voltage_buffer = None
            self._dbusservice[
                '/Dc/Battery/Alarms/CircuitBreakerTripped'] = None

    def _gettext(self, path, value):
        if path == '/Dc/Battery/State':
            state = {
                self.STATE_IDLE: 'Idle',
                self.STATE_CHARGING: 'Charging',
                self.STATE_DISCHARGING: 'Discharging'
            }
            return state[value]
        item = self._summeditems.get(path)
        if item is not None:
            return item['gettext'] % value
        return str(value)

    def _compute_phase_totals(self, path, newvalues):
        total_power = None
        number_of_phases = None
        for phase in range(1, 4):
            p = newvalues.get('%s/L%s/Power' % (path, phase))
            total_power = _safeadd(total_power, p)
            if p is not None:
                number_of_phases = phase
        newvalues[path + '/Total/Power'] = total_power
        newvalues[path + '/NumberOfPhases'] = number_of_phases

    def _get_connected_service_list(self, classfilter=None):
        services = self._dbusmonitor.get_service_list(classfilter=classfilter)
        self._remove_unconnected_services(services)
        return services

    def _get_first_connected_service(self, classfilter=None):
        services = self._get_connected_service_list(classfilter=classfilter)
        if len(services) == 0:
            return None
        return services.items()[0]

    def _process_supervised(self):
        for service, method in self._supervised.items():
            # Do an async call. If the owner of the service does not answer, we do not want to wait for
            # the timeout here.
            method.call_async(error_handler=lambda x: exit_on_error(
                self._supervise_failed, service, x))
        return True

    def _supervise_failed(self, service, error):
        try:
            if error.get_dbus_name() != 'org.freedesktop.DBus.Error.NoReply':
                logging.info('Ignoring supervise error from %s: %s' %
                             (service, error))
                return
            logging.error('%s is not responding to D-Bus requests' % service)
            proxy = self._dbusmonitor.dbusConn.get_object(
                'org.freedesktop.DBus', '/', introspect=False)
            pid = proxy.GetConnectionUnixProcessID(service)
            if pid is not None and pid > 1:
                logging.error('killing owner of %s (pid=%s)' % (service, pid))
                os.kill(pid, signal.SIGKILL)
        except (OSError, dbus.exceptions.DBusException):
            print_exc()

    def _update_relay_state(self):
        state = None
        try:
            self._relay_file_read.seek(0)
            state = int(self._relay_file_read.read().strip())
        except (IOError, ValueError):
            print_exc()
        self._dbusservice['/Relay/0/State'] = state
        return True

    def _on_relay_state_changed(self, path, value):
        if self._relay_file_write is None:
            return False
        try:
            v = int(value)
            if v < 0 or v > 1:
                return False
            self._relay_file_write.write(str(v))
            self._relay_file_write.flush()
            return True
        except (IOError, ValueError):
            print_exc()
            return False

    def _check_lg_battery(self, multi_path):
        if self._lg_battery is None or multi_path is None:
            return
        battery_current = self._dbusmonitor.get_value(self._lg_battery,
                                                      '/Dc/0/Current')
        if battery_current is None or abs(battery_current) > 0.01:
            if len(self._lg_voltage_buffer) > 0:
                logging.debug('LG voltage buffer reset')
                self._lg_voltage_buffer = []
            return
        vebus_voltage = self._dbusmonitor.get_value(multi_path,
                                                    '/Dc/0/Voltage')
        if vebus_voltage is None:
            return
        self._lg_voltage_buffer.append(float(vebus_voltage))
        if len(self._lg_voltage_buffer) > 40:
            self._lg_voltage_buffer = self._lg_voltage_buffer[-40:]
        elif len(self._lg_voltage_buffer) < 20:
            return
        min_voltage = min(self._lg_voltage_buffer)
        max_voltage = max(self._lg_voltage_buffer)
        battery_voltage = self._dbusmonitor.get_value(self._lg_battery,
                                                      '/Dc/0/Voltage')
        logging.debug('LG battery current V=%s I=%s' %
                      (battery_voltage, battery_current))
        if min_voltage < 0.9 * battery_voltage or max_voltage > 1.1 * battery_voltage:
            logging.error(
                'LG shutdown detected V=%s I=%s %s' %
                (battery_voltage, battery_current, self._lg_voltage_buffer))
            item = self._dbusmonitor.get_item(multi_path, '/Mode')
            if item is None:
                logging.error('Cannot switch off vebus device')
            else:
                self._dbusservice[
                    '/Dc/Battery/Alarms/CircuitBreakerTripped'] = 2
                item.set_value(dbus.Int32(4, variant_level=1))
                self._lg_voltage_buffer = []
Esempio n. 11
0
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')
Esempio n. 12
0
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)
Esempio n. 13
0
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)