├── README.md ├── main.py ├── misc ├── README.md ├── __init__.py ├── poll_conversation.py └── requirements.txt └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # 用小爱同学控制电脑 2 | 3 | ## 使用步骤 4 | 5 | 1. 搜索下载 "点灯 Blinker" app 6 | 2. 注册并登录,添加设备,选择 "独立设备",选择网络接入,复制这里的 key 7 | 3. `export AUTH_KEY=xxx` python3 main.py 8 | 4. 这时在点灯 app 中应该可以看到一个在线设备,为它起一个合适的名字,之后会这个名字来控制,我这里取名为 "PC" 9 | 5. 在米家 - 我的 - 连接其他平台 中选择 "点灯科技",绑定点灯的账号 10 | 6. 选择同步设备,即可将点灯中的设备同步到米家,现在就可以通过小爱同学来控制你的电脑了 11 | 12 | ## 指令 13 | 14 | 因为原理是将这台电脑模拟为一个台灯,用小爱同学控制它。所以可以通过小爱同学来控制开关、调节模式、控制色温 15 | 16 | 1. 关机:"小爱同学,将 PC 关机" 17 | 2. 静音:"小爱同学,将 PC 设为夜间模式" 18 | 3. 取消静音:"小爱同学,将 PC 设为日光模式" 19 | 4. 调节音量: "小爱同学,将 PC 的色温调到 5000" (色温的范围是 1000-10000,对应音量从0-100) 20 | 21 | ## 已知问题 22 | 23 | 目前 blinker 的 SDK 在收到小爱同学的请求时,回复指令的执行结果时会有问题,导致小爱同学总是会回复 "要操作的设备好像出问题了",但实际上操作时成功的。 24 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import subprocess 4 | 5 | from blinker import Device, ButtonWidget, NumberWidget 6 | from blinker.voice_assistant import VAType, VoiceAssistant, DataMessage, PowerMessage, ModeMessage, ColorTempMessage 7 | from ctypes import cast, POINTER 8 | from comtypes import CLSCTX_ALL 9 | from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume 10 | 11 | speakers = AudioUtilities.GetSpeakers() 12 | interface = speakers.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None) 13 | volume = cast(interface, POINTER(IAudioEndpointVolume)) 14 | 15 | device = Device(os.environ["AUTH_KEY"], mi_type=VAType.LIGHT) 16 | 17 | voice_assistant = VoiceAssistant(VAType.LIGHT) 18 | 19 | btn_shutdown = ButtonWidget('btn-shutdown') 20 | btn_mute = ButtonWidget('btn-mute') 21 | num_volume = NumberWidget('num-volume') 22 | num_brightness = NumberWidget('num-brightness') 23 | 24 | device.addVoiceAssistant(voice_assistant) 25 | device.addWidget(btn_shutdown) 26 | device.addWidget(btn_mute) 27 | device.addWidget(num_volume) 28 | device.addWidget(num_brightness) 29 | 30 | 31 | async def set_volume(msg): 32 | print(f"received volume: {msg}") 33 | vol = msg["num-volume"] 34 | volume.SetMasterVolumeLevelScalar(vol / 100, None) 35 | await num_volume.value(vol).update() 36 | 37 | 38 | # TODO control brightness 39 | async def set_brightness(msg): 40 | print(f"received brightness: {msg}") 41 | 42 | 43 | async def mute(msg): 44 | print(f"received mute: {msg}") 45 | if msg["btn-mute"] == "off": 46 | volume.SetMute(False, None) 47 | await btn_mute.turn("off").update() 48 | else: 49 | volume.SetMute(True, None) 50 | await btn_mute.turn("on").update() 51 | 52 | 53 | async def shutdown(msg): 54 | print(f"received shutdown: {msg}") 55 | await btn_shutdown.turn("off").update() 56 | await asyncio.sleep(1) 57 | subprocess.run(["shutdown", "/h"]) 58 | 59 | 60 | async def heartbeat_func(msg): 61 | print(f"received heartbeat: {msg}") 62 | muted = volume.GetMute() 63 | vol = int(volume.GetMasterVolumeLevelScalar() * 100) 64 | await btn_mute.turn("on" if muted else "off").update() 65 | await btn_shutdown.turn("on").update() 66 | await num_volume.value(vol).update() 67 | await num_brightness.value(0).update() 68 | 69 | 70 | async def ready_func(): 71 | # 获取设备配置信息 72 | print(vars(device.config)) 73 | 74 | 75 | btn_mute.func = mute 76 | btn_shutdown.func = shutdown 77 | num_volume.func = set_volume 78 | num_brightness.func = set_brightness 79 | device.heartbeat_callable = heartbeat_func 80 | device.ready_callable = ready_func 81 | 82 | # https://diandeng.tech/doc/xiaoai 83 | # 将这台电脑模拟为一个台灯,可以用小爱同学控制开关、调节模式、控制色温 84 | 85 | # 1. 将模式调节用来控制静音 86 | # "小爱同学,将 PC 设为日光模式" 87 | MODE_UNMUTE = 0 88 | # "小爱同学,将 PC 设为月光模式" 89 | MODE_MUTE = 1 90 | 91 | 92 | # 2. 将色温调节用来控制音量,色温范围为 1000-10000 93 | # "小爱同学,将 PC 的色温调节到 2000" 94 | 95 | def volume_to_color_temp(value: int) -> int: 96 | return int((value - 0) * (10000 - 1000) / (100 - 0) + 1000) 97 | 98 | 99 | def color_temp_to_volume(value: int) -> int: 100 | return int((value - 1000) * (100 - 0) / (10000 - 1000) + 0) 101 | 102 | 103 | async def voice_query_state(msg: DataMessage): 104 | print(f"received voice query: {vars(msg)}") 105 | match msg.data: 106 | case 'state': 107 | await msg.power('on') 108 | case 'mode': 109 | muted = volume.GetMute() 110 | await msg.mode(MODE_MUTE if muted else MODE_UNMUTE) 111 | case 'bright': 112 | pass 113 | case 'colTemp': 114 | vol = int(volume.GetMasterVolumeLevelScalar() * 100) 115 | col_temp = volume_to_color_temp(vol) 116 | await msg.colorTemp(col_temp) 117 | # await msg.update() 118 | 119 | 120 | async def voice_echo(msg): 121 | print(f"received voice message: {vars(msg)}") 122 | await msg.update() 123 | 124 | 125 | async def voice_shutdown(msg: PowerMessage): 126 | print(f"received shutdown message: {vars(msg)}") 127 | await msg.power('off') 128 | await msg.update() 129 | await asyncio.sleep(1) 130 | subprocess.run(["shutdown", "/h"]) 131 | 132 | 133 | async def voice_mute_unmute(msg: ModeMessage): 134 | print(f"received mute_unmute message: {vars(msg)}") 135 | if msg.data['mode'] == MODE_MUTE: 136 | volume.SetMute(True, None) 137 | await msg.mode(MODE_MUTE) 138 | else: 139 | volume.SetMute(False, None) 140 | await msg.mode(MODE_UNMUTE) 141 | await msg.update() 142 | 143 | 144 | async def voice_volume(msg: ColorTempMessage): 145 | print(f"received volume message: {vars(msg)}") 146 | col_temp = msg.data["colTemp"] 147 | vol = color_temp_to_volume(col_temp) 148 | volume.SetMasterVolumeLevelScalar(vol / 100, None) 149 | await msg.colorTemp(col_temp) 150 | await msg.update() 151 | 152 | 153 | voice_assistant.state_query_callable = voice_query_state 154 | voice_assistant.power_change_callable = voice_shutdown 155 | voice_assistant.mode_change_callable = voice_mute_unmute 156 | voice_assistant.colortemp_change_callable = voice_volume 157 | voice_assistant.brightness_change_callable = voice_echo 158 | voice_assistant.color_change_callable = voice_echo 159 | 160 | if __name__ == '__main__': 161 | device.run() 162 | -------------------------------------------------------------------------------- /misc/README.md: -------------------------------------------------------------------------------- 1 | # "Xiaoai, shutdown the computer" 2 | 3 | ```shell 4 | export MI_USER=xxx 5 | export MI_PASS=xxx 6 | 7 | python3 main.py devices 8 | 9 | # Get from previous output 10 | export HARDWARE=xxx 11 | export DEVICE_ID=xxx 12 | 13 | # Starts to listen for commands 14 | python3 main.py 15 | ``` 16 | -------------------------------------------------------------------------------- /misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j178/xiaoai-shutdown-my-computer/e2b2d5198556fd8827dcd278a360c7f732e48e6f/misc/__init__.py -------------------------------------------------------------------------------- /misc/poll_conversation.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import subprocess 5 | import sys 6 | import time 7 | from typing import Optional 8 | 9 | from miservice.miaccount import MiAccount 10 | from miservice.minaservice import MiNAService 11 | from aiohttp import ClientSession 12 | 13 | GET_CONVERSATION = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={timestamp}&limit=2" 14 | HARDWARE = os.environ.get("HARDWARE") 15 | # micli list to get MI_DID, needed in miio service 16 | MI_DID = os.environ.get("MI_DID") 17 | # get by mina service device list, needed in cookies to get conversation 18 | DEVICE_ID = os.environ.get("DEVICE_ID") 19 | 20 | 21 | async def list_devices(mina_service: MiNAService): 22 | devices = await mina_service.device_list() 23 | for device in devices: 24 | print("============") 25 | print("Name:", device["name"]) 26 | print("Alias:", device["alias"]) 27 | print("DeviceID:", device["deviceID"]) 28 | print("DID:", device["miotDID"]) 29 | print("Hardware:", device["hardware"]) 30 | print("SerialNumber:", device["serialNumber"]) 31 | 32 | 33 | async def main(): 34 | session = ClientSession() 35 | account = MiAccount( 36 | session, 37 | os.environ["MI_USER"], 38 | os.environ["MI_PASS"], 39 | ) 40 | print("Logging in") 41 | await account.login("micoapi") 42 | 43 | session.cookie_jar.update_cookies({"deviceId": DEVICE_ID}) 44 | mina_service = MiNAService(account) 45 | 46 | if len(sys.argv) > 1 and sys.argv[1] == "devices": 47 | await list_devices(mina_service) 48 | return 49 | 50 | print("Waiting for commands...") 51 | last_timestamp = int(time.time() * 1000) 52 | while True: 53 | try: 54 | question = await poll(session, last_timestamp) 55 | except Exception as e: 56 | print(f"poll error: {e!r}") 57 | else: 58 | if question: 59 | last_timestamp = question.get("time") 60 | query = question.get("query", "").strip() 61 | print(f"got query: {query}") 62 | await handle_command(query, mina_service) 63 | await asyncio.sleep(3) 64 | 65 | 66 | async def poll(session: ClientSession, last_timestamp: int) -> Optional[dict]: 67 | url = GET_CONVERSATION.format(hardware=HARDWARE, timestamp=int(time.time() * 1000)) 68 | resp = await session.get(url) 69 | try: 70 | data = await resp.json() 71 | except Exception as e: 72 | print(f"get conversation failed: {e!r}") 73 | return None 74 | 75 | d = json.loads(data["data"]) 76 | records = d.get("records") 77 | if not records: 78 | return None 79 | if records[0].get("time") > last_timestamp: 80 | return records[0] 81 | return None 82 | 83 | 84 | async def handle_command(command: str, mina_service: MiNAService): 85 | if "关电脑" in command or "关机" in command or "shutdown" in command: 86 | await mina_service.player_pause(DEVICE_ID) 87 | await mina_service.text_to_speech(DEVICE_ID, "正在关机中...") 88 | await asyncio.sleep(1) 89 | subprocess.run(["shutdown", "/h"]) 90 | 91 | 92 | if __name__ == '__main__': 93 | asyncio.run(main()) 94 | -------------------------------------------------------------------------------- /misc/requirements.txt: -------------------------------------------------------------------------------- 1 | miservice_fork 2 | aiohttp 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/blinker-iot/blinker-py.git 2 | comtypes 3 | pycaw 4 | --------------------------------------------------------------------------------