class HUB: def __init__(self, config_file='./hub_config.ini'): log_format = '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> <level>{level}: {message}</level>' logger.remove() logger.add(sys.stdout, format=log_format, colorize=True) config = configparser.ConfigParser() if not os.path.isfile(config_file): logger.critical(f"Cannot find '{config_file}', aborting...") exit(1) config.read(config_file) logger.add(os.path.join(config['logging']['logs_path'], 'log_{time:YYYY-MM-DD}.log'), format=log_format, colorize=True, compression='zip', rotation='00:00') logger.info('Booting up...') self.mqtt_broker = config['mqtt']['mqtt_broker'] self.mqtt_topic = config['mqtt']['mqtt_topic'] self.scan_rate = int(config['scanning']['rate']) self.max_threads = int(config['scanning']['threads']) logger.info( f"[Scan configuration]:\nScan rate: {self.scan_rate}s\nScan threads: {self.max_threads}" ) mqtt_id = config['mqtt']['mqtt_id'] logger.info( f"[MQTT configuration]\nBroker: {self.mqtt_broker}\nTopic: {self.mqtt_topic}\nID: {mqtt_id}" ) self.mqtt_client = Client(client_id=mqtt_id) self.mqtt_client.enable_logger(logger) self.mqtt_client.on_message = self.toggle_bulbs self.mqtt_client.on_connect = self.mqtt_subscribe self.mqtt_client.on_disconnect = self.mqtt_connect self.mqtt_client.loop_start() self.mqtt_connect() self.bulbs = [] # [{'hostname': '<>', 'ip': '<>'}] self.threads_buffer = [] self.startup() self.loop() def mqtt_connect(self): logger.debug(f'Connecting to MQTT broker {self.mqtt_broker}...') try: response = self.mqtt_client.connect(host=self.mqtt_broker, port=1883) except Exception as e: logger.error(f"Can't connect to MQTT broker; {e}") response = 1 if response != 0: logger.error(f'Not connected to MQTT broker {self.mqtt_broker}') def mqtt_subscribe(self, *args): self.mqtt_client.subscribe(topic=self.mqtt_topic, qos=1) logger.info(f'Subscribed to {self.mqtt_topic}') def get_subnet(self): ip_scanner = IPRoute() info = [{ 'iface': x['index'], 'addr': x.get_attr('IFA_ADDRESS'), 'mask': x['prefixlen'] } for x in ip_scanner.get_addr()] ip_scanner.close() subnet = None for interface in info: if '192.168' in interface['addr']: subnet = f'192.168.{interface["addr"].split(".")[2]}' return subnet def scanner(self, ips_list): for ip in ips_list: try: hostname = socket.gethostbyaddr(ip)[0] if 'yeelink' in hostname: self.threads_buffer.append({ 'hostname': hostname, 'ip': ip }) logger.info(f'Found new bulb: {hostname} at {ip}') except socket.herror as se: if 'Unknown host' not in str(se): logger.error(str(se)) def spawn_scanners(self, ips: list): max_threads = self.max_threads ips_for_thread = int(len(ips) / max_threads) limits = [i * ips_for_thread for i in range(max_threads)] ranges = [ips[limit:limit + ips_for_thread + 1] for limit in limits] threads = [] for r in ranges: t = threading.Thread(target=self.scanner, args=(r, )) t.start() threads.append(t) t: threading.Thread for t in threads: t.join() def get_bulbs_ips(self): logger.info('Scanning network for bulbs...') subnet = self.get_subnet() if subnet is None: logger.error('No router connection! Aborting...') return None #subnet = '192.168.178' logger.debug(f'Subnet: {subnet}') ips = [f"{subnet}.{i}" for i in range(0, 256)] self.threads_buffer = [] self.spawn_scanners(ips) bulbs = self.threads_buffer result = [ f"hostname: {bulb['hostname']}, ip: {bulb['ip']}" for bulb in bulbs ] if len(bulbs) > 0: logger.info(f'Network scan ended, result:\n' + '\n'.join(result)) else: logger.warning(f'Network scan ended, no bulbs found') return bulbs def toggle_bulb(self, bulb): bulb_obj = Bulb(bulb['ip']) response = bulb_obj.toggle() if response == 'ok': logger.info(f'Toggled {bulb["hostname"]} at {bulb["ip"]}') else: logger.error( f'Toggle error for {bulb["hostname"]} at {bulb["ip"]}') def toggle_bulbs(self, bulbs, *args): if type(bulbs) is not list: available_bulbs = self.bulbs else: available_bulbs = bulbs logger.info('Toggling bulbs...') for bulb in available_bulbs: self.toggle_bulb(bulb) logger.info('All bulbs toggled.') def check_mqtt_connection(self): logger.debug("Checking mqtt broker connection...") if not self.mqtt_client.is_connected(): logger.warning("Broker connection error, trying reconnection...") self.mqtt_client.reconnect() if not self.mqtt_client.is_connected(): logger.error("Reconnection error") def loop(self): SCAN_RATE = self.scan_rate while True: try: self.bulbs = self.get_bulbs_ips() time.sleep(SCAN_RATE) except KeyboardInterrupt as ki: logger.critical("HUB killed, stopping mqtt loop...") try: self.mqtt_client.loop_stop() except: self.mqtt_client.loop_stop(force=True) except Exception as e: logger.critical(f"Unhandled exception: {e}") time.sleep(1) def turn_off_bulbs(self): logger.info("Turning off bulbs...") for bulb in self.bulbs: bulb_obj = Bulb(bulb['ip']) response = bulb_obj.turn_off() if response == 'ok': logger.info(f'{bulb["hostname"]} turned off at {bulb["ip"]}') else: logger.error( f'Turn off error for {bulb["hostname"]} at {bulb["ip"]}') def startup(self): logger.info("Setting up...") self.bulbs = self.get_bulbs_ips() self.turn_off_bulbs()
class PahoIoTClient: """ Responsible for connecting to AWS IoT. Handles all connection lifecycle events and attempts to reconnect whenever possible if disconnected. Data is sent to IoT via the method .send_telemetry(TelemetryMessage). """ MQTT_QOS_RETRY_INTERVAL_S = 60 KEEP_ALIVE_TIMEOUT_S = 2 * 60 config: IotClientConfig mqtt_client: Client() event_listener: IoTEventListener topic_subscriptions: list important_paho_messages = [ "Sending PUBLISH (d1", "Connection failed, retrying" ] def __init__(self, config: IotClientConfig, event_listener: IoTEventListener): self.set_config(config=config) self.event_listener = event_listener self.mqtt_client = Client(config.client_id) self.mqtt_client.message_retry_set(self.MQTT_QOS_RETRY_INTERVAL_S) # self.mqtt_client.tls_set(ca_certs = config.ca_path, certfile = config.certificate_path, keyfile = config.private_key_path) self.mqtt_client.username_pw_set(username=config.username, password=config.password) self.mqtt_client.on_connect = self.on_connected self.mqtt_client.on_disconnect = self.on_disconnected self.mqtt_client.on_publish = self.on_message_published self.mqtt_client.on_message = self.on_message_received def set_config(self, config: IotClientConfig): self.config = config def is_connected(self) -> bool: return self.mqtt_client.is_connected() def connect(self): self.mqtt_client.connect_async(host=self.config.endpoint, port=self.config.port, keepalive=self.KEEP_ALIVE_TIMEOUT_S) self.mqtt_client.loop_start() def disconnect(self): self.mqtt_client.disconnect() self.mqtt_client.loop_stop() # async def reconnect(self, delay_in_s: int = 5): # await asyncio.sleep(delay_in_s) # self.disconnect() # await asyncio.sleep(delay_in_s) # self.connect() def publish(self, topic: str, data: str, qos: int = 1, reconnect_after_fail: bool = False) -> (int, int): message_info = self.mqtt_client.publish(topic=topic, payload=data, qos=qos) if message_info.rc is not MQTT_ERR_SUCCESS: pass else: pass return (message_info.rc, message_info.mid) def is_response_code_success(self, response_code: int) -> bool: return response_code is MQTT_ERR_SUCCESS def initialise_subscriptions_list(self): self.topic_subscriptions = [] def subscribe(self, subscriptions: list): for subscription in subscriptions: if not self.is_topic_subscribed(subscription): self.topic_subscriptions.append(subscription) self.mqtt_client.subscribe(topic=self.topic_subscriptions) def is_topic_subscribed(self, subscription): for existing_subscription in self.topic_subscriptions: if subscription[0] == existing_subscription[0]: return True return False def on_connected(self, client, userdata, flags, rc): self.initialise_subscriptions_list() if self.event_listener is not None: self.event_listener.on_iot_connected(iot_client=self) def on_disconnected(self, client, userdata, rc): # Clear the list of subscriptions if self.event_listener is not None: self.event_listener.on_iot_disconnected(iot_client=self) def on_message_published(self, client, userdata, mid): if self.event_listener is not None: self.event_listener.on_iot_message_published( self, message_id=mid, payload_size=self.payload_sizes.pop("{0}".format(mid), None)) def on_message_received(self, client, userdata, message): if self.event_listener is not None: self.event_listener.on_iot_message_received( iot_client=self, topic=message.topic, payload=message.payload)