Skip to content
Open
58 changes: 33 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ XiaoMi Cloud Service for mi.com

## Install
```
pip3 install aiohttp aiofiles
pip3 install miservice
```

Expand All @@ -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=<Username>
export MI_PASS=<Password>
export MI_DID=<Device ID|Name>

Get Props: /usr/local/bin/micli.py <siid[-piid]>[,...]
/usr/local/bin/micli.py 1,1-2,1-3,1-4,2-1,2-2,3
Set Props: /usr/local/bin/micli.py <siid[-piid]=[#]value>[,...]
/usr/local/bin/micli.py 2=#60,2-2=#false,3=test
Do Action: /usr/local/bin/micli.py <siid[-piid]> <arg1|#NA> [...]
/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 <siid[-piid]>[,...]
./micli.py 1,1-2,1-3,1-4,2-1,2-2,3
Set Props: ./micli.py <siid[-piid]=[#]value>[,...]
./micli.py 2=60,2-1=#60,2-2=false,2-3="null",3=test
Do Action: ./micli.py <siid[-piid]> <arg1|[]> [...]
./micli.py 2 []
./micli.py 5 Hello
./micli.py 5-4 Hello 1

Call MIoT: /usr/local/bin/micli.py <cmd=prop/get|/prop/set|action> <params>
/usr/local/bin/micli.py action '{"did":"267090026","siid":5,"aiid":1,"in":["Hello"]}'
Call MIoT: ./micli.py <cmd=prop/get|/prop/set|action> <params>
./micli.py action '{"did":"267090026","siid":5,"aiid":1,"in":["Hello"]}'

Call MiIO: /usr/local/bin/micli.py /<uri> <data>
/usr/local/bin/micli.py /home/device_list '{"getVirtualModel":false,"getHuamiDevices":1}'
Call MiIO: ./micli.py /<uri> <data>
./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 <ssecurity> <nonce> <data> [gzip]
MIoT Decode: ./micli.py decode <ssecurity> <nonce> <data> [gzip]
```

## 套路,例子:
Expand Down Expand Up @@ -103,26 +106,31 @@ micli.py 2-1
```
micli.py 2=#60
```
`siid` 和 `piid` 规则同属性查询命令。注意 `#` 号的意思是整数类型,如果不带则默认是文本字符串类型,要根据接口描述文档来确定类型。

参数类型要根据接口描述文档来确定:
- `#`是强制文本类型,还可以用单引号`'`和双引号`"`来强制文本类型`'`(可单个引号,也可以两个);
- 如果不强制文本类型,默认将检测类型;可能的检测结果是 JSON 的 `null`、`false`、`true`、`整数`、`浮点数`或者`文本`。

### 7. 动作调用:TTS 播报和执行文本

以下命令执行后小爱音箱会播报“您好”:
```
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)
11 changes: 8 additions & 3 deletions micli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=<Username>")
print(" export MI_PASS=<Password>")
Expand All @@ -20,24 +22,27 @@ 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()
if len(args) > 4:
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:
result = e
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'):
Expand Down
33 changes: 18 additions & 15 deletions miservice/miaccount.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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:
Expand All @@ -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

Expand All @@ -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:
Expand Down
29 changes: 15 additions & 14 deletions miservice/miiocommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,32 @@ 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 "'"
return f'\
Get Props: {prefix}<siid[-piid]>[,...]\n\
{prefix}1,1-2,1-3,1-4,2-1,2-2,3\n\
Set Props: {prefix}<siid[-piid]=[#]value>[,...]\n\
{prefix}2=#60,2-2=#false,3=test\n\
Do Action: {prefix}<siid[-piid]> <arg1|#NA> [...] \n\
{prefix}2 #NA\n\
{prefix}2=60,2-1=#60,2-2=false,2-3="null",3=test\n\
Do Action: {prefix}<siid[-piid]> <arg1|[]> [...] \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}<cmd=prop/get|/prop/set|action> <params>\n\
{prefix}action {quote}{{"did":"{did or "267090026"}","siid":5,"aiid":1,"in":["Hello"]}}{quote}\n\n\
Call MiIO: {prefix}/<uri> <data>\n\
Expand Down Expand Up @@ -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])
3 changes: 1 addition & 2 deletions miservice/miioservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
6 changes: 4 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down