class PinHandler(object, metaclass=HandlerMaker):
    product_id = 0xFFFF
    _product_name = 'Generic GPIO'
    dbus_name = "digital"

    def __init__(self, bus, base, path, gpio, settings):
        self.gpio = gpio
        self.path = path
        self.bus = bus
        self.settings = settings
        self._level = 0  # Remember last state

        self.service = VeDbusService("{}.{}.input{:02d}".format(
            base, self.dbus_name, gpio),
                                     bus=bus)

        # Add objects required by ve-api
        self.service.add_path('/Mgmt/ProcessName', __file__)
        self.service.add_path('/Mgmt/ProcessVersion', VERSION)
        self.service.add_path('/Mgmt/Connection', path)
        self.service.add_path('/DeviceInstance', gpio)
        self.service.add_path('/ProductId', self.product_id)
        self.service.add_path('/ProductName', self.product_name)
        self.service.add_path('/Connected', 1)

        # Custom name setting
        def _change_name(p, v):
            # This should fire a change event that will update product_name
            # below.
            settings['name'] = v
            return True

        self.service.add_path('/CustomName',
                              settings['name'],
                              writeable=True,
                              onchangecallback=_change_name)

        # We'll count the pulses for all types of services
        self.service.add_path('/Count', value=settings['count'])

    @property
    def product_name(self):
        return self.settings['name'] or self._product_name

    @product_name.setter
    def product_name(self, v):
        # Some pin types don't have an associated service (Disabled pins for
        # example)
        if self.service is not None:
            self.service['/ProductName'] = v or self._product_name

    def deactivate(self):
        self.save_count()
        self.service.__del__()
        del self.service
        self.service = None

    @property
    def level(self):
        return self._level

    @level.setter
    def level(self, l):
        self._level = int(bool(l))

    def toggle(self, level):
        # Only increment Count on rising edge.
        if level and level != self._level:
            self.service['/Count'] = (self.service['/Count'] + 1) % MAXCOUNT
        self._level = level

    def refresh(self):
        """ Toggle state to last remembered state. This is called if settings
            are changed so the Service can recalculate paths. """
        self.toggle(self._level)

    def save_count(self):
        if self.service is not None:
            self.settings['count'] = self.count

    @property
    def active(self):
        return self.service is not None

    @property
    def count(self):
        return self.service['/Count']

    @count.setter
    def count(self, v):
        self.service['/Count'] = v

    @classmethod
    def createHandler(cls, _type, *args, **kwargs):
        if _type in cls.handlers:
            return cls.handlers[_type](*args, **kwargs)
        return None
