Esempio n. 1
0
    def __init__(self, configuration_controller=INJECTED):
        config = ConfigParser()
        config.read(constants.get_config_file())

        self._message_client = MessageClient('vpn_service')
        self._message_client.add_event_handler(self._event_receiver)
        self._message_client.set_state_handler(self._check_state)

        self._iterations = 0
        self._last_cycle = 0
        self._cloud_enabled = True
        self._sleep_time = 0
        self._previous_sleep_time = 0
        self._vpn_open = False
        self._debug_data = {}
        self._eeprom_events = deque()
        self._gateway = Gateway()
        self._vpn_controller = VpnController()
        self._config_controller = configuration_controller
        self._cloud = Cloud(config.get('OpenMotics', 'vpn_check_url') % config.get('OpenMotics', 'uuid'),
                            self._message_client,
                            self._config_controller)

        self._collectors = {'thermostats': DataCollector(self._gateway.get_thermostats, 60),
                            'inputs': DataCollector(self._gateway.get_inputs_status),
                            'outputs': DataCollector(self._gateway.get_enabled_outputs),
                            'pulses': DataCollector(self._gateway.get_pulse_counter_diff, 60),
                            'power': DataCollector(self._gateway.get_real_time_power),
                            'errors': DataCollector(self._gateway.get_errors, 600),
                            'local_ip': DataCollector(self._gateway.get_local_ip_address, 1800)}
Esempio n. 2
0
def setup_minimal_vpn_platform(message_client_name):
    # type: (str) -> None
    # IPC
    message_client = None
    if message_client_name is not None:
        message_client = MessageClient(message_client_name)
    Injectable.value(message_client=message_client)
Esempio n. 3
0
    def __init__(self, i2c_device, i2c_address, input_button):
        self._i2c_device = i2c_device
        self._i2c_address = i2c_address
        self._input_button = input_button
        self._input_button_pressed_since = None
        self._input_button_released = True
        self._ticks = 0

        self._network_enabled = False
        self._network_activity = False
        self._network_bytes = 0

        self._serial_activity = {4: False, 5: False}
        self._enabled_leds = {}
        self._previous_leds = {}
        self._last_i2c_led_code = 0

        self._indicate_started = 0
        self._indicate_pointer = 0
        self._indicate_sequence = [True, False, False, False]

        self._authorized_mode = False
        self._authorized_timeout = 0

        self._check_states_thread = None
        self._leds_thread = None
        self._button_thread = None

        self._last_run_i2c = 0
        self._last_run_gpio = 0
        self._last_state_check = 0
        self._last_button_check = 0
        self._running = False

        self._message_client = MessageClient('led_service')
        self._message_client.add_event_handler(self.event_receiver)
        self._message_client.set_state_handler(self.get_state)

        self._gpio_led_config = Hardware.get_gpio_led_config()
        self._i2c_led_config = Hardware.get_i2c_led_config()
        for led in self._gpio_led_config.keys() + self._i2c_led_config.keys():
            self._enabled_leds[led] = False
            self._write_leds()
