Beispiel #1
0
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)