class DbusGenerator:
    def __init__(self, retries=300):
        self._bus = dbus.SystemBus() if (
            platform.machine() == 'armv7l' or 'DBUS_SESSION_BUS_ADDRESS'
            not in environ) else dbus.SessionBus()
        self.RELAY_GPIO_FILE = '/sys/class/gpio/gpio182/value'
        self.HISTORY_DAYS = 30
        # One second per retry
        self.RETRIES_ON_ERROR = retries
        self._testrun_soc_retries = 0
        self._last_counters_check = 0
        self._dbusservice = None
        self._starttime = 0
        self._manualstarttimer = 0
        self._last_runtime_update = 0
        self._timer_runnning = 0
        self._battery_measurement_voltage_import = None
        self._battery_measurement_current_import = None
        self._battery_measurement_soc_import = None
        self._battery_measurement_available = True
        self._vebusservice_high_temperature_import = None
        self._vebusservice_overload_import = None
        self._vebusservice = None
        self._vebusservice_available = False
        self._relay_state_import = None

        self._condition_stack = {
            'batteryvoltage': {
                'name': 'batteryvoltage',
                'reached': False,
                'boolean': False,
                'timed': True,
                'start_timer': 0,
                'stop_timer': 0,
                'valid': True,
                'enabled': False,
                'retries': 0,
                'monitoring': 'battery'
            },
            'batterycurrent': {
                'name': 'batterycurrent',
                'reached': False,
                'boolean': False,
                'timed': True,
                'start_timer': 0,
                'stop_timer': 0,
                'valid': True,
                'enabled': False,
                'retries': 0,
                'monitoring': 'battery'
            },
            'acload': {
                'name': 'acload',
                'reached': False,
                'boolean': False,
                'timed': True,
                'start_timer': 0,
                'stop_timer': 0,
                'valid': True,
                'enabled': False,
                'retries': 0,
                'monitoring': 'vebus'
            },
            'inverterhightemp': {
                'name': 'inverterhightemp',
                'reached': False,
                'boolean': True,
                'timed': True,
                'start_timer': 0,
                'stop_timer': 0,
                'valid': True,
                'enabled': False,
                'retries': 0,
                'monitoring': 'vebus'
            },
            'inverteroverload': {
                'name': 'inverteroverload',
                'reached': False,
                'boolean': True,
                'timed': True,
                'start_timer': 0,
                'stop_timer': 0,
                'valid': True,
                'enabled': False,
                'retries': 0,
                'monitoring': 'vebus'
            },
            'soc': {
                'name': 'soc',
                'reached': False,
                'boolean': False,
                'timed': False,
                'valid': True,
                'enabled': False,
                'retries': 0,
                'monitoring': 'battery'
            }
        }

        # DbusMonitor expects these values to be there, even though we don need them. So just
        # add some dummy data. This can go away when DbusMonitor is more generic.
        dummy = {
            'code': None,
            'whenToLog': 'configChange',
            'accessLevel': None
        }

        # TODO: possible improvement: don't use the DbusMonitor it all, since we are only monitoring
        # a set of static values which will always be available. DbusMonitor watches for services
        # that come and go, and takes care of automatic signal subscribtions etc. etc: all not necessary
        # in this use case where we have fixed services names (com.victronenergy.settings, and c
        # com.victronenergy.system).
        self._dbusmonitor = DbusMonitor({
         'com.victronenergy.settings': {   # This is not our setting so do it here. not in supportedSettings
          '/Settings/Relay/Function': dummy,
          '/Settings/Relay/Polarity': dummy,
          '/Settings/System/TimeZone': dummy,
          },
         'com.victronenergy.system': {   # This is not our setting so do it here. not in supportedSettings
          '/Ac/Consumption/Total/Power': dummy,
          '/Ac/PvOnOutput/Total/Power': dummy,
          '/Ac/PvOnGrid/Total/Power': dummy,
          '/Ac/PvOnGenset/Total/Power': dummy,
          '/Dc/Pv/Power': dummy,
          '/AutoSelectedBatteryMeasurement': dummy,
          }
        }, self._dbus_value_changed, self._device_added, self._device_removed)

        # Set timezone to user selected timezone
        environ['TZ'] = self._dbusmonitor.get_value(
            'com.victronenergy.settings', '/Settings/System/TimeZone')

        # Connect to localsettings
        self._settings = SettingsDevice(
            bus=self._bus,
            supportedSettings={
                'autostart':
                ['/Settings/Generator0/AutoStartEnabled', 1, 0, 1],
                'accumulateddaily':
                ['/Settings/Generator0/AccumulatedDaily', '', 0, 0],
                'accumulatedtotal':
                ['/Settings/Generator0/AccumulatedTotal', 0, 0, 0],
                'batterymeasurement':
                ['/Settings/Generator0/BatteryService', "default", 0, 0],
                'minimumruntime':
                ['/Settings/Generator0/MinimumRuntime', 0, 0,
                 86400],  # minutes
                # On permanent loss of communication: 0 = Stop, 1 = Start, 2 = keep running
                'onlosscommunication':
                ['/Settings/Generator0/OnLossCommunication', 0, 0, 2],
                # Quiet hours
                'quiethoursenabled':
                ['/Settings/Generator0/QuietHours/Enabled', 0, 0, 1],
                'quiethoursstarttime':
                ['/Settings/Generator0/QuietHours/StartTime', 75600, 0, 86400],
                'quiethoursendtime':
                ['/Settings/Generator0/QuietHours/EndTime', 21600, 0, 86400],
                # SOC
                'socenabled': ['/Settings/Generator0/Soc/Enabled', 0, 0, 1],
                'socstart':
                ['/Settings/Generator0/Soc/StartValue', 90, 0, 100],
                'socstop': ['/Settings/Generator0/Soc/StopValue', 90, 0, 100],
                'qh_socstart':
                ['/Settings/Generator0/Soc/QuietHoursStartValue', 90, 0, 100],
                'qh_socstop':
                ['/Settings/Generator0/Soc/QuietHoursStopValue', 90, 0, 100],
                # Voltage
                'batteryvoltageenabled': [
                    '/Settings/Generator0/BatteryVoltage/Enabled', 0, 0, 1
                ],
                'batteryvoltagestart': [
                    '/Settings/Generator0/BatteryVoltage/StartValue', 11.5, 0,
                    150
                ],
                'batteryvoltagestop': [
                    '/Settings/Generator0/BatteryVoltage/StopValue', 12.4, 0,
                    150
                ],
                'batteryvoltagestarttimer': [
                    '/Settings/Generator0/BatteryVoltage/StartTimer', 20, 0,
                    10000
                ],
                'batteryvoltagestoptimer': [
                    '/Settings/Generator0/BatteryVoltage/StopTimer', 20, 0,
                    10000
                ],
                'qh_batteryvoltagestart': [
                    '/Settings/Generator0/BatteryVoltage/QuietHoursStartValue',
                    11.9, 0, 100
                ],
                'qh_batteryvoltagestop': [
                    '/Settings/Generator0/BatteryVoltage/QuietHoursStopValue',
                    12.4, 0, 100
                ],
                # Current
                'batterycurrentenabled': [
                    '/Settings/Generator0/BatteryCurrent/Enabled', 0, 0, 1
                ],
                'batterycurrentstart': [
                    '/Settings/Generator0/BatteryCurrent/StartValue', 10.5,
                    0.5, 1000
                ],
                'batterycurrentstop': [
                    '/Settings/Generator0/BatteryCurrent/StopValue', 5.5, 0,
                    1000
                ],
                'batterycurrentstarttimer': [
                    '/Settings/Generator0/BatteryCurrent/StartTimer', 20, 0,
                    10000
                ],
                'batterycurrentstoptimer': [
                    '/Settings/Generator0/BatteryCurrent/StopTimer', 20, 0,
                    10000
                ],
                'qh_batterycurrentstart': [
                    '/Settings/Generator0/BatteryCurrent/QuietHoursStartValue',
                    20.5, 0, 1000
                ],
                'qh_batterycurrentstop': [
                    '/Settings/Generator0/BatteryCurrent/QuietHoursStopValue',
                    15.5, 0, 1000
                ],
                # AC load
                'acloadenabled': [
                    '/Settings/Generator0/AcLoad/Enabled', 0, 0, 1
                ],
                'acloadstart': [
                    '/Settings/Generator0/AcLoad/StartValue', 1600, 5, 100000
                ],
                'acloadstop': [
                    '/Settings/Generator0/AcLoad/StopValue', 800, 0, 100000
                ],
                'acloadstarttimer': [
                    '/Settings/Generator0/AcLoad/StartTimer', 20, 0, 10000
                ],
                'acloadstoptimer': [
                    '/Settings/Generator0/AcLoad/StopTimer', 20, 0, 10000
                ],
                'qh_acloadstart': [
                    '/Settings/Generator0/AcLoad/QuietHoursStartValue', 1900,
                    0, 100000
                ],
                'qh_acloadstop': [
                    '/Settings/Generator0/AcLoad/QuietHoursStopValue', 1200, 0,
                    100000
                ],
                # VE.Bus high temperature
                'inverterhightempenabled': [
                    '/Settings/Generator0/InverterHighTemp/Enabled', 0, 0, 1
                ],
                'inverterhightempstarttimer': [
                    '/Settings/Generator0/InverterHighTemp/StartTimer', 20, 0,
                    10000
                ],
                'inverterhightempstoptimer': [
                    '/Settings/Generator0/InverterHighTemp/StopTimer', 20, 0,
                    10000
                ],
                # VE.Bus overload
                'inverteroverloadenabled': [
                    '/Settings/Generator0/InverterOverload/Enabled', 0, 0, 1
                ],
                'inverteroverloadstarttimer': [
                    '/Settings/Generator0/InverterOverload/StartTimer', 20, 0,
                    10000
                ],
                'inverteroverloadstoptimer': [
                    '/Settings/Generator0/InverterOverload/StopTimer', 20, 0,
                    10000
                ],
                # TestRun
                'testrunenabled': [
                    '/Settings/Generator0/TestRun/Enabled', 0, 0, 1
                ],
                'testrunstartdate': [
                    '/Settings/Generator0/TestRun/StartDate',
                    time.time(), 0, 10000000000.1
                ],
                'testrunstarttimer': [
                    '/Settings/Generator0/TestRun/StartTime', 54000, 0, 86400
                ],
                'testruninterval': [
                    '/Settings/Generator0/TestRun/Interval', 28, 1, 365
                ],
                'testrunruntime': [
                    '/Settings/Generator0/TestRun/Duration', 7200, 1, 86400
                ],
                'testrunskipruntime': [
                    '/Settings/Generator0/TestRun/SkipRuntime', 0, 0, 100000
                ],
                'testruntillbatteryfull': [
                    '/Settings/Generator0/TestRun/RunTillBatteryFull', 0, 0, 1
                ]
            },
            eventCallback=self._handle_changed_setting)

        # Whenever services come or go, we need to check if it was a service we use. Note that this
        # is a bit double: DbusMonitor does the same thing. But since we don't use DbusMonitor to
        # monitor for com.victronenergy.battery, .vebus, .charger or any other possible source of
        # battery data, it is necessary to monitor for changes in the available dbus services.
        self._bus.add_signal_receiver(self._dbus_name_owner_changed,
                                      signal_name='NameOwnerChanged')

        self._evaluate_if_we_are_needed()
        gobject.timeout_add(1000, self._handletimertick)
        self._update_relay()
        self._changed = True

    def _evaluate_if_we_are_needed(self):
        if self._dbusmonitor.get_value('com.victronenergy.settings',
                                       '/Settings/Relay/Function') == 1:
            if not self._relay_state_import:
                logger.info('Getting relay from systemcalc.')
                try:
                    self._relay_state_import = VeDbusItemImport(
                        bus=self._bus,
                        serviceName='com.victronenergy.system',
                        path='/Relay/0/State',
                        eventCallback=None,
                        createsignal=True)
                except dbus.exceptions.DBusException:
                    logger.info('Systemcalc relay not available.')
                    self._relay_state_import = None
                    pass

            if self._dbusservice is None:
                logger.info(
                    'Action! Going on dbus and taking control of the relay.')

                relay_polarity_import = VeDbusItemImport(
                    bus=self._bus,
                    serviceName='com.victronenergy.settings',
                    path='/Settings/Relay/Polarity',
                    eventCallback=None,
                    createsignal=True)
                # As is not possible to keep the relay state during the CCGX power cycles,
                # set the relay polarity to normally open.
                if relay_polarity_import.get_value() == 1:
                    relay_polarity_import.set_value(0)
                    logger.info('Setting relay polarity to normally open.')

                # put ourselves on the dbus
                self._dbusservice = VeDbusService(
                    'com.victronenergy.generator.startstop0')
                self._dbusservice.add_mandatory_paths(
                    processname=__file__,
                    processversion=softwareversion,
                    connection='generator',
                    deviceinstance=0,
                    productid=None,
                    productname=None,
                    firmwareversion=None,
                    hardwareversion=None,
                    connected=1)
                # State: None = invalid, 0 = stopped, 1 = running
                self._dbusservice.add_path('/State', value=0)
                # Condition that made the generator start
                self._dbusservice.add_path('/RunningByCondition', value='')
                # Runtime
                self._dbusservice.add_path('/Runtime',
                                           value=0,
                                           gettextcallback=self._gettext)
                # Today runtime
                self._dbusservice.add_path('/TodayRuntime',
                                           value=0,
                                           gettextcallback=self._gettext)
                # Test run runtime
                self._dbusservice.add_path(
                    '/TestRunIntervalRuntime',
                    value=self._interval_runtime(
                        self._settings['testruninterval']),
                    gettextcallback=self._gettext)
                # Next tes trun date, values is 0 for test run disabled
                self._dbusservice.add_path('/NextTestRun',
                                           value=None,
                                           gettextcallback=self._gettext)
                # Next tes trun is needed 1, not needed 0
                self._dbusservice.add_path('/SkipTestRun', value=None)
                # Manual start
                self._dbusservice.add_path('/ManualStart',
                                           value=0,
                                           writeable=True)
                # Manual start timer
                self._dbusservice.add_path('/ManualStartTimer',
                                           value=0,
                                           writeable=True)
                # Silent mode active
                self._dbusservice.add_path('/QuietHours', value=0)
                self._determineservices()

        else:
            if self._dbusservice is not None:
                self._stop_generator()
                self._dbusservice.__del__()
                self._dbusservice = None
                # Reset conditions
                for condition in self._condition_stack:
                    self._reset_condition(self._condition_stack[condition])
                logger.info(
                    'Relay function is no longer set to generator start/stop: made sure generator is off '
                    + 'and now going off dbus')
                self._relay_state_import = None

    def _device_added(self, dbusservicename, instance):
        self._evaluate_if_we_are_needed()
        self._determineservices()

    def _device_removed(self, dbusservicename, instance):
        self._evaluate_if_we_are_needed()
        # Relay handling depends on systemcalc, if the service disappears restart
        # the relay state import
        if dbusservicename == "com.victronenergy.system":
            self._relay_state_import = None
        self._determineservices()

    def _dbus_value_changed(self, dbusServiceName, dbusPath, options, changes,
                            deviceInstance):
        if dbusPath == '/AutoSelectedBatteryMeasurement' and self._settings[
                'batterymeasurement'] == 'default':
            self._determineservices()
        if dbusPath == '/Settings/Relay/Function':
            self._evaluate_if_we_are_needed()
        self._changed = True
        # Update relay state when polarity is changed
        if dbusPath == '/Settings/Relay/Polarity':
            self._update_relay()

    def _handle_changed_setting(self, setting, oldvalue, newvalue):
        self._changed = True
        self._evaluate_if_we_are_needed()
        if setting == 'batterymeasurement':
            self._determineservices()
            # Reset retries and valid if service changes
            for condition in self._condition_stack:
                if self._condition_stack[condition]['monitoring'] == 'battery':
                    self._condition_stack[condition]['valid'] = True
                    self._condition_stack[condition]['retries'] = 0

        if setting == 'autostart':
            logger.info('Autostart function %s.' %
                        ('enabled' if newvalue == 1 else 'disabled'))
        if self._dbusservice is not None and setting == 'testruninterval':
            self._dbusservice[
                '/TestRunIntervalRuntime'] = self._interval_runtime(
                    self._settings['testruninterval'])

    def _dbus_name_owner_changed(self, name, oldowner, newowner):
        self._determineservices()

    def _gettext(self, path, value):
        if path == '/NextTestRun':
            # Locale format date
            d = datetime.datetime.fromtimestamp(value)
            return d.strftime('%c')
        elif path in ['/Runtime', '/TestRunIntervalRuntime', '/TodayRuntime']:
            m, s = divmod(value, 60)
            h, m = divmod(m, 60)
            return '%dh, %dm, %ds' % (h, m, s)
        else:
            return value

    def _handletimertick(self):
        # try catch, to make sure that we kill ourselves on an error. Without this try-catch, there would
        # be an error written to stdout, and then the timer would not be restarted, resulting in a dead-
        # lock waiting for manual intervention -> not good!
        try:
            if self._dbusservice is not None:
                self._evaluate_startstop_conditions()
            self._changed = False
        except:
            self._stop_generator()
            import traceback
            traceback.print_exc()
            sys.exit(1)
        return True

    def _evaluate_startstop_conditions(self):

        # Conditions will be evaluated in this order
        conditions = [
            'soc', 'acload', 'batterycurrent', 'batteryvoltage',
            'inverterhightemp', 'inverteroverload'
        ]
        start = False
        runningbycondition = None
        today = calendar.timegm(datetime.date.today().timetuple())
        self._timer_runnning = False
        values = self._get_updated_values()
        connection_lost = False

        self._check_quiet_hours()

        # New day, register it
        if self._last_counters_check < today and self._dbusservice[
                '/State'] == 0:
            self._last_counters_check = today
            self._update_accumulated_time()

        # Update current and accumulated runtime.
        if self._dbusservice['/State'] == 1:
            self._dbusservice['/Runtime'] = int(time.time() - self._starttime)
            # By performance reasons, accumulated runtime is only updated
            # once per 10s. When the generator stops is also updated.
            if self._dbusservice['/Runtime'] - self._last_runtime_update >= 10:
                self._update_accumulated_time()

        if self._evaluate_manual_start():
            runningbycondition = 'manual'
            start = True

        # Autostart conditions will only be evaluated if the autostart functionality is enabled
        if self._settings['autostart'] == 1:

            if self._evaluate_testrun_condition():
                runningbycondition = 'testrun'
                start = True

            # Evaluate value conditions
            for condition in conditions:
                start = self._evaluate_condition(
                    self._condition_stack[condition],
                    values[condition]) or start
                runningbycondition = condition if start and runningbycondition is None else runningbycondition
                # Connection lost is set to true if the numbear of retries of one or more enabled conditions
                # >= RETRIES_ON_ERROR
                if self._condition_stack[condition]['enabled']:
                    connection_lost = self._condition_stack[condition][
                        'retries'] >= self.RETRIES_ON_ERROR

            # If none condition is reached check if connection is lost and start/keep running the generator
            # depending on '/OnLossCommunication' setting
            if not start and connection_lost:
                # Start always
                if self._settings['onlosscommunication'] == 1:
                    start = True
                    runningbycondition = 'lossofcommunication'
                # Keep running if generator already started
                if self._dbusservice['/State'] == 1 and self._settings[
                        'onlosscommunication'] == 2:
                    start = True
                    runningbycondition = 'lossofcommunication'

        if start:
            self._start_generator(runningbycondition)
        elif (self._dbusservice['/Runtime'] >=
              self._settings['minimumruntime'] * 60
              or self._dbusservice['/RunningByCondition'] == 'manual'):
            self._stop_generator()

    def _reset_condition(self, condition):
        condition['reached'] = False
        if condition['timed']:
            condition['start_timer'] = 0
            condition['stop_timer'] = 0

    def _check_condition(self, condition, value):
        name = condition['name']

        if self._settings[name + 'enabled'] == 0:
            if condition['enabled']:
                condition['enabled'] = False
                logger.info('Disabling (%s) condition' % name)
                condition['retries'] = 0
                condition['valid'] = True
                self._reset_condition(condition)
            return False

        elif not condition['enabled']:
            condition['enabled'] = True
            logger.info('Enabling (%s) condition' % name)

        if (condition['monitoring']
                == 'battery') and (self._settings['batterymeasurement']
                                   == 'nobattery'):
            return False

        if value is None and condition['valid']:
            if condition['retries'] >= self.RETRIES_ON_ERROR:
                logger.info(
                    'Error getting (%s) value, skipping evaluation till get a valid value'
                    % name)
                self._reset_condition(condition)
                self._comunnication_lost = True
                condition['valid'] = False
            else:
                condition['retries'] += 1
                if condition['retries'] == 1 or (condition['retries'] %
                                                 10) == 0:
                    logger.info('Error getting (%s) value, retrying(#%i)' %
                                (name, condition['retries']))
            return False

        elif value is not None and not condition['valid']:
            logger.info('Success getting (%s) value, resuming evaluation' %
                        name)
            condition['valid'] = True
            condition['retries'] = 0

        # Reset retries if value is valid
        if value is not None:
            condition['retries'] = 0

        return condition['valid']

    def _evaluate_condition(self, condition, value):
        name = condition['name']
        setting = ('qh_'
                   if self._dbusservice['/QuietHours'] == 1 else '') + name
        startvalue = self._settings[setting +
                                    'start'] if not condition['boolean'] else 1
        stopvalue = self._settings[setting +
                                   'stop'] if not condition['boolean'] else 0

        # Check if the condition has to be evaluated
        if not self._check_condition(condition, value):
            # If generator is started by this condition and value is invalid
            # wait till RETRIES_ON_ERROR to skip the condition
            if condition['reached'] and condition[
                    'retries'] <= self.RETRIES_ON_ERROR:
                return True

            return False

        # As this is a generic evaluation method, we need to know how to compare the values
        # first check if start value should be greater than stop value and then compare
        start_is_greater = startvalue > stopvalue

        # When the condition is already reached only the stop value can set it to False
        start = condition['reached'] or (
            value >= startvalue if start_is_greater else value <= startvalue)
        stop = value <= stopvalue if start_is_greater else value >= stopvalue

        # Timed conditions must start/stop after the condition has been reached for a minimum
        # time.
        if condition['timed']:
            if not condition['reached'] and start:
                condition['start_timer'] += time.time(
                ) if condition['start_timer'] == 0 else 0
                start = time.time(
                ) - condition['start_timer'] >= self._settings[name +
                                                               'starttimer']
                condition['stop_timer'] *= int(not start)
                self._timer_runnning = True
            else:
                condition['start_timer'] = 0

            if condition['reached'] and stop:
                condition['stop_timer'] += time.time(
                ) if condition['stop_timer'] == 0 else 0
                stop = time.time() - condition['stop_timer'] >= self._settings[
                    name + 'stoptimer']
                condition['stop_timer'] *= int(not stop)
                self._timer_runnning = True
            else:
                condition['stop_timer'] = 0

        condition['reached'] = start and not stop
        return condition['reached']

    def _evaluate_manual_start(self):
        if self._dbusservice['/ManualStart'] == 0:
            if self._dbusservice['/RunningByCondition'] == 'manual':
                self._dbusservice['/ManualStartTimer'] = 0
            return False

        start = True
        # If /ManualStartTimer has a value greater than zero will use it to set a stop timer.
        # If no timer is set, the generator will not stop until the user stops it manually.
        # Once started by manual start, each evaluation the timer is decreased
        if self._dbusservice['/ManualStartTimer'] != 0:
            self._manualstarttimer += time.time(
            ) if self._manualstarttimer == 0 else 0
            self._dbusservice['/ManualStartTimer'] -= int(time.time()) - int(
                self._manualstarttimer)
            self._manualstarttimer = time.time()
            start = self._dbusservice['/ManualStartTimer'] > 0
            self._dbusservice['/ManualStart'] = int(start)
            # Reset if timer is finished
            self._manualstarttimer *= int(start)
            self._dbusservice['/ManualStartTimer'] *= int(start)

        return start

    def _evaluate_testrun_condition(self):
        if self._settings['testrunenabled'] == 0:
            self._dbusservice['/SkipTestRun'] = None
            self._dbusservice['/NextTestRun'] = None
            return False

        today = datetime.date.today()
        runtillbatteryfull = self._settings['testruntillbatteryfull'] == 1
        soc = self._get_updated_values()['soc']
        batteryisfull = runtillbatteryfull and soc == 100

        try:
            startdate = datetime.date.fromtimestamp(
                self._settings['testrunstartdate'])
            starttime = time.mktime(
                today.timetuple()) + self._settings['testrunstarttimer']
        except ValueError:
            logger.debug('Invalid dates, skipping testrun')
            return False

        # If start date is in the future set as NextTestRun and stop evaluating
        if startdate > today:
            self._dbusservice['/NextTestRun'] = time.mktime(
                startdate.timetuple())
            return False

        start = False
        # If the accumulated runtime during the tes trun interval is greater than '/TestRunIntervalRuntime'
        # the tes trun must be skipped
        needed = (self._settings['testrunskipruntime'] >
                  self._dbusservice['/TestRunIntervalRuntime']
                  or self._settings['testrunskipruntime'] == 0)
        self._dbusservice['/SkipTestRun'] = int(not needed)

        interval = self._settings['testruninterval']
        stoptime = (starttime + self._settings['testrunruntime']
                    ) if not runtillbatteryfull else (starttime + 60)
        elapseddays = (today - startdate).days
        mod = elapseddays % interval

        start = (not bool(mod) and (time.time() >= starttime)
                 and (time.time() <= stoptime))

        if runtillbatteryfull:
            if soc is not None:
                self._testrun_soc_retries = 0
                start = (start or self._dbusservice['/RunningByCondition']
                         == 'testrun') and not batteryisfull
            elif self._dbusservice['/RunningByCondition'] == 'testrun':
                if self._testrun_soc_retries < self.RETRIES_ON_ERROR:
                    self._testrun_soc_retries += 1
                    start = True
                    if (self._testrun_soc_retries % 10) == 0:
                        logger.info(
                            'Test run failed to get SOC value, retrying(#%i)' %
                            self._testrun_soc_retries)
                else:
                    logger.info(
                        'Failed to get SOC after %i retries, terminating test run condition'
                        % self._testrun_soc_retries)
                    start = False
            else:
                start = False

        if not bool(mod) and (time.time() <= stoptime):
            self._dbusservice['/NextTestRun'] = starttime
        else:
            self._dbusservice['/NextTestRun'] = (
                time.mktime(
                    (today +
                     datetime.timedelta(days=interval - mod)).timetuple()) +
                self._settings['testrunstarttimer'])
        return start and needed

    def _check_quiet_hours(self):
        active = False
        if self._settings['quiethoursenabled'] == 1:
            # Seconds after today 00:00
            timeinseconds = time.time() - time.mktime(
                datetime.date.today().timetuple())
            quiethoursstart = self._settings['quiethoursstarttime']
            quiethoursend = self._settings['quiethoursendtime']

            # Check if the current time is between the start time and end time
            if quiethoursstart < quiethoursend:
                active = quiethoursstart <= timeinseconds and timeinseconds < quiethoursend
            else:  # End time is lower than start time, example Start: 21:00, end: 08:00
                active = not (quiethoursend < timeinseconds
                              and timeinseconds < quiethoursstart)

        if self._dbusservice['/QuietHours'] == 0 and active:
            logger.info('Entering to quiet mode')

        elif self._dbusservice['/QuietHours'] == 1 and not active:
            logger.info('Leaving secondary quiet mode')

        self._dbusservice['/QuietHours'] = int(active)

        return active

    def _update_accumulated_time(self):
        seconds = self._dbusservice['/Runtime']
        accumulated = seconds - self._last_runtime_update

        self._settings['accumulatedtotal'] = int(
            self._settings['accumulatedtotal']) + accumulated
        # Using calendar to get timestamp in UTC, not local time
        today_date = str(calendar.timegm(datetime.date.today().timetuple()))

        # If something goes wrong getting the json string create a new one
        try:
            accumulated_days = json.loads(self._settings['accumulateddaily'])
        except ValueError:
            accumulated_days = {today_date: 0}

        if (today_date in accumulated_days):
            accumulated_days[today_date] += accumulated
        else:
            accumulated_days[today_date] = accumulated

        self._last_runtime_update = seconds

        # Keep the historical with a maximum of HISTORY_DAYS
        while len(accumulated_days) > self.HISTORY_DAYS:
            accumulated_days.pop(min(accumulated_days.keys()), None)

        # Upadate settings
        self._settings['accumulateddaily'] = json.dumps(accumulated_days,
                                                        sort_keys=True)
        self._dbusservice['/TodayRuntime'] = self._interval_runtime(0)
        self._dbusservice['/TestRunIntervalRuntime'] = self._interval_runtime(
            self._settings['testruninterval'])

    def _interval_runtime(self, days):
        summ = 0
        try:
            daily_record = json.loads(self._settings['accumulateddaily'])
        except ValueError:
            return 0

        for i in range(days + 1):
            previous_day = calendar.timegm(
                (datetime.date.today() -
                 datetime.timedelta(days=i)).timetuple())
            if str(previous_day) in daily_record.keys():
                summ += daily_record[str(previous_day)] if str(
                    previous_day) in daily_record.keys() else 0

        return summ

    def _get_updated_values(self):

        values = {
            'batteryvoltage':
            (self._battery_measurement_voltage_import.get_value()
             if self._battery_measurement_voltage_import else None),
            'batterycurrent':
            (self._battery_measurement_current_import.get_value()
             if self._battery_measurement_current_import else None),
            'soc':
            self._battery_measurement_soc_import.get_value()
            if self._battery_measurement_soc_import else None,
            'acload':
            self._dbusmonitor.get_value('com.victronenergy.system',
                                        '/Ac/Consumption/Total/Power'),
            'inverterhightemp':
            (self._vebusservice_high_temperature_import.get_value()
             if self._vebusservice_high_temperature_import else None),
            'inverteroverload':
            (self._vebusservice_overload_import.get_value()
             if self._vebusservice_overload_import else None)
        }

        if values['batterycurrent']:
            values['batterycurrent'] *= -1

        return values

    def _determineservices(self):
        # batterymeasurement is either 'default' or 'com_victronenergy_battery_288/Dc/0'.
        # In case it is set to default, we use the AutoSelected battery measurement, given by
        # SystemCalc.
        batterymeasurement = None
        batteryservicename = None
        newbatteryservice = None
        batteryprefix = ""
        selectedbattery = self._settings['batterymeasurement']
        vebusservice = None

        if selectedbattery == 'default':
            batterymeasurement = self._dbusmonitor.get_value(
                'com.victronenergy.system', '/AutoSelectedBatteryMeasurement')
        elif len(selectedbattery.split(
                "/", 1)) == 2:  # Only very basic sanity checking..
            batterymeasurement = self._settings['batterymeasurement']
        elif selectedbattery == 'nobattery':
            batterymeasurement = None
        else:
            # Exception: unexpected value for batterymeasurement
            pass

        if batterymeasurement:
            batteryprefix = "/" + batterymeasurement.split("/", 1)[1]

        # Get the current battery servicename
        if self._battery_measurement_voltage_import:
            oldservice = (
                self._battery_measurement_voltage_import.serviceName +
                self._battery_measurement_voltage_import.path.replace(
                    "/Voltage", ""))
        else:
            oldservice = None

        if batterymeasurement:
            try:
                batteryservicename = VeDbusItemImport(
                    bus=self._bus,
                    serviceName="com.victronenergy.system",
                    path='/ServiceMapping/' +
                    batterymeasurement.split("/", 1)[0],
                    eventCallback=None,
                    createsignal=False)

                if batteryservicename.get_value():
                    newbatteryservice = batteryservicename.get_value(
                    ) + batteryprefix
            except dbus.exceptions.DBusException:
                pass
            else:
                newbatteryservice = None

        if batteryservicename and batteryservicename.get_value():
            self._battery_measurement_available = True

            logger.info(
                'Battery service we need (%s) found! Using it for generator start/stop'
                % batterymeasurement)
            try:
                self._battery_measurement_voltage_import = VeDbusItemImport(
                    bus=self._bus,
                    serviceName=batteryservicename.get_value(),
                    path=batteryprefix + '/Voltage',
                    eventCallback=None,
                    createsignal=True)

                self._battery_measurement_current_import = VeDbusItemImport(
                    bus=self._bus,
                    serviceName=batteryservicename.get_value(),
                    path=batteryprefix + '/Current',
                    eventCallback=None,
                    createsignal=True)

                # Exception caused by Matthijs :), we forgot to batteryprefix the Soc during the big path-change...
                self._battery_measurement_soc_import = VeDbusItemImport(
                    bus=self._bus,
                    serviceName=batteryservicename.get_value(),
                    path='/Soc',
                    eventCallback=None,
                    createsignal=True)
            except Exception:
                logger.debug('Error getting battery service!')
                self._battery_measurement_voltage_import = None
                self._battery_measurement_current_import = None
                self._battery_measurement_soc_import = None

        elif selectedbattery == 'nobattery' and self._battery_measurement_available:
            logger.info(
                'Battery monitoring disabled! Stop evaluating related conditions'
            )
            self._battery_measurement_voltage_import = None
            self._battery_measurement_current_import = None
            self._battery_measurement_soc_import = None
            self._battery_measurement_available = False

        elif batteryservicename and batteryservicename.get_value(
        ) is None and self._battery_measurement_available:
            logger.info(
                'Battery service we need (%s) is not available! Stop evaluating related conditions'
                % batterymeasurement)
            self._battery_measurement_voltage_import = None
            self._battery_measurement_current_import = None
            self._battery_measurement_soc_import = None
            self._battery_measurement_available = False

        # Get the default VE.Bus service and import high temperature and overload warnings
        try:
            vebusservice = VeDbusItemImport(
                bus=self._bus,
                serviceName="com.victronenergy.system",
                path='/VebusService',
                eventCallback=None,
                createsignal=False)

            if vebusservice.get_value() and (
                    vebusservice.get_value() != self._vebusservice
                    or not self._vebusservice_available):
                self._vebusservice = vebusservice.get_value()
                self._vebusservice_available = True

                logger.info(
                    'Vebus service (%s) found! Using it for generator start/stop'
                    % vebusservice.get_value())

                self._vebusservice_high_temperature_import = VeDbusItemImport(
                    bus=self._bus,
                    serviceName=vebusservice.get_value(),
                    path='/Alarms/HighTemperature',
                    eventCallback=None,
                    createsignal=True)

                self._vebusservice_overload_import = VeDbusItemImport(
                    bus=self._bus,
                    serviceName=vebusservice.get_value(),
                    path='/Alarms/Overload',
                    eventCallback=None,
                    createsignal=True)
        except Exception:
            logger.info('Error getting Vebus service!')
            self._vebusservice_available = False
            self._vebusservice_high_temperature_import = None
            self._vebusservice_overload_import = None

            logger.info(
                'Vebus service (%s) dissapeared! Stop evaluating related conditions'
                % self._vebusservice)

        # Trigger an immediate check of system status
        self._changed = True

    def _start_generator(self, condition):
        if not self._relay_state_import:
            logger.info(
                "Relay import not available, can't start generator by %s condition"
                % condition)
            return

        systemcalc_relay_state = 0
        state = self._dbusservice['/State']

        try:
            systemcalc_relay_state = self._relay_state_import.get_value()
        except dbus.exceptions.DBusException:
            logger.info('Error getting relay state')

        # This function will start the generator in the case generator not
        # already running. When differs, the RunningByCondition is updated
        if state == 0 or systemcalc_relay_state != state:
            self._dbusservice['/State'] = 1
            self._update_relay()
            self._starttime = time.time()
            logger.info('Starting generator by %s condition' % condition)
        elif self._dbusservice['/RunningByCondition'] != condition:
            logger.info(
                'Generator previously running by %s condition is now running by %s condition'
                % (self._dbusservice['/RunningByCondition'], condition))

        self._dbusservice['/RunningByCondition'] = condition

    def _stop_generator(self):
        if not self._relay_state_import:
            logger.info("Relay import not available, can't stop generator")
            return

        systemcalc_relay_state = 1
        state = self._dbusservice['/State']

        try:
            systemcalc_relay_state = self._relay_state_import.get_value()
        except dbus.exceptions.DBusException:
            logger.info('Error getting relay state')

        if state == 1 or systemcalc_relay_state != state:
            self._dbusservice['/State'] = 0
            self._update_relay()
            logger.info('Stopping generator that was running by %s condition' %
                        str(self._dbusservice['/RunningByCondition']))
            self._dbusservice['/RunningByCondition'] = ''
            self._update_accumulated_time()
            self._starttime = 0
            self._dbusservice['/Runtime'] = 0
            self._dbusservice['/ManualStartTimer'] = 0
            self._manualstarttimer = 0
            self._last_runtime_update = 0

    def _update_relay(self):
        if not self._relay_state_import:
            logger.info("Relay import not available")
            return
        # Relay polarity 0 = NO, 1 = NC
        polarity = bool(
            self._dbusmonitor.get_value('com.victronenergy.settings',
                                        '/Settings/Relay/Polarity'))
        w = int(not polarity) if bool(
            self._dbusservice['/State']) else int(polarity)

        try:
            self._relay_state_import.set_value(dbus.Int32(w, variant_level=1))
        except dbus.exceptions.DBusException:
            logger.info('Error setting relay state')