Esempio n. 4
0
class LedController(object):
    """
    The LEDController contains all logic to control the leds, and read out the physical buttons
    """
    def __init__(self, i2c_device, i2c_address, input_button):
        self._i2c_device = i2c_device
        self._i2c_address = i2c_address
        self._input_button = input_button
        self._input_button_pressed_since = None
        self._input_button_released = True
        self._ticks = 0

        self._network_enabled = False
        self._network_activity = False
        self._network_bytes = 0

        self._serial_activity = {4: False, 5: False}
        self._enabled_leds = {}
        self._previous_leds = {}
        self._last_i2c_led_code = 0

        self._indicate_started = 0
        self._indicate_pointer = 0
        self._indicate_sequence = [True, False, False, False]

        self._authorized_mode = False
        self._authorized_timeout = 0

        self._check_states_thread = None
        self._leds_thread = None
        self._button_thread = None

        self._last_run_i2c = 0
        self._last_run_gpio = 0
        self._last_state_check = 0
        self._last_button_check = 0
        self._running = False

        self._message_client = MessageClient('led_service')
        self._message_client.add_event_handler(self.event_receiver)
        self._message_client.set_state_handler(self.get_state)

        self._gpio_led_config = Hardware.get_gpio_led_config()
        self._i2c_led_config = Hardware.get_i2c_led_config()
        for led in self._gpio_led_config.keys() + self._i2c_led_config.keys():
            self._enabled_leds[led] = False
            self._write_leds()

    def start(self):
        """ Start the leds and buttons thread. """
        self._running = True
        self._check_states_thread = Thread(target=self._check_states)
        self._check_states_thread.daemon = True
        self._check_states_thread.start()

        self._leds_thread = Thread(target=self.drive_leds)
        self._leds_thread.daemon = True
        self._leds_thread.start()

        self._button_thread = Thread(target=self.check_button)
        self._button_thread.daemon = True
        self._button_thread.start()

    def stop(self):
        self._running = False

    def set_led(self, led_name, enable):
        """ Set the state of a LED, enabled means LED on in this context. """
        self._enabled_leds[led_name] = bool(enable)

    def toggle_led(self, led_name):
        """ Toggle the state of a LED. """
        self._enabled_leds[led_name] = not self._enabled_leds.get(
            led_name, False)

    def serial_activity(self, port):
        """ Report serial activity on the given serial port. Port is 4 or 5. """
        self._serial_activity[port] = True

    @staticmethod
    def _is_button_pressed(gpio_pin):
        """ Read the input button: returns True if the button is pressed, False if not. """
        with open('/sys/class/gpio/gpio{0}/value'.format(gpio_pin),
                  'r') as fh_inp:
            line = fh_inp.read()
        return int(line) == 0

    def _write_leds(self):
        """ Set the LEDs using the current status. """
        try:
            # Get i2c code
            code = 0
            for led in self._i2c_led_config:
                if self._enabled_leds.get(led, False) is True:
                    code |= self._i2c_led_config[led]
            if self._authorized_mode:
                # Light all leds in authorized mode
                for led in AUTH_MODE_LEDS:
                    code |= self._i2c_led_config.get(led, 0)
            code = (~code) & 255

            # Push code if needed
            if code != self._last_i2c_led_code:
                self._last_i2c_led_code = code
                with open(self._i2c_device, 'r+', 1) as i2c:
                    fcntl.ioctl(i2c, Hardware.IOCTL_I2C_SLAVE,
                                self._i2c_address)
                    i2c.write(chr(code))
                    self._last_run_i2c = time.time()
            else:
                self._last_run_i2c = time.time()
        except Exception as exception:
            logger.error('Error while writing to i2c: {0}'.format(exception))

        for led in self._gpio_led_config:
            on = self._enabled_leds.get(led, False)
            if self._previous_leds.get(led) != on:
                self._previous_leds[led] = on
                try:
                    gpio = self._gpio_led_config[led]
                    with open('/sys/class/gpio/gpio{0}/value'.format(gpio),
                              'w') as fh_s:
                        fh_s.write('1' if on else '0')
                        self._last_run_gpio = time.time()
                except IOError:
                    pass  # The GPIO doesn't exist or is read only
            else:
                self._last_run_gpio = time.time()

    def _check_states(self):
        """ Checks various states of the system (network) """
        while self._running:
            try:
                with open('/sys/class/net/eth0/carrier', 'r') as fh_up:
                    line = fh_up.read()
                self._network_enabled = int(line) == 1

                with open('/proc/net/dev', 'r') as fh_stat:
                    for line in fh_stat.readlines():
                        if 'eth0' in line:
                            received, transmitted = 0, 0
                            parts = line.split()
                            if len(parts) == 17:
                                received = parts[1]
                                transmitted = parts[9]
                            elif len(parts) == 16:
                                (_, received) = tuple(parts[0].split(':'))
                                transmitted = parts[8]
                            new_bytes = received + transmitted
                            if self._network_bytes != new_bytes:
                                self._network_bytes = new_bytes
                                self._network_activity = True
                            else:
                                self._network_activity = False
            except Exception as exception:
                logger.error(
                    'Error while checking states: {0}'.format(exception))
            self._last_state_check = time.time()
            time.sleep(0.5)

    def drive_leds(self):
        """ This drives different leds (status, alive and serial) """
        while self._running:
            start = time.time()
            try:
                now = time.time()
                if now - 30 < self._indicate_started < now:
                    self.set_led(
                        Hardware.Led.STATUS,
                        self._indicate_sequence[self._indicate_pointer])
                    self._indicate_pointer = self._indicate_pointer + 1 if self._indicate_pointer < len(
                        self._indicate_sequence) - 1 else 0
                else:
                    self.set_led(Hardware.Led.STATUS,
                                 not self._network_enabled)
                if self._network_activity:
                    self.toggle_led(Hardware.Led.ALIVE)
                else:
                    self.set_led(Hardware.Led.ALIVE, False)
                # Calculate serial led states
                comm_map = {4: Hardware.Led.COMM_1, 5: Hardware.Led.COMM_2}
                for uart in [4, 5]:
                    if self._serial_activity[uart]:
                        self.toggle_led(comm_map[uart])
                    else:
                        self.set_led(comm_map[uart], False)
                    self._serial_activity[uart] = False
                # Update all leds
                self._write_leds()
            except Exception as exception:
                logger.error('Error while driving leds: {0}'.format(exception))
            duration = time.time() - start
            time.sleep(max(0.05, 0.25 - duration))

    def check_button(self):
        """ Handles input button presses """
        while self._running:
            try:
                button_pressed = LedController._is_button_pressed(
                    self._input_button)
                if button_pressed is False:
                    self._input_button_released = True
                if self._authorized_mode:
                    if time.time() > self._authorized_timeout or (
                            button_pressed and self._input_button_released):
                        self._authorized_mode = False
                else:
                    if button_pressed:
                        self._ticks += 0.25
                        self._input_button_released = False
                        if self._input_button_pressed_since is None:
                            self._input_button_pressed_since = time.time()
                        if self._ticks > 5.75:  # After 5.75 seconds + time to execute the code it should be pressed between 5.8 and 6.5 seconds.
                            self._authorized_mode = True
                            self._authorized_timeout = time.time() + 60
                            self._input_button_pressed_since = None
                            self._ticks = 0
                    else:
                        self._input_button_pressed_since = None
            except Exception as exception:
                logger.error(
                    'Error while checking button: {0}'.format(exception))
            self._last_button_check = time.time()
            time.sleep(0.25)

    def event_receiver(self, event, payload):
        if event == OMBusEvents.CLOUD_REACHABLE:
            self.set_led(Hardware.Led.CLOUD, payload)
        elif event == OMBusEvents.VPN_OPEN:
            self.set_led(Hardware.Led.VPN, payload)
        elif event == OMBusEvents.SERIAL_ACTIVITY:
            self.serial_activity(payload)
        elif event == OMBusEvents.INDICATE_GATEWAY:
            self._indicate_started = time.time()

    def get_state(self):
        authorized_mode = self._authorized_mode
        if Platform.get_platform() == Platform.Type.CORE_PLUS:
            authorized_mode = True  # TODO: Should be handled by actual button

        return {
            'run_gpio': self._last_run_gpio,
            'run_i2c': self._last_run_i2c,
            'run_buttons': self._last_button_check,
            'run_state_check': self._last_state_check,
            'authorized_mode': authorized_mode
        }
