speedport-api-expanded 0.7.0__py3-none-any.whl
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.
- speedport/__init__.py +3 -0
- speedport/__main__.py +239 -0
- speedport/api.py +186 -0
- speedport/call.py +46 -0
- speedport/connection.py +159 -0
- speedport/const.py +2 -0
- speedport/device.py +87 -0
- speedport/exceptions.py +10 -0
- speedport/speedport.py +253 -0
- speedport_api_expanded-0.7.0.dist-info/METADATA +188 -0
- speedport_api_expanded-0.7.0.dist-info/RECORD +15 -0
- speedport_api_expanded-0.7.0.dist-info/WHEEL +5 -0
- speedport_api_expanded-0.7.0.dist-info/entry_points.txt +2 -0
- speedport_api_expanded-0.7.0.dist-info/licenses/LICENSE +22 -0
- speedport_api_expanded-0.7.0.dist-info/top_level.txt +1 -0
speedport/__init__.py
ADDED
speedport/__main__.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from getpass import getpass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
14
|
+
|
|
15
|
+
from speedport.speedport import Speedport
|
|
16
|
+
|
|
17
|
+
_LOGGER = logging.getLogger("speedport")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_logger(args):
|
|
21
|
+
logging.getLogger("aiohttp").setLevel(logging.WARNING)
|
|
22
|
+
if args["debug"]:
|
|
23
|
+
_LOGGER.setLevel(logging.DEBUG)
|
|
24
|
+
formatter = logging.Formatter("%(levelname)s - %(message)s")
|
|
25
|
+
elif args["quiet"]:
|
|
26
|
+
_LOGGER.setLevel(logging.WARNING)
|
|
27
|
+
formatter = logging.Formatter("%(message)s")
|
|
28
|
+
else:
|
|
29
|
+
_LOGGER.setLevel(logging.INFO)
|
|
30
|
+
formatter = logging.Formatter("%(message)s")
|
|
31
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
32
|
+
console_handler.setFormatter(formatter)
|
|
33
|
+
_LOGGER.addHandler(console_handler)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def data_table(data, keys):
|
|
37
|
+
columns = {}
|
|
38
|
+
for key in keys:
|
|
39
|
+
columns[key] = max([len(str(d[key])) for d in data] + [len(key)])
|
|
40
|
+
line = (
|
|
41
|
+
"+-"
|
|
42
|
+
+ "-+-".join([f"{'-' * length}" for key, length in columns.items()])
|
|
43
|
+
+ "-+\n"
|
|
44
|
+
)
|
|
45
|
+
text = (
|
|
46
|
+
f"{line}| "
|
|
47
|
+
+ " | ".join([f"{key:<{length}}" for key, length in columns.items()])
|
|
48
|
+
+ f" |\n{line}"
|
|
49
|
+
)
|
|
50
|
+
for d in data:
|
|
51
|
+
text += (
|
|
52
|
+
"| "
|
|
53
|
+
+ " | ".join(
|
|
54
|
+
[f"{str(d[key]):<{length}}" for key, length in columns.items()]
|
|
55
|
+
)
|
|
56
|
+
+ " |\n"
|
|
57
|
+
)
|
|
58
|
+
return text + line
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def batch_output(devices):
|
|
62
|
+
for device in devices.values():
|
|
63
|
+
print(
|
|
64
|
+
int(device.connected),
|
|
65
|
+
device.ipv4,
|
|
66
|
+
device.type,
|
|
67
|
+
device.name,
|
|
68
|
+
sep="\t",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_arguments():
|
|
73
|
+
"""Get parsed arguments."""
|
|
74
|
+
parser = argparse.ArgumentParser(description="Speedport: Command Line Utility")
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"-H",
|
|
77
|
+
"--host",
|
|
78
|
+
help="ip address or hostname of Speedport webinterface",
|
|
79
|
+
default=os.environ.get("SPEEDPORT_HOST", "speedport.ip"),
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"-s", "--https", help="use https connection", action="store_true"
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"-p",
|
|
86
|
+
"--password",
|
|
87
|
+
help="password of Speedport webinterface (default: SPEEDPORT_PASSWORD)",
|
|
88
|
+
default=os.environ.get("SPEEDPORT_PASSWORD"),
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"-d", "--debug", help="enable debug logging", action="store_true"
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument("-q", "--quiet", help="output only errors", action="store_true")
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"-b", "--batch", help="print output for batch processing", action="store_true"
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"-t", "--table", help="print output as table (default)", action="store_true"
|
|
99
|
+
)
|
|
100
|
+
subparser = parser.add_subparsers(
|
|
101
|
+
title="commands", metavar="COMMAND", required=True
|
|
102
|
+
)
|
|
103
|
+
for network in ["", "guest-", "office-"]:
|
|
104
|
+
wifi = subparser.add_parser(f"{network}wifi", help=f"Turn on/off {network}wifi")
|
|
105
|
+
wifi.add_argument(
|
|
106
|
+
f"{network}wifi", choices=["on", "off"], help=f"Turn on/off {network}wifi"
|
|
107
|
+
)
|
|
108
|
+
reconnect = subparser.add_parser(
|
|
109
|
+
"reconnect", help="Reconnect internet and receive new ip"
|
|
110
|
+
)
|
|
111
|
+
reconnect.set_defaults(reconnect=True)
|
|
112
|
+
reboot = subparser.add_parser("reboot", help="Reboot device")
|
|
113
|
+
reboot.set_defaults(reboot=True)
|
|
114
|
+
wps = subparser.add_parser("wps", help="Turn on wps for 2 minutes")
|
|
115
|
+
wps.set_defaults(wps=True)
|
|
116
|
+
devices = subparser.add_parser("devices", help="Output devices")
|
|
117
|
+
devices.set_defaults(devices=True)
|
|
118
|
+
calls = subparser.add_parser("calls", help="Output calls")
|
|
119
|
+
calls.set_defaults(calls=True)
|
|
120
|
+
ip_info = subparser.add_parser("ip-info", help="Output IPv4 and IPv6 information")
|
|
121
|
+
ip_info.set_defaults(ip_info=True)
|
|
122
|
+
ip_info.add_argument(
|
|
123
|
+
"--ipv4", help="Print only the public IPv4 address", action="store_true"
|
|
124
|
+
)
|
|
125
|
+
return vars(parser.parse_args())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def check_args(speedport, args):
|
|
129
|
+
if args.get("ip_info"):
|
|
130
|
+
info = await speedport.ip_info
|
|
131
|
+
if args.get("ipv4"):
|
|
132
|
+
address = info.get("public_ip_v4")
|
|
133
|
+
if not address:
|
|
134
|
+
raise SystemExit("Router did not report a public IPv4 address")
|
|
135
|
+
print(address)
|
|
136
|
+
elif args.get("batch"):
|
|
137
|
+
for key, value in info.items():
|
|
138
|
+
print(key, value, sep="\t")
|
|
139
|
+
else:
|
|
140
|
+
print(
|
|
141
|
+
data_table(
|
|
142
|
+
[{"field": key, "value": value} for key, value in info.items()],
|
|
143
|
+
["field", "value"],
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
if args.get("wps"):
|
|
147
|
+
await wps_enable(args, speedport)
|
|
148
|
+
if args.get("reconnect"):
|
|
149
|
+
await reconnect(args, speedport)
|
|
150
|
+
if args.get("reboot"):
|
|
151
|
+
await speedport.reboot()
|
|
152
|
+
if args.get("devices"):
|
|
153
|
+
if args.get("batch"):
|
|
154
|
+
batch_output(await speedport.devices)
|
|
155
|
+
else:
|
|
156
|
+
print(
|
|
157
|
+
data_table(
|
|
158
|
+
(await speedport.devices).values(),
|
|
159
|
+
["ipv4", "name", "type", "connected"],
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
if args.get("calls"):
|
|
163
|
+
if args.get("batch"):
|
|
164
|
+
batch_output(await speedport.devices)
|
|
165
|
+
else:
|
|
166
|
+
print(
|
|
167
|
+
data_table(
|
|
168
|
+
await speedport.calls,
|
|
169
|
+
["number", "type", "duration", "date"],
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def check_wifi_args(speedport, args):
|
|
175
|
+
if args.get("wifi") and args["wifi"] == "on":
|
|
176
|
+
await speedport.wifi_on()
|
|
177
|
+
elif args.get("wifi") and args["wifi"] == "off":
|
|
178
|
+
await speedport.wifi_off()
|
|
179
|
+
if args.get("guest-wifi") and args["guest-wifi"] == "on":
|
|
180
|
+
await speedport.wifi_guest_on()
|
|
181
|
+
elif args.get("guest-wifi") and args["guest-wifi"] == "off":
|
|
182
|
+
await speedport.wifi_guest_off()
|
|
183
|
+
if args.get("office-wifi") and args["office-wifi"] == "on":
|
|
184
|
+
await speedport.wifi_office_on()
|
|
185
|
+
elif args.get("office-wifi") and args["office-wifi"] == "off":
|
|
186
|
+
await speedport.wifi_office_off()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def main():
|
|
190
|
+
args = get_arguments()
|
|
191
|
+
set_logger(args)
|
|
192
|
+
password = ""
|
|
193
|
+
if not args.get("devices"):
|
|
194
|
+
if not (password := args["password"]):
|
|
195
|
+
password = getpass("Password of Speedports webinterface: ")
|
|
196
|
+
async with Speedport(args["host"], password, args.get("https")) as speedport:
|
|
197
|
+
await check_wifi_args(speedport, args)
|
|
198
|
+
await check_args(speedport, args)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def reconnect(args, speedport):
|
|
202
|
+
if not args.get("quiet"):
|
|
203
|
+
await speedport.update_ip_data()
|
|
204
|
+
_LOGGER.info(f"{speedport.public_ip_v4} / {speedport.public_ip_v6}")
|
|
205
|
+
await speedport.reconnect()
|
|
206
|
+
if not args.get("quiet"):
|
|
207
|
+
for i in range(240):
|
|
208
|
+
await speedport.update_ip_data()
|
|
209
|
+
if await speedport.online_status == "online":
|
|
210
|
+
_LOGGER.info(f"{speedport.public_ip_v4} / {speedport.public_ip_v6}")
|
|
211
|
+
break
|
|
212
|
+
await asyncio.sleep(0.5)
|
|
213
|
+
print(f"Connecting.{'.' * (i % 3)} ", end="\r", flush=True)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def wps_enable(args, speedport):
|
|
217
|
+
await speedport.wps_on()
|
|
218
|
+
if not args.get("quiet"):
|
|
219
|
+
event = time.time()
|
|
220
|
+
next_time = event
|
|
221
|
+
while await speedport.wps_state == 1:
|
|
222
|
+
await asyncio.sleep(next_time - time.time())
|
|
223
|
+
next_time = time.time() + 1
|
|
224
|
+
print(
|
|
225
|
+
f"wps connect enabled for {120 - int(time.time() - event)}s...",
|
|
226
|
+
end="\r",
|
|
227
|
+
flush=True,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def start():
|
|
232
|
+
try:
|
|
233
|
+
asyncio.run(main())
|
|
234
|
+
except KeyboardInterrupt:
|
|
235
|
+
print("Aborted.")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
start()
|
speedport/api.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from speedport import exceptions
|
|
10
|
+
from speedport.connection import Connection
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def need_auth(func):
|
|
16
|
+
@functools.wraps(func)
|
|
17
|
+
async def inner(self: "SpeedportApi", *args, **kwargs):
|
|
18
|
+
if not self.api.is_logged_in:
|
|
19
|
+
if not self.password:
|
|
20
|
+
error = f'You need to set a password to use "{func.__name__}"'
|
|
21
|
+
_LOGGER.error(error)
|
|
22
|
+
raise PermissionError(error)
|
|
23
|
+
await self.api.login(self.password)
|
|
24
|
+
try:
|
|
25
|
+
return await func(self, *args, **kwargs)
|
|
26
|
+
except exceptions.DecryptionKeyError as exception:
|
|
27
|
+
if not self.last_logout:
|
|
28
|
+
_LOGGER.info(f"Paused fetching for {self.pause_time} min")
|
|
29
|
+
self.last_logout = datetime.now()
|
|
30
|
+
if datetime.now() > (
|
|
31
|
+
time := self.last_logout + timedelta(minutes=self.pause_time)
|
|
32
|
+
):
|
|
33
|
+
self.last_logout = None
|
|
34
|
+
await self.api.login(self.password)
|
|
35
|
+
return await func(self, *args, **kwargs)
|
|
36
|
+
remaining = time - datetime.now()
|
|
37
|
+
error = f"Paused for 00:{remaining.seconds // 60:02d}:{remaining.seconds % 60:02d}"
|
|
38
|
+
_LOGGER.debug(error)
|
|
39
|
+
raise exceptions.LoginPausedError(error) from exception
|
|
40
|
+
|
|
41
|
+
return inner
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SpeedportApi:
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
host: str = "speedport.ip",
|
|
48
|
+
password: str = "",
|
|
49
|
+
https: bool = False,
|
|
50
|
+
session: aiohttp.ClientSession | None = None,
|
|
51
|
+
pause_time: int = 5,
|
|
52
|
+
):
|
|
53
|
+
self._api: Connection | None = None
|
|
54
|
+
self._host: str = host
|
|
55
|
+
self._password: str = password
|
|
56
|
+
self._https: bool = https
|
|
57
|
+
self._url = f"https://{host}" if https else f"http://{host}"
|
|
58
|
+
self._session: aiohttp.ClientSession | None = session
|
|
59
|
+
self._pause_time: int = pause_time
|
|
60
|
+
self._last_logout: datetime | None = None
|
|
61
|
+
|
|
62
|
+
async def __aenter__(self):
|
|
63
|
+
return await self.create()
|
|
64
|
+
|
|
65
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
66
|
+
await self.close()
|
|
67
|
+
|
|
68
|
+
async def create(self):
|
|
69
|
+
self._api = await Connection(self._url, self._session).create()
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
async def close(self):
|
|
73
|
+
if self._api:
|
|
74
|
+
await self._api.close()
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def api(self) -> Connection:
|
|
78
|
+
if self._api:
|
|
79
|
+
return self._api
|
|
80
|
+
raise ConnectionError()
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def password(self) -> str:
|
|
84
|
+
return self._password
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def url(self) -> str:
|
|
88
|
+
return self._url
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def pause_time(self) -> int:
|
|
92
|
+
return self._pause_time
|
|
93
|
+
|
|
94
|
+
@pause_time.setter
|
|
95
|
+
def pause_time(self, pause_time: int):
|
|
96
|
+
self._pause_time = pause_time
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def last_logout(self) -> datetime | None:
|
|
100
|
+
return self._last_logout
|
|
101
|
+
|
|
102
|
+
@last_logout.setter
|
|
103
|
+
def last_logout(self, last_logout: datetime | None):
|
|
104
|
+
self._last_logout = last_logout
|
|
105
|
+
|
|
106
|
+
@need_auth
|
|
107
|
+
async def get_secure_status(self):
|
|
108
|
+
return await self.api.get("data/SecureStatus.json", auth=True)
|
|
109
|
+
|
|
110
|
+
@need_auth
|
|
111
|
+
async def get_phone_handler(self):
|
|
112
|
+
return await self.api.get("data/IPPhoneHandler.json", auth=True)
|
|
113
|
+
|
|
114
|
+
async def get_router(self):
|
|
115
|
+
return await self.api.get("data/Router.json")
|
|
116
|
+
|
|
117
|
+
async def get_status(self):
|
|
118
|
+
return await self.api.get("data/Status.json")
|
|
119
|
+
|
|
120
|
+
async def get_devices(self):
|
|
121
|
+
return await self.api.get("data/DeviceList.json")
|
|
122
|
+
|
|
123
|
+
async def get_login(self):
|
|
124
|
+
return await self.api.get("data/Login.json")
|
|
125
|
+
|
|
126
|
+
@need_auth
|
|
127
|
+
async def get_ip_data(self):
|
|
128
|
+
referer = "html/content/internet/con_ipdata.html"
|
|
129
|
+
return await self.api.get("data/IPData.json", referer=referer, auth=True)
|
|
130
|
+
|
|
131
|
+
@need_auth
|
|
132
|
+
async def get_wps_state(self):
|
|
133
|
+
referer = "html/content/network/wlan_wps.html"
|
|
134
|
+
return await self.api.get("data/WPSStatus.json", referer=referer)
|
|
135
|
+
|
|
136
|
+
@need_auth
|
|
137
|
+
async def get_phone_calls(self):
|
|
138
|
+
referer = "html/content/phone/phone_call_taken.html"
|
|
139
|
+
return await self.api.get("data/PhoneCalls.json", referer=referer, auth=True)
|
|
140
|
+
|
|
141
|
+
@need_auth
|
|
142
|
+
async def set_wifi(self, status=True, guest=False, office=False):
|
|
143
|
+
"""Set wifi on/off"""
|
|
144
|
+
extra = "guest" if guest else "office" if office else ""
|
|
145
|
+
_LOGGER.info(
|
|
146
|
+
"Turn %s %s wifi...", ["off", "on"][bool(status)], extra if extra else ""
|
|
147
|
+
)
|
|
148
|
+
data = (
|
|
149
|
+
{f"wlan_{extra}_active": str(int(status))}
|
|
150
|
+
if extra
|
|
151
|
+
else {"use_wlan": str(int(status))}
|
|
152
|
+
)
|
|
153
|
+
referer = f"html/content/network/wlan_{extra if extra else 'basic'}.html"
|
|
154
|
+
return await self.api.post(
|
|
155
|
+
f"data/{'WLANBasic' if extra else 'Modules'}.json", data, referer
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@need_auth
|
|
159
|
+
async def wps_on(self):
|
|
160
|
+
_LOGGER.info("Enable wps connect...")
|
|
161
|
+
await self.api.post(
|
|
162
|
+
"data/WLANAccess.json",
|
|
163
|
+
{"wlan_add": "on", "wps_key": "connect"},
|
|
164
|
+
"html/content/network/wlan_wps.html",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@need_auth
|
|
168
|
+
async def reconnect(self):
|
|
169
|
+
_LOGGER.info("Reconnect with internet provider...")
|
|
170
|
+
await self.api.post(
|
|
171
|
+
"data/Connect.json",
|
|
172
|
+
{"req_connect": "reconnect"},
|
|
173
|
+
"html/content/internet/con_ipdata.html",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@need_auth
|
|
177
|
+
async def reboot(self):
|
|
178
|
+
_LOGGER.info("Reboot speedport...")
|
|
179
|
+
await self.api.post(
|
|
180
|
+
"data/Reboot.json",
|
|
181
|
+
{"reboot_device": "true"},
|
|
182
|
+
"html/content/config/restart.html",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def login(self, password=""):
|
|
186
|
+
return await self.api.login(password or self.password)
|
speedport/call.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Call:
|
|
5
|
+
def __init__(self, data, call_type):
|
|
6
|
+
self._data = data
|
|
7
|
+
self._call_type = call_type
|
|
8
|
+
|
|
9
|
+
def __str__(self):
|
|
10
|
+
return self.number
|
|
11
|
+
|
|
12
|
+
def __repr__(self):
|
|
13
|
+
return f"{self.__class__} ({self.__dict__})"
|
|
14
|
+
|
|
15
|
+
def __getitem__(self, item):
|
|
16
|
+
try:
|
|
17
|
+
return self.__getattribute__(item)
|
|
18
|
+
except AttributeError:
|
|
19
|
+
return self._data.get(
|
|
20
|
+
f"{self._call_type}_{item}", self.__getattribute__(item)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def data(self):
|
|
25
|
+
return self._data
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def type(self):
|
|
29
|
+
return self._call_type.replace("calls", "")
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def connection(self):
|
|
33
|
+
return self._data.get(f"{self._call_type}_as")
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def duration(self):
|
|
37
|
+
return int(self._data.get(f"{self._call_type}_duration", 0))
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def date(self):
|
|
41
|
+
date = f'{self._data.get(f"{self._call_type}_date")} {self._data.get(f"{self._call_type}_time")}'
|
|
42
|
+
return datetime.strptime(date, "%d.%m.%y %H:%M:%S")
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def number(self):
|
|
46
|
+
return self._data.get(f"{self._call_type}_who")
|
speedport/connection.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from hashlib import sha256
|
|
8
|
+
from json import JSONDecodeError
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
from Crypto.Cipher import AES
|
|
12
|
+
|
|
13
|
+
from speedport import const, exceptions
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def decode(data, key: str = "") -> dict[str, list | dict | str] | str:
|
|
19
|
+
"""Decode speedport's self-implemented encryption"""
|
|
20
|
+
key = key or const.DEFAULT_KEY
|
|
21
|
+
try:
|
|
22
|
+
ciphertext_tag = bytes.fromhex(data)
|
|
23
|
+
except ValueError as exc:
|
|
24
|
+
raise exceptions.DecryptionKeyError("Wrong decryption key") from exc
|
|
25
|
+
cipher = AES.new(bytes.fromhex(key), AES.MODE_CCM, bytes.fromhex(key)[:8])
|
|
26
|
+
decrypted = cipher.decrypt_and_verify(ciphertext_tag[:-16], ciphertext_tag[-16:])
|
|
27
|
+
try:
|
|
28
|
+
return simplify_response(json.loads(decrypted.decode()))
|
|
29
|
+
except JSONDecodeError:
|
|
30
|
+
return decrypted.decode()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def encode(data, key: str = "") -> str:
|
|
34
|
+
"""Encode for speedport's self-implemented encryption"""
|
|
35
|
+
key = key or const.DEFAULT_KEY
|
|
36
|
+
cipher = AES.new(bytes.fromhex(key), AES.MODE_CCM, bytes.fromhex(key)[:8])
|
|
37
|
+
ciphertext, tag = cipher.encrypt_and_digest(data.encode())
|
|
38
|
+
return ciphertext.hex() + tag.hex()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def simplify_response(
|
|
42
|
+
data: list[dict[str, str | list[dict[str, str]]]],
|
|
43
|
+
) -> dict[str, list | dict | str]:
|
|
44
|
+
"""Remove 'varid' and 'varvalue' to have a flatter data structure without overhead"""
|
|
45
|
+
result = {}
|
|
46
|
+
for item in data:
|
|
47
|
+
if isinstance(item["varvalue"], list):
|
|
48
|
+
list_item = {}
|
|
49
|
+
for sub_item in item["varvalue"]:
|
|
50
|
+
list_item[sub_item["varid"]] = sub_item["varvalue"]
|
|
51
|
+
result.setdefault(item["varid"], []).append(list_item)
|
|
52
|
+
else:
|
|
53
|
+
result[item["varid"]] = item["varvalue"]
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Connection:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
host_url: str = "",
|
|
61
|
+
session: aiohttp.ClientSession | None = None,
|
|
62
|
+
):
|
|
63
|
+
self._login_key = ""
|
|
64
|
+
self._url = host_url
|
|
65
|
+
self._own_session = session is None
|
|
66
|
+
self._session: aiohttp.ClientSession | None = session
|
|
67
|
+
self._verify_ssl: bool = False
|
|
68
|
+
self._timeout: float = 30
|
|
69
|
+
|
|
70
|
+
async def __aenter__(self):
|
|
71
|
+
return await self.create()
|
|
72
|
+
|
|
73
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
74
|
+
await self.close()
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def _kwargs(self):
|
|
78
|
+
return {"verify_ssl": self._verify_ssl, "timeout": self._timeout}
|
|
79
|
+
|
|
80
|
+
async def create(self) -> Connection:
|
|
81
|
+
"""Create aiohttp session"""
|
|
82
|
+
connector = aiohttp.TCPConnector(verify_ssl=self._verify_ssl)
|
|
83
|
+
if not self._session:
|
|
84
|
+
jar = aiohttp.CookieJar(unsafe=True)
|
|
85
|
+
self._session = aiohttp.ClientSession(connector=connector, cookie_jar=jar)
|
|
86
|
+
else:
|
|
87
|
+
self._session.cookie_jar._unsafe = True
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
async def close(self):
|
|
91
|
+
"""Close aiohttp session"""
|
|
92
|
+
if self._own_session:
|
|
93
|
+
await self._session.close()
|
|
94
|
+
|
|
95
|
+
async def get(self, path: str, auth: bool = False, referer: str = ""):
|
|
96
|
+
url = f"{self._url}/{path}"
|
|
97
|
+
if referer:
|
|
98
|
+
referer = f"{self._url}/{referer}"
|
|
99
|
+
if token := await self._get_httoken(referer):
|
|
100
|
+
url += f"?_tn={token}"
|
|
101
|
+
async with self._session.get(
|
|
102
|
+
url, headers={"Referer": referer}, **self._kwargs
|
|
103
|
+
) as response:
|
|
104
|
+
_LOGGER.debug("GET - %s - %s", url, response.status)
|
|
105
|
+
key = self._login_key if auth else const.DEFAULT_KEY
|
|
106
|
+
return decode(await response.text(), key=key)
|
|
107
|
+
|
|
108
|
+
async def post(self, path: str, data: dict[str, str], referer: str):
|
|
109
|
+
url = f"{self._url}/{path}"
|
|
110
|
+
referer = f"{self._url}/{referer}"
|
|
111
|
+
if token := await self._get_httoken(referer):
|
|
112
|
+
data.update({"httoken": token})
|
|
113
|
+
data = "&".join([f"{k}={v}" for k, v in data.items()])
|
|
114
|
+
data = encode(data, key=self._login_key)
|
|
115
|
+
async with self._session.post(
|
|
116
|
+
url, headers={"Referer": referer}, data=data, **self._kwargs
|
|
117
|
+
) as response:
|
|
118
|
+
_LOGGER.debug("POST - %s - %s", url, response.status)
|
|
119
|
+
return decode(await response.text(), key=self._login_key)
|
|
120
|
+
|
|
121
|
+
async def _get_httoken(self, url: str) -> str:
|
|
122
|
+
async with self._session.get(url, **self._kwargs) as response:
|
|
123
|
+
_LOGGER.debug("GET - %s - %s", url, response.status)
|
|
124
|
+
if token := re.findall("httoken = (\\d+)", await response.text()):
|
|
125
|
+
return token[0]
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
async def _get_login_key(self):
|
|
129
|
+
data = {"getChallenge": "1"}
|
|
130
|
+
if response := await self.post("data/Login.json", data, "/"):
|
|
131
|
+
self._login_key = response["challenge"]
|
|
132
|
+
return self._login_key
|
|
133
|
+
|
|
134
|
+
async def login(self, password: str):
|
|
135
|
+
url = f"{self._url}/data/Login.json"
|
|
136
|
+
self.clear()
|
|
137
|
+
login_data = f"{await self._get_login_key()}:{password}".encode()
|
|
138
|
+
login_key = sha256(login_data).hexdigest()
|
|
139
|
+
data = encode("showpw=0&password=" + login_key)
|
|
140
|
+
async with self._session.post(url, data=data, **self._kwargs) as response:
|
|
141
|
+
_LOGGER.debug("POST - %s - %s", url, response.status)
|
|
142
|
+
result = decode(await response.text())
|
|
143
|
+
_LOGGER.debug(result)
|
|
144
|
+
if (sleep_time := result.get("login_locked")) and int(sleep_time) < 60:
|
|
145
|
+
_LOGGER.warning("Can't login, wait %ss...", sleep_time)
|
|
146
|
+
await asyncio.sleep(int(sleep_time) + 1)
|
|
147
|
+
return await self.login(password)
|
|
148
|
+
if result["login"] != "success":
|
|
149
|
+
_LOGGER.error(result)
|
|
150
|
+
raise exceptions.LoginException("Can't login")
|
|
151
|
+
return result["login"]
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def is_logged_in(self):
|
|
155
|
+
return bool(self._login_key)
|
|
156
|
+
|
|
157
|
+
def clear(self):
|
|
158
|
+
self._login_key = ""
|
|
159
|
+
self._session.cookie_jar.clear_domain(self._url)
|
speedport/const.py
ADDED
speedport/device.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
class WlanDevice:
|
|
2
|
+
def __init__(self, data):
|
|
3
|
+
self._data = data
|
|
4
|
+
|
|
5
|
+
def __str__(self):
|
|
6
|
+
return self.name
|
|
7
|
+
|
|
8
|
+
def __repr__(self):
|
|
9
|
+
return f"{self.__class__} ({self.__dict__})"
|
|
10
|
+
|
|
11
|
+
def __getitem__(self, item):
|
|
12
|
+
try:
|
|
13
|
+
return self.__getattribute__(item)
|
|
14
|
+
except AttributeError:
|
|
15
|
+
return self._data.get(f"mdevice_{item}", self._data.get(item))
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def data(self):
|
|
19
|
+
return self._data
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def connected(self):
|
|
23
|
+
return bool(int(self._data.get("mdevice_connected") or "0"))
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def downspeed(self):
|
|
27
|
+
return int(self._data.get("mdevice_downspeed") or "0")
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def fix_dhcp(self):
|
|
31
|
+
return bool(int(self._data.get("mdevice_fix_dhcp") or "0"))
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def gua_ipv6(self):
|
|
35
|
+
return self._data.get("mdevice_gua_ipv6")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def hasui(self):
|
|
39
|
+
return int(self._data.get("mdevice_hasui") or "0")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def ipv4(self):
|
|
43
|
+
return self._data.get("mdevice_ipv4")
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def mac(self):
|
|
47
|
+
return self._data.get("mdevice_mac")
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def name(self):
|
|
51
|
+
return self._data.get("mdevice_name")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def reservedip(self):
|
|
55
|
+
return self._data.get("mdevice_reservedip")
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def rssi(self):
|
|
59
|
+
return int(self._data.get("mdevice_rssi") or "0")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def slave(self):
|
|
63
|
+
return bool(int(self._data.get("mdevice_slave") or "0"))
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def type(self):
|
|
67
|
+
return ["lan", "wlan", "wlan5"][int(self._data.get("mdevice_type", "0")) % 3]
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def ula_ipv6(self):
|
|
71
|
+
return self._data.get("mdevice_ula_ipv6")
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def upspeed(self):
|
|
75
|
+
return int(self._data.get("mdevice_upspeed") or "0")
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def use_dhcp(self):
|
|
79
|
+
return bool(int(self._data.get("mdevice_use_dhcp") or "0"))
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def wifi(self):
|
|
83
|
+
return int(self._data.get("mdevice_wifi") or "0")
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def id(self):
|
|
87
|
+
return int(self._data.get("id") or "0")
|
speedport/exceptions.py
ADDED
speedport/speedport.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from . import exceptions
|
|
10
|
+
from .api import SpeedportApi
|
|
11
|
+
from .call import Call
|
|
12
|
+
from .device import WlanDevice
|
|
13
|
+
|
|
14
|
+
_LOGGER = logging.getLogger(__name__)
|
|
15
|
+
_LOGGER.setLevel(logging.WARNING)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoginException(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Speedport:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
host: str = "speedport.ip",
|
|
26
|
+
password: str = "",
|
|
27
|
+
https: bool = False,
|
|
28
|
+
session: aiohttp.ClientSession | None = None,
|
|
29
|
+
pause_time: int = 5,
|
|
30
|
+
):
|
|
31
|
+
self._api: SpeedportApi | None = None
|
|
32
|
+
self._host: str = host
|
|
33
|
+
self._password: str = password
|
|
34
|
+
self._https: bool = https
|
|
35
|
+
self._session: aiohttp.ClientSession | None = session
|
|
36
|
+
self._status = {}
|
|
37
|
+
self._ip_data = {}
|
|
38
|
+
self._pause_time = pause_time
|
|
39
|
+
|
|
40
|
+
async def __aenter__(self):
|
|
41
|
+
return await self.create()
|
|
42
|
+
|
|
43
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
44
|
+
await self.close()
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, item):
|
|
47
|
+
if (status := self._status.get(item)) is not None:
|
|
48
|
+
return status
|
|
49
|
+
if (ip_data := self._ip_data.get(item)) is not None:
|
|
50
|
+
return ip_data
|
|
51
|
+
raise KeyError
|
|
52
|
+
|
|
53
|
+
def get(self, item, default=None):
|
|
54
|
+
try:
|
|
55
|
+
return self[item]
|
|
56
|
+
except KeyError:
|
|
57
|
+
return default
|
|
58
|
+
|
|
59
|
+
async def create(self):
|
|
60
|
+
self._api = await SpeedportApi(
|
|
61
|
+
self._host, self._password, self._https, self._session, self._pause_time
|
|
62
|
+
).create()
|
|
63
|
+
await self.update_status()
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
async def close(self):
|
|
67
|
+
if self._api:
|
|
68
|
+
await self._api.close()
|
|
69
|
+
|
|
70
|
+
def set_pause_time(self, pause_time: int):
|
|
71
|
+
if self._api:
|
|
72
|
+
self._api.pause_time = pause_time
|
|
73
|
+
self._pause_time = pause_time
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def api(self) -> SpeedportApi:
|
|
77
|
+
if self._api:
|
|
78
|
+
return self._api
|
|
79
|
+
raise ConnectionError()
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
async def devices(self) -> dict[str, WlanDevice]:
|
|
83
|
+
data = await self.api.get_devices()
|
|
84
|
+
devices = data.get("addmlandevice", []) + data.get("addmwlan5device", [])
|
|
85
|
+
devices += data.get("addmwlandevice", []) + data.get("addmdevice", [])
|
|
86
|
+
devices = sorted(devices, key=lambda d: int(d["mdevice_ipv4"].split(".")[-1]))
|
|
87
|
+
return {device.get("mdevice_mac"): WlanDevice(device) for device in devices}
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
async def calls(self) -> list[Call]:
|
|
91
|
+
data = await self.api.get_phone_calls()
|
|
92
|
+
types = ["dialedcalls", "missedcalls", "takencalls"]
|
|
93
|
+
result: list[Call] = []
|
|
94
|
+
for call_type in types:
|
|
95
|
+
for call in data.get(f"add{call_type}"):
|
|
96
|
+
result.append(Call(call, call_type))
|
|
97
|
+
result.sort(key=lambda d: d.date)
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
async def wifi_on(self):
|
|
101
|
+
await self.api.set_wifi(status=True, guest=False)
|
|
102
|
+
|
|
103
|
+
async def wifi_off(self):
|
|
104
|
+
await self.api.set_wifi(status=False, guest=False)
|
|
105
|
+
|
|
106
|
+
async def wps_on(self):
|
|
107
|
+
await self.api.wps_on()
|
|
108
|
+
|
|
109
|
+
async def reconnect(self):
|
|
110
|
+
await self.api.reconnect()
|
|
111
|
+
|
|
112
|
+
async def reboot(self):
|
|
113
|
+
await self.api.reboot()
|
|
114
|
+
|
|
115
|
+
async def wifi_guest_on(self):
|
|
116
|
+
await self.api.set_wifi(status=True, guest=True)
|
|
117
|
+
|
|
118
|
+
async def wifi_guest_off(self):
|
|
119
|
+
await self.api.set_wifi(status=False, guest=True)
|
|
120
|
+
|
|
121
|
+
async def wifi_office_on(self):
|
|
122
|
+
await self.api.set_wifi(status=True, office=True)
|
|
123
|
+
|
|
124
|
+
async def wifi_office_off(self):
|
|
125
|
+
await self.api.set_wifi(status=False, office=True)
|
|
126
|
+
|
|
127
|
+
async def update_ip_data(self):
|
|
128
|
+
with suppress(exceptions.LoginPausedError):
|
|
129
|
+
self._ip_data = await self.api.get_ip_data()
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
async def ip_info(self) -> dict:
|
|
133
|
+
"""Fetch IPv4/IPv6 addresses, gateways, DNS servers and IPv6 prefixes."""
|
|
134
|
+
self._ip_data = await self.api.get_ip_data()
|
|
135
|
+
return self._ip_data.copy()
|
|
136
|
+
|
|
137
|
+
async def update_status(self):
|
|
138
|
+
self._status = await self.api.get_status()
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def device_name(self):
|
|
142
|
+
return self._status.get("device_name", "")
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def serial_number(self):
|
|
146
|
+
return self._status.get("serial_number", "")
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def firmware_version(self):
|
|
150
|
+
return self._status.get("firmware_version", "")
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def dsl_downstream(self):
|
|
154
|
+
return int(self._status.get("dsl_downstream", "0"))
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def dsl_upstream(self):
|
|
158
|
+
return int(self._status.get("dsl_upstream", "0"))
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def inet_download(self):
|
|
162
|
+
return int(self._status.get("inet_download", "0"))
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def inet_upload(self):
|
|
166
|
+
return int(self._status.get("inet_upload", "0"))
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def inet_uptime(self):
|
|
170
|
+
return datetime.fromisoformat(self._status.get("inet_uptime"))
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def online_status(self):
|
|
174
|
+
return self._status.get("onlinestatus", "")
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def login_state(self):
|
|
178
|
+
return bool(self._status.get("loginstate"))
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def rebooting(self):
|
|
182
|
+
return bool(self._status.get("rebooting"))
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def wlan_active(self):
|
|
186
|
+
return bool(int(self._status.get("use_wlan")))
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def wlan_guest_active(self):
|
|
190
|
+
return bool(int(self._status.get("wlan_guest_active")))
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def wlan_office_active(self):
|
|
194
|
+
return bool(int(self._status.get("wlan_office_active")))
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def wlan_ssid(self):
|
|
198
|
+
return self._status.get("wlan_ssid")
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def wlan_guest_ssid(self):
|
|
202
|
+
return self._status.get("wlan_guest_ssid")
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def wlan_office_ssid(self):
|
|
206
|
+
return self._status.get("wlan_office_ssid")
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def dns_v4(self):
|
|
210
|
+
return self._ip_data.get("dns_v4", "")
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def dns_v6(self):
|
|
214
|
+
return self._ip_data.get("dns_v6", "")
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def gateway_ip_v4(self):
|
|
218
|
+
return self._ip_data.get("gateway_ip_v4", "")
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def gateway_ip_v6(self):
|
|
222
|
+
return self._ip_data.get("gateway_ip_v6", "")
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def public_ip_v4(self):
|
|
226
|
+
return self._ip_data.get("public_ip_v4", "")
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def public_ip_v6(self):
|
|
230
|
+
return self._ip_data.get("public_ip_v6", "")
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def sec_dns_v4(self):
|
|
234
|
+
return self._ip_data.get("sec_dns_v4", "")
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def sec_dns_v6(self):
|
|
238
|
+
return self._ip_data.get("sec_dns_v6", "")
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def transmitted_ip_v6_pool_for_lan(self):
|
|
242
|
+
return self._ip_data.get("transmitted_ip_v6_pool_for_lan", "")
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def used_ip_v6_lan(self):
|
|
246
|
+
return self._ip_data.get("used_ip_v6_lan", "")
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
async def wps_remaining(self):
|
|
250
|
+
return int((await self.api.get_wps_state())["wlan_wps_state"])
|
|
251
|
+
|
|
252
|
+
async def login(self, password=""):
|
|
253
|
+
return await self.api.login(password or self._password)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: speedport-api-expanded
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Control Telekom Speedport routers with Python
|
|
5
|
+
Home-page: https://github.com/kplrm/speedport-api-expanded
|
|
6
|
+
Author: Andre Basche, Diego Guillen-Rosaperez
|
|
7
|
+
Maintainer: Diego Guillen-Rosaperez
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: GitHub, https://github.com/kplrm/speedport-api-expanded
|
|
10
|
+
Project-URL: PyPI, https://pypi.org/project/speedport-api-expanded/
|
|
11
|
+
Project-URL: Upstream, https://github.com/Andre0512/speedport-api
|
|
12
|
+
Platform: any
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: aiohttp>=3.8
|
|
30
|
+
Requires-Dist: pycryptodome>=3.18
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: description-content-type
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: license
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
Dynamic: maintainer
|
|
39
|
+
Dynamic: platform
|
|
40
|
+
Dynamic: project-url
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
44
|
+
|
|
45
|
+
**This python package is unofficial and is not related in any way to Telekom. It was developed by reversed engineered http requests and can stop working at anytime!**
|
|
46
|
+
|
|
47
|
+
## Speedport-API
|
|
48
|
+
[](https://pypi.org/project/speedport-api-expanded/)
|
|
49
|
+
[](https://pypi.org/project/speedport-api-expanded/)
|
|
50
|
+
[](https://www.python.org/)
|
|
51
|
+
[](https://github.com/kplrm/speedport-api-expanded/blob/main/LICENSE)
|
|
52
|
+
[](https://pypistats.org/packages/speedport-api-expanded)
|
|
53
|
+
|
|
54
|
+
Control Telekom Speedport routers with Python!
|
|
55
|
+
|
|
56
|
+
### Installation
|
|
57
|
+
```commandline
|
|
58
|
+
pip install speedport-api-expanded
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For this fork's changes, install from a checkout with `python -m pip install -e .`
|
|
62
|
+
or download a wheel from [GitHub releases](https://github.com/kplrm/speedport-api-expanded/releases).
|
|
63
|
+
The Python import remains `from speedport import Speedport`.
|
|
64
|
+
See [local development and adding features](docs/development.md),
|
|
65
|
+
[contribution guidelines](CONTRIBUTING.md), and [contributors](CONTRIBUTORS.md).
|
|
66
|
+
Store local router credentials in an
|
|
67
|
+
ignored `.env` copied from [.env.example](.env.example); see the guide for loading it.
|
|
68
|
+
|
|
69
|
+
### Supported Devices
|
|
70
|
+
* Speedport Smart 4
|
|
71
|
+
|
|
72
|
+
### Commandline tool
|
|
73
|
+
With this shipped commandline tool can a speedport in your network be controlled.
|
|
74
|
+
|
|
75
|
+
#### Turn wifi off
|
|
76
|
+
```commandline
|
|
77
|
+
$ speedport wifi off
|
|
78
|
+
Turn off wifi...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Turn guest wifi on
|
|
82
|
+
```commandline
|
|
83
|
+
$ speedport guest-wifi on
|
|
84
|
+
Turn on guest wifi...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Reconnect for new ip address
|
|
88
|
+
```commandline
|
|
89
|
+
$ speedport reconnect
|
|
90
|
+
123.45.67.89 / 5403:f3:35aa:12f:7287:41cf:fb1c:3c83
|
|
91
|
+
Reconnect with internet provider...
|
|
92
|
+
123.45.67.12 / 5403:f3:35fe:12f:7287:41cf:fb1c:3c83
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Query IPv4 and IPv6 information
|
|
96
|
+
```commandline
|
|
97
|
+
speedport --host 192.168.2.1 ip-info
|
|
98
|
+
speedport --host 192.168.2.1 --batch ip-info
|
|
99
|
+
```
|
|
100
|
+
Prints addresses, gateways, primary/secondary DNS servers, and IPv6 prefixes.
|
|
101
|
+
Batch output uses tab-separated field/value pairs. A password is prompted for
|
|
102
|
+
unless `SPEEDPORT_PASSWORD` is set.
|
|
103
|
+
|
|
104
|
+
To print only the public IPv4 address, without a label or table:
|
|
105
|
+
```commandline
|
|
106
|
+
python -m speedport --host 192.168.2.1 ip-info --ipv4
|
|
107
|
+
93.218.250.118
|
|
108
|
+
```
|
|
109
|
+
If no public IPv4 address is available, the command exits with an error.
|
|
110
|
+
|
|
111
|
+
#### Enable wps connect
|
|
112
|
+
```commandline
|
|
113
|
+
$ speedport wps
|
|
114
|
+
Enable wps connect...
|
|
115
|
+
wps connect enabled for 113s...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Reboot device
|
|
119
|
+
```commandline
|
|
120
|
+
$ speedport reboot
|
|
121
|
+
Reboot speedport...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### Print devices
|
|
125
|
+
```commandline
|
|
126
|
+
$ speedport devices
|
|
127
|
+
+-------------+---------------------+-------+-----------+
|
|
128
|
+
| ipv4 | name | type | connected |
|
|
129
|
+
+-------------+---------------------+-------+-----------+
|
|
130
|
+
| 10.5.12.32 | Google-Home-Mini-1 | wlan | 1 |
|
|
131
|
+
| 10.5.12.157 | PC10-5-12-157 | lan | 0 |
|
|
132
|
+
| 10.5.12.227 | andre-xps | wlan5 | 1 |
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
#### Print calls
|
|
137
|
+
```commandline
|
|
138
|
+
$ speedport calls
|
|
139
|
+
+-------------+--------+----------+---------------------+
|
|
140
|
+
| number | type | duration | date |
|
|
141
|
+
+-------------+--------+----------+---------------------+
|
|
142
|
+
| 01578212345 | missed | 0 | 2024-04-04 09:34:35 |
|
|
143
|
+
| 026361245 | taken | 1337 | 2024-04-06 05:12:53 |
|
|
144
|
+
| 7866 | dialed | 20 | 2024-04-06 18:39:00 |
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Library
|
|
148
|
+
|
|
149
|
+
#### IP information example
|
|
150
|
+
```python
|
|
151
|
+
import asyncio
|
|
152
|
+
import os
|
|
153
|
+
from speedport import Speedport
|
|
154
|
+
|
|
155
|
+
async def ip_info():
|
|
156
|
+
async with Speedport(
|
|
157
|
+
os.environ.get("SPEEDPORT_HOST", "192.168.2.1"),
|
|
158
|
+
os.environ["SPEEDPORT_PASSWORD"],
|
|
159
|
+
) as router:
|
|
160
|
+
info = await router.ip_info
|
|
161
|
+
print(info["public_ip_v4"], info["public_ip_v6"])
|
|
162
|
+
print(info) # All fields, including DNS servers and IPv6 prefixes.
|
|
163
|
+
|
|
164
|
+
asyncio.run(ip_info())
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Reconnect example
|
|
168
|
+
```python
|
|
169
|
+
import asyncio
|
|
170
|
+
from speedport import Speedport
|
|
171
|
+
|
|
172
|
+
async def reconnect():
|
|
173
|
+
speedport = Speedport("192.168.1.1")
|
|
174
|
+
await speedport.login("password123")
|
|
175
|
+
await speedport.reconnect()
|
|
176
|
+
|
|
177
|
+
asyncio.run(reconnect())
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Devices example
|
|
181
|
+
```python
|
|
182
|
+
import asyncio
|
|
183
|
+
from speedport import Speedport
|
|
184
|
+
|
|
185
|
+
devices = asyncio.run(Speedport().devices)
|
|
186
|
+
for device in devices:
|
|
187
|
+
print(device.ipv4, device.connected)
|
|
188
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
speedport/__init__.py,sha256=emHP7xbzqtem60b6_C2V-8Iks9eCwB8iWFdJirC2hPU,58
|
|
2
|
+
speedport/__main__.py,sha256=37TJblmz-CM6jybaLkHdqiDQv0oe7Wz2pK8gkxWx3D0,7765
|
|
3
|
+
speedport/api.py,sha256=sAWBA12UZL1Id01ptHMLAG7IGyxT-MKYnt2x7PPsSog,5824
|
|
4
|
+
speedport/call.py,sha256=OopvZcyz6n49ngLL2PezTTaN-UGtZil1aomMGVQdyYM,1169
|
|
5
|
+
speedport/connection.py,sha256=yYxBoDxRemV7cFUdFs9yIlTCLm81bK-MVR7ZgrtEWkE,5877
|
|
6
|
+
speedport/const.py,sha256=TFSJKaDPKUdOnepb2MUzpe1ecnaMOtMvoN0dBFUUwp8,143
|
|
7
|
+
speedport/device.py,sha256=XvOkcg0sxaRIGJKp_Z1VGo6TQJSOIPngNFFyIQ2IdDI,2083
|
|
8
|
+
speedport/exceptions.py,sha256=RLqgPTXKEE1m20egEgvei3mrOMT9sqjCj4X8iRv5P_g,148
|
|
9
|
+
speedport/speedport.py,sha256=ydpxvWjSfP9acxpeHBucf_uLMBIUXHnSpl0C4FyPAaE,6958
|
|
10
|
+
speedport_api_expanded-0.7.0.dist-info/licenses/LICENSE,sha256=j4F0U6fAmiZRpjNhTlP7Q6PiK_3kHxyoEgB6ura82aA,1112
|
|
11
|
+
speedport_api_expanded-0.7.0.dist-info/METADATA,sha256=4QHCELF8QDY419qMlRsPYOxXxSyByl42Fi16AGwDgUI,6028
|
|
12
|
+
speedport_api_expanded-0.7.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
13
|
+
speedport_api_expanded-0.7.0.dist-info/entry_points.txt,sha256=iMYZzg3YJVhauEu-I6QC8lQHvQEdueA4gjwriyHDYnY,55
|
|
14
|
+
speedport_api_expanded-0.7.0.dist-info/top_level.txt,sha256=9AObLD9lR2H0X-nJfCceDaKNdR5y2PV55HveX05H-b8,10
|
|
15
|
+
speedport_api_expanded-0.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Andre Basche
|
|
4
|
+
Copyright (c) 2026 Diego Guillen-Rosaperez
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
speedport
|