class BergCloudSocketApi(Singleton): def __init__(self, bridge, host_eui64_hex, thread_exit_event, command_queue, event_queue, log_queue, watchdog_delegate): self.host_eui64_hex = host_eui64_hex self.commandProcessingEnabled = True self.eventProcessingEnabled = True self.logProcessingEnabled = True self.thread_exit_event = thread_exit_event self.command_queue = command_queue self.event_queue = event_queue self.log_queue = log_queue self.watchdog_delegate = watchdog_delegate self.current_json_message = None self.bridge = bridge self.networkUp = bridge.networkUp self.handshake_sent = False self.logger = Logger('cloud') self.command_logger = Logger('cloud.command') self.event_logger = Logger('cloud.event') self.logging_logger = Logger('cloud.logging') self.socket_logger = Logger('cloud.socket') self.resolver = simple_caching_resolver.SimpleCachingResolver() self.sm = statistics_monitor.StatisticsMonitor() self.event_sources = {} self.connection_failure_count = 0 self.ip_pool = [] self.last_connected_at = None self.ws = None socket.setdefaulttimeout(HTTP_SOCKET_TIMEOUT) def start(self): event_state = self.thread_exit_event.wait(THREAD_WAIT_TIMEOUT) while not event_state: if self.watchdog_delegate: self.watchdog_delegate.updateKey('socketThread') try: if self.ws == None or self.ws.connectionState == 'closed': if len(self.ip_pool) == 0: (ip_addresses, expiration_time,) = self.resolver.lookup(WS_HOSTNAME) random.shuffle(ip_addresses) self.logger.info('IP pool now %s with an expiry in %d seconds' % (ip_addresses, expiration_time - time.time())) self.ip_pool = ip_addresses self.connection_expiration_time = expiration_time ip_address = self.ip_pool.pop() if self.connect(ip_address): self.last_connected_at = time.time() self.current_ip_address = ip_address self.socket_logger.debug('Resetting connection_failure_count') self.connection_failure_count = 0 self.handshake_sent = False elif len(self.ip_pool) == 0: self.connection_failure_count += 1 self.socket_logger.debug("Incrementing connection_failure_count (now %d) as we're out of IPs" % self.connection_failure_count) event_state = self.thread_exit_event.wait(5) elif time.time() > self.connection_expiration_time: if self.currentIPIsValid(): self.logger.info('DNS timeout reached: Current IP address is still valid, not dropping') self.connection_expiration_time += 3600 else: self.logger.info("Closing websocket as we're over expiry time and DNS has changed") self.ws.close() else: self.sendPendingData() except: self.socket_logger.exception('Unhandled socket api exception:', sys.exc_info()) self.connection_failure_count += 1 self.handshake_sent = False event_state = self.thread_exit_event.wait(THREAD_WAIT_TIMEOUT) if self.ws != None: self.logger.warning('Closing websocket from bridge side') try: self.ws.close() finally: if self.ws._th.isAlive(): self.ws._th.join() self.ws = None def currentIPIsValid(self): current_ip_address = self.current_ip_address (resolved_ip_addresses, expiration_time,) = self.resolver.lookup(WS_HOSTNAME) return current_ip_address in resolved_ip_addresses def sendPendingData(self): if self.current_json_message != None: self.socket_logger.info('We have pending JSON data to send: %s' % self.current_json_message) if self.sendJson(self.current_json_message): self.current_json_message = None else: self.socket_logger.warning("Still can't send the current JSON") return if not self.handshake_sent: self.onEstablishConnection() if self.eventProcessingEnabled: self.postFromEventQueue() if self.log_queue != None and self.logProcessingEnabled: self.postFromLogQueue() self.assess_active_devices() def endpoint(self): return '%s://%s:%s/' % (WS_SCHEME, WS_HOSTNAME, WS_PORT) def connect(self, ip_address): connect_success = False self.socket_logger.info('Creating websocket client') ssl_options = None ws_protocols = ['bergcloud-bridge-v1'] if WS_SCHEME == 'https': proto = 'wss' ssl_options = {'keyfile': SSL_KEY, 'certfile': SSL_CERT, 'ca_certs': SSL_CA_PEM, 'cert_reqs': ssl.CERT_REQUIRED} else: proto = 'ws' ws_uri = '%s://%s:%s/api/v1/connection' % (proto, ip_address, WS_PORT) try: if ssl_options == None: self.ws = BergCloudStreamingClient(ws_uri, protocols=ws_protocols, heartbeat_freq=5) else: self.ws = BergCloudStreamingClient(ws_uri, ssl_options=ssl_options, protocols=ws_protocols, heartbeat_freq=5) self.ws.daemon = False self.ws.set_command_queue(self.command_queue) self.ws.set_event_queue(self.event_queue) self.ws.set_host_eui64_hex(self.host_eui64_hex) self.socket_logger.debug('Connecting to %s' % ws_uri) self.ws.connect() connect_success = True except socket.error as e: if e.errno == errno.ECONNREFUSED: self.socket_logger.error('Socket connection refused') elif e.errno == errno.ETIMEDOUT: self.socket_logger.error('Socket connection timed out') elif e.errno == errno.EHOSTUNREACH: self.socket_logger.error('No route to host') else: self.socket_logger.error('Socket connection exception %s [%s]' % (e, str(e.errno))) except socket.timeout: self.socket_logger.error('Socket connection timed out') except: self.socket_logger.exception('Unexpected socket connect error:', sys.exc_info()) if not connect_success: self.ws = None return connect_success def isConnected(self): if self.ws == None: return False else: return self.ws.connectionState == 'connected' def onEstablishConnection(self): if self.networkUp(): self.socket_logger.info('Sending bridge power_on handshake') event = bridge_event.BridgeEvent(self.host_eui64_hex, {'name': 'power_on'}) event.append_payload(self.bridge.system_environment) ver_info = version.Version() version_dict = {'firmware_version': ver_info.firmware_version, 'mac_address': ver_info.mac_address, 'ncp_version': ver_info.ncp_stack_version, 'model': ver_info.model} event.append_payload(version_dict) event_json = event.to_json() self.logger.debug('Posting event JSON %s' % event_json) result = self.sendJson(event_json) if result: self.handshake_sent = True self.event_sources = {} def postFromEventQueue(self): try: event = self.event_queue.get(False) if isinstance(event, device_event.DeviceEvent) and event.device_address == None: self.logger.warning('Device Event has no device_address! Cannot post to cloud!') return if isinstance(event, device_event.DeviceEvent): source_eui64_hex = byte_tuple.eui64ToHexString(event.device_address, False) self._register_active_device(source_eui64_hex) event.rssi_stats = self.sm.rssiStatsForEui64(source_eui64_hex) self.current_json_message = event.to_json() self.logger.info('Posting event JSON %s' % self.current_json_message) result = self.sendJson(self.current_json_message) if result: self.current_json_message = None except Queue.Empty: pass def postFromLogQueue(self): log_records = [] try: while True: record = self.log_queue.get(False) clr = cloud_log_record.CloudLogRecord(record) log_records.append(clr) except Queue.Empty: if len(log_records) > 0: self.current_json_message = cloud_log_record.CloudLogRecord.to_json(log_records, self.host_eui64_hex) result = self.sendJson(self.current_json_message) if result: self.current_json_message = None def sendJson(self, json): if self.ws == None: self.logger.error('No socket client to post with!') return False try: self.ws.send(json) return True except socket.error as e: if e.errno == errno.EPIPE: self.logger.error('Broken pipe. Re-stashing event and reconnecting') time.sleep(2) else: self.logger.error('Socket error %s' % e) self.ws.close() self.ws = None return False except: self.logger.exception('socket send exception', sys.exc_info()) return False def assess_active_devices(self): current_time = time.time() nodes = [] for eui64_hex in self.event_sources.keys(): times = self.event_sources[eui64_hex] first_contact_time = times[0] last_contact_time = times[1] if current_time - last_contact_time < HEARTBEAT_TIMEOUT: nodes.append(eui64_hex) else: self._deregister_active_device(eui64_hex) if len(nodes) == 0: return None else: return nodes def active_device_uptime(self, eui64_hex): current_time = time.time() if self.event_sources.has_key(eui64_hex): return current_time - self.event_sources[eui64_hex][0] def _register_active_device(self, eui64_hex): current_time = time.time() if self.event_sources.has_key(eui64_hex): self.event_sources[eui64_hex][1] = current_time else: self.event_sources[eui64_hex] = [current_time, current_time] self.command_logger.info('Issuing device_connect for %s' % eui64_hex) event = bridge_event.BridgeEvent(self.host_eui64_hex, {'name': 'device_connect', 'device_address': eui64_hex}) self.event_queue.put(event, False) def _deregister_active_device(self, eui64_hex): if self.event_sources.has_key(eui64_hex): self.command_logger.info('Issuing device_disconnect for %s' % eui64_hex) event = bridge_event.BridgeEvent(self.host_eui64_hex, {'name': 'device_disconnect', 'device_address': eui64_hex}) self.event_queue.put(event, False) del self.event_sources[eui64_hex]
class BergCloudStreamingClient(WebSocketClient): def __init__(self, *args, **kwargs): self.logger = Logger('cloud.socketclient') self.connectionState = 'connecting' super(BergCloudStreamingClient, self).__init__(*args, **kwargs) def set_command_queue(self, queue): self.command_queue = queue def set_event_queue(self, queue): self.event_queue = queue def set_host_eui64_hex(self, value): self.host_eui64_hex = value def opened(self): self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 3) self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3) self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5) self.connectionState = 'connected' linux_hub.set_led_state('bergcloud', 'on', True) self.logger.info('Websocket open and connected') def closed(self, code, reason = None): self.connectionState = 'closed' linux_hub.set_led_state('bergcloud', 'off', True) self.logger.warning('Closed down websocket, code: %d, reason: %s' % (code, reason)) def received_message(self, m): try: decoded_json_message = json.loads(str(m)) self.process_command(decoded_json_message) except: self.logger.exception('Exception decoding JSON:', sys.exc_info()) def process_command(self, response): if response.has_key('type'): command_type = response['type'] if command_type == 'BridgeCommand': bc = bridge_command.BridgeCommand.from_json(response) self.logger.debug('Queueing %s' % bc) self.command_queue.put(bc, False) elif command_type == 'DeviceCommand': try: dc = device_command.DeviceCommand.from_json(response) dc.bridge_address = self.host_eui64_hex self.logger.debug('Queueing %s' % dc) self.command_queue.put(dc, False) except Queue.Full: self.logger.warning('Ut oh, the command queue is full!') dc.return_code = device_command.RSP_QUEUE_FULL self.event_queue.put(dc, False) except: self.logger.exception('Exception creating command:', sys.exc_info()) dc.return_code = 255 self.event_queue.put(dc, False) else: self.logger.error("Unknown command type '%s'" % command_type) return else: self.logger.error('Required type key missing in command') return