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()
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()
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
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()
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)
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()
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()
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()
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')
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
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()
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()
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} 断开连接')
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()
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()
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()
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()
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"])
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"])
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
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
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
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
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)
#!/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()
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}.")
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)
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))
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)
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()
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)
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()