Esempio n. 5
0
def setup_target_platform(target_platform, message_client_name):
    # type: (str, Optional[str]) -> None
    config = ConfigParser()
    config.read(constants.get_config_file())

    config_lock = Lock()
    metrics_lock = Lock()

    config_database_file = constants.get_config_database_file()

    # Debugging options
    try:
        debug_logger = config.get('OpenMotics', 'debug_logger')
        if debug_logger:
            logging.getLogger(debug_logger).setLevel(logging.DEBUG)
    except NoOptionError:
        pass

    # Webserver / Presentation layer
    try:
        https_port = int(config.get('OpenMotics', 'https_port'))
    except NoOptionError:
        https_port = 443
    try:
        http_port = int(config.get('OpenMotics', 'http_port'))
    except NoOptionError:
        http_port = 80
    Injectable.value(https_port=https_port)
    Injectable.value(http_port=http_port)
    Injectable.value(ssl_private_key=constants.get_ssl_private_key_file())
    Injectable.value(ssl_certificate=constants.get_ssl_certificate_file())

    # TODO: Clean up dependencies more to reduce complexity

    # IOC announcements
    # When below modules are imported, the classes are registerd in the IOC graph. This is required for
    # instances that are used in @Inject decorated functions below, and is also needed to specify
    # abstract implementations depending on e.g. the platform (classic vs core) or certain settings (classic
    # thermostats vs gateway thermostats)
    from plugins import base
    from gateway import (metrics_controller, webservice, scheduling, observer,
                         gateway_api, metrics_collector,
                         maintenance_controller, user_controller,
                         pulse_counter_controller, metrics_caching, watchdog,
                         output_controller, room_controller, sensor_controller,
                         shutter_controller, group_action_controller,
                         module_controller, ventilation_controller)
    from cloud import events
    _ = (metrics_controller, webservice, scheduling, observer, gateway_api,
         metrics_collector, maintenance_controller, base, events,
         user_controller, pulse_counter_controller, metrics_caching, watchdog,
         output_controller, room_controller, sensor_controller,
         shutter_controller, group_action_controller, module_controller,
         ventilation_controller)

    # IPC
    message_client = None
    if message_client_name is not None:
        message_client = MessageClient(message_client_name)
    Injectable.value(message_client=message_client)

    # Cloud API
    Injectable.value(gateway_uuid=config.get('OpenMotics', 'uuid'))

    try:
        parsed_url = urlparse(config.get('OpenMotics', 'vpn_check_url'))
    except NoOptionError:
        parsed_url = urlparse('')
    Injectable.value(cloud_endpoint=parsed_url.hostname)
    Injectable.value(cloud_port=parsed_url.port)
    Injectable.value(cloud_ssl=parsed_url.scheme == 'https')
    Injectable.value(cloud_api_version=0)

    cloud_url = urlunparse(
        (parsed_url.scheme, parsed_url.netloc, '', '', '', ''))
    Injectable.value(cloud_url=cloud_url or None)

    try:
        firmware_url = config.get('OpenMotics', 'firmware_url')
    except NoOptionError:
        path = '/portal/firmware_metadata'
        firmware_url = urlunparse(
            (parsed_url.scheme, parsed_url.netloc, path, '', '', ''))
    Injectable.value(firmware_url=firmware_url or None)

    # User Controller
    Injectable.value(user_db=config_database_file)
    Injectable.value(user_db_lock=config_lock)
    Injectable.value(token_timeout=3600)
    Injectable.value(
        config={
            'username': config.get('OpenMotics', 'cloud_user'),
            'password': config.get('OpenMotics', 'cloud_pass')
        })

    # Metrics Controller
    Injectable.value(metrics_db=constants.get_metrics_database_file())
    Injectable.value(metrics_db_lock=metrics_lock)

    # Energy Controller
    try:
        power_serial_port = config.get('OpenMotics', 'power_serial')
    except NoOptionError:
        power_serial_port = ''
    if power_serial_port:
        Injectable.value(power_db=constants.get_power_database_file())
        Injectable.value(power_store=PowerStore())
        # TODO: make non blocking?
        Injectable.value(power_serial=RS485(
            Serial(power_serial_port, 115200, timeout=None)))
        Injectable.value(power_communicator=PowerCommunicator())
        Injectable.value(power_controller=PowerController())
        Injectable.value(p1_controller=P1Controller())
    else:
        Injectable.value(power_serial=None)
        Injectable.value(power_store=None)
        Injectable.value(
            power_communicator=None)  # TODO: remove from gateway_api
        Injectable.value(power_controller=None)
        Injectable.value(p1_controller=None)

    # Pulse Controller
    Injectable.value(pulse_db=constants.get_pulse_counter_database_file())

    # Master Controller
    try:
        controller_serial_port = config.get('OpenMotics', 'controller_serial')
    except NoOptionError:
        controller_serial_port = ''

    if controller_serial_port:
        Injectable.value(controller_serial=Serial(
            controller_serial_port, 115200, exclusive=True))
    if target_platform in [Platform.Type.DUMMY, Platform.Type.ESAFE]:
        Injectable.value(maintenance_communicator=None)
        Injectable.value(passthrough_service=None)
        Injectable.value(master_controller=MasterDummyController())
        Injectable.value(eeprom_db=None)
        from gateway.hal.master_controller_dummy import DummyEepromObject
        Injectable.value(eeprom_extension=DummyEepromObject())
    elif target_platform in Platform.CoreTypes:
        # FIXME don't create singleton for optional controller?
        from master.core import ucan_communicator, slave_communicator
        _ = ucan_communicator, slave_communicator
        core_cli_serial_port = config.get('OpenMotics', 'cli_serial')
        Injectable.value(cli_serial=Serial(core_cli_serial_port, 115200))
        Injectable.value(passthrough_service=None)  # Mark as "not needed"
        # TODO: Remove; should not be needed for Core
        Injectable.value(
            eeprom_db=constants.get_eeprom_extension_database_file())

        Injectable.value(master_communicator=CoreCommunicator())
        Injectable.value(
            maintenance_communicator=MaintenanceCoreCommunicator())
        Injectable.value(memory_file=MemoryFile())
        Injectable.value(master_controller=MasterCoreController())
    elif target_platform in Platform.ClassicTypes:
        # FIXME don't create singleton for optional controller?
        from master.classic import eeprom_extension
        _ = eeprom_extension
        leds_i2c_address = config.get('OpenMotics', 'leds_i2c_address')
        passthrough_serial_port = config.get('OpenMotics',
                                             'passthrough_serial')
        Injectable.value(
            eeprom_db=constants.get_eeprom_extension_database_file())
        Injectable.value(leds_i2c_address=int(leds_i2c_address, 16))
        if passthrough_serial_port:
            Injectable.value(
                passthrough_serial=Serial(passthrough_serial_port, 115200))
            from master.classic.passthrough import PassthroughService
            _ = PassthroughService  # IOC announcement
        else:
            Injectable.value(passthrough_service=None)
        Injectable.value(master_communicator=MasterCommunicator())
        Injectable.value(
            maintenance_communicator=MaintenanceClassicCommunicator())
        Injectable.value(master_controller=MasterClassicController())
    else:
        logger.warning('Unhandled master implementation for %s',
                       target_platform)

    if target_platform in [Platform.Type.DUMMY, Platform.Type.ESAFE]:
        Injectable.value(frontpanel_controller=None)
    elif target_platform in Platform.CoreTypes:
        Injectable.value(frontpanel_controller=FrontpanelCoreController())
    elif target_platform in Platform.ClassicTypes:
        Injectable.value(frontpanel_controller=FrontpanelClassicController())
    else:
        logger.warning('Unhandled frontpanel implementation for %s',
                       target_platform)

    # Thermostats
    thermostats_gateway_feature = Feature.get_or_none(
        name='thermostats_gateway')
    thermostats_gateway_enabled = thermostats_gateway_feature is not None and thermostats_gateway_feature.enabled
    if target_platform not in Platform.ClassicTypes or thermostats_gateway_enabled:
        Injectable.value(thermostat_controller=ThermostatControllerGateway())
    else:
        Injectable.value(thermostat_controller=ThermostatControllerMaster())
