Пример #1
0
def main():
    print("Hello! this is aggry! I send wonderful data :)")

    client = Client(client_id="aggry")
    client.on_connect = on_connect
    client.on_message = on_message

    if debug:
        client.on_log = on_log
    else:
        print("Debug mode disabled by configuration.")

    # Swarm does not handle dependencies
    sleep(2)

    try:
        rc = client.connect("broker", 1883, 60)
    except:
        # just try again
        print("connect: trying again...")
        sleep(4)
        client.connect("broker", 1883, 60)

    # main loop
    client.loop_forever()
Пример #2
0
    def _run(self):
        '''
        The main function of this task.
        '''
        # join function input buffer, it's a shared memory
        join_in_buf = [None] * len(self.idf_confs)

        def on_connect(client, userdata, rc):
            log.info('ESM connect to mqtt broker with return code %s', rc)

            # create odf publish partial function
            for conf in self.odf_confs:
                conf['pub'] = partial(client.publish, conf['topic'])

            for idx, idf in enumerate(self.idf_confs):
                topic = idf['topic']
                client.subscribe(topic)
                client.message_callback_add(
                    topic,
                    self._msg_callback(
                        idf.get('func'), self.join_func.get('func'),
                        self.odf_confs, join_in_buf, idx))

        mqtt_conn = Client()
        mqtt_conn.on_connect = on_connect
        mqtt_conn.connect(config.mqtt_conf['host'],
                          port=config.mqtt_conf['port'])
        mqtt_conn.loop_forever()
Пример #3
0
class MqttSubscriber(BaseSubscriber):
    def __init__(self, *args, **kwargs):
        self.client = None
        self.message = None
        self.f = open("time_taken_mqtt.txt", "a+")

    def connect(self, host="localhost", port=5000, topic="test"):
        print("Connected")
        self.client = Client()
        self.client.connect(host=host)
        self.client.on_message = self.recv_message
        self.client.on_subscribe = self.on_subscribe
        self.client.subscribe(topic)
        self.client.loop_forever()

    def on_subscribe(self, *args, **kwargs):
        print("Subscribed")

    def recv_message(self, client, userdata, message):
        print(message.payload)
        message = json.loads(message.payload)
        latency = time() - message["sentAt"]
        msg_size = len(message["message"].encode('utf-8'))
        self.f.write("{} : {}\n".format(msg_size, latency))
        print("Message received : {} size is {} in {}".format(
            message, msg_size, str(latency)))

    def close(self):
        pass
Пример #4
0
class MQTTClient(object):
    """Manages Paho MQTT client lifecycle and callbacks"""

    def __init__(self, config: dict, message_processor=None):
        self.config = config

        self.client = Client(
            client_id=config.mqtt_client,
            clean_session=config.mqtt_clean_session,
            userdata={"client": config.mqtt_client},
        )

        self.client.username_pw_set(config.mqtt_username, config.mqtt_password)

        if self.config.mqtt_debug:
            self.client.on_log = self._on_log

        self.client.on_connect = self._on_connect
        self.client.on_subscribe = self._on_subscribe
        self.client.on_message = self._on_message
        self.client.on_publish = self._on_publish
        self.client.on_disconnect = self._on_disconnect

        self.client.connect(config.mqtt_host, config.mqtt_port, 60)

        if message_processor:
            self.message_processor = message_processor

    def _on_log(self, client, userdata, level, buf):
        click.echo(f"{buf}, origin: {userdata['client']}")

    def _on_connect(self, client, userdata, flags, rc):
        click.echo(f"Connected {userdata['client']}, result code: {str(rc)} {str(flags)}")
        click.echo(f"Subscribing to all topics...")
        self.client.subscribe(self.config.mqtt_topics)

    def _on_subscribe(self, client, userdata, mid, granted_qos):
        click.echo(f"Subscribed {userdata['client']}, mid: {mid}, granted qos: {granted_qos}")
        click.echo(f"Listening for {userdata['client']} messages...")

    def _on_disconnect(self, client, userdata, rc):
        click.echo(f"Disconnected {userdata['client']}, result code: {str(rc)}")

    def _on_message(self, client, userdata, msg):
        if hasattr(self, "message_processor"):
            self.message_processor(client, userdata, msg)
        else:
            click.echo(f"Topic: {msg.topic}, Mid: {msg.mid}, Payload: {msg.payload.decode('utf-8')}")

    def _on_publish(self, client, userdata, mid):
        click.echo(f"Published by {userdata['client']}, mid: {mid}")

    def listen(self):
        try:
            self.client.loop_forever()
        except KeyboardInterrupt:
            click.echo(f"Received KeyboardInterrupt, disconnecting {self.config.mqtt_client}")
            self.client.disconnect()
Пример #5
0
class MQTTSnipsComponent(SnipsComponent):
    """A Snips component using the MQTT protocol directly.

    Attributes:
        snips (:class:`.SnipsConfig`): The Snips configuration.
        mqtt (`paho.mqtt.client.Client`_): The MQTT client object.

    .. _`paho.mqtt.client.Client`: https://www.eclipse.org/paho/clients/python/docs/#client
    """
    def _connect(self):
        """Connect with the MQTT broker referenced in the Snips configuration
        file.
        """
        self.mqtt = Client()
        self.mqtt.on_connect = self._subscribe_topics
        connect(self.mqtt, self.snips.mqtt)

    def _start(self):
        """Start the event loop to the MQTT broker so the component starts
        listening to MQTT topics and the callback methods are called.
        """
        self.mqtt.loop_forever()

    def _subscribe_topics(self, client, userdata, flags, connection_result):
        """Subscribe to the MQTT topics we're interested in.

        Each method with an attribute set by a
        :func:`snipskit.decorators.mqtt.topic` decorator is registered as a
        callback for the corresponding topic.
        """
        for name in dir(self):
            callable_name = getattr(self, name)
            if hasattr(callable_name, 'topic'):
                self.mqtt.subscribe(getattr(callable_name, 'topic'))
                self.mqtt.message_callback_add(getattr(callable_name, 'topic'),
                                               callable_name)

    def publish(self, topic, payload, json_encode=True):
        """Publish a payload on an MQTT topic on the MQTT broker of this object.

        Args:
            topic (str): The MQTT topic to publish the payload on.
            payload (str): The payload to publish.
            json_encode (bool, optional): Whether or not the payload is a dict
                that will be encoded as a JSON string. The default value is
                True. Set this to False if you want to publish a binary payload
                as-is.

        Returns:
            :class:`paho.mqtt.MQTTMessageInfo`: Information about the
            publication of the message.

        .. versionadded:: 0.5.0
        """
        if json_encode:
            payload = json.dumps(payload)

        return self.mqtt.publish(topic, payload)
Пример #6
0
def main():
    influxdb_client = InfluxDBClient(INFLUXDB_HOST, INFLUXDB_PORT, INFLUXDB_USERNAME, INFLUXDB_PASSWORD, INFLUXDB_DATABASE) #First the database is initialized
    mqtt_client = MQTTClient( MQTT_CLIENT_ID, userdata=influxdb_client) #Then we create a client object
    mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) #and set the username and password for the MQTT client
    mqtt_client.tls_set()
    mqtt_client.on_connect = mqtt_connect_callback #We tell the client which functions are to be run on connecting
    mqtt_client.on_message = mqtt_message_callback #and on receiving a message
    mqtt_client.connect(MQTT_HOST, MQTT_PORT) #we can connect to the broker with the broker host and port
    mqtt_client.loop_forever()
Пример #7
0
def publish_job():
    """ 打卡任务 """
    client = Client()
    client.on_connect = on_connect
    with open('captured_packet.json', encoding='utf-8') as f:
        packet_info = json.load(f)
    # 连接代理服务器
    print('🙈 正在连接代理服务器...')
    client.connect(packet_info['dst host'], 1883)
    client.loop_forever()
Пример #8
0
def main():
    influxdb_client = InfluxDBClient(INFLUXDB_HOST, INFLUXDB_PORT,
                                     INFLUXDB_USERNAME, INFLUXDB_PASSWORD,
                                     INFLUXDB_DATABASE)
    mqtt_client = MQTTClient(MQTT_CLIENT_ID, userdata=influxdb_client)
    mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
    mqtt_client.tls_set()
    mqtt_client.on_connect = mqtt_connect_callback
    mqtt_client.on_message = mqtt_message_callback
    mqtt_client.connect(MQTT_HOST, MQTT_PORT)
    mqtt_client.loop_forever()
Пример #9
0
def run_mqtt_listener():
    mqtt_broker_url = '167.86.108.163'
    MQTT_HOST = os.getenv('MQTT_HOST', mqtt_broker_url)
    client_name = 'mqtt-csw-importer'
    topic = 'update_csw'

    print("Sono partito")
    print("->167.86.108.163")
    print("->mqtt-csw-importer")

    def _on_connect(client, userdata, flags, rc):
        print("Connesso con successo al topic", topic)

    def _on_log(client, userdata, level, buf):
        # print("log", level, buf)
        pass

    def _on_message(client, userdata, message):
        message = str(message.payload.decode("utf-8"))
        payload = json.loads(message)['rndt_xml']
        print('received event', payload)
        print(
            '----ENDED------------------------------------------------------')
        cmd = 'pycsw-admin.py -c load_records -f /etc/pycsw/pycsw.cfg  -p /home/pycsw/datatemp'
        filename = "/home/pycsw/datatemp/temp1.xml"
        try:
            with open(filename, 'w') as file:
                file.write(payload)
            execute(cmd)
            # execute('rm -f ' + filename)
        except Exception as e:
            print('Exception', e)

    try:
        client = Client(client_id='{}_{}'.format(client_name,
                                                 random.randint(1, 10000)),
                        clean_session=True)

        print('Connecting to MQTT broker at ')
        client.connect(MQTT_HOST)
    except ConnectionRefusedError as e:
        print('Connection refused by MQTT broker at ')
        raise ConnectionRefusedError
    client.on_connect = _on_connect
    client.on_message = _on_message
    client.on_log = _on_log

    client.subscribe(topic)

    client.loop_forever()
    # client.loop_start()
    print('loop started')
Пример #10
0
def start_mqtt(client: mqtt.Client):
    """Начало обработки сетевого трафика между клиентом и брокером."""

    try:
        client.loop_forever()
    except KeyboardInterrupt:
        if get_setting("is_debug_mode"):
            event_log.info("Завершение соединения с брокером mqtt")
    except Exception as err:
        error_log.error(
            "Произошла ошибка во время работы клиента. Текст ошибки: %s",
            str(err))
        raise ClientError  # pylint: disable = raise-missing-from
Пример #11
0
class Command(BaseCommand):
    help = 'Long-running Daemon Process to Integrate MQTT Messages with Django'

    def _create_default_user_if_needed(self):
        # make sure the user account exists that holds all new devices
        try:
            User.objects.get(username=settings.DEFAULT_USER)
        except User.DoesNotExist:
            print("Creating user {} to own new LAMPI devices".format(
                settings.DEFAULT_USER))
            new_user = User()
            new_user.username = settings.DEFAULT_USER
            new_user.password = '******'
            new_user.is_active = False
            new_user.save()

    def _on_connect(self, client, userdata, flags, rc):
        self.client.message_callback_add('$SYS/broker/connection/+/state',
                                         self._device_broker_status_change)
        self.client.subscribe('$SYS/broker/connection/+/state')

    def _create_mqtt_client_and_loop_forever(self):
        self.client = Client()
        self.client.on_connect = self._on_connect
        self.client.connect('localhost', port=50001)
        self.client.loop_forever()

    def _device_broker_status_change(self, client, userdata, message):
        print("RECV: '{}' on '{}'".format(message.payload, message.topic))
        # message payload has to treated as type "bytes" in Python 3
        if message.payload == b'1':
            # broker connected
            results = re.search(MQTT_BROKER_RE_PATTERN, message.topic.lower())
            device_id = results.group('device_id')
            try:
                device = Lampi.objects.get(device_id=device_id)
                print("Found {}".format(device))
            except Lampi.DoesNotExist:
                # this is a new device - create new record for it
                new_device = Lampi(device_id=device_id)
                uname = settings.DEFAULT_USER
                new_device.user = User.objects.get(username=uname)
                new_device.save()
                print("Created {}".format(new_device))
                # send association MQTT message
                new_device.publish_unassociated_msg()

    def handle(self, *args, **options):
        self._create_default_user_if_needed()
        self._create_mqtt_client_and_loop_forever()
Пример #12
0
def mqtt_handler():
    global mqtt_client
    Client.connected_flag = False
    mqtt_client = Client()
    mqtt_client.on_connect = on_connect
    mqtt_client.on_message = on_message
    mqtt_client.loop_start()
    mqtt_client.connect(host=MQTT_ADDR, port=MQTT_PRT)
    while not mqtt_client.connected_flag:  # wait in loop
        print("In wait loop")
        time.sleep(1)

    # subscribe all rooms, using MQTT single layer wildcard
    mqtt_client.subscribe(topic='%s/+' % ORDER_STATUS)
    mqtt_client.loop_forever()
    mqtt_client.disconnect()
Пример #13
0
class PublishThread(QThread):
    """ 发布消息 """

    publishStateChangedSignal = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.client = Client()
        self.client.on_connect = self.on_connect

    def run(self):
        """ 发布消息 """
        self.client.connect(self.broker, 1883)
        self.client.loop_forever()

    def publish(self, broker: str, topic: str, message: str):
        """ 发布消息

        Parameters
        ----------
        broker : str
            代理服务器 IP 地址

        topic : str
            订阅主题

        message : str
            消息
        """
        self.topic = topic
        self.broker = broker
        self.message = message
        self.publishStateChangedSignal.emit('🙈 正在连接代理服务器...')
        self.start()

    def on_connect(self, client: Client, userdata, flags, rc, properties=None):
        """ mqtt连接回调函数 """
        status = ['连接成功', '协议版本错误', '客户端标识符无效', '服务器不可用', '用户名或密码错误', '未授权']
        if rc != 0:
            self.publishStateChangedSignal.emit(status[rc])
            return
        # 发布信息并发送消息给主界面
        self.publishStateChangedSignal.emit('🙉 代理服务器连接成功!')
        client.publish(self.topic, self.message, 1)
        self.publishStateChangedSignal.emit('🙊 假数据包发送成功!')
        client.disconnect()
        self.publishStateChangedSignal.emit(f'🙊 已与代理服务器 {self.broker} 断开连接')
