diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 9047433..c8d558d 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Check if PR should be auto-merged - uses: ahmadnassri/action-dependabot-auto-merge@v2 + uses: ahmadnassri/action-dependabot-auto-merge@v3 with: # This must be a personal access token with push access github-token: ${{ secrets.AUTO_MERGE_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b72a041..29ade71 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: '14.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index c8c3e2a..c71426a 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: node-version: '14.x' @@ -41,15 +41,15 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [14.x, 16.x, 18.x] - os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [14, 16, 18] + os: [ubuntu-latest] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: 'npm' diff --git a/README.md b/README.md index 5b47522..387c643 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,12 @@ This library is not, in any way, affiliated or related to AVM GmbH. Use it at yo * control switches, thermostats, blinds, lamps * control grouped devices * control configured templates -* uses new session ID method (FW >7.25), as well as the fallback to md5 method as a fallback -* no production dependencies +* uses new session ID method (FW >7.25), as well as the fallback to md5 method +* no production dependencies for the API itself (the dependencies are only related to the testscript and emulation) ## Getting Started -it is an ES module with named exports +it is an common js module with named exports. +it exposes 2 classes, the API (Fritz) and an emulation (FritzEmu) ### Prerequisites * nodejs >14 (may work with older version, but tested with > 14) @@ -27,10 +28,29 @@ install the released version on npm with npm install fritzdect-aha-nodejs ``` +### Usage +```javascript +const Fritz = require('fritzdect-aha-nodejs').Fritz; +fritz = new Fritz(yourUsername, yourPassword, your.Url || '', your.options || {}); + +//your async function +... +const login = await fritz.login_SID(); +const devicelistinfos = await fritz.getDeviceListInfos(); +const logout = await fritz.logout_SID(); +... +``` +see the example.js. + +## API Calls +* todo for 1.0.1 + ## Changelog -### **WORK IN PROGRESS** -* 0.9.1 (foxthefox) first release on npm -* 0.0.1 (foxthefox) initial release +### 1.0.0 +* (foxthefox) common js module with 2 named exports Fritz and FritzEmu + +### 0.9.1 +* (foxthefox) first release on npm as ESM ## License Copyright (c) 2022 foxthefox diff --git a/example.js b/example.js new file mode 100644 index 0000000..e9dc371 --- /dev/null +++ b/example.js @@ -0,0 +1,66 @@ +//--------------- sample code ---------------- + +const Fritz = require('./index.js').Fritz; + +const fritz = new Fritz('admin', 'password', 'http://localhost:3333'); + +async function test() { + const login = await fritz.login_SID().catch((e) => { + console.log('fault calling login() ', e); + }); + console.log('login', login); + if (login) { + await fritz + .getDeviceListInfos() + .then(function(response) { + console.log('Devices' + response); + }) + .catch((e) => { + console.log('Fehler Devicelist ', e); + }); + + await fritz + .getUserPermissions() + .then(function(response) { + console.log('Rights : ' + response); + }) + .catch((e) => { + console.log('Fehler getUserPermissions', e); + }); + + await fritz + .check_SID() + .then(function(response) { + console.log('Checkresponse : ' + response); + }) + .catch((e) => { + console.log('Fehler checkSID', e); + }); + await fritz + .logout_SID() + .then(function(response) { + console.log('logout : ' + response); + }) + .catch((e) => { + console.log('Fehler logout_SID', e); + }); + } + //with relogin + await fritz + .getDeviceListInfos() + .then(function(response) { + console.log('Devices' + response); + }) + .catch((e) => { + console.log('Fehler Devicelist ', e); + }); + await fritz + .logout_SID() + .then(function(response) { + console.log('logout : ' + response); + }) + .catch((e) => { + console.log('Fehler logout_SID', e); + }); +} +test(); diff --git a/index.js b/index.js index f3ea029..1907cd5 100644 --- a/index.js +++ b/index.js @@ -1,4 +1 @@ -import Fritz from './lib/fritz_ahaapi.js'; -import FritzEmu from './lib/fritz_mockserver.js'; - -export { Fritz, FritzEmu }; +module.exports = { Fritz: require('./lib/fritz_ahaapi.js'), FritzEmu: require('./lib/fritz_mockserver.js') }; diff --git a/lib/conn_test/fritz.js b/lib/conn_test/fritz.js new file mode 100644 index 0000000..313228a --- /dev/null +++ b/lib/conn_test/fritz.js @@ -0,0 +1,111 @@ +const Fritz = require('fritzdect-aha-nodejs'); +const commandLineArgs = require('command-line-args'); +const getUsage = require('command-line-usage'); +const parser = require('xml2json-light'); + +const cmdOptionsDefinition = [ + { name: 'username', alias: 'u', type: String, description: 'username for FB login' }, + { name: 'password', alias: 'p', type: String, description: 'password of that user' }, + { + name: 'url', + type: String, + description: 'the url of the FB' + }, + { name: 'help', alias: 'h', type: Boolean } +]; + +const cmdOptions = commandLineArgs(cmdOptionsDefinition); + +if ( + cmdOptions.username === undefined || + cmdOptions.password === undefined || + cmdOptions.url === undefined || + cmdOptions.help +) { + const sections = [ + { + header: 'Fritzbox Setup Check', + content: + 'A simple app checking the Fritzbox Setup. call: node testscript.js -u admin -p password --url http:/192.168.178.1' + }, + { + header: 'Options', + optionList: cmdOptionsDefinition + } + ]; + console.log(getUsage(sections)); +} else { + var fritz = new Fritz(cmdOptions.username, cmdOptions.password, cmdOptions.url, null); + + async function test() { + console.log('\n Try to Login ...\n'); + const login = await fritz.login_SID().catch((e) => { + console.log('fault calling login() ', e); + }); + console.log('login OK? : ', login); + if (login) { + const devicelistinfos = await fritz.getDeviceListInfos(); + let devices = parser.xml2json(devicelistinfos); + // devices + devices = [].concat((devices.devicelist || {}).device || []).map((device) => { + //id │ functionbitmask │ fwversion │ manufacturer │ productname │ present │ txbusy,name + // remove spaces in AINs + //device.identifier = device.identifier.replace(/\s/g, ''); + const dev = { + identifier: device.identifier, + id: device.id, + functionbitmask: device.functionbitmask, + fwversion: device.fwversion, + manufacturer: device.manufacturer, + productname: device.productname, + present: device.present, + name: device.name + }; + return dev; + }); + console.log('\n your devices\n'); + console.table(devices); + let groups = parser.xml2json(devicelistinfos); + // devices + groups = [].concat((groups.devicelist || {}).group || []).map((device) => { + //id │ functionbitmask │ fwversion │ manufacturer │ productname │ present │ txbusy,name + // remove spaces in AINs + //device.identifier = device.identifier.replace(/\s/g, ''); + const dev = { + identifier: device.identifier, + id: device.id, + functionbitmask: device.functionbitmask, + fwversion: device.fwversion, + present: device.present, + name: device.name + }; + return dev; + }); + console.log('\n your groups\n'); + console.table(groups); + + await fritz + .check_SID() + .then(function(response) { + console.log('Check SID OK?: ' + response.session + '\n'); + console.log('Check Rights : \n'); + console.log('1 = read only; 2 = ready and write \n'); + console.table(parser.xml2json(response.rights)); + }) + .catch((e) => { + console.log('Fehler checkSID', e); + }); + await fritz + .logout_SID() + .then(function(response) { + console.log('\n logout : ' + response); + }) + .catch((e) => { + console.log('Fehler logout_SID', e); + }); + } else { + console.log('your login was not successful '); + } + } + test(); +} diff --git a/lib/conn_test/fritz.py b/lib/conn_test/fritz.py new file mode 100644 index 0000000..6fd6f13 --- /dev/null +++ b/lib/conn_test/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/lib/data/getbasicdevicestats_switch.xml b/lib/data/getbasicdevicestats_switch.xml new file mode 100644 index 0000000..25109eb --- /dev/null +++ b/lib/data/getbasicdevicestats_switch.xml @@ -0,0 +1,15 @@ + + + 215,215,220,215,215,215,215,220,215,215,215,215,215,215,215,215,215,215,215,210,215,210,210,210,210,210,210,210,210,210,210,210,210,210,210,210,210,210,215,210,210,210,215,215,215,215,215,215,215,215,215,215,220,220,220,215,220,220,220,220,215,220,220,220,220,220,215,215,220,220,215,215,215,215,215,215,215,215,215,215,215,210,210,210,210,210,215,210,210,210,210,210,210,210,205,210 + + + 233552,233552,233552,233552,233552,233552,233552,233552,233552,233552,233552,233552,234524,234524,234524,234524,234524,234524,234524,234524,234524,234524,234524,234524,234140,234140,234140,234140,234140,234140,234140,234140,234140,234140,234140,234140,234209,234209,234209,234209,234209,234209,234209,234209,234209,234209,234209,234209,233658,233658,233658,233658,233658,233658,233658,233658,233658,233658,233658,233658,234472,234472,234472,234472,234472,234472,234472,234472,234472,234472,234472,234472,237400,237400,237400,237400,237400,237400,237400,237400,237400,237400,237400,237400,237306,237306,237306,237306,237306,237306,237306,237306,237306,237306,237306,237306,236655,236655,236655,236655,236655,236655,236655,236655,236655,236655,236655,236655,236555,236555,236555,236555,236555,236555,236555,236555,236555,236555,236555,236555,237610,237610,237610,237610,237610,237610,237610,237610,237610,237610,237610,237610,237344,237344,237344,237344,237344,237344,237344,237344,237344,237344,237344,237344,237152,237152,237152,237152,237152,237152,237152,237152,237152,237152,237152,237152,237942,237942,237942,237942,237942,237942,237942,237942,237942,237942,237942,237942,238126,238126,238126,238126,238126,238126,238126,238126,238126,238126,238126,238126,238380,238380,238380,238380,238380,238380,238380,238380,238380,238380,238380,238380,238113,238113,238113,238113,238113,238113,238113,238113,238113,238113,238113,238113,237824,237824,237824,237824,237824,237824,237824,237824,237824,237824,237824,237824,237678,237678,237678,237678,237678,237678,237678,237678,237678,237678,237678,237678,237311,237311,237311,237311,237311,237311,237311,237311,237311,237311,237311,237311,237486,237486,237486,237486,237486,237486,237486,237486,237486,237486,237486,237486,237250,237250,237250,237250,237250,237250,237250,237250,237250,237250,237250,237250,237316,237316,237316,237316,237316,237316,237316,237316,237316,237316,237316,237316,237228,237228,237228,237228,237228,237228,237228,237228,237228,237228,237228,237228,237059,237059,237059,237059,237059,237059,237059,237059,237059,237059,237059,237059,237123,237123,237123,237123,237123,237123,237123,237123,237123,237123,237123,237123,237513,237513,237513,237513,237513,237513,237513,237513,237513,237513,237513,237513,237079,237079,237079,237079,237079,237079,237079,237079,237079,237079,237079,237079,237086,237086,237086,237086,237086,237086,237086,237086,237086,237086,237086,237086,237809,237809,237809,237809,237809,237809,237809,237809,237809,237809,237809,237809 + + + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + 1985,0,5643,698,2554,1581,1890,3022,1028,563,2646,3712 + 0,20,38,36,36,36,36,36,5,0,0,0,0,408,20,0,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + \ No newline at end of file diff --git a/lib/data/getbasicdevstat_comet.xml b/lib/data/getbasicdevstat_comet.xml new file mode 100644 index 0000000..0f651a2 --- /dev/null +++ b/lib/data/getbasicdevstat_comet.xml @@ -0,0 +1,5 @@ + + + 220,210,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,-,- + + \ No newline at end of file diff --git a/lib/fritz_ahaapi.js b/lib/fritz_ahaapi.js index a6a7a35..24f1bcd 100644 --- a/lib/fritz_ahaapi.js +++ b/lib/fritz_ahaapi.js @@ -14,12 +14,12 @@ * * refactored version 30.12.2021 * first version 19.12.2020 - * part of fritzdect-aha-nodejs 16.11.2022 as ES module + * */ -import { pbkdf2Sync, createHash } from 'crypto'; -import http from 'http'; -import https from 'http'; +//import crypto from 'crypto'; + +const crypto = require('crypto'); const LOGIN_SID_ROUTE = '/login_sid.lua?version=2'; const SMARTHOME_ROUTE = '/webservices/homeautoswitch.lua?0=0'; @@ -310,6 +310,15 @@ class Fritz { return Promise.reject(error); } } + // getswitchpower + async getSwitchPower(ain) { + try { + const body = this.executeCommand2('getswitchpower', ain, 1); + return Promise.resolve(body); + } catch (error) { + return Promise.reject(error); + } + } // getswitchenergy async getSwitchEnergy(ain) { try { @@ -461,7 +470,7 @@ class Fritz { async get_login_state(box_url) { if (box_url) { - let httpprot = null; + let http = null; let defaultport = null; const req_url = new URL(box_url); const hostname = req_url.hostname; @@ -469,11 +478,11 @@ class Fritz { const protocol = req_url.protocol; if (protocol == 'http:') { - httpprot = http; // vom import + http = require('http'); defaultport = 80; if (this.debug) console.log('using http'); } else if (protocol == 'https:') { - httpprot = https; // vom import + http = require('https'); defaultport = 443; //http.globalAgent.options.secureProtocol = 'SSLv3_method'; if (this.debug) console.log('using https'); @@ -493,7 +502,7 @@ class Fritz { }; let p = new Promise((resolve, reject) => { - const req = httpprot.request(options, (res) => { + const req = http.request(options, (res) => { res.setEncoding('utf8'); if (res.statusCode !== 200) { console.log(`HTTP request Failed. Status Code: ${res.statusCode}`); @@ -562,9 +571,9 @@ class Fritz { // Hash twice, once with static salt... // Once with dynamic salt. // py hash1 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt1, iter1) - const hash1 = pbkdf2Sync(password, salt1, iter1, 32, 'sha256'); + const hash1 = crypto.pbkdf2Sync(password, salt1, iter1, 32, 'sha256'); // py hash2 = hashlib.pbkdf2_hmac("sha256", hash1, salt2, iter2) - const hash2 = pbkdf2Sync(hash1, salt2, iter2, 32, 'sha256'); + const hash2 = crypto.pbkdf2Sync(hash1, salt2, iter2, 32, 'sha256'); // response salt2 + hash2 return challenge_parts[4] + '$' + hash2.toString('hex'); } @@ -578,7 +587,10 @@ class Fritz { if (this.debug) console.log('Calculate the response for a challenge using legacy MD5'); // response = challenge + "-" + password // the legacy response needs utf_16_le encoding - const md5_sum = createHash('md5').update(Buffer.from(challenge + '-' + password, 'utf16le')).digest('hex'); + const md5_sum = crypto + .createHash('md5') + .update(Buffer.from(challenge + '-' + password, 'utf16le')) + .digest('hex'); const response = challenge + '-' + md5_sum; return response; } @@ -592,7 +604,7 @@ class Fritz { async send_response(box_url, username, challenge_response) { if (box_url && username && challenge_response) { - let httpprot = null; + let http = null; let defaultport = null; const req_url = new URL(box_url); const hostname = req_url.hostname; @@ -600,11 +612,11 @@ class Fritz { const protocol = req_url.protocol; if (protocol == 'http:') { - httpprot = http; //vom import + http = require('http'); defaultport = 80; if (this.debug) console.log('using http'); } else if (protocol == 'https:') { - httpprot = https; // vom import + http = require('https'); defaultport = 443; //http.globalAgent.options.secureProtocol = 'SSLv3_method'; if (this.debug) console.log('using https'); @@ -628,7 +640,7 @@ class Fritz { }; let p = new Promise((resolve, reject) => { - const req = httpprot.request(options, (res) => { + const req = http.request(options, (res) => { res.setEncoding('utf8'); if (res.statusCode !== 200) { console.log(`HTTP request Failed. Status Code: ${res.statusCode}`); @@ -702,7 +714,7 @@ class Fritz { //console.log('execCMD sid ', sid, command); - if (sid) path += 'sid=' + sid; + if (sid) path += '&sid=' + sid; if (command) path += '&logout=' + command; // console.log('valid SID ' + path); // path includes the whole command string including SID @@ -767,7 +779,7 @@ class Fritz { */ async fritzAHA_Request(path, box_url, addOptions) { if (box_url && path) { - let httpprot = null; + let http = null; let defaultport = null; const req_url = new URL(box_url); const hostname = req_url.hostname; @@ -775,11 +787,11 @@ class Fritz { const protocol = req_url.protocol; if (protocol == 'http:') { - httpprot = http; //vom import + http = require('http'); defaultport = 80; if (this.debug) console.log('using http'); } else if (protocol == 'https:') { - httpprot = https; // vom import + http = require('https'); defaultport = 443; //http.globalAgent.options.secureProtocol = 'SSLv3_method'; if (this.debug) console.log('using https'); @@ -799,7 +811,7 @@ class Fritz { Object.assign(options, addOptions); let p = new Promise((resolve, reject) => { - const req = httpprot.request(options, (res) => { + const req = http.request(options, (res) => { res.setEncoding('utf8'); if (res.statusCode !== 200) { // throw Error(`HTTP request Failed. Status Code: ${res.statusCode}`); @@ -871,7 +883,7 @@ class Fritz { } if (sessionID === '0000000000000000') { return Promise.reject({ - msg: 'error calling executeCommand', + msg: 'error calling executeCommand, could not check SID', function: 'check_SID', error: sessionID }); @@ -1011,71 +1023,4 @@ class Fritz { } } -export default Fritz; - -//--------------- sample code ---------------- -/* -const Fritz = require('../fritzhttp.js'); -var fritz = new Fritz('admin', 'password', 'http://localhost.3333'); - -async function test() { - const login = await fritz.login_SID().catch((e) => { - console.log('fault calling login() ', e); - }); - console.log('login', login); - if (login) { - await fritz - .getDeviceListInfos() - .then(function(response) { - console.log('Devices' + response); - }) - .catch((e) => { - console.log('Fehler Devicelist ', e); - }); - - await fritz - .getUserPermissions() - .then(function(response) { - console.log('Rights : ' + response); - }) - .catch((e) => { - console.log('Fehler getUserPermissions', e); - }); - - await fritz - .check_SID() - .then(function(response) { - console.log('Checkresponse : ' + response); - }) - .catch((e) => { - console.log('Fehler checkSID', e); - }); - await fritz - .logout_SID() - .then(function(response) { - console.log('logout : ' + response); - }) - .catch((e) => { - console.log('Fehler logout_SID', e); - }); - } - //with relogin - await fritz - .getDeviceListInfos() - .then(function(response) { - console.log('Devices' + response); - }) - .catch((e) => { - console.log('Fehler Devicelist ', e); - }); - await fritz - .logout_SID() - .then(function(response) { - console.log('logout : ' + response); - }) - .catch((e) => { - console.log('Fehler logout_SID', e); - }); -} -test(); -*/ +module.exports = Fritz; diff --git a/lib/fritz_mockserver.js b/lib/fritz_mockserver.js index d8364db..cbe5cb8 100644 --- a/lib/fritz_mockserver.js +++ b/lib/fritz_mockserver.js @@ -1,50 +1,37 @@ // @ts-nocheck //server to emulate the fritzbox responses -import { createServer } from 'http'; -import { readFileSync } from 'fs'; -import { parse } from 'querystring'; -import { xml2json } from 'xml2json-light'; -import figlet from 'figlet'; -import chalk from 'chalk'; +const http = require('http'); +const fs = require('fs'); +const { parse } = require('querystring'); +const parser = require('xml2json-light'); +const figlet = require('figlet'); +const chalk = require('chalk'); +const crypto = require('crypto'); -import { pbkdf2Sync, createHash } from 'crypto'; +const path = require('path'); +console.log('PATH ist ' + path.join(__dirname, './data/')); -import { join } from 'path'; - -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -console.log('PATH ist ' + join(__dirname, './data/')); - -const xmlDevicesGroups = readFileSync(join(__dirname, './data/') + 'test_api_response.xml'); +const xmlDevicesGroups = fs.readFileSync(path.join(__dirname, './data/') + 'test_api_response.xml'); //var xmlDevicesGroups = fs.readFileSync('./test.xml'); +const xmlTemplate = fs.readFileSync(path.join(__dirname, './data/') + 'template_answer.xml'); +const xmlTempStat = fs.readFileSync(path.join(__dirname, './data/') + 'devicestat_temp_answer.xml'); +const xmlPowerStats = fs.readFileSync(path.join(__dirname, './data/') + 'devicestat_power_answer.xml'); +const xmlColorDefaults = fs.readFileSync(path.join(__dirname, './data/') + 'color_defaults.xml'); +const hkr_batt = fs.readFileSync(path.join(__dirname, './data/') + 'hkr_response.xml'); +const guestWlan = fs.readFileSync(path.join(__dirname, './data/') + 'guest_wlan_form.xml'); -const xmlTemplate = readFileSync(join(__dirname, './data/') + 'template_answer.xml'); - -const xmlTempStat = readFileSync(join(__dirname, './data/') + 'devicestat_temp_answer.xml'); - -const xmlPowerStats = readFileSync(join(__dirname, './data/') + 'devicestat_power_answer.xml'); - -const xmlColorDefaults = readFileSync(join(__dirname, './data/') + 'color_defaults.xml'); - -const hkr_batt = readFileSync(join(__dirname, './data/') + 'hkr_response.xml'); - -const guestWlan = readFileSync(join(__dirname, './data/') + 'guest_wlan_form.xml'); - -let server; - +//hashing stuff const challenge = (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8); const challenge2 = (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8); const password = 'password'; const challengeResponse = - challenge + '-' + createHash('md5').update(Buffer.from(challenge + '-' + password, 'utf16le')).digest('hex'); + challenge + '-' + crypto.createHash('md5').update(Buffer.from(challenge + '-' + password, 'utf16le')).digest('hex'); const mocksid = (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8) + (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8); -const devices2json = xml2json(String(xmlDevicesGroups)); +//Devices and Groups derved from xmlDevicesGroups +const devices2json = parser.xml2json(String(xmlDevicesGroups)); let devices = [].concat((devices2json.devicelist || {}).device || []).map((device) => { // remove spaces in AINs device.identifier = device.identifier.replace(/\s/g, ''); @@ -55,19 +42,23 @@ let groups = [].concat((devices2json.devicelist || {}).group || []).map((group) group.identifier = group.identifier.replace(/\s/g, ''); return group; }); -const templates2json = xml2json(String(xmlTemplate)); +//Templates derived from xmlTemplate +const templates2json = parser.xml2json(String(xmlTemplate)); let templates = [].concat((templates2json.templatelist || {}).template || []).map(function(template) { // remove spaces in AINs // template.identifier = group.identifier.replace(/\s/g, ''); return template; }); -let result = templates; //apiresponse is the xml file with AINs not having the spaces inside +//used in the response var apiresponse = {}; apiresponse['devicelist'] = { version: '1', device: devices, group: groups }; apiresponse['templatelist'] = { version: '1', template: templates }; -console.log(apiresponse); +//console.log(apiresponse); + +// Functions for reply on the requests + function loginoutAnswerV2(response, sid, method, username, userresponse, request) { if (!sid && method == 'GET') { response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); @@ -180,28 +171,8 @@ function loginoutAnswerV1(response, sid, method, username, userresponse) { } } -function findAin(type, ain) { - let position = null; - if (type === 'device') { - for (let i = 0; i < apiresponse['devicelist']['device'].length; i++) { - if (apiresponse['devicelist']['device'][i].identifier === ain) { - position = i; - break; - } - } - } else if (type === 'group') { - for (let i = 0; i < apiresponse['devicelist']['device'].length; i++) { - if (apiresponse['devicelist']['device'][i].identifier === ain) { - position = i; - break; - } - } - } - return position; -} - -function errorAnswer(response) { - response.statusCode = 403; +function errorAnswer(response, code) { + response.statusCode = code; response.end(); return response; } @@ -237,7 +208,7 @@ function homeautoswitchAnswer( response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -260,7 +231,7 @@ function homeautoswitchAnswer( response.end(); } else { console.log(' did not find the ain in templates ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -276,7 +247,7 @@ function homeautoswitchAnswer( .map((device) => device.identifier) ); response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify(switchlist)); + response.write(String(switchlist)); response.end(); return response; break; @@ -290,7 +261,7 @@ function homeautoswitchAnswer( .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) .map((group) => group.switch.state); if (setswitchstate) { - const pos = this.findAin('device', ain); + const pos = findAin('device', ain); if ((switchcmd = 'setswitchon')) { apiresponse.devicelist.device[pos].switch.state = 1; response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); @@ -308,7 +279,7 @@ function homeautoswitchAnswer( response.end(); } } else if (setgroupstate) { - const pos = this.findAin('group', ain); + const pos = findAin('group', ain); if ((switchcmd = 'setswitchon')) { apiresponse.devicelist.group[pos].switch.state = 0; response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); @@ -327,7 +298,7 @@ function homeautoswitchAnswer( } } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -340,15 +311,15 @@ function homeautoswitchAnswer( .map((group) => group.temperature.celsius); if (gettemp) { response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + gettemp + "'" ])); + response.write(String(gettemp)); response.end(); } else if (getgrouptemp) { response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + getgrouptemp + "'" ])); + response.write(String(getgrouptemp)); response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -371,7 +342,7 @@ function homeautoswitchAnswer( response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -392,7 +363,7 @@ function homeautoswitchAnswer( response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -405,17 +376,20 @@ function homeautoswitchAnswer( const getgroupmeter = apiresponse['devicelist']['group'] .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) .map((group) => group.powermeter[item2]); - if (getswitchmeter) { + if (getswitchmeter.length > 0) { response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + getswitchmeter + "'" ])); + //response.write(JSON.stringify([ "'" + getswitchmeter + "'" ])); + response.write(String(getswitchmeter)); response.end(); - } else if (getgroupmeter) { + } else if (getgroupmeter.length > 0) { response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + getgroupmeter + "'" ])); + //response.write(JSON.stringify([ "'" + getgroupmeter + "'" ])); + response.write(String(getgroupmeter)); response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + //verursacht StatusCode400 + response = errorAnswer(response, 400); } return response; break; @@ -432,15 +406,15 @@ function homeautoswitchAnswer( .map((group) => group.hkr[item3]); if (gethkrtemp) { response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + gethkrtemp + "'" ])); + response.write(String(gethkrtemp)); response.end(); } else if (getgrouphkrtemp) { response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + getgrouphkrtemp + "'" ])); + response.write(String(getgrouphkrtemp)); response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -453,18 +427,18 @@ function homeautoswitchAnswer( .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) .map((group) => group.hkr.tsoll); if (sethkrtemp) { - const pos = this.findAin('device', ain); + const pos = findAin('device', ain); apiresponse.devicelist.device[pos].hkr.tsoll = param; response.statusCode = 200; response.end(); } else if (setgrouphkrtemp) { - const pos = this.findAin('group', ain); + const pos = findAin('group', ain); apiresponse.devicelist.group[pos].hkr.tsoll = param; response.statusCode = 200; response.end(); } else { console.log('-> did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -491,12 +465,11 @@ function homeautoswitchAnswer( } else if (switcher || groupswitcher) { //check the URL of the current request response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(String(xmlTempStat)); response.write(String(xmlPowerStats)); response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -517,7 +490,7 @@ function homeautoswitchAnswer( //ohne prüfung ob auch 0 oder 1 geschickt wird newstate = onoff; } - const pos = this.findAin('device', ain); + const pos = findAin('device', ain); apiresponse.devicelist.device[pos].simpleonoff.state = newstate; response.statusCode = 200; response.end(); @@ -528,7 +501,7 @@ function homeautoswitchAnswer( //ohne prüfung ob auch 0 oder 1 geschickt wird newstate = onoff; } - const pos = this.findAin('group', ain); + const pos = findAin('group', ain); apiresponse.devicelist.group[pos].simpleonoff.state = newstate; response.statusCode = 200; response.end(); @@ -547,13 +520,13 @@ function homeautoswitchAnswer( .filter((group) => group.hasOwnProperty('levelcontrol') && group.identifier === ain) .map((group) => group.levelcontrol.level); if (levelvalue) { - const pos = this.findAin('device', ain); + const pos = findAin('device', ain); apiresponse.devicelist.device[pos].levelcontrol.level = level; apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = Math.floor(Number(level) / 255 * 100); response.statusCode = 200; response.end(); } else if (grouplevel) { - const pos = this.findAin('group', ain); + const pos = findAin('group', ain); apiresponse.devicelist.group[pos].levelcontrol.level = level; apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = Math.floor(Number(level) / 255 * 100); response.statusCode = 200; @@ -573,20 +546,20 @@ function homeautoswitchAnswer( .filter((group) => group.hasOwnProperty('levelcontrol') && group.identifier === ain) .map((group) => group.levelcontrol.levelpercentage); if (levelvalueperc) { - const pos = this.findAin('device', ain); + const pos = findAin('device', ain); apiresponse.devicelist.device[pos].levelcontrol.level = Math.floor(Number(level) / 100 * 255); apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = level; response.statusCode = 200; response.end(); } else if (grouplevelperc) { - const pos = this.findAin('group', ain); + const pos = findAin('group', ain); apiresponse.devicelist.group[pos].levelcontrol.level = Math.floor(Number(level) / 100 * 255); apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = level; response.statusCode = 200; response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -603,18 +576,18 @@ function homeautoswitchAnswer( .map((group) => group.colorcontrol[cmd]); const newvalue = hue || saturation; if (colorvalue) { - const pos = this.findAin('device', ain); + const pos = findAin('device', ain); apiresponse.devicelist.device[pos].colorcontrol[cmd] = newvalue; response.statusCode = 200; response.end(); } else if (groupcolor) { - const pos = this.findAin('group', ain); + const pos = findAin('group', ain); apiresponse.devicelist.group[pos].colorcontrol[cmd] = newvalue; response.statusCode = 200; response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -627,18 +600,18 @@ function homeautoswitchAnswer( .filter((group) => group.hasOwnProperty('colorcontrol') && group.identifier === ain) .map((group) => group.colorcontrol.temperature); if (settempvalue) { - const pos = this.findAin('device', ain); + const pos = findAin('device', ain); apiresponse.devicelist.device[pos].colorcontrol.temperature = temperature; response.statusCode = 200; response.end(); } else if (setgrouptemp) { - const pos = this.findAin('group', ain); + const pos = findAin('group', ain); apiresponse.devicelist.group[pos].colorcontrol.temperature = temperature; response.statusCode = 200; response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -661,20 +634,20 @@ function homeautoswitchAnswer( endtimepstamp = ''; } if (hkrboost) { - const pos = this.findAin('device', ain); + const pos = findAin('device', ain); apiresponse.devicelist.device[pos].hkr.boostactiveendtime = endtimestamp; response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.write(String(endtimestamp)); response.end(); } else if (groupboost) { - const pos = this.findAin('group', ain); + const pos = findAin('group', ain); apiresponse.devicelist.group[pos].hkr.boostactiveendtime = endtimestamp; response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.write(String(endtimestamp)); response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -690,20 +663,20 @@ function homeautoswitchAnswer( endtimepstamp = ''; } if (hkrwindow) { - const pos = this.findAin('device', ain); + const pos = findAin('device', ain); apiresponse.devicelist.device[pos].hkr.windowopenactiveendtime = endtimestamp; response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.write(String(endtimestamp)); response.end(); } else if (groupwindow) { - const pos = this.findAin('group', ain); + const pos = findAin('group', ain); apiresponse.devicelist.group[pos].hkr.windowopenactiveendtime = endtimestamp; response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); - response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.write(String(endtimestamp)); response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -723,7 +696,7 @@ function homeautoswitchAnswer( response.end(); } else { console.log(' did not find the ain in devices/groups ' + ain); - response = errorAnswer(response); + response = errorAnswer(response, 400); } return response; break; @@ -738,11 +711,34 @@ function homeautoswitchAnswer( break; default: console.log('switchcmd no case found ' + switchcmd); - response = errorAnswer(response); + response = errorAnswer(response, 400); return response; break; } } +// helper function +function findAin(type, ain) { + let position = null; + if (type === 'device') { + for (let i = 0; i < apiresponse['devicelist']['device'].length; i++) { + if (apiresponse['devicelist']['device'][i].identifier === ain) { + position = i; + break; + } + } + } else if (type === 'group') { + for (let i = 0; i < apiresponse['devicelist']['device'].length; i++) { + if (apiresponse['devicelist']['device'][i].identifier === ain) { + position = i; + break; + } + } + } + return position; +} + +// the emulation class +let server; class FritzEmu { constructor(testfile, port, debugmode) { this.testfile = testfile; @@ -765,7 +761,7 @@ class FritzEmu { ); //We need a which handles requests and send response //Create a server - server = createServer(this.handleHttpRequest); + server = http.createServer(this.handleHttpRequest); //Lets start our server server.listen(3333, function() { //Callback triggered when server is successfully listening. Hurray! @@ -814,7 +810,7 @@ class FritzEmu { break; case 'ain': ain = commandsplit[1]; - console.log('ain : ', commandsplit[1]); + console.log('-> ain : ', commandsplit[1]); break; case 'ain': onoff = commandsplit[1]; @@ -952,9 +948,8 @@ class FritzEmu { } } } -//setupHttpServer(function() {}); -export default FritzEmu; +module.exports = FritzEmu; // ausprobieren bei echter FB ob getswitchname, getswitchpresent, gettemperature auch auf thermostat geht // gettemperature hat 0.1 diff --git a/lib/old/fritzdectaha.js b/lib/mjs_version/fritz_ahaapi.mjs similarity index 97% rename from lib/old/fritzdectaha.js rename to lib/mjs_version/fritz_ahaapi.mjs index 2d84a7b..a6a7a35 100644 --- a/lib/old/fritzdectaha.js +++ b/lib/mjs_version/fritz_ahaapi.mjs @@ -14,12 +14,12 @@ * * refactored version 30.12.2021 * first version 19.12.2020 - * + * part of fritzdect-aha-nodejs 16.11.2022 as ES module */ -//import crypto from 'crypto'; - -const crypto = require('crypto'); +import { pbkdf2Sync, createHash } from 'crypto'; +import http from 'http'; +import https from 'http'; const LOGIN_SID_ROUTE = '/login_sid.lua?version=2'; const SMARTHOME_ROUTE = '/webservices/homeautoswitch.lua?0=0'; @@ -461,7 +461,7 @@ class Fritz { async get_login_state(box_url) { if (box_url) { - let http = null; + let httpprot = null; let defaultport = null; const req_url = new URL(box_url); const hostname = req_url.hostname; @@ -469,11 +469,11 @@ class Fritz { const protocol = req_url.protocol; if (protocol == 'http:') { - http = require('http'); + httpprot = http; // vom import defaultport = 80; if (this.debug) console.log('using http'); } else if (protocol == 'https:') { - http = require('https'); + httpprot = https; // vom import defaultport = 443; //http.globalAgent.options.secureProtocol = 'SSLv3_method'; if (this.debug) console.log('using https'); @@ -493,7 +493,7 @@ class Fritz { }; let p = new Promise((resolve, reject) => { - const req = http.request(options, (res) => { + const req = httpprot.request(options, (res) => { res.setEncoding('utf8'); if (res.statusCode !== 200) { console.log(`HTTP request Failed. Status Code: ${res.statusCode}`); @@ -562,9 +562,9 @@ class Fritz { // Hash twice, once with static salt... // Once with dynamic salt. // py hash1 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt1, iter1) - const hash1 = crypto.pbkdf2Sync(password, salt1, iter1, 32, 'sha256'); + const hash1 = pbkdf2Sync(password, salt1, iter1, 32, 'sha256'); // py hash2 = hashlib.pbkdf2_hmac("sha256", hash1, salt2, iter2) - const hash2 = crypto.pbkdf2Sync(hash1, salt2, iter2, 32, 'sha256'); + const hash2 = pbkdf2Sync(hash1, salt2, iter2, 32, 'sha256'); // response salt2 + hash2 return challenge_parts[4] + '$' + hash2.toString('hex'); } @@ -578,10 +578,7 @@ class Fritz { if (this.debug) console.log('Calculate the response for a challenge using legacy MD5'); // response = challenge + "-" + password // the legacy response needs utf_16_le encoding - const md5_sum = crypto - .createHash('md5') - .update(Buffer.from(challenge + '-' + password, 'utf16le')) - .digest('hex'); + const md5_sum = createHash('md5').update(Buffer.from(challenge + '-' + password, 'utf16le')).digest('hex'); const response = challenge + '-' + md5_sum; return response; } @@ -595,7 +592,7 @@ class Fritz { async send_response(box_url, username, challenge_response) { if (box_url && username && challenge_response) { - let http = null; + let httpprot = null; let defaultport = null; const req_url = new URL(box_url); const hostname = req_url.hostname; @@ -603,11 +600,11 @@ class Fritz { const protocol = req_url.protocol; if (protocol == 'http:') { - http = require('http'); + httpprot = http; //vom import defaultport = 80; if (this.debug) console.log('using http'); } else if (protocol == 'https:') { - http = require('https'); + httpprot = https; // vom import defaultport = 443; //http.globalAgent.options.secureProtocol = 'SSLv3_method'; if (this.debug) console.log('using https'); @@ -631,7 +628,7 @@ class Fritz { }; let p = new Promise((resolve, reject) => { - const req = http.request(options, (res) => { + const req = httpprot.request(options, (res) => { res.setEncoding('utf8'); if (res.statusCode !== 200) { console.log(`HTTP request Failed. Status Code: ${res.statusCode}`); @@ -770,7 +767,7 @@ class Fritz { */ async fritzAHA_Request(path, box_url, addOptions) { if (box_url && path) { - let http = null; + let httpprot = null; let defaultport = null; const req_url = new URL(box_url); const hostname = req_url.hostname; @@ -778,11 +775,11 @@ class Fritz { const protocol = req_url.protocol; if (protocol == 'http:') { - http = require('http'); + httpprot = http; //vom import defaultport = 80; if (this.debug) console.log('using http'); } else if (protocol == 'https:') { - http = require('https'); + httpprot = https; // vom import defaultport = 443; //http.globalAgent.options.secureProtocol = 'SSLv3_method'; if (this.debug) console.log('using https'); @@ -802,7 +799,7 @@ class Fritz { Object.assign(options, addOptions); let p = new Promise((resolve, reject) => { - const req = http.request(options, (res) => { + const req = httpprot.request(options, (res) => { res.setEncoding('utf8'); if (res.statusCode !== 200) { // throw Error(`HTTP request Failed. Status Code: ${res.statusCode}`); @@ -1014,7 +1011,7 @@ class Fritz { } } -module.exports = Fritz; +export default Fritz; //--------------- sample code ---------------- /* diff --git a/lib/mjs_version/fritz_mockserver.mjs b/lib/mjs_version/fritz_mockserver.mjs new file mode 100644 index 0000000..6474609 --- /dev/null +++ b/lib/mjs_version/fritz_mockserver.mjs @@ -0,0 +1,962 @@ +// @ts-nocheck +//server to emulate the fritzbox responses +import { createServer } from 'http'; +import { readFileSync } from 'fs'; +import { parse } from 'querystring'; +import { xml2json } from 'xml2json-light'; +import figlet from 'figlet'; +import chalk from 'chalk'; + +import { pbkdf2Sync, createHash } from 'crypto'; + +import { join } from 'path'; + +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +console.log('PATH ist ' + join(__dirname, './data/')); + +const xmlDevicesGroups = readFileSync(join(__dirname, './data/') + 'test_api_response.xml'); +//var xmlDevicesGroups = fs.readFileSync('./test.xml'); + +const xmlTemplate = readFileSync(join(__dirname, './data/') + 'template_answer.xml'); + +const xmlTempStat = readFileSync(join(__dirname, './data/') + 'devicestat_temp_answer.xml'); + +const xmlPowerStats = readFileSync(join(__dirname, './data/') + 'devicestat_power_answer.xml'); + +const xmlColorDefaults = readFileSync(join(__dirname, './data/') + 'color_defaults.xml'); + +const hkr_batt = readFileSync(join(__dirname, './data/') + 'hkr_response.xml'); + +const guestWlan = readFileSync(join(__dirname, './data/') + 'guest_wlan_form.xml'); + +let server; + +const challenge = (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8); +const challenge2 = (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8); +const password = 'password'; +const challengeResponse = + challenge + '-' + createHash('md5').update(Buffer.from(challenge + '-' + password, 'utf16le')).digest('hex'); +const mocksid = + (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8) + + (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8); + +const devices2json = xml2json(String(xmlDevicesGroups)); +let devices = [].concat((devices2json.devicelist || {}).device || []).map((device) => { + // remove spaces in AINs + device.identifier = device.identifier.replace(/\s/g, ''); + return device; +}); +let groups = [].concat((devices2json.devicelist || {}).group || []).map((group) => { + // remove spaces in AINs + group.identifier = group.identifier.replace(/\s/g, ''); + return group; +}); +const templates2json = xml2json(String(xmlTemplate)); +let templates = [].concat((templates2json.templatelist || {}).template || []).map(function(template) { + // remove spaces in AINs + // template.identifier = group.identifier.replace(/\s/g, ''); + return template; +}); +let result = templates; + +//apiresponse is the xml file with AINs not having the spaces inside +var apiresponse = {}; +apiresponse['devicelist'] = { version: '1', device: devices, group: groups }; +apiresponse['templatelist'] = { version: '1', template: templates }; +console.log(apiresponse); +function loginoutAnswerV2(response, sid, method, username, userresponse, request) { + if (!sid && method == 'GET') { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge + + '0' + ); + response.end(); + return response; + } else if (!sid && method == 'POST') { + let body = ''; + request.on('data', (chunk) => { + body += chunk.toString(); // convert Buffer to string + }); + request.on('end', () => { + const form = parse(body); + console.log('user: ' + form.username); + console.log('pbkf2 response: ' + form.response); + // pbkf2 ausrechnen und vergleichen + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '' + + mocksid + + '' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2' + ); + response.end(); + return response; + }); + } else if (sid && method == 'GET') { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '' + + mocksid + + '' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2admin' + ); + response.end(); + return response; + } else if (sid && logout === 1 && method == 'GET') { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2admin' + ); + response.end(); + return response; + } +} +function loginoutAnswerV1(response, sid, method, username, userresponse) { + if (!sid && method == 'GET') { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge + + '0' + ); + response.end(); + return response; + } else if (!sid && username === 'admin') { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge + + '0' + ); + response.end(); + return response; + } else if (!sid && username === 'admin' && userresponse == challengeResponse) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '' + + mocksid + + '' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2' + ); + response.end(); + return response; + } else if (sid) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '' + + mocksid + + '' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2admin' + ); + response.end(); + return response; + } else if (sid == mocksid && logout == 1) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2admin' + ); + response.end(); + return response; + } +} + +function findAin(type, ain) { + let position = null; + if (type === 'device') { + for (let i = 0; i < apiresponse['devicelist']['device'].length; i++) { + if (apiresponse['devicelist']['device'][i].identifier === ain) { + position = i; + break; + } + } + } else if (type === 'group') { + for (let i = 0; i < apiresponse['devicelist']['device'].length; i++) { + if (apiresponse['devicelist']['device'][i].identifier === ain) { + position = i; + break; + } + } + } + return position; +} + +function errorAnswer(response) { + response.statusCode = 403; + response.end(); + return response; +} + +function homeautoswitchAnswer( + response, + switchcmd, + ain, + onoff, + param, + endtimestamp, + colorcmd, + hue, + saturation, + temperature, + duration, + target +) { + switch (switchcmd) { + case 'getdevicelistinfos': + const devicelistinfos = apiresponse['devicelist']['device'].concat(apiresponse['devicelist']['group']); + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + //response.write(String(devicelistinfos)); + response.write(String(xmlDevicesGroups)); + response.end(); + return response; + break; + case 'getdeviceinfos': + const deviceinfos = apiresponse['devicelist']['device'].filter((device) => device.identifier === ain); + if (deviceinfos) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(deviceinfos)); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'gettemplatelistinfos': + // todo empty templates + //const templatelistinfos = apiresponse['templatelist']['template'] + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + //response.write(String(templatelistinfo)); + response.write(String(xmlTemplate)); + response.end(); + return response; + break; + case 'applytemplate': + const template = apiresponse['templatelist']['template'].filter( + (template) => template.hasOwnProperty('identifier') && template.identifier === ain + ); + if (template) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(template[0].id); + response.end(); + } else { + console.log(' did not find the ain in templates ' + ain); + response = errorAnswer(response); + } + return response; + break; + //alles zu switches + case 'getswitchlist': + //check the URL of the current request + const switchlist = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch')) + .map((device) => device.identifier) + .concat( + apiresponse['devicelist']['group'] + .filter((device) => device.hasOwnProperty('switch')) + .map((device) => device.identifier) + ); + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify(switchlist)); + response.end(); + return response; + break; + case 'setswitchon': + case 'setswitchoff': + case 'setswitchtoggle': + const setswitchstate = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch') && device.identifier === ain) + .map((device) => device.switch.state); + const setgroupstate = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) + .map((group) => group.switch.state); + if (setswitchstate) { + const pos = findAin('device', ain); + if ((switchcmd = 'setswitchon')) { + apiresponse.devicelist.device[pos].switch.state = 1; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ '1' ])); + response.end(); + } else if ((switchcmd = 'setswitchoff')) { + apiresponse.devicelist.device[pos].switch.state = 0; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ '0' ])); + response.end(); + } else if ((switchcmd = 'setswitchtoggle')) { + apiresponse.devicelist.device[pos].switch.state = !setswitchstate; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + !setswitchstate + "'" ])); + response.end(); + } + } else if (setgroupstate) { + const pos = findAin('group', ain); + if ((switchcmd = 'setswitchon')) { + apiresponse.devicelist.group[pos].switch.state = 0; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ '1' ])); + response.end(); + } else if ((switchcmd = 'setswitchoff')) { + apiresponse.devicelist.group[pos].switch.state = 1; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ '0' ])); + response.end(); + } else if ((switchcmd = 'setswitchtoggle')) { + apiresponse.devicelist.group[pos].switch.state = !setgroupstate; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + !setgroupstate + "'" ])); + response.end(); + } + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'gettemperature': + const gettemp = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('temperature') && device.identifier === ain) + .map((device) => device.temperature.celsius); + const getgrouptemp = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('temperature') && device.identifier === ain) + .map((group) => group.temperature.celsius); + if (gettemp) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + gettemp + "'" ])); + response.end(); + } else if (getgrouptemp) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getgrouptemp + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'getswitchname': + case 'getswitchpresent': + const item = switchcmd.replace('getswitch', ''); + const switchvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch') && device.identifier === ain) + .map((device) => device[item]); + const groupvalue = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) + .map((group) => group[item]); + if (switchvalue) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + switchvalue + "'" ])); + response.end(); + } else if (groupvalue) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + groupvalue + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'getswitchstate': + const getswitchstate = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch') && device.identifier === ain) + .map((device) => device.switch.state); + const getgroupstate = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) + .map((group) => group.switch.state); + if (getswitchstate) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getswitchstate + "'" ])); + response.end(); + } else if (groupstate) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getgroupstate + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'getswitchpower': + case 'getswitchenergy': + const item2 = switchcmd.replace('getswitch', ''); + const getswitchmeter = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch') && device.identifier === ain) + .map((device) => device.powermeter[item2]); + const getgroupmeter = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) + .map((group) => group.powermeter[item2]); + if (getswitchmeter) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getswitchmeter + "'" ])); + response.end(); + } else if (getgroupmeter) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getgroupmeter + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + //alles KHR + case 'gethkrtsoll': + case 'gethkrkomfort': + case 'gethkrabsenk': + const item3 = switchcmd.replace('gethkr', ''); + const gethkrtemp = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr[item3]); + const getgrouphkrtemp = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr[item3]); + if (gethkrtemp) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + gethkrtemp + "'" ])); + response.end(); + } else if (getgrouphkrtemp) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getgrouphkrtemp + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'sethkrtsoll': + console.log('tsoll = ' + param); + const sethkrtemp = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.tsoll); + const setgrouphkrtemp = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.tsoll); + if (sethkrtemp) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].hkr.tsoll = param; + response.statusCode = 200; + response.end(); + } else if (setgrouphkrtemp) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].hkr.tsoll = param; + response.statusCode = 200; + response.end(); + } else { + console.log('-> did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + //different + case 'getbasicdevicestats': + // todo prüfen ob gruppen statistik haben können + const thermostat = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.tsoll); + const groupthermo = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.tsoll); + const switcher = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.tsoll); + const groupswitcher = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.tsoll); + if (thermostat || groupthermo) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(xmlTempStat)); + response.end(); + } else if (switcher || groupswitcher) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(xmlTempStat)); + response.write(String(xmlPowerStats)); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setsimpleonoff': + console.log('simple status ' + onoff); + const simplevalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('simpleonoff') && device.identifier === ain) + .map((device) => device.simpleonoff.state); + const groupsimplevalue = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('simpleonoff') && group.identifier === ain) + .map((group) => group.simpleonoff.state); + let newstate = null; + + if (simplevalue) { + if (onoff == 2) { + newstate = !simplevalue; + } else { + //ohne prüfung ob auch 0 oder 1 geschickt wird + newstate = onoff; + } + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].simpleonoff.state = newstate; + response.statusCode = 200; + response.end(); + } else if (groupsimplevalue) { + if (onoff == 2) { + newstate = !groupsimplevalue; + } else { + //ohne prüfung ob auch 0 oder 1 geschickt wird + newstate = onoff; + } + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].simpleonoff.state = newstate; + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setlevel': + console.log('level ' + level); + const levelvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('levelcontrol') && device.identifier === ain) + .map((device) => device.levelcontrol.level); + const grouplevel = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('levelcontrol') && group.identifier === ain) + .map((group) => group.levelcontrol.level); + if (levelvalue) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].levelcontrol.level = level; + apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = Math.floor(Number(level) / 255 * 100); + response.statusCode = 200; + response.end(); + } else if (grouplevel) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].levelcontrol.level = level; + apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = Math.floor(Number(level) / 255 * 100); + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setlevelpercentage': + console.log('level ' + level); + const levelvalueperc = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('levelcontrol') && device.identifier === ain) + .map((device) => device.levelcontrol.levelpercentage); + const grouplevelperc = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('levelcontrol') && group.identifier === ain) + .map((group) => group.levelcontrol.levelpercentage); + if (levelvalueperc) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].levelcontrol.level = Math.floor(Number(level) / 100 * 255); + apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = level; + response.statusCode = 200; + response.end(); + } else if (grouplevelperc) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].levelcontrol.level = Math.floor(Number(level) / 100 * 255); + apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = level; + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setcolor': + console.log('colorcmd ' + colorcmd); + console.log('hue ' + hue); + console.log('saturation ' + saturation); + console.log('additional cmd duration' + duration); + const colorvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('colorcontrol') && device.identifier === ain) + .map((device) => device.colorontrol[cmd]); + const groupcolor = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('colorcontrol') && group.identifier === ain) + .map((group) => group.colorcontrol[cmd]); + const newvalue = hue || saturation; + if (colorvalue) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].colorcontrol[cmd] = newvalue; + response.statusCode = 200; + response.end(); + } else if (groupcolor) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].colorcontrol[cmd] = newvalue; + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setcolortemperature': + console.log('temperature ' + temperature); + const settempvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('colorcontrol') && device.identifier === ain) + .map((device) => device.colorontrol.temperature); + const setgrouptemp = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('colorcontrol') && group.identifier === ain) + .map((group) => group.colorcontrol.temperature); + if (settempvalue) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].colorcontrol.temperature = temperature; + response.statusCode = 200; + response.end(); + } else if (setgrouptemp) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].colorcontrol.temperature = temperature; + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'getcolordefaults': + //wird unabhängig von Lampen geschickt + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(xmlColorDefaults)); + response.end(); + return response; + break; + case 'sethkrboost': + console.log('endtimestamp ' + endtimestamp); + const hkrboost = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.boostactiveendtime); + const groupboost = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.boostactiveendtime); + if (endttimestamp > now) { + endtimepstamp = ''; + } + if (hkrboost) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].hkr.boostactiveendtime = endtimestamp; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.end(); + } else if (groupboost) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].hkr.boostactiveendtime = endtimestamp; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'sethkrwindowopen': + console.log('endtimestamp ' + endtimestamp); + const hkrwindow = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.windowopenactiveendtime); + const groupwindow = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.windowopenactiveendtime); + if (endttimestamp > now) { + endtimepstamp = ''; + } + if (hkrwindow) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].hkr.windowopenactiveendtime = endtimestamp; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.end(); + } else if (groupwindow) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].hkr.windowopenactiveendtime = endtimestamp; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setblind': + console.log('target ' + target); + const blindvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('blind') && device.identifier === ain) + .map((device) => device.blind.mode); + const groupblind = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('blind') && group.identifier === ain) + .map((group) => group.blind.mode); + if (blindvalue) { + response.statusCode = 200; + response.end(); + } else if (groupblind) { + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setname': + response.statusCode = 200; + response.end(); + return response; + break; + case 'startulesubscription': + break; + case 'getsubscriptionstate': + break; + default: + console.log('switchcmd no case found ' + switchcmd); + response = errorAnswer(response); + return response; + break; + } +} +class FritzEmu { + constructor(testfile, port, debugmode) { + this.testfile = testfile; + this.emuport = port; + this.debugmode = debugmode; + this.deviceresponse = null; + } + + setupHttpServer(callback) { + console.log( + chalk.yellow( + figlet.textSync('FB AHA Emulation', { + font: 'Standard', + horizontalLayout: 'default', + verticalLayout: 'default', + width: 80, + whitespaceBreak: true + }) + ) + ); + //We need a which handles requests and send response + //Create a server + server = createServer(this.handleHttpRequest); + //Lets start our server + server.listen(3333, function() { + //Callback triggered when server is successfully listening. Hurray! + console.log('MOCK HTTP-Server (Fritzbox Emulation) listening on: http://localhost:3333'); + console.log(); + console.log('for testing, setup in iobroker for second instance admin:password'); + console.log('-------------------------------------------------------------------'); + callback(); + }); + } + + handleHttpRequest(request, response) { + console.log('HTTP-Server (Fritzbox Emulation): Request: ' + request.method + ' ' + request.url); + // requesturl zerlegen .split('?') + // erste Teil ist entweder login oder webservice + let reqstring = request.url.split('?'); + let switchcmd = null, + sid = null, + ain = null, + onoff = null, + param = null, + endtimestamp = null, + colorcmd = null, + hue = null, + saturation = null, + temperature = null, + duration = null, + target = null, + username = null, + version = null, + logout = null, + userresponse = null; + + let command = reqstring[1].split('&'); + //right part of the http after '?' + for (let i = 0; i < command.length; i++) { + let commandsplit = command[i].split('='); + switch (commandsplit[0]) { + case 'sid': + sid = commandsplit[1]; + console.log('-> sid : ', commandsplit[1]); + break; + case 'switchcmd': + switchcmd = commandsplit[1]; + console.log('-> switchcmd : ', commandsplit[1]); + break; + case 'ain': + ain = commandsplit[1]; + console.log('ain : ', commandsplit[1]); + break; + case 'ain': + onoff = commandsplit[1]; + console.log('-> onoff : ', commandsplit[1]); + break; + case 'param': + onoff = commandsplit[1]; + console.log('-> param : ', commandsplit[1]); + break; + case 'endtimestamp': + onoff = commandsplit[1]; + console.log('-> endtimestamp : ', commandsplit[1]); + break; + case 'hue': + colorcmd = 'hue'; + hue = commandsplit[1]; + console.log('-> hue : ', commandsplit[1]); + break; + case 'saturation': + colorcmd = 'saturation'; + saturation = commandsplit[1]; + console.log('-> saturation : ', commandsplit[1]); + break; + case 'temperature': + temperature = commandsplit[1]; + console.log('-> temperature : ', commandsplit[1]); + break; + case 'duration': + temperature = commandsplit[1]; + console.log('-> duration : ', commandsplit[1]); + break; + case 'target': + target = commandsplit[1]; + console.log('-> target : ', commandsplit[1]); + break; + case 'version': + version = commandsplit[1]; + console.log('-> version : ', commandsplit[1]); + break; + case 'username': + username = commandsplit[1]; + console.log('-> username : ', commandsplit[1]); + break; + case 'response': + response = commandsplit[1]; + console.log('-> response : ', commandsplit[1]); + break; + case 'logout': + logout = commandsplit[1]; + console.log('-> logout : ', commandsplit[1]); + break; + default: + break; + } + } + + if (reqstring[0] == '/login_sid.lua') { + if (version == 2) { + //console.log(request); + response = loginoutAnswerV2(response, sid, request.method, username, userresponse, request); + } else { + response = loginoutAnswerV1(response, sid, request.method, username, userresponse, request); + } + } else if (reqstring[0] == '/webservices/homeautoswitch.lua' && sid === mocksid) { + response = homeautoswitchAnswer( + response, + switchcmd, + ain, + onoff, + param, + endtimestamp, + colorcmd, + hue, + saturation, + temperature, + duration, + target + ); + } else if (request.url == '/wlan/guest_access.lua?0=0&sid=' + mocksid) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(guestWlan)); + response.end(); + } else if (request.url == '/data.lua' && request.method === 'POST') { + //check the URL of the current request + let body = ''; + request.on('data', (chunk) => { + body += chunk.toString(); // convert Buffer to string + }); + request.on('end', () => { + const form = parse(body); + console.log(form); + if (form.sid === sid && form.xhr === '1' && form.page === 'overview') { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + JSON.stringify({ + data: { + naslink: 'nas', + SERVICEPORTAL_URL: + 'https://www.avm.de/fritzbox-service-portal.php?hardware=156&oem=avm&language=de&country=049&version=84.06.85&subversion=', + fritzos: { + Productname: 'FRITZ!Box Fon WLAN 7390', + NoPwd: false, + ShowDefaults: false, + expert_mode: '1', + nspver_lnk: '/home/pp_fbos.lua?sid=' + sid, + nspver: '06.85', + isLabor: false, + FirmwareSigned: false, + fb_name: '', + isUpdateAvail: false, + energy: '40', + boxDate: '13:22:00 09.12.2018' + } + } + }) + ); + response.end(); + } else if ( + form.sid === sid && + form.xhr === '1' && + form.device === '20' && + form.oldpage === '/net/home_auto_hkr_edit.lua' && + form.back_to_page === '/net/network.lua' + ) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write(String(hkr_batt)); + response.end(); + } + }); + } else { + console.log('-> not supported call ' + request.method + ' ' + request.url); + response.statusCode = 403; + response.end(); + } + } +} +//setupHttpServer(function() {}); + +export default FritzEmu; + +// ausprobieren bei echter FB ob getswitchname, getswitchpresent, gettemperature auch auf thermostat geht +// gettemperature hat 0.1 +// gethkrtemps hat 0,5 Schrittweite +// was kommt bei logout von FB zurück? diff --git a/lib/mjs_version/fritz_mockserver_wo_class.js b/lib/mjs_version/fritz_mockserver_wo_class.js new file mode 100644 index 0000000..67961c0 --- /dev/null +++ b/lib/mjs_version/fritz_mockserver_wo_class.js @@ -0,0 +1,933 @@ +// @ts-nocheck +//server to emulate the fritzbox responses +const http = require('http'); +const fs = require('fs'); +const { parse } = require('querystring'); +const parser = require('xml2json-light'); + +const path = require('path'); +console.log('PATH ist ' + path.join(__dirname, './data/')); + +const xmlDevicesGroups = fs.readFileSync(path.join(__dirname, './data/') + 'test_api_response.xml'); +//var xmlDevicesGroups = fs.readFileSync('./test.xml'); + +const xmlTemplate = fs.readFileSync(path.join(__dirname, './data/') + 'template_answer.xml'); + +const xmlTempStat = fs.readFileSync(path.join(__dirname, './data/') + 'devicestat_temp_answer.xml'); + +const xmlPowerStats = fs.readFileSync(path.join(__dirname, './data/') + 'devicestat_power_answer.xml'); + +const xmlColorDefaults = fs.readFileSync(path.join(__dirname, './data/') + 'color_defaults.xml'); + +const hkr_batt = fs.readFileSync(path.join(__dirname, './data/') + 'hkr_response.xml'); + +const guestWlan = fs.readFileSync(path.join(__dirname, './data/') + 'guest_wlan_form.xml'); + +let server; + +function setupHttpServer(callback) { + //We need a function which handles requests and send response + //Create a server + server = http.createServer(handleHttpRequest); + //Lets start our server + server.listen(3333, function() { + //Callback triggered when server is successfully listening. Hurray! + console.log('MOCK HTTP-Server (Fritzbox Emulation) listening on: http://localhost:%s', 3333); + console.log('for testing, setup in iobroker for second instance admin:password'); + callback(); + }); +} + +const challenge = (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8); +const challenge2 = (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8); +const password = 'password'; +const challengeResponse = + challenge + + '-' + + require('crypto').createHash('md5').update(Buffer.from(challenge + '-' + password, 'utf16le')).digest('hex'); +const mocksid = + (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8) + + (4294967295 + Math.floor(Math.random() * 4294967295)).toString(16).slice(-8); + +const devices2json = parser.xml2json(String(xmlDevicesGroups)); +devices = [].concat((devices2json.devicelist || {}).device || []).map((device) => { + // remove spaces in AINs + device.identifier = device.identifier.replace(/\s/g, ''); + return device; +}); +groups = [].concat((devices2json.devicelist || {}).group || []).map((group) => { + // remove spaces in AINs + group.identifier = group.identifier.replace(/\s/g, ''); + return group; +}); +const templates2json = parser.xml2json(String(xmlTemplate)); +templates = [].concat((templates2json.templatelist || {}).template || []).map(function(template) { + // remove spaces in AINs + // template.identifier = group.identifier.replace(/\s/g, ''); + return template; +}); +result = templates; + +//apiresponse is the xml file with AINs not having the spaces inside +var apiresponse = {}; +apiresponse['devicelist'] = { version: '1', device: devices, group: groups }; +apiresponse['templatelist'] = { version: '1', template: templates }; +console.log(apiresponse); + +function handleHttpRequest(request, response) { + console.log('HTTP-Server (Fritzbox Emulation): Request: ' + request.method + ' ' + request.url); + // requesturl zerlegen .split('?') + // erste Teil ist entweder login oder webservice + let reqstring = request.url.split('?'); + let switchcmd = null, + sid = null, + ain = null, + onoff = null, + param = null, + endtimestamp = null, + colorcmd = null, + hue = null, + saturation = null, + temperature = null, + duration = null, + target = null, + username = null, + version = null, + logout = null, + userresponse = null; + + let command = reqstring[1].split('&'); + //right part of the http after '?' + for (let i = 0; i < command.length; i++) { + let commandsplit = command[i].split('='); + switch (commandsplit[0]) { + case 'sid': + sid = commandsplit[1]; + console.log('sid : ', commandsplit[1]); + break; + case 'switchcmd': + switchcmd = commandsplit[1]; + console.log('switchcmd : ', commandsplit[1]); + break; + case 'ain': + ain = commandsplit[1]; + console.log('ain : ', commandsplit[1]); + break; + case 'ain': + onoff = commandsplit[1]; + console.log('onoff : ', commandsplit[1]); + break; + case 'param': + onoff = commandsplit[1]; + console.log('param : ', commandsplit[1]); + break; + case 'endtimestamp': + onoff = commandsplit[1]; + console.log('endtimestamp : ', commandsplit[1]); + break; + case 'hue': + colorcmd = 'hue'; + hue = commandsplit[1]; + console.log('hue : ', commandsplit[1]); + break; + case 'saturation': + colorcmd = 'saturation'; + saturation = commandsplit[1]; + console.log('saturation : ', commandsplit[1]); + break; + case 'temperature': + temperature = commandsplit[1]; + console.log('temperature : ', commandsplit[1]); + break; + case 'duration': + temperature = commandsplit[1]; + console.log('duration : ', commandsplit[1]); + break; + case 'target': + target = commandsplit[1]; + console.log('target : ', commandsplit[1]); + break; + case 'version': + version = commandsplit[1]; + console.log('version : ', commandsplit[1]); + break; + case 'username': + username = commandsplit[1]; + console.log('username : ', commandsplit[1]); + break; + case 'response': + response = commandsplit[1]; + console.log('response : ', commandsplit[1]); + break; + case 'logout': + logout = commandsplit[1]; + console.log('logout : ', commandsplit[1]); + break; + default: + break; + } + } + + if (reqstring[0] == '/login_sid.lua') { + if (version == 2) { + //console.log(request); + response = loginoutAnswerV2(response, sid, request.method, username, userresponse, request); + } else { + response = loginoutAnswerV1(response, sid, request.method, username, userresponse, request); + } + } else if (reqstring[0] == '/webservices/homeautoswitch.lua' && sid === mocksid) { + response = homeautoswitchAnswer( + response, + switchcmd, + ain, + onoff, + param, + endtimestamp, + colorcmd, + hue, + saturation, + temperature, + duration, + target + ); + } else if (request.url == '/wlan/guest_access.lua?0=0&sid=' + mocksid) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(guestWlan)); + response.end(); + } else if (request.url == '/data.lua' && request.method === 'POST') { + //check the URL of the current request + let body = ''; + request.on('data', (chunk) => { + body += chunk.toString(); // convert Buffer to string + }); + request.on('end', () => { + const form = parse(body); + console.log(form); + if (form.sid === sid && form.xhr === '1' && form.page === 'overview') { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + JSON.stringify({ + data: { + naslink: 'nas', + SERVICEPORTAL_URL: + 'https://www.avm.de/fritzbox-service-portal.php?hardware=156&oem=avm&language=de&country=049&version=84.06.85&subversion=', + fritzos: { + Productname: 'FRITZ!Box Fon WLAN 7390', + NoPwd: false, + ShowDefaults: false, + expert_mode: '1', + nspver_lnk: '/home/pp_fbos.lua?sid=' + sid, + nspver: '06.85', + isLabor: false, + FirmwareSigned: false, + fb_name: '', + isUpdateAvail: false, + energy: '40', + boxDate: '13:22:00 09.12.2018' + } + } + }) + ); + response.end(); + } else if ( + form.sid === sid && + form.xhr === '1' && + form.device === '20' && + form.oldpage === '/net/home_auto_hkr_edit.lua' && + form.back_to_page === '/net/network.lua' + ) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write(String(hkr_batt)); + response.end(); + } + }); + } else { + console.log(' not supported call ' + request.method + ' ' + request.url); + response.statusCode = 403; + response.end(); + } +} + +function loginoutAnswerV2(response, sid, method, username, userresponse, request) { + if (!sid && method == 'GET') { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge + + '0' + ); + response.end(); + return response; + } else if (!sid && method == 'POST') { + let body = ''; + request.on('data', (chunk) => { + body += chunk.toString(); // convert Buffer to string + }); + request.on('end', () => { + const form = parse(body); + console.log('user: ' + form.username); + console.log('pbkf2 response: ' + form.response); + // pbkf2 ausrechnen und vergleichen + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '' + + mocksid + + '' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2' + ); + response.end(); + return response; + }); + } else if (sid && method == 'GET') { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '' + + mocksid + + '' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2admin' + ); + response.end(); + return response; + } else if (sid && logout === 1 && method == 'GET') { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2admin' + ); + response.end(); + return response; + } +} +function loginoutAnswerV1(response, sid, method, username, userresponse) { + if (!sid && method == 'GET') { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge + + '0' + ); + response.end(); + return response; + } else if (!sid && username === 'admin') { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge + + '0' + ); + response.end(); + return response; + } else if (!sid && username === 'admin' && userresponse == challengeResponse) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '' + + mocksid + + '' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2' + ); + response.end(); + return response; + } else if (sid) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '' + + mocksid + + '' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2admin' + ); + response.end(); + return response; + } else if (sid == mocksid && logout == 1) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/xml' }); + response.write( + '0000000000000000' + + challenge2 + + '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2admin' + ); + response.end(); + return response; + } +} + +function findAin(type, ain) { + let position = null; + if (type === 'device') { + for (let i = 0; i < apiresponse['devicelist']['device'].length; i++) { + if (apiresponse['devicelist']['device'][i].identifier === ain) { + position = i; + break; + } + } + } else if (type === 'group') { + for (let i = 0; i < apiresponse['devicelist']['device'].length; i++) { + if (apiresponse['devicelist']['device'][i].identifier === ain) { + position = i; + break; + } + } + } + return position; +} + +function errorAnswer(response) { + response.statusCode = 403; + response.end(); + return response; +} + +function homeautoswitchAnswer( + response, + switchcmd, + ain, + onoff, + param, + endtimestamp, + colorcmd, + hue, + saturation, + temperature, + duration, + target +) { + switch (switchcmd) { + case 'getdevicelistinfos': + const devicelistinfos = apiresponse['devicelist']['device'].concat(apiresponse['devicelist']['group']); + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + //response.write(String(devicelistinfos)); + response.write(String(xmlDevicesGroups)); + response.end(); + return response; + break; + case 'getdeviceinfos': + const deviceinfos = apiresponse['devicelist']['device'].filter((device) => device.identifier === ain); + if (deviceinfos) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(deviceinfos)); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'gettemplatelistinfos': + // todo empty templates + //const templatelistinfos = apiresponse['templatelist']['template'] + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + //response.write(String(templatelistinfo)); + response.write(String(xmlTemplate)); + response.end(); + return response; + break; + case 'applytemplate': + const template = apiresponse['templatelist']['template'].filter( + (template) => template.hasOwnProperty('identifier') && template.identifier === ain + ); + if (template) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(template[0].id); + response.end(); + } else { + console.log(' did not find the ain in templates ' + ain); + response = errorAnswer(response); + } + return response; + break; + //alles zu switches + case 'getswitchlist': + //check the URL of the current request + const switchlist = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch')) + .map((device) => device.identifier) + .concat( + apiresponse['devicelist']['group'] + .filter((device) => device.hasOwnProperty('switch')) + .map((device) => device.identifier) + ); + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify(switchlist)); + response.end(); + return response; + break; + case 'setswitchon': + case 'setswitchoff': + case 'setswitchtoggle': + const setswitchstate = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch') && device.identifier === ain) + .map((device) => device.switch.state); + const setgroupstate = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) + .map((group) => group.switch.state); + if (setswitchstate) { + const pos = findAin('device', ain); + if ((switchcmd = 'setswitchon')) { + apiresponse.devicelist.device[pos].switch.state = 1; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ '1' ])); + response.end(); + } else if ((switchcmd = 'setswitchoff')) { + apiresponse.devicelist.device[pos].switch.state = 0; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ '0' ])); + response.end(); + } else if ((switchcmd = 'setswitchtoggle')) { + apiresponse.devicelist.device[pos].switch.state = !setswitchstate; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + !setswitchstate + "'" ])); + response.end(); + } + } else if (setgroupstate) { + const pos = findAin('group', ain); + if ((switchcmd = 'setswitchon')) { + apiresponse.devicelist.group[pos].switch.state = 0; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ '1' ])); + response.end(); + } else if ((switchcmd = 'setswitchoff')) { + apiresponse.devicelist.group[pos].switch.state = 1; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ '0' ])); + response.end(); + } else if ((switchcmd = 'setswitchtoggle')) { + apiresponse.devicelist.group[pos].switch.state = !setgroupstate; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + !setgroupstate + "'" ])); + response.end(); + } + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'gettemperature': + const gettemp = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('temperature') && device.identifier === ain) + .map((device) => device.temperature.celsius); + const getgrouptemp = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('temperature') && device.identifier === ain) + .map((group) => group.temperature.celsius); + if (gettemp) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + gettemp + "'" ])); + response.end(); + } else if (getgrouptemp) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getgrouptemp + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'getswitchname': + case 'getswitchpresent': + const item = switchcmd.replace('getswitch', ''); + const switchvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch') && device.identifier === ain) + .map((device) => device[item]); + const groupvalue = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) + .map((group) => group[item]); + if (switchvalue) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + switchvalue + "'" ])); + response.end(); + } else if (groupvalue) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + groupvalue + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'getswitchstate': + const getswitchstate = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch') && device.identifier === ain) + .map((device) => device.switch.state); + const getgroupstate = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) + .map((group) => group.switch.state); + if (getswitchstate) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getswitchstate + "'" ])); + response.end(); + } else if (groupstate) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getgroupstate + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'getswitchpower': + case 'getswitchenergy': + const item2 = switchcmd.replace('getswitch', ''); + const getswitchmeter = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch') && device.identifier === ain) + .map((device) => device.powermeter[item2]); + const getgroupmeter = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('switch') && group.identifier === ain) + .map((group) => group.powermeter[item2]); + if (getswitchmeter) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getswitchmeter + "'" ])); + response.end(); + } else if (getgroupmeter) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getgroupmeter + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + //alles KHR + case 'gethkrtsoll': + case 'gethkrkomfort': + case 'gethkrabsenk': + const item3 = switchcmd.replace('gethkr', ''); + const gethkrtemp = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr[item3]); + const getgrouphkrtemp = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr[item3]); + if (gethkrtemp) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + gethkrtemp + "'" ])); + response.end(); + } else if (getgrouphkrtemp) { + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + getgrouphkrtemp + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'sethkrtsoll': + console.log('tsoll = ' + param); + const sethkrtemp = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.tsoll); + const setgrouphkrtemp = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.tsoll); + if (sethkrtemp) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].hkr.tsoll = param; + response.statusCode = 200; + response.end(); + } else if (setgrouphkrtemp) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].hkr.tsoll = param; + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + //different + case 'getbasicdevicestats': + // todo prüfen ob gruppen statistik haben können + const thermostat = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.tsoll); + const groupthermo = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.tsoll); + const switcher = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.tsoll); + const groupswitcher = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.tsoll); + if (thermostat || groupthermo) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(xmlTempStat)); + response.end(); + } else if (switcher || groupswitcher) { + //check the URL of the current request + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(xmlTempStat)); + response.write(String(xmlPowerStats)); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setsimpleonoff': + console.log('simple status ' + onoff); + const simplevalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('simpleonoff') && device.identifier === ain) + .map((device) => device.simpleonoff.state); + const groupsimplevalue = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('simpleonoff') && group.identifier === ain) + .map((group) => group.simpleonoff.state); + let newstate = null; + + if (simplevalue) { + if (onoff == 2) { + newstate = !simplevalue; + } else { + //ohne prüfung ob auch 0 oder 1 geschickt wird + newstate = onoff; + } + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].simpleonoff.state = newstate; + response.statusCode = 200; + response.end(); + } else if (groupsimplevalue) { + if (onoff == 2) { + newstate = !groupsimplevalue; + } else { + //ohne prüfung ob auch 0 oder 1 geschickt wird + newstate = onoff; + } + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].simpleonoff.state = newstate; + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setlevel': + console.log('level ' + level); + const levelvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('levelcontrol') && device.identifier === ain) + .map((device) => device.levelcontrol.level); + const grouplevel = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('levelcontrol') && group.identifier === ain) + .map((group) => group.levelcontrol.level); + if (levelvalue) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].levelcontrol.level = level; + apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = Math.floor(Number(level) / 255 * 100); + response.statusCode = 200; + response.end(); + } else if (grouplevel) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].levelcontrol.level = level; + apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = Math.floor(Number(level) / 255 * 100); + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setlevelpercentage': + console.log('level ' + level); + const levelvalueperc = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('levelcontrol') && device.identifier === ain) + .map((device) => device.levelcontrol.levelpercentage); + const grouplevelperc = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('levelcontrol') && group.identifier === ain) + .map((group) => group.levelcontrol.levelpercentage); + if (levelvalueperc) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].levelcontrol.level = Math.floor(Number(level) / 100 * 255); + apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = level; + response.statusCode = 200; + response.end(); + } else if (grouplevelperc) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].levelcontrol.level = Math.floor(Number(level) / 100 * 255); + apiresponse.devicelist.device[pos].levelcontrol.levelpercentage = level; + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setcolor': + console.log('colorcmd ' + colorcmd); + console.log('hue ' + hue); + console.log('saturation ' + saturation); + console.log('additional cmd duration' + duration); + const colorvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('colorcontrol') && device.identifier === ain) + .map((device) => device.colorontrol[cmd]); + const groupcolor = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('colorcontrol') && group.identifier === ain) + .map((group) => group.colorcontrol[cmd]); + const newvalue = hue || saturation; + if (colorvalue) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].colorcontrol[cmd] = newvalue; + response.statusCode = 200; + response.end(); + } else if (groupcolor) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].colorcontrol[cmd] = newvalue; + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setcolortemperature': + console.log('temperature ' + temperature); + const settempvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('colorcontrol') && device.identifier === ain) + .map((device) => device.colorontrol.temperature); + const setgrouptemp = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('colorcontrol') && group.identifier === ain) + .map((group) => group.colorcontrol.temperature); + if (settempvalue) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].colorcontrol.temperature = temperature; + response.statusCode = 200; + response.end(); + } else if (setgrouptemp) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].colorcontrol.temperature = temperature; + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'getcolordefaults': + //wird unabhängig von Lampen geschickt + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(String(xmlColorDefaults)); + response.end(); + return response; + break; + case 'sethkrboost': + console.log('endtimestamp ' + endtimestamp); + const hkrboost = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.boostactiveendtime); + const groupboost = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.boostactiveendtime); + if (endttimestamp > now) { + endtimepstamp = ''; + } + if (hkrboost) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].hkr.boostactiveendtime = endtimestamp; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.end(); + } else if (groupboost) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].hkr.boostactiveendtime = endtimestamp; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'sethkrwindowopen': + console.log('endtimestamp ' + endtimestamp); + const hkrwindow = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('hkr') && device.identifier === ain) + .map((device) => device.hkr.windowopenactiveendtime); + const groupwindow = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('hkr') && group.identifier === ain) + .map((group) => group.hkr.windowopenactiveendtime); + if (endttimestamp > now) { + endtimepstamp = ''; + } + if (hkrwindow) { + const pos = findAin('device', ain); + apiresponse.devicelist.device[pos].hkr.windowopenactiveendtime = endtimestamp; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.end(); + } else if (groupwindow) { + const pos = findAin('group', ain); + apiresponse.devicelist.group[pos].hkr.windowopenactiveendtime = endtimestamp; + response.writeHead(200, { 'xmlDevicesGroups-Type': 'application/json' }); + response.write(JSON.stringify([ "'" + endtimestamp + "'" ])); + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setblind': + console.log('target ' + target); + const blindvalue = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('blind') && device.identifier === ain) + .map((device) => device.blind.mode); + const groupblind = apiresponse['devicelist']['group'] + .filter((group) => group.hasOwnProperty('blind') && group.identifier === ain) + .map((group) => group.blind.mode); + if (blindvalue) { + response.statusCode = 200; + response.end(); + } else if (groupblind) { + response.statusCode = 200; + response.end(); + } else { + console.log(' did not find the ain in devices/groups ' + ain); + response = errorAnswer(response); + } + return response; + break; + case 'setname': + response.statusCode = 200; + response.end(); + return response; + break; + case 'startulesubscription': + break; + case 'getsubscriptionstate': + break; + default: + console.log('switchcmd no case found ' + switchcmd); + response = errorAnswer(response); + return response; + break; + } +} +//setupHttpServer(function() {}); +module.exports.setupHttpServer = setupHttpServer; + +// ausprobieren bei echter FB ob getswitchname, getswitchpresent, gettemperature auch auf thermostat geht +// gettemperature hat 0.1 +// gethkrtemps hat 0,5 Schrittweite +// was kommt bei logout von FB zurück? diff --git a/test/start_mockserver.js b/lib/start_mockserver.js similarity index 60% rename from test/start_mockserver.js rename to lib/start_mockserver.js index 94ab951..aa8c732 100644 --- a/test/start_mockserver.js +++ b/lib/start_mockserver.js @@ -1,4 +1,4 @@ -import { FritzEmu } from '../index.js'; +const FritzEmu = require('../index.js').FritzEmu; const emulation = new FritzEmu(); emulation.setupHttpServer(function() {}); diff --git a/package-lock.json b/package-lock.json index 4beb36e..c356dc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "fritzdect-aha-nodejs", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -132,7 +132,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -153,6 +152,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==" + }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -227,7 +231,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -281,7 +284,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -289,8 +291,90 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "requires": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + } + }, + "command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "requires": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" + } + } }, "concat-map": { "version": "0.0.1", @@ -333,6 +417,11 @@ "type-detect": "^4.0.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -543,8 +632,7 @@ "figlet": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.2.tgz", - "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==", - "dev": true + "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==" }, "file-entry-cache": { "version": "6.0.1", @@ -564,6 +652,14 @@ "to-regex-range": "^5.0.1" } }, + "find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "requires": { + "array-back": "^3.0.1" + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -653,8 +749,7 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "he": { "version": "1.2.0", @@ -806,6 +901,11 @@ "p-locate": "^5.0.0" } }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1079,6 +1179,11 @@ "picomatch": "^2.2.1" } }, + "reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==" + }, "regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -1181,11 +1286,33 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "requires": { "has-flag": "^4.0.0" } }, + "table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "requires": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "dependencies": { + "array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==" + }, + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" + } + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -1222,6 +1349,11 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==" + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -1246,6 +1378,22 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "requires": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "dependencies": { + "typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" + } + } + }, "workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -1272,8 +1420,7 @@ "xml2json-light": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/xml2json-light/-/xml2json-light-1.0.6.tgz", - "integrity": "sha512-6CSibpteBS4B8/fzJaj6TDtWatIlonSFfVVK3TLM23mlTOxkMgVA4b2FaGeTIrrhOMdDZ8X1/dvo4mfBtsU4yw==", - "dev": true + "integrity": "sha512-6CSibpteBS4B8/fzJaj6TDtWatIlonSFfVVK3TLM23mlTOxkMgVA4b2FaGeTIrrhOMdDZ8X1/dvo4mfBtsU4yw==" }, "y18n": { "version": "5.0.8", diff --git a/package.json b/package.json index cf5b54b..9da22e8 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,19 @@ { "name": "fritzdect-aha-nodejs", - "version": "0.9.1", + "version": "1.0.0", "description": "NodeJS library using the AHA api of Fritzbox to control DECT smarthome devices.", "main": "index.js", - "type": "module", + "dependencies": { + "chalk": "^4.1.2", + "figlet": "^1.5.2", + "xml2json-light": "^1.0.6", + "command-line-args": "^5.2.0", + "command-line-usage": "^6.1.1" + }, "devDependencies": { "chai": "^4.3.5", - "chalk": "^4.1.2", "eslint": "^8.26.0", - "figlet": "^1.5.2", - "mocha": "^10.1.0", - "xml2json-light": "^1.0.6" + "mocha": "^10.1.0" }, "scripts": { "test:integration": "mocha test/integration --exit", @@ -20,7 +23,11 @@ "type": "git", "url": "git+https://github.com/foxthefox/fritzdect-aha-nodejs.git" }, - "keywords": [ "DECT", "Fritzbox", "AHA-api" ], + "keywords": [ + "DECT", + "Fritzbox", + "AHA-api" + ], "author": "foxthefox@wysiwis.net", "license": "MIT", "bugs": { diff --git a/test/integration.js b/test/integration.js index 290c254..db805a5 100644 --- a/test/integration.js +++ b/test/integration.js @@ -1,28 +1,31 @@ -import { expect } from 'chai'; -import { Fritz, FritzEmu } from '../index.js'; +const expect = require('chai').expect; +const Fritz = require('../index.js').Fritz; +const FritzEmu = require('../index.js').FritzEmu; /*Setup*/ -import { readFileSync } from 'fs'; -import { xml2json } from 'xml2json-light'; -import { join } from 'path'; +const http = require('http'); +const fs = require('fs'); +//const { parse } = require('querystring'); +const parser = require('xml2json-light'); -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const figlet = require('figlet'); +const chalk = require('chalk'); +const crypto = require('crypto'); -console.log('PATH is ' + join(__dirname, './data/')); -const xmlDevicesGroups = readFileSync(join(__dirname, './data/') + 'test_api_response.xml'); +const path = require('path'); +console.log('PATH ist ' + path.join(__dirname, './data/')); + +const xmlDevicesGroups = fs.readFileSync(path.join(__dirname, './data/') + 'test_api_response.xml'); //var xmlDevicesGroups = fs.readFileSync('./test.xml'); -const xmlTemplate = readFileSync(join(__dirname, './data/') + 'template_answer.xml'); +const xmlTemplate = fs.readFileSync(path.join(__dirname, './data/') + 'template_answer.xml'); -const xmlTempStat = readFileSync(join(__dirname, './data/') + 'devicestat_temp_answer.xml'); +const xmlTempStat = fs.readFileSync(path.join(__dirname, './data/') + 'devicestat_temp_answer.xml'); -const xmlPowerStats = readFileSync(join(__dirname, './data/') + 'devicestat_power_answer.xml'); +const xmlPowerStats = fs.readFileSync(path.join(__dirname, './data/') + 'devicestat_power_answer.xml'); -const xmlColorDefaults = readFileSync(join(__dirname, './data/') + 'color_defaults.xml'); -const devices2json = xml2json(String(xmlDevicesGroups)); +const xmlColorDefaults = fs.readFileSync(path.join(__dirname, './data/') + 'color_defaults.xml'); +const devices2json = parser.xml2json(String(xmlDevicesGroups)); let devices = [].concat((devices2json.devicelist || {}).device || []).map((device) => { // remove spaces in AINs device.identifier = device.identifier.replace(/\s/g, ''); @@ -33,7 +36,7 @@ let groups = [].concat((devices2json.devicelist || {}).group || []).map((group) group.identifier = group.identifier.replace(/\s/g, ''); return group; }); -const templates2json = xml2json(String(xmlTemplate)); +const templates2json = parser.xml2json(String(xmlTemplate)); let templates = [].concat((templates2json.templatelist || {}).template || []).map(function(template) { // remove spaces in AINs // template.identifier = group.identifier.replace(/\s/g, ''); @@ -75,7 +78,7 @@ describe('Test of Fritzdect-AHA-API', () => { */ it('function getswitchlist', async () => { const result = await fritz.getSwitchList(); - //console.log('getswitchlist result', JSON.parse(result)); + //console.log('getswitchlist result', result); const switchlist = apiresponse['devicelist']['device'] .filter((device) => device.hasOwnProperty('switch')) .map((device) => device.identifier) @@ -84,9 +87,9 @@ describe('Test of Fritzdect-AHA-API', () => { .filter((device) => device.hasOwnProperty('switch')) .map((device) => device.identifier) ); - //console.log(switchlist); - expect(JSON.parse(result)).to.have.length(3); - expect(JSON.parse(result)).to.eql(switchlist); + //FB liefert kein array zurück, sondern ain über Komma getrennt + //switchlist wäre ein array, über String() wird es vergleichbarer Text + expect(result).to.eql(String(switchlist)); }); it('logout success returns true', async () => { const result = await fritz.logout_SID(); @@ -94,6 +97,10 @@ describe('Test of Fritzdect-AHA-API', () => { }); }); +// alte Fb prüfen +// alle exponierten CMDs prüfen +// Inhalte prüfen + /* var assert = require('assert'); describe('login test', () => { diff --git a/test/mjs_version/integration.mjs b/test/mjs_version/integration.mjs new file mode 100644 index 0000000..290c254 --- /dev/null +++ b/test/mjs_version/integration.mjs @@ -0,0 +1,107 @@ +import { expect } from 'chai'; +import { Fritz, FritzEmu } from '../index.js'; + +/*Setup*/ +import { readFileSync } from 'fs'; +import { xml2json } from 'xml2json-light'; +import { join } from 'path'; + +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +console.log('PATH is ' + join(__dirname, './data/')); +const xmlDevicesGroups = readFileSync(join(__dirname, './data/') + 'test_api_response.xml'); +//var xmlDevicesGroups = fs.readFileSync('./test.xml'); + +const xmlTemplate = readFileSync(join(__dirname, './data/') + 'template_answer.xml'); + +const xmlTempStat = readFileSync(join(__dirname, './data/') + 'devicestat_temp_answer.xml'); + +const xmlPowerStats = readFileSync(join(__dirname, './data/') + 'devicestat_power_answer.xml'); + +const xmlColorDefaults = readFileSync(join(__dirname, './data/') + 'color_defaults.xml'); +const devices2json = xml2json(String(xmlDevicesGroups)); +let devices = [].concat((devices2json.devicelist || {}).device || []).map((device) => { + // remove spaces in AINs + device.identifier = device.identifier.replace(/\s/g, ''); + return device; +}); +let groups = [].concat((devices2json.devicelist || {}).group || []).map((group) => { + // remove spaces in AINs + group.identifier = group.identifier.replace(/\s/g, ''); + return group; +}); +const templates2json = xml2json(String(xmlTemplate)); +let templates = [].concat((templates2json.templatelist || {}).template || []).map(function(template) { + // remove spaces in AINs + // template.identifier = group.identifier.replace(/\s/g, ''); + return template; +}); + +//apiresponse is the xml file with AINs not having the spaces inside +var apiresponse = {}; +apiresponse['devicelist'] = { version: '1', device: devices, group: groups }; +apiresponse['templatelist'] = { version: '1', template: templates }; + +/*Test*/ +describe('Test of Fritzdect-AHA-API', () => { + let port = 3311; + let testfile = 'bla.xml'; + let testdevice = 'fritzbox'; + before('start the FB emulation', () => { + const emulation = new FritzEmu(testfile, port, false); + emulation.setupHttpServer(function() {}); + }); + var fritz; + // if promise is returned = success + it('should create a new fritzdect instance', function() { + fritz = new Fritz('admin', 'password', 'http://localhost:3333', null); + }); + it('login success returns true', async () => { + const result = await fritz.login_SID(); + //assert.equal(result, true); + expect(result).to.equal(true); + }); + /* + it('function getdevicelistinfos', async () => { + const result = await fritz.getDeviceListInfos(); + //console.log('getdevicelistinfos result', JSON.parse(result)); + const devicelist = apiresponse['devicelist']; + //console.log(switchlist); + expect(parser.xml2json(result).devicelist).to.eql(devicelist); + }); + */ + it('function getswitchlist', async () => { + const result = await fritz.getSwitchList(); + //console.log('getswitchlist result', JSON.parse(result)); + const switchlist = apiresponse['devicelist']['device'] + .filter((device) => device.hasOwnProperty('switch')) + .map((device) => device.identifier) + .concat( + apiresponse['devicelist']['group'] + .filter((device) => device.hasOwnProperty('switch')) + .map((device) => device.identifier) + ); + //console.log(switchlist); + expect(JSON.parse(result)).to.have.length(3); + expect(JSON.parse(result)).to.eql(switchlist); + }); + it('logout success returns true', async () => { + const result = await fritz.logout_SID(); + expect(result).to.equal(false); + }); +}); + +/* +var assert = require('assert'); +describe('login test', () => { + const fritz = new Fritz('admin', 'password', 'http://localhost:3333', null); + it('login success returns true', () => { + return fritz.login_SID().then((result) => { + assert.equal(result, true); + }); + }); +}); +*/