Esempio n. 6
0
    def build_graph():
        config = ConfigParser()
        config.read(constants.get_config_file())

        config_lock = Lock()
        scheduling_lock = Lock()
        metrics_lock = Lock()

        config_database_file = constants.get_config_database_file()

        # TODO: Clean up dependencies more to reduce complexity

        # IOC announcements
        # When below modules are imported, the classes are registerd in the IOC graph. This is required for
        # instances that are used in @Inject decorated functions below, and is also needed to specify
        # abstract implementations depending on e.g. the platform (classic vs core) or certain settings (classic
        # thermostats vs gateway thermostats)
        from power import power_communicator, power_controller
        from plugins import base
        from gateway import (metrics_controller, webservice, scheduling,
                             observer, gateway_api, metrics_collector,
                             maintenance_controller, comm_led_controller,
                             users, pulses, config as config_controller,
                             metrics_caching, watchdog)
        from cloud import events
        _ = (metrics_controller, webservice, scheduling, observer, gateway_api,
             metrics_collector, maintenance_controller, base, events,
             power_communicator, comm_led_controller, users, power_controller,
             pulses, config_controller, metrics_caching, watchdog)
        if Platform.get_platform() == Platform.Type.CORE_PLUS:
            from gateway.hal import master_controller_core
            from master_core import maintenance, core_communicator, ucan_communicator
            from master import eeprom_extension  # TODO: Obsolete, need to be removed
            _ = master_controller_core, maintenance, core_communicator, ucan_communicator
        else:
            from gateway.hal import master_controller_classic
            from master import maintenance, master_communicator, eeprom_extension
            _ = master_controller_classic, maintenance, master_communicator, eeprom_extension

        thermostats_gateway_feature = Feature.get_or_none(
            name='thermostats_gateway')
        thermostats_gateway_enabled = thermostats_gateway_feature is not None and thermostats_gateway_feature.enabled
        if Platform.get_platform(
        ) == Platform.Type.CORE_PLUS or thermostats_gateway_enabled:
            from gateway.thermostat.gateway import thermostat_controller_gateway
            _ = thermostat_controller_gateway
        else:
            from gateway.thermostat.master import thermostat_controller_master
            _ = thermostat_controller_master

        # IPC
        Injectable.value(message_client=MessageClient('openmotics_service'))

        # Cloud API
        parsed_url = urlparse(config.get('OpenMotics', 'vpn_check_url'))
        Injectable.value(gateway_uuid=config.get('OpenMotics', 'uuid'))
        Injectable.value(cloud_endpoint=parsed_url.hostname)
        Injectable.value(cloud_port=parsed_url.port)
        Injectable.value(cloud_ssl=parsed_url.scheme == 'https')
        Injectable.value(cloud_api_version=0)

        # User Controller
        Injectable.value(user_db=config_database_file)
        Injectable.value(user_db_lock=config_lock)
        Injectable.value(token_timeout=3600)
        Injectable.value(
            config={
                'username': config.get('OpenMotics', 'cloud_user'),
                'password': config.get('OpenMotics', 'cloud_pass')
            })

        # Configuration Controller
        Injectable.value(config_db=config_database_file)
        Injectable.value(config_db_lock=config_lock)

        # Energy Controller
        power_serial_port = config.get('OpenMotics', 'power_serial')
        Injectable.value(power_db=constants.get_power_database_file())
        if power_serial_port:
            Injectable.value(power_serial=RS485(
                Serial(power_serial_port, 115200, timeout=None)))
        else:
            Injectable.value(power_serial=None)
            Injectable.value(power_communicator=None)
            Injectable.value(power_controller=None)

        # Pulse Controller
        Injectable.value(pulse_db=constants.get_pulse_counter_database_file())

        # Scheduling Controller
        Injectable.value(
            scheduling_db=constants.get_scheduling_database_file())
        Injectable.value(scheduling_db_lock=scheduling_lock)

        # Master Controller
        controller_serial_port = config.get('OpenMotics', 'controller_serial')
        Injectable.value(
            controller_serial=Serial(controller_serial_port, 115200))
        if Platform.get_platform() == Platform.Type.CORE_PLUS:
            from master_core.memory_file import MemoryFile, MemoryTypes
            core_cli_serial_port = config.get('OpenMotics', 'cli_serial')
            Injectable.value(cli_serial=Serial(core_cli_serial_port, 115200))
            Injectable.value(passthrough_service=None)  # Mark as "not needed"
            Injectable.value(
                memory_files={
                    MemoryTypes.EEPROM: MemoryFile(MemoryTypes.EEPROM),
                    MemoryTypes.FRAM: MemoryFile(MemoryTypes.FRAM)
                })
            # TODO: Remove; should not be needed for Core
            Injectable.value(
                eeprom_db=constants.get_eeprom_extension_database_file())
        else:
            passthrough_serial_port = config.get('OpenMotics',
                                                 'passthrough_serial')
            Injectable.value(
                eeprom_db=constants.get_eeprom_extension_database_file())
            if passthrough_serial_port:
                Injectable.value(
                    passthrough_serial=Serial(passthrough_serial_port, 115200))
                from master.passthrough import PassthroughService
                _ = PassthroughService  # IOC announcement
            else:
                Injectable.value(passthrough_service=None)

        # Metrics Controller
        Injectable.value(metrics_db=constants.get_metrics_database_file())
        Injectable.value(metrics_db_lock=metrics_lock)

        # Webserver / Presentation layer
        Injectable.value(ssl_private_key=constants.get_ssl_private_key_file())
        Injectable.value(ssl_certificate=constants.get_ssl_certificate_file())