Exemplo n.º 3
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)
Exemplo n.º 4
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')
Exemplo n.º 5
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)
Exemplo n.º 6
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)
class AcDevice(object):
	def __init__(self, position):
		# Dictionary containing the AC Sensors per phase. This is the source of the data
		self._acSensors = {'L1': [], 'L2': [], 'L3': []}

		# Type and position (numbering is equal to numbering in VE.Bus Assistant):
		self._names = {0: 'PV Inverter on input 1', 1: 'PV Inverter on output', 2: 'PV Inverter on input 2'}
		self._name = position
		self._dbusService = None

	def __str__(self):
		return self._names[self._name] + ' containing ' + \
			str(len(self._acSensors['L1'])) + ' AC-sensors on L1, ' + \
			str(len(self._acSensors['L2'])) + ' AC-sensors on L2, ' + \
			str(len(self._acSensors['L3'])) + ' AC-sensors on L3'

	# add_ac_sensor function is called to add dbusitems that represent power for a certain phase
	def add_ac_sensor(self, acsensor, phase):
		acsensor.set_eventcallback(self.value_has_changed)
		self._acSensors[phase].append(acsensor)

	def value_has_changed(self, dbusName, dbusObjectPath, changes):
		# decouple, and process update in the mainloop
		idle_add(self.update_values)

	# iterates through all sensor dbusItems, and recalculates our values. Adds objects to exported
	# dbus values if necessary.
	def update_values(self):

		if not self._dbusService:
			return

		totals = {'I': 0, 'P': 0, 'E': 0}

		for phase in ['L1', 'L2', 'L3']:
			pre = '/Ac/' + phase

			if len(self._acSensors[phase]) == 0:
				if (pre + '/Power') in self._dbusService:
					self._dbusService[pre + '/Power'] = None
					self._dbusService[pre + '/Energy/Forward'] = None
					self._dbusService[pre + '/Voltage'] = None
					self._dbusService[pre + '/Current'] = None
			else:
				phaseTotals = {'I': 0, 'P': 0, 'E': 0}
				for o in self._acSensors[phase]:
					phaseTotals['I'] += float(o['current'].get_value() or 0)
					phaseTotals['P'] += float(o['power'].get_value() or 0)
					phaseTotals['E'] += float(o['energycounter'].get_value() or 0)
					voltage = float(o['voltage'].get_value() or 0) # just take the last voltage

				if (pre + '/Power') not in self._dbusService:
					# This phase hasn't been added yet, adding it now

					self._dbusService.add_path(pre + '/Voltage', voltage, gettextcallback=self.gettextforV)
					self._dbusService.add_path(pre + '/Current', phaseTotals['I'], gettextcallback=self.gettextforA)
					self._dbusService.add_path(pre + '/Power', phaseTotals['P'], gettextcallback=self.gettextforW)
					self._dbusService.add_path(pre + '/Energy/Forward', phaseTotals['E'], gettextcallback=self.gettextforkWh)
				else:
					self._dbusService[pre + '/Voltage'] = voltage
					self._dbusService[pre + '/Current'] = phaseTotals['I']
					self._dbusService[pre + '/Power'] = phaseTotals['P']
					self._dbusService[pre + '/Energy/Forward'] = phaseTotals['E']

				totals['I'] += phaseTotals['I']
				totals['P'] += phaseTotals['P']
				totals['E'] += phaseTotals['E']

				#logging.debug(
				#	self._names[self._name] + '. Phase ' + phase + ' recalculated: %0.2fV,  %0.2fA, %0.4fW and %0.4f kWh' %
				#	(voltage,  phaseTotals['I'],  phaseTotals['P'],  phaseTotals['E']))

			# TODO, why doesn't the application crash on an exception? I want it to crash, also on exceptions
			# in threads.
			#raise Exception ("exit Exception!")

		if '/Ac/Current' not in self._dbusService:
			self._dbusService.add_path('/Ac/Current', totals['I'], gettextcallback=self.gettextforA)
			self._dbusService.add_path('/Ac/Power', totals['P'], gettextcallback=self.gettextforW)
			self._dbusService.add_path('/Ac/Energy/Forward', totals['E'], gettextcallback=self.gettextforkWh)
		else:
			self._dbusService['/Ac/Current'] = totals['I']
			self._dbusService['/Ac/Power'] =  totals['P']
			self._dbusService['/Ac/Energy/Forward'] = totals['E']

	# Call this function after you have added AC sensors to this class. Code will check if we have any,
	# and if yes, add ourselves to the dbus.
	def update_dbus_service(self):
		if (len(self._acSensors['L1']) > 0 or len(self._acSensors['L2']) > 0 or
			len(self._acSensors['L3']) > 0):

			if self._dbusService is None:

				pf = {0: 'input1', 1: 'output', 2: 'input2'}
				self._dbusService = VeDbusService('com.victronenergy.pvinverter.vebusacsensor_' + pf[self._name])
				#, self._dbusConn)

				self._dbusService.add_path('/Position', self._name, description=None, gettextcallback=self.gettextforposition)

				# Create the mandatory objects, as per victron dbus api document
				self._dbusService.add_path('/Mgmt/ProcessName', __file__)
				self._dbusService.add_path('/Mgmt/ProcessVersion', softwareVersion)
				self._dbusService.add_path('/Mgmt/Connection', 'AC Sensor on VE.Bus device')
				self._dbusService.add_path('/DeviceInstance', int(self._name) + 10)
				self._dbusService.add_path('/ProductId', 0xA141)
				self._dbusService.add_path('/ProductName', self._names[self._name])
				self._dbusService.add_path('/Connected', 1)

				logging.info('Added to D-Bus: ' + self.__str__())

			self.update_values()

	# Apparantly some service from which we imported AC Sensors has gone offline. Remove those sensors
	# from our repo.
	def remove_ac_sensors_imported_from(self, serviceBeingRemoved):
		logging.debug(
			'%s: Checking if we have sensors from %s, and removing them' %
			(self._names[self._name], serviceBeingRemoved))

		for phase in ['L1', 'L2', 'L3']:
			self._acSensors[phase][:] = [x for x in self._acSensors[phase] if not x['power'].serviceName == serviceBeingRemoved]

		if self._dbusService is None:
			return

		if (not self._acSensors['L1'] and not self._acSensors['L2'] and
			not self._acSensors['L3']):
			# No sensors left for us, clean up

			self._dbusService.__del__()  # explicitly call __del__(), instead of waiting for gc
			self._dbusService = None

			logging.info("Removed from D-Bus: %s" % self.__str__())
		else:
			# Still some sensors left for us, update values
			self.update_values()

	def gettextforkWh(self, path, value):
		return ("%.3FkWh" % (float(value) / 1000.0))

	def gettextforW(self, path, value):
		return ("%.0FW" % (float(value)))

	def gettextforV(self, path, value):
		return ("%.0FV" % (float(value)))

	def gettextforA(self, path, value):
		return ("%.0FA" % (float(value)))

	def gettextforposition(self, path, value):
		return self._names[value]