forked from seanrees/prometheus-dyson
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig_builder.py
230 lines (188 loc) · 7.2 KB
/
config_builder.py
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
"""Implements device-lookup via libdyson to produce a local credential cache.
This is based heavily on shenxn@'s implementation of get_devices.py:
https://github.com/shenxn/libdyson/blob/main/get_devices.py
"""
import argparse
import configparser
import io
import logging
import sys
from typing import Dict, List, Tuple, Optional
from libdyson import DEVICE_TYPE_NAMES, get_mqtt_info_from_wifi_info
from libdyson.cloud import DysonAccount, DysonDeviceInfo
from libdyson.cloud.account import DysonAccountCN
from libdyson.exceptions import DysonOTPTooFrequently, DysonLoginFailure
import config
def _query_credentials() -> config.DysonLinkCredentials:
"""Asks the user for their DysonLink/Cloud credentials.
Returns:
DysonLinkCredentials based on what the user supplied
"""
print('First, we need your app/DysonLink login details.')
print('This is used to get a list of your devices from Dyson. This')
print('should be the same username&password you use to login into')
print('the Dyson app (e.g; on your phone:')
username = input('Username (or number phone if in China): ')
password = input('Password: ')
country = input('Country code (e.g; IE): ')
return config.DysonLinkCredentials(username, password, country)
def _query_wifi() -> Tuple[str, str, str]:
"""Asks the user for their DysonLink/Cloud credentials.
Returns:
DysonLinkCredentials based on what the user supplied
"""
print('This requires the WiFi credentials from the label on the Dyson')
print('device. This will be used to calculate the secret required')
print('to connect to this device locally. This script does NOT need')
print('to modify WiFi settings in any way.')
print('')
print('The product SSID might look like: DYSON-AB0-XX-ABC1234D-123')
print('')
ssid = input('Enter product SSID : ')
password = input('Enter product WiFi password : ')
name = input('Device name (e.g; Living Room): ')
return (ssid, password, name)
def _query_dyson(creds: config.DysonLinkCredentials) -> List[DysonDeviceInfo]:
"""Queries Dyson's APIs for a device list.
This function requires user interaction, to check either their mobile or email
for a one-time password.
Args:
username: email address or mobile number (mobile if country is CN)
password: login password
country: two-letter country code for account, e.g; IE, CN
Returns:
list of DysonDeviceInfo
"""
username = creds.username
country = creds.country
if country == 'CN':
# Treat username like a phone number and use login_mobile_otp.
account = DysonAccountCN()
if not username.startswith('+86'):
username = '+86' + username
print(
f'Please check your mobile device ({username}) for a one-time password.')
verify_fn = account.login_mobile_otp(username)
else:
account = DysonAccount()
verify_fn = account.login_email_otp(username, country)
print(f'Please check your email ({username}) for a one-time password.')
print()
otp = input('Enter OTP: ')
try:
verify_fn(otp, creds.password)
return account.devices()
except DysonLoginFailure:
print('Incorrect OTP.')
sys.exit(-1)
def write_config(filename: str, creds: Optional[config.DysonLinkCredentials],
devices: List[DysonDeviceInfo], hosts: Dict[str, str]) -> None:
"""Writes the config out to filename.
Args:
filename: relative or fully-qualified path to the config file (ini format)
creds: DysonLinkCredentials with Dyson username/password/country.
devices: a list of Devices
hosts: a serial->IP address (or host) map for direct (non-zeroconf) connection
"""
cfg = configparser.ConfigParser()
if creds:
cfg['Dyson Link'] = {
'Username': creds.username,
'Password': creds.password,
'Country': creds.country
}
cfg['Hosts'] = hosts
for dev in devices:
cfg[dev.serial] = {
'Name': dev.name,
'Serial': dev.serial,
'LocalCredentials': dev.credential,
'ProductType': dev.product_type
}
input('Configuration generated; press return to view.')
buf = io.StringIO()
cfg.write(buf)
print(buf.getvalue())
print('--------------------------------------------------------------------------------')
print(f'Answering yes to the following question will overwrite {filename}')
ack = input('Does this look reasonable? [Y/N]: ')
if len(ack) > 0 and ack.upper()[0] == 'Y':
with open(filename, 'w') as f:
cfg.write(f)
print(f'Config written to {filename}.')
else:
print('Received negative answer; nothing written.')
def main(argv):
"""Main body of the program."""
parser = argparse.ArgumentParser(prog=argv[0])
parser.add_argument(
'--log_level',
help='Logging level (DEBUG, INFO, WARNING, ERROR)',
type=str,
default='ERROR')
parser.add_argument(
'--config',
help='Configuration file (INI file)',
default='/etc/prometheus-dyson/config.ini')
parser.add_argument(
'--mode',
help='Use "wifi" to add devices from WiFi credentials, "cloud" to try via Dyson Link"',
default='cloud'
)
args = parser.parse_args()
try:
level = getattr(logging, args.log_level)
except AttributeError:
print(f'Invalid --log_level: {args.log_level}')
sys.exit(-1)
args = parser.parse_args()
logging.basicConfig(
format='%(asctime)s [%(thread)d] %(levelname)10s %(message)s',
datefmt='%Y/%m/%d %H:%M:%S',
level=level)
print('Welcome to the prometheus-dyson config builder.')
if args.mode not in ('cloud', 'wifi'):
print(f'Invalid --mode: {args.mode}, must be one of "cloud" or "wifi"')
sys.exit(-2)
cfg = None
creds = None
hosts = {}
try:
cfg = config.Config(args.config)
creds = cfg.dyson_credentials
hosts = cfg.hosts
except:
logging.info(
'Could not load configuration: %s (assuming no configuration)', args.config)
if args.mode == 'cloud':
if not creds:
print('')
creds = _query_credentials()
else:
print(f'Using Dyson credentials from {args.config}')
try:
print()
devices = _query_dyson(creds)
print(f'Found {len(devices)} devices.')
except DysonOTPTooFrequently:
print(
'DysonOTPTooFrequently: too many OTP attempts, please wait and try again')
sys.exit(-1)
else:
ssid, password, name = _query_wifi()
serial, credential, product_type = get_mqtt_info_from_wifi_info(
ssid, password)
devices = [DysonDeviceInfo(
name=name,
active=True,
version='unknown',
new_version_available=False,
auto_update=False,
serial=serial,
credential=credential,
product_type=product_type
)]
print()
write_config(args.config, creds, devices, hosts)
if __name__ == '__main__':
main(sys.argv)