diff --git a/README.md b/README.md index 96220b4..012f241 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ XiaoMi Cloud Service for mi.com ## Install ``` +pip3 install aiohttp aiofiles pip3 install miservice ``` @@ -25,36 +26,38 @@ MiService:XiaoMi Cloud Service ## Command Line ``` +MiService 2.1.2 - XiaoMi Cloud Service + Usage: The following variables must be set: export MI_USER= export MI_PASS= export MI_DID= -Get Props: /usr/local/bin/micli.py [,...] - /usr/local/bin/micli.py 1,1-2,1-3,1-4,2-1,2-2,3 -Set Props: /usr/local/bin/micli.py [,...] - /usr/local/bin/micli.py 2=#60,2-2=#false,3=test -Do Action: /usr/local/bin/micli.py [...] - /usr/local/bin/micli.py 2 #NA - /usr/local/bin/micli.py 5 Hello - /usr/local/bin/micli.py 5-4 Hello #1 +Get Props: ./micli.py [,...] + ./micli.py 1,1-2,1-3,1-4,2-1,2-2,3 +Set Props: ./micli.py [,...] + ./micli.py 2=60,2-1=#60,2-2=false,2-3="null",3=test +Do Action: ./micli.py [...] + ./micli.py 2 [] + ./micli.py 5 Hello + ./micli.py 5-4 Hello 1 -Call MIoT: /usr/local/bin/micli.py - /usr/local/bin/micli.py action '{"did":"267090026","siid":5,"aiid":1,"in":["Hello"]}' +Call MIoT: ./micli.py + ./micli.py action '{"did":"267090026","siid":5,"aiid":1,"in":["Hello"]}' -Call MiIO: /usr/local/bin/micli.py / - /usr/local/bin/micli.py /home/device_list '{"getVirtualModel":false,"getHuamiDevices":1}' +Call MiIO: ./micli.py / + ./micli.py /home/device_list '{"getVirtualModel":false,"getHuamiDevices":1}' -Devs List: /usr/local/bin/micli.py list [name=full|name_keyword] [getVirtualModel=false|true] [getHuamiDevices=0|1] - /usr/local/bin/micli.py list Light true 0 +Devs List: ./micli.py list [name=full|name_keyword] [getVirtualModel=false|true] [getHuamiDevices=0|1] + ./micli.py list Light true 0 -MIoT Spec: /usr/local/bin/micli.py spec [model_keyword|type_urn] [format=text|python|json] - /usr/local/bin/micli.py spec - /usr/local/bin/micli.py spec speaker - /usr/local/bin/micli.py spec xiaomi.wifispeaker.lx04 - /usr/local/bin/micli.py spec urn:miot-spec-v2:device:speaker:0000A015:xiaomi-lx04:1 +MIoT Spec: ./micli.py spec [model_keyword|type_urn] [format=text|python|json] + ./micli.py spec + ./micli.py spec speaker + ./micli.py spec xiaomi.wifispeaker.lx04 + ./micli.py spec urn:miot-spec-v2:device:speaker:0000A015:xiaomi-lx04:1 -MIoT Decode: /usr/local/bin/micli.py decode [gzip] +MIoT Decode: ./micli.py decode [gzip] ``` ## 套路,例子: @@ -103,7 +106,10 @@ micli.py 2-1 ``` micli.py 2=#60 ``` -`siid` 和 `piid` 规则同属性查询命令。注意 `#` 号的意思是整数类型,如果不带则默认是文本字符串类型,要根据接口描述文档来确定类型。 + +参数类型要根据接口描述文档来确定: +- `#`是强制文本类型,还可以用单引号`'`和双引号`"`来强制文本类型`'`(可单个引号,也可以两个); +- 如果不强制文本类型,默认将检测类型;可能的检测结果是 JSON 的 `null`、`false`、`true`、`整数`、`浮点数`或者`文本`。 ### 7. 动作调用:TTS 播报和执行文本 @@ -111,18 +117,20 @@ micli.py 2=#60 ``` micli.py 5 您好 ``` -其中,5 为 `siid`,此处省略了 `1` 的 `aiid`。 +其中,5 为 `siid`,此处省略了 `aiid`(默认为`1`)。 以下命令执行后相当于直接对对音箱说“小爱同学,查询天气”是一个效果: ``` -micli.py 5-4 查询天气 #1 +micli.py 5-4 查询天气 1 ``` -其中 `#1` 表示设备语音回应,如果要执行默默关灯(不要音箱回应),可以如下: +其中 `1` 表示设备语音回应,如果要执行默默关灯(不要音箱回应),可以如下: ``` -micli.py 5-4 关灯 #0 +micli.py 5-4 关灯 0 ``` +如果没有参数,请传入`[]`保留占位。 + ### 8. 其它应用 在扩展插件中使用,比如,参考 [ZhiMsg 小爱同学 TTS 播报/执行插件](https://github.com/Yonsm/ZhiMsg) diff --git a/micli.py b/micli.py index 39c50f0..3a7bbe9 100755 --- a/micli.py +++ b/micli.py @@ -9,8 +9,10 @@ from miservice import MiAccount, MiNAService, MiIOService, miio_command, miio_command_help +MISERVICE_VERSION = '2.1.2' def usage(): + print("MiService %s - XiaoMi Cloud Service\n" % MISERVICE_VERSION) print("Usage: The following variables must be set:") print(" export MI_USER=") print(" export MI_PASS=") @@ -20,9 +22,10 @@ def usage(): async def main(args): try: + env_get = os.environ.get + store = os.path.join(str(Path.home()), '.mi.token') async with ClientSession() as session: - env = os.environ - account = MiAccount(session, env.get('MI_USER'), env.get('MI_PASS'), os.path.join(str(Path.home()), '.mi.token')) + account = MiAccount(session, env_get('MI_USER'), env_get('MI_PASS'), store) if args.startswith('mina'): service = MiNAService(account) result = await service.device_list() @@ -30,7 +33,7 @@ async def main(args): await service.send_message(result, -1, args[4:]) else: service = MiIOService(account) - result = await miio_command(service, env.get('MI_DID'), args, sys.argv[0] + ' ') + result = await miio_command(service, env_get('MI_DID'), args, sys.argv[0] + ' ') if not isinstance(result, str): result = json.dumps(result, indent=2, ensure_ascii=False) except Exception as e: @@ -38,6 +41,8 @@ async def main(args): print(result) if __name__ == '__main__': + if sys.platform.startswith('win'): + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) argv = sys.argv argc = len(argv) if argc > 1 and argv[1].startswith('-v'): diff --git a/miservice/miaccount.py b/miservice/miaccount.py old mode 100755 new mode 100644 index ecf5fcf..3284b5a --- a/miservice/miaccount.py +++ b/miservice/miaccount.py @@ -7,6 +7,7 @@ import string from urllib import parse from aiohttp import ClientSession +from aiofiles import open as async_open _LOGGER = logging.getLogger(__package__) @@ -20,34 +21,34 @@ class MiTokenStore: def __init__(self, token_path): self.token_path = token_path - def load_token(self): + async def load_token(self): if os.path.isfile(self.token_path): try: - with open(self.token_path) as f: - return json.load(f) - except Exception: - _LOGGER.exception("Exception on load token from %s", self.token_path) + async with async_open(self.token_path) as f: + return json.loads(await f.read()) + except Exception as e: + _LOGGER.exception("Exception on load token from %s: %s", self.token_path, e) return None - def save_token(self, token=None): + async def save_token(self, token=None): if token: try: - with open(self.token_path, 'w') as f: - json.dump(token, f, indent=2) - except Exception: - _LOGGER.exception("Exception on save token to %s", self.token_path) + async with async_open(self.token_path, 'w') as f: + await f.write(json.dumps(token, indent=2)) + except Exception as e: + _LOGGER.exception("Exception on save token to %s: %s", self.token_path, e) elif os.path.isfile(self.token_path): os.remove(self.token_path) class MiAccount: - def __init__(self, session: ClientSession, username, password, token_store=None): + def __init__(self, session: ClientSession, username, password, token_store='.mi.token'): self.session = session self.username = username self.password = password self.token_store = MiTokenStore(token_store) if isinstance(token_store, str) else token_store - self.token = token_store is not None and self.token_store.load_token() + self.token = None async def login(self, sid): if not self.token: @@ -74,13 +75,13 @@ async def login(self, sid): serviceToken = await self._securityTokenService(resp['location'], resp['nonce'], resp['ssecurity']) self.token[sid] = (resp['ssecurity'], serviceToken) if self.token_store: - self.token_store.save_token(self.token) + await self.token_store.save_token(self.token) return True except Exception as e: self.token = None if self.token_store: - self.token_store.save_token() + await self.token_store.save_token() _LOGGER.exception("Exception on login %s: %s", self.username, e) return False @@ -107,11 +108,13 @@ async def _securityTokenService(self, location, nonce, ssecurity): return serviceToken async def mi_request(self, sid, url, data, headers, relogin=True): + if self.token is None and self.token_store is not None: + self.token = await self.token_store.load_token() if (self.token and sid in self.token) or await self.login(sid): # Ensure login cookies = {'userId': self.token['userId'], 'serviceToken': self.token[sid][1]} content = data(self.token, cookies) if callable(data) else data method = 'GET' if data is None else 'POST' - _LOGGER.info("%s %s", url, content) + _LOGGER.debug("%s %s", url, content) async with self.session.request(method, url, data=content, cookies=cookies, headers=headers) as r: status = r.status if status == 200: diff --git a/miservice/miiocommand.py b/miservice/miiocommand.py index 46d7129..f4cac2a 100755 --- a/miservice/miiocommand.py +++ b/miservice/miiocommand.py @@ -9,19 +9,20 @@ def twins_split(string, sep, default=None): def string_to_value(string): - if string == 'null' or string == 'none': + if string[0] in '"\'#': + return string[1:-1] if string[-1] in '"\'#' else string[1:] + elif string == 'null': return None elif string == 'false': return False elif string == 'true': return True - else: + elif string.isdigit(): return int(string) - - -def string_or_value(string): - return string_to_value(string[1:]) if string[0] == '#' else string - + try: + return float(string) + except: + return string def miio_command_help(did=None, prefix='?'): quote = '' if prefix == '?' else "'" @@ -29,11 +30,11 @@ def miio_command_help(did=None, prefix='?'): Get Props: {prefix}[,...]\n\ {prefix}1,1-2,1-3,1-4,2-1,2-2,3\n\ Set Props: {prefix}[,...]\n\ - {prefix}2=#60,2-2=#false,3=test\n\ -Do Action: {prefix} [...] \n\ - {prefix}2 #NA\n\ + {prefix}2=60,2-1=#60,2-2=false,2-3="null",3=test\n\ +Do Action: {prefix} [...] \n\ + {prefix}2 []\n\ {prefix}5 Hello\n\ - {prefix}5-4 Hello #1\n\n\ + {prefix}5-4 Hello 1\n\n\ Call MIoT: {prefix} \n\ {prefix}action {quote}{{"did":"{did or "267090026"}","siid":5,"aiid":1,"in":["Hello"]}}{quote}\n\n\ Call MiIO: {prefix}/ \n\ @@ -92,12 +93,12 @@ async def miio_command(service: MiIOService, did, text, prefix='?'): if value is None: setp = False elif setp: - prop.append(string_or_value(value)) + prop.append(string_to_value(value)) props.append(prop) if miot and argc > 0: - args = [string_or_value(a) for a in argv] if arg != '#NA' else [] + args = [] if arg == '[]' else [string_to_value(a) for a in argv] return await service.miot_action(did, props[0], args) do_props = ((service.home_get_props, service.miot_get_props), (service.home_set_props, service.miot_set_props))[setp][miot] - return await do_props(did, props) + return await do_props(did, props if miot or setp else [p[0] for p in props]) diff --git a/miservice/miioservice.py b/miservice/miioservice.py index bb6b0e0..12ec166 100755 --- a/miservice/miioservice.py +++ b/miservice/miioservice.py @@ -4,14 +4,13 @@ import hashlib import hmac import json -from .miaccount import MiAccount # REGIONS = ['cn', 'de', 'i2', 'ru', 'sg', 'us'] class MiIOService: - def __init__(self, account: MiAccount, region=None): + def __init__(self, account=None, region=None): self.account = account self.server = 'https://' + ('' if region is None or region == 'cn' else region + '.') + 'api.io.mi.com/app' diff --git a/setup.py b/setup.py index 18fd7e1..3f1ca94 100755 --- a/setup.py +++ b/setup.py @@ -14,15 +14,17 @@ from pathlib import Path from setuptools import setup +from micli import MISERVICE_VERSION + setup( name='miservice', description='XiaoMi Cloud Service', - version=time.strftime("%Y.%m.%d"), + version=MISERVICE_VERSION, license='MIT', author='Yonsm', author_email='Yonsm@qq.com', url='https://github.com/Yonsm/MiService', - long_description=Path('README.md').read_text(), + long_description=Path('README.md').read_text(encoding="utf-8"), long_description_content_type='text/markdown', packages=['miservice'], scripts=['micli.py'],