midea-dishwasher-api 1.0.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.
- midea_dishwasher_api-1.0.0/.gitignore +9 -0
- midea_dishwasher_api-1.0.0/LICENSE +21 -0
- midea_dishwasher_api-1.0.0/PKG-INFO +146 -0
- midea_dishwasher_api-1.0.0/README.md +98 -0
- midea_dishwasher_api-1.0.0/pyproject.toml +52 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/__init__.py +63 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/client.py +41 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/enums/__init__.py +17 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/enums/bright_level.py +13 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/enums/cycle_state.py +28 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/enums/error_code.py +20 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/enums/machine_state.py +12 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/enums/mode.py +52 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/enums/msg_type.py +11 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/enums/wash_stage.py +21 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/protocol/__init__.py +29 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/protocol/codec.py +124 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/protocol/frame_error.py +5 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/security/__init__.py +44 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/security/crypto.py +67 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/security/security.py +163 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/security/v2.py +69 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/security/v3_error.py +5 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/state/__init__.py +4 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/state/decoder.py +64 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/state/dishwasher_status.py +20 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/transport/__init__.py +3 -0
- midea_dishwasher_api-1.0.0/src/midea_dishwasher_api/transport/v3_transport.py +140 -0
- midea_dishwasher_api-1.0.0/tests/test_protocol.py +140 -0
- midea_dishwasher_api-1.0.0/tests/test_security.py +162 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rodrigo Roque
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: midea-dishwasher-api
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Cliente Python para lava-louças Midea (device_type 0xE1, plugin v5) — codec do frame AA + transporte LAN V3.
|
|
5
|
+
Project-URL: Homepage, https://github.com/roquerodrigo/midea-dishwasher-api
|
|
6
|
+
Project-URL: Repository, https://github.com/roquerodrigo/midea-dishwasher-api
|
|
7
|
+
Project-URL: Issues, https://github.com/roquerodrigo/midea-dishwasher-api/issues
|
|
8
|
+
Author-email: Rodrigo Roque <rodrigogoncalvesroque@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Rodrigo Roque
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: dishwasher,iot,lan,midea,smart-home
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
39
|
+
Classifier: Topic :: Home Automation
|
|
40
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
41
|
+
Classifier: Typing :: Typed
|
|
42
|
+
Requires-Python: >=3.14
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
45
|
+
Provides-Extra: lan
|
|
46
|
+
Requires-Dist: cryptography>=42; extra == 'lan'
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# midea-dishwasher-api
|
|
50
|
+
|
|
51
|
+
Cliente Python para lava-louças Midea (`device_type 0xE1`, plugin v5).
|
|
52
|
+
|
|
53
|
+
Implementa o protocolo de aplicação `AA … E1` e a camada de transporte LAN V3
|
|
54
|
+
(handshake 8370 + AES-128-CBC + SHA-256, com framing V2 5A5A interno).
|
|
55
|
+
|
|
56
|
+
## Instalação
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install midea-dishwasher-api[lan]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
O extra `lan` instala `cryptography`, necessário para falar diretamente com a
|
|
63
|
+
máquina via LAN. Sem o extra, o pacote ainda expõe o codec do frame e o
|
|
64
|
+
parser de status para uso com transporte próprio (cloud, mock, etc.).
|
|
65
|
+
|
|
66
|
+
## Uso rápido
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from midea_dishwasher_api import BrightLevel, Client, Mode, V3Transport
|
|
70
|
+
|
|
71
|
+
with V3Transport(
|
|
72
|
+
host="192.168.5.100",
|
|
73
|
+
device_id=151732606394621,
|
|
74
|
+
token=bytes.fromhex("..."), # 64 bytes
|
|
75
|
+
key=bytes.fromhex("..."), # 32 bytes
|
|
76
|
+
) as transport:
|
|
77
|
+
client = Client(send=transport)
|
|
78
|
+
|
|
79
|
+
status = client.query_status()
|
|
80
|
+
print(status.machine_state) # MachineState.POWER_ON / POWER_OFF
|
|
81
|
+
print(status.cycle_state) # CycleState.IDLE / WORK / ORDER / ...
|
|
82
|
+
print(status.left_time) # minutos restantes (apenas em WORK)
|
|
83
|
+
print(status.door_closed)
|
|
84
|
+
print(status.bright_lack) # secante acabou?
|
|
85
|
+
|
|
86
|
+
client.power_on()
|
|
87
|
+
client.start_to_work(mode=Mode.ECO, extra_drying=True)
|
|
88
|
+
client.set_bright(BrightLevel.L3)
|
|
89
|
+
client.cancel_work()
|
|
90
|
+
client.power_off()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Os métodos de controle não retornam estado (a máquina demora alguns segundos
|
|
94
|
+
para refletir a mudança). Chame `query_status()` quando quiser estado fresco.
|
|
95
|
+
|
|
96
|
+
## API
|
|
97
|
+
|
|
98
|
+
### Client
|
|
99
|
+
|
|
100
|
+
| Método | Efeito |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `query_status() -> DishwasherStatus` | Lê o estado atual |
|
|
103
|
+
| `power_on()` | Liga a máquina |
|
|
104
|
+
| `power_off()` | Desliga |
|
|
105
|
+
| `cancel_work()` | Cancela ciclo / volta ao idle |
|
|
106
|
+
| `start_to_work(mode, extra_drying=False)` | Inicia ciclo |
|
|
107
|
+
| `set_bright(level: BrightLevel)` | Ajusta o nível do abrilhantador (1–5) |
|
|
108
|
+
|
|
109
|
+
### DishwasherStatus
|
|
110
|
+
|
|
111
|
+
Campos decodificados da resposta:
|
|
112
|
+
|
|
113
|
+
- `machine_state: MachineState | None` — `POWER_ON` / `POWER_OFF`
|
|
114
|
+
- `cycle_state: CycleState | None` — `idle`, `order`, `work`, `error`, ...
|
|
115
|
+
- `wash_stage: WashStage | int | None` — `IDLE`, `PRE_WASH`, `MAIN_WASH`, `RINSE`, `DRY`, `FINISH`
|
|
116
|
+
- `error_code: ErrorCode | int` — `NONE`, `WATER_SUPPLY`, `HEATING`, `OVERFLOW`, `WATER_VALVE`
|
|
117
|
+
- `left_time: int | None` — minutos restantes (preenchido apenas quando `cycle_state == WORK`)
|
|
118
|
+
- `door_closed: bool`
|
|
119
|
+
- `bright_lack: bool` — secante (rinse aid) acabou
|
|
120
|
+
|
|
121
|
+
### Modos disponíveis (`Mode`)
|
|
122
|
+
|
|
123
|
+
`AUTO`, `INTENSIVE`, `NORMAL`, `ECO`, `GLASS`, `NINETY_MIN`, `ONE_HOUR`,
|
|
124
|
+
`RAPID`, `SOAK`, `THREE_IN_ONE`, `HYGIENE`, `QUIET`, `PARTY`, `FRUIT`.
|
|
125
|
+
|
|
126
|
+
## Transporte customizado
|
|
127
|
+
|
|
128
|
+
`Client` aceita qualquer `Callable[[bytes], bytes]` como `send`. Útil para
|
|
129
|
+
testes com transporte mock, integração com cloud, ou pipeline próprio:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
def fake_send(frame: bytes) -> bytes:
|
|
133
|
+
return assemble_frame(b"...", 0x02)
|
|
134
|
+
|
|
135
|
+
client = Client(send=fake_send)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Como obter `token` e `key`
|
|
139
|
+
|
|
140
|
+
São credenciais por dispositivo emitidas pela cloud da Midea. Use ferramentas
|
|
141
|
+
existentes (`midea-msmart`, `midea-beautiful-air`, `midea-discover`) para
|
|
142
|
+
extrair a partir da sua conta no app.
|
|
143
|
+
|
|
144
|
+
## Licença
|
|
145
|
+
|
|
146
|
+
MIT — ver `LICENSE`.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# midea-dishwasher-api
|
|
2
|
+
|
|
3
|
+
Cliente Python para lava-louças Midea (`device_type 0xE1`, plugin v5).
|
|
4
|
+
|
|
5
|
+
Implementa o protocolo de aplicação `AA … E1` e a camada de transporte LAN V3
|
|
6
|
+
(handshake 8370 + AES-128-CBC + SHA-256, com framing V2 5A5A interno).
|
|
7
|
+
|
|
8
|
+
## Instalação
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install midea-dishwasher-api[lan]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
O extra `lan` instala `cryptography`, necessário para falar diretamente com a
|
|
15
|
+
máquina via LAN. Sem o extra, o pacote ainda expõe o codec do frame e o
|
|
16
|
+
parser de status para uso com transporte próprio (cloud, mock, etc.).
|
|
17
|
+
|
|
18
|
+
## Uso rápido
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from midea_dishwasher_api import BrightLevel, Client, Mode, V3Transport
|
|
22
|
+
|
|
23
|
+
with V3Transport(
|
|
24
|
+
host="192.168.5.100",
|
|
25
|
+
device_id=151732606394621,
|
|
26
|
+
token=bytes.fromhex("..."), # 64 bytes
|
|
27
|
+
key=bytes.fromhex("..."), # 32 bytes
|
|
28
|
+
) as transport:
|
|
29
|
+
client = Client(send=transport)
|
|
30
|
+
|
|
31
|
+
status = client.query_status()
|
|
32
|
+
print(status.machine_state) # MachineState.POWER_ON / POWER_OFF
|
|
33
|
+
print(status.cycle_state) # CycleState.IDLE / WORK / ORDER / ...
|
|
34
|
+
print(status.left_time) # minutos restantes (apenas em WORK)
|
|
35
|
+
print(status.door_closed)
|
|
36
|
+
print(status.bright_lack) # secante acabou?
|
|
37
|
+
|
|
38
|
+
client.power_on()
|
|
39
|
+
client.start_to_work(mode=Mode.ECO, extra_drying=True)
|
|
40
|
+
client.set_bright(BrightLevel.L3)
|
|
41
|
+
client.cancel_work()
|
|
42
|
+
client.power_off()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Os métodos de controle não retornam estado (a máquina demora alguns segundos
|
|
46
|
+
para refletir a mudança). Chame `query_status()` quando quiser estado fresco.
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### Client
|
|
51
|
+
|
|
52
|
+
| Método | Efeito |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `query_status() -> DishwasherStatus` | Lê o estado atual |
|
|
55
|
+
| `power_on()` | Liga a máquina |
|
|
56
|
+
| `power_off()` | Desliga |
|
|
57
|
+
| `cancel_work()` | Cancela ciclo / volta ao idle |
|
|
58
|
+
| `start_to_work(mode, extra_drying=False)` | Inicia ciclo |
|
|
59
|
+
| `set_bright(level: BrightLevel)` | Ajusta o nível do abrilhantador (1–5) |
|
|
60
|
+
|
|
61
|
+
### DishwasherStatus
|
|
62
|
+
|
|
63
|
+
Campos decodificados da resposta:
|
|
64
|
+
|
|
65
|
+
- `machine_state: MachineState | None` — `POWER_ON` / `POWER_OFF`
|
|
66
|
+
- `cycle_state: CycleState | None` — `idle`, `order`, `work`, `error`, ...
|
|
67
|
+
- `wash_stage: WashStage | int | None` — `IDLE`, `PRE_WASH`, `MAIN_WASH`, `RINSE`, `DRY`, `FINISH`
|
|
68
|
+
- `error_code: ErrorCode | int` — `NONE`, `WATER_SUPPLY`, `HEATING`, `OVERFLOW`, `WATER_VALVE`
|
|
69
|
+
- `left_time: int | None` — minutos restantes (preenchido apenas quando `cycle_state == WORK`)
|
|
70
|
+
- `door_closed: bool`
|
|
71
|
+
- `bright_lack: bool` — secante (rinse aid) acabou
|
|
72
|
+
|
|
73
|
+
### Modos disponíveis (`Mode`)
|
|
74
|
+
|
|
75
|
+
`AUTO`, `INTENSIVE`, `NORMAL`, `ECO`, `GLASS`, `NINETY_MIN`, `ONE_HOUR`,
|
|
76
|
+
`RAPID`, `SOAK`, `THREE_IN_ONE`, `HYGIENE`, `QUIET`, `PARTY`, `FRUIT`.
|
|
77
|
+
|
|
78
|
+
## Transporte customizado
|
|
79
|
+
|
|
80
|
+
`Client` aceita qualquer `Callable[[bytes], bytes]` como `send`. Útil para
|
|
81
|
+
testes com transporte mock, integração com cloud, ou pipeline próprio:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
def fake_send(frame: bytes) -> bytes:
|
|
85
|
+
return assemble_frame(b"...", 0x02)
|
|
86
|
+
|
|
87
|
+
client = Client(send=fake_send)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Como obter `token` e `key`
|
|
91
|
+
|
|
92
|
+
São credenciais por dispositivo emitidas pela cloud da Midea. Use ferramentas
|
|
93
|
+
existentes (`midea-msmart`, `midea-beautiful-air`, `midea-discover`) para
|
|
94
|
+
extrair a partir da sua conta no app.
|
|
95
|
+
|
|
96
|
+
## Licença
|
|
97
|
+
|
|
98
|
+
MIT — ver `LICENSE`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "midea-dishwasher-api"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Cliente Python para lava-louças Midea (device_type 0xE1, plugin v5) — codec do frame AA + transporte LAN V3."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { file = "LICENSE" }
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Rodrigo Roque", email = "rodrigogoncalvesroque@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["midea", "dishwasher", "iot", "smart-home", "lan"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Topic :: Home Automation",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
lan = ["cryptography>=42"]
|
|
28
|
+
dev = ["pytest>=8"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/roquerodrigo/midea-dishwasher-api"
|
|
32
|
+
Repository = "https://github.com/roquerodrigo/midea-dishwasher-api"
|
|
33
|
+
Issues = "https://github.com/roquerodrigo/midea-dishwasher-api/issues"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/midea_dishwasher_api"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = [
|
|
44
|
+
"src/midea_dishwasher_api",
|
|
45
|
+
"tests",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"pyproject.toml",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Cliente Python para a lava-louças Midea (device type `0xE1`)."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from .client import Client
|
|
7
|
+
from .enums import (
|
|
8
|
+
BrightLevel,
|
|
9
|
+
CycleState,
|
|
10
|
+
ErrorCode,
|
|
11
|
+
MachineState,
|
|
12
|
+
Mode,
|
|
13
|
+
MsgType,
|
|
14
|
+
WashStage,
|
|
15
|
+
)
|
|
16
|
+
from .protocol import (
|
|
17
|
+
ControlPayload,
|
|
18
|
+
FrameError,
|
|
19
|
+
assemble_frame,
|
|
20
|
+
build_control,
|
|
21
|
+
build_query,
|
|
22
|
+
parse_frame,
|
|
23
|
+
)
|
|
24
|
+
from .state import DishwasherStatus, decode_response
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .transport import OnWireCallback, V3Transport
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
__version__ = version("midea-dishwasher-api")
|
|
31
|
+
except PackageNotFoundError:
|
|
32
|
+
__version__ = "0.0.0+local"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"BrightLevel",
|
|
36
|
+
"Client",
|
|
37
|
+
"ControlPayload",
|
|
38
|
+
"CycleState",
|
|
39
|
+
"DishwasherStatus",
|
|
40
|
+
"ErrorCode",
|
|
41
|
+
"FrameError",
|
|
42
|
+
"MachineState",
|
|
43
|
+
"Mode",
|
|
44
|
+
"MsgType",
|
|
45
|
+
"OnWireCallback",
|
|
46
|
+
"V3Transport",
|
|
47
|
+
"WashStage",
|
|
48
|
+
"__version__",
|
|
49
|
+
"assemble_frame",
|
|
50
|
+
"build_control",
|
|
51
|
+
"build_query",
|
|
52
|
+
"decode_response",
|
|
53
|
+
"parse_frame",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
_LAZY_TRANSPORT_EXPORTS = {"V3Transport", "OnWireCallback"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __getattr__(name: str) -> Any:
|
|
60
|
+
if name in _LAZY_TRANSPORT_EXPORTS:
|
|
61
|
+
from . import transport
|
|
62
|
+
return getattr(transport, name)
|
|
63
|
+
raise AttributeError(f"module 'midea_dishwasher_api' has no attribute {name!r}")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Cliente de alto nível: cada método replica uma operação `$api(...)` do app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from .enums import BrightLevel, Mode
|
|
8
|
+
from .protocol import ControlPayload, build_control, build_query
|
|
9
|
+
from .state import DishwasherStatus, decode_response
|
|
10
|
+
|
|
11
|
+
Send = Callable[[bytes], bytes]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Client:
|
|
15
|
+
def __init__(self, send: Send) -> None:
|
|
16
|
+
self._send: Send = send
|
|
17
|
+
|
|
18
|
+
def query_status(self) -> DishwasherStatus:
|
|
19
|
+
return decode_response(self._send(build_query()))
|
|
20
|
+
|
|
21
|
+
def power_on(self) -> None:
|
|
22
|
+
self._control({"machine_state": "power_on"})
|
|
23
|
+
|
|
24
|
+
def power_off(self) -> None:
|
|
25
|
+
self._control({"machine_state": "power_off"})
|
|
26
|
+
|
|
27
|
+
def cancel_work(self) -> None:
|
|
28
|
+
self._control({"machine_state": "cancel"})
|
|
29
|
+
|
|
30
|
+
def start_to_work(self, mode: Mode, extra_drying: bool = False) -> None:
|
|
31
|
+
self._control({
|
|
32
|
+
"mode": str(mode),
|
|
33
|
+
"machine_state": "work",
|
|
34
|
+
"additional": 1 if extra_drying else 0,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
def set_bright(self, level: BrightLevel) -> None:
|
|
38
|
+
self._control({"bright": int(BrightLevel(level))})
|
|
39
|
+
|
|
40
|
+
def _control(self, payload: ControlPayload) -> None:
|
|
41
|
+
self._send(build_control(payload))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .bright_level import BrightLevel
|
|
2
|
+
from .cycle_state import CycleState
|
|
3
|
+
from .error_code import ErrorCode
|
|
4
|
+
from .machine_state import MachineState
|
|
5
|
+
from .mode import Mode
|
|
6
|
+
from .msg_type import MsgType
|
|
7
|
+
from .wash_stage import WashStage
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BrightLevel",
|
|
11
|
+
"CycleState",
|
|
12
|
+
"ErrorCode",
|
|
13
|
+
"MachineState",
|
|
14
|
+
"Mode",
|
|
15
|
+
"MsgType",
|
|
16
|
+
"WashStage",
|
|
17
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Estado atual do ciclo (byte 1 da resposta de status)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CycleState(StrEnum):
|
|
9
|
+
POWER_OFF = "power_off"
|
|
10
|
+
IDLE = "idle"
|
|
11
|
+
ORDER = "order"
|
|
12
|
+
WORK = "work"
|
|
13
|
+
ERROR = "error"
|
|
14
|
+
SOFT_GEAR = "soft_gear"
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_byte(cls, byte: int) -> "CycleState | None":
|
|
18
|
+
return _BYTE_TO_CYCLE_STATE.get(byte)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_BYTE_TO_CYCLE_STATE: dict[int, CycleState] = {
|
|
22
|
+
0x00: CycleState.POWER_OFF,
|
|
23
|
+
0x01: CycleState.IDLE,
|
|
24
|
+
0x02: CycleState.ORDER,
|
|
25
|
+
0x03: CycleState.WORK,
|
|
26
|
+
0x04: CycleState.ERROR,
|
|
27
|
+
0x05: CycleState.SOFT_GEAR,
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Código de falha reportado pela máquina (byte 10 da resposta)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ErrorCode(IntEnum):
|
|
9
|
+
NONE = 0
|
|
10
|
+
WATER_SUPPLY = 1
|
|
11
|
+
HEATING = 2
|
|
12
|
+
OVERFLOW = 3
|
|
13
|
+
WATER_VALVE = 4
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_byte(cls, byte: int) -> "ErrorCode | int":
|
|
17
|
+
try:
|
|
18
|
+
return cls(byte)
|
|
19
|
+
except ValueError:
|
|
20
|
+
return byte
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MachineState(StrEnum):
|
|
7
|
+
POWER_ON = "power_on"
|
|
8
|
+
POWER_OFF = "power_off"
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def from_byte(cls, byte: int) -> "MachineState":
|
|
12
|
+
return cls.POWER_OFF if byte == 0x00 else cls.POWER_ON
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Modos de lavagem suportados pela máquina (byte 2 do controle 0x08)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Mode(StrEnum):
|
|
9
|
+
AUTO = "auto"
|
|
10
|
+
INTENSIVE = "intensive"
|
|
11
|
+
NORMAL = "normal"
|
|
12
|
+
ECO = "eco"
|
|
13
|
+
GLASS = "glass"
|
|
14
|
+
NINETY_MIN = "90min"
|
|
15
|
+
ONE_HOUR = "1hour"
|
|
16
|
+
RAPID = "rapid"
|
|
17
|
+
SOAK = "soak"
|
|
18
|
+
THREE_IN_ONE = "3in1"
|
|
19
|
+
HYGIENE = "hygiene"
|
|
20
|
+
QUIET = "quiet"
|
|
21
|
+
PARTY = "party"
|
|
22
|
+
FRUIT = "fruit"
|
|
23
|
+
|
|
24
|
+
def to_byte(self) -> int:
|
|
25
|
+
return _MODE_TO_BYTE[self]
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def byte_for(cls, mode: str | None) -> int:
|
|
29
|
+
if mode is None:
|
|
30
|
+
return 0x00
|
|
31
|
+
try:
|
|
32
|
+
return _MODE_TO_BYTE[cls(mode)]
|
|
33
|
+
except ValueError:
|
|
34
|
+
return 0x00
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_MODE_TO_BYTE: dict[Mode, int] = {
|
|
38
|
+
Mode.AUTO: 0x01,
|
|
39
|
+
Mode.INTENSIVE: 0x02,
|
|
40
|
+
Mode.NORMAL: 0x03,
|
|
41
|
+
Mode.ECO: 0x04,
|
|
42
|
+
Mode.GLASS: 0x05,
|
|
43
|
+
Mode.NINETY_MIN: 0x06,
|
|
44
|
+
Mode.RAPID: 0x07,
|
|
45
|
+
Mode.SOAK: 0x08,
|
|
46
|
+
Mode.ONE_HOUR: 0x09,
|
|
47
|
+
Mode.THREE_IN_ONE: 0x0A,
|
|
48
|
+
Mode.PARTY: 0x0C,
|
|
49
|
+
Mode.QUIET: 0x0D,
|
|
50
|
+
Mode.HYGIENE: 0x0F,
|
|
51
|
+
Mode.FRUIT: 0x13,
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Etapa atual do ciclo de lavagem (byte 9 da resposta)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WashStage(IntEnum):
|
|
9
|
+
IDLE = 0
|
|
10
|
+
PRE_WASH = 1
|
|
11
|
+
MAIN_WASH = 2
|
|
12
|
+
RINSE = 3
|
|
13
|
+
DRY = 4
|
|
14
|
+
FINISH = 5
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_byte(cls, byte: int) -> "WashStage | int":
|
|
18
|
+
try:
|
|
19
|
+
return cls(byte)
|
|
20
|
+
except ValueError:
|
|
21
|
+
return byte
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .codec import (
|
|
2
|
+
CONTROL_BODY_LEN,
|
|
3
|
+
DEVICE_TYPE,
|
|
4
|
+
HEADER_LEN,
|
|
5
|
+
QUERY_BODY,
|
|
6
|
+
SYNC,
|
|
7
|
+
ControlPayload,
|
|
8
|
+
assemble_frame,
|
|
9
|
+
build_control,
|
|
10
|
+
build_query,
|
|
11
|
+
make_sum,
|
|
12
|
+
parse_frame,
|
|
13
|
+
)
|
|
14
|
+
from .frame_error import FrameError
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CONTROL_BODY_LEN",
|
|
18
|
+
"ControlPayload",
|
|
19
|
+
"DEVICE_TYPE",
|
|
20
|
+
"FrameError",
|
|
21
|
+
"HEADER_LEN",
|
|
22
|
+
"QUERY_BODY",
|
|
23
|
+
"SYNC",
|
|
24
|
+
"assemble_frame",
|
|
25
|
+
"build_control",
|
|
26
|
+
"build_query",
|
|
27
|
+
"make_sum",
|
|
28
|
+
"parse_frame",
|
|
29
|
+
]
|