hello,
I tried to packt this code
into a node red function.
i tried with node red http request node, with authentication, but can't get it running.
so I used an online code converter from python to javascript and adapted afterwards the code to have it maybe working.
the http request does not work at all, so I added nodejs http module to globals - and I still can't get it running, more worse, node red crashes as (I guess) nodejs http opens a connection and leaves it open - and on the next request it dies there.
20 Apr 18:31:00 - [red] Uncaught Exception:
20 Apr 18:31:00 - [error] Error: socket hang up
at connResetException (node:internal/errors:704:14)
at Socket.socketOnEnd (node:_http_client:505:23)
at Socket.emit (node:events:525:35)
at endReadableNT (node:internal/streams/readable:1359:12)
at processTicksAndRejections (node:internal/process/task_queues:82:21)
nodered.service: Main process exited, code=exited, status=1/FAILURE
nodered.service: Failed with result 'exit-code'.
I am quite sure, its not intended to be used this way.
maybe somebody can either help me replacing the http request in line 312 with an better function, so I can experiment further, or help me with a "node red" way speaking to the webinterface with the inverter?
Thanks.
[{"id":"226590f61c67af28","type":"function","z":"df7c9eb28d26a1c2","d":true,"name":"converttest","func":"//from https://github.com/muexxl/batcontrol/blob/main/fronius/fronius.py\n\nfunction hash_utf8(x) {\n if (typeof x === 'string') {\n x = x.encode('utf-8');\n }\n var crypto = require('crypto');\n return crypto.createHash('md5').update(x).digest(\"hex\");;\n}\n\nclass FroniusWR {\n constructor(address, user, password) {\n this.login_attempts = 0;\n this.address = '192.168.0.249';\n this.capacity = -1;\n this.max_charge_rate = 4000;\n this.max_grid_power = 5000;\n this.nonce = 0;\n this.user = 'technican';\n this.password = 'mypassword';\n this.previous_config = this.get_battery_config();\n if (!this.previous_config) {\n throw new Error(`[Fronius] failed to load Battery config from Inverter at ${this.address}`);\n }\n this.min_soc = this.previous_config['BAT_M0_SOC_MIN'];\n this.max_soc = this.previous_config['BAT_M0_SOC_MAX'];\n this.get_time_of_use();\n }\n\n get_SOC() {\n const path = '/solar_api/v1/GetPowerFlowRealtimeData.fcgi';\n const response = this.send_request(path);\n if (!response) {\n node.error('[Fronius] Failed to get SOC. Returning default value of 99.0');\n return 99.0;\n }\n const result = JSON.parse(response.text);\n const soc = result['Body']['Data']['Inverters']['1']['SOC'];\n return soc;\n }\n\n get_free_capacity() {\n const current_soc = this.get_SOC();\n const capa = this.get_capacity();\n const free_capa = (this.max_soc - current_soc) / 100 * capa;\n return free_capa;\n }\n\n get_stored_energy() {\n const current_soc = this.get_SOC();\n const capa = this.get_capacity();\n const energy = (current_soc - this.min_soc) / 100 * capa;\n return energy;\n }\n\n get_usable_capacity() {\n const usable_capa = (this.max_soc - this.min_soc) / 100 * this.get_capacity();\n return usable_capa;\n }\n\n get_battery_config() {\n const path = '/config/batteries';\n const response = this.send_request(path, true);\n if (!response) {\n node.error('[Fronius] Failed to get SOC. Returning empty dict');\n return {};\n }\n const result = JSON.parse(response.text);\n flow.set('Batteryconfig', result)\n return result;\n }\n\n restore_battery_config() {\n const settings_to_restore = [\n 'BAT_M0_SOC_MAX',\n 'BAT_M0_SOC_MIN',\n 'BAT_M0_SOC_MODE',\n 'HYB_BM_CHARGEFROMAC',\n 'HYB_EM_MODE',\n 'HYB_EM_POWER',\n 'HYB_EVU_CHARGEFROMGRID'\n ];\n const settings = {};\n for (const key in settings_to_restore) {\n if (key in this.previous_config) {\n settings[key] = this.previous_config[key];\n } else {\n throw new Error(`Unable to restore settings. Parameter ${key} is missing`);\n }\n }\n const path = '/config/batteries';\n const payload = JSON.stringify(settings);\n node.warn(`[Fronius] Restoring previous battery configuration: ${payload} `);\n const response = this.send_request(path, 'POST', payload, true);\n if (!response) {\n throw new Error('failed to restore battery config');\n }\n const response_dict = JSON.parse(response.text);\n const expected_write_successes = settings_to_restore;\n for (const expected_write_success in expected_write_successes) {\n if (!(expected_write_success in response_dict['writeSuccess'])) {\n throw new Error(`failed to set ${expected_write_success}`);\n }\n }\n return response;\n }\n\n set_allow_grid_charging(value) {\n let payload;\n if (value) {\n payload = '{\"HYB_EVU_CHARGEFROMGRID\": true}';\n } else {\n payload = '{\"HYB_EVU_CHARGEFROMGRID\": false}';\n }\n const path = '/config/batteries';\n const response = this.send_request(path, 'POST', payload, true);\n const response_dict = JSON.parse(response.text);\n const expected_write_successes = ['HYB_EVU_CHARGEFROMGRID'];\n for (const expected_write_success in expected_write_successes) {\n if (!(expected_write_success in response_dict['writeSuccess'])) {\n throw new Error(`failed to set ${expected_write_success}`);\n }\n }\n return response;\n }\n\n set_wr_parameters(minsoc, maxsoc, allow_grid_charging, grid_power) {\n const path = '/config/batteries';\n if (typeof allow_grid_charging !== 'boolean') {\n throw new Error(`Expected type: bool actual type: ${typeof allow_grid_charging}`);\n }\n grid_power = parseInt(grid_power);\n minsoc = parseInt(minsoc);\n maxsoc = parseInt(maxsoc);\n if (!(0 <= grid_power && grid_power <= this.max_charge_rate)) {\n throw new Error(`gridpower out of allowed limits ${grid_power}`);\n }\n if (minsoc > maxsoc) {\n throw new Error('Min SOC needs to be higher than Max SOC');\n }\n if (minsoc < this.min_soc) {\n throw new Error(`Min SOC not allowed below ${this.min_soc}`);\n }\n if (maxsoc > this.max_soc) {\n throw new Error(`Max SOC not allowed above ${this.max_soc}`);\n }\n const parameters = {\n 'HYB_EVU_CHARGEFROMGRID': allow_grid_charging,\n 'HYB_EM_POWER': grid_power,\n 'HYB_EM_MODE': 1,\n 'BAT_M0_SOC_MIN': minsoc,\n 'BAT_M0_SOC_MAX': maxsoc,\n 'BAT_M0_SOC_MODE': 'manual'\n };\n const payload = JSON.stringify(parameters);\n node.warn(`[Fronius] Setting battery parameters: ${payload} `);\n const response = this.send_request(path, 'POST', payload, true);\n if (!response) {\n node.error('[Fronius] Failed to set parameters. No response from server');\n return response;\n }\n const response_dict = JSON.parse(response.text);\n for (const expected_write_success in parameters) {\n if (!(expected_write_success in response_dict['writeSuccess'])) {\n throw new Error(`failed to set ${expected_write_success}`);\n }\n }\n return response;\n }\n\n get_time_of_use() {\n const response = this.send_request('/config/timeofuse', true);\n if (!response) {\n return null;\n }\n const result = JSON.parse(response.text)['timeofuse'];\n flow.set('time_of_use_config_json', result)\n //TODO: do not overwrite!!!\n return result;\n }\n\n set_mode_avoid_discharge() {\n const timeofuselist = [{\n 'Active': true,\n 'Power': 0,\n 'ScheduleType': 'DISCHARGE_MAX',\n 'TimeTable': {\n 'End': '23:59',\n 'Start': '00:00'\n },\n 'Weekdays': {\n 'Fri': true,\n 'Mon': true,\n 'Sat': true,\n 'Sun': true,\n 'Thu': true,\n 'Tue': true,\n 'Wed': true\n }\n }];\n this.set_allow_grid_charging(false);\n return this.set_time_of_use(timeofuselist);\n }\n\n set_mode_allow_discharge() {\n this.set_allow_grid_charging(false);\n const response = this.set_time_of_use([]); //set empty time of use list to remove all rules\n return response;\n }\n\n set_mode_force_charge(chargerate = 500) {\n const timeofuselist = [{\n 'Active': true,\n 'Power': chargerate,\n 'ScheduleType': 'CHARGE_MIN',\n 'TimeTable': {\n 'End': '23:59',\n 'Start': '00:00'\n },\n 'Weekdays': {\n 'Fri': true,\n 'Mon': true,\n 'Sat': true,\n 'Sun': true,\n 'Thu': true,\n 'Tue': true,\n 'Wed': true\n }\n }];\n this.set_allow_grid_charging(true);\n return this.set_time_of_use(timeofuselist);\n }\n\n restore_time_of_use_config() {\n let time_of_use_config_json = flow.get(\"time_of_use_config_json\") || 0;\n if (time_of_use_config_json == 0) {\n node.error('[Fronius] could not restore timeofuse config');\n return;\n }\n let time_of_use_config;\n try {\n time_of_use_config = JSON.parse(time_of_use_config_json);\n } catch (err) {\n node.error(`[Fronius] could not parse timeofuse config from time_of_use_config_json`);\n return;\n }\n const stripped_time_of_use_config = [];\n for (const listitem in time_of_use_config) {\n const new_item = {};\n new_item['Active'] = listitem['Active'];\n new_item['Power'] = listitem['Power'];\n new_item['ScheduleType'] = listitem['ScheduleType'];\n new_item['TimeTable'] = {\n 'Start': listitem['TimeTable']['Start'],\n 'End': listitem['TimeTable']['End']\n };\n const weekdays = {};\n for (const day in ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']) {\n weekdays[day] = listitem['Weekdays'][day];\n }\n new_item['Weekdays'] = weekdays;\n stripped_time_of_use_config.push(new_item);\n }\n this.set_time_of_use(stripped_time_of_use_config);\n }\n\n set_time_of_use(timeofuselist) {\n const config = {\n 'timeofuse': timeofuselist\n };\n const payload = JSON.stringify(config);\n const response = this.send_request('/config/timeofuse', 'POST', payload, true);\n const response_dict = JSON.parse(response.text);\n const expected_write_successes = ['timeofuse'];\n for (const expected_write_success in expected_write_successes) {\n if (!(expected_write_success in response_dict['writeSuccess'])) {\n throw new Error(`failed to set ${expected_write_success}`);\n }\n }\n return response;\n }\n\n get_capacity() {\n if (this.capacity >= 0) {\n return this.capacity;\n }\n const response = this.send_request('/solar_api/v1/GetStorageRealtimeData.cgi');\n if (!response) {\n node.warn('[Fronius] capacity request failed. Returning default value');\n return 1000;\n }\n const result = JSON.parse(response.text);\n const capacity = result['Body']['Data']['0']['Controller']['DesignedCapacity'];\n this.capacity = capacity;\n return capacity;\n }\n\n send_request(path, method = 'GET', payload = '', params = null, headers = {}, auth = false) {\n for (let i = 0; i < 1; i++) {\n const url = 'http://' + this.address + path;\n let fullpath = path;\n if (params) {\n fullpath += '?' + Object.keys(params).map(k => `${k}=${params[k]}`).join('&');\n }\n node.warn('url: ' + url)\n if (auth) {\n headers['Authorization'] = this.get_auth_header(method, fullpath);\n }\n node.warn('auth: ' + auth)\n // try {\n var http = global.get('http');\n var options = {method: 'GET', headers: {headers}}\n let response = http.request(url, options);\n //let response = requests.request(method, url, params, headers, payload);\n node.warn(response)\n if (response.statusCode === 200) {\n return response;\n } else if (response.statusCode === 401) {\n this.nonce = this.get_nonce(response);\n if (this.login_attempts >= 3) {\n node.warn('[Fronius] Login failed 3 times .. aborting');\n throw new Error('[Fronius] Login failed repeatedly .. wrong credentials?');\n }\n response = this.login();\n if (response.statusCode === 200) {\n node.warn('[Fronius] Login successful');\n this.login_attempts = 0;\n } else {\n node.warn('[Fronius] Login failed');\n }\n } else {\n throw new Error(`Server ${this.address} returned ${response.statusCode}`);\n }\n // } catch (err) {\n // node.error(`[Fronius] Connection to Inverter failed on ${this.address}. Retrying in 120 seconds`);\n // // const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));\n // // await sleep(20000)\n // }\n }\n let response = null;\n return response;\n }\n\n login() {\n const params = {\n 'user': this.user\n };\n const path = '/commands/Login';\n this.login_attempts += 1;\n return this.send_request(path, true);\n }\n\n logout() {\n const params = {\n 'user': this.user\n };\n const path = '/commands/Logout';\n const response = this.send_request(path, true);\n if (!response) {\n node.warn('[Fronius] Logout failed. No response from server');\n }\n if (response.statusCode === 200) {\n node.warn('[Fronius] Logout successful');\n } else {\n node.warn('[Fronius] Logout failed');\n }\n return response;\n }\n\n get_nonce(response) {\n let auth_string;\n if ('X-WWW-Authenticate' in response.headers) {\n auth_string = response.headers['X-WWW-Authenticate'];\n } else if ('X-Www-Authenticate' in response.headers) {\n auth_string = response.headers['X-Www-Authenticate'];\n } else {\n auth_string = '';\n }\n const auth_list = auth_string.replace(' ', '').replace('\"', '').split(',');\n const auth_dict = {};\n for (const item in auth_list) {\n const [key, value] = item.split('=');\n auth_dict[key] = value;\n }\n return auth_dict['nonce'];\n }\n\n get_auth_header(method, path) {\n const nonce = this.nonce;\n const realm = 'Webinterface area';\n const ncvalue = '00000001';\n const cnonce = 'NaN';\n const user = this.user;\n const password = this.password;\n if (this.user.length < 4) {\n throw new Error('User needed for Authorization');\n }\n if (this.password.length < 4) {\n throw new Error('Password needed for Authorization');\n }\n const A1 = `${user}:${realm}:${password}`;\n const A2 = `${method}:${path}`;\n const HA1 = hash_utf8(A1);\n const HA2 = hash_utf8(A2);\n const noncebit = `${nonce}:${ncvalue}:${cnonce}:auth:${HA2}`;\n const respdig = hash_utf8(`${HA1}:${noncebit}`);\n const auth_header = `Digest username=\"${user}\", realm=\"${realm}\", nonce=\"${nonce}\", uri=\"${path}\", algorithm=\"MD5\", qop=auth, nc=${ncvalue}, cnonce=\"${cnonce}\", response=\"${respdig}\"`;\n return auth_header;\n }\n\n __del__() {\n this.restore_battery_config();\n this.restore_time_of_use_config();\n this.logout();\n }\n}\n\nvar tst = new FroniusWR();\nvar tst2 = tst.get_battery_config();\n\nnode.warn(tst2);\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":830,"y":140,"wires":[[]]}]