This repository was archived by the owner on Sep 10, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup_device.py
More file actions
110 lines (91 loc) · 4.36 KB
/
Copy pathsetup_device.py
File metadata and controls
110 lines (91 loc) · 4.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""
Discover all Midea AC units on the LAN and save them to config.json.
This is the command-line front end for `meow_ac.devices.discovery` and
`meow_ac.config.ConfigStore` — the discovery and config-writing logic
itself lives in the package, so the running service (and any future
"scan for new units" button in the web UI) shares the exact same code.
By default this broadcasts and picks up every unit that answers — no
need to run it three times for three units. Re-running it later (e.g.
one unit was powered off the first time, or you add a fourth one) merges
into the existing config rather than wiping it: units are matched by
their device id, and previously-set names are kept unless you type a
new one.
Usage:
python setup_device.py # broadcast, finds everything
python setup_device.py --ip 192.168.1.50 # add/update one unit by IP
python setup_device.py --no-prompt # skip the naming prompts
"""
import argparse
import asyncio
import json
from pathlib import Path
from meow_ac.config.store import ConfigStore
from meow_ac.devices.discovery import discover_all, discover_one, to_unit
from meow_ac.settings import DEFAULT_CONFIG_PATH
async def main(ip, out_path: Path, interactive_names: bool):
store = ConfigStore(out_path)
_, status = store.read_lenient()
if status == "invalid":
print(f"Warning: {out_path} exists but isn't valid JSON, starting fresh.")
is_new_key = store.config.api_key is None
store.ensure_api_key()
devices = await (discover_one(ip) if ip else discover_all())
if not devices:
print(
"No devices found. Make sure all the units are powered on and "
"on the same subnet as meow, or pass --ip for one you already "
"know (check your router's DHCP leases). You can also just "
"re-run this later for the ones that were off — it won't "
"touch the units you've already paired."
)
return
found_count = 0
for i, device in enumerate(devices, start=1):
if not device.supported:
print(f"Warning: device at {device.ip} reports supported=False, skipping.")
continue
existing = store.find_unit(str(device.id))
name = existing.name if existing else f"AC {i}"
if interactive_names:
typed = input(f"Name for unit at {device.ip} [{name}]: ").strip()
if typed:
name = typed
store.add_or_update_unit(to_unit(device, name))
found_count += 1
store.save()
units = store.config.units
# Redacted summary — never echo the api_key or per-unit V3 token/key to the
# terminal (they'd linger in scrollback / CI logs). Show only safe fields.
summary = [
{"id": u.unit_id, "name": u.name, "ip": u.ip, "port": u.port,
"has_v3_credentials": bool(u.token and u.key)}
for u in units
]
print(f"\nThis run found {found_count} unit(s). Config now has {len(units)} total at {out_path}:")
print(json.dumps(summary, indent=2))
if is_new_key:
# Deliberately do NOT print the key itself. It's written to config.json
# (mode 600); read it from there when a client first asks for it, e.g.
# python -c "import json;print(json.load(open('config.json'))['api_key'])"
print(
f"\nGenerated a new API key and stored it in {out_path} (mode 600). "
"The web UI / app asks for it the first time you connect on each "
"device. Read it from that file when you need it — anyone who has "
"it can control these units over the LAN, so keep it off anywhere public."
)
if any(u.token and u.key for u in units):
print(
"\nAt least one of these is a V3 device — keep its token/key "
"safe somewhere off this box too, in case the Midea cloud "
"ever goes down."
)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--ip", help="Skip broadcast discovery, target a known IP directly")
parser.add_argument("--out", type=Path, default=DEFAULT_CONFIG_PATH)
parser.add_argument(
"--no-prompt", action="store_true",
help="Don't interactively ask for friendly names, just use defaults / existing names",
)
args = parser.parse_args()
asyncio.run(main(args.ip, args.out, interactive_names=not args.no_prompt))