├── .gitignore ├── README.md ├── custom_components └── meizu_ble │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── ir.yaml │ ├── manifest.json │ ├── meizu.py │ ├── meizu_ble.py │ ├── meizu_ble.yaml │ ├── remote.py │ ├── sensor.py │ ├── shaonianzhentan.py │ ├── test.py │ └── translations │ └── en.json ├── hacs.json ├── meizu_ir_reader_from_android ├── README.md ├── btsnoop │ ├── __init__.py │ ├── bt │ │ ├── __init__.py │ │ ├── att.py │ │ ├── hci.py │ │ ├── hci_acl.py │ │ ├── hci_cmd.py │ │ ├── hci_evt.py │ │ ├── hci_sco.py │ │ ├── hci_uart.py │ │ ├── l2cap.py │ │ └── smp.py │ └── btsnoop │ │ ├── __init__.py │ │ └── btsnoop.py └── irdatareader.py └── remote ├── README.md └── 松下吸顶灯HKC9603.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__ 3 | btsnoop_hci.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 魅族蓝牙红外遥控温湿度传感器 2 | 3 | [![hacs_badge](https://img.shields.io/badge/Home-Assistant-%23049cdb)](https://www.home-assistant.io/) 4 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) 5 | 6 | ![visit](https://visitor-badge.glitch.me/badge?page_id=shaonianzhentan.meizu_ble&left_text=visit) 7 | ![forks](https://img.shields.io/github/forks/shaonianzhentan/meizu_ble) 8 | ![stars](https://img.shields.io/github/stars/shaonianzhentan/meizu_ble) 9 | 10 | ## 安装方式 11 | 12 | 安装完成重启HA,刷新一下页面,在集成里搜索`魅族智能遥控器`即可 13 | 14 | [![Add Integration](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=meizu_ble) 15 | 16 | 注意:只在树莓派之中测试过 17 | 18 | 本插件是基于原作者的研究进行套壳接入HA,关于设备的整体操作与获取,请参考原作者代码 19 | 20 | 原项目地址:https://github.com/junnikokuki/Meizu-BLE-Thermometer 21 | 22 | --- 23 | 24 | - [蓝牙抓包,红外码解析 点这里](./meizu_ir_reader_from_android/) 25 | - [遥控红外码,支持部分内置设备 点这里](./remote/) 26 | 27 | ## 更新日志 28 | 29 | - 更新时间默认5分钟 30 | - 增加小米电视开关红外码 31 | - 增加乐视超级电视红外码 32 | 33 | ## 已知问题 34 | 35 | 树莓派的蓝牙有点辣鸡,可能会有获取失败的情况发生,这个时候一般重启蓝牙就好了 36 | 37 | ```bash 38 | # 关闭蓝牙 39 | sudo hciconfig hci0 down 40 | 41 | # 打开蓝牙 42 | sudo hciconfig hci0 up 43 | 44 | # 如果报下面这个异常的话,就执行下面的命令 45 | # Can't init device hci0: Connection timed out (110) 46 | 47 | rfkill block bluetooth 48 | 49 | rfkill unblock bluetooth 50 | 51 | sudo hciconfig hci0 up 52 | 53 | # 一般重启蓝牙后,过30秒就会显示信息了 54 | # 如果还是不行的话,那对不起,告辞,再见 55 | ``` 56 | 57 | --- 58 | 59 | 在非树莓派的环境之中,有些设备有蓝牙模块,但因为版本型号的关系,可能无法在docker内部编译`bluepy`这个依赖 60 | 如果在系统之中可以编译这个依赖,可以使用MQTT方式接入 61 | 62 | ```bash 63 | # 安装相关依赖 64 | pip3 install bluepy paho-mqtt pyyaml 65 | 66 | # 修改 meizu_ble.yaml 配置文件 67 | 68 | # 运行主程序 69 | python3 meizu_ble.py 70 | 71 | 72 | ``` 73 | 74 | ```bash 75 | # 如果有安装NodeJs环境,可以使用pm2开机启动管理 76 | pm2 start meizu_ble.py -x --interpreter python3 77 | 78 | # 重启 79 | pm2 restart meizu_ble 80 | 81 | # 查看日志 82 | pm2 logs meizu_ble 83 | 84 | ``` -------------------------------------------------------------------------------- /custom_components/meizu_ble/__init__.py: -------------------------------------------------------------------------------- 1 | from homeassistant.config_entries import ConfigEntry 2 | from homeassistant.core import HomeAssistant 3 | import homeassistant.helpers.config_validation as cv 4 | 5 | from .const import DOMAIN, PLATFORMS 6 | 7 | CONFIG_SCHEMA = cv.deprecated(DOMAIN) 8 | 9 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 10 | hass.config_entries.async_setup_platforms(entry, PLATFORMS) 11 | return True 12 | 13 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 14 | return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -------------------------------------------------------------------------------- /custom_components/meizu_ble/config_flow.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | import voluptuous as vol 5 | 6 | from homeassistant.config_entries import ConfigFlow 7 | from homeassistant.data_entry_flow import FlowResult 8 | 9 | from .const import DOMAIN, DEFAULT_NAME, SCAN_INTERVAL 10 | 11 | DATA_SCHEMA = vol.Schema({ 12 | vol.Required("name", default=DEFAULT_NAME): str, 13 | vol.Required("mac"): str, 14 | vol.Required("scan_interval", default=SCAN_INTERVAL): int 15 | }) 16 | 17 | class SimpleConfigFlow(ConfigFlow, domain=DOMAIN): 18 | 19 | VERSION = 1 20 | 21 | async def async_step_user( 22 | self, user_input: dict[str, Any] | None = None 23 | ) -> FlowResult: 24 | 25 | if user_input is None: 26 | return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) 27 | user_input['mac'] = user_input['mac'].upper() 28 | return self.async_create_entry(title=user_input['name'], data=user_input) -------------------------------------------------------------------------------- /custom_components/meizu_ble/const.py: -------------------------------------------------------------------------------- 1 | DEFAULT_NAME = "魅族智能遥控器" 2 | SCAN_INTERVAL = 300 3 | DOMAIN = "meizu_ble" 4 | VERSION = "1.1" 5 | PLATFORMS = ["sensor", "remote"] -------------------------------------------------------------------------------- /custom_components/meizu_ble/ir.yaml: -------------------------------------------------------------------------------- 1 | 松下吸顶灯HKC9603: 2 | poweroff: 74001c958ddf2dde3c:540074220054000001ef007e003d0010000f0010002e00100a8f00101daf0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011221211121121212112111122221211122112113011221211121121212112111122221211122112114 3 | poweron: 5f001c58ef8aabde3c:54005f22002a000001ca007e003d0010000f0010002e00101daf000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011221211121121212112221122121112121122123 4 | 小米电视: 5 | power: 57001c996119da50ea:550057200018000001efedca3b2cedf83f2aedfe3b07edfc46a29d824eb29d964f449d904b469d9247488d9c5b4a8d9e5f4c8d985b4e8d9a47509d844b529d864f549d804b569d824758acae6948bfad7e7ebf9a697aed 6 | 创维电视: 7 | power: 5c001ce07aac0ebb5a:55005c200022000001ad822658f28e9e5c4a8e985c198aa66597baad7079a6ab747fa6ad7479a2a37877b2ad6879beab6c7fbead6c79bab37067aacd0019d6cb04141fd6cd0419d2c30817e3ef1908ffe91d0effdf1d08f8925245b982 8 | down: 5c001cc8d94613bb5a:55005c200022000001becf651db7cbdd1d0bcfdf2560f3e120d2fff62d24fbf02d26fff22528f3fc252aeffe3d2cebf83d2eeffa2530f3e42532ffe62d34fbe02d36ffe22538f3ec253acecc3c2ddaca3c2fddcb7751a28644609ccf 9 | 海信HZ65U7E: 10 | power: 5d001cc5ab3ba8439b:55005d200024000001e0c39518b5cede1c09ced81c58cae62638fbba306fe6fe31cfe6ed3439e2e33837f2ed2839feeb2c3ffeed2c39faf33027ea8d4059968b445f968d445992834857a39c6948bca95e7e8c9f5d48bbd21205f9a8c2 11 | up: 5d001ce3dcb376439b:55005d2000240000019ba7f674d9a2ba786daac50044d6c40414dec20a1bdb99104cc2d41de5cec71c13d6c10415d2cf081bdad11005c6d71403ced11c05caef203bf2e12835fee72c33c7f00524d0cd3a18e9f33216e4c60521da87a6 12 | down: 5d001cd12c1bdd439b:55005d200024000001dc92c549e39f89495c9b8b490d9f8d4f5192cd59068f915ca28b8659548f805956938249589f8c495a9b8e495c9f88495e938a7960afb47962abb67964afb0796692a358798d9e6b4bb99c5b7d8ea9684cb0ff93 13 | left: 5d001c09df13cb439b:55005d20002400000192f5a0228ff4e02237f0e22662f0e0203ffda02a75fce02fd1f8f72e23f8f52e21e4f73223e4f53221e0f73623e0f53621ecf73a23ecf53a21e8f73e23e8f53e21d5e61332c6d72000f2e62432c1d71703ffb2f4 14 | right: 5d001c07773ed8439b:55005d200024000001d0e2ba3993eff9392cebfa397deffd3f21e2bd49169f814cb29b9649449f904946839259488f9c594a8b9e594c8f98594e839a49509f8449529b8649549f804956a2936849bdae5b7b8a9f5b4dbdaa687c80cfe3 15 | source: 5d001ce6c35c6e439b:55005d200024000001ace3bb3894eefe3c29eef83c79ea8646599bdb500f869e51af868d545982835857928d48599e8b4c5f9e8d4c599a9350478aad6079b6ab647fb6ad6479b2a3687783bc49689c897e5eaf8c4e689881416599c8e2 16 | enter: 5d001cc4a672d3439b:55005d20002400000178a7f774d8a2ba786daac40045d6c2021ddf870c53dada15ebc2c11815cec71c13d6c10415d2cf081bdad11005c6d71403ced11c05caef203bf2e12835fee72c33c7f00524d0cd3a1ae8c33214e7f60521dd94a6 17 | back: 5c001c110a1231439b:55005c200022000001dd8add61cbb7a26177b3a36125b7a56496bbb26168b7bc616ab3be616cb7b8616eabba6170b7a46172b3a66174b7a06176bba26178b7ac617ab3ae617cb7a8617e8abb3011e5f60323d2c40316e5f13027d88b 18 | menu: 5d001c92a7d209439b:55005d200024000001839fce4ce69a925045829c581c8e9c5c19869a544a82925e4c8bee603bb6ac619baeb97c6daac70013d2c9081ddecf0c1bd6c9041dd2c70813dad9100dc6df140bffc83d1ce8e51232c3da2b09fceb1838c3ae9e 19 | volumedown: 5d001c924f6ad0439b:55005d200024000001d4f9a32e84f8ef2e3bfced2a6efcef2c33f1a32679f0ec23ddf4fb222ff4f9222de8fb3e2fe8f93e2decfb3a2fecf93a2de0fb362fe0f9362de4fb322fe4f9322dd9ea1f3ecadb2c0cfeda1b3dcdeb280cf3bef8 20 | volumeup: 5d001cbcae726e439b:55005d200024000001cbe7b73499e2fa382dea8440059682425c9fc74c139a9955ab828158558e875c5396814455928f485b9a915045869754438e915c458aaf607bb2a16875bea76c7387b04564908d7a5aab837157a4b646629dd4e6 21 | mute: 5d001cd532e56f439b:55005d200024000001b38ed95df78b9d5d488f9c6521b3a1637cbeee6d32bba56896bfb26568b3bc656aafbe7d6cabb87d6eafba6570b3a46572bfa66d74bba06d76bfa26578b3ac657a8ebf4c6d998a7f5fae880411e1c53720dc938f 22 | sleep: 5c001c944e6a183b60:55005c200022000001dc9bd650fd8697544186965410829f5daf828558518e835c578e855c518abb606faac50011d6c30417d6c50411d2cb081fd2c50811dec30c17dec50c11dadb100ffbd42100f4e11636c7e41600f0d9293dc19a 23 | 乐视超3X50: 24 | power: 5d001c865cc57ca8e5:55005d200024000001b393cb48e29e8e4c5a9e884c769a96564c8beb603ab6ad619fb6bd6469b2b36867a2bd7869aebb7c6faebd7c69aac30017dadd1009c6db140fc6dd1409c2d31807f3cf091becc93d2deffc0d18e8d12235c9f892 25 | menu: 5c001ce0d52ad1cb5b:55005c20002200000169a8fc7fd4a9bf0316d1c10742d1c306f4d9d00f06d9d20308d1dc070ad1de030cc9d81f0ec9da0310d1c40712d1c60314d9c00f16d9c20318d1cc071ad1ce031ce8da1e0cfbd81202c3d61503c0e72236faa9 26 | back: 5c001cfb55e11ccb5b:55005c20002200000131b1e766cdb0a46673b4a66227b4a46795b8b36e67b8b16e65bcb36a67bcb16a65a0b37667a0b17665a4b37267a4b17265a8b37e67a8b17e65acb37a67acb17a6591a1677582a3574786a1607685905347bbb0 27 | volumeup: 5c001c5135945ccb5b:55005c2000220000016bf7a5248ff2ea283dfaf43075e6f231c3eee13c35ea9f404b929148459e974c4386915445829f584b8aa16075b6a76473bea16c75babf706ba2b17865aeb77c6397a3657780ad594988b3426497867551adf6 28 | volumedown: 5c001c27015548cb5b:55005c200022000001ecfbb1309be6f63421e6f03471e2fe3dcfe2e53831eee33c37eee53c31ea9b404f8aa56071b6a36477b6a56471b2ab687fb2a56871bea36c77bea56c71babb706f9bb7716394b1455597b77660908a495da1fa 29 | up: 5c001c6da5582ccb5b:55005c2000220000018482d759f28f99594f8b9b591c8f9d5cae838a6960bfb46962bbb66964bfb06966a3b27968afbc796aabbe796cafb8796ea3ba6970bfa46972bba66974bfa0697682b0786a9dbe4858998f786d9eba4b5ca083 30 | down: 5c001c6e464813cb5b:55005c2000220000015dbeea6dc6bbad6d78bfaf7530a3b17082afa67d74aba07d76afa27578a3ac757abfae6d7cbba86d7ebfaa1500c3d41502cfd61d04cbd01d06cfd21508c3dc150afecc0c1ee9ca3c2ceef81401f1d52730ccbf 31 | left: 5c001c03de3882cb5b:55005c200022000001439ace51fa8797514483955115879551408b975118879954aa838e515c8788515e9b8a7160a7b47162a3b67164a7b07166abb27168a7bc716aa3be716ca7b8716e9aa87044b3906236b0956547b3e41232ce9b 32 | right: 5c001c94411f33cb5b:55005c20002200000123affd7cd7aac20015d2cc084ddeca09fbd6d9040dd2d70803dac9101dc6cf141bdec90c1ddad71003c2d9180dcedf1c0bc6d9140dc2d71803cae9203df6ef243bcffb3d2fd8c53121e0c83a1ceffd0d29d5ae 33 | back: 5c001c044fdde2cb5b:55005c200022000001c2bdeb6ac1bca86a7fb8aa6e2bb8a86b99b4bf626bb4bd6269b0bf666bb0bd6669acbf7a6bacbd7a69a8bf7e6ba8bd7e69a4bf726ba4bd7269a0bf766ba0bd76699dad6b798eaf5b4b8aae5f79899f6c48b7bc -------------------------------------------------------------------------------- /custom_components/meizu_ble/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "meizu_ble", 3 | "name": "\u9B45\u65CF\u667A\u80FD\u9065\u63A7\u5668", 4 | "version": "1.0", 5 | "config_flow": true, 6 | "documentation": "https://github.com/shaonianzhentan/meizu_ble", 7 | "requirements": [ 8 | "bluepy==1.3.0" 9 | ], 10 | "dependencies": [], 11 | "codeowners": [ 12 | "@shaonianzhentan" 13 | ] 14 | } -------------------------------------------------------------------------------- /custom_components/meizu_ble/meizu.py: -------------------------------------------------------------------------------- 1 | import sys, time 2 | from bluepy.btle import Peripheral 3 | from binascii import a2b_hex 4 | from threading import Lock 5 | from datetime import datetime 6 | 7 | SERVICE_UUID = "000016f2-0000-1000-8000-00805f9b34fb" 8 | 9 | class MZBtIr(object): 10 | 11 | def __init__(self, mac, min_update_inteval=300): 12 | """ 13 | Initialize a Meizu for the given MAC address. 14 | """ 15 | self._mac = mac 16 | self._lock = Lock() 17 | self._sequence = 0 18 | if min_update_inteval < 60: 19 | self._min_update_inteval = 60 20 | else: 21 | self._min_update_inteval = min_update_inteval 22 | self._last_update = None 23 | self._temperature = None 24 | self._humidity = None 25 | self._battery = None 26 | self._receive_handle = None 27 | self._receive_buffer = None 28 | self._received_packet = 0 29 | self._total_packet = -1 30 | 31 | def get_sequence(self): 32 | self._sequence = self._sequence + 1 33 | if self._sequence > 255: 34 | self._sequence = 0 35 | return self._sequence 36 | 37 | def temperature(self): 38 | if self._temperature == None: 39 | return 0 40 | return self._temperature 41 | 42 | def humidity(self): 43 | if self._humidity == None: 44 | return 0 45 | return self._humidity 46 | 47 | def battery(self): 48 | v = self.voltage() 49 | b = int((v - 2.3) / 1.3 * 100) 50 | if b < 0: 51 | b = 0 52 | return b 53 | 54 | def voltage(self): 55 | if self._battery == None: 56 | return 0 57 | return self._battery 58 | 59 | def update(self, force_update=False): 60 | if force_update or (self._last_update is None) or (datetime.now() - self._min_update_inteval > self._last_update): 61 | self._lock.acquire() 62 | p = None 63 | try: 64 | p = Peripheral(self._mac, "public") 65 | chList = p.getCharacteristics() 66 | for ch in chList: 67 | if str(ch.uuid) == SERVICE_UUID: 68 | if p.writeCharacteristic(ch.getHandle(), b'\x55\x03' + bytes([self.get_sequence()]) + b'\x11', True): 69 | data = ch.read() 70 | humihex = data[6:8] 71 | temphex = data[4:6] 72 | temp10 = int.from_bytes(temphex, byteorder='little') 73 | humi10 = int.from_bytes(humihex, byteorder='little') 74 | self._temperature = float(temp10) / 100.0 75 | self._humidity = float(humi10) / 100.0 76 | if p.writeCharacteristic(ch.getHandle(), b'\x55\x03' + bytes([self.get_sequence()]) + b'\x10', True): 77 | data = ch.read() 78 | battery10 = data[4] 79 | self._battery = float(battery10) / 10.0 80 | break 81 | except Exception as ex: 82 | print("【{}】Unexpected error: {}".format(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), ex)) 83 | finally: 84 | if p is not None: 85 | p.disconnect() 86 | self._lock.release() 87 | 88 | def sendIrRaw(self, data): 89 | ir = data.strip() 90 | arr = ir.split(':', 1) 91 | return self.sendIr(arr[0], arr[1]) 92 | 93 | def sendIr(self, key, ir_data): 94 | self._lock.acquire() 95 | sent = False 96 | p = None 97 | try: 98 | p = Peripheral(self._mac, "public") 99 | chList = p.getCharacteristics() 100 | for ch in chList: 101 | if str(ch.uuid) == SERVICE_UUID: 102 | sequence = self.get_sequence() 103 | if p.writeCharacteristic(ch.getHandle(), b'\x55' + bytes([len(a2b_hex(key)) + 3, sequence]) + b'\x03' + a2b_hex(key), True): 104 | data = ch.read() 105 | if len(data) == 5 and data[4] == 1: 106 | sent = True 107 | else: 108 | send_list = [] 109 | packet_count = int(len(ir_data) / 30) + 1 110 | if len(data) % 30 != 0: 111 | packet_count = packet_count + 1 112 | send_list.append(b'\x55' + bytes([len(a2b_hex(key)) + 5, sequence]) + b'\x00' + bytes([0, packet_count]) + a2b_hex(key)) 113 | i = 0 114 | while i < packet_count - 1: 115 | send_ir_data = None 116 | if len(ir_data) - i * 30 < 30: 117 | send_ir_data = ir_data[i * 30:] 118 | else: 119 | send_ir_data = ir_data[i * 30:(i + 1) * 30] 120 | send_list.append(b'\x55' + bytes([int(len(send_ir_data) / 2 + 4), sequence]) + b'\x00' + bytes([i + 1]) + a2b_hex(send_ir_data)) 121 | i = i + 1 122 | error = False 123 | for j in range(len(send_list)): 124 | r = p.writeCharacteristic(ch.getHandle(), send_list[j], True) 125 | if r == False: 126 | error = True 127 | break 128 | if error == False: 129 | sent = True 130 | break 131 | except Exception as ex: 132 | print("【{}】Unexpected error: {}".format(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), ex)) 133 | finally: 134 | if p is not None: 135 | p.disconnect() 136 | self._lock.release() 137 | return sent 138 | 139 | def receiveIr(self, timeout=15): 140 | self._lock.acquire() 141 | self._receive_handle = None 142 | self._receive_buffer = False 143 | self._received_packet = 0 144 | self._total_packet = -1 145 | try: 146 | p = Peripheral(self._mac, "public") 147 | chList = p.getCharacteristics() 148 | for ch in chList: 149 | if str(ch.uuid) == SERVICE_UUID: 150 | self._receive_handle = ch.getHandle() 151 | sequence = self.get_sequence() 152 | if p.writeCharacteristic(ch.getHandle(), b'\x55\x03' + bytes([sequence]) + b'\x05', True): 153 | data = ch.read() 154 | if len(data) == 4 and data[3] == 7: 155 | p.withDelegate(self) 156 | while self._received_packet != self._total_packet: 157 | if p.waitForNotifications(timeout) == False: 158 | self._receive_buffer = False 159 | break 160 | p.writeCharacteristic(ch.getHandle(), b'\x55\x03' + bytes([sequence]) + b'\x0b', True) 161 | p.writeCharacteristic(ch.getHandle(), b'\x55\x03' + bytes([sequence]) + b'\x06', True) 162 | else: 163 | self._receive_buffer = False 164 | break 165 | self._receive_handle = None 166 | p.disconnect() 167 | except Exception as ex: 168 | print("Unexpected error: {}".format(ex)) 169 | self._receive_handle = None 170 | self._receive_buffer = False 171 | p.disconnect() 172 | finally: 173 | self._lock.release() 174 | return self._receive_buffer 175 | 176 | def handleNotification(self, cHandle, data): 177 | if cHandle == self._receive_handle: 178 | if len(data) > 4 and data[3] == 9: 179 | if data[4] == 0: 180 | self._total_packet = data[5] 181 | self._receive_buffer = [] 182 | self._received_packet = self._received_packet + 1 183 | elif data[4] == self._received_packet: 184 | self._receive_buffer.extend(data[5:]) 185 | self._received_packet = self._received_packet + 1 186 | else: 187 | self._receive_buffer = False 188 | self._total_packet = -1 189 | -------------------------------------------------------------------------------- /custom_components/meizu_ble/meizu_ble.py: -------------------------------------------------------------------------------- 1 | import paho.mqtt.client as mqtt 2 | import json, time, hashlib, threading, random 3 | from shaonianzhentan import load_yaml 4 | from meizu import MZBtIr 5 | 6 | def md5(text): 7 | data = hashlib.md5(text.encode('utf-8')).hexdigest() 8 | return data[8:-8] 9 | 10 | # 读取配置 11 | config = load_yaml('meizu_ble.yaml') 12 | config_mqtt = config['mqtt'] 13 | client_id = "meizu_ble" 14 | HOST = config_mqtt['host'] 15 | PORT = int(config_mqtt['port']) 16 | USERNAME = config_mqtt['user'] 17 | PASSWORD = config_mqtt['password'] 18 | SCAN_INTERVAL = config.get('scan_interval', 300) 19 | # 自动发现 20 | discovery_topic = "homeassistant/status" 21 | # 读取红外码 22 | config_ir = load_yaml('ir.yaml') 23 | 24 | # 自动配置 25 | def auto_config(domain, data, mac): 26 | param = { 27 | "device":{ 28 | "name": "魅族智能遥控器", 29 | "identifiers": [ mac ], 30 | "model": mac, 31 | "sw_version": "1.0", 32 | "manufacturer":"shaonianzhentan" 33 | }, 34 | } 35 | param.update(data) 36 | client.publish(f"homeassistant/{domain}/{data['unique_id']}/config", payload=json.dumps(param), qos=0) 37 | 38 | # 自动发送信息 39 | def auto_publish(): 40 | for config_meizu in config['meizu']: 41 | mac = config_meizu['mac'] 42 | # 获取设备信息 43 | # print(mac) 44 | try: 45 | ble = MZBtIr(mac) 46 | ble.update() 47 | temperature = ble.temperature() 48 | humidity = ble.humidity() 49 | voltage = ble.voltage() 50 | battery = ble.battery() 51 | # 为 0 则不上报,防止异常数据 52 | if temperature != 0: 53 | client.publish(f"meizu_ble/{mac}/temperature", payload=temperature, qos=0) 54 | if humidity != 0: 55 | client.publish(f"meizu_ble/{mac}/humidity", payload=humidity, qos=0) 56 | # 如果电量大于0,则推送,否则会有异常信息 57 | if battery > 0: 58 | client.publish(f"meizu_ble/{mac}/battery", payload=battery, qos=0) 59 | attrs = { 60 | 'voltage': voltage, 61 | 'mac': mac 62 | } 63 | client.publish(f"meizu_ble/{mac}/battery/attrs", payload=json.dumps(attrs), qos=0) 64 | time.sleep(2) 65 | except Exception as ex: 66 | print(f"{mac}:出现异常") 67 | print(ex) 68 | 69 | global timer 70 | timer = threading.Timer(SCAN_INTERVAL, auto_publish) 71 | timer.start() 72 | 73 | # 自动发现配置 74 | def discovery_config(): 75 | options = ['初始化'] 76 | for key in config_ir: 77 | for ir_key in config_ir[key]: 78 | options.append(f"{key}_{ir_key}") 79 | 80 | # 读取配置 81 | for config_meizu in config['meizu']: 82 | name = config_meizu['name'] 83 | mac = config_meizu['mac'] 84 | 85 | select_unique_id = md5(f"{mac}红外遥控") 86 | command_topic = f"meizu_ble/{select_unique_id}/{mac}" 87 | client.subscribe(command_topic) 88 | # 自动配置红外遥控 89 | auto_config("select", { 90 | "unique_id": select_unique_id, 91 | "name": f"{name}红外遥控", 92 | "state_topic": f"meizu_ble/{mac}/irdata", 93 | "command_topic": command_topic, 94 | "options": options 95 | }, mac) 96 | # 自动配置温湿度传感器 97 | auto_config("sensor", { 98 | "unique_id": md5(f"{mac}温度"), 99 | "name": f"{name}温度", 100 | "state_topic": f"meizu_ble/{mac}/temperature", 101 | "unit_of_measurement": "°C", 102 | "device_class": "temperature", 103 | }, mac) 104 | auto_config("sensor", { 105 | "unique_id": md5(f"{mac}湿度"), 106 | "name": f"{name}湿度", 107 | "state_topic": f"meizu_ble/{mac}/humidity", 108 | "unit_of_measurement": "%", 109 | "device_class": "humidity" 110 | }, mac) 111 | auto_config("sensor", { 112 | "unique_id": md5(f"{mac}电量"), 113 | "name": f"{name}电量", 114 | "state_topic": f"meizu_ble/{mac}/battery", 115 | "json_attributes_topic": f"meizu_ble/{mac}/battery/attrs", 116 | "unit_of_measurement": "%", 117 | "device_class": "battery" 118 | }, mac) 119 | 120 | def on_connect(client, userdata, flags, rc): 121 | print("Connected with result code "+str(rc)) 122 | client.subscribe(discovery_topic) 123 | discovery_config() 124 | # 定时执行获取设备信息 125 | timer = threading.Timer(10, auto_publish) 126 | timer.start() 127 | 128 | # 发送红外命令 129 | ir_counter = 0 130 | def send_irdata(mac, ir_command): 131 | global ir_counter 132 | ble = MZBtIr(mac) 133 | result = ble.sendIrRaw(ir_command) 134 | if result: 135 | print('红外命令发送成功') 136 | ir_counter = 0 137 | else: 138 | print(f"{mac}:出现异常,正在重试: {ir_counter}") 139 | # 出现异常,进行重试 140 | if ir_counter < 2: 141 | time.sleep(0.1) 142 | ir_counter = ir_counter + 1 143 | send_irdata(mac, ir_command) 144 | 145 | def on_message(client, userdata, msg): 146 | payload = str(msg.payload.decode('utf-8')) 147 | print("主题:" + msg.topic + " 消息:" + payload) 148 | # 自动发现配置 149 | if msg.topic == discovery_topic and payload == 'online': 150 | discovery_config() 151 | return 152 | # 发送红外命令 153 | arr = msg.topic.split('/') 154 | mac = arr[len(arr)-1] 155 | arr = payload.split('_', 1) 156 | if len(arr) == 2: 157 | device = arr[0] 158 | command = arr[1] 159 | print(mac, device, command) 160 | if device in config_ir: 161 | if command in config_ir[device]: 162 | print('发送红外命令') 163 | ir_command = config_ir[device][command] 164 | send_irdata(mac, ir_command) 165 | # 重置数据 166 | client.publish(f"meizu_ble/{mac}/irdata", payload='初始化', qos=0) 167 | 168 | def on_subscribe(client, userdata, mid, granted_qos): 169 | print("On Subscribed: qos = %d" % granted_qos) 170 | 171 | def on_disconnect(client, userdata, rc): 172 | if rc != 0: 173 | print("Unexpected disconnection %s" % rc) 174 | global timer 175 | timer.cancel() 176 | 177 | client = mqtt.Client(client_id) 178 | client.username_pw_set(USERNAME, PASSWORD) 179 | client.on_connect = on_connect 180 | client.on_message = on_message 181 | client.on_subscribe = on_subscribe 182 | client.on_disconnect = on_disconnect 183 | client.connect(HOST, PORT, 60) 184 | client.loop_forever() -------------------------------------------------------------------------------- /custom_components/meizu_ble/meizu_ble.yaml: -------------------------------------------------------------------------------- 1 | scan_interval: 300 2 | mqtt: 3 | host: localhost 4 | port: 1883 5 | user: '' 6 | password: '' 7 | meizu: 8 | - name: 魅族智能遥控器 9 | mac: 68:3E:34:CC:DB:7A -------------------------------------------------------------------------------- /custom_components/meizu_ble/remote.py: -------------------------------------------------------------------------------- 1 | import time, asyncio 2 | import logging 3 | import voluptuous as vol 4 | from datetime import timedelta 5 | from homeassistant.util.dt import utcnow 6 | from .shaonianzhentan import save_yaml, load_yaml 7 | from .meizu import MZBtIr 8 | from .const import DOMAIN, VERSION 9 | 10 | from homeassistant.components.remote import ( 11 | PLATFORM_SCHEMA, 12 | ATTR_DELAY_SECS, 13 | ATTR_NUM_REPEATS, 14 | DEFAULT_DELAY_SECS, 15 | RemoteEntity, 16 | ) 17 | 18 | from homeassistant.const import CONF_NAME, CONF_MAC 19 | import homeassistant.helpers.config_validation as cv 20 | 21 | DEFAULT_NAME = "魅族智能遥控器" 22 | 23 | _LOGGER = logging.getLogger(__name__) 24 | 25 | async def async_setup_entry(hass, entry, async_add_entities): 26 | config = entry.data 27 | name = config.get(CONF_NAME) 28 | mac = config.get(CONF_MAC) 29 | async_add_entities([MeizuRemote(mac, name, hass)], True) 30 | 31 | class MeizuRemote(RemoteEntity): 32 | 33 | def __init__(self, mac, name, hass): 34 | self.hass = hass 35 | self._mac = mac 36 | self._name = name 37 | self.config_file = hass.config.path("custom_components/meizu_ble/ir.yaml") 38 | self.ble = MZBtIr(mac) 39 | 40 | @property 41 | def name(self): 42 | return self._name 43 | 44 | @property 45 | def unique_id(self): 46 | return self._mac 47 | 48 | @property 49 | def device_info(self): 50 | mac = self._mac 51 | return { 52 | "configuration_url": "https://github.com/shaonianzhentan/meizu_ble", 53 | "identifiers": { 54 | (DOMAIN, mac) 55 | }, 56 | "name": self._name, 57 | "manufacturer": "Meizu", 58 | "model": mac, 59 | "sw_version": VERSION, 60 | "via_device": (DOMAIN, mac), 61 | } 62 | 63 | @property 64 | def is_on(self): 65 | return True 66 | 67 | @property 68 | def should_poll(self): 69 | return False 70 | 71 | async def async_turn_on(self, activity: str = None, **kwargs): 72 | """Turn the remote on.""" 73 | 74 | async def async_turn_off(self, activity: str = None, **kwargs): 75 | """Turn the remote off.""" 76 | 77 | async def async_send_command(self, command, **kwargs): 78 | device = kwargs.get('device', '') 79 | if device == '': 80 | return 81 | key = command[0] 82 | # 读取配置文件 83 | command_list = load_yaml(self.config_file) 84 | if device != '': 85 | dev = command_list.get(device, {}) 86 | # 判断配置是否存在 87 | if key in dev: 88 | ir_command = dev[key] 89 | else: 90 | ir_command = key 91 | self.ble.sendIrRaw(ir_command) 92 | return 93 | 94 | async def async_learn_command(self, **kwargs): 95 | print('未测试通过') 96 | # try: 97 | # data = self.ble.receiveIr() 98 | # bb = bytes(data) 99 | # self.hass.components.persistent_notification.async_create(f''' 100 | # 收到的红外码是:{bb.hex()} 101 | # 建议共享到公共库:https://github.com/shaonianzhentan/meizu_ble/issues/1 102 | # ''', title="魅族智能遥控器") 103 | # except Exception as ex: 104 | # self.hass.components.persistent_notification.async_create(f"录码失败,请隔近一点再试试", title="魅族智能遥控器") -------------------------------------------------------------------------------- /custom_components/meizu_ble/sensor.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import logging, asyncio 3 | 4 | import voluptuous as vol 5 | 6 | from homeassistant.helpers.event import async_track_time_interval 7 | from homeassistant.components.sensor import SensorEntity 8 | from homeassistant.const import ( 9 | CONF_NAME, 10 | CONF_MAC, 11 | CONF_SCAN_INTERVAL, 12 | DEVICE_CLASS_HUMIDITY, 13 | DEVICE_CLASS_TEMPERATURE, 14 | DEVICE_CLASS_BATTERY, 15 | PERCENTAGE, 16 | ) 17 | import homeassistant.helpers.config_validation as cv 18 | 19 | from .meizu import MZBtIr 20 | from .const import DOMAIN, VERSION 21 | 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | SENSOR_TEMPERATURE = "temperature" 25 | SENSOR_HUMIDITY = "humidity" 26 | SENSOR_BATTERY = "battery" 27 | 28 | SENSOR_TYPES = { 29 | SENSOR_TEMPERATURE: ["温度", None, DEVICE_CLASS_TEMPERATURE], 30 | SENSOR_HUMIDITY: ["湿度", PERCENTAGE, DEVICE_CLASS_HUMIDITY], 31 | SENSOR_BATTERY: ["电量", PERCENTAGE, DEVICE_CLASS_BATTERY], 32 | } 33 | 34 | async def async_setup_entry(hass, entry, async_add_entities): 35 | config = entry.data 36 | SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit 37 | name = config[CONF_NAME] 38 | mac = config.get(CONF_MAC) 39 | client = MZBtIr(mac) 40 | 41 | dev = [ 42 | MeizuBLESensor( 43 | client, 44 | SENSOR_TEMPERATURE, 45 | SENSOR_TYPES[SENSOR_TEMPERATURE][1], 46 | name, 47 | ), 48 | MeizuBLESensor( 49 | client, 50 | SENSOR_HUMIDITY, 51 | SENSOR_TYPES[SENSOR_HUMIDITY][1], 52 | name, 53 | ), 54 | MeizuBLESensor( 55 | client, 56 | SENSOR_BATTERY, 57 | SENSOR_TYPES[SENSOR_BATTERY][1], 58 | name, 59 | ) 60 | ] 61 | 62 | async_add_entities(dev, True) 63 | 64 | # 定时更新 65 | async def update_interval(now): 66 | client.update() 67 | for ble in dev: 68 | ble.update() 69 | 70 | async_track_time_interval(hass, update_interval, timedelta(seconds=config.get(CONF_SCAN_INTERVAL))) 71 | 72 | class MeizuBLESensor(SensorEntity): 73 | """Implementation of the DHT sensor.""" 74 | 75 | def __init__( 76 | self, 77 | client, 78 | sensor_type, 79 | temp_unit, 80 | name, 81 | ): 82 | """Initialize the sensor.""" 83 | self.client_name = name 84 | self._name = SENSOR_TYPES[sensor_type][0] 85 | self.client = client 86 | self.temp_unit = temp_unit 87 | self.type = sensor_type 88 | self._state = None 89 | self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] 90 | self._attr_device_class = SENSOR_TYPES[sensor_type][2] 91 | self._attributes = {} 92 | 93 | @property 94 | def unique_id(self): 95 | return f"{self.client._mac}{self.type}" 96 | 97 | @property 98 | def device_info(self): 99 | mac = self.client._mac 100 | return { 101 | "configuration_url": "https://github.com/shaonianzhentan/meizu_ble", 102 | "identifiers": { 103 | (DOMAIN, mac) 104 | }, 105 | "name": self.client_name, 106 | "manufacturer": "Meizu", 107 | "model": mac, 108 | "sw_version": VERSION, 109 | "via_device": (DOMAIN, mac), 110 | } 111 | 112 | @property 113 | def name(self): 114 | """Return the name of the sensor.""" 115 | return f"{self.client_name}{self._name}" 116 | 117 | @property 118 | def state(self): 119 | """Return the state of the sensor.""" 120 | return self._state 121 | 122 | @property 123 | def unit_of_measurement(self): 124 | """Return the unit of measurement of this entity, if any.""" 125 | return self._unit_of_measurement 126 | 127 | @property 128 | def extra_state_attributes(self): 129 | return self._attributes 130 | 131 | def update(self): 132 | state = 0 133 | # 显示数据 134 | if self.type == SENSOR_TEMPERATURE: 135 | state = self.client.temperature() 136 | elif self.type == SENSOR_HUMIDITY: 137 | state = self.client.humidity() 138 | elif self.type == SENSOR_BATTERY: 139 | state = self.client.battery() 140 | self._attributes.update({ 'voltage': self.client.voltage(), 'mac': self.client._mac }) 141 | # 数据大于0,则更新 142 | if state > 0: 143 | self._state = state -------------------------------------------------------------------------------- /custom_components/meizu_ble/shaonianzhentan.py: -------------------------------------------------------------------------------- 1 | import yaml, asyncio, hashlib, os 2 | 3 | # MD5加密 4 | def md5(data): 5 | return hashlib.md5(data.encode(encoding='UTF-8')).hexdigest() 6 | 7 | # 执行异步方法 8 | def async_create_task(async_func): 9 | loop = asyncio.get_event_loop() 10 | loop.run_until_complete(async_func) 11 | 12 | # 加载yaml 13 | def load_yaml(file_path): 14 | # 不存在则返回空字典 15 | if os.path.exists(file_path) == False: 16 | return {} 17 | fs = open(file_path, encoding="UTF-8") 18 | data = yaml.load(fs, Loader=yaml.FullLoader) 19 | return data 20 | 21 | # 存储为yaml 22 | def save_yaml(file_path, data): 23 | _dict = {} 24 | _dict.update(data) 25 | with open(file_path, 'w') as f: 26 | yaml.dump(_dict, f) -------------------------------------------------------------------------------- /custom_components/meizu_ble/test.py: -------------------------------------------------------------------------------- 1 | #from meizu import MZBtIr 2 | 3 | #ble = MZBtIr('68:3E:34:CC:E0:67') 4 | 5 | #print('开始录码') 6 | #print(ble.receiveIr()) 7 | # ble.sendIr("5c001cc8d94613bb5a", "55005c200022000001becf651db7cbdd1d0bcfdf2560f3e120d2fff62d24fbf02d26fff22528f3fc252aeffe3d2cebf83d2eeffa2530f3e42532ffe62d34fbe02d36ffe22538f3ec253acecc3c2ddaca3c2fddcb7751a28644609ccf") 8 | 9 | 10 | # [98, 6, 24, 1, 65, 1, 1, 2, 1, 1, 1, 0, 170, 5, 110, 0, 21, 0, 163, 0, 21, 2, 1, 0, 7, 2, 116, 0, 3, 1, 56, 0, 1, 255, 255, 254] 11 | 12 | def handleNotification(data): 13 | _received_packet = 0 14 | _receive_buffer = [] 15 | _total_packet = 0 16 | if len(data) > 4 and data[3] == 9: 17 | if data[4] == 0: 18 | _total_packet = data[5] 19 | _receive_buffer = [] 20 | _received_packet = _received_packet + 1 21 | elif data[4] == _received_packet: 22 | _receive_buffer.extend(data[5:]) 23 | _received_packet = _received_packet + 1 24 | else: 25 | _receive_buffer = False 26 | _total_packet = -1 27 | print(_received_packet) 28 | print(_total_packet) 29 | print(_receive_buffer) 30 | 31 | 32 | bb = bytes([98, 6, 24, 1, 65, 1, 1, 2, 1, 1, 1, 0, 170, 5, 110, 0, 21, 0, 163, 0, 21, 2, 1, 0, 7, 2, 116, 0, 3, 1, 56, 0, 1, 255, 255, 254]) 33 | print(bb.hex()) 34 | -------------------------------------------------------------------------------- /custom_components/meizu_ble/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "魅族智能遥控器", 6 | "description": "项目: https://github.com/shaonianzhentan/meizu_ble", 7 | "data": { 8 | "name": "设备名称", 9 | "mac": "蓝牙MAC地址(68:3E:34:CC:E0:67)", 10 | "scan_interval": "扫描时间" 11 | } 12 | } 13 | }, 14 | "error": {} 15 | } 16 | } -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "魅族智能遥控器", 3 | "country": "CN", 4 | "render_readme": true, 5 | "domains": [] 6 | } -------------------------------------------------------------------------------- /meizu_ir_reader_from_android/README.md: -------------------------------------------------------------------------------- 1 | # Android手机蓝牙抓包 2 | 3 | 请先参考原项目:https://github.com/junnikokuki/Meizu-BLE-Thermometer/tree/master/meizu_ir_reader_from_android 4 | 5 | ## 关于Android设备获取蓝牙调试日志 `btsnoop_hci.log` 的方法 6 | 7 | 由于原项目已经过去了2年之久,部分操作方法可能已经不适用于现在的Android设备了,如果使用原项目的操作方式不行的话,可以试试以下操作方式 8 | 9 | > 小米手机抓取蓝牙日志(使用MI 9 测试正常) 10 | 1. 打开开发者选项,打开蓝牙调试日志和蓝牙数据包日志开关 11 | 1. 在拨号盘输入一次 `*#*#5959#*#*` 即开始抓蓝牙日志 12 | 1. 在魅家添加需要获取遥控码的设备,并把需要获取的按键按顺序按一遍(记住顺序) 13 | 1. 再拨号盘输入一次 `*#*#5959#*#*` 14 | 1. 等待大概半分钟,在文件管理器中 `/sdcard/MIUI/debug_log`下会生成`bugreport-当前时间.zip`调试文件 15 | 1. 解压调试文件,然后找到解压目录中的文件 `common/com.android.bluetooth/btsnoop_hci.log` 16 | 1. 将本项目clone到本地 17 | 1. 将`btsnoop_hci.log`放到当前目录,与`irdatareader.py`同级 18 | 1. 然后执行`python3 irdatareader.py` 19 | 1. 如果没啥毛病,就会显示红外码了 20 | 21 | 22 | 注意:不知道是我的手机蓝牙抓包问题,还是程序解析的问题,只能解析最后一个红外码,有的手机解析正常 -------------------------------------------------------------------------------- /meizu_ir_reader_from_android/btsnoop/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # btsnoop module 3 | # 4 | # TODO: Move stuff here to their corresponding modules 5 | # 6 | import binascii 7 | from . import btsnoop as bts 8 | from .bt import hci_cmd as hci_cmd 9 | from .bt import hci_uart as hci_uart 10 | -------------------------------------------------------------------------------- /meizu_ir_reader_from_android/btsnoop/bt/__init__.py: -------------------------------------------------------------------------------- 1 | from . import hci 2 | from . import hci_uart 3 | from . import hci_cmd 4 | from . import hci_evt 5 | from . import hci_sco 6 | from . import hci_acl 7 | from . import l2cap 8 | from . import att 9 | from . import smp -------------------------------------------------------------------------------- /meizu_ir_reader_from_android/btsnoop/bt/att.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse ATT packets 3 | """ 4 | import struct 5 | 6 | 7 | """ 8 | ATT PDUs 9 | 10 | References can be found here: 11 | * https://www.bluetooth.org/en-us/specification/adopted-specifications - Core specification 4.1 12 | ** [vol 3] Part F (Section 3.4.8) - Attribute Opcode Summary 13 | """ 14 | ATT_PDUS = { 15 | 0x01 : "ATT Error_Response", 16 | 0x02 : "ATT Exchange_MTU_Request", 17 | 0x03 : "ATT Exchange_MTU_Response", 18 | 0x04 : "ATT Find_Information_Request", 19 | 0x05 : "ATT Find_Information_Response", 20 | 0x06 : "ATT Find_By_Type_Value_Request", 21 | 0x07 : "ATT Find_By_Type_Value_Response", 22 | 0x08 : "ATT Read_By_Type_Request", 23 | 0x09 : "ATT Read_By_Type_Response", 24 | 0x0A : "ATT Read_Request", 25 | 0x0B : "ATT Read_Response", 26 | 0x0C : "ATT Read_Blob_Request", 27 | 0x0D : "ATT Read_Blob_Response", 28 | 0x0E : "ATT Read_Multiple_Request", 29 | 0x0F : "ATT Read_Multiple_Response", 30 | 0x10 : "ATT Read_By_Group_Type_Request", 31 | 0x11 : "ATT Read_By_Group_Type_Response", 32 | 0x12 : "ATT Write_Request", 33 | 0x13 : "ATT Write_Response", 34 | 0x52 : "ATT Write_Command", 35 | 0xD2 : "ATT Signed_Write_Command", 36 | 0x16 : "ATT Prepare_Write_Request", 37 | 0x17 : "ATT Prepare_Write_Response", 38 | 0x18 : "ATT Execute_Write_Request", 39 | 0x19 : "ATT Execute_Write_Response", 40 | 0x1B : "ATT Handle_Value_Notification", 41 | 0x1D : "ATT Handle_Value_Indication", 42 | 0x1E : "ATT Handle_Value_Confirmation" 43 | } 44 | 45 | 46 | def parse(data): 47 | """ 48 | Attribute opcode is the first octet of the PDU 49 | 50 | 0 1 2 3 4 5 6 7 51 | ----------------- 52 | | att opcode | 53 | ----------------- 54 | | a |b|c| 55 | ----------------- 56 | a - method 57 | b - command flag 58 | c - authentication signature flag 59 | 60 | References can be found here: 61 | * https://www.bluetooth.org/en-us/specification/adopted-specifications - Core specification 4.1 62 | ** [vol 3] Part F (Section 3.3) - Attribute PDU 63 | 64 | Return a tuple (opcode, data) 65 | """ 66 | opcode = struct.unpack(" 5 | """ 6 | import datetime 7 | import sys 8 | import struct 9 | 10 | 11 | """ 12 | Record flags conform to: 13 | - bit 0 0 = sent, 1 = received 14 | - bit 1 0 = data, 1 = command/event 15 | - bit 2-31 reserved 16 | 17 | Direction is relative to host / DTE. i.e. for Bluetooth controllers, 18 | Send is Host->Controller, Receive is Controller->Host 19 | """ 20 | BTSNOOP_FLAGS = { 21 | 0 : ("host", "controller", "data"), 22 | 1 : ("controller", "host", "data"), 23 | 2 : ("host", "controller", "command"), 24 | 3 : ("controller", "host", "event") 25 | } 26 | 27 | 28 | def parse(filename): 29 | """ 30 | Parse a Btsnoop packet capture file. 31 | 32 | Btsnoop packet capture file is structured as: 33 | 34 | ----------------------- 35 | | header | 36 | ----------------------- 37 | | packet record nbr 1 | 38 | ----------------------- 39 | | packet record nbr 2 | 40 | ----------------------- 41 | | ... | 42 | ----------------------- 43 | | packet record nbr n | 44 | ----------------------- 45 | 46 | References can be found here: 47 | * http://tools.ietf.org/html/rfc1761 48 | * http://www.fte.com/webhelp/NFC/Content/Technical_Information/BT_Snoop_File_Format.htm 49 | 50 | Return a list of records, each holding a tuple of: 51 | * sequence nbr 52 | * record length (in bytes) 53 | * flags 54 | * timestamp 55 | * data 56 | """ 57 | with open(filename, "rb") as f: 58 | 59 | # Validate file header 60 | (identification, version, type) = _read_file_header(f) 61 | _validate_file_header(identification, version, type) 62 | 63 | # Not using the following data: 64 | # record[1] - original length 65 | # record[4] - cumulative drops 66 | return list(map(lambda record: 67 | (record[0], record[2], record[3], _parse_time(record[5]), record[6]), 68 | _read_packet_records(f))) 69 | 70 | 71 | def _read_file_header(f): 72 | """ 73 | Header should conform to the following format 74 | 75 | ---------------------------------------- 76 | | identification pattern| 77 | | 8 bytes | 78 | ---------------------------------------- 79 | | version number | 80 | | 4 bytes | 81 | ---------------------------------------- 82 | | data link type = HCI UART (H4) | 83 | | 4 bytes | 84 | ---------------------------------------- 85 | 86 | All integer values are stored in "big-endian" order, with the high-order bits first. 87 | """ 88 | ident = f.read(8) 89 | version, data_link_type = struct.unpack( ">II", f.read(4 + 4) ) 90 | return (ident, version, data_link_type) 91 | 92 | 93 | def _validate_file_header(identification, version, data_link_type): 94 | """ 95 | The identification pattern should be: 96 | 'btsnoop\0' 97 | 98 | The version number should be: 99 | 1 100 | 101 | The data link type can be: 102 | - Reserved 0 - 1000 103 | - Un-encapsulated HCI (H1) 1001 104 | - HCI UART (H4) 1002 105 | - HCI BSCP 1003 106 | - HCI Serial (H5) 1004 107 | - Unassigned 1005 - 4294967295 108 | 109 | For SWAP, data link type should be: 110 | HCI UART (H4) 1002 111 | """ 112 | 113 | assert identification == b"btsnoop\x00" 114 | assert version == 1 115 | assert data_link_type == 1002 116 | print("Btsnoop capture file version {0}, type {1}".format(version, data_link_type)) 117 | 118 | 119 | def _read_packet_records(f): 120 | """ 121 | A record should confirm to the following format 122 | 123 | -------------------------- 124 | | original length | 125 | | 4 bytes 126 | -------------------------- 127 | | included length | 128 | | 4 bytes 129 | -------------------------- 130 | | packet flags | 131 | | 4 bytes 132 | -------------------------- 133 | | cumulative drops | 134 | | 4 bytes 135 | -------------------------- 136 | | timestamp microseconds | 137 | | 8 bytes 138 | -------------------------- 139 | | packet data | 140 | -------------------------- 141 | 142 | All integer values are stored in "big-endian" order, with the high-order bits first. 143 | """ 144 | seq_nbr = 1 145 | while True: 146 | pkt_hdr = f.read(4 + 4 + 4 + 4 + 8) 147 | if not pkt_hdr or len(pkt_hdr) != 24: 148 | # EOF 149 | break 150 | 151 | orig_len, inc_len, flags, drops, time64 = struct.unpack( ">IIIIq", pkt_hdr) 152 | assert orig_len == inc_len 153 | 154 | data = f.read(inc_len) 155 | assert len(data) == inc_len 156 | 157 | yield ( seq_nbr, orig_len, inc_len, flags, drops, time64, data ) 158 | seq_nbr += 1 159 | 160 | 161 | def _parse_time(time): 162 | """ 163 | Record time is a 64-bit signed integer representing the time of packet arrival, 164 | in microseconds since midnight, January 1st, 0 AD nominal Gregorian. 165 | 166 | In order to avoid leap-day ambiguity in calculations, note that an equivalent 167 | epoch may be used of midnight, January 1st 2000 AD, which is represented in 168 | this field as 0x00E03AB44A676000. 169 | """ 170 | time_betw_0_and_2000_ad = int("0x00E03AB44A676000", 16) 171 | time_since_2000_epoch = datetime.timedelta(microseconds=time) - datetime.timedelta(microseconds=time_betw_0_and_2000_ad) 172 | return datetime.datetime(2000, 1, 1) + time_since_2000_epoch 173 | 174 | 175 | def flags_to_str(flags): 176 | """ 177 | Returns a tuple of (src, dst, type) 178 | """ 179 | assert flags in [0,1,2,3] 180 | return BTSNOOP_FLAGS[flags] 181 | 182 | 183 | def print_hdr(): 184 | """ 185 | Print the script header 186 | """ 187 | print("") 188 | print("##############################") 189 | print("# #") 190 | print("# btsnoop parser v0.1 #") 191 | print("# #") 192 | print("##############################") 193 | print("") 194 | 195 | 196 | def main(filename): 197 | records = parse(filename) 198 | print(records) 199 | return 0 200 | 201 | 202 | if __name__ == "__main__": 203 | if len(sys.argv) < 2: 204 | print(__doc__) 205 | sys.exit(1) 206 | 207 | print_hdr() 208 | sys.exit(main(sys.argv[1])) -------------------------------------------------------------------------------- /meizu_ir_reader_from_android/irdatareader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import btsnoop.btsnoop.btsnoop as btsnoop 3 | import btsnoop.bt.hci_uart as hci_uart 4 | import btsnoop.bt.hci_acl as hci_acl 5 | import btsnoop.bt.l2cap as l2cap 6 | import btsnoop.bt.att as att 7 | 8 | def get_ir_infos(records): 9 | ir_infos = {} 10 | for record in records: 11 | seq_nbr = record[0] 12 | 13 | hci_pkt_type, hci_pkt_data = hci_uart.parse(record[4]) 14 | 15 | if hci_pkt_type != hci_uart.ACL_DATA: 16 | continue 17 | 18 | hci_data = hci_acl.parse(hci_pkt_data) 19 | l2cap_length, l2cap_cid, l2cap_data = l2cap.parse(hci_data[2], hci_data[4]) 20 | 21 | if l2cap_cid != l2cap.L2CAP_CID_ATT: 22 | continue 23 | 24 | att_opcode, att_data = att.parse(l2cap_data) 25 | 26 | data = att_data 27 | 28 | if att_opcode == 0x12: 29 | if len(data) > 6: 30 | if data[2] == 0x55 and data[5] == 0x00: #real ir data 31 | ir_seq = int(data[4]) 32 | send_seq = int(data[6]) 33 | if send_seq == 0: 34 | packet_count = int(data[7]) 35 | ir_id = data[8:].hex() 36 | ir_data_list = [] 37 | index = 1 38 | while index < packet_count: 39 | ir_data_list.append('') 40 | index = index + 1 41 | ir_info = {'id':ir_id, 'data':ir_data_list } 42 | ir_infos[ir_seq] = ir_info 43 | else: 44 | ir_info = ir_infos[ir_seq] 45 | if send_seq <= len(ir_info['data']): 46 | ir_data = data[7:].hex() 47 | ir_data_list = ir_info['data'] 48 | ir_data_list[send_seq - 1] = ir_data 49 | ir_info['data'] = ir_data_list 50 | ir_infos[ir_seq] = ir_info 51 | else: 52 | continue 53 | 54 | sorted_keys = sorted(ir_infos.keys()) 55 | for k in sorted_keys: 56 | ir_info = ir_infos[k] 57 | print(ir_info['id'] + ':' + ''.join(ir_info['data'])) 58 | print(' ') 59 | 60 | if __name__ == "__main__": 61 | 62 | filename = os.path.expanduser("btsnoop_hci.log") 63 | if not os.path.isfile(filename): 64 | print("btsnoop_hci.log 文件不存在") 65 | sys.exit(-1) 66 | 67 | records = btsnoop.parse(filename) 68 | get_ir_infos(records) -------------------------------------------------------------------------------- /remote/README.md: -------------------------------------------------------------------------------- 1 | # 红外遥控器 2 | 3 | 遥控器使用说明(由于录码比较繁琐,建议大家贡献自己录的码,然后集成到插件之中使用) 4 | 5 | 下面是大家贡献的设备与命令,通过调用 `remote.send_command` 服务传入设备与命令即可 6 | 7 | ```yaml 8 | 创维电视: 9 | power: 开关机 10 | down: 下一频道 11 | 海信HZ65U7E: 12 | power: 开关机 13 | up: 上 14 | down: 下 15 | left: 左 16 | right: 右 17 | source: 信号来源 18 | enter: 确认 19 | back: 返回 20 | menu: 菜单 21 | volumedown: 音量减小 22 | volumeup: 音量增加 23 | mute: 静音 24 | sleep: 休眠 25 | 松下吸顶灯HKC9603: 26 | poweroff: 关 27 | poweron: 适悦按键 28 | 乐视超3X50: 29 | power: 开关 30 | menu: 菜单 31 | back: 返回 32 | volumeup: 音量+ 33 | volumedown: 音量- 34 | up: 上 35 | down: 下 36 | left: 左 37 | right: 右 38 | back: 返回 39 | ``` 40 | 41 | ### 松下吸顶灯HKC9603 42 | 43 | ![松下吸顶灯HKC9603](./松下吸顶灯HKC9603.jpg) -------------------------------------------------------------------------------- /remote/松下吸顶灯HKC9603.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/meizu_ble/0e44543e5e32ea6cd15255df2b777185a49a1d85/remote/松下吸顶灯HKC9603.jpg --------------------------------------------------------------------------------