Пример #14
0
def run(client: mqtt.Client):

    client.publish(
        f"homeassistant/switch/{CONFIG.get_device_name()}/screen/config",
        f'{{"unique_id": "screen-{CONFIG.get_device_name()}", "name": "{CONFIG.get_device_name()} Screen", "device": {{"identifiers": ["{CONFIG.get_device_name()}"], "name": "{CONFIG.get_device_name()}"}}, "~": "homeassistant/switch/{CONFIG.get_device_name()}/screen", "availability_topic": "~/state", "command_topic": "~/set", "retain": true}}',
        retain=True)
    client.publish(
        f"homeassistant/switch/{CONFIG.get_device_name()}/screen/state",
        "online",
        retain=True)
    logging.info("Set mqtt toptic state to online")

    client.subscribe(
        f"homeassistant/switch/{CONFIG.get_device_name()}/screen/set")
    client.on_message = on_message

    client.loop_forever()
Пример #15
0
class MqttClient(object):
    """Mqtt通讯封装"""
    def __init__(self, address):
        logging.info(
            "MqttClient.__init__() address=({address[0]}, {address[1]})".
            format(address=address))
        self.client = Mqtt()
        self.address = address
        assert isinstance(address, tuple), "the address is invalid."

    def handleConnected(self):
        logging.info("MqttClient.handleConnected()")

    def publish(self, topic, payload=None, qos=0, retain=False):
        logging.info("MqttClient.publish() topic={}".format(topic))
        self.client.publish(topic, payload, qos, retain)

    def subscribe(self, topic, qos=0):
        logging.info("MqttClient.subscribe() topic={}".format(topic))
        self.client.subscribe(topic, qos)

    def handleMessage(self, topic, payload):
        logging.info("MqttClient.handleMessage() topic={}".format(topic))

    def sendMessage(self, topic, payload=None, qos=0, retain=False):
        logging.info("MqttClient.sendMessage() topic={}".format(topic))
        self.client.publish(topic, payload, qos, retain)

    def run(self):
        logging.info("MqttClient.run()")

        def on_connect(client, userdata, flags, rc):
            self.handleConnected()

        def on_message(client, userdata, msg):
            self.handleMessage(msg.topic, msg.payload)

        self.client.on_connect = on_connect
        self.client.on_message = on_message
        self.client.connect(self.address[0], self.address[1])
        self.client.loop_forever()
Пример #16
0
def mqtt_handler():
    global mqtt_client
    Client.connected_flag = False
    mqtt_client = Client()

    # set mosquitto broker password and username
    mqtt_client.username_pw_set(username=USERNAME, password=PASSWORD)
    # set TLS cert for the client
    mqtt_client.tls_set(ca_certs=TLS_CERT)
    mqtt_client.tls_insecure_set(True)

    mqtt_client.on_connect = on_connect
    mqtt_client.on_message = on_message
    mqtt_client.loop_start()
    mqtt_client.connect(host=MQTT_ADDR, port=MQTT_PRT)
    while not mqtt_client.connected_flag:  # wait in loop
        print("In wait loop")
        time.sleep(1)
    mqtt_client.subscribe(topic='%s/+' % ORDER_STATUS)
    mqtt_client.loop_forever()
    mqtt_client.disconnect()
