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 }
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)