diff --git a/docs/de/userlogin.png b/docs/de/userlogin.png
new file mode 100644
index 00000000..0ab0cd71
Binary files /dev/null and b/docs/de/userlogin.png differ
diff --git a/docs/de/userpermissions.png b/docs/de/userpermissions.png
new file mode 100644
index 00000000..fe3d7790
Binary files /dev/null and b/docs/de/userpermissions.png differ
diff --git a/lib/fritz.py b/lib/fritz.py
new file mode 100644
index 00000000..6fd6f13a
--- /dev/null
+++ b/lib/fritz.py
@@ -0,0 +1,120 @@
+import xml.etree.ElementTree as ET
+import urllib.parse
+import urllib.request
+import time
+import hashlib
+import sys
+
+"""
+Example code Python3
+#!/usr/bin/env python3
+# vim: expandtab sw=4 ts=4
+
+FRITZ!OS WebGUI Login
+Get a sid (session ID) via PBKDF2 based challenge response algorithm. Fallback to MD5 if FRITZ!OS has no PBKDF2 support.
+AVM 2020-09-25
+"""
+
+LOGIN_SID_ROUTE = "/login_sid.lua?version=2"
+
+
+class LoginState:
+ def __init__(self, challenge: str, blocktime: int):
+ self.challenge = challenge
+ self.blocktime = blocktime
+ self.is_pbkdf2 = challenge.startswith("2$")
+
+
+def get_sid(box_url: str, username: str, password: str) -> str:
+ """ Get a sid by solving the PBKDF2 (or MD5) challenge-response process. """
+ try:
+ state = get_login_state(box_url)
+ except Exception as ex:
+ raise Exception("failed to get challenge") from ex
+ if state.is_pbkdf2:
+ print("PBKDF2 supported")
+ challenge_response = calculate_pbkdf2_response(
+ state.challenge, password)
+ else:
+ print("Falling back to MD5")
+ challenge_response = calculate_md5_response(state.challenge, password)
+ if state.blocktime > 0:
+ print(f"Waiting for {state.blocktime} seconds...")
+ time.sleep(state.blocktime)
+ try:
+ sid = send_response(box_url, username, challenge_response)
+ except Exception as ex:
+ raise Exception("failed to login") from ex
+ if sid == "0000000000000000":
+ raise Exception("wrong username or password")
+ return sid
+
+
+def get_login_state(box_url: str) -> LoginState:
+ """ Get login state from FRITZ!Box using login_sid.lua?version=2 """
+ url = box_url + LOGIN_SID_ROUTE
+ http_response = urllib.request.urlopen(url)
+ xml = ET.fromstring(http_response.read())
+ # print(f"xml: {xml}")
+ challenge = xml.find("Challenge").text
+ blocktime = int(xml.find("BlockTime").text)
+ return LoginState(challenge, blocktime)
+
+
+def calculate_pbkdf2_response(challenge: str, password: str) -> str:
+ """ Calculate the response for a given challenge via PBKDF2 """
+ challenge_parts = challenge.split("$")
+ # Extract all necessary values encoded into the challenge
+ iter1 = int(challenge_parts[1])
+ salt1 = bytes.fromhex(challenge_parts[2])
+ iter2 = int(challenge_parts[3])
+ salt2 = bytes.fromhex(challenge_parts[4])
+ # Hash twice, once with static salt...
+ # Once with dynamic salt.
+ hash1 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt1, iter1)
+ hash2 = hashlib.pbkdf2_hmac("sha256", hash1, salt2, iter2)
+ return f"{challenge_parts[4]}${hash2.hex()}"
+
+
+def calculate_md5_response(challenge: str, password: str) -> str:
+ """ Calculate the response for a challenge using legacy MD5 """
+ response = challenge + "-" + password
+ # the legacy response needs utf_16_le encoding
+ response = response.encode("utf_16_le")
+ md5_sum = hashlib.md5()
+ md5_sum.update(response)
+ response = challenge + "-" + md5_sum.hexdigest()
+ return response
+
+
+def send_response(box_url: str, username: str, challenge_response: str) -> str:
+ """ Send the response and return the parsed sid. raises an Exception on error """
+ # Build response params
+ post_data_dict = {"username": username, "response": challenge_response}
+ post_data = urllib.parse.urlencode(post_data_dict).encode()
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
+ url = box_url + LOGIN_SID_ROUTE
+ # Send response
+ http_request = urllib.request.Request(url, post_data, headers)
+ http_response = urllib.request.urlopen(http_request)
+ # Parse SID from resulting XML.
+ xml = ET.fromstring(http_response.read())
+ return xml.find("SID").text
+
+
+def main():
+ if len(sys.argv) < 4:
+ print(
+ f"Usage: {sys.argv[0]} http://fritz.box user pass"
+ )
+ exit(1)
+ url = sys.argv[1]
+ username = sys.argv[2]
+ password = sys.argv[3]
+ sid = get_sid(url, username, password)
+ print(f"Successful login for user: {username}")
+ print(f"sid: {sid}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/main.js b/main.js
index a2d5424b..396f92ad 100644
--- a/main.js
+++ b/main.js
@@ -51,8 +51,8 @@ var fritzTimeout;
512 = ON_OFF
513 = LEVEL_CTRL
514 = COLOR_CTRL
-516 = ? detected with blinds
-517 = ? detected with blinds
+516 = ? detected with blinds, different alert?
+517 = ? detected with blinds, different alerttimestamp
772 = SIMPLE_BUTTON
1024 = SUOTA-Update
*/
@@ -1734,6 +1734,7 @@ function main() {
},
native: {}
});
+ adapter.setState(typ + newId + '.blindsopen', { val: false, ack: true }); //Button auf Ausgangszustand
adapter.setObjectNotExists(typ + newId + '.blindsclose', {
type: 'state',
common: {
@@ -1746,6 +1747,7 @@ function main() {
},
native: {}
});
+ adapter.setState(typ + newId + '.blindsclose', { val: false, ack: true }); //Button auf Ausgangszustand
adapter.setObjectNotExists(typ + newId + '.blindsstop', {
type: 'state',
common: {
@@ -1758,6 +1760,7 @@ function main() {
},
native: {}
});
+ adapter.setState(typ + newId + '.blindsstop', { val: false, ack: true }); //Button auf Ausgangszustand
}
function createDevices() {
diff --git a/test/lib/fritzserver.js b/test/lib/fritzserver.js
index a558a83b..065df0d7 100644
--- a/test/lib/fritzserver.js
+++ b/test/lib/fritzserver.js
@@ -56,7 +56,7 @@ function handleHttpRequest(request, response) {
'0'
);
response.end();
- } else if (request.url == '/login_sid.lua?version=2') {
+ } else if (request.url == '/login_sid.lua?version=2' && request.method == 'GET') {
//check the URL of the current request
response.writeHead(200, { 'Content-Type': 'application/xml' });
response.write(
@@ -85,7 +85,7 @@ function handleHttpRequest(request, response) {
'0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2'
);
response.end();
- } else if (request.url == '/login_sid.lua?version=2?username=admin&response=' + challengeResponse) {
+ } else if (request.url == '/login_sid.lua?version=2' && request.method == 'POST') {
//check the URL of the current request
response.writeHead(200, { 'Content-Type': 'application/xml' });
response.write(