Пример #17
0
def main():
    parser = argparse.ArgumentParser(description=help_text)
    parser.add_argument('-l',
                        '--log-level',
                        action='store',
                        type=str,
                        dest='log_level',
                        help='Log level',
                        default='INFO')

    args = parser.parse_args()
    numeric_level = getattr(logging, args.log_level.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError('Invalid log level: %s' % args.log_level)
    coloredlogs.install(level=numeric_level)
    logger.info('Log level set to %s', args.log_level)

    client = Client()
    client.username_pw_set("****", "****")
    client.on_connect = on_connect
    client.on_message = on_message

    client.connect(MQTT_ADRESS, MQTT_PORT, 60)
    client.loop_forever()
Пример #18
0
    def connauth(host, client_id=None, user=None, passwd=None, **kw):
        """
        Helper to check if a client can connect to a broker with specific
        client ID and/or credentials.

        :param host: Host to connect to
        :param client_id: Client ID to use. If not specified paho-mqtt
                        generates a random id.
        :param user: User name of the client. If None or empty, connection is
                     attempted without username and password
        :param passwd: Password of the client. If None, only user name is sent
        :param kw: Client.connect() keyword arguments (excluding host)
        :return: Two comma separated values. The result code and its string
                 representation
        """
        return_code = {"rc": None}
        client = Client(client_id, userdata=return_code)
        if user is not None and user != "":
            client.username_pw_set(user, passwd)
        client.on_connect = SimpleMqttClient._on_connauth

        client.connect(host, **kw)
        client.loop_forever()
        return return_code["rc"], connack_string(return_code["rc"])
Пример #19
0
    def connauth(host, clientid=None, user=None, passwd=None, **kw):
        """
        connauth helps in checking if a client can connect to a broker with specific client id and/or credentials

        :param host:     Host to connect to
        :param clientid: Client ID to use. If not specified paho-mqtt generates a random id.
        :param user:     User name of the client. If None or empty, connection is attempted without user, pwd
        :param passwd:   Password of the client. If None, only user name is sent
        :param kw:       Client.connect() keyword arguments (excluding host)
        :return:         Two comma separated values - The result code and it's string representation
        """
        rc = {"rc": None}
        c = Client(clientid, userdata=rc)
        if user is not None and user is not "":
            c.username_pw_set(user, passwd)
        c.on_connect = SimpleMqttClient._on_connauth

        #print("connecting to ({})".format(host))
        r = c.connect(host, **kw)
        #print("connect() returned r.__class__ = ({}), r = ({})".format(r.__class__, r))

        r = c.loop_forever()
        return rc["rc"], connack_string(rc["rc"])
Пример #20
0
class MQTTClient:
    """ mqtt客户端的父类
    """

    def __init__(self, host, port,
                 client_id=None,
                 clean_session=False,
                 keepalive=60):

        self.client = Client()
        if client_id:
            self.client._client_id = client_id
            self.client._clean_session = False

        self.host = host
        self.port = port
        self.keepalive = keepalive

        self.connected = False

        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message
        self.client.on_log = self._on_log

    def connect(self, user='******', password='******'):
        rc = 1
        if self.connected:
            return 0

        self.client.username_pw_set(user, password)
        try:
            rc = self.client.connect(self.host, self.port, self.keepalive)
            assert rc == 0, ConnectionRefusedError
            self.connected = True

        except ConnectionRefusedError:
            log.error('Retry after 1 second.')

        return rc

    def disconnect(self):
        self.connected = False
        self.client.disconnect()

    def loop(self, timeout=None):
        if timeout:
            self.client.loop(timeout=timeout)
        else:
            self.client.loop_forever()

    def loop_start(self):
        return self.client.loop_start()

    def publish(self, topic, data={}, qos=1):
        (rc, final_mid) = self.client.publish(topic, json.dumps(data), qos=qos)
        return rc, final_mid

    def _on_connect(self, client, userdata, flags, rc):
        pass

    def _on_message(self, client, userdata, msg):
        pass

    def _on_log(self, client, userdata, level, buf):
        return buf
Пример #21
0
class Gateway3(Thread, GatewayV, GatewayMesh, GatewayInfo):
    mesh_thread = None
    pair_model = None
    pair_payload = None

    def __init__(self,
                 host: str,
                 token: str,
                 config: dict,
                 ble: bool = True,
                 zha: bool = False):
        super().__init__(daemon=True)

        self.host = host
        self.ble = ble
        self.zha = zha

        # TODO: in the end there can be only one
        self.miio = SyncmiIO(host, token)

        self.mqtt = Client()
        self.mqtt.on_connect = self.on_connect
        self.mqtt.on_disconnect = self.on_disconnect
        self.mqtt.on_message = self.on_message
        self.mqtt.connect_async(host)

        self._debug = config['debug'] if 'debug' in config else ''
        self._disable_buzzer = config.get('buzzer') is False
        self._zigbee_info = config.get('zigbee_info')
        self.default_devices = config['devices']

        self.devices = {}
        self.updates = {}
        self.setups = {}
        self.info = {}

    @property
    def device(self):
        return self.devices['lumi.0']

    def add_update(self, did: str, handler):
        """Add handler to device update event."""
        self.updates.setdefault(did, []).append(handler)

    def remove_update(self, did: str, handler):
        self.updates.setdefault(did, []).remove(handler)

    def add_setup(self, domain: str, handler):
        """Add hass device setup funcion."""
        self.setups[domain] = handler

    def debug(self, message: str):
        _LOGGER.debug(f"{self.host} | {message}")

    def run(self):
        """Main thread loop."""
        while True:
            # if not telnet - enable it
            if not self._check_port(23) and not self._enable_telnet():
                time.sleep(30)
                continue

            devices = self._prepeare_gateway(with_devices=True)
            if devices:
                self.setup_devices(devices)
                break

        self.mesh_start()

        while True:
            # if not telnet - enable it
            if not self._check_port(23) and not self._enable_telnet():
                time.sleep(30)
                continue

            # if not mqtt - enable it (handle Mi Home and ZHA mode)
            if not self._mqtt_connect() and not self._prepeare_gateway():
                time.sleep(60)
                continue

            self.mqtt.loop_forever()

    def _check_port(self, port: int):
        """Check if gateway port open."""
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            return s.connect_ex((self.host, port)) == 0
        finally:
            s.close()

    def _enable_telnet(self):
        """Enable telnet with miio protocol."""
        self.debug("Try enable telnet")

        if self.miio.send("enable_telnet_service") != 'ok':
            self.debug(f"Can't enable telnet")
            return False

        return True

    def _prepeare_gateway(self, with_devices: bool = False):
        """Launching the required utilities on the hub, if they are not already
        running.
        """
        self.debug("Prepare Gateway")
        try:
            shell = TelnetShell(self.host)

            self.ver = shell.get_version()
            self.debug(f"Version: {self.ver}")

            ps = shell.get_running_ps()

            if "mosquitto -d" not in ps:
                self.debug("Run public mosquitto")
                shell.run_public_mosquitto()

            # all data or only necessary events
            pattern = '\\{"' if 'miio' in self._debug \
                else "ble_event|properties_changed"

            if f"awk /{pattern} {{" not in ps:
                self.debug(f"Redirect miio to MQTT")
                shell.redirect_miio2mqtt(pattern, self.ver_miio)

            if self._disable_buzzer and "basic_gw -b" in ps:
                _LOGGER.debug("Disable buzzer")
                shell.stop_buzzer()

            if self.zha:
                if "socat" not in ps:
                    if "Received" in shell.check_or_download_socat():
                        self.debug("Download socat")
                    self.debug("Run socat")
                    shell.run_socat()

                if "Lumi_Z3GatewayHost_MQTT" in ps:
                    self.debug("Stop Lumi Zigbee")
                    shell.stop_lumi_zigbee()

            else:
                if "socat" in ps:
                    self.debug("Stop socat")
                    shell.stop_socat()

                if self._zigbee_info:
                    if "Lumi_Z3GatewayHost_MQTT -n 1 -b 115200 -v" not in ps:
                        self.debug("Run public Zigbee console")
                        shell.run_public_zb_console()

                else:
                    if "Lumi_Z3GatewayHost_MQTT" not in ps:
                        self.debug("Run Lumi Zigbee")
                        shell.run_lumi_zigbee()

            if with_devices:
                self.debug("Get devices")
                return self._get_devices(shell)

            return True

        except (ConnectionRefusedError, socket.timeout):
            return False

        except Exception as e:
            _LOGGER.debug(f"Can't read devices: {e}")
            return False

    def _mqtt_connect(self) -> bool:
        try:
            self.mqtt.reconnect()
            return True
        except:
            return False

    def _miio_connect(self) -> bool:
        if not self.miio.ping():
            self.debug("Can't send handshake")
            return False

        return True

    def _get_devices(self, shell: TelnetShell):
        """Load devices info for Coordinator, Zigbee and Mesh."""

        # 1. Read coordinator info
        raw = shell.read_file('/data/zigbee/coordinator.info')
        device = json.loads(raw)
        devices = [{
            'did': 'lumi.0',
            'model': 'lumi.gateway.mgl03',
            'mac': device['mac'],
            'type': 'gateway',
            'init': {
                'firmware lock': shell.check_firmware_lock()
            }
        }]

        # 2. Read zigbee devices
        if not self.zha:
            raw = shell.read_file('/data/zigbee_gw/' + self.ver_zigbee_db,
                                  as_base64=True)
            if raw.startswith(b'unqlite'):
                db = Unqlite(raw)
                data = db.read_all()
            else:
                raw = re.sub(br'}\s+{', b',', raw)
                data = json.loads(raw)

            # data = {} or data = {'dev_list': 'null'}
            dev_list = json.loads(data.get('dev_list', 'null')) or []

            for did in dev_list:
                model = data[did + '.model']
                desc = utils.get_device(model)

                # skip unknown model
                if desc is None:
                    self.debug(f"{did} has an unsupported modell: {model}")
                    continue

                retain = json.loads(data[did + '.prop'])['props']
                self.debug(f"{did} {model} retain: {retain}")

                params = {
                    p[2]: retain.get(p[1])
                    for p in (desc['params'] or desc['mi_spec'])
                    if p[1] is not None
                }

                device = {
                    'did': did,
                    'mac': '0x' + data[did + '.mac'],
                    'model': data[did + '.model'],
                    'type': 'zigbee',
                    'zb_ver': data[did + '.version'],
                    'init': utils.fix_xiaomi_props(params),
                    'online': retain.get('alive', 1) == 1
                }
                devices.append(device)

        # 3. Read bluetooth devices
        if self.ble:
            raw = shell.read_file('/data/miio/mible_local.db', as_base64=True)
            db = SQLite(raw)

            # load BLE devices
            rows = db.read_table('gateway_authed_table')
            for row in rows:
                device = {
                    'did': row[4],
                    'mac': RE_REVERSE.sub(r'\6\5\4\3\2\1', row[1]),
                    'model': row[2],
                    'type': 'ble'
                }
                devices.append(device)

            # load Mesh groups
            try:
                mesh_groups = {}

                rows = db.read_table(self.ver_mesh_group)
                for row in rows:
                    # don't know if 8 bytes enougth
                    mac = int(row[0]).to_bytes(8, 'big').hex()
                    device = {
                        'did': 'group.' + row[0],
                        'mac': mac,
                        'model': 0,
                        'childs': [],
                        'type': 'mesh'
                    }
                    group_addr = row[1]
                    mesh_groups[group_addr] = device

                # load Mesh bulbs
                rows = db.read_table('mesh_device')
                for row in rows:
                    device = {
                        'did': row[0],
                        'mac': row[1].replace(':', ''),
                        'model': row[2],
                        'type': 'mesh'
                    }
                    devices.append(device)

                    group_addr = row[5]
                    if group_addr in mesh_groups:
                        # add bulb to group if exist
                        mesh_groups[group_addr]['childs'].append(row[0])

                for device in mesh_groups.values():
                    if device['childs']:
                        devices.append(device)

            except:
                _LOGGER.exception("Can't read mesh devices")

        # for testing purposes
        for k, v in self.default_devices.items():
            if k[0] == '_':
                devices.append(v)

        return devices

    def lock_firmware(self, enable: bool):
        self.debug(f"Set firmware lock to {enable}")
        try:
            shell = TelnetShell(self.host)
            if "Received" in shell.check_or_download_busybox():
                self.debug("Download busybox")
            shell.lock_firmware(enable)
            locked = shell.check_firmware_lock()
            shell.close()
            return enable == locked

        except Exception as e:
            self.debug(f"Can't set firmware lock: {e}")
            return False

    def on_connect(self, client, userdata, flags, rc):
        self.debug("MQTT connected")
        self.mqtt.subscribe('#')

        self.process_gw_message({'online': True})

    def on_disconnect(self, client, userdata, rc):
        self.debug("MQTT disconnected")
        # force end mqtt.loop_forever()
        self.mqtt.disconnect()

        self.process_gw_message({'online': False})

    def on_message(self, client: Client, userdata, msg: MQTTMessage):
        if 'mqtt' in self._debug:
            self.debug(f"[MQ] {msg.topic} {msg.payload.decode()}")

        if msg.topic == 'zigbee/send':
            payload = json.loads(msg.payload)
            self.process_message(payload)

        elif msg.topic == 'log/miio':
            if 'miio' in self._debug:
                _LOGGER.debug(f"[MI] {msg.payload}")

            if self.ble and (b'_async.ble_event' in msg.payload
                             or b'properties_changed' in msg.payload):
                try:
                    for raw in utils.extract_jsons(msg.payload):
                        if b'_async.ble_event' in raw:
                            self.process_ble_event(raw)
                        elif b'properties_changed' in raw:
                            self.process_mesh_data(raw)
                except:
                    _LOGGER.warning(f"Can't read BT: {msg.payload}")

        elif msg.topic.endswith('/heartbeat'):
            payload = json.loads(msg.payload)
            self.process_gw_message(payload)

        elif msg.topic.endswith(('/MessageReceived', '/devicestatechange')):
            payload = json.loads(msg.payload)
            self.process_zb_message(payload)

        # read only retained ble
        elif msg.topic.startswith('ble') and msg.retain:
            payload = json.loads(msg.payload)
            self.process_ble_retain(msg.topic[4:], payload)

        elif self.pair_model and msg.topic.endswith('/commands'):
            self.process_pair(msg.payload)

    def setup_devices(self, devices: list):
        """Add devices to hass."""
        for device in devices:
            if device['type'] in ('gateway', 'zigbee'):
                desc = utils.get_device(device['model'])
                if not desc:
                    self.debug(f"Unsupported model: {device}")
                    continue

                self.debug(f"Setup Zigbee device {device}")

                device.update(desc)

                # update params from config
                default_config = self.default_devices.get(device['mac']) or \
                                 self.default_devices.get(device['did'])
                if default_config:
                    device.update(default_config)

                self.devices[device['did']] = device

                for param in (device['params'] or device['mi_spec']):
                    domain = param[3]
                    if not domain:
                        continue

                    # wait domain init
                    while domain not in self.setups:
                        time.sleep(1)

                    attr = param[2]
                    self.setups[domain](self, device, attr)

                if self._zigbee_info and device['type'] != 'gateway':
                    self.setups['sensor'](self, device, self._zigbee_info)

            elif device['type'] == 'mesh':
                desc = bluetooth.get_device(device['model'], 'Mesh')
                device.update(desc)

                self.debug(f"Setup Mesh device {device}")

                # update params from config
                default_config = self.default_devices.get(device['did'])
                if default_config:
                    device.update(default_config)

                device['online'] = False

                self.devices[device['did']] = device

                # wait domain init
                while 'light' not in self.setups:
                    time.sleep(1)

                self.setups['light'](self, device, 'light')

            elif device['type'] == 'ble':
                # only save info for future
                desc = bluetooth.get_device(device['model'], 'BLE')
                device.update(desc)

                # update params from config
                default_config = self.default_devices.get(device['did'])
                if default_config:
                    device.update(default_config)

                self.devices[device['did']] = device

                device['init'] = {}

    def process_message(self, data: dict):
        if data['cmd'] == 'heartbeat':
            # don't know if only one item
            assert len(data['params']) == 1, data

            data = data['params'][0]
            pkey = 'res_list'
        elif data['cmd'] == 'report':
            pkey = 'params' if 'params' in data else 'mi_spec'
        elif data['cmd'] in ('write_rsp', 'read_rsp'):
            pkey = 'results'
        elif data['cmd'] == 'write_ack':
            return
        else:
            _LOGGER.warning(f"Unsupported cmd: {data}")
            return

        did = data['did']

        # skip without callback
        if did not in self.updates:
            return

        device = self.devices[did]
        payload = {}

        # convert codes to names
        for param in data[pkey]:
            if param.get('error_code', 0) != 0:
                continue

            prop = param['res_name'] if 'res_name' in param else \
                f"{param['siid']}.{param['piid']}"

            if prop in GLOBAL_PROP:
                prop = GLOBAL_PROP[prop]
            else:
                prop = next((p[2]
                             for p in (device['params'] or device['mi_spec'])
                             if p[0] == prop), prop)

            if prop in ('temperature', 'humidity', 'pressure'):
                payload[prop] = param['value'] / 100.0
            elif prop == 'battery' and param['value'] > 1000:
                # xiaomi light sensor
                payload[prop] = round((min(param['value'], 3200) - 2500) / 7)
            elif prop == 'alive':
                # {'res_name':'8.0.2102','value':{'status':'online','time':0}}
                device['online'] = (param['value']['status'] == 'online')
            elif prop == 'angle':
                # xiaomi cube 100 points = 360 degrees
                payload[prop] = param['value'] * 4
            elif prop == 'duration':
                # xiaomi cube
                payload[prop] = param['value'] / 1000.0
            elif prop in ('consumption', 'power'):
                payload[prop] = round(param['value'], 2)
            else:
                payload[prop] = param['value']

        self.debug(f"{device['did']} {device['model']} <= {payload}")

        for handler in self.updates[did]:
            handler(payload)

        if 'added_device' in payload:
            # {'did': 'lumi.fff', 'mac': 'fff', 'model': 'lumi.sen_ill.mgl01',
            # 'version': '21', 'zb_ver': '3.0'}
            device = payload['added_device']
            device['mac'] = '0x' + device['mac']
            device['type'] = 'zigbee'
            device['init'] = payload
            self.setup_devices([device])

    def process_gw_message(self, payload: dict):
        self.debug(f"gateway <= {payload}")

        if 'lumi.0' not in self.updates:
            return

        if 'networkUp' in payload:
            # {"networkUp":false}
            if not payload['networkUp']:
                _LOGGER.warning("Network down")
                return

            payload = {
                'network_pan_id': payload['networkPanId'],
                'radio_tx_power': payload['radioTxPower'],
                'radio_channel': payload['radioChannel'],
            }
        elif 'online' in payload:
            self.device['online'] = payload['online']

        for handler in self.updates['lumi.0']:
            handler(payload)

    def process_ble_event(self, raw: Union[bytes, str]):
        if isinstance(raw, bytes):
            data = json.loads(raw)['params']
        else:
            data = json.loads(raw)

        self.debug(f"Process BLE {data}")

        pdid = data['dev'].get('pdid')

        did = data['dev']['did']
        if did not in self.devices:
            mac = data['dev']['mac'].replace(':', '').lower() \
                if 'mac' in data['dev'] else \
                'ble_' + did.replace('blt.3.', '')
            self.devices[did] = device = {
                'did': did,
                'mac': mac,
                'init': {},
                'type': 'bluetooth'
            }
            desc = bluetooth.get_device(pdid, 'BLE')
            device.update(desc)

            # update params from config
            default_config = self.default_devices.get(did)
            if default_config:
                device.update(default_config)

        else:
            device = self.devices[did]

        if isinstance(data['evt'], list):
            # check if only one
            assert len(data['evt']) == 1, data
            payload = bluetooth.parse_xiaomi_ble(data['evt'][0], pdid)
        elif isinstance(data['evt'], dict):
            payload = bluetooth.parse_xiaomi_ble(data['evt'], pdid)
        else:
            payload = None

        if payload is None:
            self.debug(f"Unsupported BLE {data}")
            return

        # init entities if needed
        init = device['init']
        for k in payload.keys():
            if k in init:
                # update for retain
                init[k] = payload[k]
                continue

            init[k] = payload[k]

            domain = bluetooth.get_ble_domain(k)
            if not domain:
                continue

            # wait domain init
            while domain not in self.setups:
                time.sleep(1)

            self.setups[domain](self, device, k)

        if did in self.updates:
            for handler in self.updates[did]:
                handler(payload)

        raw = json.dumps(init, separators=(',', ':'))
        self.mqtt.publish(f"ble/{did}", raw, retain=True)

    def process_ble_retain(self, did: str, payload: dict):
        if did not in self.devices:
            _LOGGER.debug(f"BLE device {did} is no longer on the gateway")
            return

        _LOGGER.debug(f"{did} retain: {payload}")

        device = self.devices[did]

        # init entities if needed
        for k in payload.keys():
            # don't retain action
            if k in device['init'] or k == 'action':
                continue

            device['init'][k] = payload[k]

            domain = bluetooth.get_ble_domain(k)
            if not domain:
                continue

            # wait domain init
            while domain not in self.setups:
                time.sleep(1)

            self.setups[domain](self, device, k)

        if did in self.updates:
            for handler in self.updates[did]:
                handler(payload)

    def process_pair(self, raw: bytes):
        # get shortID and eui64 of paired device
        if b'lumi send-nwk-key' in raw:
            # create model response
            payload = f"0x18010105000042{len(self.pair_model):02x}" \
                      f"{self.pair_model.encode().hex()}"
            m = RE_NWK_KEY.search(raw.decode())
            self.pair_payload = json.dumps(
                {
                    'sourceAddress': m[1],
                    'eui64': '0x' + m[2],
                    'profileId': '0x0104',
                    'clusterId': '0x0000',
                    'sourceEndpoint': '0x01',
                    'destinationEndpoint': '0x01',
                    'APSCounter': '0x01',
                    'APSPlayload': payload
                },
                separators=(',', ':'))

        # send model response "from device"
        elif b'zdo active ' in raw:
            mac = self.device['mac'][2:].upper()
            self.mqtt.publish(f"gw/{mac}/MessageReceived", self.pair_payload)

    def send(self, device: dict, data: dict):
        payload = {'cmd': 'write', 'did': device['did']}

        # convert hass prop to lumi prop
        if device['mi_spec']:
            params = []
            for k, v in data.items():
                if k == 'switch':
                    v = bool(v)
                k = next(p[0] for p in device['mi_spec'] if p[2] == k)
                params.append({'siid': k[0], 'piid': k[1], 'value': v})

            payload['mi_spec'] = params
        else:
            params = [{
                'res_name':
                next(p[0] for p in device['params'] if p[2] == k),
                'value':
                v
            } for k, v in data.items()]

            payload = {
                'cmd': 'write',
                'did': device['did'],
                'params': params,
            }

        self.debug(f"{device['did']} {device['model']} => {payload}")
        payload = json.dumps(payload, separators=(',', ':')).encode()
        self.mqtt.publish('zigbee/recv', payload)

    def send_telnet(self, *args: str):
        try:
            shell = TelnetShell(self.host)
            for command in args:
                if command == 'ftp':
                    shell.check_or_download_busybox()
                    shell.run_ftp()
                else:
                    shell.exec(command)
            shell.close()

        except Exception as e:
            _LOGGER.exception(f"Telnet command error: {e}")

    def send_mqtt(self, cmd: str):
        if cmd == 'publishstate':
            mac = self.device['mac'][2:].upper()
            self.mqtt.publish(f"gw/{mac}/publishstate")

    def get_device(self, mac: str) -> Optional[dict]:
        for device in self.devices.values():
            if device.get('mac') == mac:
                return device
        return None
Пример #22
0
class Gateway3(Thread):
    pair_model = None
    pair_payload = None

    def __init__(self, host: str, token: str, config: dict, zha: bool = False):
        super().__init__(daemon=True)

        self.host = host
        self.zha = zha

        self.miio = Device(host, token)

        self.mqtt = Client()
        self.mqtt.on_connect = self.on_connect
        self.mqtt.on_disconnect = self.on_disconnect
        self.mqtt.on_message = self.on_message
        self.mqtt.connect_async(host)

        self.ble = GatewayBLE(self)

        self.debug = config['debug'] if 'debug' in config else ''
        self.devices = config['devices'] if 'devices' in config else {}
        self.updates = {}
        self.setups = {}

    @property
    def device(self):
        return self.devices['lumi.0']

    def add_update(self, did: str, handler):
        """Add handler to device update event."""
        self.updates.setdefault(did, []).append(handler)

    def add_setup(self, domain: str, handler):
        """Add hass device setup funcion."""
        self.setups[domain] = handler

    def run(self):
        """Main loop"""
        while True:
            # if not telnet - enable it
            if not self._check_port(23) and not self._enable_telnet():
                time.sleep(30)
                continue

            devices = self._get_devices_v3()
            if devices:
                self.setup_devices(devices)
                break

        # start bluetooth read loop
        self.ble.start()

        while True:
            # if not telnet - enable it
            if not self._check_port(23) and not self._enable_telnet():
                time.sleep(30)
                continue

            if not self.zha:
                # if not mqtt - enable it
                if not self._mqtt_connect() and not self._enable_mqtt():
                    time.sleep(60)
                    continue

                self.mqtt.loop_forever()

            elif not self._check_port(8888) and not self._enable_zha():
                time.sleep(60)
                continue

            else:
                # ZHA works fine, check every 60 seconds
                time.sleep(60)

    def _check_port(self, port: int):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            return s.connect_ex((self.host, port)) == 0
        finally:
            s.close()

    def _mqtt_connect(self) -> bool:
        try:
            self.mqtt.reconnect()
            return True
        except:
            return False

    def _miio_connect(self) -> bool:
        try:
            self.miio.send_handshake()
            return True
        except:
            _LOGGER.debug(f"{self.host} | Can't send handshake")
            return False

    def _get_devices_v1(self) -> Optional[list]:
        """Load devices via miio protocol."""
        _LOGGER.debug(f"{self.host} | Read devices")
        try:
            devices = {}

            # endless loop protection
            for _ in range(16):
                # load only 8 device per part
                part = self.miio.send('get_device_list', retry_count=10)
                if len(part) == 0:
                    return []

                for item in part:
                    devices[item['num']] = {
                        'did': item['did'],
                        'mac': f"0x{item['did'][5:]}",
                        'model': item['model'],
                    }

                if part[0]['total'] == len(devices):
                    break

            devices = list(devices.values())
            for device in devices:
                desc = utils.get_device(device['model'])
                # skip unknown model
                if desc is None:
                    continue
                # get xiaomi param names
                params = [p[1] for p in desc['params'] if p[1] is not None]
                # skip if don't have retain params
                if not params:
                    continue
                # load param values
                values = self.miio.send('get_device_prop',
                                        [device['did']] + params)
                # get hass param names
                params = [p[2] for p in desc['params'] if p[1] is not None]

                data = dict(zip(params, values))
                # fix some param values
                for k, v in data.items():
                    if k in ('temperature', 'humidity'):
                        data[k] = v / 100.0
                    elif v in ('on', 'open'):
                        data[k] = 1
                    elif v in ('off', 'close'):
                        data[k] = 0

                device['init'] = data

            device = self.miio.info()
            devices.append({
                'did': 'lumi.0',
                'mac': device.mac_address,  # wifi mac!!!
                'model': device.model
            })

            return devices

        except Exception as e:
            _LOGGER.exception(f"{self.host} | Get devices: {e}")
            return None

    def _get_devices_v2(self) -> Optional[list]:
        """Load device list via Telnet.

        Device desc example:
          mac: '0x158d0002c81234'
          shortId: '0x0691'
          manuCode: '0x115f'
          model: 'lumi.sensor_ht'
          did: 'lumi.158d0002c81234'
          devType: 0
          appVer: 2
          hardVer: 0
          devID: 770
          status: 0
          model_ver: 2
        """
        _LOGGER.debug(f"{self.host} | Read devices")
        try:
            telnet = Telnet(self.host)
            telnet.read_until(b"login: "******"admin\r\n")
            telnet.read_until(b'\r\n# ')  # skip greeting

            telnet.write(b"cat /data/zigbee/coordinator.info\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')
            device = json.loads(raw[:-2])
            device.update({
                'did': 'lumi.0',
                'model': 'lumi.gateway.mgl03',
                'host': self.host
            })

            devices = [device]

            telnet.write(b"cat /data/zigbee/device.info\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')
            raw = json.loads(raw[:-2])
            devices += raw['devInfo']
            telnet.close()

            return devices
        except Exception as e:
            _LOGGER.exception(f"Can't read devices: {e}")
            return None

    def _get_devices_v3(self):
        """Load device list via Telnet."""
        _LOGGER.debug(f"{self.host} | Read devices")
        try:
            telnet = Telnet(self.host, timeout=5)
            telnet.read_until(b"login: "******"admin\r\n")
            telnet.read_until(b'\r\n# ')  # skip greeting

            # read coordinator info
            telnet.write(b"cat /data/zigbee/coordinator.info\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')

            device = json.loads(raw[:-2])
            devices = [{
                'did': 'lumi.0',
                'model': 'lumi.gateway.mgl03',
                'mac': device['mac'],
                'type': 'gateway'
            }]

            if self.zha:
                return devices

            # https://github.com/AlexxIT/XiaomiGateway3/issues/14
            # fw 1.4.6_0012 and below have one zigbee_gw.db file
            # fw 1.4.6_0030 have many json files in this folder
            telnet.write(b"cat /data/zigbee_gw/* | base64\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')
            raw = base64.b64decode(raw)
            if raw.startswith(b'unqlite'):
                db = Unqlite(raw)
                data = db.read_all()
            else:
                raw = re.sub(br'}\s+{', b',', raw)
                data = json.loads(raw)

            # data = {} or data = {'dev_list': 'null'}
            dev_list = json.loads(data.get('dev_list', 'null')) or []
            _LOGGER.debug(f"{self.host} | Load {len(dev_list)} zigbee devices")

            for did in dev_list:
                model = data[did + '.model']
                desc = utils.get_device(model)

                # skip unknown model
                if desc is None:
                    _LOGGER.debug(f"{did} has an unsupported modell: {model}")
                    continue

                retain = json.loads(data[did + '.prop'])['props']
                _LOGGER.debug(f"{self.host} | {did} {model} retain: {retain}")

                params = {
                    p[2]: retain.get(p[1])
                    for p in desc['params'] if p[1] is not None
                }

                device = {
                    'did': did,
                    'mac': '0x' + data[did + '.mac'],
                    'model': data[did + '.model'],
                    'type': 'zigbee',
                    'zb_ver': data[did + '.version'],
                    'init': utils.fix_xiaomi_props(params)
                }
                devices.append(device)

            return devices

        except (ConnectionRefusedError, socket.timeout):
            return None

        except Exception as e:
            _LOGGER.debug(f"Can't read devices: {e}")
            return None

    def _enable_telnet(self):
        _LOGGER.debug(f"{self.host} | Try enable telnet")
        try:
            resp = self.miio.send("enable_telnet_service")
            return resp[0] == 'ok'
        except Exception as e:
            _LOGGER.exception(f"Can't enable telnet: {e}")
            return False

    def _enable_mqtt(self):
        _LOGGER.debug(f"{self.host} | Try run public MQTT")
        try:
            telnet = Telnet(self.host)
            telnet.read_until(b"login: "******"admin\r\n")
            telnet.read_until(b"\r\n# ")  # skip greeting

            # enable public mqtt
            telnet.write(b"killall mosquitto\r\n")
            telnet.read_until(b"\r\n")  # skip command
            time.sleep(.5)  # it's important to wait
            telnet.write(b"mosquitto -d\r\n")
            telnet.read_until(b"\r\n")  # skip command
            time.sleep(.5)  # it's important to wait

            # fix CPU 90% full time bug
            telnet.write(b"killall zigbee_gw\r\n")
            telnet.read_until(b"\r\n")  # skip command
            time.sleep(.5)  # it's important to wait

            telnet.close()
            return True
        except Exception as e:
            _LOGGER.debug(f"Can't run MQTT: {e}")
            return False

    def _enable_zha(self):
        _LOGGER.debug(f"{self.host} | Try enable ZHA")
        try:
            check_socat = \
                "(md5sum /data/socat | grep 92b77e1a93c4f4377b4b751a5390d979)"
            download_socat = \
                "(curl -o /data/socat http://pkg.musl.cc/socat/" \
                "mipsel-linux-musln32/bin/socat && chmod +x /data/socat)"
            run_socat = "/data/socat tcp-l:8888,reuseaddr,fork /dev/ttyS2"

            telnet = Telnet(self.host, timeout=5)
            telnet.read_until(b"login: "******"admin\r\n")
            telnet.read_until(b"\r\n# ")  # skip greeting

            # download socat and check md5
            telnet.write(f"{check_socat} || {download_socat}\r\n".encode())
            raw = telnet.read_until(b"\r\n# ")
            if b"Received" in raw:
                _LOGGER.debug(f"{self.host} | Downloading socat")

            telnet.write(f"{check_socat} && {run_socat} &\r\n".encode())
            telnet.read_until(b"\r\n# ")

            telnet.write(
                b"killall daemon_app.sh; killall Lumi_Z3GatewayHost_MQTT\r\n")
            telnet.read_until(b"\r\n# ")

            telnet.close()
            return True

        except Exception as e:
            _LOGGER.debug(f"Can't enable ZHA: {e}")
            return False

    def on_connect(self, client, userdata, flags, rc):
        _LOGGER.debug(f"{self.host} | MQTT connected")
        self.mqtt.subscribe('#')

    def on_disconnect(self, client, userdata, rc):
        _LOGGER.debug(f"{self.host} | MQTT disconnected")
        # force end mqtt.loop_forever()
        self.mqtt.disconnect()

    def on_message(self, client: Client, userdata, msg: MQTTMessage):
        if 'mqtt' in self.debug:
            _LOGGER.debug(f"[MQ] {msg.topic} {msg.payload.decode()}")

        if msg.topic == 'zigbee/send':
            payload = json.loads(msg.payload)
            self.process_message(payload)
        elif self.pair_model and msg.topic.endswith('/commands'):
            self.process_pair(msg.payload)

    def setup_devices(self, devices: list):
        """Add devices to hass."""
        for device in devices:
            desc = utils.get_device(device['model'])
            if not desc:
                _LOGGER.debug(f"Unsupported model: {device}")
                continue

            _LOGGER.debug(f"{self.host} | Setup device {device['model']}")

            device.update(desc)

            # update params from config
            default_config = self.devices.get(device['mac'])
            if default_config:
                device.update(default_config)

            self.devices[device['did']] = device

            for param in device['params']:
                domain = param[3]
                if not domain:
                    continue

                # wait domain init
                while domain not in self.setups:
                    time.sleep(1)

                attr = param[2]
                self.setups[domain](self, device, attr)

    def process_message(self, data: dict):
        if data['cmd'] == 'heartbeat':
            # don't know if only one item
            assert len(data['params']) == 1, data

            data = data['params'][0]
            pkey = 'res_list'
        elif data['cmd'] == 'report':
            pkey = 'params' if 'params' in data else 'mi_spec'
        elif data['cmd'] == 'write_rsp':
            pkey = 'results'
        else:
            raise NotImplemented(f"Unsupported cmd: {data}")

        did = data['did']

        # skip without callback
        if did not in self.updates:
            return

        device = self.devices[did]
        payload = {}

        # convert codes to names
        for param in data[pkey]:
            if param.get('error_code', 0) != 0:
                continue

            prop = param['res_name'] if 'res_name' in param else \
                f"{param['siid']}.{param['piid']}"

            if prop in GLOBAL_PROP:
                prop = GLOBAL_PROP[prop]
            else:
                prop = next((p[2] for p in device['params'] if p[0] == prop),
                            prop)

            if prop in ('temperature', 'humidity', 'pressure'):
                payload[prop] = param['value'] / 100.0
            elif prop == 'battery' and param['value'] > 1000:
                # xiaomi light sensor
                payload[prop] = round((min(param['value'], 3200) - 2500) / 7)
            elif prop == 'angle':
                # xiaomi cube 100 points = 360 degrees
                payload[prop] = param['value'] * 4
            elif prop == 'duration':
                # xiaomi cube
                payload[prop] = param['value'] / 1000.0
            else:
                payload[prop] = param['value']

        _LOGGER.debug(f"{self.host} | {device['did']} {device['model']} <= "
                      f"{payload}")

        for handler in self.updates[did]:
            handler(payload)

        if 'added_device' in payload:
            # {'did': 'lumi.fff', 'mac': 'fff', 'model': 'lumi.sen_ill.mgl01',
            # 'version': '21', 'zb_ver': '3.0'}
            device = payload['added_device']
            device['mac'] = '0x' + device['mac']
            device['type'] = 'zigbee'
            device['init'] = payload
            self.setup_devices([device])

    def process_pair(self, raw: bytes):
        # get shortID and eui64 of paired device
        if b'lumi send-nwk-key' in raw:
            # create model response
            payload = f"0x18010105000042{len(self.pair_model):02x}" \
                      f"{self.pair_model.encode().hex()}"
            m = RE_NWK_KEY.search(raw.decode())
            self.pair_payload = json.dumps(
                {
                    'sourceAddress': m[1],
                    'eui64': '0x' + m[2],
                    'profileId': '0x0104',
                    'clusterId': '0x0000',
                    'sourceEndpoint': '0x01',
                    'destinationEndpoint': '0x01',
                    'APSCounter': '0x01',
                    'APSPlayload': payload
                },
                separators=(',', ':'))

        # send model response "from device"
        elif b'zdo active ' in raw:
            mac = self.device['mac'][2:].upper()
            self.mqtt.publish(f"gw/{mac}/MessageReceived", self.pair_payload)

    def process_ble_event(self, raw: Union[bytes, str]):
        data = json.loads(raw[10:])['params'] \
            if isinstance(raw, bytes) else json.loads(raw)

        _LOGGER.debug(f"{self.host} | Process BLE {data}")

        did = data['dev']['did']
        if did not in self.devices:
            mac = data['dev']['mac'].replace(':', '').lower() \
                if 'mac' in data['dev'] else \
                'ble_' + did.replace('blt.3.', '')
            self.devices[did] = device = {
                'did': did,
                'mac': mac,
                'init': {},
                'device_name': "BLE",
                'type': 'ble'
            }
        else:
            device = self.devices[did]

        if isinstance(data['evt'], list):
            # check if only one
            assert len(data['evt']) == 1, data
            payload = ble.parse_xiaomi_ble(data['evt'][0])
        elif isinstance(data['evt'], dict):
            payload = ble.parse_xiaomi_ble(data['evt'])
        else:
            payload = None

        if payload is None:
            _LOGGER.debug(f"Unsupported BLE {data}")
            return

        # init entities if needed
        for k in payload.keys():
            if k in device['init']:
                continue

            device['init'][k] = payload[k]

            domain = ble.get_ble_domain(k)
            if not domain:
                continue

            # wait domain init
            while domain not in self.setups:
                time.sleep(1)

            self.setups[domain](self, device, k)

        if did in self.updates:
            for handler in self.updates[did]:
                handler(payload)

    def send(self, device: dict, data: dict):
        # convert hass prop to lumi prop
        params = [{
            'res_name': next(p[0] for p in device['params'] if p[2] == k),
            'value': v
        } for k, v in data.items()]

        payload = {
            'cmd': 'write',
            'did': device['did'],
            'params': params,
        }

        _LOGGER.debug(f"{self.host} | {device['did']} {device['model']} => "
                      f"{payload}")

        payload = json.dumps(payload, separators=(',', ':')).encode()
        self.mqtt.publish('zigbee/recv', payload)

    def get_device(self, mac: str) -> Optional[dict]:
        for device in self.devices.values():
            if device.get('mac') == mac:
                return device
        return None
Пример #23
0
class GatewayEntry(Thread, GatewayStats):
    """Main class for working with the gateway via Telnet (23), MQTT (1883) and
    miIO (54321) protocols.
    """
    time_offset = 0
    pair_model = None
    pair_payload = None
    pair_payload2 = None
    telnet_cmd = None

    def __init__(self, host: str, token: str, config: dict, **options):
        super().__init__(daemon=True, name=f"{host}_main")

        self.host = host
        self.options = options

        self.miio = SyncmiIO(host, token)

        self.mqtt = Client()
        self.mqtt.on_connect = self.on_connect
        self.mqtt.on_disconnect = self.on_disconnect
        self.mqtt.on_message = self.on_message

        self._ble = options.get('ble', True)  # for fast access
        self._debug = options.get('debug', '')  # for fast access
        self.parent_scan_interval = options.get('parent', -1)
        self.default_devices = config['devices'] if config else None

        self.telnet_cmd = options.get('telnet_cmd') or TELNET_CMD

        if 'true' in self._debug:
            self.miio.debug = True

        self.setups = {}
        self.stats = {}

    @property
    def device(self):
        return self.devices[self.did]

    def debug(self, message: str):
        # basic logs
        if 'true' in self._debug:
            _LOGGER.debug(f"{self.host} | {message}")

    def stop(self, *args):
        self.enabled = False
        self.mqtt.loop_stop()

        for device in self.devices.values():
            if self in device['gateways']:
                device['gateways'].remove(self)

    def run(self):
        """Main thread loop."""
        self.debug("Start main thread")

        self.mqtt.connect_async(self.host)

        self.enabled = True
        while self.enabled:
            # if not telnet - enable it
            if not self._check_port(23) and not self._enable_telnet():
                time.sleep(30)
                continue

            if not self.did:
                devices = self._get_devices()
                if not devices:
                    time.sleep(60)
                    continue

                self.setup_devices(devices)
                self.update_time_offset()
                self.mesh_start()

            # if not mqtt - enable it (handle Mi Home and ZHA mode)
            if not self._prepare_gateway() or not self._mqtt_connect():
                time.sleep(60)
                continue

            self.mqtt.loop_forever()

        self.debug("Stop main thread")

    def update_time_offset(self):
        gw_time = ntp_time(self.host)
        if gw_time:
            self.time_offset = gw_time - time.time()
            self.debug(f"Gateway time offset: {self.time_offset}")

    def _check_port(self, port: int):
        """Check if gateway port open."""
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            return s.connect_ex((self.host, port)) == 0
        finally:
            s.close()

    def _enable_telnet(self):
        """Enable telnet with miio protocol."""
        raw = json.loads(self.telnet_cmd)
        if self.miio.send(raw['method'], raw.get('params')) != ['ok']:
            self.debug(f"Can't enable telnet")
            return False
        return True

    def _prepare_gateway(self):
        """Launching the required utilities on the hub, if they are not already
        running.
        """
        self.debug("Prepare Gateway")
        try:
            shell = TelnetShell(self.host)
            self.debug(f"Version: {shell.ver}")

            ps = shell.get_running_ps()

            if "mosquitto -d" not in ps:
                self.debug("Run public mosquitto")
                shell.run_public_mosquitto()

            if "ntpd" not in ps:
                # run NTPd for sync time
                shell.run_ntpd()

            bt_fix = shell.check_bt()
            if bt_fix is None:
                self.debug("Fixed BT don't supported")

            elif bt_fix is False:
                self.debug("Download fixed BT")
                shell.download_bt()

                # check after download
                if shell.check_bt():
                    self.debug("Run fixed BT")
                    shell.run_bt()

            elif "-t log/ble" not in ps:
                self.debug("Run fixed BT")
                shell.run_bt()

            if "-t log/miio" not in ps:
                # all data or only necessary events
                pattern = ('\\{"' if 'miio' in self._debug else
                           "ot_agent_recv_handler_one.+"
                           "ble_event|properties_changed|heartbeat")
                self.debug(f"Redirect miio to MQTT")
                shell.redirect_miio2mqtt(pattern)

            if self.options.get('buzzer'):
                if "dummy:basic_gw" not in ps:
                    self.debug("Disable buzzer")
                    shell.stop_buzzer()
            else:
                if "dummy:basic_gw" in ps:
                    self.debug("Enable buzzer")
                    shell.run_buzzer()

            if self.options.get('zha'):
                if "Lumi_Z3GatewayHost_MQTT" in ps:
                    self.debug("Stop Lumi Zigbee")
                    shell.stop_lumi_zigbee()

                if "tcp-l:8888" not in ps:
                    if "Received" in shell.check_or_download_socat():
                        self.debug("Download socat")
                    self.debug("Run Zigbee TCP")
                    shell.run_zigbee_tcp()

            else:
                if "tcp-l:8888" in ps:
                    self.debug("Stop Zigbee TCP")
                    shell.stop_zigbee_tcp()

                if (self.parent_scan_interval >= 0 and
                        "Lumi_Z3GatewayHost_MQTT -n 1 -b 115200 -l" not in ps):
                    self.debug("Run public Zigbee console")
                    shell.run_public_zb_console()

                elif "Lumi_Z3GatewayHost_MQTT" not in ps:
                    self.debug("Run Lumi Zigbee")
                    shell.run_lumi_zigbee()

            shell.close()

            return True

        except (ConnectionRefusedError, socket.timeout):
            return False

        except Exception as e:
            self.debug(f"Can't prepare gateway: {e}")
            return False

    def _mqtt_connect(self) -> bool:
        try:
            self.mqtt.reconnect()
            return True
        except:
            return False

    def _get_devices(self):
        """Load devices info for Coordinator, Zigbee and Mesh."""
        try:
            shell = TelnetShell(self.host)

            # 1. Read coordinator info
            raw = shell.read_file('/data/zigbee/coordinator.info')
            device = json.loads(raw)
            devices = [{
                'did': shell.get_did(),
                'model': 'lumi.gateway.mgl03',
                'mac': device['mac'],
                'wlan_mac': shell.get_wlan_mac(),
                'type': 'gateway',
                'fw_ver': shell.ver,
                'online': True,
                'init': {
                    'firmware lock': shell.check_firmware_lock(),
                }
            }]

            # 2. Read zigbee devices
            if not self.options.get('zha'):
                # read Silicon devices DB
                nwks = {}
                try:
                    raw = shell.read_file(
                        '/data/silicon_zigbee_host/devices.txt')
                    raw = raw.decode().split(' ')
                    for i in range(0, len(raw) - 1, 32):
                        ieee = reversed(raw[i + 3:i + 11])
                        ieee = ''.join(f"{i:>02s}" for i in ieee)
                        nwks[ieee] = f"{raw[i]:>04s}"
                except:
                    _LOGGER.exception("Can't read Silicon devices DB")

                # read Xiaomi devices DB
                raw = shell.read_file(shell.zigbee_db, as_base64=True)
                # self.debug(f"Devices RAW: {raw}")
                if raw.startswith(b'unqlite'):
                    db = Unqlite(raw)
                    data = db.read_all()
                else:
                    raw = re.sub(br'}\s*{', b',', raw)
                    data = json.loads(raw)

                # data = {} or data = {'dev_list': 'null'}
                dev_list = json.loads(data.get('dev_list', 'null')) or []

                for did in dev_list:
                    model = data.get(did + '.model')
                    if not model:
                        self.debug(f"{did} has not in devices DB")
                        continue
                    desc = zigbee.get_device(model)

                    # skip unknown model
                    if desc is None:
                        self.debug(f"{did} has an unsupported modell: {model}")
                        continue

                    retain = json.loads(data[did + '.prop'])['props']
                    self.debug(f"{did} {model} retain: {retain}")

                    params = {
                        p[2]: retain.get(p[1])
                        for p in (desc['params'] or desc['mi_spec'])
                        if p[1] is not None
                    }

                    ieee = f"{data[did + '.mac']:>016s}"
                    device = {
                        'did': did,
                        'mac': '0x' + data[did + '.mac'],
                        'ieee': ieee,
                        'nwk': nwks.get(ieee),
                        'model': model,
                        'type': 'zigbee',
                        'fw_ver': retain.get('fw_ver'),
                        'init': zigbee.fix_xiaomi_props(model, params),
                        'online': retain.get('alive', 1) == 1
                    }
                    devices.append(device)

            # 3. Read bluetooth devices
            if self._ble:
                raw = shell.read_file('/data/miio/mible_local.db',
                                      as_base64=True)
                db = SQLite(raw)

                # load BLE devices
                rows = db.read_table('gateway_authed_table')
                for row in rows:
                    device = {
                        'did': row[4],
                        'mac': RE_REVERSE.sub(r'\6\5\4\3\2\1', row[1]),
                        'model': row[2],
                        'type': 'ble',
                        'online': True,
                        'init': {}
                    }
                    devices.append(device)

                # load Mesh groups
                try:
                    mesh_groups = {}

                    rows = db.read_table(shell.mesh_group_table)
                    for row in rows:
                        # don't know if 8 bytes enougth
                        mac = int(row[0]).to_bytes(8, 'big').hex()
                        device = {
                            'did': 'group.' + row[0],
                            'mac': mac,
                            'model': 0,
                            'childs': [],
                            'type': 'mesh',
                            'online': True
                        }
                        devices.append(device)

                        group_addr = row[1]
                        mesh_groups[group_addr] = device

                    # load Mesh bulbs
                    rows = db.read_table(shell.mesh_device_table)
                    for row in rows:
                        device = {
                            'did': row[0],
                            'mac': row[1].replace(':', ''),
                            'model': row[2],
                            'type': 'mesh',
                            'online': False
                        }
                        devices.append(device)

                        group_addr = row[5]
                        if group_addr in mesh_groups:
                            # add bulb to group if exist
                            mesh_groups[group_addr]['childs'].append(row[0])

                except:
                    _LOGGER.exception("Can't read mesh devices")

            # for testing purposes
            for k, v in self.default_devices.items():
                if k[0] == '_':
                    devices.append(v)

            return devices

        except (ConnectionRefusedError, socket.timeout):
            return None

        except Exception as e:
            _LOGGER.exception(f"{self.host} | Can't read devices: {e}")
            return None

    def lock_firmware(self, enable: bool):
        self.debug(f"Set firmware lock to {enable}")
        try:
            shell = TelnetShell(self.host)
            if "Received" in shell.check_or_download_busybox():
                self.debug("Download busybox")
            shell.lock_firmware(enable)
            locked = shell.check_firmware_lock()
            shell.close()
            return enable == locked

        except Exception as e:
            self.debug(f"Can't set firmware lock: {e}")
            return False

    def update_entities_states(self):
        for device in list(self.devices.values()):
            if self in device['gateways']:
                for entity in device['entities'].values():
                    if entity:
                        entity.schedule_update_ha_state()

    def on_connect(self, client, userdata, flags, rc):
        self.debug("MQTT connected")
        self.mqtt.subscribe('#')

        self.available = True
        self.process_gw_stats()
        self.update_entities_states()

    def on_disconnect(self, client, userdata, rc):
        self.debug("MQTT disconnected")
        # force end mqtt.loop_forever()
        self.mqtt.disconnect()

        self.available = False
        self.process_gw_stats()
        self.update_entities_states()

    def on_message(self, client: Client, userdata, msg: MQTTMessage):
        # for debug purpose
        enabled = self.enabled
        try:
            topic = msg.topic

            if 'mqtt' in self._debug:
                _LOGGER.debug(f"{self.host} | MQTT | {topic} {msg.payload}")

            if topic == 'zigbee/send':
                payload = json.loads(msg.payload)
                self.process_message(payload)

            elif topic == 'log/miio':
                # don't need to process another data
                if b'ot_agent_recv_handler_one' not in msg.payload:
                    return

                for raw in utils.extract_jsons(msg.payload):
                    if self._ble and b'_async.ble_event' in raw:
                        data = json.loads(raw)['params']
                        self.process_ble_event(data)
                        self.process_ble_stats(data)
                    elif self._ble and b'properties_changed' in raw:
                        data = json.loads(raw)['params']
                        self.debug(f"Process props {data}")
                        self.process_mesh_data(data)
                    elif b'event.gw.heartbeat' in raw:
                        payload = json.loads(raw)['params'][0]
                        self.process_gw_stats(payload)
                        # time offset may changed right after gw.heartbeat
                        self.update_time_offset()

            elif topic == 'log/ble':
                payload = json.loads(msg.payload)
                self.process_ble_event_fix(payload)
                self.process_ble_stats(payload)

            elif topic == 'log/z3':
                self.process_z3(msg.payload.decode())

            elif topic.endswith('/heartbeat'):
                payload = json.loads(msg.payload)
                self.process_gw_stats(payload)

            elif topic.endswith(('/MessageReceived', '/devicestatechange')):
                payload = json.loads(msg.payload)
                self.process_zb_stats(payload)

            # read only retained ble
            elif topic.startswith('ble') and msg.retain:
                payload = json.loads(msg.payload)
                self.process_ble_retain(topic[4:], payload)

            elif self.pair_model and topic.endswith('/commands'):
                self.process_pair(msg.payload)

        except:
            _LOGGER.exception(f"Processing MQTT: {msg.topic} {msg.payload}")

    def setup_devices(self, devices: list):
        """Add devices to hass."""
        for device in devices:
            did = device['did']
            type_ = device['type']

            if type_ == 'gateway':
                self.did = device['did']
                self.gw_topic = f"gw/{device['mac'][2:].upper()}/"

            # if device already exists - take it from registry
            if did not in self.devices:
                if type_ in ('gateway', 'zigbee'):
                    desc = zigbee.get_device(device['model'])
                elif type_ == 'mesh':
                    desc = bluetooth.get_device(device['model'], 'Mesh')
                elif type_ == 'ble':
                    desc = bluetooth.get_device(device['model'], 'BLE')
                else:
                    raise NotImplemented

                device.update(desc)

                # update params from config
                default_config = (self.default_devices.get(device['mac'])
                                  or self.default_devices.get(device['did']))
                if default_config:
                    device.update(default_config)

                self.debug(f"Setup {type_} device {device}")

                device['entities'] = {}
                device['gateways'] = []
                self.devices[did] = device

            else:
                device = self.devices[did]

            if type_ in ('gateway', 'zigbee', 'mesh'):
                for param in (device['params'] or device['mi_spec']):
                    self.add_entity(param[3], device, param[2])

            if self.options.get('stats') and type_ != 'mesh':
                self.add_entity('sensor', device, device['type'])

    def process_message(self, data: dict):
        if data['cmd'] == 'heartbeat':
            # don't know if only one item
            assert len(data['params']) == 1, data

            data = data['params'][0]
            pkey = 'res_list'
        elif data['cmd'] == 'report':
            pkey = 'params' if 'params' in data else 'mi_spec'
        elif data['cmd'] in ('write_rsp', 'read_rsp'):
            pkey = 'results' if 'results' in data else 'mi_spec'
        elif data['cmd'] == 'write_ack':
            return
        else:
            _LOGGER.warning(f"Unsupported cmd: {data}")
            return

        did = data['did'] if data['did'] != 'lumi.0' else self.did

        # skip without callback and without data
        if did not in self.devices or pkey not in data:
            return

        ts = time.time()

        device = self.devices[did]
        payload = {}

        # convert codes to names
        for param in data[pkey]:
            if param.get('error_code', 0) != 0:
                continue

            if 'res_name' in param:
                prop = param['res_name']
            elif 'piid' in param:
                prop = f"{param['siid']}.{param['piid']}"
            elif 'eiid' in param:
                prop = f"{param['siid']}.{param['eiid']}"
            else:
                _LOGGER.warning(f"Unsupported param: {data}")
                return

            if prop in zigbee.GLOBAL_PROP:
                prop = zigbee.GLOBAL_PROP[prop]
            else:
                prop = next((p[2]
                             for p in (device['params'] or device['mi_spec'])
                             if p[0] == prop), prop)

            # https://github.com/Koenkk/zigbee2mqtt/issues/798
            # https://www.maero.dk/aqara-temperature-humidity-pressure-sensor-teardown/
            if (prop == 'temperature'
                    and device['model'] != 'lumi.airmonitor.acn01'):
                if -4000 < param['value'] < 12500:
                    payload[prop] = param['value'] / 100.0
            elif (prop == 'humidity'
                  and device['model'] != 'lumi.airmonitor.acn01'):
                if 0 <= param['value'] <= 10000:
                    payload[prop] = param['value'] / 100.0
            elif prop == 'pressure':
                payload[prop] = param['value'] / 100.0
            elif prop in ('battery', 'voltage'):
                # sometimes voltage and battery came in one payload
                if prop == 'voltage' and 'battery' in payload:
                    continue
                # I do not know if the formula is correct, so battery is more
                # important than voltage
                payload['battery'] = (param['value']
                                      if param['value'] < 1000 else int(
                                          (min(param['value'], 3200) - 2600) /
                                          6))
            elif prop == 'alive' and param['value']['status'] == 'offline':
                device['online'] = False
            elif prop == 'angle':
                # xiaomi cube 100 points = 360 degrees
                payload[prop] = param['value'] * 4
            elif prop == 'duration':
                # xiaomi cube
                payload[prop] = param['value'] / 1000.0
            elif prop in ('consumption', 'power'):
                payload[prop] = round(param['value'], 2)
            elif 'value' in param:
                payload[prop] = param['value']
            elif 'arguments' in param:
                if prop == 'motion':
                    payload[prop] = 1
                else:
                    payload[prop] = param['arguments']

        # no time in device add command
        ts = round(ts - data['time'] * 0.001 + self.time_offset, 2) \
            if 'time' in data else '?'
        self.debug(f"{device['did']} {device['model']} <= {payload} [{ts}]")

        if payload:
            device['online'] = True

        for entity in device['entities'].values():
            if entity:
                entity.update(payload)

        # TODO: move code earlier!!!
        if 'added_device' in payload:
            # {'did': 'lumi.fff', 'mac': 'fff', 'model': 'lumi.sen_ill.mgl01',
            # 'version': '21', 'zb_ver': '3.0'}
            device = payload['added_device']
            device['mac'] = '0x' + device['mac']
            device['type'] = 'zigbee'
            device['init'] = payload
            self.setup_devices([device])

        # return for tests purpose
        return payload

    def process_ble_event(self, data: dict):
        self.debug(f"Process BLE {data}")

        pdid = data['dev'].get('pdid')

        did = data['dev']['did']
        if did not in self.devices:
            mac = data['dev']['mac'].replace(':', '').lower() \
                if 'mac' in data['dev'] else \
                'ble_' + did.replace('blt.3.', '')
            self.devices[did] = device = {
                'did': did,
                'mac': mac,
                'init': {},
                'type': 'bluetooth'
            }
            desc = bluetooth.get_device(pdid, 'BLE')
            device.update(desc)

            # update params from config
            default_config = self.default_devices.get(did)
            if default_config:
                device.update(default_config)

        else:
            device = self.devices[did]

        if device.get('seq') == data['frmCnt']:
            return
        device['seq'] = data['frmCnt']

        if isinstance(data['evt'], list):
            # check if only one
            assert len(data['evt']) == 1, data
            payload = bluetooth.parse_xiaomi_ble(data['evt'][0], pdid)
        elif isinstance(data['evt'], dict):
            payload = bluetooth.parse_xiaomi_ble(data['evt'], pdid)
        else:
            payload = None

        if payload:
            self._process_ble_event(device, payload)

    def process_ble_event_fix(self, data: dict):
        self.debug(f"Process BLE Fix {data}")

        did = data['did']
        if did not in self.devices:
            self.debug(f"Unregistered BLE device {did}")
            return

        device = self.devices[did]
        if device.get('seq') == data['seq']:
            return
        device['seq'] = data['seq']

        payload = bluetooth.parse_xiaomi_ble(data, data['pdid'])
        if payload:
            self._process_ble_event(device, payload)

    def _process_ble_event(self, device: dict, payload: dict):
        did = device['did']

        # init entities if needed
        init = device['init']
        for k in payload.keys():
            if k in init:
                # update for retain
                init[k] = payload[k]
                continue

            init[k] = payload[k]

            domain = bluetooth.get_ble_domain(k)
            self.add_entity(domain, device, k)

        for entity in device['entities'].values():
            if entity:
                entity.update(payload)

        raw = json.dumps(init, separators=(',', ':'))
        self.mqtt.publish(f"ble/{did}", raw, retain=True)

    def process_ble_retain(self, did: str, payload: dict):
        if did not in self.devices:
            self.debug(f"BLE device {did} is no longer on the gateway")
            return

        self.debug(f"{did} retain: {payload}")

        device = self.devices[did]

        # init entities if needed
        for k in payload.keys():
            # don't retain action and motion
            if k in device['entities']:
                continue

            if k in ('action', 'motion'):
                device['init'][k] = ''
            else:
                device['init'][k] = payload[k]

            domain = bluetooth.get_ble_domain(k)
            self.add_entity(domain, device, k)

        for entity in device['entities'].values():
            if entity:
                entity.update(payload)

    def process_pair(self, raw: bytes):
        _LOGGER.debug(f"!!! {raw}")
        # get shortID and eui64 of paired device
        if b'lumi send-nwk-key' in raw:
            # create model response
            payload = f"0x08020105000042{len(self.pair_model):02x}" \
                      f"{self.pair_model.encode().hex()}"
            m = RE_NWK_KEY.search(raw.decode())
            self.pair_payload = json.dumps(
                {
                    'sourceAddress': m[1],
                    'eui64': '0x' + m[2],
                    'profileId': '0x0104',
                    'clusterId': '0x0000',
                    'sourceEndpoint': '0x01',
                    'destinationEndpoint': '0x01',
                    'APSCounter': '0x01',
                    'APSPlayload': payload
                },
                separators=(',', ':'))
            self.pair_payload2 = json.dumps(
                {
                    'sourceAddress': m[1],
                    'eui64': '0x' + m[2],
                    'profileId': '0x0104',
                    'clusterId': '0x0000',
                    'sourceEndpoint': '0x01',
                    'destinationEndpoint': '0x01',
                    'APSCounter': '0x01',
                    'APSPlayload': '0x0801010100002001'
                },
                separators=(',', ':'))

        # send model response "from device"
        elif b'zdo active ' in raw:
            self.mqtt.publish(self.gw_topic + 'MessageReceived',
                              self.pair_payload2)
            self.mqtt.publish(self.gw_topic + 'MessageReceived',
                              self.pair_payload)

    def send(self, device: dict, data: dict):
        did = device['did'] if device['did'] != self.did else 'lumi.0'
        payload = {'cmd': 'write', 'did': did}

        # convert hass prop to lumi prop
        if device['mi_spec']:
            params = []
            for k, v in data.items():
                if k == 'switch':
                    v = bool(v)
                k = next(p[0] for p in device['mi_spec'] if p[2] == k)
                params.append({
                    'siid': int(k[0]),
                    'piid': int(k[2]),
                    'value': v
                })

            payload['mi_spec'] = params
        else:
            params = [{
                'res_name':
                next(p[0] for p in device['params'] if p[2] == k),
                'value':
                v
            } for k, v in data.items()]

            payload = {
                'cmd': 'write',
                'did': device['did'],
                'params': params,
            }

        self.debug(f"{device['did']} {device['model']} => {payload}")
        payload = json.dumps(payload, separators=(',', ':')).encode()
        self.mqtt.publish('zigbee/recv', payload)

    def send_telnet(self, *args: str):
        try:
            shell = TelnetShell(self.host)
            for command in args:
                if command == 'ftp':
                    shell.check_or_download_busybox()
                    shell.run_ftp()
                elif command == 'dump':
                    raw = shell.tar_data()
                    filename = Path().absolute() / f"{self.host}.tar.gz"
                    with open(filename, 'wb') as f:
                        f.write(raw)
                else:
                    shell.exec(command)
            shell.close()

        except Exception as e:
            _LOGGER.exception(f"Telnet command error: {e}")

    def send_mqtt(self, cmd: str):
        if cmd == 'publishstate':
            self.mqtt.publish(self.gw_topic + 'publishstate')

    def get_device(self, mac: str) -> Optional[dict]:
        for device in self.devices.values():
            if device.get('mac') == mac:
                return device
        return None
Пример #24
0
class Gateway3(Thread):
    def __init__(self, host: str, token: str, config: dict):
        super().__init__(daemon=True)

        self.host = host
        self.miio = Device(host, token)

        self.mqtt = Client()
        self.mqtt.on_connect = self.on_connect
        self.mqtt.on_disconnect = self.on_disconnect
        self.mqtt.on_message = self.on_message
        self.mqtt.connect_async(host)

        self.ble = GatewayBLE(self)

        self.debug = config['debug'] if 'debug' in config else ''
        self.devices = config['devices'] if 'devices' in config else {}
        self.updates = {}
        self.setups = {}

    @property
    def device(self):
        return self.devices['lumi.0']

    def add_update(self, did: str, handler):
        """Add handler to device update event."""
        self.updates.setdefault(did, []).append(handler)

    def add_setup(self, domain: str, handler):
        """Add hass device setup funcion."""
        self.setups[domain] = handler

    def run(self):
        """Main loop"""
        while 'lumi.0' not in self.devices:
            if self._miio_connect():
                devices = self._get_devices_v3()
                if devices:
                    self.setup_devices(devices)
                else:
                    self._enable_telnet()
            else:
                time.sleep(30)

        # start bluetooth read loop
        self.ble.start()

        while True:
            if self._mqtt_connect():
                self.mqtt.loop_forever()

            elif self._miio_connect() and self._enable_telnet():
                self._enable_mqtt()

            else:
                _LOGGER.debug("sleep 30")
                time.sleep(30)

    def _mqtt_connect(self) -> bool:
        try:
            self.mqtt.reconnect()
            return True
        except:
            return False

    def _miio_connect(self) -> bool:
        try:
            self.miio.send_handshake()
            return True
        except:
            _LOGGER.debug(f"{self.host} | Can't send handshake")
            return False

    def _get_devices_v1(self) -> Optional[list]:
        """Load devices via miio protocol."""
        _LOGGER.debug(f"{self.host} | Read devices")
        try:
            devices = {}

            # endless loop protection
            for _ in range(16):
                # load only 8 device per part
                part = self.miio.send('get_device_list', retry_count=10)
                if len(part) == 0:
                    return []

                for item in part:
                    devices[item['num']] = {
                        'did': item['did'],
                        'mac': f"0x{item['did'][5:]}",
                        'model': item['model'],
                    }

                if part[0]['total'] == len(devices):
                    break

            devices = list(devices.values())
            for device in devices:
                desc = utils.get_device(device['model'])
                # skip unknown model
                if desc is None:
                    continue
                # get xiaomi param names
                params = [p[1] for p in desc['params'] if p[1] is not None]
                # skip if don't have retain params
                if not params:
                    continue
                # load param values
                values = self.miio.send('get_device_prop',
                                        [device['did']] + params)
                # get hass param names
                params = [p[2] for p in desc['params'] if p[1] is not None]

                data = dict(zip(params, values))
                # fix some param values
                for k, v in data.items():
                    if k in ('temperature', 'humidity'):
                        data[k] = v / 100.0
                    elif v in ('on', 'open'):
                        data[k] = 1
                    elif v in ('off', 'close'):
                        data[k] = 0

                device['init'] = data

            device = self.miio.info()
            devices.append({
                'did': 'lumi.0',
                'mac': device.mac_address,  # wifi mac!!!
                'model': device.model
            })

            return devices

        except Exception as e:
            _LOGGER.exception(f"{self.host} | Get devices: {e}")
            return None

    def _get_devices_v2(self) -> Optional[list]:
        """Load device list via Telnet.

        Device desc example:
          mac: '0x158d0002c81234'
          shortId: '0x0691'
          manuCode: '0x115f'
          model: 'lumi.sensor_ht'
          did: 'lumi.158d0002c81234'
          devType: 0
          appVer: 2
          hardVer: 0
          devID: 770
          status: 0
          model_ver: 2
        """
        _LOGGER.debug(f"{self.host} | Read devices")
        try:
            telnet = Telnet(self.host)
            telnet.read_until(b"login: "******"admin\r\n")
            telnet.read_until(b'\r\n# ')  # skip greeting

            telnet.write(b"cat /data/zigbee/coordinator.info\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')
            device = json.loads(raw[:-2])
            device.update({
                'did': 'lumi.0',
                'model': 'lumi.gateway.mgl03',
                'host': self.host
            })

            devices = [device]

            telnet.write(b"cat /data/zigbee/device.info\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')
            raw = json.loads(raw[:-2])
            devices += raw['devInfo']
            telnet.close()

            return devices
        except Exception as e:
            _LOGGER.exception(f"Can't read devices: {e}")
            return None

    def _get_devices_v3(self):
        """Load device list via Telnet."""
        _LOGGER.debug(f"{self.host} | Read devices")
        try:
            telnet = Telnet(self.host, timeout=5)
            telnet.read_until(b"login: "******"admin\r\n")
            telnet.read_until(b'\r\n# ')  # skip greeting

            # https://github.com/AlexxIT/XiaomiGateway3/issues/14
            # fw 1.4.6_0012 and below have one zigbee_gw.db file
            # fw 1.4.6_0030 have many json files in this folder
            telnet.write(b"cat /data/zigbee_gw/* | base64\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')
            raw = base64.b64decode(raw)
            if raw.startswith(b'unqlite'):
                db = Unqlite(raw)
                data = db.read_all()
            else:
                raw = re.sub(br'}\s+{', b',', raw)
                data = json.loads(raw)

            devices = []

            # data = {} or data = {'dev_list': 'null'}
            dev_list = json.loads(data.get('dev_list', 'null')) or []
            _LOGGER.debug(f"{self.host} | Load {len(dev_list)} zigbee devices")

            for did in dev_list:
                model = data[did + '.model']
                desc = utils.get_device(model)

                # skip unknown model
                if desc is None:
                    _LOGGER.debug(f"Unsupported model: {model}")
                    continue

                retain = json.loads(data[did + '.prop'])['props']
                _LOGGER.debug(f"{self.host} | {model} retain: {retain}")

                params = {
                    p[2]: retain.get(p[1])
                    for p in desc['params']
                    if p[1] is not None
                }

                # fix some param values
                for k, v in params.items():
                    if k in ('temperature', 'humidity'):
                        params[k] = v / 100.0
                    elif v in ('on', 'open'):
                        params[k] = 1
                    elif v in ('off', 'close'):
                        params[k] = 0
                    elif k == 'battery' and v and v > 1000:
                        params[k] = round((min(v, 3200) - 2500) / 7)

                device = {
                    'did': did,
                    'mac': '0x' + data[did + '.mac'],
                    'model': data[did + '.model'],
                    'type': 'zigbee',
                    'zb_ver': data[did + '.version'],
                    'init': params
                }
                devices.append(device)

            telnet.write(b"cat /data/zigbee/coordinator.info\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')

            device = json.loads(raw[:-2])
            devices.insert(0, {
                'did': 'lumi.0',
                'model': 'lumi.gateway.mgl03',
                'mac': device['mac'],
                'type': 'gateway'
            })

            return devices

        except (ConnectionRefusedError, socket.timeout):
            return None

        except Exception as e:
            _LOGGER.debug(f"Can't read devices: {e}")
            return None

    def _enable_telnet(self):
        _LOGGER.debug(f"{self.host} | Try enable telnet")
        try:
            resp = self.miio.send("enable_telnet_service")
            return resp[0] == 'ok'
        except Exception as e:
            _LOGGER.exception(f"Can't enable telnet: {e}")
            return False

    def _enable_mqtt(self):
        _LOGGER.debug(f"{self.host} | Try run public MQTT")
        try:
            telnet = Telnet(self.host)
            telnet.read_until(b"login: "******"admin\r\n")
            telnet.read_very_eager()  # skip response

            # enable public mqtt
            telnet.write(b"killall mosquitto\r\n")
            telnet.read_very_eager()  # skip response
            time.sleep(.5)
            telnet.write(b"mosquitto -d\r\n")
            telnet.read_very_eager()  # skip response
            time.sleep(1)

            telnet.close()
            return True
        except Exception as e:
            _LOGGER.debug(f"Can't run MQTT: {e}")
            return False

    def on_connect(self, client, userdata, flags, rc):
        _LOGGER.debug(f"{self.host} | MQTT connected")
        self.mqtt.subscribe('#')

    def on_disconnect(self, client, userdata, rc):
        _LOGGER.debug(f"{self.host} | MQTT disconnected")
        # force end mqtt.loop_forever()
        self.mqtt.disconnect()

    def on_message(self, client: Client, userdata, msg: MQTTMessage):
        if 'mqtt' in self.debug:
            _LOGGER.debug(f"[MQ] {msg.topic} {msg.payload.decode()}")

        if msg.topic == 'zigbee/send':
            payload = json.loads(msg.payload)
            self.process_message(payload)

    def setup_devices(self, devices: list):
        """Add devices to hass."""
        for device in devices:
            desc = utils.get_device(device['model'])
            if not desc:
                _LOGGER.debug(f"Unsupported model: {device}")
                continue

            _LOGGER.debug(f"{self.host} | Setup device {device['model']}")

            device.update(desc)

            # update params from config
            default_config = self.devices.get(device['mac'])
            if default_config:
                device.update(default_config)

            self.devices[device['did']] = device

            for param in device['params']:
                domain = param[3]
                if not domain:
                    continue

                # wait domain init
                while domain not in self.setups:
                    time.sleep(1)

                attr = param[2]
                self.setups[domain](self, device, attr)

    def process_message(self, data: dict):
        if data['cmd'] == 'heartbeat':
            # don't know if only one item
            assert len(data['params']) == 1, data

            data = data['params'][0]
            pkey = 'res_list'
        elif data['cmd'] == 'report':
            pkey = 'params' if 'params' in data else 'mi_spec'
        elif data['cmd'] == 'write_rsp':
            pkey = 'results'
        else:
            raise NotImplemented(f"Unsupported cmd: {data}")

        did = data['did']

        # skip without callback
        if did not in self.updates:
            return

        device = self.devices[did]
        payload = {}

        # convert codes to names
        for param in data[pkey]:
            if param.get('error_code', 0) != 0:
                continue

            prop = param['res_name'] if 'res_name' in param else \
                f"{param['siid']}.{param['piid']}"

            if prop in GLOBAL_PROP:
                prop = GLOBAL_PROP[prop]
            else:
                prop = next((p[2] for p in device['params']
                             if p[0] == prop), prop)

            if prop in ('temperature', 'humidity'):
                payload[prop] = param['value'] / 100.0
            elif prop == 'battery' and param['value'] > 1000:
                payload[prop] = round((min(param['value'], 3200) - 2500) / 7)
            else:
                payload[prop] = param['value']

        _LOGGER.debug(f"{self.host} | {device['did']} {device['model']} <= "
                      f"{payload}")

        for handler in self.updates[did]:
            handler(payload)

        if 'added_device' in payload:
            # {'did': 'lumi.fff', 'mac': 'fff', 'model': 'lumi.sen_ill.mgl01',
            # 'version': '21', 'zb_ver': '3.0'}
            device = payload['added_device']
            device['mac'] = '0x' + device['mac']
            self.setup_devices([device])

    def process_ble_event(self, raw: Union[bytes, str]):
        data = json.loads(raw[10:])['params'] \
            if isinstance(raw, bytes) else json.loads(raw)

        _LOGGER.debug(f"{self.host} | Process BLE {data}")

        did = data['dev']['did']
        if did not in self.devices:
            mac = data['dev']['mac'].replace(':', '').lower() \
                if 'mac' in data['dev'] else \
                'ble_' + did.replace('blt.3.', '')
            self.devices[did] = device = {
                'did': did, 'mac': mac, 'init': {}, 'device_name': "BLE",
                'type': 'ble'}
        else:
            device = self.devices[did]

        if isinstance(data['evt'], list):
            # check if only one
            assert len(data['evt']) == 1, data
            payload = ble.parse_xiaomi_ble(data['evt'][0])
        elif isinstance(data['evt'], dict):
            payload = ble.parse_xiaomi_ble(data['evt'])
        else:
            payload = None

        if payload is None:
            _LOGGER.debug(f"Unsupported BLE {data}")
            return

        # init entities if needed
        for k in payload.keys():
            if k in device['init']:
                continue

            device['init'][k] = payload[k]

            domain = ble.get_ble_domain(k)
            if not domain:
                continue

            # wait domain init
            while domain not in self.setups:
                time.sleep(1)

            self.setups[domain](self, device, k)

        if did in self.updates:
            for handler in self.updates[did]:
                handler(payload)

    def send(self, device: dict, data: dict):
        # convert hass prop to lumi prop
        params = [{
            'res_name': next(p[0] for p in device['params'] if p[2] == k),
            'value': v
        } for k, v in data.items()]

        payload = {
            'cmd': 'write',
            'did': device['did'],
            'params': params,
        }

        _LOGGER.debug(f"{self.host} | {device['did']} {device['model']} => "
                      f"{payload}")

        payload = json.dumps(payload, separators=(',', ':')).encode()
        self.mqtt.publish('zigbee/recv', payload)
Пример #25
0
#!/usr/bin/env python

from paho.mqtt.client import Client

client = Client(client_id="Subscriber_test")


def on_connect(client, userdata, flags, rc):
    print("Connesso con successo")


def on_message(client, userdata, message):
    print(message.payload.decode())


client.on_connect = on_connect
client.on_message = on_message

client.connect("inoxhome.duckdns.org")
client.subscribe("outTopic")
client.loop_forever()
Пример #26
0
def subscribe(
    topics: str | list[str],
    hostname=leader_hostname,
    retries: int = 10,
    timeout: Optional[float] = None,
    allow_retained: bool = True,
    **mqtt_kwargs,
) -> Optional[MQTTMessage]:
    """
    Modeled closely after the paho version, this also includes some try/excepts and
    a timeout. Note that this _does_ disconnect after receiving a single message.

    A failure case occurs if this is called in a thread (eg: a callback) and is waiting
    indefinitely for a message. The parent job may not exit properly.

    """

    retry_count = 1
    for retry_count in range(retries):
        try:

            lock: Optional[threading.Lock]

            def on_connect(client, userdata, flags, rc):
                client.subscribe(userdata["topics"])
                return

            def on_message(client, userdata, message: MQTTMessage):
                if not allow_retained and message.retain:
                    return

                userdata["messages"] = message
                client.disconnect()

                if userdata["lock"]:
                    userdata["lock"].release()

                return

            if timeout:
                lock = threading.Lock()
            else:
                lock = None

            topics = [topics] if isinstance(topics, str) else topics
            userdata: dict[str, Any] = {
                "topics": [(topic, mqtt_kwargs.pop("qos", 0)) for topic in topics],
                "messages": None,
                "lock": lock,
            }

            client = Client(userdata=userdata)
            client.on_connect = on_connect
            client.on_message = on_message
            client.connect(leader_hostname)

            if timeout is None:
                client.loop_forever()
            else:
                assert lock is not None
                lock.acquire()
                client.loop_start()
                lock.acquire(timeout=timeout)
                client.loop_stop()
                client.disconnect()

            return userdata["messages"]

        except (ConnectionRefusedError, socket.gaierror, OSError, socket.timeout):
            from pioreactor.logging import create_logger

            logger = create_logger("pubsub.subscribe", to_mqtt=False)
            logger.debug(
                f"Attempt {retry_count}: Unable to connect to host: {hostname}",
                exc_info=True,
            )

            time.sleep(5 * retry_count)  # linear backoff

    else:
        logger = create_logger("pubsub.subscribe", to_mqtt=False)
        logger.error(f"Unable to connect to host: {hostname}. Exiting.")
        raise ConnectionRefusedError(f"Unable to connect to host: {hostname}.")
Пример #27
0
class MqttHandler:
    def __init__(self, client_id='DEFAULT_CLIENT_ID', topic='DEFAULT_TOPIC', broker_host='localhost',
                 broker_port=MQTT_BROKER_PORT):
        self.subscribed = False
        self.client_id = client_id
        self.client = Client(client_id=self.client_id, protocol=MQTT_PROTOCOL_VERSION)
        self.client.on_message = self.on_message_callback
        self.client.on_publish = self.on_publish_callback
        self.client.on_connect = self.connect_callback
        self.client.on_disconnect = self.disconnect_callback
        self.topic = topic
        self.broker_host = broker_host
        self.broker_port = broker_port
        self.message_received = 0
        userdata = {
            USER_DATA_MESSAGE_RECEIVED: 0,
        }
        self.client.user_data_set(userdata)

    def connect(self):
        self.client.connect(host=self.broker_host, port=self.broker_port)

    def connect_async(self):
        self.client.connect_async(host=self.broker_host, port=self.broker_port)

    def connect_callback(self, client, userdata, flags, rc):
        print('connect_callback: result code[' + str(rc) + ']')
        (result, _) = client.subscribe(topic=self.topic)
        self.subscribed = result

    def disconnect(self):
        self.client.disconnect()

    def disconnect_callback(self, client, userdata, rc):
        print('disconnect_callback')

    def is_valid(self, my_json: json):
        if app.config.get('DEBUG', False):
            print("json_validation")
        # try:
        #     if my_json['id'] is None or my_json['byte_stream'] is None:
        #         return False
        # except KeyError:
        #     return False
        return True

    def on_message_callback(self, client, userdata, message):
        from core.socketio_runner import emit_command

        userdata[USER_DATA_MESSAGE_RECEIVED] += 1

        topic = message.topic
        payload = json.loads(message.payload)

        if app.config.get('DEBUG', False):
            print('on_message_callback: topic[' + topic + ']')

        if self.is_valid(payload):
            emit_command(topic, payload)
        else:
            raise Exception('Message payload not valid')

    @staticmethod
    def publish_single_message(topic, payload=None, qos=0, retain=False, hostname="localhost",
                               port=MQTT_BROKER_PORT, client_id="", keepalive=60, will=None, auth=None, tls=None):
        if app.config.get('DEBUG', False):
            print("publish_single_message")
        single(topic=topic, payload=payload, qos=qos, retain=retain, hostname=hostname, port=port, client_id=client_id,
               keepalive=keepalive, will=will, auth=auth, tls=tls)

    def on_publish_callback(self, client, userdata, mid):
        print('on_publish_callback')

    def loop_for_ever(self):
        self.client.loop_forever()

    def loop_start(self):
        self.client.loop_start()

    def loop_stop(self, force=False):
        self.client.loop_stop(force=force)
Пример #28
0
class Broker:
    """
    Respresents a connection to the MQTT broker.
    """
    def __init__(self,
                 bot,
                 host=BROKER_HOST,
                 port=BROKER_PORT,
                 subscriptions=None):
        """
        Create an instance and connect to broker.

        :param bot: reference to the `Bot` instance.
        :param host: the hoststring for the broker.
        :param port: the port of the broker.
        :param subscriptions: a dictionary in the form `topic name (string) => function to execute (callback)`. callback must be
        able to receive three arguments - namely `client_id`, `userdata`, `message`.
        """
        self._bot = bot

        self._host = host
        self._port = port

        self._client = Client()
        self._client.connect(self._host, self._port, 60)

        self._subscribed_thread = None

        if subscriptions:
            self._subscribe(subscriptions)

    def _subscribe(self, subscriptions):
        from threading import Thread

        def on_connect(client_id, userdata, flags, rc):
            print('subscribed with code {}'.format(rc))

        def listen():
            for topic, callback in subscriptions.items():
                self._client.callback(callback,
                                      topic=topic,
                                      hostname=self._host)
            self._client.loop_forever()

        self._client.on_connect = on_connect

        self._subscribed_thread = Thread(group=None,
                                         target=listen,
                                         daemon=True)
        self._subscribed_thread.start()

    def _publish(self, topic, payload, qos=0):
        # TODO: add json headers here
        # msg = json.encode()
        self._client.publish(topic, payload, qos=qos)

    def send_message(self, topic, message):
        """
        Send a text message to the broker.

        :param topic: mqtt topic to which the message will be published.
        :param payload: the message as string.
        """
        self._publish(topic, message)

    def send_file(self, topic, payload):
        """
        Send a file to the broker.

        :param topic: mqtt topic to which the message will be published.
        :param payload: the binary content of the file.
        """
        self._publish(topic, base64.encode(payload))
Пример #29
0
class Daemon():
    """MQTTToRDD Daemon."""
    def __init__(self, config, foreground=False):
        self.cfg = config
        self.logger = logging.getLogger('MQTToRRD')
        self.logger.setLevel(self.cfg.log_level)
        formatter = logging.Formatter(self.cfg.log_format)
        self.client = None

        if foreground:
            self.handler = logging.StreamHandler()
            self.cfg.log_handler = "stderr"
        elif self.cfg.log_handler == "file":
            if sys.platform == 'windows':
                self.handler = logging.FileHandler(self.cfg.log_file,
                                                   encoding="utf-8")
            else:
                self.handler = WatchedFileHandler(self.cfg.log_file,
                                                  encoding="utf-8")
        else:
            self.handler = SysLogHandler(self.cfg.log_syslog,
                                         SysLogHandler.LOG_DAEMON)

        for hdlr in logger.root.handlers:  # reset root logger handlers
            logger.root.removeHandler(hdlr)

        logger.root.addHandler(self.handler)
        self.handler.setFormatter(formatter)

    def check(self):
        """Check configuration."""
        for section in self.cfg.sections():
            # this check configuration values
            if section.startswith("/"):
                self.cfg.get_topic(section)  # read from config
            elif section.startswith("$SYS/"):
                self.cfg.get_topic(section)  # read from config
        self.logger.info("Configuration looks OK")

        if not isdir(self.cfg.data_dir):
            raise RuntimeError("Data dir `%s' does not exist." %
                               self.cfg.data_dir)
        if not access(self.cfg.data_dir, R_OK | W_OK):
            raise RuntimeError("Data dir `%s' is not readable and writable" %
                               self.cfg.data_dir)
        if self.cfg.log_handler == "file" and \
                access(self.cfg.log_file, R_OK | W_OK) and \
                isdir(dirname(self.cfg.log_file)) and \
                access(dirname(self.cfg.log_file), R_OK | W_OK):
            raise RuntimeError("Could not write to log")

    @staticmethod
    def on_connect(client, daemon, flags, res):
        """connect mqtt handler."""
        # pylint: disable=unused-argument
        daemon.logger.info("Connected to server")
        for sub in daemon.cfg.subscriptions:
            daemon.logger.info("Subscribing to topic: %s", sub)
            client.subscribe(sub)

    @staticmethod
    def on_message(client, daemon, msg):
        # pylint: disable=unused-argument
        """message mqtt handler."""
        daemon.logger.info(
            "Message received on topic %s with QoS %s and payload `%s'",
            msg.topic, msg.qos, msg.payload)
        try:
            value = float(msg.payload)
        except ValueError:
            daemon.logger.warning(
                "Unable to get float from topic %s and payload %s", msg.topic,
                msg.payload)
            return
        topic = msg.topic.replace('.', '_')
        topic = topic[1:] if topic.startswith('/') else topic
        rrd_path = join(daemon.cfg.data_dir, dirname(topic),
                        "%s.rrd" % basename(topic))
        daemon.rrd(rrd_path, msg.topic, value)

    def rrd(self, rrd_path, topic, value):
        """Create or update RRD file."""
        dir_path = dirname(rrd_path)
        if not isdir(dir_path):
            self.logger.debug("Creating topic directory %s", dir_path)
            makedirs(dir_path)
        if not exists(rrd_path):
            self.logger.debug("Creatting RRD file %s", rrd_path)
            # pylint: disable=invalid-name
            step, ds, rra = self.cfg.find_topic(topic)
            ds = ds.format(topic=basename(topic))
            try:
                create_rrd(rrd_path, "--step", str(step), "--start", "0", ds,
                           *rra)
            except (ProgrammingError, OperationalError) as exc:
                self.logger.error("Could not create RRD for topic %s: %s",
                                  topic, str(exc))
        self.logger.info("Updating %s with value %f", topic, value)
        try:
            update_rrd(rrd_path, "N:%f" % value)
        except (ProgrammingError, OperationalError) as exc:
            self.logger.error("Could not log value %f to RRD for topic %s: %s",
                              value, topic, str(exc))

    def run(self, daemon=True):
        """Run daemon."""
        self.check()
        while True:
            try:
                self.client = Client(client_id=self.cfg.client_id,
                                     userdata=self)
                self.client.on_connect = Daemon.on_connect
                self.client.on_message = Daemon.on_message
                if self.cfg.tls:
                    self.client.tls_set(ca_certs=self.cfg.ca_certs,
                                        certfile=self.cfg.certfile,
                                        keyfile=self.cfg.keyfile)
                self.logger.debug("Attempting to connect to server %s:%s",
                                  self.cfg.hostname, self.cfg.port)
                self.client.connect(self.cfg.hostname, self.cfg.port,
                                    self.cfg.keepalive)
                self.logger.info("Connected to %s:%s", self.cfg.hostname,
                                 self.cfg.port)
                self.client.loop_forever()
                return 0
            except Exception as exc:  # pylint: disable=broad-except
                logging.debug("%s", format_exc())
                self.logger.debug("%s", format_exc())
                self.logger.fatal("%s", exc)
                if not daemon:
                    return 1
            sleep(30)

    def shutdown(self, signum, frame):
        """Signal handler for termination."""
        # pylint: disable=unused-argument
        self.logger.info("Shutting down with signal %s", Signals(signum).name)
        self.client.disconnect()
        sys.exit(1)
Пример #30
0
Client.connected_flag = False
mqtt_client = Client()


def on_connect(client, userdata, flags, rc):
    if rc == 0:
        mqtt_client.connected_flag = True
    else:
        mqtt_client.connected_flag = False


def on_message(client, userdata, message):
    info = simplejson.loads(message.payload)
    print('Customer Order:')
    info['Order_Status'] = 'Confirmed'
    print(info)
    client.publish(topic='%s/%s' % (ORDER_STATUS, info['Room']),
                   payload=simplejson.dumps(info))


mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message
mqtt_client.loop_start()
mqtt_client.connect(host=MQTT_ADDR, port=MQTT_PRT)
while not mqtt_client.connected_flag:  # wait in loop
    print("In wait loop")
    time.sleep(1)
mqtt_client.subscribe(topic='%s/+' % FD_TOPIC)
mqtt_client.loop_forever()
mqtt_client.disconnect()
Пример #31
0
class Gateway3(Thread):
    devices: dict = None
    updates: dict = None
    setups: dict = None

    log = None

    def __init__(self, host: str, token: str):
        super().__init__(daemon=True)

        self.host = host
        self.miio = Device(host, token)

        self.mqtt = Client()
        self.mqtt.on_connect = self.on_connect
        self.mqtt.on_disconnect = self.on_disconnect
        self.mqtt.on_message = self.on_message
        self.mqtt.connect_async(host)

        if isinstance(self.log, str):
            self.log = utils.get_logger(self.log)

    @property
    def device(self):
        return self.devices['lumi.0']

    def add_update(self, did: str, handler):
        """Add handler to device update event."""
        if self.updates is None:
            self.updates = {}
        self.updates.setdefault(did, []).append(handler)

    def add_setup(self, domain: str, handler):
        """Add hass device setup funcion."""
        if self.setups is None:
            self.setups = {}
        self.setups[domain] = handler

    def run(self):
        """Main loop"""
        while self.devices is None:
            if self._miio_connect():
                devices = self._get_devices1()
                if devices:
                    self.setup_devices(devices)
                # else:
                #     self._enable_telnet()
            else:
                time.sleep(30)

        while True:
            if self._mqtt_connect():
                self.mqtt.loop_forever()

            elif self._miio_connect() and self._enable_telnet():
                self._enable_mqtt()

            else:
                _LOGGER.debug("sleep 30")
                time.sleep(30)

    def _mqtt_connect(self) -> bool:
        try:
            self.mqtt.reconnect()
            return True
        except:
            return False

    def _miio_connect(self) -> bool:
        try:
            self.miio.send_handshake()
            return True
        except:
            return False

    def _get_devices1(self) -> Optional[list]:
        """Load devices via miio protocol."""
        _LOGGER.debug(f"{self.host} | Read devices")
        try:
            devices = {}

            # endless loop protection
            for _ in range(16):
                # load only 8 device per part
                part = self.miio.send('get_device_list', retry_count=10)
                if len(part) == 0:
                    return []

                for item in part:
                    devices[item['num']] = {
                        'did': item['did'],
                        'mac': f"0x{item['did'][5:]}",
                        'model': item['model'],
                    }

                if part[0]['total'] == len(devices):
                    break

            devices = list(devices.values())
            for device in devices:
                desc = utils.get_device(device['model'])
                # skip unknown model
                if desc is None:
                    continue
                # get xiaomi param names
                params = [p[1] for p in desc['params'] if p[1] is not None]
                # skip if don't have retain params
                if not params:
                    continue
                # load param values
                values = self.miio.send('get_device_prop',
                                        [device['did']] + params)
                # get hass param names
                params = [p[2] for p in desc['params'] if p[1] is not None]

                data = dict(zip(params, values))
                # fix some param values
                for k, v in data.items():
                    if k in ('temperature', 'humidity'):
                        data[k] = v / 100.0
                    elif v == 'on':
                        data[k] = 1
                    elif v == 'off':
                        data[k] = 0

                device['init'] = data

            device = self.miio.info()
            devices.append({
                'did': 'lumi.0',
                'mac': device.mac_address,  # wifi mac!!!
                'model': device.model
            })

            return devices

        except Exception as e:
            return None

    def _get_devices2(self) -> Optional[list]:
        """Load device list via Telnet.

        Device desc example:
          mac: '0x158d0002c81234'
          shortId: '0x0691'
          manuCode: '0x115f'
          model: 'lumi.sensor_ht'
          did: 'lumi.158d0002c81234'
          devType: 0
          appVer: 2
          hardVer: 0
          devID: 770
          status: 0
          model_ver: 2
        """
        _LOGGER.debug(f"{self.host} | Read devices")
        try:
            telnet = Telnet(self.host)
            telnet.read_until(b"login: "******"admin\r\n")
            telnet.read_until(b'\r\n# ')  # skip greeting

            telnet.write(b"cat /data/zigbee/coordinator.info\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')
            device = json.loads(raw[:-2])
            device.update({
                'did': 'lumi.0',
                'model': 'lumi.gateway.mgl03',
                'host': self.host
            })

            devices = [device]

            telnet.write(b"cat /data/zigbee/device.info\r\n")
            telnet.read_until(b'\r\n')  # skip command
            raw = telnet.read_until(b'# ')
            raw = json.loads(raw[:-2])
            devices += raw['devInfo']
            telnet.close()

            return devices
        except Exception as e:
            _LOGGER.exception(f"Can't read devices: {e}")
            return None

    def _enable_telnet(self):
        _LOGGER.debug(f"{self.host} | Try enable telnet")
        try:
            resp = self.miio.send("enable_telnet_service")
            return resp[0] == 'ok'
        except Exception as e:
            _LOGGER.exception(f"Can't enable telnet: {e}")
            return False

    def _enable_mqtt(self):
        _LOGGER.debug(f"{self.host} | Try run public MQTT")
        try:
            telnet = Telnet(self.host)
            telnet.read_until(b"login: "******"admin\r\n")
            telnet.read_very_eager()  # skip response
            telnet.write(b"killall mosquitto\r\n")
            telnet.read_very_eager()  # skip response
            telnet.write(b"mosquitto -d\r\n")
            telnet.read_very_eager()  # skip response
            time.sleep(1)
            telnet.close()
            return True
        except Exception as e:
            _LOGGER.exception(f"Can't run MQTT: {e}")
            return False

    def on_connect(self, client, userdata, flags, rc):
        _LOGGER.debug(f"{self.host} | MQTT connected")
        # self.mqtt.subscribe('#')
        self.mqtt.subscribe('zigbee/send')

    def on_disconnect(self, client, userdata, rc):
        _LOGGER.debug(f"{self.host} | MQTT disconnected")
        # force end mqtt.loop_forever()
        self.mqtt.disconnect()

    def on_message(self, client: Client, userdata, msg: MQTTMessage):
        if self.log:
            self.log.debug(f"[{self.host}] {msg.topic} {msg.payload.decode()}")

        if msg.topic == 'zigbee/send':
            payload = json.loads(msg.payload)
            self.process_message(payload)

    def setup_devices(self, devices: list):
        """Add devices to hass."""
        for device in devices:
            desc = utils.get_device(device['model'])
            if not desc:
                _LOGGER.debug(f"Unsupported model: {device}")
                continue

            _LOGGER.debug(f"Setup device {device['model']}")

            device.update(desc)

            if self.devices is None:
                self.devices = {}
            self.devices[device['did']] = device

            for param in device['params']:
                domain = param[3]
                if not domain:
                    continue

                # wait domain init
                while domain not in self.setups:
                    time.sleep(1)

                attr = param[2]
                self.setups[domain](self, device, attr)

    def process_message(self, data: dict):
        if data['cmd'] == 'heartbeat':
            # don't know if only one item
            assert len(data['params']) == 1, data

            data = data['params'][0]
            pkey = 'res_list'
        elif data['cmd'] == 'report':
            pkey = 'params'
        elif data['cmd'] == 'write_rsp':
            pkey = 'results'
        else:
            raise NotImplemented(f"Unsupported cmd: {data}")

        did = data['did']
        # skip without callback
        if did not in self.updates:
            return

        device = self.devices[did]
        payload = {}
        # convert codes to names
        for param in data[pkey]:
            if param.get('error_code', 0) != 0:
                continue
            prop = param['res_name']
            if prop in GLOBAL_PROP:
                prop = GLOBAL_PROP[prop]
            else:
                prop = next((p[2] for p in device['params'] if p[0] == prop),
                            prop)
            payload[prop] = (param['value'] /
                             100.0 if prop in DIV_100 else param['value'])

        _LOGGER.debug(f"{self.host} | {device['did']} {device['model']} <= "
                      f"{payload}")

        for handler in self.updates[did]:
            handler(payload)

        if 'added_device' in payload:
            device = payload['added_device']
            device['mac'] = '0x' + device['mac']
            self.setup_devices([device])

    def send(self, device: dict, param: str, value):
        # convert hass prop to lumi prop
        prop = next(p[0] for p in device['params'] if p[2] == param)
        payload = {
            'cmd': 'write',
            'did': device['did'],
            'params': [{
                'res_name': prop,
                'value': value
            }],
        }

        _LOGGER.debug(f"{self.host} | {device['did']} {device['model']} => "
                      f"{payload}")

        payload = json.dumps(payload, separators=(',', ':')).encode()
        self.mqtt.publish('zigbee/recv', payload)
Пример #32
0
from paho.mqtt.client import Client

def on_connect(client, userdata, rc):
    client.subscribe("#")
def on_message(client, userdata, msg):
    print(msg.topic+" "+str(msg.payload))
client = Client()
client.on_message = on_message
client.on_connect = on_connect
client.connect("broker.mqttdashboard.com")
client.loop_forever()