python-library-xiaomi-miot 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .env
9
+ .pytest_cache/
10
+ config.yaml
11
+ logs/
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-library-xiaomi-miot
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.10
5
+ Requires-Dist: pydantic
6
+ Requires-Dist: python-miio
@@ -0,0 +1,11 @@
1
+ @echo off
2
+ cd /d %~dp0
3
+
4
+ if not exist .venv (
5
+ call update.bat
6
+ )
7
+
8
+ call .venv\Scripts\activate.bat
9
+
10
+ pip install python-dotenv
11
+ python examples/switch
@@ -0,0 +1,59 @@
1
+ import os
2
+ import time
3
+ from pathlib import Path
4
+ from dotenv import load_dotenv
5
+ from xiaomi_miot import execute
6
+
7
+ load_dotenv(Path(__file__).resolve().parents[2] / ".env")
8
+
9
+ IP = os.environ["SWITCH_IP"]
10
+ TOKEN = os.environ["SWITCH_TOKEN"]
11
+ EXPECTED = os.environ["SWITCH_VALUE"].lower() == "true"
12
+
13
+ base = {"type": "switch", "ip": IP, "token": TOKEN}
14
+ tests: list[tuple[str, dict, bool]] = [] # (name, result, passed)
15
+
16
+
17
+ def check(name: str, result: dict, expect_on: bool | None = None):
18
+ if expect_on is not None:
19
+ passed = result.get("ok") and result.get("value") == expect_on
20
+ else:
21
+ passed = result.get("ok", False)
22
+ tests.append((name, result, passed))
23
+
24
+
25
+ # Step 1: 获取初始状态
26
+ r = execute(base)
27
+ check("获取初始状态", r)
28
+
29
+ # Step 2: 设置为期望值的【相反值】
30
+ r = execute({**base, "on": not EXPECTED})
31
+ check(f"设置为 {not EXPECTED}", r)
32
+
33
+ time.sleep(3)
34
+
35
+ # Step 3: 验证反向值生效
36
+ r = execute(base)
37
+ check(f"验证状态 == {not EXPECTED}", r, expect_on=not EXPECTED)
38
+
39
+ # Step 4: 设置为最终期望值
40
+ r = execute({**base, "on": EXPECTED})
41
+ check(f"设置为 {EXPECTED}", r)
42
+
43
+ # Step 5: 验证最终期望值生效
44
+ r = execute(base)
45
+ check(f"验证状态 == {EXPECTED}", r, expect_on=EXPECTED)
46
+
47
+ # 打印测试结果
48
+ print("\n" + "=" * 50)
49
+ print("测试结果")
50
+ print("=" * 50)
51
+ all_passed = True
52
+ for name, result, passed in tests:
53
+ icon = "PASS" if passed else "FAIL"
54
+ print(f" [{icon}] {name}")
55
+ if not passed:
56
+ all_passed = False
57
+ print(f" {result}")
58
+ print("=" * 50)
59
+ print(f"{'全部通过' if all_passed else '存在失败'} ({sum(p for *_, p in tests)}/{len(tests)})")
@@ -0,0 +1,18 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "python-library-xiaomi-miot"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "python-miio",
11
+ "pydantic",
12
+ ]
13
+
14
+ [tool.hatch.build.targets.wheel]
15
+ packages = ["xiaomi_miot"]
16
+
17
+ [tool.hatch.metadata]
18
+ allow-direct-references = true
@@ -0,0 +1,9 @@
1
+ @echo off
2
+ cd /d %~dp0
3
+
4
+ if not exist .venv (
5
+ python -m venv .venv
6
+ )
7
+
8
+ call .venv\Scripts\activate.bat
9
+ python -m pip install -e .
@@ -0,0 +1,17 @@
1
+ import warnings
2
+ warnings.filterwarnings("ignore", category=FutureWarning, module=r"miio\.miot_device")
3
+
4
+ from .device import MiotDevice
5
+ from .models import DeviceParams, GetParams, SetParams
6
+ from .extensions import get_extension, list_extensions
7
+ from ._api import execute
8
+
9
+ __all__ = [
10
+ "MiotDevice",
11
+ "DeviceParams",
12
+ "GetParams",
13
+ "SetParams",
14
+ "get_extension",
15
+ "list_extensions",
16
+ "execute",
17
+ ]
@@ -0,0 +1,54 @@
1
+ from .device import MiotDevice
2
+ from .models import DeviceParams, GetParams, SetParams
3
+ from .extensions import get_extension, list_extensions
4
+
5
+
6
+ def execute(params: dict) -> dict:
7
+ """统一入口,用户输入永远是 dict
8
+
9
+ 通用模式(无 type):
10
+ {"ip": "...", "token": "...", "siid": 2, "piid": 1} → 读属性
11
+ {"ip": "...", "token": "...", "siid": 2, "piid": 1, "value": 1} → 写属性
12
+ {"ip": "...", "token": "...", "action": "info"} → 设备信息
13
+
14
+ 扩展模式(有 type):
15
+ {"type": "switch", "ip": "...", "token": "...", "on": True} → 走开关扩展
16
+ """
17
+ device_type = params.get("type")
18
+
19
+ if device_type:
20
+ return _execute_extension(device_type, params)
21
+ return _execute_generic(params)
22
+
23
+
24
+ def _execute_extension(device_type: str, params: dict) -> dict:
25
+ ext_cls = get_extension(device_type)
26
+ if ext_cls is None:
27
+ return {
28
+ "ok": False,
29
+ "error": f"未知设备类型: {device_type}",
30
+ "available": list_extensions(),
31
+ }
32
+
33
+ ext = ext_cls()
34
+ validated = ext.Params(**params)
35
+ device = MiotDevice(validated.ip, validated.token)
36
+ return ext.execute(device, validated)
37
+
38
+
39
+ def _execute_generic(params: dict) -> dict:
40
+ action = params.get("action", "prop")
41
+
42
+ if action == "info":
43
+ dp = DeviceParams(**params)
44
+ device = MiotDevice(dp.ip, dp.token)
45
+ return {"ok": True, **device.info()}
46
+
47
+ if "value" in params:
48
+ sp = SetParams(**params)
49
+ device = MiotDevice(sp.ip, sp.token)
50
+ return device.set_prop(sp.did, sp.siid, sp.piid, sp.value)
51
+
52
+ gp = GetParams(**params)
53
+ device = MiotDevice(gp.ip, gp.token)
54
+ return device.get_prop(gp.did, gp.siid, gp.piid)
@@ -0,0 +1,48 @@
1
+ from miio import Device
2
+
3
+
4
+ class MiotDevice:
5
+ """纯局域网 MIoT 设备操作,不依赖云端"""
6
+
7
+ def __init__(self, ip: str, token: str):
8
+ self._dev = Device(ip, token)
9
+
10
+ def info(self) -> dict:
11
+ """获取设备基本信息(model, firmware, hardware 等)"""
12
+ raw = self._dev.info()
13
+ return {
14
+ "model": raw.model,
15
+ "mac": raw.mac_address,
16
+ "firmware": raw.firmware_version,
17
+ "hardware": raw.hardware_version,
18
+ "raw": str(raw),
19
+ }
20
+
21
+ def get_prop(self, did: str, siid: int, piid: int) -> dict:
22
+ results = self._dev.send("get_properties", [
23
+ {"did": did, "siid": siid, "piid": piid}
24
+ ])
25
+ r = results[0]
26
+ if r.get("code") == 0:
27
+ return {"ok": True, "value": r["value"]}
28
+ return {"ok": False, "error": r}
29
+
30
+ def get_props(self, props: list[dict]) -> list[dict]:
31
+ return self._dev.send("get_properties", props)
32
+
33
+ def set_prop(self, did: str, siid: int, piid: int, value) -> dict:
34
+ results = self._dev.send("set_properties", [
35
+ {"did": did, "siid": siid, "piid": piid, "value": value}
36
+ ])
37
+ r = results[0]
38
+ if r.get("code") == 0:
39
+ return {"ok": True}
40
+ return {"ok": False, "error": r}
41
+
42
+ def set_props(self, props: list[dict]) -> list[dict]:
43
+ return self._dev.send("set_properties", props)
44
+
45
+ def call_action(self, did: str, siid: int, aiid: int, params: list | None = None) -> dict:
46
+ return self._dev.send("action", {
47
+ "did": did, "siid": siid, "aiid": aiid, "in": params or []
48
+ })
@@ -0,0 +1,7 @@
1
+ from ._registry import extension, get_extension, list_extensions
2
+
3
+ __all__ = [
4
+ "extension",
5
+ "get_extension",
6
+ "list_extensions",
7
+ ]
@@ -0,0 +1,44 @@
1
+ import importlib
2
+ import pkgutil
3
+ from pathlib import Path
4
+
5
+ _extensions: dict[str, type] = {}
6
+ _discovered = False
7
+
8
+
9
+ def extension(name: str):
10
+ """装饰器:注册设备扩展
11
+
12
+ @extension("switch")
13
+ class SwitchExtension:
14
+ class Params(DeviceParams): ...
15
+ def execute(self, device, params): ...
16
+ """
17
+ def decorator(cls):
18
+ _extensions[name] = cls
19
+ return cls
20
+ return decorator
21
+
22
+
23
+ def get_extension(name: str):
24
+ discover()
25
+ return _extensions.get(name)
26
+
27
+
28
+ def list_extensions() -> list[str]:
29
+ discover()
30
+ return list(_extensions.keys())
31
+
32
+
33
+ def discover():
34
+ """动态扫描 extensions/ 目录下所有模块,导入以触发 @extension 注册"""
35
+ global _discovered
36
+ if _discovered:
37
+ return
38
+ _discovered = True
39
+
40
+ ext_dir = Path(__file__).parent
41
+ for finder, module_name, is_pkg in pkgutil.iter_modules([str(ext_dir)]):
42
+ if module_name.startswith("_"):
43
+ continue
44
+ importlib.import_module(f"{__package__}.{module_name}")
@@ -0,0 +1,31 @@
1
+ from pydantic import BaseModel, Field
2
+ from xiaomi_miot.device import MiotDevice
3
+ from xiaomi_miot.extensions import extension
4
+ from xiaomi_miot.models import DeviceParams
5
+
6
+ class SwitchParams(DeviceParams):
7
+ type: str = "switch"
8
+ on: bool | None = Field(default=None,description="None=查询状态, True=开, False=关")
9
+ siid: int = Field(default=2,description="默认 service:Switch")
10
+ piid: int = Field(default=1,description="默认 property:On")
11
+
12
+ @extension("switch")
13
+ class SwitchExtension:
14
+ Params = SwitchParams
15
+
16
+ def execute(self, device: MiotDevice, params: SwitchParams) -> dict:
17
+ if params.on is None:
18
+ return self._get_state(device, params)
19
+ return self._set_state(device, params)
20
+
21
+ def _get_state(self, device: MiotDevice, params: SwitchParams) -> dict:
22
+ result = device.get_prop("switch", params.siid, params.piid)
23
+ if result["ok"]:
24
+ result["state"] = "开启" if result["value"] else "关闭"
25
+ return result
26
+
27
+ def _set_state(self, device: MiotDevice, params: SwitchParams) -> dict:
28
+ result = device.set_prop("switch", params.siid, params.piid, params.on)
29
+ if result["ok"]:
30
+ result["state"] = "开启" if params.on else "关闭"
31
+ return result
@@ -0,0 +1,22 @@
1
+ from typing import Any
2
+ from pydantic import BaseModel, Field, ConfigDict
3
+
4
+
5
+ class DeviceParams(BaseModel):
6
+ """最基础的设备参数:局域网连接所需"""
7
+ model_config = ConfigDict(extra="ignore")
8
+
9
+ ip: str = Field(description="设备 IP 地址")
10
+ token: str = Field(description="设备 Token")
11
+ type: str | None = Field(default=None,description="设备类型")
12
+
13
+ class GetParams(DeviceParams):
14
+ """通用属性读取"""
15
+ did: str = Field(default="prop",description="默认 did:prop")
16
+ siid: int = Field(description="service ID")
17
+ piid: int = Field(description="property ID")
18
+
19
+
20
+ class SetParams(GetParams):
21
+ """通用属性设置"""
22
+ value: Any = Field(description="属性值")