Esempio n. 7
0
class VPNService(object):
    """ The VPNService contains all logic to be able to send the heartbeat and check whether the VPN should be opened """

    @Inject
    def __init__(self, configuration_controller=INJECTED):
        config = ConfigParser()
        config.read(constants.get_config_file())

        self._message_client = MessageClient('vpn_service')
        self._message_client.add_event_handler(self._event_receiver)
        self._message_client.set_state_handler(self._check_state)

        self._iterations = 0
        self._last_cycle = 0
        self._cloud_enabled = True
        self._sleep_time = 0
        self._previous_sleep_time = 0
        self._vpn_open = False
        self._debug_data = {}
        self._eeprom_events = deque()
        self._gateway = Gateway()
        self._vpn_controller = VpnController()
        self._config_controller = configuration_controller
        self._cloud = Cloud(config.get('OpenMotics', 'vpn_check_url') % config.get('OpenMotics', 'uuid'),
                            self._message_client,
                            self._config_controller)

        self._collectors = {'thermostats': DataCollector(self._gateway.get_thermostats, 60),
                            'inputs': DataCollector(self._gateway.get_inputs_status),
                            'outputs': DataCollector(self._gateway.get_enabled_outputs),
                            'pulses': DataCollector(self._gateway.get_pulse_counter_diff, 60),
                            'power': DataCollector(self._gateway.get_real_time_power),
                            'errors': DataCollector(self._gateway.get_errors, 600),
                            'local_ip': DataCollector(self._gateway.get_local_ip_address, 1800)}

    @staticmethod
    def ping(target, verbose=True):
        """ Check if the target can be pinged. Returns True if at least 1/4 pings was successful. """
        if target is None:
            return False

        # The popen_timeout has been added as a workaround for the hanging subprocess
        # If NTP date changes the time during a execution of a sub process this hangs forever.
        def popen_timeout(command, timeout):
            p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            for _ in xrange(timeout):
                time.sleep(1)
                if p.poll() is not None:
                    stdout_data, stderr_data = p.communicate()
                    if p.returncode == 0:
                        return True
                    raise Exception('Non-zero exit code. Stdout: {0}, stderr: {1}'.format(stdout_data, stderr_data))
            logger.warning('Got timeout during ping')
            p.kill()
            return False

        if verbose is True:
            logger.info("Testing ping to {0}".format(target))
        try:
            # Ping returns status code 0 if at least 1 ping is successful
            return popen_timeout(["ping", "-c", "3", target], 10)
        except Exception as ex:
            logger.error("Error during ping: {0}".format(ex))
            return False

    @staticmethod
    def has_connectivity():
        # Check connectivity by using ping to recover from a messed up network stack on the BeagleBone
        # Prefer using OpenMotics infrastructure first

        if VPNService.ping('cloud.openmotics.com'):
            # OpenMotics infrastructure can be pinged
            # > Connectivity
            return True
        can_ping_internet_by_fqdn = VPNService.ping('example.com') or VPNService.ping('google.com')
        if can_ping_internet_by_fqdn:
            # Public internet servers can be pinged by FQDN
            # > Assume maintenance on OpenMotics infrastructure. Sufficient connectivity
            return True
        can_ping_internet_by_ip = VPNService.ping('8.8.8.8') or VPNService.ping('1.1.1.1')
        if can_ping_internet_by_ip:
            # Public internet servers can be pinged by IP, but not by FQDN
            # > Assume DNS resolving issues. Insufficient connectivity
            return False
        # Public internet servers cannot be pinged by IP, nor by FQDN
        can_ping_default_gateway = VPNService.ping(VPNService._get_gateway())
        if can_ping_default_gateway:
            # > Assume ISP outage. Sufficient connectivity
            return True
        # > Assume broken TCP stack. No connectivity
        return False

    def _get_debug_dumps(self):
        if not self._config_controller.get_setting('cloud_support', False):
            return {}
        found_timestamps = []
        for filename in glob.glob('/tmp/debug_*.json'):
            timestamp = int(filename.replace('/tmp/debug_', '').replace('.json', ''))
            if timestamp not in self._debug_data:
                with open(filename, 'r') as debug_file:
                    self._debug_data[timestamp] = json.load(debug_file)
            found_timestamps.append(timestamp)
        for timestamp in self._debug_data:
            if timestamp not in found_timestamps:
                del self._debug_data[timestamp]
        return self._debug_data

    def _clean_debug_dumps(self):
        for timestamp in self._debug_data:
            filename = '/tmp/debug_{0}.json'.format(timestamp)
            try:
                os.remove(filename)
            except Exception as ex:
                logger.error('Could not remove debug file {0}: {1}'.format(filename, ex))

    @staticmethod
    def _get_gateway():
        """ Get the default gateway. """
        try:
            return subprocess.check_output("ip r | grep '^default via' | awk '{ print $3; }'", shell=True)
        except Exception as ex:
            logger.error("Error during get_gateway: {0}".format(ex))
            return

    def _check_state(self):
        return {'cloud_disabled': not self._cloud_enabled,
                'sleep_time': self._sleep_time,
                'cloud_last_connect': None if self._cloud is None else self._cloud.get_last_connect(),
                'vpn_open': self._vpn_open,
                'last_cycle': self._last_cycle}

    def _event_receiver(self, event, payload):
        _ = payload
        if event == OMBusEvents.DIRTY_EEPROM:
            self._eeprom_events.appendleft(True)

    @staticmethod
    def _unload_queue(queue):
        events = []
        try:
            while True:
                events.append(queue.pop())
        except IndexError:
            pass
        return events

    def _set_vpn(self, should_open):
        is_running = VpnController.check_vpn()
        if should_open and not is_running:
            logger.info("opening vpn")
            VpnController.start_vpn()
        elif not should_open and is_running:
            logger.info("closing vpn")
            VpnController.stop_vpn()
        is_running = VpnController.check_vpn()
        self._vpn_open = is_running and self._vpn_controller.vpn_connected
        self._message_client.send_event(OMBusEvents.VPN_OPEN, self._vpn_open)

    def start(self):
        self._check_vpn()

    def _check_vpn(self):
        while True:
            self._last_cycle = time.time()
            try:
                start_time = time.time()

                # Check whether connection to the Cloud is enabled/disabled
                cloud_enabled = self._config_controller.get_setting('cloud_enabled')
                if cloud_enabled is False:
                    self._sleep_time = None
                    self._set_vpn(False)
                    self._message_client.send_event(OMBusEvents.VPN_OPEN, False)
                    self._message_client.send_event(OMBusEvents.CLOUD_REACHABLE, False)

                    time.sleep(DEFAULT_SLEEP_TIME)
                    continue

                call_data = {'events': {}}

                # Events  # TODO: Replace this by websocket events in the future
                dirty_events = VPNService._unload_queue(self._eeprom_events)
                if dirty_events:
                    call_data['events']['DIRTY_EEPROM'] = True

                # Collect data to be send to the Cloud
                for collector_name in self._collectors:
                    collector = self._collectors[collector_name]
                    data = collector.collect()
                    if data is not None:
                        call_data[collector_name] = data
                call_data['debug'] = {'dumps': self._get_debug_dumps()}

                # Send data to the cloud and see if the VPN should be opened
                feedback = self._cloud.call_home(call_data)

                if feedback['success']:
                    self._clean_debug_dumps()

                if self._iterations > 20 and self._cloud.get_last_connect() < time.time() - REBOOT_TIMEOUT:
                    # We can't connect for over `REBOOT_TIMEOUT` seconds and we tried for at least 20 times.
                    # Try to figure out whether the network stack works as expected
                    if not VPNService.has_connectivity():
                        reboot_gateway()
                self._iterations += 1
                # Open or close the VPN
                self._set_vpn(feedback['open_vpn'])

                # Getting some sleep
                exec_time = time.time() - start_time
                if exec_time > 2:
                    logger.warning('Heartbeat took more than 2s to complete: {0:.2f}s'.format(exec_time))
                sleep_time = self._cloud.get_sleep_time()
                if self._previous_sleep_time != sleep_time:
                    logger.info('Set sleep interval to {0}s'.format(sleep_time))
                    self._previous_sleep_time = sleep_time
                time.sleep(sleep_time)
            except Exception as ex:
                logger.error("Error during vpn check loop: {0}".format(ex))
                time.sleep(1)