diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..1cbe002b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = false diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..908f27e2 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,46 @@ +{ + "env": { + "node": true, + "es2021": true + }, + "rules": { + "brace-style": "off", + "camelcase": "off", + "consistent-return": "off", + "eqeqeq": "off", + "func-names": "off", + "global-require": "off", + "guard-for-in": "off", + "import/extensions": "off", + "max-len": 0, + "no-async-promise-executor": "off", + "no-cond-assign": "off", + "no-console": "off", + "no-const-assign": "off", + "no-constant-condition": "off", + "no-mixed-spaces-and-tabs": "off", + "no-new": "off", + "no-trailing-spaces": "off", + "no-restricted-syntax": "off", + "no-tabs": "off", + "no-undef": "off", + "comma-dangle": "off", + "no-unused-vars": "off", + "no-multi-spaces": "off", + "node/no-unsupported-features/node-builtins": "off", + "no-multiple-empty-lines": "off", + "padding-line-between-statements": "off", + "radix": "off", + "indent": "off", + "valid-typeof": "off", + "import/no-commonjs": "off", + "no-useless-concat": "off", + "linebreak-style": 0, + "object-curly-newline": "off", + "object-property-newline": "off", + "quote-props": "off", + "node/no-commonjs": "off", + "quotes": "off", + "prefer-template": "off" + } +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..b30b7d70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +** Version of Homey ** +** Version of the Homewizard Homey app ** +** Version of the firmware of the device you are trying to add (Homewizard wifi dongle p1 must be 2.09) +** Confirm Local API has been enabled in Homewizard Energy app needed for discovery + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/workflows/homey-publish.yaml b/.github/workflows/homey-publish.yaml new file mode 100644 index 00000000..21a230c0 --- /dev/null +++ b/.github/workflows/homey-publish.yaml @@ -0,0 +1,23 @@ +name: Publish Homey App (old) + +on: + push: + branches: + - main # Replace with the branch you want to trigger the publish + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Publish to Homey + uses: jtebbens/homey-app-publish@v1 + with: + HOMEY_CLI_TOKEN: ${{ secrets.HOMEY_CLI_TOKEN }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..b309709a --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,20 @@ +name: Publish Homey app +on: + workflow_dispatch: + +jobs: + main: + name: Publish Homey App + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Publish + uses: athombv/github-action-homey-app-publish@master + id: publish + with: + personal_access_token: ${{ secrets.HOMEY_CLI_TOKEN }} + + - name: URL + run: | + echo "Manage your app at ${{ steps.publish.outputs.url }}." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 00000000..34e57c6d --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,46 @@ +--- +name: CI + +on: + pull_request: + types: [opened, reopened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + homey-validate: + runs-on: ubuntu-latest + name: Validate Homey App + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 20.9.0 + cache: 'npm' + + - name: Install Homey CLI + run: npm ci --no-optional homey + - run: npm ci --include=optional sharp + + - name: Validate Homey App + run: npx homey app validate --level=publish + + lint-eslint: + name: eslint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.9.0 + cache: 'npm' + + - run: npm ci + - run: npm run lint-check diff --git a/.gitignore b/.gitignore index 40eb83a5..8baa4127 100644 --- a/.gitignore +++ b/.gitignore @@ -1,243 +1,15 @@ *.DS_Store -.AppleDouble -.LSOverride +/node_modules -# Icon must end with two \r -Icon +# Added by Homey CLI +/.homeybuild/ +# AI instruction files (keep private) +CLAUDE.md +CONTEXT.md +DEVELOPMENT.md +SKILL.md -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -################# -## Eclipse -################# - -*.pydevproject -.project -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.classpath -.settings/ -.loadpath - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# CDT-specific -.cproject - -# PDT-specific -.buildpath - - -################# -## Visual Studio -################# - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results - -[Dd]ebug/ -[Rr]elease/ -x64/ -build/ -[Bb]in/ -[Oo]bj/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.log -*.scc - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.Publish.xml -*.pubxml -*.publishproj - -# NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line -#packages/ - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.[Pp]ublish.xml -*.pfx -*.publishsettings - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -App_Data/*.mdf -App_Data/*.ldf - -############# -## Windows detritus -############# - -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac crap -.DS_Store - - -############# -## Python -############# - -*.py[cod] - -# Packages -*.egg -*.egg-info -dist/ -build/ -eggs/ -parts/ -var/ -sdist/ -develop-eggs/ -.installed.cfg - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox - -#Translations -*.mo - -#Mr Developer -.mr.developer.cfg +# Optional: any other internal prompt files +*.prompt.md +*.ai.md \ No newline at end of file diff --git a/.homeychangelog.json b/.homeychangelog.json new file mode 100644 index 00000000..23213e83 --- /dev/null +++ b/.homeychangelog.json @@ -0,0 +1,1208 @@ +{ + "2.0.8": { + "en": "Bug fix: setClass(solar)" + }, + "2.0.9": { + "en": "Bug fixes (memory)" + }, + "2.0.10": { + "en": "node-fetch increased timeout 20s" + }, + "2.0.11": { + "en": "Bug fixes on P1 dongle (removal of Current usage all phases -> duplicate)" + }, + "2.0.12": { + "en": "Bug fix attempt (customer request SDM230 & P1 dongle)" + }, + "2.0.13": { + "en": "Solar class fix for KWH module" + }, + "2.0.14": { + "en": "Bugfix SDM230 (Measure_power value empty)" + }, + "2.0.15": { + "en": "Manifest update" + }, + "2.0.16": { + "en": "Heatlink and Energylink icon update" + }, + "2.0.17": { + "en": "svg files size issue" + }, + "2.1.1": { + "en": "Energy socket support" + }, + "2.1.2": { + "en": "Images and icon" + }, + "2.1.3": { + "en": "Improved mdns discovery (avoid wrong devices)" + }, + "2.1.4": { + "en": "Thermometer and Humidity offset support" + }, + "2.1.5": { + "en": "Attempt to catch unhandled rejections" + }, + "2.1.6": { + "en": "async fixes" + }, + "2.1.7": { + "en": "measure_rain invalid check" + }, + "2.1.8": { + "en": "Energylink error catch fixes" + }, + "2.1.9": { + "en": "Adjusting timeouts polling for slow Homewizard legacy devices" + }, + "2.1.10": { + "en": "Homewizard Legacy polling to 20seconds and timeout 18seconds" + }, + "2.1.11": { + "en": "Energylink bug fix" + }, + "2.1.13": { + "en": "AbortController & FetchError catch error handling" + }, + "2.1.14": { + "en": "Rainmeter fix" + }, + "2.1.15": { + "en": "Typo fix for rainmeter" + }, + "2.1.16": { + "en": "Adjusted mdns condition check as it was matching the wrong devices" + }, + "2.1.17": { + "en": "Changed mdns discovery string for sdm230 and sdm630 to host = kwhmeter" + }, + "2.1.18": { + "en": "Conditional timeouts and code fixes" + }, + "2.1.19": { + "en": "Added precheck for Homewizard Legacy before attempting node-fetch connect" + }, + "2.1.20": { + "en": "Typo fix" + }, + "2.1.21": { + "en": "Code fix (removed async on json parse, didnt update the values)" + }, + "2.1.22": { + "en": "Potential workaround for fqdn based ip for homewizard legacy" + }, + "2.1.23": { + "en": "Fix for string to number convert (Homewizard port)" + }, + "2.1.25": { + "en": "Added Homewizard Energy - Watermeter support" + }, + "2.1.26": { + "en": "Fix unhandledRejection" + }, + "2.1.27": { + "en": "Replaced node-fetch with Axios for Homewizard Legacy as its easier for timeout issues." + }, + "2.1.28": { + "en": "Heatlink function error fix" + }, + "2.1.29": { + "en": "Adjusted error logging to show less details, added axios abort controller to enforce abort to pending session." + }, + "2.1.30": { + "en": "Homewizard context/preset bug fix" + }, + "2.1.31": { + "en": "Watermeter offset support to align with real value" + }, + "2.1.32": { + "en": "Updated kwh1 and kwh3 icons" + }, + "2.1.33": { + "en": "Updated icons for Homewizard legacy devices (credits to basvanderploeg)" + }, + "2.1.34": { + "en": "Energylink car support and Wifi signal strength value added to Energy Sockets" + }, + "2.1.35": { + "en": "Updated wifi ssid logo, added P1, watermeter, kwh1 & kwh3 wifi strength signal support" + }, + "2.1.36": { + "en": "Changed energy_socket to class socket (was sensor)." + }, + "2.1.37": { + "en": "Minor fixes on energy_socket adjustment" + }, + "2.1.38": { + "en": "Changed SDM230 to socket type to allow Solar based tracking (was sensor)" + }, + "2.1.39": { + "en": "Bug fix" + }, + "2.1.40": { + "en": "Modified solar flag to ignore socket type" + }, + "2.1.41": { + "en": "Force socket type to SDM230" + }, + "3.0.0": { + "en": "SDKv3 Support (in prep for Pro2023 release)" + }, + "3.0.1": { + "en": "Textual fix in app name (showed v3 at end)" + }, + "3.0.2": { + "en": "Code cleanup and bugfix" + }, + "3.0.3": { + "en": "Offset watermeter and thermometer fixed (callback is not a function error)" + }, + "3.0.4": { + "en": "P1 meter fix for returning power and for Sweden having different values from the P1 as aggregated meter." + }, + "3.0.5": { + "en": "Code cleanup" + }, + "3.0.6": { + "en": "Revert legacy homewizard connectivity back to Axios module, others stay node-fetch" + }, + "3.0.7": { + "en": "Preset bugfix (await problem in code)" + }, + "3.0.8": { + "en": "Some async/await changes" + }, + "3.1.0": { + "en": "New features P1 HW Energy firmware and bugfixes" + }, + "3.1.1": { + "en": "Peak/OffPeak trigger fixed" + }, + "3.1.2": { + "en": "Pre FW 4.x P1 dongle fix for T1 & T2 export meter values" + }, + "3.1.3": { + "en": "Added volt support P1 dongle (3phase types) and code change Homewizard legacy preset changes (rollback to avoid undefined error)" + }, + "3.1.4": { + "en": "Bug fix voltage. 3 decimal support for KwH P1 device" + }, + "3.1.5": { + "en": "Remove Amp 2 and 3 from Phase 1 P1 dongle" + }, + "3.1.6": { + "en": "Typo fix for Phase3 Volt value being empty" + }, + "3.1.7": { + "en": "Added custom import and export total kwh meters and triggers for better tracking energy usage" + }, + "3.2.0": { + "en": "Improved Heatlink" + }, + "3.2.1": { + "en": "Replaced axios with node-fetch and additional retry & abort code. Belgium P1 value added for monthly peak watt" + }, + "3.2.2": { + "en": "Text fix for new meter as it got the default measure_power" + }, + "3.2.3": { + "en": "Code fix for P1 voltage & amp. Attempt to get rid of callback calls" + }, + "3.2.4": { + "en": "Several bug fixes, see changelog" + }, + "3.2.5": { + "en": "Homewizard Sensor Driver fix SDK3 problem" + }, + "3.2.6": { + "en": "Removed retry code for legacy Homewizard (HW wifi chip cant handle extra connections)" + }, + "3.2.7": { + "en": "Increased polling cycle to 20s for HW Legacy" + }, + "3.2.8": { + "en": "30s poll and 28s timeout" + }, + "3.2.9": { + "en": "Removed retry fetch for HW Legacy. Heatlink added tapwater and updated icons." + }, + "3.2.10": { + "en": "Modifications to mDNS. Unhandled rejection code for Heatlink. Drivername changes." + }, + "3.2.11": { + "en": "mDNS bugfix on regex and product_type to find devices." + }, + "3.2.12": { + "en": "Energylink bug fix s2 for \"other\" or \"car\" type sources." + }, + "3.2.13": { + "en": "Windmeter fix (device not found)" + }, + "3.2.14": { + "en": "P1 added Power failures, voltage sags & swell counts" + }, + "3.2.15": { + "en": "Bug fix socket class for KWH3" + }, + "3.2.16": { + "en": "Added optional energy socket Watt compensation" + }, + "3.2.17": { + "en": "Controller error bug fix" + }, + "3.2.18": { + "en": "Wattcher bug fix and windmeter battery support" + }, + "3.2.19": { + "en": "Doorcontact 868 fix for Homewizard Legacy" + }, + "3.2.20": { + "en": "Windmeter battery code fix (when battery it needs to update it)" + }, + "3.2.21": { + "en": "Windmeter conditional fix (battery empty but shows still data)" + }, + "3.2.22": { + "en": "Rainmeter battery alarm added (Homewizard Legacy)" + }, + "3.2.24": { + "en": "P1 adjustment for Phase3 circuit. Offset watermeter import from Homewizard Energy when set." + }, + "3.2.25": { + "en": "Bugfix watermeter offset" + }, + "3.3.0": { + "en": "KWH Meters SDM230 & SDM630 added support for Voltage & Amp" + }, + "3.3.1": { + "en": "Bug fix SDM630 active_current_l1_a undefined" + }, + "3.3.2": { + "en": "Additional mDNS matching for kWh meters" + }, + "3.3.3": { + "en": "mDNS timing problem (async onDiscoveryAvailable)" + }, + "3.3.4": { + "en": "Attempt to improve mDNS functions with async calls" + }, + "3.3.5": { + "en": "Lowered CPU footprint (polling energy sockets to 10s)" + }, + "3.3.6": { + "en": "Attempt to get correct gasmeter from P1 when replaced unit, Voltage & Amp support for Norway" + }, + "3.3.7": { + "en": "Bugfix Capability remove (P1). " + }, + "3.3.8": { + "en": "Energylink Insight support (user request)" + }, + "3.3.9": { + "en": "Energylink added S2 solar capability" + }, + "3.3.10": { + "en": "Energylink name tags S2 (solar) added" + }, + "3.3.11": { + "en": "Rollback gasmeter code (old P1 firmware fails check)" + }, + "3.3.12": { + "en": "Energylink user request fix to force positive solar values from unit" + }, + "3.3.13": { + "en": "Other means of making a negative value positive from the Energylink T2" + }, + "3.3.14": { + "en": "Removed delayed push code for Energylink" + }, + "3.3.15": { + "en": "Added T3 meter to P1 (User request)" + }, + "3.3.16": { + "en": "Bugfix T3 meter, added to app.json file" + }, + "3.3.17": { + "en": "Added 60s timeout for Homewizard wifi devices due to bad user wifi coverage" + }, + "3.3.18": { + "en": "Added support to the Energy usage for Homey (Homey SDK)" + }, + "3.3.19": { + "en": "Remove node-fetch and use fetch from NodeJS" + }, + "3.3.20": { + "en": "Watermeter cumulative energy support" + }, + "3.3.21": { + "en": "Enabled Wifi RSSI (Insights)" + }, + "3.3.22": { + "en": "Pairing prompt to enable LOCAL API and warning for watermeter that needs USB power" + }, + "3.3.23": { + "en": "Update images and manifest to match HomeWizard branding" + }, + "3.3.24": { + "en": "Icon color adjustment" + }, + "3.3.25": { + "en": "BUGFIX SD230 device set as solar showing negative values" + }, + "3.3.26": { + "en": "Watermeter support for Belgium" + }, + "3.4.0": { + "en": "First attempt plugin battery Homewizard APIv2" + }, + "3.4.1": { + "en": "Images update, temp svg icon for battery" + }, + "3.4.2": { + "en": "Bug fixes and addition of extra metrics that were missed for plugin battery." + }, + "3.4.3": { + "en": "Icon update, pair text in readme as workaround" + }, + "3.4.4": { + "en": "Energy socket naming improved (serial added), voltage support added." + }, + "3.5.0": { + "en": "Conversion to homey-compose" + }, + "3.5.1": { + "en": "Socket identification (push button, led blink)" + }, + "3.5.2": { + "en": "SDM630 clone to allow P1-like tracking with SDM630." + }, + "3.5.3": { + "en": "Improved pairing process P1 APIv2 (DCSBL)" + }, + "3.5.4": { + "en": "APIv2 pairing process P1 and Plugin Battery aligned" + }, + "3.5.5": { + "en": "Text fix during plugin battery pairing process" + }, + "3.6.0": { + "en": "Massive code rework (credits to DCSBL for time and effort)" + }, + "3.6.1": { + "en": "Homey Energy dashhboard: Energylink meter_gas capability added" + }, + "3.6.2": { + "en": "Text fix PIB localization and an attempt to resolve a APIv2 pairing timer timeout problem" + }, + "3.6.3": { + "en": "Polling interval Energy devices lowered to 1s" + }, + "3.6.4": { + "en": "Reverted socket interval back 10s as this has an increased load on some wifi networks and (older) homeys" + }, + "3.6.5": { + "en": "Adjusted P1 polling for Homey Early2019 models" + }, + "3.6.6": { + "en": "Reverted interval back 10s as this has an increased load on some wifi networks and (older) homeys (Early2019)" + }, + "3.6.7": { + "en": "Update code for custom polling for P1 and sockets (Default is back to 10s)" + }, + "3.6.8": { + "en": "Bug fixes polling timers that suddenly stopped" + }, + "3.6.9": { + "en": "P1(APIv2) bug fixes" + }, + "3.6.10": { + "en": "P1(apiv2) aggregated total usage added (support for PowerByTheHour app)" + }, + "3.6.11": { + "en": "Custom polling added for SDM230, SDM630 and the SDM630-p1 mode. Default is 10s." + }, + "3.6.12": { + "en": "kwh meter insights added, custom polling watermeter (user requests)" + }, + "3.6.13": { + "en": "Bugfix firmware version check P1apiv2 for action cards" + }, + "3.6.14": { + "en": "Additional logging Plugin Battery Mode get/set. Wifi metric added for P1 and Battery" + }, + "3.6.15": { + "en": "RSSI capability fix for P1 and PluginBattery. Also added imported/exported capability for Battery" + }, + "3.6.16": { + "en": "Custom polling plugin battery added, default 10s" + }, + "3.6.17": { + "en": "emoved version check for battery mode, using API query to verify if data is there, only then condition and action cards should show." + }, + "3.6.18": { + "en": "Bugfix P1(apiv2) showing as unresponsive due to battery getMode query error." + }, + "3.6.19": { + "en": "Attempt to get condition and action flow card error register sorted" + }, + "3.6.20": { + "en": "Daily usage imported power and gas (P1apiv1) - User request" + }, + "3.6.21": { + "en": "P1apiv2 tariff fix." + }, + "3.6.22": { + "en": "ReferenceError: body is not defined (fix)" + }, + "3.6.23": { + "en": "Temporary store pair token in settings for troubleshooting" + }, + "3.6.24": { + "en": "Moved temp setting to oninit" + }, + "3.6.25": { + "en": "this.log dump" + }, + "3.6.26": { + "en": "redo oninit token P1" + }, + "3.6.27": { + "en": "Recreated condition and action cards via homeycompose" + }, + "3.6.28": { + "en": "Adjusted args (included device), added filter (P1apiv2)" + }, + "3.6.29": { + "en": "Added 3 static action cards for battery mode " + }, + "3.6.30": { + "en": "Typo fix actioncard ActionCardFullChargeMode" + }, + "3.6.31": { + "en": "Added extra log if url and token are there upon action card call" + }, + "3.6.32": { + "en": "Restructerd oninit. Cards are now registered." + }, + "3.6.33": { + "en": "Condition card updated (PIB), added extra battery group information to P1apiv2" + }, + "3.6.34": { + "en": "Plugin Battery: added time_to_empty and time_to_full (minutes)" + }, + "3.6.35": { + "en": "Bugfix Energy - Invalid Capability: meter_gas.daily" + }, + "3.6.36": { + "en": "Code split for daily tracking gas and energy. (bug fix)" + }, + "3.6.37": { + "en": "Resolved async function call that should not have been async at all." + }, + "3.6.38": { + "en": "Added trigger for battery mode change" + }, + "3.6.39": { + "en": "Cloud connection setting made available for P1, Sockets, Watermeter, SDM230, SDM630" + }, + "3.6.40": { + "en": "Offset watermeter fix" + }, + "3.6.41": { + "en": "Phase capacity added, adjust setting for 1 or 3 phases and their capacity in Amps" + }, + "3.6.42": { + "en": "Bugfix : ReferenceError: temp_current_phase2_load is not defined" + }, + "3.6.43": { + "en": "Bugfix for sliders when gridconnection has 3 phases" + }, + "3.6.44": { + "en": "Actual gas meter measurement added (5min poll pending on smartmeter)" + }, + "3.6.45": { + "en": "P1apiv1 code refactored" + }, + "3.6.46": { + "en": "Bugfix: batteryCapacityWh is not defined" + }, + "3.6.47": { + "en": "Extra plugin battery trigger cards (state change, time to full, time to empty)" + }, + "3.6.48": { + "en": "Bugfix: time_to_empty is not defined" + }, + "3.6.49": { + "en": "Removed sliders in GUI for P1 phases" + }, + "3.6.50": { + "en": "Removed sliders from Homeycompose" + }, + "3.6.51": { + "en": "Bugfix: added hasCapability test to avoid error upon untest removal capability" + }, + "3.6.52": { + "en": "Firmware 12.5.2RC3 issue with removal or hascapabilty checks. " + }, + "3.6.53": { + "en": "Attempt to fix reported user problem after firmware 12.5.2" + }, + "3.6.54": { + "en": "Another attempt to resolve user reported problem" + }, + "3.6.55": { + "en": "Typo fix for 3rd phase removal slider. Many const - let replacements in code (attempt to lower memory footprint)" + }, + "3.6.57": { + "en": "Code cleanup, Energyv2 tweak, Energy socket Battery tracking (imported/exported energy for dashboard)" + }, + "3.6.58": { + "en": "Const / let assignment error for plugin battery (fix)" + }, + "3.6.59": { + "en": "SDM230 (p1 mode), Daily usage kwh for APIv2 P1, Adjustment P1 for Norway specific P1's" + }, + "3.6.60": { + "en": "HTTP - keepalive agent added to P1, sockets, APIv2 devices" + }, + "3.6.61": { + "en": "Increase keepAlive default time from 1000ms to 15000 or 35000ms as polling cycle is more than 1sec." + }, + "3.6.62": { + "en": "AbortController added (apiv2) and Wifi quality capability" + }, + "3.6.63": { + "en": "Bugfix: P1, missed setAvailable(). Code didn’t recover from a failed P1 connection and kept P1 offline" + }, + "3.6.64": { + "en": "Fallback url for mDNS problems. Homewizard Legacy devices removed retry code, changed to keepAlive agent mode" + }, + "3.6.65": { + "en": "Battery Group data removed from P1 after a fetch fail (bugfix)" + }, + "3.6.66": { + "en": "APIv2 increased timeout authorization, Language update notification P1 warning phase overload" + }, + "3.6.67": { + "en": "Enforcing interval clears on various devices when interval is reset" + }, + "3.6.68": { + "en": "Finetuning polling and capability during init phase of various drivers" + }, + "3.6.69": { + "en": "Added more logging for diagnostic reports" + }, + "3.6.70": { + "en": "Bugfix SDM230 solar parameter was undefined" + }, + "3.6.71": { + "en": "Added an estimate charge available in plugin battery value" + }, + "3.6.72": { + "en": "More try/catch code to avoid any crashes on Homewizard Legacy main unit getStatus fail (Device not found)" + }, + "3.6.73": { + "en": "Code fixes: unhandledRejections CloudOn/Off for sockets and P1" + }, + "3.6.74": { + "en": "Homewizard Legacy - Thermometer recode, main unit adjustment (promises), finetuning keepAlive for other devices" + }, + "3.6.75": { + "en": "Added verbose mDNS discovery results for troubleshooting (apiv1)" + }, + "3.6.76": { + "en": "Custom polling-interval option made available for Homewizard Legacy main unit" + }, + "3.6.77": { + "en": "Fallback url for P1 mode SDM230 / SDM630" + }, + "3.6.78": { + "en": "Realtime data for P1 (apiv2) via Websocket" + }, + "3.6.79": { + "en": "Realtime data for Plugin Battery via Websocket / Bugfix P1apiv2 when gas is null" + }, + "3.6.80": { + "en": "Gas reading fix, websocket logic (reconnect) added" + }, + "3.6.81": { + "en": "Finetuning gas data via websockets" + }, + "3.6.82": { + "en": "Plugin Battery group fix (tracking combined set of batteries) - bugfix / Refenece error" + }, + "3.6.83": { + "en": "Various bugfixes (websocket) and added netfrequency capability to Plugin Battery and trigger for out of range" + }, + "3.6.84": { + "en": "Bugfix - WebSocket was closed before the connection was established" + }, + "3.6.85": { + "en": "Homewizard Legacy - code rollback (pairing problems after improvements)" + }, + "3.7.0": { + "en": "P1 (apiv2) - Added checkbox setting to fallback to polling if websocket is to heavy for Homey device" + }, + "3.7.1": { + "en": "Version bump to refresh package upload and potentially fix non working app comments on forum." + }, + "3.7.2": { + "en": "Extra check upon websocket creation to avoid crashes" + }, + "3.7.3": { + "en": "Plugin battery catch all error (unhandled exception)" + }, + "3.7.4": { + "en": "Additional checking and error handling on bad wifi connections (websocket based)" + }, + "3.7.5": { + "en": "Version bump (unknown install problem)" + }, + "3.7.6": { + "en": "Syntax error in P1 (websocket)" + }, + "3.7.7": { + "en": "Fetch was not defined for fetchWithTimeout" + }, + "3.7.8": { + "en": "net_frequence fix (3 decimals)" + }, + "3.7.9": { + "en": "Capability update fix (avoid removal check)" + }, + "3.8.0": { + "en": "Removed node-fetch for Homey 12.9.x (nodejs v22 - native fetch). Moved websocket functions to include to clean up P1 and plugin_battery code." + }, + "3.8.3": { + "en": "Force Homey firmware 12.9.0 version check" + }, + "3.8.4": { + "en": "Conditional require (node-fetch) it will try to use native fetch with a fallback to take the node-fetch module instead" + }, + "3.8.5": { + "en": "global.fetch not working for nodejs v12, adjusted code to cover this" + }, + "3.8.6": { + "en": "Fetch attempt" + }, + "3.8.7": { + "en": "After attempting conditional fetch, roll back to node-fetch until 12.9.x releases (Homey Pro 2016 - 2019)" + }, + "3.8.8": { + "en": "Bugfix: SDM230-p1mode - error during initialization" + }, + "3.8.9": { + "en": "Roll back energy dongle code v3.7.0" + }, + "3.8.10": { + "en": "Strange SD630 problem on older Homey's" + }, + "3.8.11": { + "en": "Extra verbose logging in urls to expose mDNS problems for older Homeys (url)" + }, + "3.8.12": { + "en": "Extra error handling (updateCapability) based on received crashreports" + }, + "3.8.13": { + "en": "Bugfix: ReferenceError: err is not defined (energy_socket)" + }, + "3.8.14": { + "en": "Updated APIv2 to add more text upon fetch failed" + }, + "3.8.15": { + "en": "Websocket based battery mode settings added (both condition and action)" + }, + "3.8.16": { + "en": "Websocket enhancements (heartbeat for plugin battery), P1 and Energy Socket agent tuning (ETIMEOUT and ECONNRESET)" + }, + "3.8.17": { + "en": "Bugfix: Failed to recreate agent: TypeError: Assignment to constant variable (energy)" + }, + "3.8.18": { + "en": "Adjustment to async/await code several drivers" + }, + "3.8.19": { + "en": "Websocket finetuning (energy_v2 and Plugin Battery), Centralized fetch queue for all fetch calls. Removed internal interval check" + }, + "3.8.20": { + "en": "WebsocketManager tuning" + }, + "3.8.21": { + "en": "Restore custom polling sockets (got removed by accident rollback)" + }, + "3.8.22": { + "en": "Additional watchdog code to reconnect energy_v2 and plugin_battery upon firmware up/downgrades" + }, + "3.9.0": { + "en": "New Plugin Battery mode support (zero_charge_only & zero_discharge_only) - dynamic tariff capabilities" + }, + "3.9.1": { + "en": "Checkbox show gasmeter (P1, apiv1 and apiv2). Belgium datapoint (avarage_power_15m_w) - P1apiv2" + }, + "3.9.2": { + "en": "Plugin Battery - Bugfix setMode for to_full (PUT)" + }, + "3.9.3": { + "en": "Bugfix - Updated P1apiv2 check-battery-mode condition card" + }, + "3.9.4": { + "en": "Backward compatibilty fix for the new battery mode applied to older P1 firmware." + }, + "3.9.5": { + "en": "Bugfix - Websocket payload battery mode adjustment" + }, + "3.9.6": { + "en": "Bugfixes condition and action cards for battery mode and permissions" + }, + "3.9.7": { + "en": "Fixed: rare crash when _handleBatteries() ran after a device was deleted" + }, + "3.9.8": { + "en": "New Feature: Baseload (sluipverbruik) detection (experimental)" + }, + "3.9.9": { + "en": "Bugfix: energy_socket connection_error capability fix" + }, + "3.9.10": { + "en": "Multiple bugfixes (energy_socket, energy_v2, sdm230_v2, pair token with new SHS" + }, + "3.9.11": { + "en": "Bugfix: APIv2 pairing" + }, + "3.9.12": { + "en": "Rollback random username to something easy" + }, + "3.9.13": { + "en": "Bugfix: APIv2 pairing -> local/homey_xxxxxx" + }, + "3.9.14": { + "en": "Bugfix: SDM630v2 trigger cards removed (obsolete as these are default Homey)" + }, + "3.9.15": { + "en": "Finetune: P1(apiv2) websocket + polling, capability updates" + }, + "3.9.16": { + "en": "Refractor code update for P1apiv1, SDM230, SDM630, watermeter. And Customizable phase overload warning + reset marker." + }, + "3.9.17": { + "en": "Phase 1 / 3 fix for P1(apiv1) after refractor code update" + }, + "3.9.18": { + "en": "Bugfix: Fallback url for SDM230v2 and P1apiv2 (mDNS fail workaround)" + }, + "3.9.19": { + "en": "Bugfix: pairing problem \"Cannot read properties of undefined (reading 'log')" + }, + "3.9.20": { + "en": "Bugfix: pairing problem apiv1 and apiv2" + }, + "3.9.21": { + "en": "Wsmanager optimize, custom polling Homewizard Legacy and fix Driver.js (this.log)" + }, + "3.9.22": { + "en": "Thermometer rollback (name index matching doesnt work as expected)" + }, + "3.9.23": { + "en": "Homewizard legacy -> node-fetch and not the fetchQueue utility (bad user experience feedback)" + }, + "3.9.24": { + "en": "Baseload (sluipverbruik) improvement (fridge/freezer should not be flagged as invalid )" + }, + "3.9.25": { + "en": "Homewizard app setting page with log or debug information for discovery, fetch failures, websocket problems and baseload samples" + }, + "3.9.26": { + "en": "Bugfix: Homewizard.poll (legacy unit)" + }, + "3.9.27": { + "en": "Homewizard Preset addition, Heatlink control improvement, Gasmeter fix (external source)" + }, + "3.9.28": { + "en": "Thermometer trigger and condition cards for no response X hours." + }, + "3.9.29": { + "en": "Improvement fetchQueue (protect against high cpu warning for devices on 1s polling)" + }, + "3.10.0": { + "en": "fetchQueue method dropped, using fetch directly so debug information can be shown in app settings tabs" + }, + "3.10.1": { + "en": "Watermeter daily usage added, Bugfix: Device Fetch Debug wasn't updating" + }, + "3.10.2": { + "en": "Bugfix: Circular Reference \"device\"" + }, + "3.10.3": { + "en": "Bugfix: SDM230(p1mode) - updateCapability missed" + }, + "3.10.4": { + "en": "Another Circular Reference \"device\" error fix" + }, + "3.10.5": { + "en": "Removed all retry/timeout code for fetch as it seems to lock up devices" + }, + "3.10.6": { + "en": "Cleanup device drivers with overcomplicated checks that ended up with polling deadlocks" + }, + "3.10.7": { + "en": "SDM230(p1mode) - Extra code handling for TIMEOUT issues" + }, + "3.10.8": { + "en": "SDM630 added per phase kwh meter tracking + daily kwh meter (estimate)" + }, + "3.10.9": { + "en": "More gas fix reset at night time (apiv1 and apiv2)" + }, + "3.10.10": { + "en": "Bugfix: incorrect daily reset during day of gas usage" + }, + "3.10.11": { + "en": "Cleanup first_run_logged key" + }, + "3.10.12": { + "en": "Bugfix: Energylink (watermeter) and Thermometer (battery)" + }, + "3.10.13": { + "en": "Rollback Daily gas reset for both P1 apiv1 and apiv2" + }, + "3.10.14": { + "en": "Energy P1 changed to modular code" + }, + "3.11.0": { + "en": "Modular code for P1 (both versions) & Heatlink set target_temperature fallback check" + }, + "3.11.1": { + "en": "P1, changed order of processing, eletric first then gas/water" + }, + "3.11.2": { + "en": "P1 energy tuning" + }, + "3.11.3": { + "en": "P1 missed call in onPoll interval to reset daily calculation" + }, + "3.11.4": { + "en": "Bugfix: P1 (apiv2) polling mode - Charge mode fixes & Extra log information on Group Battery State of Charge" + }, + "3.11.5": { + "en": "Bugfix: Group Battery State of Charge (increased timestamp check)" + }, + "3.11.6": { + "en": "Troubleshooting user plugin battery group" + }, + "3.11.7": { + "en": "Fallback Plugin battery Soc (api fetch)" + }, + "3.11.8": { + "en": "Fetch based group soc state" + }, + "3.11.9": { + "en": "Realtime pull from all batteries as fallback Battery Group State" + }, + "3.12.0": { + "en": "Baseload ignore return power. Plugin battery LED bridghtness. Websocket & cache improvements. " + }, + "3.12.1": { + "en": "Bug fix: Battery Group (SoC missed when there are fetch errors)" + }, + "3.12.2": { + "en": "Bug fix: Polling deadlock fix for (energy, energy_socket, SDM230, SDM630, watermeter)" + }, + "3.12.3": { + "en": "setAvailable fix for energy_sockets" + }, + "3.12.4": { + "en": "Bugfix: _cacheSet undefined" + }, + "3.12.5": { + "en": "P1 tuning for TIMEOUT and Unreachable problem" + }, + "3.12.6": { + "en": "Extra P1 logging" + }, + "3.12.7": { + "en": "Removed pollingActive check, unwanted side effect" + }, + "3.12.8": { + "en": "Plugin battery charge mode now selectable from UI" + }, + "3.12.9": { + "en": "Energy(apiv2) guard for add / remove \"battery_group_charge_mode\"" + }, + "3.13.0": { + "en": "Watermeter battery mode support (via hwenergy cloud)" + }, + "3.13.1": { + "en": "Update platform settings cloud and local" + }, + "3.13.2": { + "en": "platform update cloud and local" + }, + "3.13.3": { + "en": "Platform update cloud and local" + }, + "3.13.4": { + "en": "Process error account not allowed to publish cloud app" + }, + "3.13.5": { + "en": "Cloud_p1 (experimental release) and tariff trigger fix (energy_v2)" + }, + "3.13.6": { + "en": "Bugfix: capability_already_exists (cloud_p1)" + }, + "3.13.7": { + "en": "Addtional check on capability_already_exists error" + }, + "3.13.8": { + "en": "Plugin Battery state of charge icon added as tile for dashboard" + }, + "3.13.9": { + "en": "Battery-policy (virtual driver) - Experimental" + }, + "3.13.10": { + "en": "Battery Policy - dynamic mode fine tuning" + }, + "3.13.11": { + "en": "Bug fixes: Battery Policy, energy, energy_v2 and cloud_watermeter" + }, + "3.13.12": { + "en": "Bugfixes and a PV option to estimate what your PV system produce without linking directly" + }, + "3.13.13": { + "en": "Flowcard added to update any realtime PV production" + }, + "3.13.14": { + "en": "User request, grid outage, sag and swells trigger cards added" + }, + "3.13.15": { + "en": "Added restore triggercards for power outage, sags and swells" + }, + "3.13.17": { + "en": "Repair button for P1 dongle, Baseload improvement, dynamic price fallback" + }, + "3.13.18": { + "en": "Homewizard Legacy pairing problem (user report)" + }, + "3.13.19": { + "en": "Tariff adjustments and RTE battery forced in calculations" + }, + "3.13.20": { + "en": "Remapping states and icons on planning page" + }, + "3.13.21": { + "en": "RTE efficiency added to improve cheap and expensive profit gain calculations" + }, + "3.13.22": { + "en": "Improved dynamic price margins" + }, + "3.13.23": { + "en": "Tuning precharge before peak time only when it is profitable" + }, + "3.13.24": { + "en": "Planning fixes on icons and actions" + }, + "3.13.25": { + "en": "Planning history will not change back to standby, improved cheaphours and calculation with solar adjustments" + }, + "3.13.26": { + "en": "New tuning after very cloudy day with odd planning outcome" + }, + "3.13.27": { + "en": "Bugfix onSettings Battery Policy and some cleanup policy mappings" + }, + "3.13.28": { + "en": "Planning update when user changes settings for min and max price thresholds" + }, + "3.13.29": { + "en": "15min planning and custom setting to respect min max prices or allow a little deviation" + }, + "3.13.30": { + "en": "Extra settings for dynamic mode for future 2027 energy contracts" + }, + "3.13.32": { + "en": "Remove lastBatteryAt in energy_v2" + }, + "3.13.33": { + "en": "Websocket rewrite with TLS" + }, + "3.13.34": { + "en": "Websocket adjusments, circuitbreaker, backoff on failure attempts, TLS IP fix, Connection throttle, hard timeouts" + }, + "3.13.35": { + "en": "Bugfix energy driver connection_error must be a string" + }, + "3.13.36": { + "en": "Websocket prefetch problem when devices are unreachable" + }, + "3.13.37": { + "en": "Fixed incorrect grid charging during active PV production" + }, + "3.13.38": { + "en": "Fix race condition in WebSocket cleanup causing null reference crashes" + }, + "3.13.39": { + "en": "Cleanup timers upon restart, throttled websocket updates to 3s" + }, + "3.13.40": { + "en": "More verbose logging in websocket" + }, + "3.13.41": { + "en": "Troubleshooting user crashes" + }, + "3.13.42": { + "en": "Random startup delay to avoid start cpu spikes" + }, + "3.13.43": { + "en": "P1 flow triggers reduction" + }, + "3.13.44": { + "en": "Error in battery policy" + }, + "3.13.45": { + "en": "battery-policy cleanup after removal checks" + }, + "3.13.47": { + "en": "Websocket throttle, energy_v2 tiered updates, flow triggers rate limited, Energy socket startup offset, Battery Policy P1 polling increased from 5s to 15s" + }, + "3.13.48": { + "en": "Battery Policy tuning and Websocket cleared from some timers" + }, + "3.13.49": { + "en": "Websocket refractored, debug and statistics now in settings page" + }, + "3.13.50": { + "en": "Memory limit fixes" + }, + "3.13.51": { + "en": "Memory limit fixes" + }, + "3.13.52": { + "en": "Negative baseload fix, RTE learing fix, Websocket slow_handler threshold adjustment" + }, + "3.13.53": { + "en": "Price fix on planning" + }, + "3.13.54": { + "en": "Charge improvement, poor sun (ie fog) and cheap prices" + }, + "3.13.55": { + "en": "User report on battery policy not discharging while comments suggested it should" + }, + "3.13.56": { + "en": "Explain summary text modified to active state and modified action during high prices." + }, + "3.13.57": { + "en": "Fix on to_full when PV is not enough to charge during cheap prices" + }, + "3.13.58": { + "en": "Ratelimit on battery mode action cards, energysocket retry logic and longer timeouts" + }, + "3.13.59": { + "en": "Fix incorrect pv charge during the night" + }, + "3.13.60": { + "en": "Energyv2 cpu fix and websocket tuning" + }, + "3.13.61": { + "en": "Websocket tuning for both energy_v2 and plugin batteyr" + }, + "3.13.62": { + "en": "Energy_v2 websocket throttle is customizable. Default 2s but can be adjusted in settings" + }, + "3.13.63": { + "en": "Bugfix planning, still showed solar charge in the evening-night" + }, + "3.13.64": { + "en": "Improved websocket log and tracking to detect issues" + }, + "3.13.65": { + "en": "Change explainability for battery policy with tags and weight" + }, + "3.13.68": { + "en": "OptimizationEngine added which computes the optimal charge and discharge schedule across a full 24hours" + }, + "3.13.69": { + "en": "Bugfixes in optimization and battery-policy" + }, + "3.13.70": { + "en": "Color code style aligned with Homewizard colors and fixed Homewizard Legacy debug fetch tab data" + }, + "3.13.71": { + "en": "Added PV charge reasons to explainability view" + }, + "3.14.0": { + "en": "Battery Policy now uses 15min prices, weather forecast 36h, Radiation-based SoC in planning, improved reason" + }, + "3.14.1": { + "en": "Sun radiation fixes, cloud coverage. Dim led workaround percentage" + }, + "3.14.2": { + "en": "Added estimate Solar graph in planner. Energy-socket interval changed back" + }, + "3.14.3": { + "en": "Azitmuth and angle added for better PV graph plotting" + }, + "3.14.4": { + "en": "Expected kW per hour added in planning" + }, + "3.14.5": { + "en": "Various bugfixes" + }, + "3.14.6": { + "en": "Improved fetch statistics and added dynamic interval when user has over 20 energy-sockets making app crash" + }, + "3.14.7": { + "en": "Performance tracking energy-socket fetch calls" + }, + "3.14.8": { + "en": "Weather and solar improvement estimation on kWh, centralized fetchWithTimeout" + }, + "3.14.9": { + "en": "Plugin battery fetch connection pool fix, energy-socket pairing fallback + timeout and watermeter fix." + }, + "3.14.10": { + "en": "Crash hunting added logging" + }, + "3.14.11": { + "en": "http agent adjustment" + }, + "3.14.12": { + "en": "Bug hunting" + }, + "3.14.13": { + "en": "Pairing sockets limited to 4 concurrent additions" + }, + "3.14.14": { + "en": "Only 2 connections allowed at pairing" + }, + "3.14.15": { + "en": "Ignore already paired devices" + }, + "3.14.16": { + "en": "Battery policy improvements, Soc projection, Solar yield learning, Weekend/weekday consumption patterns, consumption awareness, battery cycle costs" + }, + "3.14.17": { + "en": "Bugfix planning grid charging while solar is forecasted later" + }, + "3.14.18": { + "en": "Baseload fix with plugin batteries that zero the overall usage, watts should be seen as baseload" + }, + "3.14.19": { + "en": "Baseload fix with plugin battery and heap memory fix at startup, processes postponed for later use rather all update upon starting app" + }, + "3.14.20": { + "en": "Extra logging baseload detection with batteries" + }, + "3.14.21": { + "en": "Tuned log rules to only night summary" + }, + "3.14.22": { + "en": "Baseload fix for energy_v2 (notification tag added), PV Graph planning adjusted, added green line with actual Watts production vs estimate" + }, + "3.14.23": { + "en": "Baseload fix (oscillation), Changed city looking for weather to long lat method instead" + }, + "3.14.24": { + "en": "Cheap tariff fix when battery is not full but pv cant cover it" + } +} diff --git a/.homeycompose/app.json b/.homeycompose/app.json new file mode 100644 index 00000000..1f1c32e9 --- /dev/null +++ b/.homeycompose/app.json @@ -0,0 +1,74 @@ +{ + "id": "com.homewizard", + "name": { + "en": "HomeWizard" + }, + "version": "3.14.24", + "platforms": [ + "local" + ], + "sdk": 3, + "brandColor": "#2fc052", + "compatibility": ">=12.9.0", + "description": { + "en": "Helps you understand and save" + }, + "category": [ + "energy", + "appliances", + "climate" + ], + "images": { + "xlarge": "assets/images/xlarge.png", + "large": "assets/images/large.png", + "small": "assets/images/small.png" + }, + "author": { + "name": "Jeroen Tebbens", + "email": "jeroen@tebbens.net" + }, + "contributors": { + "developers": [ + { + "name": "Jeroen Bos", + "email": "jeroenbos22@gmail.com" + }, + { + "name": "Nick Bockmeulen", + "email": "git@bockmeulen.nl" + }, + { + "name": "Jeroen Tebbens", + "email": "jeroen@tebbens.net" + }, + { + "name": "Freddie Welvering", + "email": "freddie@welvering.eu" + }, + { + "name": "Emile Nijssen", + "email": "emile@emilenijssen.nl" + }, + { + "name": "Dennie de Groot", + "email": "mail@denniedegroot.nl" + } + ] + }, + "contributing": { + "donate": { + "paypal": { + "username": "jtebbens" + } + } + }, + "bugs": { + "url": "https://community.homey.app/t/app-pro-homewizard/19267" + }, + "source": "https://github.com/jtebbens/com.homewizard", + "homeyCommunityTopicId": 19267, + "support": "https://community.homey.app/t/app-pro-homewizard/19267", + "permissions": [ + "homey:manager:ledring" + ] +} \ No newline at end of file diff --git a/.homeycompose/capabilities/active_mode.json b/.homeycompose/capabilities/active_mode.json new file mode 100644 index 00000000..0970ce4c --- /dev/null +++ b/.homeycompose/capabilities/active_mode.json @@ -0,0 +1,15 @@ +{ + "type": "string", + "title": { + "en": "Active Mode", + "nl": "Actieve Modus" + }, + "desc": { + "en": "The battery mode that is currently active on the hardware. May differ from the recommended mode when the confidence score is below the threshold, auto-apply is off, or a manual override is active.", + "nl": "De batterijmodus die momenteel actief is op de hardware. Kan afwijken van de aanbevolen modus als de betrouwbaarheidsscore onder de drempel ligt, automatisch toepassen uitstaat, of een handmatige override actief is." + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} diff --git a/.homeycompose/capabilities/auto_apply.json b/.homeycompose/capabilities/auto_apply.json new file mode 100644 index 00000000..d054abca --- /dev/null +++ b/.homeycompose/capabilities/auto_apply.json @@ -0,0 +1,15 @@ +{ + "type": "boolean", + "title": { + "en": "Auto-Apply", + "nl": "Automatisch Toepassen" + }, + "desc": { + "en": "Automatically apply policy recommendations to battery", + "nl": "Pas beleidsaanbevelingen automatisch toe op batterij" + }, + "getable": true, + "setable": true, + "uiComponent": "toggle", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/battery_group_average_soc.json b/.homeycompose/capabilities/battery_group_average_soc.json new file mode 100644 index 00000000..792611ca --- /dev/null +++ b/.homeycompose/capabilities/battery_group_average_soc.json @@ -0,0 +1,16 @@ +{ + "type": "number", + "title": { + "en": "Battery Group Average SoC", + "nl": "Batterij Groep Lading" + }, + "getable": true, + "setable": false, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/battery.svg", + "units": { + "en": "%", + "nl": "%" + } +} diff --git a/.homeycompose/capabilities/battery_group_charge_mode.json b/.homeycompose/capabilities/battery_group_charge_mode.json new file mode 100644 index 00000000..75fc56bc --- /dev/null +++ b/.homeycompose/capabilities/battery_group_charge_mode.json @@ -0,0 +1,49 @@ +{ + "type": "enum", + "title": { + "en": "Battery Group Charge Mode", + "nl": "Battery Groep Oplaadmodus" + }, + "getable": true, + "setable": true, + "uiComponent": "picker", + "insights": true, + "icon": "assets/battery.svg", + "values": [ + { + "id": "zero", + "title": { + "en": "Zero (Net Zero)", + "nl": "Nul op de meter" + } + }, + { + "id": "zero_charge_only", + "title": { + "en": "Zero – Charge Only", + "nl": "NOM – Alleen laden" + } + }, + { + "id": "zero_discharge_only", + "title": { + "en": "Zero – Discharge Only", + "nl": "NOM – Alleen ontladen" + } + }, + { + "id": "standby", + "title": { + "en": "Standby", + "nl": "Stand‑by" + } + }, + { + "id": "to_full", + "title": { + "en": "Full Charge", + "nl": "Volledig laden" + } + } + ] +} diff --git a/.homeycompose/capabilities/battery_group_state.json b/.homeycompose/capabilities/battery_group_state.json new file mode 100644 index 00000000..527abe3c --- /dev/null +++ b/.homeycompose/capabilities/battery_group_state.json @@ -0,0 +1,12 @@ +{ + "type": "string", + "title": { + "en": "Battery Group Charge state", + "nl": "Battery Groep Laadt status" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/battery.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/battery_group_total_capacity_kwh.json b/.homeycompose/capabilities/battery_group_total_capacity_kwh.json new file mode 100644 index 00000000..1dd62f96 --- /dev/null +++ b/.homeycompose/capabilities/battery_group_total_capacity_kwh.json @@ -0,0 +1,14 @@ +{ + "type": "number", + "title": { + "en": "Battery Group Total Capacity" + }, + "units": { + "en": "kWh" + }, + "getable": true, + "setable": false, + "insights": false, + "icon": "assets/battery.svg", + "uiComponent": "sensor" +} diff --git a/.homeycompose/capabilities/battery_rte.json b/.homeycompose/capabilities/battery_rte.json new file mode 100644 index 00000000..8838436e --- /dev/null +++ b/.homeycompose/capabilities/battery_rte.json @@ -0,0 +1,22 @@ +{ + "type": "number", + "title": { + "en": "Battery RTE", + "nl": "Batterij RTE" + }, + "desc": { + "en": "Learned round-trip efficiency of battery (0-100%)", + "nl": "Geleerde round-trip efficiency van batterij (0-100%)" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 1, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} diff --git a/.homeycompose/capabilities/battery_soc_mirror.json b/.homeycompose/capabilities/battery_soc_mirror.json new file mode 100644 index 00000000..3f2d101d --- /dev/null +++ b/.homeycompose/capabilities/battery_soc_mirror.json @@ -0,0 +1,22 @@ +{ + "type": "number", + "title": { + "en": "Battery Level", + "nl": "Batterijniveau" + }, + "desc": { + "en": "Current battery state of charge", + "nl": "Huidige batterij laadstatus" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/central_heating_flame.json b/.homeycompose/capabilities/central_heating_flame.json new file mode 100644 index 00000000..d0445376 --- /dev/null +++ b/.homeycompose/capabilities/central_heating_flame.json @@ -0,0 +1,12 @@ +{ + "type": "boolean", + "title": { + "en": "Central Heating Burner", + "nl": "CV brander" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/flame.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/central_heating_pump.json b/.homeycompose/capabilities/central_heating_pump.json new file mode 100644 index 00000000..bdeb6467 --- /dev/null +++ b/.homeycompose/capabilities/central_heating_pump.json @@ -0,0 +1,12 @@ +{ + "type": "boolean", + "title": { + "en": "Central Heating", + "nl": "Central Verwarming" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/central_heating.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/confidence_score.json b/.homeycompose/capabilities/confidence_score.json new file mode 100644 index 00000000..8e3823d3 --- /dev/null +++ b/.homeycompose/capabilities/confidence_score.json @@ -0,0 +1,22 @@ +{ + "type": "number", + "title": { + "en": "Confidence", + "nl": "Vertrouwen" + }, + "desc": { + "en": "Confidence in current recommendation (0-100%)", + "nl": "Vertrouwen in huidige aanbeveling (0-100%)" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/connection_error.json b/.homeycompose/capabilities/connection_error.json new file mode 100644 index 00000000..dac385dd --- /dev/null +++ b/.homeycompose/capabilities/connection_error.json @@ -0,0 +1,11 @@ +{ + "type": "string", + "title": { + "en": "Connection Error", + "nl": "Verbindingsfout" + }, + "getable": true, + "setable": false, + "insights": true, + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/cycles.json b/.homeycompose/capabilities/cycles.json new file mode 100644 index 00000000..bbb7696f --- /dev/null +++ b/.homeycompose/capabilities/cycles.json @@ -0,0 +1,16 @@ +{ + "type": "number", + "title": { + "en": "Number of battery cycles", + "nl": "Aantal battery laadcycli" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/cycles.svg", + "units": { + "en": "cycles", + "nl": "cycli" + } +} \ No newline at end of file diff --git a/.homeycompose/capabilities/estimate_kwh.json b/.homeycompose/capabilities/estimate_kwh.json new file mode 100644 index 00000000..6928543e --- /dev/null +++ b/.homeycompose/capabilities/estimate_kwh.json @@ -0,0 +1,17 @@ +{ +"type": "number", + "title": { + "en": "Estimate kWh in battery", + "nl": "Geschatte kWh in batterij" + }, + "getable": true, + "setable": true, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/tariff.svg", + "units": { + "en": "kWh", + "nl": "kWh" + } +} + \ No newline at end of file diff --git a/.homeycompose/capabilities/explanation_summary.json b/.homeycompose/capabilities/explanation_summary.json new file mode 100644 index 00000000..5e04cbbe --- /dev/null +++ b/.homeycompose/capabilities/explanation_summary.json @@ -0,0 +1,15 @@ +{ + "type": "string", + "title": { + "en": "Decision Reason", + "nl": "Beslissingsreden" + }, + "desc": { + "en": "Explanation of current policy decision", + "nl": "Uitleg van huidige beleidsbeslissing" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/grid_power_mirror.json b/.homeycompose/capabilities/grid_power_mirror.json new file mode 100644 index 00000000..ae558b89 --- /dev/null +++ b/.homeycompose/capabilities/grid_power_mirror.json @@ -0,0 +1,20 @@ +{ + "type": "number", + "title": { + "en": "Grid Power", + "nl": "Netvermogen" + }, + "desc": { + "en": "Current grid import/export power", + "nl": "Huidig net import/export vermogen" + }, + "units": { + "en": "W", + "nl": "W" + }, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/identify.json b/.homeycompose/capabilities/identify.json new file mode 100644 index 00000000..1274398e --- /dev/null +++ b/.homeycompose/capabilities/identify.json @@ -0,0 +1,12 @@ +{ + "type": "boolean", + "title": { + "en": "Identify", + "nl": "Identificeren" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "insights": false, + "icon": "assets/magnify.svg" +} diff --git a/.homeycompose/capabilities/last_update.json b/.homeycompose/capabilities/last_update.json new file mode 100644 index 00000000..6b827614 --- /dev/null +++ b/.homeycompose/capabilities/last_update.json @@ -0,0 +1,15 @@ +{ + "type": "string", + "title": { + "en": "Last Update", + "nl": "Laatste Update" + }, + "desc": { + "en": "Timestamp of last policy check", + "nl": "Tijdstempel van laatste beleidscontrole" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/led_brightness_pct.json b/.homeycompose/capabilities/led_brightness_pct.json new file mode 100644 index 00000000..0f8f93ab --- /dev/null +++ b/.homeycompose/capabilities/led_brightness_pct.json @@ -0,0 +1,18 @@ +{ + "type": "number", + "title": { + "en": "LED Brightness", + "nl": "LED Helderheid" + }, + "getable": true, + "setable": true, + "uiComponent": "sensor", + "insights": false, + "min": 0, + "max": 100, + "step": 1, + "units": { + "en": "%", + "nl": "%" + } +} diff --git a/.homeycompose/capabilities/long_power_fail_count.json b/.homeycompose/capabilities/long_power_fail_count.json new file mode 100644 index 00000000..b6101a73 --- /dev/null +++ b/.homeycompose/capabilities/long_power_fail_count.json @@ -0,0 +1,12 @@ +{ + "type": "number", + "title": { + "en": "Power failures", + "nl": "Stroomstoringen" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/measure_gas.json b/.homeycompose/capabilities/measure_gas.json new file mode 100644 index 00000000..4a7ef24a --- /dev/null +++ b/.homeycompose/capabilities/measure_gas.json @@ -0,0 +1,17 @@ +{ + "type": "number", + "title": { + "en": "Current gas usage", + "nl": "Huidig gasverbruik" + }, + "getable": true, + "setable": false, + "decimals": 3, + "uiComponent": "sensor", + "insights": true, + "units": { + "en": "m3", + "nl": "m3" + }, + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/measure_soc.json b/.homeycompose/capabilities/measure_soc.json new file mode 100644 index 00000000..f40c2d95 --- /dev/null +++ b/.homeycompose/capabilities/measure_soc.json @@ -0,0 +1,22 @@ +{ + "type": "number", + "title": { + "en": "Battery Level", + "nl": "Batterijniveau" + }, + "desc": { + "en": "Current battery state of charge", + "nl": "Huidige batterij laadstatus" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "battery", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/net_load_phase1.json b/.homeycompose/capabilities/net_load_phase1.json new file mode 100644 index 00000000..5e016cf5 --- /dev/null +++ b/.homeycompose/capabilities/net_load_phase1.json @@ -0,0 +1,29 @@ +{ + "type": "number", + "title": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "titleFormatted": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "desc" : { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "min": 0, + "max": 100, + "step": 1, + "decimals": 0, + "chartType": "stepLine", + "getable": true, + "setable": false, + "uiComponent": "slider", + "insights": true, + "units": { + "en": "A", + "nl": "A" + }, + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/net_load_phase1_pct.json b/.homeycompose/capabilities/net_load_phase1_pct.json new file mode 100644 index 00000000..a1fef216 --- /dev/null +++ b/.homeycompose/capabilities/net_load_phase1_pct.json @@ -0,0 +1,25 @@ +{ + "type": "number", + "title": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "titleFormatted": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "desc" : { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "units": { + "en": "%", + "nl": "%" + }, + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/net_load_phase2.json b/.homeycompose/capabilities/net_load_phase2.json new file mode 100644 index 00000000..71f18c3c --- /dev/null +++ b/.homeycompose/capabilities/net_load_phase2.json @@ -0,0 +1,29 @@ +{ + "type": "number", + "title": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "titleFormatted": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "desc" : { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "min": 0, + "max": 100, + "step": 1, + "decimals": 0, + "chartType": "stepLine", + "getable": true, + "setable": false, + "uiComponent": "slider", + "insights": true, + "units": { + "en": "A", + "nl": "A" + }, + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/net_load_phase2_pct.json b/.homeycompose/capabilities/net_load_phase2_pct.json new file mode 100644 index 00000000..c62bc997 --- /dev/null +++ b/.homeycompose/capabilities/net_load_phase2_pct.json @@ -0,0 +1,25 @@ +{ + "type": "number", + "title": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "titleFormatted": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "desc" : { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "units": { + "en": "%", + "nl": "%" + }, + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/net_load_phase3.json b/.homeycompose/capabilities/net_load_phase3.json new file mode 100644 index 00000000..2c18da19 --- /dev/null +++ b/.homeycompose/capabilities/net_load_phase3.json @@ -0,0 +1,29 @@ +{ + "type": "number", + "title": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "titleFormatted": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "desc" : { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "min": 0, + "max": 100, + "step": 1, + "decimals": 0, + "chartType": "stepLine", + "getable": true, + "setable": false, + "uiComponent": "slider", + "insights": true, + "units": { + "en": "A", + "nl": "A" + }, + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/net_load_phase3_pct.json b/.homeycompose/capabilities/net_load_phase3_pct.json new file mode 100644 index 00000000..c8ef02cb --- /dev/null +++ b/.homeycompose/capabilities/net_load_phase3_pct.json @@ -0,0 +1,25 @@ +{ + "type": "number", + "title": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "titleFormatted": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "desc" : { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "units": { + "en": "%", + "nl": "%" + }, + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/override_until.json b/.homeycompose/capabilities/override_until.json new file mode 100644 index 00000000..24e5a4e2 --- /dev/null +++ b/.homeycompose/capabilities/override_until.json @@ -0,0 +1,15 @@ +{ + "type": "string", + "title": { + "en": "Override Until", + "nl": "Overschrijven Tot" + }, + "desc": { + "en": "Manual override expiry time", + "nl": "Handmatige overschrijving vervaltijd" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/policy_debug.json b/.homeycompose/capabilities/policy_debug.json new file mode 100644 index 00000000..e6b62110 --- /dev/null +++ b/.homeycompose/capabilities/policy_debug.json @@ -0,0 +1,16 @@ +{ + "id": "policy_debug", + "type": "string", + "title": { + "en": "Policy Debug", + "nl": "Policy Debug" + }, + "desc": { + "en": "Current tariff and weather inputs used by the policy engine", + "nl": "Huidige tarief- en weerinputs gebruikt door de policy engine" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false +} diff --git a/.homeycompose/capabilities/policy_debug_learning.json b/.homeycompose/capabilities/policy_debug_learning.json new file mode 100644 index 00000000..d1b312e7 --- /dev/null +++ b/.homeycompose/capabilities/policy_debug_learning.json @@ -0,0 +1,17 @@ +{ + "id": "policy_debug_learning", + "type": "string", + "title": { + "en": "Policy Learning", + "nl": "Policy Leren" + }, + "desc": { + "en": "Historical learning statistics and pattern coverage", + "nl": "Historische leerstatistieken en patroonbedekking" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" +} diff --git a/.homeycompose/capabilities/policy_debug_price.json b/.homeycompose/capabilities/policy_debug_price.json new file mode 100644 index 00000000..b793bc06 --- /dev/null +++ b/.homeycompose/capabilities/policy_debug_price.json @@ -0,0 +1,17 @@ +{ + "id": "policy_debug_price", + "type": "string", + "title": { + "en": "Policy Price", + "nl": "Policy Prijs" + }, + "desc": { + "en": "Current DAP price and rate bucket", + "nl": "Huidige DAP-prijs en tariefcategorie" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" +} diff --git a/.homeycompose/capabilities/policy_debug_sun.json b/.homeycompose/capabilities/policy_debug_sun.json new file mode 100644 index 00000000..a4b2f200 --- /dev/null +++ b/.homeycompose/capabilities/policy_debug_sun.json @@ -0,0 +1,17 @@ +{ + "id": "policy_debug_sun", + "type": "string", + "title": { + "en": "Policy Sun", + "nl": "Policy Zon" + }, + "desc": { + "en": "Sunshine forecast inputs used by the policy", + "nl": "Zon-voorspelling gebruikt door de policy" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" +} diff --git a/.homeycompose/capabilities/policy_debug_top3high.json b/.homeycompose/capabilities/policy_debug_top3high.json new file mode 100644 index 00000000..1757c61b --- /dev/null +++ b/.homeycompose/capabilities/policy_debug_top3high.json @@ -0,0 +1,17 @@ +{ + "id": "policy_debug_top3high", + "type": "string", + "title": { + "en": "Policy Top3 High", + "nl": "Policy Top3 Hoog" + }, + "desc": { + "en": "Top 3 most expensive DAP hours", + "nl": "Top 3 duurste DAP-uren" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" +} diff --git a/.homeycompose/capabilities/policy_debug_top3low.json b/.homeycompose/capabilities/policy_debug_top3low.json new file mode 100644 index 00000000..e5db27ff --- /dev/null +++ b/.homeycompose/capabilities/policy_debug_top3low.json @@ -0,0 +1,17 @@ +{ + "id": "policy_debug_top3low", + "type": "string", + "title": { + "en": "Policy Top3 Low", + "nl": "Policy Top3 Laag" + }, + "desc": { + "en": "Top 3 cheapest DAP hours", + "nl": "Top 3 goedkoopste DAP-uren" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" +} diff --git a/.homeycompose/capabilities/policy_enabled.json b/.homeycompose/capabilities/policy_enabled.json new file mode 100644 index 00000000..619f4083 --- /dev/null +++ b/.homeycompose/capabilities/policy_enabled.json @@ -0,0 +1,15 @@ +{ + "type": "boolean", + "title": { + "en": "Policy Enabled", + "nl": "Beleid Ingeschakeld" + }, + "desc": { + "en": "Enable or disable the battery policy automation", + "nl": "Schakel batterijbeleid automatisering in of uit" + }, + "getable": true, + "setable": true, + "uiComponent": "toggle", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/policy_mode.json b/.homeycompose/capabilities/policy_mode.json new file mode 100644 index 00000000..fa99c7ef --- /dev/null +++ b/.homeycompose/capabilities/policy_mode.json @@ -0,0 +1,19 @@ +{ + "id": "policy_mode", + "type": "enum", + "title": { + "en": "Policy Mode", + "nl": "Beleidsmodus" + }, + "values": [ + { "id": "off", "title": { "en": "Off", "nl": "Uit" } }, + { "id": "balanced", "title": { "en": "Dynamic Pricing", "nl": "Dynamisch Tarief" } }, + { "id": "balanced-fixed", "title": { "en": "Fixed Pricing", "nl": "Vast Tarief" } }, + { "id": "balanced-dynamic", "title": { "en": "Dynamic Pricing (V2 — post-saldering)", "nl": "Dynamisch Tarief (V2 — na salderen)" } }, + { "id": "zero", "title": { "en": "Peak Shaving", "nl": "Peak Shaving" } } + ], + "getable": true, + "setable": true, + "uiComponent": "picker", + "icon": "assets/icon.svg" +} diff --git a/.homeycompose/capabilities/predicted_sun_hours.json b/.homeycompose/capabilities/predicted_sun_hours.json new file mode 100644 index 00000000..ffca2ead --- /dev/null +++ b/.homeycompose/capabilities/predicted_sun_hours.json @@ -0,0 +1,22 @@ +{ + "type": "number", + "title": { + "en": "Predicted Sun (4h)", + "nl": "Voorspelde Zon (4u)" + }, + "desc": { + "en": "Expected sunshine hours in next 4 hours", + "nl": "Verwachte zonuren in komende 4 uur" + }, + "units": { + "en": "h", + "nl": "u" + }, + "min": 0, + "max": 4, + "decimals": 1, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/preset.json b/.homeycompose/capabilities/preset.json new file mode 100644 index 00000000..ac2849a7 --- /dev/null +++ b/.homeycompose/capabilities/preset.json @@ -0,0 +1,16 @@ +{ + "type": "enum", + "title": { + "en": "Preset", + "nl": "Preset" + }, + "getable": true, + "setable": true, + "uiComponent": "picker", + "values": [ + { "id": "0", "title": { "nl": "Thuis", "en": "Home" } }, + { "id": "1", "title": { "nl": "Weg", "en": "Away" } }, + { "id": "2", "title": { "nl": "Slapen", "en": "Sleep" } }, + { "id": "3", "title": { "nl": "Vakantie", "en": "Holiday" } } + ] +} diff --git a/.homeycompose/capabilities/recommended_mode.json b/.homeycompose/capabilities/recommended_mode.json new file mode 100644 index 00000000..d661a24c --- /dev/null +++ b/.homeycompose/capabilities/recommended_mode.json @@ -0,0 +1,15 @@ +{ + "type": "string", + "title": { + "en": "Recommended Mode", + "nl": "Aanbevolen Modus" + }, + "desc": { + "en": "Current battery mode recommendation", + "nl": "Huidige batterij modus aanbeveling" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/rssi.json b/.homeycompose/capabilities/rssi.json new file mode 100644 index 00000000..31c64e23 --- /dev/null +++ b/.homeycompose/capabilities/rssi.json @@ -0,0 +1,16 @@ +{ + "type": "number", + "title": { + "en": "WiFi Signal", + "nl": "WiFi signaal" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/rssi.svg", + "units": { + "en": "RSSI", + "nl": "RSSI" + } +} \ No newline at end of file diff --git a/.homeycompose/capabilities/sun_score.json b/.homeycompose/capabilities/sun_score.json new file mode 100644 index 00000000..af6e10be --- /dev/null +++ b/.homeycompose/capabilities/sun_score.json @@ -0,0 +1,22 @@ +{ + "type": "number", + "title": { + "en": "Sunshine Score", + "nl": "Zonneschijn Score" + }, + "desc": { + "en": "Predicted sunshine availability (0-100%)", + "nl": "Voorspelde zonneschijn beschikbaarheid (0-100%)" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/tariff.json b/.homeycompose/capabilities/tariff.json new file mode 100644 index 00000000..d043682f --- /dev/null +++ b/.homeycompose/capabilities/tariff.json @@ -0,0 +1,16 @@ +{ + "type": "number", + "title": { + "en": "Active tariff", + "nl": "Tarief actief" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/tariff.svg", + "units": { + "en": "Tariff", + "nl": "Tarief" + } +} \ No newline at end of file diff --git a/.homeycompose/capabilities/time_to_empty.json b/.homeycompose/capabilities/time_to_empty.json new file mode 100644 index 00000000..ed7519e7 --- /dev/null +++ b/.homeycompose/capabilities/time_to_empty.json @@ -0,0 +1,16 @@ +{ + "type": "number", + "title": { + "en": "Time until discharged", + "nl": "Tijd tot ontladen" + }, + "getable": true, + "setable": false, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/clock.svg", + "units": { + "en": "min", + "nl": "min" + } + } \ No newline at end of file diff --git a/.homeycompose/capabilities/time_to_full.json b/.homeycompose/capabilities/time_to_full.json new file mode 100644 index 00000000..b8006ff3 --- /dev/null +++ b/.homeycompose/capabilities/time_to_full.json @@ -0,0 +1,16 @@ +{ + "type": "number", + "title": { + "en": "Time until full charge", + "nl": "Tijd tot vol geladen" + }, + "getable": true, + "setable": false, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/clock.svg", + "units": { + "en": "min", + "nl": "min" + } +} \ No newline at end of file diff --git a/.homeycompose/capabilities/voltage_sag_l1.json b/.homeycompose/capabilities/voltage_sag_l1.json new file mode 100644 index 00000000..b62cb606 --- /dev/null +++ b/.homeycompose/capabilities/voltage_sag_l1.json @@ -0,0 +1,12 @@ +{ + "type": "number", + "title": { + "en": "Net dip L1", + "nl": "Net dip L1" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/voltage_sag_l2.json b/.homeycompose/capabilities/voltage_sag_l2.json new file mode 100644 index 00000000..edb7d8d3 --- /dev/null +++ b/.homeycompose/capabilities/voltage_sag_l2.json @@ -0,0 +1,12 @@ +{ + "type": "number", + "title": { + "en": "Net dip L2", + "nl": "Net dip L2" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/voltage_sag_l3.json b/.homeycompose/capabilities/voltage_sag_l3.json new file mode 100644 index 00000000..bc0cbf66 --- /dev/null +++ b/.homeycompose/capabilities/voltage_sag_l3.json @@ -0,0 +1,12 @@ +{ + "type": "number", + "title": { + "en": "Net dip L3", + "nl": "Net dip L3" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/voltage_swell_l1.json b/.homeycompose/capabilities/voltage_swell_l1.json new file mode 100644 index 00000000..57ca3128 --- /dev/null +++ b/.homeycompose/capabilities/voltage_swell_l1.json @@ -0,0 +1,12 @@ +{ + "type": "number", + "title": { + "en": "Net peak L1", + "nl": "Net piek L1" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/voltage_swell_l2.json b/.homeycompose/capabilities/voltage_swell_l2.json new file mode 100644 index 00000000..a0c6a600 --- /dev/null +++ b/.homeycompose/capabilities/voltage_swell_l2.json @@ -0,0 +1,12 @@ +{ + "type": "number", + "title": { + "en": "Net peak L2", + "nl": "Net piek L2" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/voltage_swell_l3.json b/.homeycompose/capabilities/voltage_swell_l3.json new file mode 100644 index 00000000..8a8bef64 --- /dev/null +++ b/.homeycompose/capabilities/voltage_swell_l3.json @@ -0,0 +1,12 @@ +{ + "type": "number", + "title": { + "en": "Net peak L3", + "nl": "Net piek L3" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/warm_water.json b/.homeycompose/capabilities/warm_water.json new file mode 100644 index 00000000..69ef370e --- /dev/null +++ b/.homeycompose/capabilities/warm_water.json @@ -0,0 +1,12 @@ +{ + "type": "boolean", + "title": { + "en": "Warm water", + "nl": "Warm water" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/shower.svg" +} \ No newline at end of file diff --git a/.homeycompose/capabilities/weather_override.json b/.homeycompose/capabilities/weather_override.json new file mode 100644 index 00000000..4da65489 --- /dev/null +++ b/.homeycompose/capabilities/weather_override.json @@ -0,0 +1,46 @@ +{ + "id": "weather_override", + "type": "enum", + "title": { + "en": "Weather Override", + "nl": "Weer Overschrijven" + }, + "desc": { + "en": "Override weather forecast for policy decisions", + "nl": "Overschrijf weersverwachting voor beleidsbeslissingen" + }, + "values": [ + { + "id": "auto", + "title": { + "en": "Auto (use forecast)", + "nl": "Auto (gebruik voorspelling)" + } + }, + { + "id": "sunny", + "title": { + "en": "Sunny", + "nl": "Zonnig" + } + }, + { + "id": "cloudy", + "title": { + "en": "Cloudy", + "nl": "Bewolkt" + } + }, + { + "id": "rainy", + "title": { + "en": "Rainy", + "nl": "Regenachtig" + } + } + ], + "getable": true, + "setable": true, + "uiComponent": "picker", + "icon": "assets/icon.svg" +} diff --git a/.homeycompose/capabilities/wifi_quality.json b/.homeycompose/capabilities/wifi_quality.json new file mode 100644 index 00000000..50eacad0 --- /dev/null +++ b/.homeycompose/capabilities/wifi_quality.json @@ -0,0 +1,12 @@ +{ + "type": "string", + "title": { + "en": "WiFi State", + "nl": "WiFi Status" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/rssi.svg" +} \ No newline at end of file diff --git a/.homeycompose/discovery/SDM230.json b/.homeycompose/discovery/SDM230.json new file mode 100644 index 00000000..4ac8ee72 --- /dev/null +++ b/.homeycompose/discovery/SDM230.json @@ -0,0 +1,28 @@ +{ + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "SDM230-wifi" + } + } + ], + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-KWH1" + } + } + ] + ] +} \ No newline at end of file diff --git a/.homeycompose/discovery/SDM230_v2.json b/.homeycompose/discovery/SDM230_v2.json new file mode 100644 index 00000000..b0e8da21 --- /dev/null +++ b/.homeycompose/discovery/SDM230_v2.json @@ -0,0 +1,28 @@ +{ + "type": "mdns-sd", + "mdns-sd": { + "name": "homewizard", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "SDM230-wifi" + } + } + ], + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-KWH1" + } + } + ] + ] +} \ No newline at end of file diff --git a/.homeycompose/discovery/SDM630.json b/.homeycompose/discovery/SDM630.json new file mode 100644 index 00000000..32578702 --- /dev/null +++ b/.homeycompose/discovery/SDM630.json @@ -0,0 +1,28 @@ +{ + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "SDM630-wifi" + } + } + ], + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-KWH3" + } + } + ] + ] +} \ No newline at end of file diff --git a/.homeycompose/discovery/SDM630_v2.json b/.homeycompose/discovery/SDM630_v2.json new file mode 100644 index 00000000..35160dd1 --- /dev/null +++ b/.homeycompose/discovery/SDM630_v2.json @@ -0,0 +1,28 @@ +{ + "type": "mdns-sd", + "mdns-sd": { + "name": "homewizard", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "SDM630-wifi" + } + } + ], + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-KWH3" + } + } + ] + ] +} \ No newline at end of file diff --git a/.homeycompose/discovery/energy.json b/.homeycompose/discovery/energy.json new file mode 100644 index 00000000..7672c2a0 --- /dev/null +++ b/.homeycompose/discovery/energy.json @@ -0,0 +1,26 @@ +{ + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^p1meter-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-P1" + } + } + ] + ] +} \ No newline at end of file diff --git a/.homeycompose/discovery/energy_socket.json b/.homeycompose/discovery/energy_socket.json new file mode 100644 index 00000000..3f211c65 --- /dev/null +++ b/.homeycompose/discovery/energy_socket.json @@ -0,0 +1,26 @@ +{ + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^energysocket-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-SKT" + } + } + ] + ] +} \ No newline at end of file diff --git a/.homeycompose/discovery/energy_v2.json b/.homeycompose/discovery/energy_v2.json new file mode 100644 index 00000000..288793fb --- /dev/null +++ b/.homeycompose/discovery/energy_v2.json @@ -0,0 +1,26 @@ +{ + "type": "mdns-sd", + "mdns-sd": { + "name": "homewizard", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^p1meter-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-P1" + } + } + ] + ] +} \ No newline at end of file diff --git a/.homeycompose/discovery/plugin_battery.json b/.homeycompose/discovery/plugin_battery.json new file mode 100644 index 00000000..747082bf --- /dev/null +++ b/.homeycompose/discovery/plugin_battery.json @@ -0,0 +1,26 @@ +{ + "type": "mdns-sd", + "mdns-sd": { + "name": "homewizard", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^battery-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-BAT" + } + } + ] + ] +} \ No newline at end of file diff --git a/.homeycompose/discovery/watermeter.json b/.homeycompose/discovery/watermeter.json new file mode 100644 index 00000000..9503ebdc --- /dev/null +++ b/.homeycompose/discovery/watermeter.json @@ -0,0 +1,26 @@ +{ + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^watermeter-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-WTR" + } + } + ] + ] +} \ No newline at end of file diff --git a/.homeycompose/flow/conditions/check-battery-mode.json b/.homeycompose/flow/conditions/check-battery-mode.json new file mode 100644 index 00000000..40167ee9 --- /dev/null +++ b/.homeycompose/flow/conditions/check-battery-mode.json @@ -0,0 +1,58 @@ +{ + "title": { + "en": "Check Battery Mode", + "nl": "Controleer batterij Modus" + }, + "titleFormatted": { + "en": "Check battery mode !{{is|isn't}} [[mode]]", + "nl": "Controleer batterij modus !{{is|isn't}} [[mode]]" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + }, + { + "type": "dropdown", + "name": "mode", + "values": [ + { + "id": "zero", + "label": { + "en": "Zero mode", + "nl": "Null op de meter" + } + }, + { + "id": "to_full", + "label": { + "en": "Full Charge", + "nl": "Volledig laden" + } + }, + { + "id": "standby", + "label": { + "en": "Standby", + "nl": "Standby" + } + }, + { + "id": "zero_charge_only", + "label": { + "en": "Zero mode, Charge Only", + "nl": "Nul op de Meter, Alleen laden" + } + }, + { + "id": "zero_discharge_only", + "label": { + "en": "Zero mode, Discharge Only", + "nl": "Nul op de Meter, Alleen ontladen" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/.homeycompose/flow/triggers/leak_changed.json b/.homeycompose/flow/triggers/leak_changed.json new file mode 100644 index 00000000..18609dd3 --- /dev/null +++ b/.homeycompose/flow/triggers/leak_changed.json @@ -0,0 +1,29 @@ +{ + "id": "leak_changed", + "title": { + "en": "Water leakage", + "nl": "Water lekkage" + }, + "args": [ + { + "name": "Kakusensor", + "type": "device", + "filter": "driver_id=kakusensor", + "placeholder": { + "en": "Which Sensor", + "nl": "Welke Sensor" + } + } + ], + "tokens": [ + { + "name": "leak_changed", + "type": "number", + "title": { + "en": "mm", + "nl": "mm" + }, + "example": 15 + } + ] +} \ No newline at end of file diff --git a/.homeycompose/settings/weather_location.json b/.homeycompose/settings/weather_location.json new file mode 100644 index 00000000..d45e3324 --- /dev/null +++ b/.homeycompose/settings/weather_location.json @@ -0,0 +1,12 @@ +{ + "type": "text", + "title": { + "en": "Weather location", + "nl": "Weer locatie" + }, + "placeholder": "52.02, 5.04 or Utrecht", + "desc": { + "en": "Enter 'lat,lon' or a city name", + "nl": "Voer 'lat,lon' of een plaatsnaam in" + } +} diff --git a/.homeyignore b/.homeyignore new file mode 100644 index 00000000..11536f2f --- /dev/null +++ b/.homeyignore @@ -0,0 +1 @@ +hwconfig.exe.zip \ No newline at end of file diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 00000000..d52c8712 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,9 @@ +{ + "MD033": { + "allowed_elements": ["details", "summary"] + }, + "MD013": false, + "MD024": { + "siblings_only": true + } +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..af2b3669 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +--- +repos: + - repo: local + hooks: + - id: linter + name: Linter + pass_filenames: False + language: system + entry: npm run lint + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: no-commit-to-branch + name: Check if commit is not in branch 'master' + args: + - --branch=main # For future us + - --branch=master + + - id: end-of-file-fixer + - id: check-json + - id: check-yaml + - id: check-added-large-files + - id: check-case-conflict + - id: trailing-whitespace + - id: mixed-line-ending diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..8f15ca29 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "overrides": [ + { + "files": ["*.html", "**/*.js"], + "options": { + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "printWidth": 200, + "trailingComma": "none" + } + } + ] + } + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..be833c5c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [{ + "name": "Launch app", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "homey", + "args": ["app", "run"], + "outputCapture": "std", + "serverReadyAction": { + "pattern": "Debugger listening on", + "action": "startDebugging", + "name": "Attach to Homey" + } + }, + { + "name": "Attach to Homey", + "type": "node", + "request": "attach", + "address": "192.168.1.12", + "port": 9222, + "localRoot": "${workspaceFolder}", + "remoteRoot": "/" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..72dad011 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "eslint.workingDirectories": [{ "mode": "auto" }], + "eslint.validate": ["javascript"], + "eslint.options": { + "overrideConfig": { + "rules": { + "node/no-commonjs": "off", + "import/no-commonjs": "off", + "no-useless-concat": "off", + "prefer-template": "off" + } + } + }, + "javascript.validate.enable": false, + "typescript.validate.enable": false, + "typescript.disableAutomaticTypeAcquisition": true, + "eslint.run": "onSave" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..513d5376 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,4 @@ +{ + "version": "2.0.0", + "tasks": [] +} diff --git a/2026-03-30_13h31_26.png b/2026-03-30_13h31_26.png new file mode 100644 index 00000000..8711f66b Binary files /dev/null and b/2026-03-30_13h31_26.png differ diff --git a/CRASH_DIAGNOSTICS.md b/CRASH_DIAGNOSTICS.md new file mode 100644 index 00000000..8d6c5654 --- /dev/null +++ b/CRASH_DIAGNOSTICS.md @@ -0,0 +1,89 @@ +# CPU Crash Diagnostics + +## Overview +Comprehensive logging has been added to identify CPU crash causes. Logs will show exactly where the app is spending time and where crashes occur. + +## How to View Logs + +### Real-time logs: +```bash +homey app run +``` + +### Or tail the Homey log: +```bash +tail -f /tmp/homey.log +``` + +## Log Markers to Look For + +### 🔍 WebSocket Operations +All WebSocket operations are logged with `[WS-CRASH-LOG]` prefix: + +- `[WS-CRASH-LOG][ID][CONSTRUCTOR]` - WebSocket manager created +- `[WS-CRASH-LOG][ID][START]` - Connection starting +- `[WS-CRASH-LOG][ID][MSG_RECV]` - Message received +- `[WS-CRASH-LOG][ID][MSG_PARSED]` - Message parsed (shows type) +- `[WS-CRASH-LOG][ID][MEASURE_CHECK]` - Checking throttle timing +- `[WS-CRASH-LOG][ID][MEASURE_PROCESS]` - Processing measurement +- `[WS-CRASH-LOG][ID][MEASURE_DONE]` - Measurement processed +- `[WS-CRASH-LOG][ID][MEASURE_THROTTLED]` - Message throttled +- `[WS-CRASH-LOG][ID][MEASURE_ERROR]` - Error in measurement handler + +### 💥 Global Error Handlers +- `💥 UNHANDLED PROMISE REJECTION:` - Promise rejected without .catch() +- `💥 UNCAUGHT EXCEPTION:` - Uncaught exception +- `⚠️ PROCESS WARNING:` - Node.js process warnings (memory, event listeners, etc.) + +### ❌ Device Errors +- `❌ _handleMeasurement crashed` - Measurement handler crashed +- `❌ Capability update batch error` - Capability updates failed + +## What to Look For + +### CPU Issues +If you see rapid repeating patterns like: +``` +[WS-CRASH-LOG][123][MEASURE_PROCESS] Calling handler +[WS-CRASH-LOG][124][MEASURE_PROCESS] Calling handler +[WS-CRASH-LOG][125][MEASURE_PROCESS] Calling handler +``` +This indicates measurements are processing too frequently (throttling not working). + +### Memory Leaks +``` +⚠️ PROCESS WARNING: MaxListenersExceededWarning +``` +Indicates event listeners are accumulating (likely from restart loops). + +### Unhandled Errors +``` +💥 UNHANDLED PROMISE REJECTION: +``` +Shows errors that aren't being caught - these can cause silent crashes. + +## Disabling Diagnostic Logs + +To disable WebSocket crash logs (after debugging), edit `/includes/v2/Ws.js`: +```javascript +const CRASH_LOG_ENABLED = false; // Change to false +``` + +## Performance Impact + +The crash logs add minimal overhead (~1-2% CPU) but provide critical diagnostics. They should be disabled in production once the issue is resolved. + +## Next Steps After Crash + +1. **Check the last operation** - The last `[WS-CRASH-LOG]` entry shows where it crashed +2. **Look for error patterns** - Repeating errors indicate the root cause +3. **Check timing** - Look at timestamps between operations to find bottlenecks +4. **Verify cleanup** - Ensure `onUninit()` is called before crashes + +## Expected Behavior (Normal Operation) + +You should see: +- Messages throttled to ~5 second intervals +- No rapid repetition of the same operation +- No unhandled promise rejections +- Clean `onUninit()` calls during restarts diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 00000000..d7625e4a --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,70 @@ +# Credits and Acknowledgments + +## HomeWizard Homey App + +### Current Maintainer + +- **Jeroen Tebbens** ([@jtebbens](https://github.com/jtebbens)) - Current maintainer and lead developer + +### Contributors + +- **Jeroen Bos** - Contributor +- **Freddie Welvering** - Contributor +- **Emile Nijssen** - Contributor +- **Dennie de Groot** - Contributor +- **Sven Serlier** - Contributor + +### Cloud Integration + +The cloud-based device support (drivers/cloud-p1) is based on research and API documentation by: + +- **Sven Serlier** ([@smarthomesven](https://github.com/smarthomesven)) +- Repository: [homey-homewizard-energy-cloud](https://github.com/smarthomesven/homey-homewizard-energy-cloud) +- Implementation: HomeWizard Cloud API reverse engineering and documentation + +### Community Support + +Special thanks to: + +- **DCSBL** - Code improvements and homey-compose migration +- All users who reported issues and provided feedback +- The Homey community for testing and support + +## Third-Party Components + +### Dependencies + +- **homey-api** - Athom B.V. +- **ws** - WebSocket library (MIT License) +- [Other dependencies from package.json] + +## License + +This application is licensed under the GNU General Public License v3.0 (GPL-3.0). + +See [LICENSE](LICENSE) for the full license text. + +### What this means + +- ✅ You can use this app freely +- ✅ You can modify the code +- ✅ You can distribute modified versions +- ⚠️ Modified versions must also be GPL-3.0 +- ⚠️ You must include copyright notices +- ⚠️ You must provide source code access + +## HomeWizard + +This is an unofficial integration for HomeWizard devices. + +- **HomeWizard** is a trademark of HomeWizard B.V. +- This app is not affiliated with, endorsed by, or sponsored by HomeWizard B.V. +- Official HomeWizard products and services: https://www.homewizard.com + +## Disclaimer + +This software is provided "as is", without warranty of any kind, express or implied. Use at your own risk. + +--- + +**Last Updated:** February 2026 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..20f4d7b2 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,353 @@ +# HomeWizard Development Context + +## Architecture Overview + +### Driver Categories + +**WebSocket-Based (Real-Time, Low Latency):** +- `energy_v2` - P1 meter with API v2 (WebSocket preferred, polling fallback) +- `plugin_battery` - Battery system via WebSocket (real-time power updates) +- `cloud_p1` - Cloud-connected P1 meter +- `cloud_watermeter` - Cloud-connected water meter +- Communication: WSS connection to device, ~2 second measurement intervals +- Manager: `includes/v2/Ws.js` - Handles connection, authorization, reconnection logic + +**Intelligence / Automation:** +- `battery-policy` - Autonomous battery charge/discharge optimizer; runs 6 engines eagerly + 2 lazy + +**HTTP Polling-Based (API v1 & v2):** +- `energy` - P1 meter classic API (10s polling default, configurable) +- `SDM230`, `SDM230_v2`, `SDM230-p1mode` - 3-phase kWh meters +- `SDM630`, `SDM630_v2`, `SDM630-p1mode` - 3-phase kWh meters (industrial grade) +- `energy_socket` - Smart socket with power monitoring +- `watermeter` - Water consumption tracking +- Communication: HTTP REST with keep-alive agents, configurable intervals +- Manager: `includes/v2/Api.js` - Centralized fetch with timeout handling + +**Legacy Gateway-Based (Proxy via Main Hub):** +- `thermometer` - Temperature sensors (poor WiFi) +- `heatlink` - Heating control (poor WiFi) +- `rainmeter` - Rain detection (poor WiFi) +- `windmeter` - Wind speed (poor WiFi) +- `kakusensors` - Various sensors (poor WiFi) +- `energylink` - Energy gateway (poor WiFi) +- Communication: HTTP polling to main hub unit which proxies requests +- Manager: `includes/legacy/homewizard.js` - Adaptive polling with backoff + +--- + +## Key File Locations + +| File | Purpose | Key Functions | +|------|---------|---| +| `app.js` | App entry point, lifecycle | Flow cards, baseload monitor init | +| `includes/v2/Ws.js` | WebSocket manager | Connection, auth, reconnect, message buffering | +| `includes/v2/Api.js` | HTTP utilities | fetchWithTimeout, fetch queue | +| `includes/legacy/homewizard.js` | Legacy gateway | Adaptive polling, device management, retry logic | +| `includes/utils/baseloadMonitor.js` | Baseload (sluipverbruik) tracker | Night analysis, fridge detection, oscillation checks | +| `includes/utils/fetchQueue.js` | Fetch rate limiter | Prevents CPU spikes from polling | +| `drivers/energy_v2/device.js` | P1 APIv2 driver | WebSocket + polling hybrid, battery handling, power quality triggers | +| `drivers/energy/device.js` | P1 APIv1 driver | Polling-based, gas/water processing, power quality triggers | +| `drivers/plugin_battery/device.js` | Battery driver | WebSocket real-time, polling fallback | +| `drivers/battery-policy/device.js` | Battery policy automation | ML-based charging optimization, PV estimation, learning engine | +| `lib/policy-engine.js` | Battery decision logic | Score-based mode selection, profitability checks, delay-charge | +| `lib/optimization-engine.js` | 24h DP optimizer | Dynamic programming schedule over 15-min slots | +| `lib/learning-engine.js` | Historical learning | Consumption patterns, PV accuracy tracking, confidence adjustments | +| `lib/efficiency-estimator.js` | RTE tracking | Round-trip efficiency learning from charge/discharge cycles | +| `lib/tariff-manager.js` | Price orchestration | Routes 15-min and hourly prices, `_expandHourlyTo15Min` fallback | +| `lib/merged-price-provider.js` | Price aggregation | Combines Xadi + KwhPrice; Xadi wins conflicts; 1h cache | +| `lib/xadi-provider.js` | Xadi pricing | Day-ahead via API, native 15-min intervals | +| `lib/kwhprice-provider.js` | KwhPrice pricing | kwhprice.eu scraper (Chart.js JS arrays, 96 slots today only) | +| `lib/weather-forecaster.js` | Solar/weather | Open-Meteo ensemble + tilted radiation; sunrise/sunset | +| `lib/explainability-engine.js` | Decision explainer | Generates `reasons[]` per policy run; lazy-loaded | +| `lib/planner-engine.js` | Schedule planner | Day schedule builder for UI chart | +| `lib/battery-chart-generator.js` | Chart data | Price/SoC chart for settings page; lazy-loaded | + +--- + +## Communication Patterns + +### WebSocket Flow (energy_v2, plugin_battery) +``` +1. Start → Preflight check (GET /api/system) +2. Connect → WebSocket upgrade to WSS +3. Authorize → Send token via message +4. Subscribe → Request system, measurement, batteries topics +5. Receive → Messages buffered, flushed every 2-10 seconds +6. Reconnect → Exponential backoff on disconnect (max 30 attempts) +7. Watchdog → Ping every 30s, detect stale connections (190s timeout) +``` + +### HTTP Polling Flow (energy, SDM230/630, energy_socket, watermeter) +``` +1. OnInit → Create keep-alive HTTP agent, start polling interval +2. OnPoll → Fetch from device /data endpoint +3. Parse → JSON parse, validate structure +4. Update → updateCapability() with Promise.allSettled() +5. Retry → Exponential backoff on 5xx errors +6. OnDeleted → Clear interval, destroy agent, flush debug logs +``` + +### Legacy Gateway Flow (thermometer, heatlink, etc.) +``` +1. RegisterDevice → Added to homewizard.devices map +2. StartPoll → Set interval with adaptive timeout +3. Call → homewizard.callnew() with abort controller +4. Timeout → getAdaptiveTimeout() based on response history +5. Retry → Backoff on failure, record response times +6. OnDeleted → homewizard.removeDevice() clears all references +``` + +### Power Quality Monitoring (energy, energy_v2) +``` +1. Counter tracking → voltage_sag_l1/l2/l3_count, voltage_swell_l1/l2/l3_count, long_power_fail_count +2. Change detection → Compare previous value with current (increment detection) +3. Trigger → Fire flow card when any counter increases +4. Token data → Include all phase counts (L1/L2/L3) or failure count +5. Flow cards → voltage_sag_detected_v1/v2, voltage_swell_detected_v1/v2, long_power_fail_detected_v1/v2 +6. Note → v1 suffix for energy driver, no suffix for energy_v2 (unique IDs across app) +``` + +--- + +## Known Issues & Workarounds + +### WebSocket Specific +- **Reconnection spam:** Reduced debug logs during frequent reconnects +- **Authorization timeout:** Preflight check now validates device reachability first +- **Listener duplication:** Must call `removeAllListeners()` before re-attaching handlers +- **Memory leak risk:** Handler functions MUST be bound once in onInit(), not per reconnect + +### HTTP Polling Specific +- **Agent socket leak:** Must destroy agent in onDeleted() to prevent port exhaustion +- **Polling deadlock:** Removed overcomplicated interval checks that blocked polling +- **Timeout cascades:** Each device has independent timeout, not global queue +- **Debug I/O overhead:** Now batched with 5-second debounce (85% I/O reduction) + +### Legacy Gateway Specific +- **Poor WiFi:** Adaptive polling increases interval after failures, resets on success +- **Device deletion:** Must call `homewizard.removeDevice()` to clear internal maps +- **Callback leaks:** safeCallback() wrapper ensures AbortController/timeout cleanup +- **Race conditions:** Response stats array now has atomic bounds checking + +### Baseload Monitor Specific +- **Fridge false positives:** Near-zero detection now requires CONSECUTIVE samples (not cumulative) +- **Battery interference:** Negative power (export) completely filtered before analysis +- **Night data gaps:** Uses fallback calculation if too many invalid nights +- **Oscillation sensitivity:** 300W threshold for normal grid variations, 400W for battery systems + +### Battery Policy Specific +- **PV estimation:** Dual approach (flow card priority with 5-min expiry, grid export detection fallback) +- **Hysteresis:** PV state uses 100W dead zone (-125W ON, -25W OFF) to prevent log spam +- **Profitability check:** 80% round-trip efficiency before forcing grid charging +- **Learning maturity:** Needs 1-2 weeks for useful patterns, 4-6 weeks for full accuracy +- **SoC drift detection:** Two-phase BMS calibration (75W @ 45min, 800W @ 15min), 20min sustained charging required + +### Power Quality Triggers Specific +- **Counter persistence:** Voltage sag/swell/fail counters cumulative (never reset by device) +- **Trigger frequency:** Fires on ANY increase in counter value (not just specific thresholds) +- **Token format:** energy driver uses phase_l1/l2/l3, energy_v2 uses phase string + count +- **Flow card IDs:** Must be unique across all drivers (_v1 suffix for energy, no suffix for energy_v2) + +--- + +## Performance Baselines & Targets + +### CPU Usage Per Device Type +| Driver | Type | Typical CPU | Update Frequency | +|--------|------|-------------|---| +| energy_v2 (WS) | WebSocket | 0.5-1% | ~2s (buffered) | +| plugin_battery (WS) | WebSocket | 0.3-0.5% | ~2s (buffered) | +| energy (polling) | HTTP | 1-2% | 10s default | +| SDM230/630 | HTTP | 0.8-1.5% | 10s default | +| energy_socket | HTTP | 0.5-1% | 10s default | +| watermeter | HTTP | 0.3-0.5% | 10s default | +| Legacy (thermometer, etc.) | HTTP | 0.2-0.8% | 15-60s adaptive | + +### Memory Usage Per Device +| Driver | Typical | Notes | +|--------|---------|-------| +| energy_v2 | ~5-8 MB | Includes cache, capability store, battery tracking | +| plugin_battery | ~3-5 MB | Smaller dataset than P1 | +| polling drivers | ~2-3 MB each | Minimal state | +| legacy drivers | ~200-300 KB | Lightweight, sparse data | + +### I/O Operations Reduction (v3.11.10) +- Settings.get() calls: 8,640/day → 1,440/day (83% reduction) +- Debug log writes: 50/min → ~10/min (85% reduction) +- WebSocket hash calculations: 30/min → optimized loop (garbage collection reduced) + +--- + +## Debug Logging & Monitoring + +### App Settings Dashboard +- Location: App settings → "Fetch Debug" / "WebSocket Debug" / "Baseload Samples" +- Batched writes: Every 5 seconds max, 500-log global limit per app +- Per-device buffer: Max 20 logs before flush +- Cleared on device deletion to prevent unbounded growth + +### Key Debug Flags +```javascript +const debug = false; // Set to true for verbose WebSocket/measurement logging +const wsDebug = require('./wsDebug'); // WebSocket connection lifecycle +``` + +### Common Log Patterns +- `❌` - Error, action failed or malformed +- `⚠️` - Warning, degraded but functional +- `🔐` - Security/authorization +- `📡` - Network communication +- `⚡` - Battery/power-related +- `🕒` - Timing/watchdog +- `💧` - Gas/water data +- `🔌` - WebSocket lifecycle + +--- + +## Baseload Monitor (Sluipverbruik) Logic + +### Detection Window +- **Night hours:** 1 AM - 5 AM (configurable) +- **Sample collection:** Every power update during night (typically 10-30s intervals) +- **History:** Last 30 nights kept for stability + +### Invalid Night Conditions +1. **High Plateau** - Avg power > baseload + 500W for 10+ min (indicates external load) +2. **Negative Long** - Power < 0W for 5+ min (grid export, disabled for battery systems) +3. **Near-Zero Consecutive** - Continuous ±50W for 20+ min (grid balancing detected) +4. **Oscillation** - 300W+ swing in 5-min window (unstable conditions) +5. **PV Startup** - Negative power (export) at 4-6 AM (solar generation) + +### Valid Night Conditions +- Fridge cycles (50-300W, 30-120 min duration) detected and ignored +- Battery discharge (negative power) filtered out completely +- Baseload = average of all positive samples during night +- Stability = average of last 7 valid nights + +### Calculation Formula +``` +baseload = average(last_7_valid_nights) +- Each valid night = average power during 1-5 AM +- Filters applied: negative power removed, fridge cycles allowed +- Fallback: 10th percentile of all samples if < 7 valid nights +``` + +--- + +## Common Development Tasks + +### Adding a New Polling Driver +1. Create `drivers/my_device/device.js` +2. Extend with polling loop in onInit() +3. Implement onPoll() with fetchWithTimeout() +4. Use updateCapability() in Promise.allSettled() pattern +5. Destroy HTTP agent in onDeleted() +6. Add debug logging with _debugLog() pattern +7. Update app.json with device definition + +### Fixing a Memory Leak +1. Check for event listeners not removed (WebSocket.removeAllListeners()) +2. Check for closures holding references (bind once in onInit()) +3. Check for intervals/timeouts not cleared (verify onDeleted()) +4. Check for device map entries not cleaned (verify removeDevice() called) +5. Monitor with: `node --inspect` and Chrome DevTools + +### Optimizing CPU Usage +1. Batch updates with Promise.allSettled() (parallel, not sequential) +2. Throttle expensive operations (e.g., baseload detection every 30s) +3. Eliminate spread operators in tight loops +4. Cache settings.get() results instead of repeated calls +5. Use reverse iteration with early exit for filters + +### Testing Battery Integration +1. Set energy_v2 to use_polling = true (WebSocket fallback) +2. Trigger reconnects: restart device or kill WiFi +3. Monitor battery message buffering (should flush every 10s) +4. Verify battery mode changes trigger flow cards +5. Check that negative power doesn't invalidate baseload + +--- + +## Async/Await Patterns + +### Safe Pattern (Parallel Execution) +```javascript +const tasks = []; +tasks.push(updateCapability(this, 'cap1', value1).catch(this.error)); +tasks.push(updateCapability(this, 'cap2', value2).catch(this.error)); +await Promise.allSettled(tasks); // All run in parallel +``` + +### Anti-Pattern (Sequential, Slow) +```javascript +await updateCapability(this, 'cap1', value1); // Wait for cap1 +await updateCapability(this, 'cap2', value2); // Then wait for cap2 +``` + +### Error Handling +```javascript +try { + const data = await fetchWithTimeout(url, options); + // Process data +} catch (err) { + this.error('Failed to fetch:', err.message); + await this.setUnavailable(err.message || 'Fetch error'); +} +``` + +--- + +## Configuration Reference + +### Device Settings (app.json) +- `polling_interval` - Fetch interval in seconds (default 10) +- `url` - Device IP/hostname +- `show_gas` - Include gas meter data (P1 only) +- `offset_polling` - Socket smart plug polling offset +- `use_polling` - Force polling instead of WebSocket (energy_v2, plugin_battery) + +### App Settings +- `baseload_state` - Persisted baseload history and preferences +- `pluginBatteryGroup` - Fallback battery data when realtime unavailable +- Debug logs - Per-driver and per-app entries + +--- + +## Testing Procedures + +### Device Connectivity +``` +1. Verify Local API enabled in HomeWizard app +2. Check device reachability: curl http://device_ip/api/system +3. Verify authentication: curl with Authorization header +4. Test WebSocket: wscat wss://device_ip/api/ws +``` + +### Polling Stability +``` +1. Monitor poll intervals: check debug logs for timing +2. Verify no deadlocks: check pollingActive flag doesn't stick +3. Test timeout recovery: kill network briefly, verify reconnect +4. Stress test: set polling_interval to 1s, monitor CPU +``` + +### Baseload Accuracy +``` +1. Run 7+ full nights to build history +2. Verify fridge cycles logged but don't invalidate nights +3. Check app settings for baseload_state history +4. Manually inspect currentNightSamples to verify filtering +``` + +--- + +## Version History Key Milestones + +- **v3.14.x** - 15-min pricing (kwhprice.eu Chart.js parser), ExplainabilityEngine improvements, BaseloadMonitor battery correction +- **v3.13.x** - MergedPriceProvider (Xadi + KwhPrice), delay-charge strategy, OptimizationEngine 24h-DP +- **v3.12.x** - ExplainabilityEngine, BatteryChartGenerator (both lazy-loaded), LearningEngine maturity +- **v3.11.10** - CPU optimization (caching, detection throttling), baseload near-zero fix +- **v3.9.29** - Baseload monitor introduced (sluipverbruik tracking) +- **v3.8.22** - WebSocket optimization, fetchQueue centralization +- **v3.0+** - Battery support, modular P1 driver, API v2 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index c842eeda..b4ccbcaa 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,595 @@ -# HomeWizard -# -This app let's you connect your HomeWizard to Homey. You can add your HomeWizard in the device section. Once done it will show up in the flow-editor, ready to be used! +# HomeWizard for Homey -V0.1.0: +Control and monitor your HomeWizard Energy devices directly from your Homey smart home hub. -* Improved polling (far less requests to HomeWizard) -* Various bugfixes and improvements +## 🚀 Quick Start -V0.0.9: +1. **Enable Local API** - Open the official HomeWizard Energy app and enable "Local API" for your devices you like to add -* Energylink + Wattcher support added (credits: Jeroen Tebbens) -* SIDENOTE: All devices paired before 0.0.9 (expect HomeWizard) should be re-paired! -* Make sure your solar meter is connected to s1 on energylink. +## 🚀 Quick Start LEGACY (OLD MODEL) -V0.0.8: +1. **Add Homewizard Unit** - First add your main Homewizard unit in Homey +2. **Add Devices** - Then add related/connected components from Homewizard to your Homey (Heatlink, Energylink, Thermometers etc) -* Heatlink support added (credits: Nick Bockmeulen) +⚠️ **IMPORTANT**: You must enable "Local API" for your device in the official HomeWizard Energy app before adding devices to Homey. -V0.0.7: +## ✨ Features -* Made fixes for app to work on 0.10.x +### Smart Energy Management -V0.0.5 & V0.0.6: +* **P1 Meter Support** - Monitor energy consumption in real-time (API v1 & v2) +* **Smart Sockets** - Control and monitor individual devices +* **Battery Management** - Track and control home battery systems +* **Solar Integration** - Monitor solar production and consumption -* Made fixes for app to work on 0.9.x +### Advanced Features -V0.0.4: +* **Battery Policy Driver** - Automated battery management based on dynamic tariffs or peak shaving +* **Power Quality Monitoring** - Trigger cards for voltage sags, swells, power failures, and restoration events +* **Baseload Detection** - Identify standby power consumption (sluipverbruik) +* **Learning Engine** - AI-powered pattern recognition for optimized battery charging +* **Cloud API Support** - Connect P1 meters and water meters via HomeWizard cloud -* Added switching on/off scenes +### Supported Devices -V0.0.3: +* P1 Energy Meters (API v1 & v2, including cloud-connected) +* Energy Sockets +* Plugin Battery +* SDM230 & SDM630 kWh Meters (3-phase, industrial grade) +* Water Meters (local & cloud) +* Legacy Devices (thermometer, heatlink, rainmeter, windmeter, sensors) -* Added a time-out of 10 sec -* Added extra logging +## 📊 Battery Policy Manager -V0.0.2: +NEW in v3.13.14: Intelligent battery management system that: -* Save HomeWizard’s as a device +* Responds to dynamic electricity tariffs +* Implements peak shaving strategies +* Learns consumption patterns over time +* Adjusts PV production estimates based on historical accuracy +* Provides confidence scoring for policy decisions -V0.0.1: +**Note**: Cloud-based features depend on internet connectivity and HomeWizard Energy platform availability. During maintenance or outages, you may experience errors or incorrect data. -* Use HomeWizard’s preset as a condition in flows -* Set HomeWizard’s preset as action in flows +## 📝 Latest Updates (v3.14.24+) + +### Battery Policy — Planning & Optimizer + +* **Discharge SoC projection now consumption-aware** - `zero_discharge_only` keeps grid at ~0W by matching discharge to actual house consumption (variable 0–800W), not fixed max discharge power. Planning chart SoC curve now reflects real depletion speed based on learned consumption per slot +* **Discharge floor consistent between DP and display** - Optimizer now enforces `min_discharge_price` as a hard constraint in backward induction; eliminates the bug where planning showed "standby" but SoC dropped (DP had internally discharged below the threshold) +* **Opportunistic discharge in dynamic mode** - When `respect_minmax` is disabled, the DP uses `opportunistic_discharge_floor` (default €0.20) instead of `min_discharge_price`, consistent with the policy engine's opportunistic logic. Planning display matches +* **Pre-peak urgent charging** - When an expensive hour is ≤30 min away and SoC is below target, policy switches to `to_full` even when PV is producing (≥400W), ensuring the battery fills in time + +### Battery Policy — Weather & PV + +* **Lat/lon location fields** - Weather location now uses separate latitude/longitude number fields instead of a city name text field. Existing city names are automatically migrated on first startup +* **Forecast blending** - New weather fetch is blended with the previous cache (α=0.6) to smooth sudden Open-Meteo model-run jumps; prevents PV forecast from jumping between runs +* **Weather refresh interval** - Cache refresh now uses the existing `weather_update_interval` setting (default 3h, min 1h) instead of a hardcoded 3-hour interval + +### Battery Policy — Scoring + +* **PV overschot score rebalanced** - Score reduced from +1000 to +250 to keep all scores in the readable 20–300 range; still dominates preserve but no longer produces scores like 1170 + +### Bug Fixes + +* **`battery_group_charge_mode` capability missing** - Self-healing guard added: if the capability is absent when a battery event arrives, it is re-added before `setCapabilityValue()` is called, preventing repeated "Invalid Capability" errors +* **BaseloadMonitor false oscillation** - `_detectOscillation()` now trims 1 outlier from each end before computing the range; a single bad sample from battery mode-transition measurement lag no longer invalidates an otherwise clean night +* **BaseloadMonitor energy_v2** - Battery power sourced from `plugin_battery` (accurate) instead of the P1 `payload.power_w` field (unreliable when firmware doesn't report battery state) +* **Settings crash on PV/weather change** - `homey.settings.unset()` is synchronous; removed erroneous `.catch()` call that crashed when settings were changed + +--- + +## Previous Updates (v3.14.19) + +### Battery Policy — Planning & Intelligence + +* **Single source of truth** - Planning view now reads directly from the optimizer schedule; no duplicate policy logic in the frontend +* **Accurate SoC projection** - Forward pass now reflects PV-assisted charging during preserve slots (firmware runs zero_charge_only when PV is available) +* **Solar yield learning** - Per-slot yield factors (W per W/m²) learned from actual PV measurements, absorbing panel capacity, orientation, PR and shading into one number. Approach inspired by de Gruijter's app +* **Weekend/weekday consumption patterns** - Learning engine distinguishes weekday vs weekend consumption; falls back to group average until enough per-day samples accumulate +* **Consumption-aware planning** - Optimizer uses learned consumption forecasts per slot; discharge offsetting local consumption is valued at full retail price vs 30% for export +* **Battery cycle cost** - Configurable degradation cost (€/kWh discharged); optimizer only cycles the battery when the price spread exceeds the cycle cost, preventing unprofitable small arbitrage rounds + +--- + +## Previous Updates (v3.14.0) + +### Battery Policy — 15-Minute Pricing + +* **15-min price granularity** - Policy decisions now use the actual 15-minute spot price instead of the hourly average, enabling more precise charge/discharge timing during short price dips or peaks +* **Optimizer on 15-min slots** - The 24h dynamic-programming scheduler now plans across 96 slots (15-min) instead of 24 hourly slots, making it possible to exploit short cheap windows (e.g. wind surplus at night) + +### Battery Policy — Explainability + +* **Reasons match the winning mode** - Decision reasons are now filtered to only show why the actual recommendation was made; conflicting reasons from other modes no longer appear +* **Mapping explanation** - When scoring favours charging but conditions prevent it (price above ceiling, no PV), a prominent notice now explains the gap: *"Laden wint (score 140) maar prijs €0.26 > max laadprijs €0.14 → Standby: wacht op betere conditie"* +* **Battery very low reason** - SoC between 1–10% now correctly shows "Batterij erg laag — laden aanbevolen" instead of "normaal bereik" +* **Zero mode threshold** - Explainability engine now mirrors policy engine exactly: respects `min_soc = 0` without a hardcoded 1% floor + +### Bug Fixes + +* **Weather forecast timezone** - Fixed 1-hour offset caused by `timezone: auto` + appending Z; now uses `timezone: UTC` +* **Sunshine duration ensemble** - Fixed all-zero sunshine when using multi-model Open-Meteo requests (`models=` parameter causes model-specific key names like `sunshine_duration_ecmwf_ifs04`; plain key absent) +* **Planning SoC projection** - Fixed all hours showing standby due to early return `if (soc < minSoc) return 'standby'` in planning display; radiation-based PV formula corrected (`pvCapW × radiation/1000` instead of `maxChgW × factor`) +* **ZERO MODE threshold** - Policy engine and explainability now both respect user's `min_soc` setting; removed hardcoded 5% / 1% floors + +### Technical + +* **36-hour forecast horizon** - Weather forecaster extended from 24h to 36h to always cover tonight + full tomorrow even when run in the evening +* **SunMultiSource removed** - Replaced with WeatherForecaster ensemble (3-model average); next-4h radiation used for PV estimation + +--- + +## Previous Updates (v3.13.68) + +### Battery Policy + +* **OptimizationEngine** - New dynamic-programming scheduler that computes the optimal charge/discharge schedule across the full 24-hour price horizon. The policy engine now has genuine lookahead instead of relying purely on heuristics, improving decisions around when to charge cheap and discharge at peak. +* **Explainability color coding** - Policy decision reasons now show color-coded tags with relative weights, making it easier to understand what drove the recommendation. + +### Bug Fixes + +* **Planning solar charge** - Fixed planning page incorrectly showing solar charge in the evening/night hours +* **Incorrect PV charge at night** - Fixed battery policy charging from grid during nighttime when PV flag was incorrectly set + +### Technical + +* **WebSocket throttle configurable** - `energy_v2` WebSocket measurement throttle is now adjustable in device settings (default 2s) +* **WebSocket logging** - Improved WebSocket connection tracking and diagnostics in the settings page + +--- + +## Previous Updates (v3.13.58) + +### Bug Fixes + +* **Baseload Negative Values** - Fixed `BaseloadMonitor._fallback()` including negative power samples in bottom-10% calculation, which caused the baseload (sluipverbruik) to report negative values; now filters to `p >= 0 && p < 1000 W` (consistent with `_computeSmartBaseload`) +* **Baseload Battery Correction** - Fixed `updatePower()` only correcting for battery discharge (`batteryPower < 0`) but not for charging; now applies `householdPower = gridPower − batteryPower` for both directions; result clamped to 0 to prevent negative household consumption from rounding/timing mismatches +* **RTE Learning — Counter Reset Bug** - Fixed efficiency estimator resetting both charge and discharge counters when measured RTE < 0.50; a low ratio simply means the cycle is not complete yet (not enough discharge accumulated relative to charge); counters now preserved and continue accumulating; only reset on confirmed measurement error (RTE > 0.85) or stale counters (> 10 kWh either side) +* **RTE Learning — SoC Null Guard** - Fixed `soc <= 5` orphan-clear guard in `EfficiencyEstimator` firing on every charge start because `null <= 5` is `true` in JavaScript; guard now requires `typeof soc === 'number'` +* **RTE Learning — Wrong SoC Source** - Fixed `battery-policy/device.js` reading `measure_battery` capability (does not exist on the policy device → always `null`) instead of the `soc` variable already resolved from `battery_group_average_soc` on the P1 device +* **Battery Policy bugs** - Fixed Recommended mode text to active mode, leftover battery SoC in morning to discharge. Fix for `to_full` when PV is not enough to charge battery but the market prices are at their lowest (zero_change_only vs to_full) +* **Ratelimit on flowcards in energy (apiv2)** - Ratelimit to avoid action flowcard to execute every second crippling the application and make it cpu/memory crash +* **Energy socket connectivity** - Improve connectivity when there is poor wifi (10s timeouts, was 5s) and retry logic + +### Technical + +* **WebSocket slow_handler threshold** raised from 100 ms to 250 ms to better reflect ARM CPU reality on Homey; journal entries throttled to once per handler per 5 minutes to prevent log noise +* **WebSocket preflight_fail** journal events throttled to once per 10 minutes via `_journalThrottled()`; log output still emitted on every failure for debugging +* **Settings page copy button** — `navigator.clipboard.writeText()` silently fails inside Homey's sandboxed iframe; now always uses `textarea + execCommand('copy')` as primary path; if that also fails a selectable textarea is shown as manual fallback + +--- + +## Previous Updates (v3.13.49) + +### New Features + +* **Active Mode Capability** - New `active_mode` capability shows the battery mode actually active on the hardware, which may differ from `recommended_mode` when confidence is below threshold, auto-apply is off, or a manual override is active +* **New Policy Modes** - Added `Fixed Pricing` and `Dynamic Pricing (V2)` options to the policy mode picker +* **Global Error Handlers** - App now catches unhandled promise rejections, uncaught exceptions, and Node.js process warnings (MaxListenersExceededWarning etc.) for better crash diagnostics + +### Bug Fixes + +* **Coverage Ratio Calculation** - Fixed inverted battery coverage ratio: a 1751 W load with 800 W max now correctly reports 46% coverage instead of 100% +* **Multi-Unit Battery Power** - Max discharge/charge fallback now scales with battery group size (2.7 kWh/unit × 800 W/unit) instead of hardcoding 800 W regardless of how many units are installed +* **PV Virtual Grid Calculation** - Fixed virtual grid calculation: battery charging power is now subtracted (not added) to correctly show true export potential when evaluating PV decisions +* **WebSocket Null Guards** - Fixed crashes when `ws._events` is accessed after the socket is already cleaned up; `removeAllListeners` and event dispatch now guard against null `ws` reference +* **Cost Model Reset** - Battery cost model now resets at or below `min_soc` even when firmware cuts discharge power to 0 W (no longer waits for `isDischarging` to be true) +* **Sensor/History Overflow** - `LearningEngine` now uses exponential moving average (alpha=0.01) after 100 samples per slot, preventing `sum` and `count` from growing unboundedly over years + +### Performance (CPU) + +* **WebSocket Throttle** - Measurements now processed immediately on receive with a 2 s throttle, replacing the previous fixed polling interval; system/battery topics reduced to 30 s (was 10 s) +* **energy_v2 Tiered Updates** - Capability updates split into realtime / 10 s / 30 s / 60 s tiers; voltage, current, and frequency no longer updated on every WebSocket message +* **energy_v2 Battery Group** - Battery group interval reduced from 10 s to 60 s +* **energy_v2 Flow Triggers** - `energy_import_kwh` flow trigger rate-limited to 60 s (was 5 s), preventing 12 triggers/min per device +* **energy_socket Polling** - Minimum poll interval raised to 30 s (was 2 s); startup offset staggered 5–35 s; TCP keep-alive extended to 35 s to match interval +* **plugin_battery Startup Stagger** - First poll staggered 0–30 s per device to prevent 3 simultaneous TLS handshakes at startup +* **plugin_battery Battery Group** - Battery group interval reduced from 10 s to 60 s +* **plugin_battery Capability Batching** - All capability updates in `_handleMeasurement` are now batched with `Promise.allSettled` (non-blocking) instead of sequential `await` +* **Baseload Throttle** - `BaseloadMonitor._processNightSample` stores at most 1 sample per 30 s; `_detectNearZeroLong` is now time-based instead of sample-count-based; night history downsampled to 30 s resolution on save +* **Battery Policy P1 Polling** - P1 capability polling interval increased from 5 s to 15 s + +### Technical + +* WebSocket internals refactored; debug and runtime statistics are now surfaced in the settings page for improved diagnostics +* `onUninit` / `onDeleted` split across all drivers (`energy_v2`, `energy_socket`, `plugin_battery`, `cloud_p1`, `battery-policy`): timers/intervals cleaned up on both app stops and explicit device deletion; baseload deregistration and settings wipe only on deletion +* `__deleted` guard added to `onPoll`, `_fallbackPoll`, `_updateBatteryGroup`, and `_handleMeasurement` to prevent errors after uninit +* PV state detection uses separate hysteresis thresholds for ON (−200 W) vs OFF (−150 W) to prevent bouncing; grid range widened to −150…+250 W when already in PV state +* `active_mode` updated after every policy run to reflect the actual `battery_group_charge_mode` capability value from the P1 device; `battery_policy_state.currentMode` patched to match for accurate SoC projection +* RTE learning bounds enforced (50–85%); values outside this range fall back to the configured setting and trigger an estimator reset; learning threshold lowered to 1.0 kWh per side (was 2.5 kWh) for faster convergence +* `EfficiencyEstimator.reset(eff)` method added +* Explainability engine: soc=0 reason now mirrors policy-engine export-wins analysis; arbitrage reason now shows when price is above break-even but blocked by `min_discharge_price`; PV surplus reason mirrors PV OVERSCHOT opportunity-cost logic +* Coverage ratio formula corrected to `min(maxDischarge / load, 1.0)` in `PolicyEngine`, `_applyPeakShavingRules`, and `ExplainabilityEngine` +* Battery group max power fallback derived from `battery_group_total_capacity_kwh` in device, policy engine, and explainability engine + +--- + +## Previous Updates (v3.13.37) + +### Bug Fixes + +* **Battery Policy PV Detection** - Fixed incorrect grid charging during active PV production +* **WebSocket Stability** - Improved reconnection logic and error handling for energy_v2 and plugin_battery drivers + +### Technical + +* Policy engine mapping logic now respects sticky PV detection state set by `_applyPVReality()` +* PV detection includes `pvEstimate` as additional indicator alongside grid export and battery power +* Improved logging shows virtual grid power breakdown for better debugging +* Enhanced WebSocket connection resilience during network fluctuations + +--- + +## Previous Updates (v3.13.28) + +### New Features + +* **Manual IP Override** - Repair flow for devices when mDNS discovery fails (VLAN/UniFi/mesh Wi-Fi issues) +* **Battery-Aware Baseload** - Excludes battery discharge from grid power for accurate standby consumption +* **Smart Filtering** - Automatically filters EV charging and heat pump cycles from baseload calculation +* **Dynamic Sunrise/Sunset** - Battery discharge windows adapt to seasonal changes (sunset-based timing) +* **Weather-Aware Discharge** - Battery policy uses tomorrow's solar forecast for intelligent discharge decisions +* **Monthly Cost Display** - Settings page shows estimated baseload costs with real-time pricing + +### Improvements + +* Discovery error messages now guide users with mDNS troubleshooting steps (EN/NL) +* Baseload monitor uses median of lowest 50% samples for robust calculation +* Settings page displays visual ML score progress bars +* Explainability engine shows weather-aware reasoning with dynamic time windows +* General UI and logging refinements for clarity and consistency +* Pre charge only when it's profitable +* Planning update when min max prices are changed by user affecting decisions + +### Technical + +* Manual IP support for both P1 Meter (v1) and P1 Meter (apiv2) drivers +* Discovery events properly ignored when manual IP is configured +* Fixed €0.25 estimate for baseload costs to prevent API overload from 15k users +* Internal refactoring improves stability, caching behavior, and driver initialization + +--- + +## Previous Updates (v3.13.14) + +* Battery Policy driver with ML-based charging optimization +* Trigger cards for energy grid errors, voltage swells, voltage sags, and restoration events +* Learning engine for consumption patterns and PV accuracy tracking +* Plugin Battery state of charge icon for dashboard + +### Improvements + +* Homewizard Legacy Device updates (CSS, flow and language) - thanks smarthomesvan +* P1 meters can now connect via HomeWizard cloud API (thanks to Sven Serlier's research) +* Watermeter cloud support (4x daily updates via hwenergy) +* P1 (apiv2) tariff trigger improvements + +### Bug Fixes + +* Fixed capability_already_exists error (cloud_p1) + +--- + +## 📖 Full Changelog + +
+Click to expand complete version history + +### v3.12.9 + +* Plugin battery charge mode selectable from UI +* Energy (apiv2) guard for add/remove "battery_group_charge_mode" + +### v3.12.7 + +* P1 tuning TIMEOUT & Unreachable +* Removed pollingActive (unwanted side effect) + +### v3.12.4 + +* Baseload ignore return power (compensate battery return to grid datapoints) +* Plugin Battery LED brightness adjustment (user request) +* Bug fix: Battery Group (SoC missed when there are fetch errors) +* Bug fix: Polling deadlock fix for (energy, energy_socket, SDM230, SDM630, watermeter) +* Energy socket setAvailable fix +* Bugfix: _cacheSet undefined + +(Websocket & caching) + +* Optimized external meters hash calculation (eliminates array.map() garbage collection pressure) +* Battery group settings now cached with 60-second refresh + +Baseload / sluipverbruik + +* Detection algorithms now run every 30 seconds instead of on every power sample +* Eliminates expensive array scans during night hours + +v3.11.9 + +* P1 energy modified to modular +* P1 energy_v2 modified to modular +* Heatlink additional code check on set target_temperature +* P1, changed order of processing, eletric first then gas/water +* P1 missed call in onPoll interval to reset daily calculation +* Bugfix: P1 (apiv2) polling mode - Charge mode fixes +* Bugfix: Group Battery State of Charge (increased timestamp check) +* Realtime pull from all batteries as fallback Battery Group State + +v3.10.13 + +* Updated plugin battery mode names +* Added device name to debug messages +* SDM630 added per phase kwh meter tracking + daily kwh meter (estimate) +* More gas fix reset at night time (apiv1 and apiv2) +* Bugfix: incorrect daily reset during day of gas usage +* Bugfix: Energylink (watermeter) and Thermometer (battery) +NOTE: This is an estimate based on polling interval. If bad wifi or Homey can't reach the SDM630 the measured value will be lower than the actual data. + +v3.10.7 + +* Bugfix: Homewizard Legacy fetch (tab was empty, no entries while there were errors in the log) +* Remove fetchQueue feature in favor of capture debug information in the app settings page +* Watermeter daily usage added +* Bugfix: Device Fetch Debug wasn't updating only showed "Loading..." +* Bugfix: Circular Reference "device" +* Bugfix: SDM230(p1mode) - updateCapability missed +* Finetune debug log (ignore message circuit_open) +* Energy_socket finetune, added a device queue as a replacement for the earlier centralized fetchQueue +* Homewizard adaptive polling + tuning timeouts +* Cleanup device drivers with overcomplicated checks that ended up with polling deadlocks +* SDM230(p1mode) - Extra code handling for TIMEOUT issues +* Daily gas usage reset improvement (nighttime sometimes misses when there is no gas value received) + +v3.9.29 + +* Wsmanager optimize +* Homewizard legacy custom polling +* Driver.js (apiv2) log fix (this.log undefined) +* Thermometer rollback (name index matching doesnt work as expected) +* Homewizard legacy -> node-fetch and not the fetchQueue utility (bad user experience feedback) +* Baseload (sluipverbruik) improvement (fridge/freezer should not be flagged as invalid ) +* Homewizard app setting page with log or debug information for discovery, fetch failures, websocket problems and baseload samples +* Bugfix: Homewizard.poll (legacy unit) +* Homewizard Legacy fetch debug added to same section under Application settings +* Heatlink Legacy improvement +* Homewizard Legacy Preset improvement (UI picker in Homey app) +* Using external gas meter (timestamp X) instead of administrative meter +* Thermometer trigger and condition cards for no response for X hours. +* Improvement fetchQueue (protect against high cpu warning for devices on 1s polling) + +v3.9.20 + +* New Plugin Battery mode support (zero_charge_only & zero_discharge_only) +* Optional gas checkbox (default enabled) for P1 (apiv1 and apiv2). (User request) +* Added 15min power datapoint for Belgium (average_power_15m_w) P1(apiv2) (user request) +* Plugin Battery - Bugfix setMode for to_full (PUT) +* Updated SDM230_v2 and SDM630_v2 drivers +* Bugfix - Updated P1apiv2 check-battery-mode condition card +* Backward compatibilty fix for the new battery mode applied to older P1 firmware. +* Bugfix - Websocket payload battery mode adjustment +* Fixed: rare crash when _handleBatteries() ran after a device was deleted, causing Not Found: Device with ID … errors during setStoreValue. +* Phase overload notification setting added and a limiter to avoid notification flooding +* New Feature: Baseload (sluipverbruik) detection (experimental) +* Bugfix: energy_socket connection_error capability fix +* Bugfix: energy_v2 (handleBatteries) - device_not_found crash +* Bugfix: trigger cards for SDM230_v2 +* APIv2 change pairing: Modified the username that is used during pair made it unique per homey +* Bugfix: APIv2 pairing -> local/homey_xxxxxx +* Bugfix: SDM630v2 trigger cards removed (obsolete as these are default Homey) +* Finetune: P1(apiv2) websocket + polling, capability updates +* Finetune: energy_sockets (fetch / timeout) centralized +* Refractor code update for P1apiv1, SDM230, SDM630, watermeter +* Customizable phase overload warning + reset +* Phase 1 /3 fix for P1(apiv1) after refractor code update +* Bugfix: Fallback url for SDM230v2 and P1apiv2 (mDNS fail workaround) +* Bugfix: pairing problem "Cannot read properties of undefined (reading 'log') +* Homewizard legacy, clear some old callback methods +* Finetune async/await updates + +v3.8.22 + +* Finetune energy_v2 updates primary values are updated instant, other lesser values once every 10s +* Additional watchdog code to reconnect energy_v2 and plugin_battery upon firmware up/downgrades +* Websocket finetuning (energy_v2 and plugin battery) +* Centralized fetch queue for all fetch calls to spread all queries +* Removed interval check in onPoll loop +* Restore custom polling sockets (got removed by accident rollback) + +v3.8.18 + +* Bugfix: Failed to recreate agent: TypeError: Assignment to constant variable (energy) +* Adjustment to async/await code several drivers + +v3.8.16 + +* Updated APIv2 to add more text upon fetch failed +* Websocket based battery mode settings added (both condition and action) +* Websocket heartbeat (30s) to keep battery mode updated (workaround as battery mode is the only realtime update when it changes) +* P1 & EnergySocket driver (apiv1) http agent tuning (ETIMEOUT and ECONNRESET) + +v3.8.13 + +* Extra error handling (updateCapability) based on received crashreports +* Bugfix: ReferenceError: err is not defined (energy_socket) + +v3.8.11 + +* Rollback energy dongle code from earlier version v3.7.0 +* Strange SD630 problem on older Homey's +* Extra verbose logging in urls to expose mDNS problems for older Homeys (url) + +v3.8.8 + +* After attempting conditional fetch, roll back to node-fetch until 12.9.x releases (Homey Pro 2016 - 2019) +* Bugfix: SDM230-p1mode - error during initialization + +v3.7.9 + +* Extra check upon websocket creation to avoid crashes +* Plugin battery catch all error (unhandled exception) +* Additional checking and error handling on bad wifi connections (websocket based) +* (fix) Error: WebSocket is not open: readyState 0 (CONNECTING) +* Fetch was not defined for fetchWithTimeout function +* Missed net_frequency update, also made it 3 decimals +* Capability update fix (avoid removal check) + +v3.7.1 + +* Trigger card for battery SoC Drift (triggers on expected vs actual State-of-charge) +* Trigger card for battery error (based on energy returned to grid while battery group should be charging) +* Trigger card for battery netfrequency out of range +* Icon update for various capabilities +* Battery group details added to P1apiv2. (Charging state) +* Realtime data for P1 (apiv2) via Websocket +* Realtime data for Plugin Battery via Websocket +* Bugfixes/crashes on P1 (apiv2) - no gas data on first poll / ignore +* Websocket reconnect code for covering wifi disconnect & terminate issues +* Plugin Battery group fix (tracking combined set of batteries) - bugfix / Refenece error +* Netfrequency capability added for Plugin Battery +* Homewizard Legacy - code rollback (pairing problems after improvements) +* P1 (apiv2) - Added checkbox setting to fallback to polling if websocket is to heavy for Homey device + +v3.6.77 + +* Custom polling-interval option made for Homewizard Legacy unit (default 20s, when adjusted restart app to active it) + To adjust setting check the main unit advanced settings +* Energy sockets with poor wifi connection will have 3 attempts now +* Fallback url for P1 mode SDM230 / SDM630 + +v3.6.75 + +* Thermometer (Homewizard Legacy) - full code refractoring +* Homewizard Legacy doesnt support keep-alive, changed back to normal fetch / retry +* Finetune code keepAlive for other devices 10s +* Bugfix: number_of_phases setting incorrectly updated +* Added verbose mDNS discovery results for troubleshooting + +v3.6.73 + +* More try/catch code to avoid any crashes on Homewizard Legacy main unit getStatus fail (Device not found) +* Fine tune "estimated kwh" plugin battery calculation based on user feedback +* Code fixes: unhandledRejections CloudOn/Off for sockets and P1 + +v3.6.71 + +* Finetuning polling and capability during init phase of various drivers +* Added more logging to support diagnostic reports +* Bugfix SDM230 solar parameter was undefined +* Added an estimate charge available in plugin battery value +* Extra code checking for Homewizard Legacy (getStatus function) when there is a connection failure/device not found + +v3.6.67 + +* Enforcing interval clears on various devices when interval is reset +* try_authorize handler bugfix (interval / timeout) app crash logs + +v3.6.66 + +* Fall back url setting upon initial poll for P1, sockets, kwh's, watermeter. (older Homey Pro;s 2016/2019 seems to struggle with mDNS updates) +* Removed retry code for Homewizard legacy devices (changed to keeping http agent session open / keepAlive) +* Battery Group data removed from P1 after a fetch fail (bugfix) +* Increased timeouts (authorize / pairing APIv2) +* Language adjustment P1 warning (overload EN/NL) +NOTE: First time running this version will fail as the url setting is empty so it should improve onwards. + +v3.6.63 + +* SDM230 (p1 mode added) +* P1apiv2 - added daily usage kwh (resets at nightime) (does not cater for directly consumed solar-used energy as this does not pass the smart meter at all) +* Adjustment for P1 to look at Amp datapoints to detect 3-Phased devices in Norway +* HTTP - keepalive agent added to P1, sockets, APIv2 devices +* KeepAlive timeout increased from default 1000ms +* AbortController code added for APiv2 +* Wifi quality capability added (-48dBm is not always clear to users if it is good or bad) +* Bugfix: P1, missed setAvailable(). Code didn’t recover from a failed P1 connection and kept P1 offline + +v3.6.58 + +* Bugfix that was caused by experimental firmware Homey 12.5.2RC3 and slider capability that could not be removed +* Added energy flags for sockets so they can trace imported/exported energy in Homey Energy Dashboard (Home Batteries connected via sockets) +* Code cleanup +* Added some fine tuning to spread the API call's to the P1 + +v3.6.50 + +* Added phase monitoring +* Adjust settings to align with your energy grid +* Bugfix for sliders when gridconnection has 3 phases +* Actual gas meter measurement added (5min poll pending on smartmeter) +* P1apiv1 - Code refactored (clean up repetive lines) +* Extra plugin battery trigger cards (state change, time to full, time to empty) +* Removed sliders in GUI to show grid load per phases + +v3.6.40 + +* Cloud connection setting made available for P1, Sockets, Watermeter, SDM230, SDM630 +* Bugfix Offset watermeter (Cannot read properties of undefined - reading 'offset_water') + +v3.6.38 + +* P1(apiv2) gas meter bugfixes +* P1(apiv2) aggregated total usage added (support for PowerByTheHour app) +* Custom polling for Watermeter, SDM230, SDM630 and SDM630-p1 mode, Default 10s, adjust in advanced settings +* Action cards plugin battery - P1apiv2 device is required (P1 firmware version 6.0201 or higher) +* Wifi metric (dBm) added for P1(apiv2) and Plugin Battery +* Custom Polling interval added for Plugin Battery +* Daily usage imported power and gas (P1apiv1) - User request +* Plugin Battery: added time_to_empty and time_to_full (minutes) +* Trigger for battery mode change + +v3.6.6 + +* Homey Energy - Polling interval for all Energy devices (P1, kwh etc.) lowered to 1s (was 10s) +* Reverted interval back 10s as this has an increased load on some wifi networks and (older) homeys (Early2019) + +v3.6.2 + +* Massive code rework (credits to DCSBL for time and effort) +* Homey Energy dashhboard: Energylink meter_gas capability added +* Text fix in Plugin Battery driver +* APIv2 timer timeout problem + +v3.5.5 + +* Recode P1 APIv2, improved pairing process (DCSBL) +* Pairing process P1 and Plugin Battery aligned +* Plugin in Battery pairing text fix + +v3.5.2 + +* SDM630 clone added to allow P1 like use of kwh meter as a replacement for P1 dongle (users request) + +### v3.5.1 + +* Conversion to homey-compose (DCSBL) +* Socket identification with LED blink (DCSBL) + +
+ +--- + +## 💝 Support This Project + +If you find this app useful, consider supporting development: + +[![Donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/paypalme2/jtebbens) + +--- + +## 📄 License + +This app is licensed under the GNU General Public License v3.0 + +## 👥 Credits + +* **Jeroen Tebbens** - Main developer +* **DCSBL** - Major code contributions (homey-compose, pairing improvements) +* **Sven Serlier (smarthomesvan)** - Cloud API research, Legacy device improvements +* **Community contributors** - Bug reports and feature requests + +## 🔗 Links + +* [GitHub Repository](https://github.com/jtebbens/com.homewizard) +* [Homey App Store](https://homey.app/a/com.homewizard/) +* [HomeWizard Official Site](https://www.homewizard.com/) diff --git a/README.txt b/README.txt new file mode 100644 index 00000000..a15f6c3d --- /dev/null +++ b/README.txt @@ -0,0 +1,20 @@ +Connect your HomeWizard products with Homey. + +NOTE! - ENABLE "LOCAL API" FOR YOUR ENERGY SOCKET, WATERMETER, KWH METER FIRST IN THE OFFICIAL HOMEWIZARD ENERGY APP + +- P1 Meter +- kWh Meter 1 and 3 phase type (SDM230 and SDM630) +- Watermeter +- Energy Socket +- Plugin Battery + +OLD Homewizard Legacy (2012 device) +First install / pair your HomeWizard Base Station then you can add the following devices: +- Thermometers +- Heatlink (Control heating) +- Energylink (Power usage, Gas, Solar production & Water usage (via S1/S2), Total usage and production) +- Windmeter (Direction, Speed, Gusts) +- Rainmeter +- Wattcher (the alternative "Energylink" which only tracks usage from the blinking led) +- Motion sensor +- Smoke sensor diff --git a/README_new.md b/README_new.md new file mode 100644 index 00000000..717eaa94 --- /dev/null +++ b/README_new.md @@ -0,0 +1,255 @@ +# HomeWizard for Homey + +Control and monitor your HomeWizard Energy devices directly from your Homey smart home hub. + +## 🚀 Quick Start + +1. **Enable Local API** - Open the official HomeWizard Energy app and enable "Local API" for your devices +2. **Add Homewizard Unit** - First add your main Homewizard unit in Homey +3. **Add Devices** - Then add related/connected components from Homewizard to your Homey + +⚠️ **IMPORTANT**: You must enable "Local API" for your device in the official HomeWizard Energy app before adding devices to Homey. + +## ✨ Features + +### Smart Energy Management +- **P1 Meter Support** - Monitor energy consumption in real-time (API v1 & v2) +- **Smart Sockets** - Control and monitor individual devices +- **Battery Management** - Track and control home battery systems +- **Solar Integration** - Monitor solar production and consumption + +### Advanced Features +- **Battery Policy Driver** - Automated battery management based on dynamic tariffs or peak shaving +- **Power Quality Monitoring** - Trigger cards for voltage sags, swells, and power failures +- **Baseload Detection** - Identify standby power consumption (sluipverbruik) +- **Learning Engine** - AI-powered pattern recognition for optimized battery charging +- **Cloud API Support** - Connect P1 meters and water meters via HomeWizard cloud + +### Supported Devices +- P1 Energy Meters (API v1 & v2, including cloud-connected) +- Energy Sockets +- Plugin Battery +- SDM230 & SDM630 kWh Meters (3-phase, industrial grade) +- Water Meters (local & cloud) +- Legacy Devices (thermometer, heatlink, rainmeter, windmeter, sensors) + +## 📊 Battery Policy Manager + +NEW in v3.13.14: Intelligent battery management system that: +- Responds to dynamic electricity tariffs +- Implements peak shaving strategies +- Learns consumption patterns over time +- Adjusts PV production estimates based on historical accuracy +- Provides confidence scoring for policy decisions + +**Note**: Cloud-based features depend on internet connectivity and HomeWizard Energy platform availability. During maintenance or outages, you may experience errors or incorrect data. + +## 📝 Latest Updates (v3.13.49) + +### New Features +* Battery Policy driver with ML-based charging optimization +* Trigger cards for energy grid errors, voltage swells, and voltage sags +* Learning engine for consumption patterns and PV accuracy tracking +* Plugin Battery state of charge icon for dashboard + +### Improvements +* Homewizard Legacy Device updates (CSS, flow and language) - thanks smarthomesvan +* P1 meters can now connect via HomeWizard cloud API (thanks to Sven Serlier's research) +* Watermeter cloud support (4x daily updates via hwenergy) +* P1 (apiv2) tariff trigger improvements + +### Bug Fixes +* Fixed capability_already_exists error (cloud_p1) + +### Technical +* WebSocket internals refactored; debug and runtime statistics are now surfaced in the settings page for improved diagnostics + +--- + +## 📖 Full Changelog + +
+Click to expand complete version history + +### v3.12.9 +* Plugin battery charge mode selectable from UI +* Energy (apiv2) guard for add/remove "battery_group_charge_mode" + +### v3.12.7 +* P1 tuning TIMEOUT & Unreachable +* Removed pollingActive (unwanted side effect) + +### v3.12.4 +* Baseload ignores return power (battery compensation) +* Plugin Battery LED brightness adjustment +* Battery Group SoC improvements +* Polling deadlock fixes for multiple drivers +* WebSocket & caching optimizations +* Baseload detection runs every 30s (reduced CPU load) + +### v3.11.9 +* P1 energy and energy_v2 modular architecture +* Heatlink target_temperature safety checks +* P1 processing order optimized +* Battery Group State of Charge improvements + +### v3.10.13 +* Plugin battery mode names updated +* SDM630 per-phase kWh tracking +* Gas usage reset improvements +* Device name in debug messages + +### v3.10.7 +* Watermeter daily usage added +* Homewizard Legacy fetch improvements +* Adaptive polling & timeout tuning +* Debug information capture in app settings + +### v3.9.29 +* WebSocket manager optimization +* Baseload detection improvements +* App settings page with comprehensive logging +* Homewizard Legacy custom polling & UI improvements + +### v3.9.20 +* **NEW**: Plugin Battery zero_charge_only & zero_discharge_only modes +* **NEW**: Baseload (sluipverbruik) detection (experimental) +* Phase overload notifications with customizable thresholds +* Optional gas checkbox for P1 meters +* Belgium 15min power datapoint support +* APIv2 pairing improvements +* Code refactoring for multiple drivers + +### v3.8.22 +* Energy_v2 instant primary value updates +* WebSocket fine-tuning +* Watchdog for firmware changes +* Centralized fetch queue + +### v3.8.16 +* WebSocket-based battery mode settings +* WebSocket heartbeat for battery mode tracking +* HTTP agent tuning for ETIMEOUT/ECONNRESET + +### v3.8.11 +* Energy dongle code rollback +* Enhanced mDNS logging for older Homey devices + +### v3.7.9 +* WebSocket error handling improvements +* WiFi connection stability fixes +* Net frequency capability improvements + +### v3.7.1 +* **NEW**: Battery SoC Drift trigger card +* **NEW**: Battery error trigger card +* **NEW**: Net frequency out of range trigger +* Real-time WebSocket data for P1 apiv2 & Plugin Battery +* WebSocket reconnect for WiFi issues +* Optional polling fallback + +### v3.6.77 +* Custom polling interval for Homewizard Legacy (default 20s) +* Energy sockets: 3 retry attempts +* Fallback URL for P1 mode SDM230/SDM630 + +### v3.6.75 +* Thermometer full refactoring +* Keep-alive fine-tuning +* Verbose mDNS discovery logging + +### v3.6.73 +* Homewizard Legacy crash protection +* Plugin battery kWh estimation improvements +* CloudOn/Off error handling + +### v3.6.71 +* Enhanced diagnostic logging +* Plugin battery charge estimate +* Polling & capability initialization improvements + +### v3.6.67 +* Interval enforcement across devices +* Authorization handler bugfixes + +### v3.6.66 +* Fallback URL settings for older Homey Pro devices +* HTTP keep-alive for Legacy devices +* Increased authorization/pairing timeouts + +### v3.6.63 +* **NEW**: SDM230 P1 mode +* P1apiv2 daily usage kWh tracking +* 3-phase detection for Norway +* HTTP keep-alive agent +* WiFi quality capability + +### v3.6.58 +* Energy flags for sockets (Home Batteries tracking) +* Slider capability fixes +* API call spreading optimization + +### v3.6.50 +* **NEW**: Phase monitoring with customizable thresholds +* Actual gas meter measurement +* Plugin battery trigger cards (state, time to full/empty) +* P1apiv1 code refactoring + +### v3.6.40 +* Cloud connection settings for multiple devices +* Watermeter offset bugfix + +### v3.6.38 +* Custom polling intervals (Watermeter, SDM230, SDM630) +* **NEW**: Plugin battery action cards (requires P1 firmware 6.0201+) +* WiFi metrics (dBm) for P1apiv2 & Plugin Battery +* Daily usage tracking (P1apiv1) +* Battery mode change trigger + +### v3.6.6 +* Polling interval adjustments (1s → 10s for stability) + +### v3.6.2 +* Major code rework (credits: DCSBL) +* Homey Energy dashboard integration +* Energylink meter_gas capability + +### v3.5.5 +* P1 APIv2 pairing improvements (DCSBL) +* Aligned pairing process (P1 & Plugin Battery) + +### v3.5.2 +* **NEW**: SDM630 clone for P1-like kWh meter usage + +### v3.5.1 +* Conversion to homey-compose (DCSBL) +* Socket identification with LED blink (DCSBL) + +
+ +--- + +## 💝 Support This Project + +If you find this app useful, consider supporting development: + +[![Donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/paypalme2/jtebbens) + +--- + +## 📄 License + +This app is licensed under the GNU General Public License v3.0 + +## 👥 Credits + +- **Jeroen Tebbens** - Main developer +- **DCSBL** - Major code contributions (homey-compose, pairing improvements) +- **Sven Serlier (smarthomesvan)** - Cloud API research, Legacy device improvements +- **Community contributors** - Bug reports and feature requests + +## 🔗 Links + +- [GitHub Repository](https://github.com/jtebbens/com.homewizard) +- [Homey App Store](https://homey.app/a/com.homewizard/) +- [HomeWizard Official Site](https://www.homewizard.com/) diff --git a/app.js b/app.js index 0fbfd041..ca941c83 100644 --- a/app.js +++ b/app.js @@ -1,9 +1,112 @@ -"use strict"; +/* + * HomeWizard App for Homey + * Copyright (C) 2025 Jeroen Tebbens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ -function init() { - var request = require('request'); - Homey.log("HomeWizard app ready!"); - +'use strict'; + +const Homey = require('homey'); +const v8 = require('v8'); + +const Testing = false; + +// Helper: log current heap in MB via v8 (process.memoryUsage rss fails on Homey sandbox) +function logMem(label) { + try { + const hs = v8.getHeapStatistics(); + const heap = (hs.used_heap_size / 1024 / 1024).toFixed(1); + const total = (hs.total_heap_size / 1024 / 1024).toFixed(1); + const ext = (hs.external_memory / 1024 / 1024).toFixed(1); + console.log(`[MEM] ${label}: heap=${heap}/${total}MB ext=${ext}MB`); + } catch (e) { + console.log(`[MEM] ${label}: unavailable (${e.message})`); + } +} + +class HomeWizardApp extends Homey.App { + async onInit() { + this.log('HomeWizard app ready!'); + this.baseloadMonitor = null; + this.p1Source = null; + + // 🔍 CRASH DIAGNOSTICS: Global error handlers + this._setupGlobalErrorHandlers(); + + // 🔍 MEMORY DIAGNOSTICS: Log heap every 5s for first 3 minutes + // This helps identify which device/engine causes memory ceiling on startup + let _memCount = 0; + logMem('app-start'); + this._memInterval = setInterval(() => { + _memCount++; + logMem(`T+${_memCount * 5}s`); + if (_memCount >= 36) { // 3 minutes + clearInterval(this._memInterval); + this._memInterval = null; + console.log('[MEM] Memory monitor stopped after 3 minutes'); + } + }, 5000); + + if (process.env.DEBUG === '1' && Testing) { + try { + require('inspector').waitForDebugger(); + } + catch (error) { + require('inspector').open(9225, '0.0.0.0', true); + } + } + } + + _setupGlobalErrorHandlers() { + // Track unhandled promise rejections + process.on('unhandledRejection', (reason, promise) => { + console.error('💥 UNHANDLED PROMISE REJECTION:'); + console.error(' Promise:', promise); + console.error(' Reason:', reason?.stack || reason); + + // Log to Homey + this.error('💥 Unhandled Promise Rejection:', reason?.stack || reason); + }); + + // Track uncaught exceptions + process.on('uncaughtException', (err) => { + console.error('💥 UNCAUGHT EXCEPTION:'); + console.error(' Error:', err?.stack || err); + + // Log to Homey + this.error('💥 Uncaught Exception:', err?.stack || err); + }); + + // Track warning events (like MaxListenersExceededWarning) + process.on('warning', (warning) => { + console.warn('⚠️ PROCESS WARNING:', warning.name, warning.message); + console.warn(' Stack:', warning.stack); + + this.log('⚠️ Warning:', warning.name, warning.message); + }); + + this.log('✅ Global error handlers installed'); + } + + async onUninit() { + if (this._memInterval) { + clearInterval(this._memInterval); + this._memInterval = null; + } + } + } -module.exports.init = init; \ No newline at end of file +module.exports = HomeWizardApp; diff --git a/app.json b/app.json index da302a15..c8103561 100755 --- a/app.json +++ b/app.json @@ -1,430 +1,7436 @@ { - "id": "com.homewizard", - "name": { - "en": "HomeWizard" - }, - "version": "0.1.0", - "compatibility": ">=0.9", - "description": { - "en": "Control HomeWizard using Homey" - }, - "category": "appliances", - "images": { - "large": "assets/images/large.jpg", - "small": "assets/images/small.jpg" - }, - "author": { - "name": "Jeroen Bos", - "email": "jeroenbos22@gmail.com" - }, - "contributors": { - "developers": [{ - "name": "Jeroen Bos", - "email": "jeroenbos22@gmail.com" - }, { - "name": "Nick Bockmeulen", - "email": "git@bockmeulen.nl" - }, { - "name": "Jeroen Tebbens", - "email": "jeroen@tebbens.net" - }] - }, - "flow": { - "triggers": [{ - "id": "power_used_changed", - "title": { - "en": "Power used changed", - "nl": "Huidig vermogen veranderd" - }, - "args": [{ - "name": "Energylink", - "type": "device", - "filter": "driver_uri=homey:app:com.homewizard&driver_id=energylink", - "placeholder": { - "en": "Which energylink", - "nl": "Welke energylink" - } - }], - "tokens": [{ - "name": "power_used", - "title": { - "en": "Watt", - "nl": "Watt" - } - }] - }, { - "id": "power_s1_changed", - "title": { - "en": "Power production changed", - "nl": "Huidige productie veranderd" - }, - "args": [{ - "name": "Energylink", - "type": "device", - "filter": "driver_uri=homey:app:com.homewizard&driver_id=energylink", - "placeholder": { - "en": "Which energylink", - "nl": "Welke energylink" - } - }], - "tokens": [{ - "name": "power_s1", - "title": { - "en": "Watt", - "nl": "Watt" - } - }] - }, { - "id": "meter_power_used_changed", - "title": { - "en": "Daily usage changed", - "nl": "Dag verbruik veranderd" - }, - "args": [{ - "name": "Energylink", - "type": "device", - "filter": "driver_uri=homey:app:com.homewizard&driver_id=energylink", - "placeholder": { - "en": "Which energylink", - "nl": "Welke energylink" - } - }], - "tokens": [{ - "name": "power_daytotal_used", - "title": { - "en": "kWh", - "nl": "kWh" - } - }] - }, { - "id": "meter_power_s1_changed", - "title": { - "en": "Daily production changed", - "nl": "Dag productie veranderd" - }, - "args": [{ - "name": "Energylink", - "type": "device", - "filter": "driver_uri=homey:app:com.homewizard&driver_id=energylink", - "placeholder": { - "en": "Which energylink", - "nl": "Welke energylink" - } - }], - "tokens": [{ - "name": "power_daytotal_s1", - "title": { - "en": "kWh", - "nl": "kWh" - } - }] - } - - ], - "conditions": [{ - "id": "check_preset", - "title": { - "en": "Preset !{{is|isn't}}", - "nl": "Preset !{{is|is niet}}" - }, - "args": [{ - "name": "preset", - "type": "dropdown", - "values": [{ - "id": "0", - "label": { - "en": "Home", - "nl": "Thuis" - } - }, { - "id": "1", - "label": { - "en": "Away", - "nl": "Afwezig" - } - }, { - "id": "2", - "label": { - "en": "Sleep", - "nl": "Slapen" - } - }, { - "id": "3", - "label": { - "en": "Holiday", - "nl": "Vakantie" - } - }] - }, { - "name": "device", - "type": "device", - "filter": "driver_id=homewizard" - }] - }], - "actions": [{ - "id": "set_preset", - "title": { - "en": "Activate preset", - "nl": "Activeer preset" - }, - "args": [{ - "name": "preset", - "type": "dropdown", - "values": [{ - "id": "0", - "label": { - "en": "Home", - "nl": "Thuis" - } - }, { - "id": "1", - "label": { - "en": "Away", - "nl": "Afwezig" - } - }, { - "id": "2", - "label": { - "en": "Sleep", - "nl": "Slapen" - } - }, { - "id": "3", - "label": { - "en": "Holiday", - "nl": "Vakantie" - } - }] - }, { - "name": "device", - "type": "device", - "filter": "driver_id=homewizard" - }] - }, { - "id": "switch_scene_on", - "title": { - "en": "Switch scene on", - "nl": "Zet scene aan" - }, - "args": [{ - "name": "scene", - "type": "autocomplete" - }, { - "name": "device", - "type": "device", - "filter": "driver_id=homewizard" - }] - }, { - "id": "switch_scene_off", - "title": { - "en": "Switch scene off", - "nl": "Zet scene uit" - }, - "args": [{ - "name": "scene", - "type": "autocomplete" - }, { - "name": "device", - "type": "device", - "filter": "driver_id=homewizard" - }] - }] - }, - "drivers": [{ - "id": "homewizard", - "name": { - "en": "HomeWizard", - "nl": "HomeWizard" - }, - "images": { - "large": "drivers/homewizard/assets/images/large.jpg", - "small": "drivers/homewizard/assets/images/small.jpg" - }, - "class": "appliances", - "capabilities": [], - "pair": [{ - "id": "start" - }, { - "id": "list_my_devices", - "template": "list_devices", - "navigation": { - "next": "add_my_devices" - } - }, { - "id": "add_my_devices", - "template": "add_devices" - }], - "settings": [{ - "type": "group", - "label": { - "en": "HomeWizard settings", - "nl": "HomeWizard instellingen" - }, - "children": [{ - "id": "homewizard_ip", - "type": "text", - "label": { - "en": "IP address", - "nl": "IP adres" - }, - "value": "" - }, { - "id": "homewizard_pass", - "type": "text", - "label": { - "en": "Password", - "nl": "Wachtwoord" - }, - "value": "" - }, { - "id": "homewizard_ledring", - "type": "checkbox", - "label": { - "en": "Use ledring", - "nl": "Gebruik ledring" - }, - "value": false - }] - }] - }, { - "id": "heatlink", - "name": { - "en": "Heatlink", - "nl": "Heatlink" - }, - "images": { - "large": "drivers/heatlink/assets/images/large.jpg", - "small": "drivers/heatlink/assets/images/small.jpg" - }, - "class": "thermostat", - "capabilities": [ - "measure_temperature", - "target_temperature" - ], - "pair": [{ - "id": "start" - }, { - "id": "list_my_devices", - "template": "list_devices", - "navigation": { - "next": "add_my_devices" - } - }, { - "id": "add_my_devices", - "template": "add_devices" - }] - }, { - "id": "energylink", - "name": { - "en": "Energylink", - "nl": "Energylink" - }, - "images": { - "large": "drivers/energylink/assets/images/large.jpg", - "small": "drivers/energylink/assets/images/small.jpg" - }, - "class": "sensor", - "capabilities": [ - "meter_power.used", "meter_power.s1", "meter_gas", "measure_power.used", "measure_power.s1" - ], - - "capabilitiesOptions": { - "meter_power.used": { - "title": { - "en": "Day usage", - "nl": "Dag gebruik" - } - }, - "meter_power.s1": { - "title": { - "en": "Day production", - "nl": "Dag opbrengst" - } - - }, - "measure_power.used": { - "title": { - "en": "Power current", - "nl": "Huidig vermogen" - } - - }, - "measure_power.s1": { - "title": { - "en": "Solar current", - "nl": "Huidige opbrengst" - } - - }, - "meter_gas": { - "title": { - "en": "Gas", - "nl": "Gas" - } - } - }, - - - "pair": [{ - "id": "start" - }, { - "id": "list_my_devices", - "template": "list_devices", - "navigation": { - "next": "add_my_devices" - } - }, { - "id": "add_my_devices", - "template": "add_devices" - }] - }, { - "id": "wattcher", - "name": { - "en": "Wattcher", - "nl": "Wattcher" - }, - "images": { - "large": "drivers/wattcher/assets/images/large.jpg", - "small": "drivers/wattcher/assets/images/small.jpg" - }, - "class": "sensor", - "capabilities": [ - "measure_power", - "meter_power" - ], - - "capabilitiesOptions": { - "meter_power": { - "title": { - "en": "Day usage", - "nl": "Dag totaal" - } - }, - - "measure_power": { - "title": { - "en": "Power current", - "nl": "Huidig vermogen" - } - - } - - }, - - "pair": [{ - "id": "start" - }, { - "id": "list_my_devices", - "template": "list_devices", - "navigation": { - "next": "add_my_devices" - } - }, { - "id": "add_my_devices", - "template": "add_devices" - }] - }], - "permissions": [ - "homey:manager:ledring" - ] + "_comment": "This file is generated. Please edit .homeycompose/app.json instead.", + "id": "com.homewizard", + "name": { + "en": "HomeWizard" + }, + "version": "3.14.24", + "platforms": [ + "local" + ], + "sdk": 3, + "brandColor": "#2fc052", + "compatibility": ">=12.9.0", + "description": { + "en": "Helps you understand and save" + }, + "category": [ + "energy", + "appliances", + "climate" + ], + "images": { + "xlarge": "assets/images/xlarge.png", + "large": "assets/images/large.png", + "small": "assets/images/small.png" + }, + "author": { + "name": "Jeroen Tebbens", + "email": "jeroen@tebbens.net" + }, + "contributors": { + "developers": [ + { + "name": "Jeroen Bos", + "email": "jeroenbos22@gmail.com" + }, + { + "name": "Nick Bockmeulen", + "email": "git@bockmeulen.nl" + }, + { + "name": "Jeroen Tebbens", + "email": "jeroen@tebbens.net" + }, + { + "name": "Freddie Welvering", + "email": "freddie@welvering.eu" + }, + { + "name": "Emile Nijssen", + "email": "emile@emilenijssen.nl" + }, + { + "name": "Dennie de Groot", + "email": "mail@denniedegroot.nl" + } + ] + }, + "contributing": { + "donate": { + "paypal": { + "username": "jtebbens" + } + } + }, + "bugs": { + "url": "https://community.homey.app/t/app-pro-homewizard/19267" + }, + "source": "https://github.com/jtebbens/com.homewizard", + "homeyCommunityTopicId": 19267, + "support": "https://community.homey.app/t/app-pro-homewizard/19267", + "permissions": [ + "homey:manager:ledring" + ], + "flow": { + "triggers": [ + { + "id": "leak_changed", + "title": { + "en": "Water leakage", + "nl": "Water lekkage" + }, + "args": [ + { + "name": "Kakusensor", + "type": "device", + "filter": "driver_id=kakusensor", + "placeholder": { + "en": "Which Sensor", + "nl": "Welke Sensor" + } + } + ], + "tokens": [ + { + "name": "leak_changed", + "type": "number", + "title": { + "en": "mm", + "nl": "mm" + }, + "example": 15 + } + ] + }, + { + "id": "meter_power.import", + "title": { + "en": "Import power changed", + "nl": "Importvermogen gewijzigd" + }, + "titleFormatted": { + "en": "Import power changed", + "nl": "Importvermogen gewijzigd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM230_v2" + } + ], + "tokens": [ + { + "name": "import_power", + "type": "number", + "title": { + "en": "Import Power (W)", + "nl": "Importvermogen (W)" + } + } + ] + }, + { + "id": "meter_power.export", + "title": { + "en": "Export power changed", + "nl": "Exportvermogen gewijzigd" + }, + "titleFormatted": { + "en": "Export power changed", + "nl": "Exportvermogen gewijzigd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM230_v2" + } + ], + "tokens": [ + { + "name": "export_power", + "type": "number", + "title": { + "en": "Export Power (W)", + "nl": "Exportvermogen (W)" + } + } + ] + }, + { + "id": "battery_mode_changed_SDM230_v2", + "title": { + "en": "Battery mode changed" + }, + "titleFormatted": { + "en": "Battery mode changed" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM230_v2" + } + ], + "tokens": [] + }, + { + "id": "battery_mode_changed_SDM630_v2", + "title": { + "en": "Battery mode changed" + }, + "titleFormatted": { + "en": "Battery mode changed" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM630_v2" + } + ], + "tokens": [] + }, + { + "id": "policy_recommendation_changed", + "title": { + "en": "Recommendation changed" + }, + "titleFormatted": { + "en": "Recommendation changed" + }, + "hint": { + "en": "Triggers when the policy engine changes its recommendation" + }, + "tokens": [ + { + "name": "mode", + "type": "string", + "title": { + "en": "Mode" + }, + "example": { + "en": "charge" + } + }, + { + "name": "confidence", + "type": "number", + "title": { + "en": "Confidence" + }, + "example": 75 + }, + { + "name": "reason", + "type": "string", + "title": { + "en": "Reason" + }, + "example": { + "en": "Strong sunlight expected" + } + } + ], + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "policy_mode_applied", + "title": { + "en": "Mode applied to battery" + }, + "titleFormatted": { + "en": "Mode applied to battery" + }, + "hint": { + "en": "Triggers when the policy engine applies a mode to the battery" + }, + "tokens": [ + { + "name": "mode", + "type": "string", + "title": { + "en": "Mode" + }, + "example": { + "en": "discharge" + } + }, + { + "name": "confidence", + "type": "number", + "title": { + "en": "Confidence" + }, + "example": 85 + } + ], + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "policy_override_set", + "title": { + "en": "Manual override activated" + }, + "titleFormatted": { + "en": "Manual override activated" + }, + "hint": { + "en": "Triggers when manual override is set" + }, + "tokens": [ + { + "name": "duration", + "type": "number", + "title": { + "en": "Duration" + }, + "example": 60 + } + ], + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "tariff_changed", + "title": { + "en": "Peak / Normal Tariff changed", + "nl": "Dal / Normaal Tarief veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy" + } + ], + "tokens": [ + { + "name": "tariff_changed", + "type": "number", + "title": { + "en": "tariff", + "nl": "tarief" + }, + "example": 1 + } + ] + }, + { + "id": "import_changed", + "title": { + "en": "Total used changed", + "nl": "Som gebruik veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy" + } + ], + "tokens": [ + { + "name": "import_changed", + "type": "number", + "title": { + "en": "import", + "nl": "import" + }, + "example": 1 + } + ] + }, + { + "id": "export_changed", + "title": { + "en": "Total delivered changed", + "nl": "Som teruglevering veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy" + } + ], + "tokens": [ + { + "name": "export_changed", + "type": "number", + "title": { + "en": "export", + "nl": "export" + }, + "example": 1 + } + ] + }, + { + "id": "voltage_sag_detected_v1", + "title": { + "en": "Voltage sag detected", + "nl": "Spanningsdip gedetecteerd" + }, + "hint": { + "en": "Triggered when a voltage sag is detected on any phase", + "nl": "Wordt geactiveerd wanneer een spanningsdip wordt gedetecteerd op een willekeurige fase" + }, + "tokens": [ + { + "name": "phase_l1", + "type": "number", + "title": { + "en": "Phase L1 count", + "nl": "Fase L1 aantal" + }, + "example": 5 + }, + { + "name": "phase_l2", + "type": "number", + "title": { + "en": "Phase L2 count", + "nl": "Fase L2 aantal" + }, + "example": 3 + }, + { + "name": "phase_l3", + "type": "number", + "title": { + "en": "Phase L3 count", + "nl": "Fase L3 aantal" + }, + "example": 2 + } + ], + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy" + } + ] + }, + { + "id": "voltage_swell_detected_v1", + "title": { + "en": "Voltage swell detected", + "nl": "Spanningspiek gedetecteerd" + }, + "hint": { + "en": "Triggered when a voltage swell is detected on any phase", + "nl": "Wordt geactiveerd wanneer een spanningspiek wordt gedetecteerd op een willekeurige fase" + }, + "tokens": [ + { + "name": "phase_l1", + "type": "number", + "title": { + "en": "Phase L1 count", + "nl": "Fase L1 aantal" + }, + "example": 2 + }, + { + "name": "phase_l2", + "type": "number", + "title": { + "en": "Phase L2 count", + "nl": "Fase L2 aantal" + }, + "example": 1 + }, + { + "name": "phase_l3", + "type": "number", + "title": { + "en": "Phase L3 count", + "nl": "Fase L3 aantal" + }, + "example": 0 + } + ], + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy" + } + ] + }, + { + "id": "long_power_fail_detected_v1", + "title": { + "en": "Long power failure detected", + "nl": "Lange stroomstoring gedetecteerd" + }, + "hint": { + "en": "Triggered when a long power failure is detected", + "nl": "Wordt geactiveerd wanneer een lange stroomstoring wordt gedetecteerd" + }, + "tokens": [ + { + "name": "count", + "type": "number", + "title": { + "en": "Failure count", + "nl": "Storing aantal" + }, + "example": 1 + } + ], + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy" + } + ] + }, + { + "id": "voltage_restored_v1", + "title": { + "en": "Voltage restored to normal", + "nl": "Spanning hersteld naar normaal" + }, + "hint": { + "en": "Triggers when voltage returns to normal range after a sag or swell", + "nl": "Triggert wanneer spanning terugkeert naar normaal bereik na een dip of piek" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy" + } + ], + "tokens": [ + { + "name": "phase", + "type": "string", + "title": { + "en": "Phase", + "nl": "Fase" + }, + "example": "L1" + }, + { + "name": "voltage", + "type": "number", + "title": { + "en": "Voltage (V)", + "nl": "Spanning (V)" + }, + "example": 230 + } + ] + }, + { + "id": "power_restored_v1", + "title": { + "en": "Power restored", + "nl": "Stroom hersteld" + }, + "hint": { + "en": "Triggers when power is restored after being offline", + "nl": "Triggert wanneer stroom is hersteld na een uitval" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy" + } + ], + "tokens": [ + { + "name": "offline_duration", + "type": "number", + "title": { + "en": "Offline duration (seconds)", + "nl": "Offline duur (seconden)" + }, + "example": 120 + } + ] + }, + { + "id": "battery_mode_changed", + "title": { + "en": "Battery mode changed" + }, + "titleFormatted": { + "en": "Battery mode changed" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ], + "tokens": [] + }, + { + "id": "battery_error_detected", + "title": { + "en": "Battery error detected" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ] + }, + { + "id": "battery_group_state_changed", + "title": { + "en": "Battery group state changed" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ] + }, + { + "id": "tariff_changed_v2", + "title": { + "en": "Peak / Normal Tariff changed", + "nl": "Dal / Normaal Tarief veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ], + "tokens": [ + { + "name": "tariff", + "type": "number", + "title": { + "en": "tariff", + "nl": "tarief" + }, + "example": 1 + } + ] + }, + { + "id": "import_changed_v2", + "title": { + "en": "Total used changed", + "nl": "Som gebruik veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ], + "tokens": [ + { + "name": "import", + "type": "number", + "title": { + "en": "import", + "nl": "import" + }, + "example": 1 + } + ] + }, + { + "id": "export_changed_v2", + "title": { + "en": "Total delivered changed", + "nl": "Som teruglevering veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ], + "tokens": [ + { + "name": "export", + "type": "number", + "title": { + "en": "export", + "nl": "export" + }, + "example": 1 + } + ] + }, + { + "id": "voltage_sag_detected", + "title": { + "en": "Voltage sag detected", + "nl": "Spanningsdip gedetecteerd" + }, + "hint": { + "en": "Triggers when a voltage sag (dip) is detected on any phase", + "nl": "Triggert wanneer een spanningsdip wordt gedetecteerd op een fase" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ], + "tokens": [ + { + "name": "phase", + "type": "string", + "title": { + "en": "Phase", + "nl": "Fase" + }, + "example": "L1" + }, + { + "name": "count", + "type": "number", + "title": { + "en": "Total count", + "nl": "Totaal aantal" + }, + "example": 1 + } + ] + }, + { + "id": "voltage_swell_detected", + "title": { + "en": "Voltage swell detected", + "nl": "Spanningspiek gedetecteerd" + }, + "hint": { + "en": "Triggers when a voltage swell (surge) is detected on any phase", + "nl": "Triggert wanneer een spanningspiek wordt gedetecteerd op een fase" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ], + "tokens": [ + { + "name": "phase", + "type": "string", + "title": { + "en": "Phase", + "nl": "Fase" + }, + "example": "L1" + }, + { + "name": "count", + "type": "number", + "title": { + "en": "Total count", + "nl": "Totaal aantal" + }, + "example": 1 + } + ] + }, + { + "id": "long_power_fail_detected", + "title": { + "en": "Long power failure detected", + "nl": "Lange stroomuitval gedetecteerd" + }, + "hint": { + "en": "Triggers when a long power failure is detected (>1 minute)", + "nl": "Triggert wanneer een lange stroomuitval wordt gedetecteerd (>1 minuut)" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ], + "tokens": [ + { + "name": "count", + "type": "number", + "title": { + "en": "Total count", + "nl": "Totaal aantal" + }, + "example": 1 + } + ] + }, + { + "id": "voltage_restored", + "title": { + "en": "Voltage restored to normal", + "nl": "Spanning hersteld naar normaal" + }, + "hint": { + "en": "Triggers when voltage returns to normal range after a sag or swell", + "nl": "Triggert wanneer spanning terugkeert naar normaal bereik na een dip of piek" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ], + "tokens": [ + { + "name": "phase", + "type": "string", + "title": { + "en": "Phase", + "nl": "Fase" + }, + "example": "L1" + }, + { + "name": "voltage", + "type": "number", + "title": { + "en": "Voltage (V)", + "nl": "Spanning (V)" + }, + "example": 230 + } + ] + }, + { + "id": "power_restored", + "title": { + "en": "Power restored", + "nl": "Stroom hersteld" + }, + "hint": { + "en": "Triggers when power is restored after being offline", + "nl": "Triggert wanneer stroom is hersteld na een uitval" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ], + "tokens": [ + { + "name": "offline_duration", + "type": "number", + "title": { + "en": "Offline duration (seconds)", + "nl": "Offline duur (seconden)" + }, + "example": 120 + } + ] + }, + { + "id": "power_used_changed", + "title": { + "en": "Power used changed", + "nl": "Huidig vermogen veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "power_used", + "type": "number", + "title": { + "en": "Watt", + "nl": "Watt" + }, + "example": 15 + } + ] + }, + { + "id": "power_s1_changed", + "title": { + "en": "Power production changed", + "nl": "Huidige productie veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "power_s1", + "type": "number", + "title": { + "en": "Watt", + "nl": "Watt" + }, + "example": 15 + } + ] + }, + { + "id": "power_s2_changed", + "title": { + "en": "Power usage S2 changed", + "nl": "Huidige gebruik S2 veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "power_s2", + "type": "number", + "title": { + "en": "Watt", + "nl": "Watt" + }, + "example": 15 + } + ] + }, + { + "id": "power_netto_changed", + "title": { + "en": "Daily netto usage changed", + "nl": "Dag netto verbruik veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "netto_power_used", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_power_used_changed", + "title": { + "en": "Daily usage changed", + "nl": "Dag verbruik veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "power_daytotal_used", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_power_aggregated_changed", + "title": { + "en": "Overall usage changed", + "nl": "Netto verbruik veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "power_daytotal_aggr", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_power_s1_changed", + "title": { + "en": "Daily production changed", + "nl": "Dag productie veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "power_daytotal_s1", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_power_s2_changed", + "title": { + "en": "Daily usage S2 changed", + "nl": "Dag gebruik S2 veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "power_daytotal_s2", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_return_t1_changed", + "title": { + "en": "Meter return t1 changed", + "nl": "Meter teruglevering t1 veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "meter_power_produced_t1", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_return_t2_changed", + "title": { + "en": "Meter return t2 changed", + "nl": "Meter teruglevering t2 veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energylink" + } + ], + "tokens": [ + { + "name": "meter_power_produced_t2", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "preset_changed", + "title": { + "en": "Preset has changed", + "nl": "Preset is veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=homewizard" + } + ], + "tokens": [ + { + "name": "preset", + "type": "number", + "title": { + "en": "Preset", + "nl": "Preset" + }, + "example": 1 + }, + { + "name": "preset_text", + "type": "string", + "title": { + "en": "Text", + "nl": "Tekst" + }, + "example": "Home" + } + ] + }, + { + "id": "battery_state_changed", + "title": { + "en": "Battery state changed" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=plugin_battery" + }, + { + "name": "state", + "type": "dropdown", + "title": { + "en": "Charging state" + }, + "values": [ + { + "id": "charging", + "name": { + "en": "Charging" + } + }, + { + "id": "discharging", + "name": { + "en": "Discharging" + } + }, + { + "id": "idle", + "name": { + "en": "Idle" + } + } + ] + } + ], + "titleFormatted": { + "en": "Battery state changed to [[state]]" + } + }, + { + "id": "battery_low_runtime", + "title": { + "en": "Battery time to empty is low" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=plugin_battery" + }, + { + "name": "minutes", + "type": "number", + "title": { + "en": "Minutes remaining" + } + } + ], + "titleFormatted": { + "en": "Battery time to empty is [[minutes]] minutes" + } + }, + { + "id": "battery_full", + "title": { + "en": "Battery is fully charged" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=plugin_battery" + } + ] + }, + { + "id": "battery_soc_drift_detected", + "title": { + "en": "Battery SoC Drift Detected" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=plugin_battery" + } + ], + "titleFormatted": { + "en": "Battery SoC drift detected", + "nl": "Batterij SoC-afwijking gedetecteerd" + } + }, + { + "id": "net_frequency_out_of_range", + "title": { + "en": "Network frequency out of range", + "nl": "Netwerkfrequentie buiten bereik" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=plugin_battery" + } + ], + "titleFormatted": { + "en": "Network frequency is out of range", + "nl": "Netwerkfrequentie is buiten bereik" + } + }, + { + "id": "rainmeter_value_changed", + "title": { + "en": "Rainmeter value changed", + "nl": "Regenmeter waarde veranderd" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=rainmeter" + } + ], + "tokens": [ + { + "name": "rainmeter_changed", + "type": "number", + "title": { + "en": "mm", + "nl": "mm" + }, + "example": 15 + } + ] + }, + { + "id": "temp_not_changed_trigger", + "title": { + "en": "Temperature unchanged for X hours", + "nl": "Temperatuur niet veranderd sinds X uur" + }, + "titleFormatted": { + "en": "Temperature unchanged for [[hours]] hours", + "nl": "Temperatuur niet veranderd sinds [[hours]] uren" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=thermometer" + }, + { + "name": "hours", + "type": "number", + "title": { + "en": "Hours", + "nl": "Uren" + } + } + ] + } + ], + "conditions": [ + { + "title": { + "en": "Check Battery Mode", + "nl": "Controleer batterij Modus" + }, + "titleFormatted": { + "en": "Check battery mode !{{is|isn't}} [[mode]]", + "nl": "Controleer batterij modus !{{is|isn't}} [[mode]]" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + }, + { + "type": "dropdown", + "name": "mode", + "values": [ + { + "id": "zero", + "label": { + "en": "Zero mode", + "nl": "Null op de meter" + } + }, + { + "id": "to_full", + "label": { + "en": "Full Charge", + "nl": "Volledig laden" + } + }, + { + "id": "standby", + "label": { + "en": "Standby", + "nl": "Standby" + } + }, + { + "id": "zero_charge_only", + "label": { + "en": "Zero mode, Charge Only", + "nl": "Nul op de Meter, Alleen laden" + } + }, + { + "id": "zero_discharge_only", + "label": { + "en": "Zero mode, Discharge Only", + "nl": "Nul op de Meter, Alleen ontladen" + } + } + ] + } + ], + "id": "check-battery-mode" + }, + { + "id": "policy_is_enabled", + "title": { + "en": "Policy is !{{enabled|disabled}}" + }, + "titleFormatted": { + "en": "Policy is !{{enabled|disabled}}" + }, + "hint": { + "en": "Check if the policy engine is active" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "confidence_above", + "title": { + "en": "Confidence is above" + }, + "titleFormatted": { + "en": "Confidence is above [[threshold]]" + }, + "hint": { + "en": "Check if recommendation confidence exceeds threshold" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "threshold", + "type": "number", + "placeholder": { + "en": "70" + }, + "min": 0, + "max": 100 + } + ] + }, + { + "id": "recommended_mode_is", + "title": { + "en": "Recommended mode is" + }, + "titleFormatted": { + "en": "Recommended mode is [[mode]]" + }, + "hint": { + "en": "Check if current recommendation matches a specific mode" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "mode", + "type": "dropdown", + "values": [ + { + "id": "charge", + "label": { + "en": "Charge" + } + }, + { + "id": "discharge", + "label": { + "en": "Discharge" + } + }, + { + "id": "preserve", + "label": { + "en": "Preserve" + } + } + ] + } + ] + }, + { + "id": "sun_score_above", + "title": { + "en": "Sun score is above" + }, + "titleFormatted": { + "en": "Sun score is above [[threshold]]" + }, + "hint": { + "en": "Check if sunshine availability exceeds threshold" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "threshold", + "type": "number", + "placeholder": { + "en": "50" + }, + "min": 0, + "max": 100 + } + ] + }, + { + "id": "check_preset", + "title": { + "en": "Preset !{{is|isn't}}", + "nl": "Preset !{{is|is niet}}" + }, + "titleFormatted": { + "en": "Check !{{is|isn't}} [[preset]]", + "nl": "Controleer !{{is|isn't}} [[preset]]" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=homewizard" + }, + { + "name": "preset", + "type": "dropdown", + "values": [ + { + "id": "0", + "label": { + "en": "Home", + "nl": "Thuis" + } + }, + { + "id": "1", + "label": { + "en": "Away", + "nl": "Afwezig" + } + }, + { + "id": "2", + "label": { + "en": "Sleep", + "nl": "Slapen" + } + }, + { + "id": "3", + "label": { + "en": "Holiday", + "nl": "Vakantie" + } + } + ] + } + ] + }, + { + "id": "temp_not_changed_hours", + "title": { + "en": "Temperature not changed for", + "nl": "Temperatuur niet veranderd sinds" + }, + "titleFormatted": { + "en": "Temperature not changed for [[hours]] hours", + "nl": "Temperatuur niet veranderd sinds [[hours]] uren" + }, + "hint": { + "en": "Checks if the temperature has not changed for the given number of hours.", + "nl": "Controleert of de temperatuur niet veranderd is gedurende het opgegeven aantal uren." + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=thermometer" + }, + { + "name": "hours", + "type": "number", + "title": { + "en": "Hours", + "nl": "Uren" + } + } + ] + } + ], + "actions": [ + { + "id": "sdm230-set-battery-to-full-charge-mode", + "title": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "titleFormatted": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM230_v2" + } + ] + }, + { + "id": "sdm230-set-battery-to-zero-mode", + "title": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "titleFormatted": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM230_v2" + } + ] + }, + { + "id": "sdm230-set-battery-to-standby-mode", + "title": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "titleFormatted": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM230_v2" + } + ] + }, + { + "id": "sdm230-set-battery-to-zero-charge-only-mode", + "title": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Alleen Opladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Alleen Opladen modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM230_v2" + } + ] + }, + { + "id": "sdm230-set-battery-to-zero-discharge-only-mode", + "title": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Alleen Ontladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Alleen Ontladen modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM230_v2" + } + ] + }, + { + "id": "sdm630-set-battery-to-full-charge-mode", + "title": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "titleFormatted": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM630_v2" + } + ] + }, + { + "id": "sdm630-set-battery-to-zero-mode", + "title": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "titleFormatted": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM630_v2" + } + ] + }, + { + "id": "sdm630-set-battery-to-standby-mode", + "title": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "titleFormatted": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM630_v2" + } + ] + }, + { + "id": "sdm630-set-battery-to-zero-charge-only-mode", + "title": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Alleen Opladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Alleen Opladen modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM630_v2" + } + ] + }, + { + "id": "sdm630-set-battery-to-zero-discharge-only-mode", + "title": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Alleen Ontladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Alleen Ontladen modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=SDM630_v2" + } + ] + }, + { + "id": "enable_policy", + "title": { + "en": "Enable policy" + }, + "titleFormatted": { + "en": "Enable policy" + }, + "hint": { + "en": "Enable the battery policy engine" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "disable_policy", + "title": { + "en": "Disable policy" + }, + "titleFormatted": { + "en": "Disable policy" + }, + "hint": { + "en": "Disable the battery policy engine" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "set_policy_mode", + "title": { + "en": "Set policy mode to" + }, + "titleFormatted": { + "en": "Set policy mode to [[mode]]" + }, + "hint": { + "en": "Change the policy optimization mode" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "mode", + "type": "dropdown", + "values": [ + { + "id": "eco", + "label": { + "en": "Eco" + } + }, + { + "id": "balanced", + "label": { + "en": "Balanced" + } + }, + { + "id": "aggressive", + "label": { + "en": "Aggressive" + } + } + ] + } + ] + }, + { + "id": "enable_auto_apply", + "title": { + "en": "Enable auto-apply" + }, + "titleFormatted": { + "en": "Enable auto-apply" + }, + "hint": { + "en": "Automatically apply policy recommendations" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "disable_auto_apply", + "title": { + "en": "Disable auto-apply" + }, + "titleFormatted": { + "en": "Disable auto-apply" + }, + "hint": { + "en": "Stop automatically applying recommendations" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "set_override", + "title": { + "en": "Set manual override for" + }, + "titleFormatted": { + "en": "Set manual override for [[duration]] minutes" + }, + "hint": { + "en": "Prevent automatic policy changes for a duration" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "duration", + "type": "number", + "placeholder": { + "en": "60" + }, + "min": 15, + "max": 1440, + "units": { + "en": "minutes" + } + } + ] + }, + { + "id": "clear_override", + "title": { + "en": "Clear manual override" + }, + "titleFormatted": { + "en": "Clear manual override" + }, + "hint": { + "en": "Resume automatic policy management" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "force_policy_check", + "title": { + "en": "Run policy check now" + }, + "titleFormatted": { + "en": "Run policy check now" + }, + "hint": { + "en": "Force immediate policy recalculation" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "refresh_weather", + "title": { + "en": "Refresh weather forecast" + }, + "titleFormatted": { + "en": "Refresh weather forecast" + }, + "hint": { + "en": "Fetch latest weather data immediately" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "set_weather_override", + "title": { + "en": "Set weather override" + }, + "titleFormatted": { + "en": "Set weather override to [[override]]" + }, + "hint": { + "en": "Override the weather forecast for policy decisions" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "override", + "type": "dropdown", + "title": { + "en": "Weather condition" + }, + "values": [ + { + "id": "auto", + "title": { + "en": "Auto (use forecast)" + } + }, + { + "id": "sunny", + "title": { + "en": "Sunny" + } + }, + { + "id": "cloudy", + "title": { + "en": "Cloudy" + } + }, + { + "id": "rainy", + "title": { + "en": "Rainy" + } + } + ] + } + ] + }, + { + "id": "update_pv_production", + "title": { + "en": "Update PV production", + "nl": "PV-productie bijwerken" + }, + "titleFormatted": { + "en": "Update PV production to [[power]] watts", + "nl": "PV-productie bijwerken naar [[power]] watt" + }, + "hint": { + "en": "Provide real-time PV production data from your solar system. This overrides the sun-based estimate.", + "nl": "Geef realtime PV-productiegegevens van uw zonnesysteem. Dit overschrijft de op zon gebaseerde schatting." + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "power", + "type": "number", + "title": { + "en": "PV Power", + "nl": "PV-vermogen" + }, + "placeholder": { + "en": "1500" + }, + "min": 0, + "max": 50000, + "units": { + "en": "W", + "nl": "W" + } + } + ] + }, + { + "id": "reset_learning_data", + "title": { + "en": "Reset learning data", + "nl": "Leergegevens resetten" + }, + "titleFormatted": { + "en": "Reset all learning data", + "nl": "Alle leergegevens resetten" + }, + "hint": { + "en": "Clear all learned patterns and start fresh. Use when moving or changing your setup.", + "nl": "Wis alle geleerde patronen en begin opnieuw. Gebruik bij verhuizing of wijziging van uw opstelling." + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "set-battery-to-full-charge-mode", + "title": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "titleFormatted": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ] + }, + { + "id": "set-battery-to-zero-mode", + "title": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "titleFormatted": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Nul op de meter modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ] + }, + { + "id": "set-battery-to-standby-mode", + "title": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "titleFormatted": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ] + }, + { + "id": "set-battery-to-zero-charge-only-mode", + "title": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Nul op de meter alleen opladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Nul op de meter alleen opladen modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ] + }, + { + "id": "set-battery-to-zero-discharge-only-mode", + "title": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Nul op de meter, alleen ontladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Nul op de meter, alleen ontladen modus" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_v2" + } + ] + }, + { + "id": "set_preset", + "title": { + "en": "Activate preset", + "nl": "Activeer preset" + }, + "titleFormatted": { + "en": "Active preset [[preset]]", + "nl": "Activeer preset [[preset]]" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=homewizard" + }, + { + "name": "preset", + "type": "dropdown", + "values": [ + { + "id": "0", + "label": { + "en": "Home", + "nl": "Thuis" + } + }, + { + "id": "1", + "label": { + "en": "Away", + "nl": "Afwezig" + } + }, + { + "id": "2", + "label": { + "en": "Sleep", + "nl": "Slapen" + } + }, + { + "id": "3", + "label": { + "en": "Holiday", + "nl": "Vakantie" + } + } + ] + } + ] + }, + { + "id": "switch_scene_on", + "title": { + "en": "Switch scene on", + "nl": "Zet scene aan" + }, + "titleFormatted": { + "en": "Active scene [[scene]]", + "nl": "Activeer scene [[scene]]" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=homewizard" + }, + { + "name": "scene", + "type": "autocomplete" + } + ] + }, + { + "id": "switch_scene_off", + "title": { + "en": "Switch scene off", + "nl": "Zet scene uit" + }, + "titleFormatted": { + "en": "Deactive scene [[scene]]", + "nl": "Deactiveer scene [[scene]]" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=homewizard" + }, + { + "name": "scene", + "type": "autocomplete" + } + ] + }, + { + "id": "heatlink_off", + "title": { + "en": "Heatlink off", + "nl": "Zet heatlink uit" + }, + "titleFormatted": { + "en": "Deactive heatlink [[device]]", + "nl": "Deactiveer heatlink [[device]]" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=homewizard" + } + ] + } + ] + }, + "drivers": [ + { + "name": { + "en": "kWh Meter (1 phase)" + }, + "images": { + "large": "drivers/SDM230/assets/images/large.png", + "small": "drivers/SDM230/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM230", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "measure_current", + "measure_power.l1", + "measure_voltage", + "alarm_connectivity", + "rssi" + ], + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + }, + "insights": true + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + }, + "insights": true + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + }, + "insights": true + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "id": "SDM230", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "min": 1, + "unit": { + "en": "s" + } + }, + { + "id": "cloud", + "type": "number", + "label": { + "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { + "en": "url", + "nl": "url" + } + } + ] + }, + { + "name": { + "en": "kWh Meter (1 phase) P1 mode" + }, + "images": { + "large": "drivers/SDM230-p1mode/assets/images/large.png", + "small": "drivers/SDM230-p1mode/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM230", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "measure_current", + "measure_power.l1", + "measure_voltage", + "rssi" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.consumed.t1", + "cumulativeExportedCapability": "meter_power.produced.t1" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + }, + "insights": true + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + }, + "insights": true + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + }, + "insights": true + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "id": "SDM230-p1mode", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "min": 1, + "unit": { + "en": "s" + } + }, + { + "id": "cloud", + "type": "number", + "label": { + "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { + "en": "url", + "nl": "url" + } + }, + { + "id": "baseload_notifications", + "type": "checkbox", + "label": { + "en": "Enable baseload notifications", + "nl": "Sluipverbruik meldingen inschakelen" + }, + "value": true + } + ] + }, + { + "name": { + "en": "kWh Meter 1P (APIv2)" + }, + "images": { + "large": "drivers/SDM230_v2/assets/images/large.png", + "small": "drivers/SDM230_v2/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM230_v2", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "measure_current", + "meter_power.import", + "meter_power.export", + "measure_voltage" + ], + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "meter_power.import": { + "decimals": 3, + "title": { + "en": "Total usage", + "nl": "Totaal gebruik" + } + }, + "meter_power.export": { + "decimals": 3, + "title": { + "en": "Total deliver", + "nl": "Totaal teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + }, + "insights": true + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + }, + "insights": true + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + }, + "insights": true + }, + "measure_power.battery_group_power_w": { + "title": { + "en": "Battery group Current combined Power", + "nl": "Battery groep Huidig samengesteld vermogen" + } + }, + "measure_power.battery_group_target_power_w": { + "title": { + "en": "Battery group Target Power", + "nl": "Battery groep Doel vermogen" + } + }, + "measure_power.battery_group_max_consumption_w": { + "title": { + "en": "Battery group Max allowed Consumption Power", + "nl": "Battery groep Max toegestaand gebruiksvermogen" + } + }, + "measure_power.battery_group_max_production_w": { + "title": { + "en": "Battery group Max allowed Production Power", + "nl": "Battery groep Max toegestaand leveringssvermogen" + } + } + }, + "pair": [ + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "authorize" + }, + "options": { + "singular": true + } + }, + { + "id": "authorize", + "navigation": { + "prev": "list_devices" + } + } + ], + "id": "SDM230_v2", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "min": 1, + "unit": { + "en": "s" + } + } + ] + }, + { + "name": { + "en": "kWh Meter (3 phase)" + }, + "images": { + "large": "drivers/SDM630/assets/images/large.png", + "small": "drivers/SDM630/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM630", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "meter_power.l1", + "meter_power.l2", + "meter_power.l3", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "meter_power.day.l1", + "meter_power.day.l2", + "meter_power.day.l3", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3", + "rssi" + ], + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "meter_power.day.l1": { + "decimals": 3, + "title": { + "en": "Daily usage Phase 1", + "nl": "Dagverbruik Fase 1" + } + }, + "meter_power.day.l2": { + "decimals": 3, + "title": { + "en": "Daily usage Phase 2", + "nl": "Dagverbruik Fase 2" + } + }, + "meter_power.day.l3": { + "decimals": 3, + "title": { + "en": "Daily usage Phase 3", + "nl": "Dagverbruik Fase 3" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "meter_power.l1": { + "decimals": 3, + "title": { + "en": "Total usage KWh Phase 1", + "nl": "Totaal verbruik KWh Fase 1" + } + }, + "meter_power.l2": { + "decimals": 3, + "title": { + "en": "Total usage KWh Phase 2", + "nl": "Totaal verbruik KWh Fase 2" + } + }, + "meter_power.l3": { + "decimals": 3, + "title": { + "en": "Total usage KWh Phase 3", + "nl": "Totaal verbruik KWh Fase 3" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + } + }, + "measure_voltage.l2": { + "title": { + "en": "Current Voltage phase 2", + "nl": "Huidig Voltage fase 2" + } + }, + "measure_voltage.l3": { + "title": { + "en": "Current Voltage phase 3", + "nl": "Huidig Voltage fase 3" + } + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "id": "SDM630", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "min": 1, + "unit": { + "en": "s" + } + }, + { + "id": "cloud", + "type": "number", + "label": { + "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { + "en": "url", + "nl": "url" + } + } + ] + }, + { + "name": { + "en": "kWh Meter (3 phase) P1 mode" + }, + "images": { + "large": "drivers/SDM630-p1mode/assets/images/large.png", + "small": "drivers/SDM630-p1mode/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM630", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3", + "rssi" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.consumed.t1", + "cumulativeExportedCapability": "meter_power.produced.t1" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + } + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + } + }, + "measure_voltage.l2": { + "title": { + "en": "Current Voltage phase 2", + "nl": "Huidig Voltage fase 2" + } + }, + "measure_voltage.l3": { + "title": { + "en": "Current Voltage phase 3", + "nl": "Huidig Voltage fase 3" + } + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "id": "SDM630-p1mode", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "unit": { + "en": "s" + } + }, + { + "id": "url", + "type": "text", + "label": { + "en": "url", + "nl": "url" + } + } + ] + }, + { + "name": { + "en": "kWh Meter 3P (APIv2)" + }, + "images": { + "large": "drivers/SDM630_v2/assets/images/large.png", + "small": "drivers/SDM630_v2/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM630_v2", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "meter_power.import", + "meter_power.export", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "measure_current", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3" + ], + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "meter_power.import": { + "decimals": 3, + "title": { + "en": "Total usage", + "nl": "Totaal gebruik" + } + }, + "meter_power.export": { + "decimals": 3, + "title": { + "en": "Total deliver", + "nl": "Totaal teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "measure_power.battery_group_power_w": { + "title": { + "en": "Battery group Current combined Power", + "nl": "Battery groep Huidig samengesteld vermogen" + } + }, + "measure_power.battery_group_target_power_w": { + "title": { + "en": "Battery group Target Power", + "nl": "Battery groep Doel vermogen" + } + }, + "measure_power.battery_group_max_consumption_w": { + "title": { + "en": "Battery group Max allowed Consumption Power", + "nl": "Battery groep Max toegestaand gebruiksvermogen" + } + }, + "measure_power.battery_group_max_production_w": { + "title": { + "en": "Battery group Max allowed Production Power", + "nl": "Battery groep Max toegestaand leveringssvermogen" + } + } + }, + "pair": [ + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "authorize" + }, + "options": { + "singular": true + } + }, + { + "id": "authorize", + "navigation": { + "prev": "list_devices" + } + } + ], + "id": "SDM630_v2", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "min": 1, + "unit": { + "en": "s" + } + } + ] + }, + { + "id": "battery-policy", + "name": { + "en": "Battery Policy Manager" + }, + "class": "other", + "capabilities": [ + "policy_enabled", + "policy_mode", + "auto_apply", + "recommended_mode", + "active_mode", + "sun_score", + "predicted_sun_hours", + "confidence_score", + "explanation_summary", + "policy_debug_price", + "policy_debug_top3low", + "policy_debug_top3high", + "policy_debug_sun", + "policy_debug_learning", + "battery_soc_mirror", + "grid_power_mirror", + "battery_rte", + "last_update", + "override_until", + "weather_override" + ], + "pair": [ + { + "id": "select_battery", + "template": "list_devices", + "navigation": { + "next": "configure_policy" + }, + "options": { + "singular": true, + "title": { + "en": "Select Battery Device" + }, + "instruction": { + "en": "Select the HomeWizard battery device to manage" + } + } + }, + { + "id": "configure_policy", + "template": "add_devices", + "navigation": { + "prev": "select_battery" + } + } + ], + "settings": [ + { + "type": "group", + "label": { + "en": "Battery Device", + "nl": "Batterijapparaat" + }, + "children": [ + { + "id": "p1_device_id", + "type": "label", + "label": { + "en": "Linked P1 (Energy v2)", + "nl": "Gekoppelde P1 (Energy v2)" + }, + "value": "Not configured", + "hint": { + "en": "The HomeWizard P1 (energy_v2) device this policy uses for grid & battery group data. Use the repair tool to change.", + "nl": "Het HomeWizard P1‑apparaat (energy_v2) dat deze policy gebruikt voor net‑ en batterijgroepgegevens. Gebruik de reparatietool om dit te wijzigen." + } + } + ] + }, + { + "type": "group", + "label": { + "en": "Policy Behavior", + "nl": "Policy‑gedrag" + }, + "children": [ + { + "id": "policy_interval", + "type": "number", + "label": { + "en": "Policy Check Interval", + "nl": "Policy‑controle‑interval" + }, + "units": { + "en": "minutes", + "nl": "minuten" + }, + "value": 15, + "min": 5, + "max": 60, + "step": 5 + }, + { + "id": "min_confidence_threshold", + "type": "number", + "label": { + "en": "Minimum Confidence", + "nl": "Minimale zekerheid" + }, + "units": { + "en": "%", + "nl": "%" + }, + "value": 55, + "min": 0, + "max": 100, + "step": 5 + } + ] + }, + { + "type": "group", + "label": { + "en": "Tariff Configuration", + "nl": "Tariefinstellingen" + }, + "children": [ + { + "id": "tariff_type", + "type": "dropdown", + "label": { + "en": "Tariff Type", + "nl": "Type tarief" + }, + "value": "fixed", + "values": [ + { + "id": "fixed", + "label": { + "en": "Fixed Rate (Peak Shaving)", + "nl": "Vast tarief (Peak Shaving)" + } + }, + { + "id": "dynamic", + "label": { + "en": "Dynamic Pricing", + "nl": "Dynamische prijzen" + } + } + ] + }, + { + "id": "peak_hours", + "type": "text", + "label": { + "en": "Peak Hours", + "nl": "Piekuren" + }, + "value": "17:00-21:00", + "visible": { + "when": "tariff_type", + "is": "fixed" + } + }, + { + "id": "enable_dynamic_pricing", + "type": "checkbox", + "label": { + "en": "Enable Dynamic Pricing Provider", + "nl": "Dynamische prijsprovider inschakelen" + }, + "value": false, + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "respect_minmax", + "type": "checkbox", + "label": { + "en": "Strictly respect min/max prices", + "nl": "Min/max prijzen strikt respecteren" + }, + "hint": { + "en": "When enabled (default), battery only charges/discharges within configured price limits. When disabled, the system can make opportunistic decisions outside these limits when profitable (e.g., charging at €0.17 when future price is €0.30). Recommended: ENABLED for 2026 (with net metering), DISABLED for 2027+ (without net metering).", + "nl": "Indien ingeschakeld (standaard) laadt/ontlaadt de batterij alleen binnen geconfigureerde prijslimieten. Indien uitgeschakeld kan het systeem opportunistische beslissingen nemen buiten deze limieten wanneer winstgevend (bijv. laden op €0,17 wanneer toekomstige prijs €0,30 is). Aanbevolen: INGESCHAKELD voor 2026 (met salderen), UITGESCHAKELD voor 2027+ (zonder salderen)." + }, + "value": true, + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "opportunistic_charge_multiplier", + "type": "number", + "label": { + "en": "Opportunistic charge spread multiplier", + "nl": "Opportunistisch laden spread vermenigvuldiger" + }, + "hint": { + "en": "Multiplier for min_profit_margin to trigger opportunistic charging above max_charge_price. Formula: spread > (min_profit_margin × multiplier). Higher values = more conservative. Examples: 1.5 (aggressive, €0.015 spread), 2.0 (balanced, €0.02 spread), 3.0 (conservative, €0.03 spread). Only active when 'Strictly respect min/max prices' is DISABLED.", + "nl": "Vermenigvuldiger voor min_profit_margin om opportunistisch laden boven max_charge_price te activeren. Formule: spread > (min_profit_margin × vermenigvuldiger). Hogere waarden = conservatiever. Voorbeelden: 1,5 (agressief, €0,015 spread), 2,0 (gebalanceerd, €0,02 spread), 3,0 (conservatief, €0,03 spread). Alleen actief wanneer 'Min/max prijzen strikt respecteren' is UITGESCHAKELD." + }, + "value": 2, + "min": 1, + "max": 5, + "step": 0.5, + "units": "×", + "visible": { + "when": "respect_minmax", + "is": false + } + }, + { + "id": "opportunistic_discharge_floor", + "type": "number", + "label": { + "en": "Opportunistic discharge price floor", + "nl": "Opportunistisch ontladen prijsbodem" + }, + "hint": { + "en": "Minimum price required for opportunistic discharge when current price is below min_discharge_price. Lower values = discharge at lower prices (more aggressive). Examples: €0.10 (very aggressive), €0.20 (balanced), €0.30 (conservative). Only active when 'Strictly respect min/max prices' is DISABLED.", + "nl": "Minimale prijs vereist voor opportunistisch ontladen wanneer huidige prijs onder min_discharge_price ligt. Lagere waarden = ontladen bij lagere prijzen (agressiever). Voorbeelden: €0,10 (zeer agressief), €0,20 (gebalanceerd), €0,30 (conservatief). Alleen actief wanneer 'Min/max prijzen strikt respecteren' is UITGESCHAKELD." + }, + "value": 0.2, + "min": 0.1, + "max": 0.35, + "step": 0.01, + "units": "€/kWh", + "visible": { + "when": "respect_minmax", + "is": false + } + }, + { + "id": "opportunistic_discharge_spread_threshold", + "type": "number", + "label": { + "en": "Opportunistic discharge spread threshold", + "nl": "Opportunistisch ontladen spread drempel" + }, + "hint": { + "en": "Maximum negative spread to allow opportunistic discharge (when no better future price expected). More negative = stricter (less willing to discharge early). Formula: (future_max_price × efficiency) - current_price < threshold. Examples: -€0.10 (very strict), -€0.05 (balanced), -€0.01 (aggressive). Only active when 'Strictly respect min/max prices' is DISABLED.", + "nl": "Maximale negatieve spread om opportunistisch ontladen toe te staan (wanneer geen betere toekomstige prijs verwacht). Negatiever = strenger (minder bereid vroeg te ontladen). Formule: (toekomstige_max_prijs × efficiëntie) - huidige_prijs < drempel. Voorbeelden: -€0,10 (zeer streng), -€0,05 (gebalanceerd), -€0,01 (agressief). Alleen actief wanneer 'Min/max prijzen strikt respecteren' is UITGESCHAKELD." + }, + "value": -0.05, + "min": -0.1, + "max": -0.01, + "step": 0.01, + "units": "€/kWh", + "visible": { + "when": "respect_minmax", + "is": false + } + }, + { + "id": "max_charge_price", + "type": "number", + "label": { + "en": "Max Charge Price (€/kWh)", + "nl": "Maximale laadprijs (€/kWh)" + }, + "value": 0.15, + "min": 0, + "max": 1, + "step": 0.01, + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "min_discharge_price", + "type": "number", + "label": { + "en": "Min Discharge Price (€/kWh)", + "nl": "Minimale ontlaadprijs (€/kWh)" + }, + "value": 0.3, + "min": 0, + "max": 1, + "step": 0.01, + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "min_profit_margin", + "type": "number", + "label": { + "en": "Min Arbitrage Profit (€/kWh)", + "nl": "Minimale arbitragewinst (€/kWh)" + }, + "hint": { + "en": "Minimum profit per kWh (after battery losses) before grid charging is attempted. Higher = fewer but more profitable cycles. 0.00 = always charge at cheap hours.", + "nl": "Minimale winst per kWh (na batterijverlies) voordat laden uit het net wordt geprobeerd. Hoger = minder maar winstgevendere cycli. 0,00 = altijd laden bij goedkope uren." + }, + "value": 0.01, + "min": 0, + "max": 0.15, + "step": 0.005, + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "battery_efficiency", + "type": "number", + "label": { + "en": "Battery Efficiency (RTE)", + "nl": "Batterij efficiëntie (RTE)" + }, + "hint": { + "en": "Round-trip efficiency: the percentage of energy you get back from what you put in. The system learns the actual efficiency from real charge/discharge cycles and will override this value automatically. Typical measured values for HomeWizard: 0.72-0.82 (72-82%).", + "nl": "Round-trip efficiëntie: het percentage energie dat je terugkrijgt van wat je erin stopt. Het systeem leert de werkelijke efficiëntie van echte laad/ontlaad cycli en overschrijft deze waarde automatisch. Typische gemeten waarden voor HomeWizard: 0,72-0,82 (72-82%)." + }, + "value": 0.78, + "min": 0.5, + "max": 0.97, + "step": 0.01, + "units": "RTE", + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + } + ] + }, + { + "type": "group", + "label": { + "en": "Weather Forecasting", + "nl": "Weersvoorspelling" + }, + "visible": { + "when": "tariff_type", + "is": "dynamic" + }, + "children": [ + { + "id": "weather_latitude", + "type": "number", + "label": { + "en": "Latitude", + "nl": "Breedtegraad" + }, + "hint": { + "en": "Latitude for solar forecasting (e.g. 52.370).", + "nl": "Breedtegraad voor zonne-energievoorspelling (bijv. 52.370)." + }, + "value": 0, + "min": -90, + "max": 90, + "step": 0.001 + }, + { + "id": "weather_longitude", + "type": "number", + "label": { + "en": "Longitude", + "nl": "Lengtegraad" + }, + "hint": { + "en": "Longitude for solar forecasting (e.g. 4.895).", + "nl": "Lengtegraad voor zonne-energievoorspelling (bijv. 4.895)." + }, + "value": 0, + "min": -180, + "max": 180, + "step": 0.001 + }, + { + "id": "weather_update_interval", + "type": "number", + "label": { + "en": "Weather Update Interval", + "nl": "Update‑interval weersvoorspelling" + }, + "units": { + "en": "hours", + "nl": "uur" + }, + "value": 3, + "min": 1, + "max": 24, + "step": 1 + } + ] + }, + { + "type": "group", + "label": { + "en": "Battery Limits", + "nl": "Batterijlimieten" + }, + "children": [ + { + "id": "min_soc", + "type": "number", + "label": { + "en": "Minimum Battery %", + "nl": "Minimale batterij‑%" + }, + "hint": { + "en": "0% means no minimum — battery is allowed to fully discharge. HomeWizard firmware protects the battery hardware at 0-100%.", + "nl": "0% betekent geen minimum — de batterij mag volledig ontladen. HomeWizard firmware beschermt de batterij hardware bij 0-100%." + }, + "units": { + "en": "%", + "nl": "%" + }, + "value": 0, + "min": 0, + "max": 50, + "step": 5, + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "max_soc", + "type": "number", + "label": { + "en": "Maximum Battery %", + "nl": "Maximale batterij‑%" + }, + "units": { + "en": "%", + "nl": "%" + }, + "value": 100, + "min": 80, + "max": 100, + "step": 5, + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "preserve_cycles", + "type": "checkbox", + "label": { + "en": "Preserve Battery Cycles", + "nl": "Batterijcycli sparen" + }, + "value": true, + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "cycle_cost_per_kwh", + "type": "number", + "label": { + "en": "Battery Cycle Cost (€/kWh)", + "nl": "Batterij cycluskosten (€/kWh)" + }, + "hint": { + "en": "Degradation cost per kWh discharged. The optimizer only cycles the battery when the price spread exceeds this cost. Formula: battery_price / (rated_cycles × usable_kWh). Example for 1× HomeWizard battery: €1200 / (6000 × 2.7 kWh) = €0.074.", + "nl": "Slijtagekosten per kWh ontladen. De optimizer cycleert de batterij alleen wanneer het prijsverschil deze kosten overstijgt. Formule: batterijprijs / (nominale_cycli × bruikbare_kWh). Voorbeeld voor 1× HomeWizard batterij: €1200 / (6000 × 2,7 kWh) = €0,074." + }, + "value": 0.075, + "min": 0, + "max": 0.15, + "step": 0.005, + "units": "€/kWh", + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + } + ] + }, + { + "type": "group", + "label": { + "en": "PV Estimation", + "nl": "PV‑schatting" + }, + "children": [ + { + "id": "pv_estimation_enabled", + "type": "checkbox", + "label": { + "en": "Enable PV Estimation", + "nl": "PV‑schatting inschakelen" + }, + "hint": { + "en": "Estimate PV production using grid measurements (no privileged access needed).", + "nl": "Schat PV‑productie met behulp van netmetingen (geen verhoogde toegang nodig)." + }, + "value": false + }, + { + "id": "pv_capacity_w", + "type": "number", + "label": { + "en": "PV Peak Capacity (W)", + "nl": "PV‑piekvermogen (W)" + }, + "hint": { + "en": "Your solar system's peak power rating in watts (e.g., 3500 for 3.5kWp).", + "nl": "Het piekvermogen van uw zonnesysteem in watt (bijv. 3500 voor 3,5kWp)." + }, + "units": { + "en": "W", + "nl": "W" + }, + "value": 0, + "min": 0, + "max": 20000, + "step": 100, + "visible": { + "when": "pv_estimation_enabled", + "is": true + } + }, + { + "id": "pv_tilt", + "type": "number", + "label": { + "en": "Panel Tilt (°)", + "nl": "Paneel helling (°)" + }, + "hint": { + "en": "Roof/panel tilt angle in degrees. 0° = flat (horizontal), 90° = vertical. Typical roof: 30–40°.", + "nl": "Helling van het dak/paneel in graden. 0° = plat (horizontaal), 90° = verticaal. Typisch dak: 30–40°." + }, + "units": "°", + "value": 35, + "min": 0, + "max": 90, + "step": 5, + "visible": { + "when": "pv_estimation_enabled", + "is": true + } + }, + { + "id": "pv_azimuth", + "type": "number", + "label": { + "en": "Panel Azimuth (°)", + "nl": "Paneel azimuth (°)" + }, + "hint": { + "en": "Panel facing direction: -90° = East, 0° = South, 90° = West. E.g. southwest = 45°.", + "nl": "Richting panelen: -90° = Oost, 0° = Zuid, 90° = West. Bijv. zuidwest = 45°." + }, + "units": "°", + "value": 0, + "min": -90, + "max": 90, + "step": 5, + "visible": { + "when": "pv_estimation_enabled", + "is": true + } + }, + { + "id": "pv_performance_ratio", + "type": "number", + "label": { + "en": "Performance Ratio (PR)", + "nl": "Prestatieratio (PR)" + }, + "hint": { + "en": "Fraction of theoretical PV output actually delivered, accounting for inverter losses, wiring, temperature, and soiling. Typical range: 0.65–0.80. Lower this value if the planning page consistently overestimates your actual production.", + "nl": "Fractie van het theoretisch PV-vermogen dat daadwerkelijk wordt geleverd, rekening houdend met omvormerverlies, bedrading, temperatuur en vervuiling. Typisch bereik: 0,65–0,80. Verlaag deze waarde als de planningspagina uw werkelijke productie stelselmatig overschat." + }, + "units": "PR", + "value": 0.75, + "min": 0.5, + "max": 0.9, + "step": 0.01, + "visible": { + "when": "pv_estimation_enabled", + "is": true + } + } + ] + }, + { + "type": "group", + "label": { + "en": "Advanced", + "nl": "Geavanceerd" + }, + "children": [ + { + "id": "enable_logging", + "type": "checkbox", + "label": { + "en": "Enable Detailed Logging", + "nl": "Gedetailleerde logging inschakelen" + }, + "value": false + }, + { + "id": "enable_policy_notifications", + "type": "checkbox", + "label": { + "en": "Post policy decisions to timeline", + "nl": "Policy‑beslissingen naar tijdlijn sturen" + }, + "value": false + } + ] + } + ], + "images": { + "large": "drivers/battery-policy/assets/images/large.png", + "small": "drivers/battery-policy/assets/images/small.png" + } + }, + { + "name": { + "en": "P1 Meter (Cloud)", + "nl": "P1 Meter (Cloud)" + }, + "class": "sensor", + "capabilities": [], + "capabilitiesOptions": { + "meter_power.peak": { + "title": { + "en": "Power meter tariff 1", + "nl": "Energiemeter tarief 1" + } + }, + "meter_power.offpeak": { + "title": { + "en": "Power meter tariff 2", + "nl": "Energiemeter tarief 2" + } + }, + "meter_power.producedPeak": { + "title": { + "en": "Production tariff 1", + "nl": "Productie tarief 1" + } + }, + "meter_power.producedOffpeak": { + "title": { + "en": "Production tariff 2", + "nl": "Productie tarief 2" + } + }, + "meter_power.returned": { + "title": { + "en": "Returned Power", + "nl": "Teruggeleverde Energie" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Voltage L1", + "nl": "Spanning L1" + } + }, + "measure_current.l1": { + "title": { + "en": "Current L1", + "nl": "Stroom L1" + } + } + }, + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power", + "cumulativeExportedCapability": "meter_power.returned" + }, + "platforms": [ + "local" + ], + "connectivity": [ + "cloud" + ], + "images": { + "small": "/drivers/cloud_p1/assets/images/small.png", + "large": "/drivers/cloud_p1/assets/images/large.png", + "xlarge": "/drivers/cloud_p1/assets/images/xlarge.png" + }, + "pair": [ + { + "id": "login" + } + ], + "repair": [ + { + "id": "login" + } + ], + "settings": [ + { + "type": "group", + "label": { + "en": "Cloud Connection", + "nl": "Cloud Verbinding" + }, + "children": [ + { + "id": "cloud_email", + "type": "text", + "label": { + "en": "Email", + "nl": "E-mail" + }, + "value": "", + "hint": { + "en": "Your HomeWizard Energy account email", + "nl": "Uw HomeWizard Energy account e-mail" + } + }, + { + "id": "cloud_password", + "type": "password", + "label": { + "en": "Password", + "nl": "Wachtwoord" + }, + "value": "", + "hint": { + "en": "Your HomeWizard Energy account password", + "nl": "Uw HomeWizard Energy account wachtwoord" + } + }, + { + "id": "location_id", + "type": "text", + "label": { + "en": "Location ID", + "nl": "Locatie ID" + }, + "value": "", + "hint": { + "en": "Internal location identifier", + "nl": "Interne locatie-identificatie" + } + }, + { + "id": "location_name", + "type": "text", + "label": { + "en": "Location Name", + "nl": "Locatie Naam" + }, + "value": "", + "hint": { + "en": "Name of your home in HomeWizard Energy app", + "nl": "Naam van uw woning in HomeWizard Energy app" + } + } + ] + } + ], + "id": "cloud_p1" + }, + { + "id": "cloud_watermeter", + "name": { + "en": "Watermeter (cloud)", + "nl": "Watermeter (cloud)" + }, + "class": "sensor", + "platforms": [ + "local" + ], + "capabilities": [ + "meter_water", + "meter_water.daily" + ], + "capabilitiesOptions": { + "meter_water.daily": { + "decimals": 3, + "title": { + "en": "Water usage today", + "nl": "Waterverbruik vandaag" + } + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Water usage total", + "nl": "Waterverbruik totaal" + } + } + }, + "energy": { + "cumulative": true + }, + "images": { + "small": "/drivers/cloud_watermeter/assets/images/small.png", + "large": "/drivers/cloud_watermeter/assets/images/large.png", + "xlarge": "/drivers/cloud_watermeter/assets/images/xlarge.png" + }, + "pair": [ + { + "id": "login", + "template": "login_credentials", + "options": { + "title": { + "en": "Login to HomeWizard", + "nl": "Inloggen bij HomeWizard" + }, + "usernameLabel": { + "en": "Email", + "nl": "E-mail" + }, + "usernamePlaceholder": { + "en": "your@email.com", + "nl": "jouw@email.com" + }, + "passwordLabel": { + "en": "Password", + "nl": "Wachtwoord" + }, + "passwordPlaceholder": { + "en": "Password", + "nl": "Wachtwoord" + } + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "settings": [ + { + "type": "group", + "label": { + "en": "General settings", + "nl": "Algemene instellingen" + }, + "children": [ + { + "id": "poll_interval", + "type": "number", + "label": { + "en": "Polling interval (seconds)", + "nl": "Polling interval (seconden)" + }, + "value": 900, + "min": 300, + "max": 3600, + "hint": { + "en": "How often to fetch data from HomeWizard Cloud (default: 900s / 15 min)", + "nl": "Hoe vaak data ophalen van HomeWizard Cloud (standaard: 900s / 15 min)" + } + }, + { + "id": "manual_offset", + "type": "number", + "label": { + "en": "Manual offset (m³)", + "nl": "Handmatige correctie (m³)" + }, + "value": 0, + "hint": { + "en": "Add or subtract from the total meter reading. Use this to match your water company's meter reading.", + "nl": "Tel op of trek af van de totale meterstand. Gebruik dit om overeen te komen met de meterstand van je waterbedrijf." + } + } + ] + } + ] + }, + { + "name": { + "en": "P1 Meter" + }, + "images": { + "large": "drivers/energy/assets/images/large.png", + "small": "drivers/energy/assets/images/small.png" + }, + "class": "sensor", + "discovery": "energy", + "platforms": [ + "local" + ], + "capabilities": [ + "identify", + "measure_power", + "meter_gas", + "measure_gas", + "meter_water", + "meter_power", + "meter_power.consumed.t1", + "meter_power.produced.t1", + "meter_power.consumed.t2", + "meter_power.produced.t2", + "meter_power.consumed.t3", + "meter_power.produced.t3", + "meter_power.consumed", + "meter_power.returned", + "meter_power.daily", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "net_load_phase1", + "net_load_phase2", + "net_load_phase3", + "net_load_phase1_pct", + "net_load_phase2_pct", + "net_load_phase3_pct", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3", + "measure_power.montly_power_peak", + "rssi", + "wifi_quality", + "tariff", + "long_power_fail_count", + "voltage_sag_l1", + "voltage_sag_l2", + "voltage_sag_l3", + "voltage_swell_l1", + "voltage_swell_l2", + "voltage_swell_l3", + "connection_error", + "measure_frequency", + "alarm_connectivity" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.consumed", + "cumulativeExportedCapability": "meter_power.returned" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + } + }, + "meter_gas": { + "decimals": 3, + "title": { + "en": "Gasmeter", + "nl": "Gasmeter" + } + }, + "meter_gas.daily": { + "decimals": 3, + "title": { + "en": "Day Usage Gas", + "nl": "Dag verbruik Gas" + } + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Watermeter", + "nl": "Watermeter" + } + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "net_load_phase1_pct": { + "title": { + "en": "Phase 1 load %", + "nl": "Fase 1 belasting %" + } + }, + "net_load_phase2_pct": { + "title": { + "en": "Phase 2 load %", + "nl": "Fase 2 belasting %" + } + }, + "net_load_phase3_pct": { + "title": { + "en": "Phase 3 load %", + "nl": "Fase 3 belasting %" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + } + }, + "measure_voltage.l2": { + "title": { + "en": "Current Voltage phase 2", + "nl": "Huidig Voltage fase 2" + } + }, + "measure_voltage.l3": { + "title": { + "en": "Current Voltage phase 3", + "nl": "Huidig Voltage fase 3" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power.consumed.t2": { + "decimals": 3, + "title": { + "en": "Total t2 usage", + "nl": "Totaal t2 gebruik" + } + }, + "meter_power.produced.t2": { + "decimals": 3, + "title": { + "en": "Total t2 deliver", + "nl": "Totaal t2 teruglevering" + } + }, + "meter_power.consumed.t3": { + "decimals": 3, + "title": { + "en": "Total t3 usage", + "nl": "Totaal t3 gebruik" + } + }, + "meter_power.produced.t3": { + "decimals": 3, + "title": { + "en": "Total t3 deliver", + "nl": "Totaal t3 teruglevering" + } + }, + "meter_power.consumed": { + "decimals": 3, + "title": { + "en": "Sum Consumed", + "nl": "Som gebruik" + } + }, + "meter_power.returned": { + "decimals": 3, + "title": { + "en": "Sum returned", + "nl": "Som teruglevering" + } + }, + "meter_power.daily": { + "decimals": 3, + "title": { + "en": "Daily usage", + "nl": "Dag verbruik" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "measure_frequency": { + "decimals": 3, + "title": { + "en": "Current Frequency", + "nl": "Huidige Frequentie" + } + }, + "measure_power.montly_power_peak": { + "title": { + "en": "Monthly Power Peak", + "nl": "Maandelijks piekvermogen" + } + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "repair": [ + { + "id": "manual_ip" + } + ], + "id": "energy", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "min": 1, + "unit": { + "en": "s" + } + }, + { + "id": "number_of_phases", + "type": "number", + "label": { + "en": "Amount of phase(s)", + "nl": "Aantal fase(s)" + }, + "value": 1 + }, + { + "id": "phase_capacity", + "type": "number", + "label": { + "en": "Phase capacity A", + "nl": "Fase capaciteit A" + }, + "value": 40, + "unit": { + "en": "A" + } + }, + { + "id": "cloud", + "type": "number", + "label": { + "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { + "en": "url", + "nl": "url" + } + }, + { + "id": "show_gas", + "type": "checkbox", + "label": { + "en": "Show gas meter", + "nl": "Gas meter weergeven" + }, + "value": true + }, + { + "id": "baseload_notifications", + "type": "checkbox", + "label": { + "en": "Enable baseload notifications", + "nl": "Sluipverbruik meldingen inschakelen" + }, + "value": true + }, + { + "id": "phase_overload_notifications", + "type": "checkbox", + "label": { + "en": "Phase overload notifications", + "nl": "Fase-overbelasting meldingen" + }, + "value": true + }, + { + "id": "phase_overload_threshold", + "type": "number", + "label": { + "en": "Phase overload warning threshold (%)", + "nl": "Fase overbelasting waarschuwing (%)" + }, + "value": 97, + "min": 50, + "max": 120, + "step": 1 + }, + { + "id": "phase_overload_reset", + "type": "number", + "label": { + "en": "Phase overload reset threshold (%)", + "nl": "Fase overbelasting reset (%)" + }, + "value": 85, + "min": 20, + "max": 100, + "step": 1 + } + ] + }, + { + "name": { + "en": "Energy Socket" + }, + "images": { + "large": "drivers/energy_socket/assets/images/large.png", + "small": "drivers/energy_socket/assets/images/small.png" + }, + "class": "socket", + "discovery": "energy_socket", + "platforms": [ + "local" + ], + "capabilities": [ + "onoff", + "dim", + "identify", + "locked", + "measure_power", + "meter_power", + "meter_power.consumed.t1", + "meter_power.produced.t1", + "measure_power.l1", + "rssi", + "connection_error", + "alarm_connectivity" + ], + "energy": { + "meterPowerImportedCapability": "meter_power.consumed.t1", + "meterPowerExportedCapability": "meter_power.produced.t1" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + }, + "insights": true + } + }, + "settings": [ + { + "type": "group", + "label": { + "en": "Energy Socket settings", + "nl": "Energy Socket instellingen" + }, + "children": [ + { + "id": "offset_socket", + "type": "number", + "label": { + "en": "Offset Watt usage", + "nl": "Compensatie watt gebruik" + }, + "value": 0, + "unit": { + "en": "watt", + "nl": "watt" + } + }, + { + "id": "offset_polling", + "type": "number", + "label": { + "en": "Polling in seconds", + "nl": "Interval in seconden" + }, + "value": 10, + "min": 2 + }, + { + "id": "cloud", + "type": "number", + "label": { + "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + } + ] + } + ], + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "id": "energy_socket" + }, + { + "name": { + "en": "P1 Meter (apiv2)" + }, + "images": { + "large": "drivers/energy_v2/assets/images/large.png", + "small": "drivers/energy_v2/assets/images/small.png" + }, + "class": "sensor", + "discovery": "energy_v2", + "platforms": [ + "local" + ], + "capabilities": [ + "identify", + "measure_power", + "meter_gas", + "meter_gas.daily", + "meter_water", + "meter_power", + "meter_power.consumed.t1", + "meter_power.produced.t1", + "meter_power.consumed.t2", + "meter_power.produced.t2", + "meter_power.consumed.t3", + "meter_power.produced.t3", + "meter_power.consumed.t4", + "meter_power.produced.t4", + "meter_power.consumed", + "meter_power.returned", + "meter_power.daily", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "measure_current", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "net_load_phase1_pct", + "net_load_phase2_pct", + "net_load_phase3_pct", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3", + "measure_power.montly_power_peak", + "measure_power.average_power_15m_w", + "tariff", + "long_power_fail_count", + "voltage_sag_l1", + "voltage_sag_l2", + "voltage_sag_l3", + "voltage_swell_l1", + "voltage_swell_l2", + "voltage_swell_l3", + "rssi", + "wifi_quality", + "measure_power.battery_group_power_w", + "measure_power.battery_group_target_power_w", + "measure_power.battery_group_max_consumption_w", + "measure_power.battery_group_max_production_w", + "connection_error", + "battery_group_total_capacity_kwh", + "battery_group_average_soc", + "battery_group_state", + "battery_group_charge_mode" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.consumed", + "cumulativeExportedCapability": "meter_power.returned" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + } + }, + "meter_gas": { + "decimals": 3, + "title": { + "en": "Gasmeter", + "nl": "Gasmeter" + } + }, + "meter_gas.daily": { + "decimals": 3, + "title": { + "en": "Day Usage Gas", + "nl": "Dag verbruik Gas" + } + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Watermeter", + "nl": "Watermeter" + } + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "net_load_phase1_pct": { + "title": { + "en": "Phase 1 load %", + "nl": "Fase 1 belasting %" + } + }, + "net_load_phase2_pct": { + "title": { + "en": "Phase 2 load %", + "nl": "Fase 2 belasting %" + } + }, + "net_load_phase3_pct": { + "title": { + "en": "Phase 3 load %", + "nl": "Fase 3 belasting %" + } + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + } + }, + "measure_voltage.l2": { + "title": { + "en": "Current Voltage phase 2", + "nl": "Huidig Voltage fase 2" + } + }, + "measure_voltage.l3": { + "title": { + "en": "Current Voltage phase 3", + "nl": "Huidig Voltage fase 3" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power.consumed.t2": { + "decimals": 3, + "title": { + "en": "Total t2 usage", + "nl": "Totaal t2 gebruik" + } + }, + "meter_power.produced.t2": { + "decimals": 3, + "title": { + "en": "Total t2 deliver", + "nl": "Totaal t2 teruglevering" + } + }, + "meter_power.consumed.t3": { + "decimals": 3, + "title": { + "en": "Total t3 usage", + "nl": "Totaal t3 gebruik" + } + }, + "meter_power.produced.t3": { + "decimals": 3, + "title": { + "en": "Total t3 deliver", + "nl": "Totaal t3 teruglevering" + } + }, + "meter_power.consumed.t4": { + "decimals": 3, + "title": { + "en": "Total t4 usage", + "nl": "Totaal t4 gebruik" + } + }, + "meter_power.produced.t4": { + "decimals": 3, + "title": { + "en": "Total t4 deliver", + "nl": "Totaal t4 teruglevering" + } + }, + "meter_power.consumed": { + "decimals": 3, + "title": { + "en": "Sum Consumed", + "nl": "Som gebruik" + } + }, + "meter_power.returned": { + "decimals": 3, + "title": { + "en": "Sum returned", + "nl": "Som teruglevering" + } + }, + "meter_power.daily": { + "decimals": 3, + "title": { + "en": "Daily usage", + "nl": "Dag verbruik" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "measure_power.montly_power_peak": { + "title": { + "en": "Monthly Power Peak", + "nl": "Maandelijks piekvermogen" + } + }, + "measure_power.average_power_15m_w": { + "title": { + "en": "Active average power over 15 minutes", + "nl": "Actief gemiddeld vermogen over 15 minuten" + } + }, + "measure_power.battery_group_power_w": { + "title": { + "en": "Battery group Current combined Power", + "nl": "Battery groep Huidig samengesteld vermogen" + } + }, + "measure_power.battery_group_target_power_w": { + "title": { + "en": "Battery group Target Power", + "nl": "Battery groep Doel vermogen" + } + }, + "measure_power.battery_group_max_consumption_w": { + "title": { + "en": "Battery group Max allowed Consumption Power", + "nl": "Battery groep Max toegestaand gebruiksvermogen" + } + }, + "measure_power.battery_group_max_production_w": { + "title": { + "en": "Battery group Max allowed Production Power", + "nl": "Battery groep Max toegestaand leveringssvermogen" + } + }, + "battery_group_total_capacity_kwh": { + "type": "number", + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/battery.svg", + "units": { + "en": "kWh" + }, + "title": { + "en": "Battery Group Total Capacity" + }, + "insights": false + }, + "battery_group_average_soc": { + "type": "number", + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/battery.svg", + "units": { + "en": "%" + }, + "title": { + "en": "Battery Group Average SoC" + }, + "insights": true + }, + "battery_group_state": { + "type": "string", + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/battery.svg", + "title": { + "en": "Battery Group Charge state", + "nl": "Battery Groep Laadt status" + }, + "insights": true + }, + "rssi": { + "type": "number", + "title": { + "en": "WiFi Signal", + "nl": "WiFi signaal" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/rssi.svg", + "units": { + "en": "dBm", + "nl": "dBm" + } + } + }, + "pair": [ + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "authorize" + }, + "options": { + "singular": true + } + }, + { + "id": "authorize", + "navigation": { + "prev": "list_devices" + } + } + ], + "repair": [ + { + "id": "manual_ip" + } + ], + "id": "energy_v2", + "settings": [ + { + "id": "mode", + "type": "dropdown", + "value": "plugin-battery", + "label": { + "en": "Plugin Battery mode", + "nl": "Plugin‑batterijmodus" + }, + "values": [ + { + "id": "zero", + "label": { + "en": "Zero mode", + "nl": "Nul op de meter" + } + }, + { + "id": "to_full", + "label": { + "en": "Full charge", + "nl": "Volledig opladen" + } + }, + { + "id": "standby", + "label": { + "en": "Standby", + "nl": "Stand‑by" + } + }, + { + "id": "zero_charge_only", + "label": { + "en": "Zero mode, Charge allowed", + "nl": "Nul op de meter, laden toegestaan" + } + }, + { + "id": "zero_discharge_only", + "label": { + "en": "Zero mode, Discharge allowed", + "nl": "Nul op de meter, ontladen toegestaan" + } + } + ] + }, + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "min": 1, + "unit": { + "en": "s" + } + }, + { + "id": "grid_phase_amps", + "type": "number", + "label": { + "en": "Grid phase Amps", + "nl": "Net fase aansluiting" + }, + "value": 40, + "unit": { + "en": "A", + "nl": "A" + } + }, + { + "id": "cloud", + "type": "number", + "label": { + "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { + "en": "url", + "nl": "url" + } + }, + { + "id": "use_polling", + "type": "checkbox", + "label": { + "en": "Use polling instead of WebSocket", + "nl": "Gebruik polling in plaats van WebSocket" + }, + "value": true + }, + { + "id": "ws_throttle_ms", + "type": "number", + "label": { + "en": "WebSocket update interval (seconds)", + "nl": "WebSocket update interval (seconden)" + }, + "hint": { + "en": "How often measurements are processed from the WebSocket. Increase to 3 or 4 on older or slower Homey devices to reduce CPU load. Default: 2.", + "nl": "Hoe vaak metingen worden verwerkt via WebSocket. Verhoog naar 3 of 4 op oudere Homey apparaten om CPU-belasting te verminderen. Standaard: 2." + }, + "value": 2, + "min": 2, + "max": 10, + "step": 1 + }, + { + "id": "show_gas", + "type": "checkbox", + "label": { + "en": "Show gas meter", + "nl": "Gas meter weergeven" + }, + "value": true + }, + { + "id": "baseload_notifications", + "type": "checkbox", + "label": { + "en": "Enable baseload notifications", + "nl": "Sluipverbruik meldingen inschakelen" + }, + "value": true + }, + { + "id": "phase_overload_notifications", + "type": "checkbox", + "label": { + "en": "Phase overload notifications", + "nl": "Fase-overbelasting meldingen" + }, + "value": true + }, + { + "id": "debug_logging", + "type": "checkbox", + "label": { + "en": "Enable debug logging", + "nl": "Debug logging inschakelen" + }, + "value": false, + "hint": { + "en": "Enables verbose logging for WebSocket and battery events. Disable after debugging to reduce CPU load.", + "nl": "Schakelt uitgebreide logging in voor WebSocket en batterijgebeurtenissen. Schakel uit na het debuggen om CPU-belasting te verminderen." + } + } + ] + }, + { + "name": { + "en": "Energylink", + "nl": "Energylink" + }, + "images": { + "large": "drivers/energylink/assets/images/large.jpg", + "small": "drivers/energylink/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "capabilities": [ + "measure_power", + "meter_power.used", + "meter_power.aggr", + "meter_power.s1", + "meter_power.s2", + "meter_power", + "measure_power.used", + "measure_power.netto", + "measure_power.s1", + "measure_power.s2", + "measure_power.s1other", + "meter_power.s1other", + "measure_power.s2other", + "meter_power.s2other", + "meter_gas.today", + "meter_gas.reading", + "meter_water", + "measure_water", + "meter_power.consumed.t1", + "meter_power.produced.t1", + "meter_power.consumed.t2", + "meter_power.produced.t2" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.used", + "cumulativeExportedCapability": "meter_power.s1" + }, + "capabilitiesOptions": { + "meter_power.used": { + "decimals": 3, + "title": { + "en": "Day usage", + "nl": "Dag gebruik" + }, + "insights": true + }, + "meter_power.aggr": { + "decimals": 3, + "title": { + "en": "Overall usage", + "nl": "Netto gebruik" + }, + "insights": true + }, + "meter_power.s1": { + "decimals": 3, + "title": { + "en": "Day production S1", + "nl": "Dag opbrengst S1" + }, + "insights": true + }, + "meter_power.s2": { + "decimals": 3, + "title": { + "en": "Day production S2", + "nl": "Dag opbrengst S2" + }, + "insights": true + }, + "measure_power.s1other": { + "title": { + "en": "Power current S1 other", + "nl": "Huidig vermogen S1 other" + }, + "insights": true + }, + "meter_power.s1other": { + "decimals": 3, + "title": { + "en": "Day usage S1 other", + "nl": "Dag gebruik S1 other" + }, + "insights": true + }, + "measure_power.used": { + "title": { + "en": "Power current", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.s2other": { + "title": { + "en": "Power current S2 other", + "nl": "Huidig vermogen S2 other" + }, + "insights": true + }, + "meter_power.s2other": { + "decimals": 3, + "title": { + "en": "Day usage S2 other", + "nl": "Dag gebruik S2 other" + }, + "insights": true + }, + "measure_power.netto": { + "title": { + "en": "Netto Power current", + "nl": "Netto Huidig vermogen" + } + }, + "measure_power.s1": { + "title": { + "en": "Solar current S1", + "nl": "Huidige opbrengst S1" + }, + "insights": true + }, + "measure_power.s2": { + "title": { + "en": "Solar current S2", + "nl": "Huidige opbrengst S2" + }, + "insights": true + }, + "meter_gas.today": { + "decimals": 3, + "title": { + "en": "Gas", + "nl": "Gas" + }, + "insights": true + }, + "meter_gas.reading": { + "decimals": 3, + "title": { + "en": "Meter reading gas", + "nl": "Meterstand Gas" + }, + "insights": true + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Water Total", + "nl": "Water Totaal" + }, + "insights": true + }, + "measure_water": { + "title": { + "en": "Water l./m", + "nl": "Water l./m" + }, + "insights": true + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Meter reading low", + "nl": "Stand laag tarief" + }, + "insights": true + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Meter reading produced low", + "nl": "Stand terug levering laag" + }, + "insights": true + }, + "meter_power.consumed.t2": { + "decimals": 3, + "title": { + "en": "Meter reading normal", + "nl": "Stand verbruik normaal" + }, + "insights": true + }, + "meter_power.produced.t2": { + "decimals": 3, + "title": { + "en": "Meter reading produced normal", + "nl": "Stand terug levering normaal" + }, + "insights": true + }, + "meter_power": { + "title": { + "en": "Aggregated meter", + "nl": "Geaggregeerde meterstand" + }, + "insights": true + } + }, + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ], + "id": "energylink" + }, + { + "name": { + "en": "Heatlink", + "nl": "Heatlink" + }, + "images": { + "large": "drivers/heatlink/assets/images/large.jpg", + "small": "drivers/heatlink/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "thermostat", + "capabilities": [ + "measure_temperature", + "target_temperature", + "measure_temperature.heatlink", + "measure_temperature.boiler", + "central_heating_pump", + "central_heating_flame", + "warm_water", + "measure_pressure" + ], + "capabilitiesOptions": { + "measure_temperature.heatlink": { + "title": { + "en": "Heatlink target temperature", + "nl": "Heatlink doel temperatuur" + } + }, + "measure_temperature.boiler": { + "title": { + "en": "Boiler temperature", + "nl": "Ketel temperatuur" + } + }, + "measure_pressure": { + "decimals": 1, + "title": { + "en": "Water pressure", + "nl": "Waterdruk" + }, + "units": { + "en": "Bar", + "nl": "Bar" + } + } + }, + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ], + "id": "heatlink" + }, + { + "name": { + "en": "HomeWizard Base Station", + "nl": "HomeWizard Base Station" + }, + "images": { + "large": "drivers/homewizard/assets/images/large.jpg", + "small": "drivers/homewizard/assets/images/small.jpg" + }, + "class": "other", + "platforms": [ + "local" + ], + "capabilities": [], + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ], + "settings": [ + { + "type": "group", + "label": { + "en": "HomeWizard settings", + "nl": "HomeWizard instellingen" + }, + "children": [ + { + "id": "homewizard_ip", + "type": "text", + "label": { + "en": "IP address", + "nl": "IP adres" + }, + "value": "" + }, + { + "id": "homewizard_pass", + "type": "text", + "label": { + "en": "Password", + "nl": "Wachtwoord" + }, + "value": "" + }, + { + "id": "homewizard_ledring", + "type": "checkbox", + "label": { + "en": "Use ledring", + "nl": "Gebruik ledring" + }, + "value": false + }, + { + "id": "poll_interval", + "type": "number", + "label": { + "en": "Polling interval (seconds)", + "nl": "Polling-interval (seconden)" + }, + "value": 30, + "min": 10, + "max": 3600 + } + ] + } + ], + "id": "homewizard" + }, + { + "name": { + "en": "Smoke, Motion and door senors", + "nl": "Rook, beweging en deur sensors" + }, + "images": { + "large": "drivers/kakusensors/assets/images/large.jpg", + "small": "drivers/kakusensors/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "capabilities": [], + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ], + "id": "kakusensors" + }, + { + "name": { + "en": "Plugin Battery" + }, + "images": { + "large": "drivers/plugin_battery/assets/images/large.png", + "small": "drivers/plugin_battery/assets/images/small.png" + }, + "class": "battery", + "discovery": "plugin_battery", + "platforms": [ + "local" + ], + "capabilities": [ + "identify", + "dim", + "led_brightness_pct", + "meter_power.import", + "meter_power.export", + "measure_battery", + "battery_charging_state", + "measure_soc", + "measure_power", + "measure_current", + "measure_voltage", + "measure_frequency", + "cycles", + "rssi", + "time_to_full", + "time_to_empty", + "estimate_kwh" + ], + "energy": { + "homeBattery": true, + "meterPowerImportedCapability": "meter_power.import", + "meterPowerExportedCapability": "meter_power.export" + }, + "capabilitiesOptions": { + "dim": { + "title": { + "en": "LED Brightness", + "nl": "LED Helderheid" + } + }, + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + } + }, + "meter_power.import": { + "decimals": 3, + "title": { + "en": "Total battery import", + "nl": "Totaal batterij import" + } + }, + "meter_power.export": { + "decimals": 3, + "title": { + "en": "Total Battery Export", + "nl": "Totaal Batterij export" + } + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + } + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + } + }, + "measure_battery": { + "title": { + "en": "Battery Level", + "nl": "Batterij niveau" + } + }, + "measure_frequency": { + "decimals": 3, + "title": { + "en": "Current Frequency", + "nl": "Huidige Frequentie" + } + }, + "battery_charging_state": { + "title": { + "en": "Battery State", + "nl": "Batterij status" + } + }, + "measure_soc": { + "type": "number", + "title": { + "en": "Battery Level", + "nl": "Batterijniveau" + }, + "desc": { + "en": "Current battery state of charge", + "nl": "Huidige batterij laadstatus" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "rssi": { + "type": "number", + "title": { + "en": "WiFi Signal", + "nl": "WiFi signaal" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/rssi.svg", + "units": { + "en": "dBm", + "nl": "dBm" + } + }, + "time_to_full": { + "type": "number", + "title": { + "en": "Time until full charge", + "nl": "Tijd tot vol geladen" + }, + "getable": true, + "setable": true, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/clock.svg", + "units": { + "en": "min", + "nl": "min" + } + }, + "time_to_empty": { + "type": "number", + "title": { + "en": "Time until discharged", + "nl": "Tijd tot ontladen" + }, + "getable": true, + "setable": true, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/clock.svg", + "units": { + "en": "min", + "nl": "min" + } + }, + "estimate_kwh": { + "type": "number", + "title": { + "en": "Est. kWh in battery", + "nl": "Geschatte kWh in batterij" + }, + "getable": true, + "setable": true, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/tariff.svg", + "units": { + "en": "kWh", + "nl": "kWh" + } + } + }, + "widgets": [ + { + "id": "battery_soc_widget", + "name": { + "en": "Battery State of Charge", + "nl": "Batterij laadniveau" + }, + "description": { + "en": "Display battery percentage charge level", + "nl": "Toon batterij laadpercentage" + }, + "template": "generic", + "class": "battery", + "capabilities": [ + "measure_battery" + ] + } + ], + "pair": [ + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "authorize" + }, + "options": { + "singular": true + } + }, + { + "id": "authorize", + "navigation": { + "prev": "list_devices" + } + } + ], + "id": "plugin_battery", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "unit": { + "en": "s" + } + }, + { + "id": "url", + "type": "text", + "label": { + "en": "url", + "nl": "url" + } + }, + { + "id": "use_polling", + "type": "checkbox", + "label": { + "en": "Use polling instead of WebSocket", + "nl": "Gebruik polling in plaats van WebSocket" + }, + "value": true + } + ] + }, + { + "name": { + "en": "Rainmeter", + "nl": "Regen meter" + }, + "images": { + "large": "drivers/rainmeter/assets/images/large.jpg", + "small": "drivers/rainmeter/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "capabilities": [ + "measure_rain.last3h", + "measure_rain.total" + ], + "capabilitiesOptions": { + "measure_rain.last3h": { + "title": { + "en": "Last 3 hours rain", + "nl": "Laatste 3 uur regen" + } + }, + "measure_rain.total": { + "title": { + "en": "Rainfall today", + "nl": "Regenval vandaag" + } + } + }, + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ], + "id": "rainmeter" + }, + { + "name": { + "en": "Thermometer", + "nl": "Thermometer" + }, + "images": { + "large": "drivers/thermometer/assets/images/large.jpg", + "small": "drivers/thermometer/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "capabilities": [ + "measure_temperature", + "measure_humidity" + ], + "energy": { + "batteries": [ + "AA", + "AA" + ] + }, + "settings": [ + { + "type": "group", + "label": { + "en": "Sensor offset", + "nl": "Sensor compensatie" + }, + "children": [ + { + "id": "offset_temperature", + "type": "number", + "label": { + "en": "Temperature", + "nl": "Temperatuur" + }, + "value": 0 + }, + { + "id": "offset_humidity", + "type": "number", + "label": { + "en": "Humidity", + "nl": "Vochtigheid" + }, + "value": 0 + } + ] + } + ], + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ], + "id": "thermometer" + }, + { + "name": { + "en": "Watermeter" + }, + "images": { + "large": "drivers/watermeter/assets/images/large.png", + "small": "drivers/watermeter/assets/images/small.png" + }, + "class": "sensor", + "discovery": "watermeter", + "platforms": [ + "local" + ], + "capabilities": [ + "identify", + "measure_water", + "meter_water", + "rssi" + ], + "energy": { + "cumulative": true + }, + "capabilitiesOptions": { + "measure_water": { + "type": "number", + "title": { + "en": "Water L/min", + "nl": "Water L/min" + }, + "units": { + "en": "L/min" + }, + "desc": { + "en": "Water flow in Liters per minute (L/min)", + "nl": "Waterdoorstroming in Liters per minuut (L/min)" + }, + "chartType": "stepLine", + "decimals": 1, + "getable": true, + "setable": false + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Total usage", + "nl": "Totaal verbruik" + } + }, + "meter_water.daily": { + "decimals": 3, + "title": { + "en": "Daily water usage", + "nl": "Dagverbruik water" + }, + "units": { + "en": "m³", + "nl": "m³" + } + } + }, + "settings": [ + { + "type": "group", + "label": { + "en": "Watermeter offset", + "nl": "Watermeter compensatie" + }, + "children": [ + { + "id": "offset_water", + "type": "number", + "label": { + "en": "Offset watermeter m3", + "nl": "compensatie watermeter m3" + }, + "value": 0 + }, + { + "id": "offset_polling", + "type": "number", + "label": { + "en": "Polling in seconds", + "nl": "Interval in seconden" + }, + "value": 10 + }, + { + "id": "cloud", + "type": "number", + "label": { + "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + } + ] + } + ], + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "id": "watermeter" + }, + { + "name": { + "en": "Wattcher", + "nl": "Wattcher" + }, + "images": { + "large": "drivers/wattcher/assets/images/large.jpg", + "small": "drivers/wattcher/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "capabilities": [ + "measure_power", + "meter_power" + ], + "capabilitiesOptions": { + "meter_power": { + "title": { + "en": "Day usage", + "nl": "Dag totaal" + } + }, + "measure_power": { + "title": { + "en": "Power current", + "nl": "Huidig vermogen" + } + } + }, + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ], + "id": "wattcher" + }, + { + "name": { + "en": "Windmeter", + "nl": "Windmeter" + }, + "images": { + "large": "drivers/windmeter/assets/images/large.jpg", + "small": "drivers/windmeter/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "capabilities": [ + "measure_wind_angle", + "measure_wind_strength.cur", + "measure_wind_strength.min", + "measure_wind_strength.max", + "measure_gust_strength", + "measure_temperature.real", + "measure_temperature.windchill" + ], + "capabilitiesOptions": { + "measure_wind_angle": { + "title": { + "en": "Wind Angle", + "nl": "Wind richting" + } + }, + "measure_wind_strength.cur": { + "title": { + "en": "Wind Strength current", + "nl": "Wind sterkte huidige" + } + }, + "measure_wind_strength.min": { + "title": { + "en": "Wind Strength lowest", + "nl": "Wind Sterkte laagste" + } + }, + "measure_wind_strength.max": { + "title": { + "en": "Wind Strength highest", + "nl": "Wind Sterkte hoogste" + } + }, + "measure_gust_strength": { + "title": { + "en": "Wind Gusts", + "nl": "Ruk wind sterkte" + } + }, + "measure_temperature.real": { + "title": { + "en": "Temperature Real", + "nl": "Temperatuur Echt" + } + }, + "measure_temperature.windchill": { + "title": { + "en": "Temperature windchill", + "nl": "Gevoelstemperatuur" + } + } + }, + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ], + "id": "windmeter" + } + ], + "capabilities": { + "active_mode": { + "type": "string", + "title": { + "en": "Active Mode", + "nl": "Actieve Modus" + }, + "desc": { + "en": "The battery mode that is currently active on the hardware. May differ from the recommended mode when the confidence score is below the threshold, auto-apply is off, or a manual override is active.", + "nl": "De batterijmodus die momenteel actief is op de hardware. Kan afwijken van de aanbevolen modus als de betrouwbaarheidsscore onder de drempel ligt, automatisch toepassen uitstaat, of een handmatige override actief is." + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "auto_apply": { + "type": "boolean", + "title": { + "en": "Auto-Apply", + "nl": "Automatisch Toepassen" + }, + "desc": { + "en": "Automatically apply policy recommendations to battery", + "nl": "Pas beleidsaanbevelingen automatisch toe op batterij" + }, + "getable": true, + "setable": true, + "uiComponent": "toggle", + "icon": "assets/icon.svg" + }, + "battery_group_average_soc": { + "type": "number", + "title": { + "en": "Battery Group Average SoC", + "nl": "Batterij Groep Lading" + }, + "getable": true, + "setable": false, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/battery.svg", + "units": { + "en": "%", + "nl": "%" + } + }, + "battery_group_charge_mode": { + "type": "enum", + "title": { + "en": "Battery Group Charge Mode", + "nl": "Battery Groep Oplaadmodus" + }, + "getable": true, + "setable": true, + "uiComponent": "picker", + "insights": true, + "icon": "assets/battery.svg", + "values": [ + { + "id": "zero", + "title": { + "en": "Zero (Net Zero)", + "nl": "Nul op de meter" + } + }, + { + "id": "zero_charge_only", + "title": { + "en": "Zero – Charge Only", + "nl": "NOM – Alleen laden" + } + }, + { + "id": "zero_discharge_only", + "title": { + "en": "Zero – Discharge Only", + "nl": "NOM – Alleen ontladen" + } + }, + { + "id": "standby", + "title": { + "en": "Standby", + "nl": "Stand‑by" + } + }, + { + "id": "to_full", + "title": { + "en": "Full Charge", + "nl": "Volledig laden" + } + } + ] + }, + "battery_group_state": { + "type": "string", + "title": { + "en": "Battery Group Charge state", + "nl": "Battery Groep Laadt status" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/battery.svg" + }, + "battery_group_total_capacity_kwh": { + "type": "number", + "title": { + "en": "Battery Group Total Capacity" + }, + "units": { + "en": "kWh" + }, + "getable": true, + "setable": false, + "insights": false, + "icon": "assets/battery.svg", + "uiComponent": "sensor" + }, + "battery_rte": { + "type": "number", + "title": { + "en": "Battery RTE", + "nl": "Batterij RTE" + }, + "desc": { + "en": "Learned round-trip efficiency of battery (0-100%)", + "nl": "Geleerde round-trip efficiency van batterij (0-100%)" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 1, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "battery_soc_mirror": { + "type": "number", + "title": { + "en": "Battery Level", + "nl": "Batterijniveau" + }, + "desc": { + "en": "Current battery state of charge", + "nl": "Huidige batterij laadstatus" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "central_heating_flame": { + "type": "boolean", + "title": { + "en": "Central Heating Burner", + "nl": "CV brander" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/flame.svg" + }, + "central_heating_pump": { + "type": "boolean", + "title": { + "en": "Central Heating", + "nl": "Central Verwarming" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/central_heating.svg" + }, + "confidence_score": { + "type": "number", + "title": { + "en": "Confidence", + "nl": "Vertrouwen" + }, + "desc": { + "en": "Confidence in current recommendation (0-100%)", + "nl": "Vertrouwen in huidige aanbeveling (0-100%)" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "connection_error": { + "type": "string", + "title": { + "en": "Connection Error", + "nl": "Verbindingsfout" + }, + "getable": true, + "setable": false, + "insights": true, + "icon": "assets/icon.svg" + }, + "cycles": { + "type": "number", + "title": { + "en": "Number of battery cycles", + "nl": "Aantal battery laadcycli" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/cycles.svg", + "units": { + "en": "cycles", + "nl": "cycli" + } + }, + "estimate_kwh": { + "type": "number", + "title": { + "en": "Estimate kWh in battery", + "nl": "Geschatte kWh in batterij" + }, + "getable": true, + "setable": true, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/tariff.svg", + "units": { + "en": "kWh", + "nl": "kWh" + } + }, + "explanation_summary": { + "type": "string", + "title": { + "en": "Decision Reason", + "nl": "Beslissingsreden" + }, + "desc": { + "en": "Explanation of current policy decision", + "nl": "Uitleg van huidige beleidsbeslissing" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "grid_power_mirror": { + "type": "number", + "title": { + "en": "Grid Power", + "nl": "Netvermogen" + }, + "desc": { + "en": "Current grid import/export power", + "nl": "Huidig net import/export vermogen" + }, + "units": { + "en": "W", + "nl": "W" + }, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "identify": { + "type": "boolean", + "title": { + "en": "Identify", + "nl": "Identificeren" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "insights": false, + "icon": "assets/magnify.svg" + }, + "last_update": { + "type": "string", + "title": { + "en": "Last Update", + "nl": "Laatste Update" + }, + "desc": { + "en": "Timestamp of last policy check", + "nl": "Tijdstempel van laatste beleidscontrole" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "led_brightness_pct": { + "type": "number", + "title": { + "en": "LED Brightness", + "nl": "LED Helderheid" + }, + "getable": true, + "setable": true, + "uiComponent": "sensor", + "insights": false, + "min": 0, + "max": 100, + "step": 1, + "units": { + "en": "%", + "nl": "%" + } + }, + "long_power_fail_count": { + "type": "number", + "title": { + "en": "Power failures", + "nl": "Stroomstoringen" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" + }, + "measure_gas": { + "type": "number", + "title": { + "en": "Current gas usage", + "nl": "Huidig gasverbruik" + }, + "getable": true, + "setable": false, + "decimals": 3, + "uiComponent": "sensor", + "insights": true, + "units": { + "en": "m3", + "nl": "m3" + }, + "icon": "assets/icon.svg" + }, + "measure_soc": { + "type": "number", + "title": { + "en": "Battery Level", + "nl": "Batterijniveau" + }, + "desc": { + "en": "Current battery state of charge", + "nl": "Huidige batterij laadstatus" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "battery", + "icon": "assets/icon.svg" + }, + "net_load_phase1": { + "type": "number", + "title": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "titleFormatted": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "desc": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "min": 0, + "max": 100, + "step": 1, + "decimals": 0, + "chartType": "stepLine", + "getable": true, + "setable": false, + "uiComponent": "slider", + "insights": true, + "units": { + "en": "A", + "nl": "A" + }, + "icon": "assets/icon.svg" + }, + "net_load_phase1_pct": { + "type": "number", + "title": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "titleFormatted": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "desc": { + "en": "Phase 1 load", + "nl": "Fase 1 belasting" + }, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "units": { + "en": "%", + "nl": "%" + }, + "icon": "assets/icon.svg" + }, + "net_load_phase2": { + "type": "number", + "title": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "titleFormatted": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "desc": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "min": 0, + "max": 100, + "step": 1, + "decimals": 0, + "chartType": "stepLine", + "getable": true, + "setable": false, + "uiComponent": "slider", + "insights": true, + "units": { + "en": "A", + "nl": "A" + }, + "icon": "assets/icon.svg" + }, + "net_load_phase2_pct": { + "type": "number", + "title": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "titleFormatted": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "desc": { + "en": "Phase 2 load", + "nl": "Fase 2 belasting" + }, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "units": { + "en": "%", + "nl": "%" + }, + "icon": "assets/icon.svg" + }, + "net_load_phase3": { + "type": "number", + "title": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "titleFormatted": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "desc": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "min": 0, + "max": 100, + "step": 1, + "decimals": 0, + "chartType": "stepLine", + "getable": true, + "setable": false, + "uiComponent": "slider", + "insights": true, + "units": { + "en": "A", + "nl": "A" + }, + "icon": "assets/icon.svg" + }, + "net_load_phase3_pct": { + "type": "number", + "title": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "titleFormatted": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "desc": { + "en": "Phase 3 load", + "nl": "Fase 3 belasting" + }, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "units": { + "en": "%", + "nl": "%" + }, + "icon": "assets/icon.svg" + }, + "override_until": { + "type": "string", + "title": { + "en": "Override Until", + "nl": "Overschrijven Tot" + }, + "desc": { + "en": "Manual override expiry time", + "nl": "Handmatige overschrijving vervaltijd" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "policy_debug": { + "id": "policy_debug", + "type": "string", + "title": { + "en": "Policy Debug", + "nl": "Policy Debug" + }, + "desc": { + "en": "Current tariff and weather inputs used by the policy engine", + "nl": "Huidige tarief- en weerinputs gebruikt door de policy engine" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false + }, + "policy_debug_learning": { + "id": "policy_debug_learning", + "type": "string", + "title": { + "en": "Policy Learning", + "nl": "Policy Leren" + }, + "desc": { + "en": "Historical learning statistics and pattern coverage", + "nl": "Historische leerstatistieken en patroonbedekking" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" + }, + "policy_debug_price": { + "id": "policy_debug_price", + "type": "string", + "title": { + "en": "Policy Price", + "nl": "Policy Prijs" + }, + "desc": { + "en": "Current DAP price and rate bucket", + "nl": "Huidige DAP-prijs en tariefcategorie" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" + }, + "policy_debug_sun": { + "id": "policy_debug_sun", + "type": "string", + "title": { + "en": "Policy Sun", + "nl": "Policy Zon" + }, + "desc": { + "en": "Sunshine forecast inputs used by the policy", + "nl": "Zon-voorspelling gebruikt door de policy" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" + }, + "policy_debug_top3high": { + "id": "policy_debug_top3high", + "type": "string", + "title": { + "en": "Policy Top3 High", + "nl": "Policy Top3 Hoog" + }, + "desc": { + "en": "Top 3 most expensive DAP hours", + "nl": "Top 3 duurste DAP-uren" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" + }, + "policy_debug_top3low": { + "id": "policy_debug_top3low", + "type": "string", + "title": { + "en": "Policy Top3 Low", + "nl": "Policy Top3 Laag" + }, + "desc": { + "en": "Top 3 cheapest DAP hours", + "nl": "Top 3 goedkoopste DAP-uren" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" + }, + "policy_enabled": { + "type": "boolean", + "title": { + "en": "Policy Enabled", + "nl": "Beleid Ingeschakeld" + }, + "desc": { + "en": "Enable or disable the battery policy automation", + "nl": "Schakel batterijbeleid automatisering in of uit" + }, + "getable": true, + "setable": true, + "uiComponent": "toggle", + "icon": "assets/icon.svg" + }, + "policy_mode": { + "id": "policy_mode", + "type": "enum", + "title": { + "en": "Policy Mode", + "nl": "Beleidsmodus" + }, + "values": [ + { + "id": "off", + "title": { + "en": "Off", + "nl": "Uit" + } + }, + { + "id": "balanced", + "title": { + "en": "Dynamic Pricing", + "nl": "Dynamisch Tarief" + } + }, + { + "id": "balanced-fixed", + "title": { + "en": "Fixed Pricing", + "nl": "Vast Tarief" + } + }, + { + "id": "balanced-dynamic", + "title": { + "en": "Dynamic Pricing (V2 — post-saldering)", + "nl": "Dynamisch Tarief (V2 — na salderen)" + } + }, + { + "id": "zero", + "title": { + "en": "Peak Shaving", + "nl": "Peak Shaving" + } + } + ], + "getable": true, + "setable": true, + "uiComponent": "picker", + "icon": "assets/icon.svg" + }, + "predicted_sun_hours": { + "type": "number", + "title": { + "en": "Predicted Sun (4h)", + "nl": "Voorspelde Zon (4u)" + }, + "desc": { + "en": "Expected sunshine hours in next 4 hours", + "nl": "Verwachte zonuren in komende 4 uur" + }, + "units": { + "en": "h", + "nl": "u" + }, + "min": 0, + "max": 4, + "decimals": 1, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "preset": { + "type": "enum", + "title": { + "en": "Preset", + "nl": "Preset" + }, + "getable": true, + "setable": true, + "uiComponent": "picker", + "values": [ + { + "id": "0", + "title": { + "nl": "Thuis", + "en": "Home" + } + }, + { + "id": "1", + "title": { + "nl": "Weg", + "en": "Away" + } + }, + { + "id": "2", + "title": { + "nl": "Slapen", + "en": "Sleep" + } + }, + { + "id": "3", + "title": { + "nl": "Vakantie", + "en": "Holiday" + } + } + ] + }, + "recommended_mode": { + "type": "string", + "title": { + "en": "Recommended Mode", + "nl": "Aanbevolen Modus" + }, + "desc": { + "en": "Current battery mode recommendation", + "nl": "Huidige batterij modus aanbeveling" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "rssi": { + "type": "number", + "title": { + "en": "WiFi Signal", + "nl": "WiFi signaal" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/rssi.svg", + "units": { + "en": "RSSI", + "nl": "RSSI" + } + }, + "sun_score": { + "type": "number", + "title": { + "en": "Sunshine Score", + "nl": "Zonneschijn Score" + }, + "desc": { + "en": "Predicted sunshine availability (0-100%)", + "nl": "Voorspelde zonneschijn beschikbaarheid (0-100%)" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "tariff": { + "type": "number", + "title": { + "en": "Active tariff", + "nl": "Tarief actief" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/tariff.svg", + "units": { + "en": "Tariff", + "nl": "Tarief" + } + }, + "time_to_empty": { + "type": "number", + "title": { + "en": "Time until discharged", + "nl": "Tijd tot ontladen" + }, + "getable": true, + "setable": false, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/clock.svg", + "units": { + "en": "min", + "nl": "min" + } + }, + "time_to_full": { + "type": "number", + "title": { + "en": "Time until full charge", + "nl": "Tijd tot vol geladen" + }, + "getable": true, + "setable": false, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/clock.svg", + "units": { + "en": "min", + "nl": "min" + } + }, + "voltage_sag_l1": { + "type": "number", + "title": { + "en": "Net dip L1", + "nl": "Net dip L1" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" + }, + "voltage_sag_l2": { + "type": "number", + "title": { + "en": "Net dip L2", + "nl": "Net dip L2" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" + }, + "voltage_sag_l3": { + "type": "number", + "title": { + "en": "Net dip L3", + "nl": "Net dip L3" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" + }, + "voltage_swell_l1": { + "type": "number", + "title": { + "en": "Net peak L1", + "nl": "Net piek L1" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" + }, + "voltage_swell_l2": { + "type": "number", + "title": { + "en": "Net peak L2", + "nl": "Net piek L2" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" + }, + "voltage_swell_l3": { + "type": "number", + "title": { + "en": "Net peak L3", + "nl": "Net piek L3" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/power_fail.svg" + }, + "warm_water": { + "type": "boolean", + "title": { + "en": "Warm water", + "nl": "Warm water" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/shower.svg" + }, + "weather_override": { + "id": "weather_override", + "type": "enum", + "title": { + "en": "Weather Override", + "nl": "Weer Overschrijven" + }, + "desc": { + "en": "Override weather forecast for policy decisions", + "nl": "Overschrijf weersverwachting voor beleidsbeslissingen" + }, + "values": [ + { + "id": "auto", + "title": { + "en": "Auto (use forecast)", + "nl": "Auto (gebruik voorspelling)" + } + }, + { + "id": "sunny", + "title": { + "en": "Sunny", + "nl": "Zonnig" + } + }, + { + "id": "cloudy", + "title": { + "en": "Cloudy", + "nl": "Bewolkt" + } + }, + { + "id": "rainy", + "title": { + "en": "Rainy", + "nl": "Regenachtig" + } + } + ], + "getable": true, + "setable": true, + "uiComponent": "picker", + "icon": "assets/icon.svg" + }, + "wifi_quality": { + "type": "string", + "title": { + "en": "WiFi State", + "nl": "WiFi Status" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/rssi.svg" + } + }, + "discovery": { + "energy": { + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^p1meter-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-P1" + } + } + ] + ] + }, + "energy_socket": { + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^energysocket-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-SKT" + } + } + ] + ] + }, + "energy_v2": { + "type": "mdns-sd", + "mdns-sd": { + "name": "homewizard", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^p1meter-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-P1" + } + } + ] + ] + }, + "plugin_battery": { + "type": "mdns-sd", + "mdns-sd": { + "name": "homewizard", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^battery-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-BAT" + } + } + ] + ] + }, + "SDM230": { + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "SDM230-wifi" + } + } + ], + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-KWH1" + } + } + ] + ] + }, + "SDM230_v2": { + "type": "mdns-sd", + "mdns-sd": { + "name": "homewizard", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "SDM230-wifi" + } + } + ], + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-KWH1" + } + } + ] + ] + }, + "SDM630": { + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "SDM630-wifi" + } + } + ], + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-KWH3" + } + } + ] + ] + }, + "SDM630_v2": { + "type": "mdns-sd", + "mdns-sd": { + "name": "homewizard", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "SDM630-wifi" + } + } + ], + [ + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-KWH3" + } + } + ] + ] + }, + "watermeter": { + "type": "mdns-sd", + "mdns-sd": { + "name": "hwenergy", + "protocol": "tcp" + }, + "id": "{{txt.serial}}", + "conditions": [ + [ + { + "field": "host", + "match": { + "type": "regex", + "value": "^watermeter-" + } + }, + { + "field": "txt.product_type", + "match": { + "type": "string", + "value": "HWE-WTR" + } + } + ] + ] + } + } } \ No newline at end of file diff --git a/app/com.homewizard/planner-engine.js b/app/com.homewizard/planner-engine.js new file mode 100644 index 00000000..94eb30c1 --- /dev/null +++ b/app/com.homewizard/planner-engine.js @@ -0,0 +1,39 @@ +(function(window){ + // Minimal PlannerEngine stub + function PlannerEngine(settings){ + this.settings = Object.assign({ + battery_efficiency: 0.75, + min_profit_margin: 0.02, + max_charge_price: 0.15, + min_discharge_price: 0.30, + min_soc: 10, + max_soc: 95, + tariff_type: 'dynamic' + }, settings || {}); + } + + PlannerEngine.prototype.getRecommendationForHour = function(ctx, allPrices) { + // ctx: { hour, price, isPeak, isCheap, hasSun, projectedSOC, hoursFromNow } + const s = this.settings; + // If no price known, prefer PV when sun available + if (ctx.price === null || typeof ctx.price !== 'number') { + return ctx.hasSun ? 'pv_only' : 'standby'; + } + + // If peak badge, discharge + if (ctx.isPeak) return 'discharge'; + // If cheap badge, charge + if (ctx.isCheap) return 'charge'; + + // Price-based heuristics + if (ctx.price >= s.min_discharge_price) return 'discharge'; + if (ctx.price <= s.max_charge_price) return 'charge'; + + // PV window + if (ctx.hasSun) return 'pv_only'; + + return 'standby'; + }; + + window.PlannerEngine = PlannerEngine; +})(window); diff --git a/assets/battery.svg b/assets/battery.svg new file mode 100644 index 00000000..202e8ba5 --- /dev/null +++ b/assets/battery.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/central_heating.svg b/assets/central_heating.svg new file mode 100644 index 00000000..ae3fbd28 --- /dev/null +++ b/assets/central_heating.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/clock.svg b/assets/clock.svg new file mode 100644 index 00000000..d7c59d99 --- /dev/null +++ b/assets/clock.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/cycles.svg b/assets/cycles.svg new file mode 100644 index 00000000..8c7360f1 --- /dev/null +++ b/assets/cycles.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/flame.svg b/assets/flame.svg new file mode 100644 index 00000000..068005cc --- /dev/null +++ b/assets/flame.svg @@ -0,0 +1,79 @@ + + + + + + + + image/svg+xml + + + + + + + + Layer 1 + + + + + + diff --git a/assets/icon.svg b/assets/icon.svg index 0b58b4f2..03cd43ab 100644 --- a/assets/icon.svg +++ b/assets/icon.svg @@ -1,89 +1,29 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + logo-svg + + \ No newline at end of file diff --git a/assets/images/large.jpg b/assets/images/large.jpg deleted file mode 100644 index e468c088..00000000 Binary files a/assets/images/large.jpg and /dev/null differ diff --git a/assets/images/large.png b/assets/images/large.png new file mode 100644 index 00000000..2517c261 Binary files /dev/null and b/assets/images/large.png differ diff --git a/assets/images/small.jpg b/assets/images/small.jpg deleted file mode 100644 index b22669d0..00000000 Binary files a/assets/images/small.jpg and /dev/null differ diff --git a/assets/images/small.png b/assets/images/small.png new file mode 100644 index 00000000..a973e27d Binary files /dev/null and b/assets/images/small.png differ diff --git a/assets/images/xlarge.png b/assets/images/xlarge.png new file mode 100644 index 00000000..9ec4f231 Binary files /dev/null and b/assets/images/xlarge.png differ diff --git a/assets/magnify.svg b/assets/magnify.svg new file mode 100644 index 00000000..e888746a --- /dev/null +++ b/assets/magnify.svg @@ -0,0 +1 @@ + diff --git a/assets/power_fail.svg b/assets/power_fail.svg new file mode 100644 index 00000000..9a4a6aae --- /dev/null +++ b/assets/power_fail.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/rssi.svg b/assets/rssi.svg new file mode 100644 index 00000000..e98392f6 --- /dev/null +++ b/assets/rssi.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/shower.svg b/assets/shower.svg new file mode 100644 index 00000000..3634bce1 --- /dev/null +++ b/assets/shower.svg @@ -0,0 +1,69 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/tariff.svg b/assets/tariff.svg new file mode 100644 index 00000000..ca38a2b8 --- /dev/null +++ b/assets/tariff.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/drivers/SDM230-p1mode/assets/icon.svg b/drivers/SDM230-p1mode/assets/icon.svg new file mode 100644 index 00000000..b6d798c2 --- /dev/null +++ b/drivers/SDM230-p1mode/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/SDM230-p1mode/assets/images/large.png b/drivers/SDM230-p1mode/assets/images/large.png new file mode 100644 index 00000000..263d225a Binary files /dev/null and b/drivers/SDM230-p1mode/assets/images/large.png differ diff --git a/drivers/SDM230-p1mode/assets/images/small.png b/drivers/SDM230-p1mode/assets/images/small.png new file mode 100644 index 00000000..e43504db Binary files /dev/null and b/drivers/SDM230-p1mode/assets/images/small.png differ diff --git a/drivers/SDM230-p1mode/device.js b/drivers/SDM230-p1mode/device.js new file mode 100644 index 00000000..1c1c00a7 --- /dev/null +++ b/drivers/SDM230-p1mode/device.js @@ -0,0 +1,333 @@ +'use strict'; + +const Homey = require('homey'); +const fetchWithTimeout = require('../../includes/utils/fetchWithTimeout'); +const http = require('http'); +const BaseloadMonitor = require('../../includes/utils/baseloadMonitor'); + +/** + * Stable capability updater — never removes capabilities. + */ +async function updateCapability(device, capability, value) { + try { + const current = device.getCapabilityValue(capability); + + // --- SAFE REMOVE --- + // Removal is allowed only when: + // 1) the new value is null + // 2) the current value in Homey is also null + + if (value == null && current == null) { + if (device.hasCapability(capability)) { + await device.removeCapability(capability); + device.log(`🗑️ Removed capability "${capability}"`); + } + return; + } + + // --- ADD IF MISSING --- + if (!device.hasCapability(capability)) { + try { + await device.addCapability(capability); + device.log(`➕ Added capability "${capability}"`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + } else { + throw err; + } + } + } + + // --- UPDATE --- + if (current !== value) { + await device.setCapabilityValue(capability, value); + } + + } catch (err) { + if (err.message === 'device_not_found') { + device.log(`⚠️ Skipping capability "${capability}" — device not found`); + return; + } + device.error(`❌ Failed updateCapability("${capability}")`, err); + } +} + +module.exports = class HomeWizardEnergyDevice230 extends Homey.Device { + + async onInit() { + + this._debugLogs = []; + + this.agent = new http.Agent({ + keepAlive: true, + keepAliveMsecs: 10000, + }); + + + const settings = this.getSettings(); + + if (settings.polling_interval == null) { + await this.setSettings({ polling_interval: 10 }); + } + + const interval = Math.max(settings.polling_interval, 2); + + if (this.onPollInterval) clearInterval(this.onPollInterval); + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + + if (this.getClass() === 'sensor') { + this.setClass('socket'); + } + + // Required capabilities + const requiredCaps = [ + 'measure_power', + 'meter_power.consumed.t1', + 'measure_power.l1', + 'rssi' + ]; + + for (const cap of requiredCaps) { + if (!this.hasCapability(cap)) { + try { + await this.addCapability(cap); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log(`Capability already exists: ${cap} — ignoring`); + } else { + this.error(err); + } + } + } + } + + // Baseload monitor + this._baseloadNotificationsEnabled = this.getSetting('baseload_notifications') ?? true; + + const app = this.homey.app; + if (!app.baseloadMonitor) { + app.baseloadMonitor = new BaseloadMonitor(this.homey); + } + + app.baseloadMonitor.registerP1Device(this); + app.baseloadMonitor.trySetMaster(this); + app.baseloadMonitor.setNotificationsEnabledForDevice(this, this._baseloadNotificationsEnabled); + } + + onDeleted() { + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + + const app = this.homey.app; + if (app.baseloadMonitor) { + app.baseloadMonitor.unregisterP1Device(this); + } + } + + onDiscoveryAvailable(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this._debugLog(`Discovery available: ${this.url}`); + this.setAvailable(); + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this._debugLog(`Discovery address changed: ${this.url}`); + this.setAvailable(); + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this._debugLog(`Discovery last seen: ${this.url}`); + this.setAvailable(); + } + + /** + * Debug logger (batched writes) + */ +_debugLog(msg) { + try { + if (!this._debugBuffer) this._debugBuffer = []; + const ts = new Date().toLocaleString('nl-NL', { hour12: false, timeZone: 'Europe/Amsterdam' }); + const driverName = this.driver.id; + const deviceName = this.getName(); + const safeMsg = typeof msg === 'string' ? msg : (msg instanceof Error ? msg.message : JSON.stringify(msg)); + const line = `${ts} [${driverName}] [${deviceName}] ${safeMsg}`; + this._debugBuffer.push(line); + if (this._debugBuffer.length > 20) this._debugBuffer.shift(); + if (!this._debugFlushTimeout) { + this._debugFlushTimeout = setTimeout(() => { + this._flushDebugLogs(); + this._debugFlushTimeout = null; + }, 5000); + } + } catch (err) { + this.error('Failed to write debug logs:', err.message || err); + } +} +_flushDebugLogs() { + if (!this._debugBuffer || this._debugBuffer.length === 0) return; + try { + const logs = this.homey.settings.get('debug_logs') || []; + logs.push(...this._debugBuffer); + if (logs.length > 500) logs.splice(0, logs.length - 500); + this.homey.settings.set('debug_logs', logs); + this._debugBuffer = []; + } catch (err) { + this.error('Failed to flush debug logs:', err.message || err); + } +} + + async setCloudOn() { + if (!this.url) return; + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: true }) + }); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } catch (err) { + this._debugLog(`Cloud ON failed: ${err.code || ''} ${err.message || err}`); + this.error('Failed to enable cloud:', err); + } + } + + async setCloudOff() { + if (!this.url) return; + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: false }) + }); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } catch (err) { + this._debugLog(`Cloud OFF failed: ${err.code || ''} ${err.message || err}`); + this.error('Failed to disable cloud:', err); + } + } + + _onNewPowerValue(power) { + const app = this.homey.app; + if (app.baseloadMonitor) { + app.baseloadMonitor.updatePowerFromDevice(this, power); + } + } + + /** + * GET /data + */ +async onPoll() { + const settings = this.getSettings(); + + if (!this.url) { + if (settings.url) { + this.url = settings.url; + } else { + await this.setUnavailable('Missing URL'); + return; + } + } + + try { + + const res = await fetchWithTimeout(`${this.url}/data`, { + agent: this.agent, + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + const data = await res.json(); + if (!data || typeof data !== 'object') throw new Error('Invalid JSON'); + + // CAPABILITY UPDATES + await updateCapability(this, 'rssi', data.wifi_strength); + + const power = this.getClass() === 'solarpanel' + ? data.active_power_w * -1 + : data.active_power_w; + + await updateCapability(this, 'measure_power', power); + this._onNewPowerValue(power); + + await updateCapability(this, 'meter_power.consumed.t1', data.total_power_import_t1_kwh); + + const l1 = this.getClass() === 'solarpanel' + ? data.active_power_l1_w * -1 + : data.active_power_l1_w; + + await updateCapability(this, 'measure_power.l1', l1); + + if (data.total_power_export_t1_kwh > 1) { + await updateCapability(this, 'meter_power.produced.t1', data.total_power_export_t1_kwh); + } + + const net = data.total_power_import_t1_kwh - data.total_power_export_t1_kwh; + await updateCapability(this, 'meter_power', net); + + if (data.active_voltage_v !== undefined) { + await updateCapability(this, 'measure_voltage', data.active_voltage_v); + } + + if (data.active_current_a !== undefined) { + await updateCapability(this, 'measure_current', data.active_current_a); + } + + await this.setAvailable(); + + } catch (err) { + + if (err.message === 'TIMEOUT') { + this._debugLog('SDM230 P1-mode timeout — no new telegrams, keeping last known values'); + // No return — allow finally to run + } else { + this._debugLog(`Poll failed: ${err.message}`); + this.log('Polling error:', err.message || err); + this.setUnavailable(err.message || 'Polling error').catch(this.error); + } + } + +} + + + async onSettings(event) { + const { newSettings, oldSettings, changedKeys } = event; + + if (changedKeys.includes('polling_interval')) { + clearInterval(this.onPollInterval); + + const interval = Math.max(newSettings.polling_interval, 2); + + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + } + + if (changedKeys.includes('cloud')) { + if (newSettings.cloud == 1) { + this.setCloudOn(); + } else { + this.setCloudOff(); + } + } + + if (changedKeys.includes('baseload_notifications')) { + this._baseloadNotificationsEnabled = newSettings.baseload_notifications; + + const app = this.homey.app; + if (app.baseloadMonitor) { + app.baseloadMonitor.setNotificationsEnabledForDevice(this, this._baseloadNotificationsEnabled); + } + } + } +}; diff --git a/drivers/SDM230-p1mode/driver.compose.json b/drivers/SDM230-p1mode/driver.compose.json new file mode 100644 index 00000000..e99a1c41 --- /dev/null +++ b/drivers/SDM230-p1mode/driver.compose.json @@ -0,0 +1,97 @@ +{ + "name": { + "en": "kWh Meter (1 phase) P1 mode" + }, + "images": { + "large": "drivers/SDM230-p1mode/assets/images/large.png", + "small": "drivers/SDM230-p1mode/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM230", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "measure_current", + "measure_power.l1", + "measure_voltage", + "rssi" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.consumed.t1", + "cumulativeExportedCapability": "meter_power.produced.t1" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + }, + "insights": true + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + }, + "insights": true + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + }, + "insights": true + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/SDM230-p1mode/driver.js b/drivers/SDM230-p1mode/driver.js new file mode 100644 index 00000000..a1a0dc29 --- /dev/null +++ b/drivers/SDM230-p1mode/driver.js @@ -0,0 +1,5 @@ +'use strict'; + +const driver = require('../../includes/v1/driver.js'); + +module.exports = driver; diff --git a/drivers/SDM230-p1mode/driver.settings.compose.json b/drivers/SDM230-p1mode/driver.settings.compose.json new file mode 100644 index 00000000..a934ea6c --- /dev/null +++ b/drivers/SDM230-p1mode/driver.settings.compose.json @@ -0,0 +1,34 @@ +[ + { + "id": "polling_interval", + "type": "number", + "label": { "en": "Polling interval" }, + "value": 10, + "min": 1, + "unit": { "en": "s" } + }, + { + "id": "cloud", + "type": "number", + "label": { "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { "en": "url", + "nl": "url" + } + }, + { + "id": "baseload_notifications", + "type": "checkbox", + "label": { + "en": "Enable baseload notifications", + "nl": "Sluipverbruik meldingen inschakelen" + }, + "value": true + } +] \ No newline at end of file diff --git a/drivers/SDM230-p1mode/pair/start.html b/drivers/SDM230-p1mode/pair/start.html new file mode 100644 index 00000000..633815c6 --- /dev/null +++ b/drivers/SDM230-p1mode/pair/start.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/drivers/SDM230/assets/icon.svg b/drivers/SDM230/assets/icon.svg new file mode 100644 index 00000000..b6d798c2 --- /dev/null +++ b/drivers/SDM230/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/SDM230/assets/images/large.png b/drivers/SDM230/assets/images/large.png new file mode 100644 index 00000000..263d225a Binary files /dev/null and b/drivers/SDM230/assets/images/large.png differ diff --git a/drivers/SDM230/assets/images/small.png b/drivers/SDM230/assets/images/small.png new file mode 100644 index 00000000..e43504db Binary files /dev/null and b/drivers/SDM230/assets/images/small.png differ diff --git a/drivers/SDM230/device.js b/drivers/SDM230/device.js new file mode 100644 index 00000000..106d14c9 --- /dev/null +++ b/drivers/SDM230/device.js @@ -0,0 +1,325 @@ +'use strict'; + +const Homey = require('homey'); +const fetchWithTimeout = require('../../includes/utils/fetchWithTimeout'); +const http = require('http'); + + + + +/** + * Safe capability updater + */ +async function updateCapability(device, capability, value) { + try { + const current = device.getCapabilityValue(capability); + + // --- SAFE REMOVE --- + // Removal is allowed only when: + // 1) the new value is null + // 2) the current value in Homey is also null + + if (value == null && current == null) { + if (device.hasCapability(capability)) { + await device.removeCapability(capability); + device.log(`🗑️ Removed capability "${capability}"`); + } + return; + } + + // --- ADD IF MISSING --- + if (!device.hasCapability(capability)) { + await device.addCapability(capability); + device.log(`➕ Added capability "${capability}"`); + } + + // --- UPDATE --- + if (current !== value) { + await device.setCapabilityValue(capability, value); + } + + } catch (err) { + if (err.message === 'device_not_found') { + device.log(`⚠️ Skipping capability "${capability}" — device not found`); + return; + } + device.error(`❌ Failed updateCapability("${capability}")`, err); + } +} +module.exports = class HomeWizardEnergyDevice230 extends Homey.Device { + + async onInit() { + this._debugLogs = []; + + // KeepAlive agent (blijft) + this.agent = new http.Agent({ + keepAlive: true, + keepAliveMsecs: 10000, + }); + + + // await this.setUnavailable(`${this.getName()} ${this.homey.__('device.init')}`); + + const settings = this.getSettings(); + + if (settings.polling_interval == null) { + await this.setSettings({ polling_interval: 10 }); + } + + const interval = Math.max(settings.polling_interval, 2); + const offset = Math.floor(Math.random() * interval * 1000); + + if (this.onPollInterval) clearInterval(this.onPollInterval); + + setTimeout(() => { + this.onPoll().catch(this.error); + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + }, offset); + + if (this.getClass() === 'sensor') { + this.setClass('socket'); + this.log('Changed class from sensor to socket'); + } + + const requiredCaps = [ + 'measure_power', + 'meter_power.consumed.t1', + 'measure_power.l1', + 'rssi', + 'meter_power' + ]; + + for (const cap of requiredCaps) { + if (!this.hasCapability(cap)) { + try { + await this.addCapability(cap); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log(`Capability already exists: ${cap} — ignoring`); + } else { + this.error(err); + } + } + } + } + } + + onDeleted() { + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + } + + /** + * Discovery — simpel gehouden + */ + onDiscoveryAvailable(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this.setAvailable(); + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this._debugLog(`🔄 Discovery address changed: ${this.url}`); + this.setAvailable(); + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this.setAvailable(); + } + + /** + * Per‑device debug logger + */ +_debugLog(msg) { + try { + if (!this._debugBuffer) this._debugBuffer = []; + const ts = new Date().toLocaleString('nl-NL', { hour12: false, timeZone: 'Europe/Amsterdam' }); + const driverName = this.driver.id; + const deviceName = this.getName(); + const safeMsg = typeof msg === 'string' ? msg : (msg instanceof Error ? msg.message : JSON.stringify(msg)); + const line = `${ts} [${driverName}] [${deviceName}] ${safeMsg}`; + this._debugBuffer.push(line); + if (this._debugBuffer.length > 20) this._debugBuffer.shift(); + if (!this._debugFlushTimeout) { + this._debugFlushTimeout = setTimeout(() => { + this._flushDebugLogs(); + this._debugFlushTimeout = null; + }, 5000); + } + } catch (err) { + this.error('Failed to write debug logs:', err.message || err); + } +} +_flushDebugLogs() { + if (!this._debugBuffer || this._debugBuffer.length === 0) return; + try { + const logs = this.homey.settings.get('debug_logs') || []; + logs.push(...this._debugBuffer); + if (logs.length > 500) logs.splice(0, logs.length - 500); + this.homey.settings.set('debug_logs', logs); + this._debugBuffer = []; + } catch (err) { + this.error('Failed to flush debug logs:', err.message || err); + } +} + + + + /** + * PUT /system cloud on/off — zonder timeout wrapper + */ + async setCloudOn() { + if (!this.url) return; + + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: true }) + }); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + this.log('Cloud enabled'); + + } catch (err) { + this._debugLog(`Cloud ON failed: ${err.code || ''} ${err.message || err}`); + this.error('Failed to enable cloud:', err); + } + } + + async setCloudOff() { + if (!this.url) return; + + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: false }) + }); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + this.log('Cloud disabled'); + + } catch (err) { + this._debugLog(`Cloud OFF failed: ${err.code || ''} ${err.message || err}`); + this.error('Failed to disable cloud:', err); + } + } + + /** + * GET /data + */ + async onPoll() { + const settings = this.getSettings(); + + // URL alleen uit settings; nooit terugschrijven + if (!this.url) { + if (settings.url) { + this.url = settings.url; + this.log(`Restored URL from settings: ${this.url}`); + } else { + //this.setUnavailable('Missing URL').catch(this.error); + this.log('❌ Missing URL, skipping poll'); + await updateCapability(this, 'alarm_connectivity', true); + return; + } + } + + + try { + + const res = await fetchWithTimeout(`${this.url}/data`, { + agent: this.agent, + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + const text = await res.text(); + + let data; + try { + data = JSON.parse(text); + } catch (err) { + this.error('JSON parse error:', err.message, 'Body:', text?.slice(0, 200)); + throw new Error('Invalid JSON'); + } + + if (!data || typeof data !== 'object') { + throw new Error('Invalid JSON'); + } + + + await updateCapability(this, 'rssi', data.wifi_strength); + await updateCapability(this, 'alarm_connectivity', false); + + const power = this.getClass() === 'solarpanel' + ? data.active_power_w * -1 + : data.active_power_w; + await updateCapability(this, 'measure_power', power); + + await updateCapability(this, 'meter_power.consumed.t1', data.total_power_import_t1_kwh); + + const l1 = this.getClass() === 'solarpanel' + ? data.active_power_l1_w * -1 + : data.active_power_l1_w; + await updateCapability(this, 'measure_power.l1', l1); + + if (data.total_power_export_t1_kwh > 1) { + await updateCapability(this, 'meter_power.produced.t1', data.total_power_export_t1_kwh); + } + + const net = data.total_power_import_t1_kwh - data.total_power_export_t1_kwh; + await updateCapability(this, 'meter_power', net); + + + await updateCapability(this, 'measure_voltage', data.active_voltage_v); + await updateCapability(this, 'measure_current', data.active_current_a); + + await this.setAvailable(); + + } catch (err) { + this._debugLog(`❌ ${err.code || ''} ${err.message || err}`); + this.error('Polling failed:', err); + //this.setUnavailable(err.message || 'Polling error').catch(this.error); + await updateCapability(this, 'alarm_connectivity', true); + + } + + } + + onSettings(event) { + const { newSettings, changedKeys } = event; + + for (const key of changedKeys) { + + if (key === 'polling_interval') { + const interval = newSettings.polling_interval; + + if (typeof interval === 'number' && interval > 0) { + if (this.onPollInterval) clearInterval(this.onPollInterval); + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + } else { + this.log('Invalid polling interval:', interval); + } + } + + if (key === 'cloud') { + if (newSettings.cloud == 1) this.setCloudOn(); + else this.setCloudOff(); + } + } + } +}; diff --git a/drivers/SDM230/driver.compose.json b/drivers/SDM230/driver.compose.json new file mode 100644 index 00000000..d73d57a1 --- /dev/null +++ b/drivers/SDM230/driver.compose.json @@ -0,0 +1,93 @@ +{ + "name": { + "en": "kWh Meter (1 phase)" + }, + "images": { + "large": "drivers/SDM230/assets/images/large.png", + "small": "drivers/SDM230/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM230", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "measure_current", + "measure_power.l1", + "measure_voltage", + "alarm_connectivity", + "rssi" + ], + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + }, + "insights": true + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + }, + "insights": true + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + }, + "insights": true + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/SDM230/driver.js b/drivers/SDM230/driver.js new file mode 100644 index 00000000..a1a0dc29 --- /dev/null +++ b/drivers/SDM230/driver.js @@ -0,0 +1,5 @@ +'use strict'; + +const driver = require('../../includes/v1/driver.js'); + +module.exports = driver; diff --git a/drivers/SDM230/driver.settings.compose.json b/drivers/SDM230/driver.settings.compose.json new file mode 100644 index 00000000..857734d9 --- /dev/null +++ b/drivers/SDM230/driver.settings.compose.json @@ -0,0 +1,25 @@ +[ + { + "id": "polling_interval", + "type": "number", + "label": { "en": "Polling interval" }, + "value": 10, + "min": 1, + "unit": { "en": "s" } + }, + { + "id": "cloud", + "type": "number", + "label": { "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { "en": "url", + "nl": "url" + } + } +] \ No newline at end of file diff --git a/drivers/SDM230/pair/start.html b/drivers/SDM230/pair/start.html new file mode 100644 index 00000000..633815c6 --- /dev/null +++ b/drivers/SDM230/pair/start.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/drivers/SDM230_v2/assets/icon.svg b/drivers/SDM230_v2/assets/icon.svg new file mode 100644 index 00000000..b6d798c2 --- /dev/null +++ b/drivers/SDM230_v2/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/SDM230_v2/assets/images/large.png b/drivers/SDM230_v2/assets/images/large.png new file mode 100644 index 00000000..263d225a Binary files /dev/null and b/drivers/SDM230_v2/assets/images/large.png differ diff --git a/drivers/SDM230_v2/assets/images/small.png b/drivers/SDM230_v2/assets/images/small.png new file mode 100644 index 00000000..e43504db Binary files /dev/null and b/drivers/SDM230_v2/assets/images/small.png differ diff --git a/drivers/SDM230_v2/device.js b/drivers/SDM230_v2/device.js new file mode 100644 index 00000000..cee1d7b1 --- /dev/null +++ b/drivers/SDM230_v2/device.js @@ -0,0 +1,460 @@ +'use strict'; + +const Homey = require('homey'); +const api = require('../../includes/v2/Api'); + +// const POLL_INTERVAL = 1000 * 1; // 1 seconds + +function normalizeBatteryMode(data) { + const knownModes = [ + 'zero', + 'standby', + 'to_full', + 'zero_charge_only', + 'zero_discharge_only' + ]; + + let rawMode = data.mode; + + if (typeof rawMode === 'string') { + rawMode = rawMode.trim(); + try { rawMode = JSON.parse(rawMode); } + catch { rawMode = rawMode.replace(/^["']+|["']+$/g, ''); } + } + + if (knownModes.includes(rawMode)) return rawMode; + + if (Array.isArray(data.permissions)) { + const perms = [...data.permissions].sort().join(','); + if (perms === '') return 'standby'; + if (perms === 'charge_allowed,discharge_allowed') return 'zero'; + if (perms === 'charge_allowed') return 'zero_charge_only'; + if (perms === 'discharge_allowed') return 'zero_discharge_only'; + } + + return 'standby'; +} + +module.exports = class HomeWizardEnergyDevice230V2 extends Homey.Device { + + async onInit() { + + // await this.setUnavailable(`${this.getName()} ${this.homey.__('device.init')}`); + + this.token = await this.getStoreValue('token'); + //this.log('Token:', this.token); + + await this._updateCapabilities(); + await this._registerCapabilityListeners(); + + const settings = this.getSettings(); + this.log('Settings for SDM230 apiv2: ', settings.polling_interval); + + // Check if polling interval is set in settings else set default value + if (settings.polling_interval === undefined) { + settings.polling_interval = 10; // Default to 10 second if not set + await this.setSettings({ + // Update settings in Homey + polling_interval: 10, + }); + } + + // Register flow card listeners only once (prevent "already registered" warnings) + if (!this.homey.app._flowListenersRegistered_SDM230) { + this.homey.app._flowListenersRegistered_SDM230 = true; + + // Condition Card + const ConditionCardCheckBatteryMode = this.homey.flow.getConditionCard('check-battery-mode'); + ConditionCardCheckBatteryMode.registerRunListener(async (args, state) => { + // this.log('CheckBatteryModeCard'); + + return new Promise(async (resolve, reject) => { + try { + const response = await api.getMode(this.url, this.token); // NEEDS TESTING WITH SDM230 and BATTERY + + if (!response) { + return resolve(false); + } + + const normalized = normalizeBatteryMode(response); + return resolve(args.mode === normalized); + + + } catch (error) { + this.log('Error retrieving mode:', error); + return resolve(false); // Or reject(error), depending on your error-handling approach + } + }); + }); + + this.homey.flow.getActionCard('sdm230-set-battery-to-zero-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Zero Mode'); + // this.log('This url:', this.url); + // this.log('This token:', this.token); + return new Promise(async (resolve, reject) => { + try { + const response = await api.setMode(this.url, this.token, 'zero'); + + if (!response) return resolve(false); + + const normalized = normalizeBatteryMode(response); + return resolve(normalized); + + } catch (error) { + this.log('Error set mode to zero:', error); + return resolve(false); // Or reject(error), depending on your error-handling approach + } + }); + }); + + this.homey.flow.getActionCard('sdm230-set-battery-to-full-charge-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Full Charge Mode'); + // this.log('This url:', this.url); + // this.log('This token:', this.token); + return new Promise(async (resolve, reject) => { + try { + const response = await api.setMode(this.url, this.token, 'to_full'); + + if (!response) return resolve(false); + + const normalized = normalizeBatteryMode(response); + return resolve(normalized); + + } catch (error) { + this.log('Error set mode to full charge:', error); + return resolve(false); // Or reject(error), depending on your error-handling approach + } + }); + }); + + this.homey.flow.getActionCard('sdm230-set-battery-to-standby-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Standby Mode'); + // this.log('This url:', this.url); + // this.log('This token:', this.token); + return new Promise(async (resolve, reject) => { + try { + const response = await api.setMode(this.url, this.token, 'standby'); + + if (!response) return resolve(false); + + const normalized = normalizeBatteryMode(response); + return resolve(normalized); + + } catch (error) { + this.log('Error set mode to standby:', error); + return resolve(false); // Or reject(error), depending on your error-handling approach + } + }); + }); + + // Zero Charge Only + this.homey.flow.getActionCard('sdm230-set-battery-to-zero-charge-only-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Zero Charge Only Mode'); + + try { + const response = await api.setMode(this.url, this.token, 'zero_charge_only'); + if (!response) return false; + + const normalized = normalizeBatteryMode(response); + return normalized; + + } catch (error) { + this.error('Error set mode to zero_charge_only:', error); + return false; + } + }); + + // Zero Discharge Only + this.homey.flow.getActionCard('sdm230-set-battery-to-zero-discharge-only-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Zero Discharge Only Mode'); + + try { + const response = await api.setMode(this.url, this.token, 'zero_discharge_only'); + if (!response) return false; + + const normalized = normalizeBatteryMode(response); + return normalized; + + } catch (error) { + this.error('Error set mode to zero_discharge_only:', error); + return false; + } + }); + + } // End of _flowListenersRegistered_SDM230 guard + + this.onPollInterval = setInterval(this.onPoll.bind(this), 1000 * settings.polling_interval); + + this._triggerFlowPrevious = {}; + + + } + + onDeleted() { + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + } + + onDiscoveryAvailable(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.onPoll(); + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.log('onDiscoveryAddressChanged'); + this.onPoll(); + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.setAvailable(); + this.onPoll(); + } + + /** + * Helper function to update capabilities configuration. + * This function is called when the device is initialized. + */ + async _updateCapabilities() { + if (!this.hasCapability('identify')) { + try { + await this.addCapability('identify'); + this.log(`created capability identify for ${this.getName()}`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log('Capability already exists: identify — ignoring'); + } else { + this.error(err); + } + } + } + + // Remove capabilities that are not needed + if (this.hasCapability('measure_power.power_w')) { + await this.removeCapability('measure_power.power_w').catch(this.error); + this.log(`removed capability measure_power.power_w for ${this.getName()}`); + } + } + + /** + * Helper function to register capability listeners. + * This function is called when the device is initialized. + */ + async _registerCapabilityListeners() { + this.registerCapabilityListener('identify', async (value) => { + await api.identify(this.url, this.token); + }); + } + + /** + * Helper function for 'optional' capabilities. + * This function is called when the device is initialized. + * It will create the capability if it doesn't exist. + * + * We do not remove capabilities here, as we assume the user may want to keep them. + * Besides that we assume that the P1 Meter is connected to a smart meter that does not change often. + * + * @param {string} capability The capability to set + * @param {*} value The value to set + * @returns {Promise} A promise that resolves when the capability is set + */ + async _setCapabilityValue(capability, value) { + // Test if value is undefined, if so, we don't set the capability + if (value === undefined) { + return; + } + + // Create a new capability if it doesn't exist + if (!this.hasCapability(capability)) { + try { + await this.addCapability(capability); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log(`Capability already exists: ${capability} — ignoring`); + } else { + this.error(err); + } + } + } + + // Set the capability value + await this.setCapabilityValue(capability, value).catch(this.error); + } + + /** + * Helper function to trigger flows on change. + * This function is called when the device is initialized. + * + * We use this function to trigger flows when the value changes. + * We store the previous value in a variable. + * + * @param {*} flow_id Flow ID name + * @param {*} value The value to check for changes + * @returns {Promise} A promise that resolves when the flow is triggered + */ + async _triggerFlowOnChange(flow_id, value) { + + // Ignore if value is undefined + if (value === undefined) { + return; + } + + // Check if the value is undefined + // If so, we assume this is the first time we are setting the value + // We cannot trust the the 'trigger' function to be called with the correct value + if (this._triggerFlowPrevious[flow_id] === undefined) { + this._triggerFlowPrevious[flow_id] = value; + return; + } + + // Return of the value is the same as the previous value + if (this._triggerFlowPrevious[flow_id] === value) { + + // We don't need to trigger the flow + return; + } + + // It is a bit 'costly' to get the flow card every time + // But we can assume the trigger does not change often + const flow = this.homey.flow.getDeviceTriggerCard(flow_id); + if (flow === undefined) { + this.error('Flow not found'); + return; + } + + // Update value and trigger the flow + this._triggerFlowPrevious[flow_id] = value; + flow.trigger(this, { [flow_id]: value }).catch(this.error); + } + + async onPoll() { + try { + const settings = this.getSettings(); + + // 1. Restore URL if runtime is empty + if (!this.url) { + if (settings.url) { + this.url = settings.url; + } else { + await this.setUnavailable('Missing URL'); + return; + } + } + + // 2. Sync settings if discovery changed the URL + if (this.url && this.url !== settings.url) { + await this.setSettings({ url: this.url }).catch(this.error); + } + + // Refresh token if missing + if (!this.token) { + this.token = await this.getStoreValue('token'); + } + + // --- Main API calls --- + const data = await api.getMeasurement(this.url, this.token); + + const setCapabilityPromises = []; + + // Power + setCapabilityPromises.push(this._setCapabilityValue('measure_power', data.power_w)); + + // Import + setCapabilityPromises.push(this._setCapabilityValue('meter_power.import', data.energy_import_kwh)); + + // Export (only if non-zero) + if (data.energy_export_kwh !== 0) { + setCapabilityPromises.push(this._setCapabilityValue('meter_power.export', data.energy_export_kwh)); + } + + // Aggregated meter_power + if (!this.hasCapability('meter_power')) { + await this.addCapability('meter_power').catch(this.error); + } + if (data.energy_import_kwh !== undefined) { + const calcValue = data.energy_import_kwh - data.energy_export_kwh; + if (this.getCapabilityValue('meter_power') !== calcValue) { + setCapabilityPromises.push(this._setCapabilityValue('meter_power', calcValue)); + } + } + + // Voltage & Current + setCapabilityPromises.push(this._setCapabilityValue('measure_voltage', data.voltage_v)); + setCapabilityPromises.push(this._setCapabilityValue('measure_current', data.current_a)); + + await Promise.allSettled(setCapabilityPromises); + + // --- Battery mode handling --- + const batteryMode = await api.getMode(this.url, this.token); + + if (batteryMode) { + const normalized = normalizeBatteryMode(batteryMode); + + // Update settings if changed + if (settings.mode !== normalized) { + await this.setSettings({ mode: normalized }); + } + + // Update capabilities + await this._setCapabilityValue('measure_power.battery_group_power_w', batteryMode.power_w ?? null); + await this._setCapabilityValue('measure_power.battery_group_target_power_w', batteryMode.target_power_w ?? null); + await this._setCapabilityValue('measure_power.battery_group_max_consumption_w', batteryMode.max_consumption_w ?? null); + await this._setCapabilityValue('measure_power.battery_group_max_production_w', batteryMode.max_production_w ?? null); + + // Flow triggers + await this._triggerFlowOnChange('battery_mode_changed_SDM230_v2', normalized); + //await this._triggerFlowOnChange('measure_power.battery_group_power_w', batteryMode.power_w ?? null); + } + + + + // Trigger flows when values change + //await this._triggerFlowOnChange('measure_power', data.power_w); + await this._triggerFlowOnChange('meter_power.import', data.energy_import_kwh); + await this._triggerFlowOnChange('meter_power.export', data.energy_export_kwh); + //await this._triggerFlowOnChange('measure_voltage', data.voltage_v); + //await this._triggerFlowOnChange('measure_current', data.current_a); + + // If everything succeeded + await this.setAvailable(); + + } catch (err) { + this.error('Polling failed:', err); + await this.setUnavailable(err).catch(this.error); + } +} + + + onSettings(MySettings) { + this.log('Settings updated'); + this.log('Settings:', MySettings); + // Update interval polling + if ('polling_interval' in MySettings.oldSettings + && MySettings.oldSettings.polling_interval !== MySettings.newSettings.polling_interval + ) { + this.log('Polling_interval for P1 changed to:', MySettings.newSettings.polling_interval); + clearInterval(this.onPollInterval); + // this.onPollInterval = setInterval(this.onPoll.bind(this), MySettings.newSettings.polling_interval * 1000); + this.onPollInterval = setInterval(this.onPoll.bind(this), 1000 * this.getSettings().polling_interval); + } + if ('mode' in MySettings.oldSettings + && MySettings.oldSettings.mode !== MySettings.newSettings.mode + ) { + this.log('Mode for Plugin Battery via SDM230 advanced settings changed to:', MySettings.newSettings.mode); + api.setMode(this.url, this.token, MySettings.newSettings.mode); + } + // return true; + } + +}; diff --git a/drivers/SDM230_v2/driver.compose.json b/drivers/SDM230_v2/driver.compose.json new file mode 100644 index 00000000..7cbe6fd3 --- /dev/null +++ b/drivers/SDM230_v2/driver.compose.json @@ -0,0 +1,123 @@ +{ + "name": { + "en": "kWh Meter 1P (APIv2)" + }, + "images": { + "large": "drivers/SDM230_v2/assets/images/large.png", + "small": "drivers/SDM230_v2/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM230_v2", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "measure_current", + "meter_power.import", + "meter_power.export", + "measure_voltage" + ], + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "meter_power.import": { + "decimals": 3, + "title": { + "en": "Total usage", + "nl": "Totaal gebruik" + } + }, + "meter_power.export": { + "decimals": 3, + "title": { + "en": "Total deliver", + "nl": "Totaal teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + }, + "insights": true + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + }, + "insights": true + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + }, + "insights": true + }, + "measure_power.battery_group_power_w": { + "title": { + "en": "Battery group Current combined Power", + "nl": "Battery groep Huidig samengesteld vermogen" + } + }, + "measure_power.battery_group_target_power_w": { + "title": { + "en": "Battery group Target Power", + "nl": "Battery groep Doel vermogen" + } + }, + "measure_power.battery_group_max_consumption_w": { + "title": { + "en": "Battery group Max allowed Consumption Power", + "nl": "Battery groep Max toegestaand gebruiksvermogen" + } + }, + "measure_power.battery_group_max_production_w": { + "title": { + "en": "Battery group Max allowed Production Power", + "nl": "Battery groep Max toegestaand leveringssvermogen" + } + } + }, + "pair": [ + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "authorize" + }, + "options": { "singular": true } + }, + { + "id": "authorize", + "navigation": + { + "prev": "list_devices" + } + } + ], + "id": "SDM230_v2", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "min": 1, + "unit": { + "en": "s" + } + } + ] +} \ No newline at end of file diff --git a/drivers/SDM230_v2/driver.flow.compose.json b/drivers/SDM230_v2/driver.flow.compose.json new file mode 100644 index 00000000..d6ceedc1 --- /dev/null +++ b/drivers/SDM230_v2/driver.flow.compose.json @@ -0,0 +1,117 @@ +{ + "triggers": [ + { + "id": "meter_power.import", + "title": { + "en": "Import power changed", + "nl": "Importvermogen gewijzigd" + }, + "titleFormatted": { + "en": "Import power changed", + "nl": "Importvermogen gewijzigd" + }, + "args": [], + "tokens": [ + { + "name": "import_power", + "type": "number", + "title": { + "en": "Import Power (W)", + "nl": "Importvermogen (W)" + } + } + ] + }, + { + "id": "meter_power.export", + "title": { + "en": "Export power changed", + "nl": "Exportvermogen gewijzigd" + }, + "titleFormatted": { + "en": "Export power changed", + "nl": "Exportvermogen gewijzigd" + }, + "args": [], + "tokens": [ + { + "name": "export_power", + "type": "number", + "title": { + "en": "Export Power (W)", + "nl": "Exportvermogen (W)" + } + } + ] + }, + { + "id": "battery_mode_changed_SDM230_v2", + "title": { "en": "Battery mode changed" }, + "titleFormatted": { "en": "Battery mode changed" }, + "args": [], + "tokens": [] + } + ], + "actions": [ + { + "id": "sdm230-set-battery-to-full-charge-mode", + "title": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "titleFormatted": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "args": [] + }, + { + "id": "sdm230-set-battery-to-zero-mode", + "title": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "titleFormatted": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "args": [] + }, + { + "id": "sdm230-set-battery-to-standby-mode", + "title": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "titleFormatted": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "args": [] + }, + { + "id": "sdm230-set-battery-to-zero-charge-only-mode", + "title": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Alleen Opladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Alleen Opladen modus" + }, + "args": [] + }, + { + "id": "sdm230-set-battery-to-zero-discharge-only-mode", + "title": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Alleen Ontladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Alleen Ontladen modus" + }, + "args": [] + } + ] +} diff --git a/drivers/SDM230_v2/driver.js b/drivers/SDM230_v2/driver.js new file mode 100644 index 00000000..fa129193 --- /dev/null +++ b/drivers/SDM230_v2/driver.js @@ -0,0 +1,6 @@ +'use strict'; + +const Homey = require('homey'); +const driver = require('../../includes/v2/Driver'); + +module.exports = driver; diff --git a/drivers/SDM230_v2/pair/authorize.html b/drivers/SDM230_v2/pair/authorize.html new file mode 100644 index 00000000..7fb8dd67 --- /dev/null +++ b/drivers/SDM230_v2/pair/authorize.html @@ -0,0 +1,48 @@ +
+

+

+

+

+ +
+ + diff --git a/drivers/SDM630-p1mode/assets/icon.svg b/drivers/SDM630-p1mode/assets/icon.svg new file mode 100644 index 00000000..2dba88f5 --- /dev/null +++ b/drivers/SDM630-p1mode/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/SDM630-p1mode/assets/images/large.png b/drivers/SDM630-p1mode/assets/images/large.png new file mode 100644 index 00000000..058db062 Binary files /dev/null and b/drivers/SDM630-p1mode/assets/images/large.png differ diff --git a/drivers/SDM630-p1mode/assets/images/small.png b/drivers/SDM630-p1mode/assets/images/small.png new file mode 100644 index 00000000..e3883a2d Binary files /dev/null and b/drivers/SDM630-p1mode/assets/images/small.png differ diff --git a/drivers/SDM630-p1mode/device.js b/drivers/SDM630-p1mode/device.js new file mode 100644 index 00000000..25eadb57 --- /dev/null +++ b/drivers/SDM630-p1mode/device.js @@ -0,0 +1,195 @@ +'use strict'; + +const Homey = require('homey'); + +const fetchWithTimeout = require('../../includes/utils/fetchWithTimeout'); +// const POLL_INTERVAL = 1000 * 1; // 1 seconds + +// const Homey2023 = Homey.platform === 'local' && Homey.platformVersion === 2; + +async function updateCapability(device, capability, value) { + try { + const current = device.getCapabilityValue(capability); + + // --- SAFE REMOVE --- + // Removal is allowed only when: + // 1) the new value is null + // 2) the current value in Homey is also null + + if (value == null && current == null) { + if (device.hasCapability(capability)) { + await device.removeCapability(capability); + device.log(`🗑️ Removed capability "${capability}"`); + } + return; + } + + // --- ADD IF MISSING --- + if (!device.hasCapability(capability)) { + try { + await device.addCapability(capability); + device.log(`➕ Added capability "${capability}"`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + } else { + throw err; + } + } + } + + // --- UPDATE --- + if (current !== value) { + await device.setCapabilityValue(capability, value); + } + + } catch (err) { + if (err.message === 'device_not_found') { + device.log(`⚠️ Skipping capability "${capability}" — device not found`); + return; + } + device.error(`❌ Failed updateCapability("${capability}")`, err); + } +} + +module.exports = class HomeWizardEnergyDevice630 extends Homey.Device { + +async onInit() { + // await this.setUnavailable(`${this.getName()} ${this.homey.__('device.init')}`); + const settings = this.getSettings(); + this.log('Settings for SDM630:', settings.polling_interval); + + if (settings.polling_interval == null) { + settings.polling_interval = 10; + await this.setSettings({ polling_interval: 10 }); + } + + this.onPollInterval = setInterval(this.onPoll.bind(this), 1000 * settings.polling_interval); + +// if (this.getClass() === 'sensor') { +// this.setClass('socket'); +// this.log('Changed sensor to socket.'); +// } + +} + + + onDeleted() { + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + } + + onDiscoveryAvailable(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this.log(`URL: ${this.url}`); + this.onPoll(); + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this.log(`URL: ${this.url}`); + this.log('onDiscoveryAddressChanged'); + this.onPoll(); + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this.log(`URL: ${this.url}`); + this.setAvailable(); + this.onPoll(); + } + + async onPoll() { + const settings = this.getSettings(); + + if (!this.url) { + if (settings.url) { + this.url = settings.url; + this.log(`ℹ️ this.url was empty, restored from settings: ${this.url}`); + } else { + this.error('❌ this.url is empty and no fallback settings.url found — aborting poll'); + await this.setUnavailable().catch(this.error); + return; + } + } + + try { + let res = await fetchWithTimeout(`${this.url}/data`); + if (!res || !res.ok) { + await new Promise((resolve) => setTimeout(resolve, 60000)); + res = await fetchWithTimeout(`${this.url}/data`); + if (!res || !res.ok) throw new Error(res ? res.statusText : 'Unknown error during fetch'); + } + + const data = await res.json(); + + // Core capabilities + await updateCapability(this, 'rssi', data.wifi_strength).catch(this.error); + await updateCapability(this, 'measure_power', data.active_power_w).catch(this.error); + await updateCapability(this, 'measure_power.active_power_w', data.active_power_w).catch(this.error); + await updateCapability(this, 'meter_power.consumed.t1', data.total_power_import_t1_kwh).catch(this.error); + + // Solar export + if (data.total_power_export_t1_kwh > 1) { + await updateCapability(this, 'meter_power.produced.t1', data.total_power_export_t1_kwh).catch(this.error); + } else { + await updateCapability(this, 'meter_power.produced.t1', null).catch(this.error); + } + + // Aggregated meter + await updateCapability( + this, + 'meter_power', + data.total_power_import_t1_kwh - data.total_power_export_t1_kwh + ).catch(this.error); + + // Always update 3‑phase values + await updateCapability(this, 'measure_power.l1', data.active_power_l1_w).catch(this.error); + await updateCapability(this, 'measure_power.l2', data.active_power_l2_w).catch(this.error); + await updateCapability(this, 'measure_power.l3', data.active_power_l3_w).catch(this.error); + + // Voltage per phase + await updateCapability(this, 'measure_voltage.l1', data.active_voltage_l1_v).catch(this.error); + await updateCapability(this, 'measure_voltage.l2', data.active_voltage_l2_v).catch(this.error); + await updateCapability(this, 'measure_voltage.l3', data.active_voltage_l3_v).catch(this.error); + + // Current per phase + await updateCapability(this, 'measure_current.l1', data.active_current_l1_a).catch(this.error); + await updateCapability(this, 'measure_current.l2', data.active_current_l2_a).catch(this.error); + await updateCapability(this, 'measure_current.l3', data.active_current_l3_a).catch(this.error); + + // Update settings URL if changed + if (this.url !== settings.url) { + this.log('SDM630-p1mode - Updating settings url'); + await this.setSettings({ url: this.url }); + } + + this.setAvailable().catch(this.error); + + } catch (err) { + this.error(err); + this.setUnavailable(err).catch(this.error); + } +} + + + + async onSettings(MySettings) { + this.log('Settings updated'); + this.log('Settings:', MySettings); + // Update interval polling + if ( + 'polling_interval' in MySettings.oldSettings + && MySettings.oldSettings.polling_interval !== MySettings.newSettings.polling_interval + ) { + this.log('Polling_interval for SDM630-p1 changed to:', MySettings.newSettings.polling_interval); + clearInterval(this.onPollInterval); + // this.onPollInterval = setInterval(this.onPoll.bind(this), MySettings.newSettings.polling_interval * 1000); + this.onPollInterval = setInterval(this.onPoll.bind(this), 1000 * this.getSettings().polling_interval); + } + // return true; + } + +}; diff --git a/drivers/SDM630-p1mode/driver.compose.json b/drivers/SDM630-p1mode/driver.compose.json new file mode 100644 index 00000000..65259eb4 --- /dev/null +++ b/drivers/SDM630-p1mode/driver.compose.json @@ -0,0 +1,135 @@ +{ + "name": { + "en": "kWh Meter (3 phase) P1 mode" + }, + "images": { + "large": "drivers/SDM630-p1mode/assets/images/large.png", + "small": "drivers/SDM630-p1mode/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM630", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3", + "rssi" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.consumed.t1", + "cumulativeExportedCapability": "meter_power.produced.t1" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + } + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + } + }, + "measure_voltage.l2": { + "title": { + "en": "Current Voltage phase 2", + "nl": "Huidig Voltage fase 2" + } + }, + "measure_voltage.l3": { + "title": { + "en": "Current Voltage phase 3", + "nl": "Huidig Voltage fase 3" + } + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/SDM630-p1mode/driver.js b/drivers/SDM630-p1mode/driver.js new file mode 100644 index 00000000..a1a0dc29 --- /dev/null +++ b/drivers/SDM630-p1mode/driver.js @@ -0,0 +1,5 @@ +'use strict'; + +const driver = require('../../includes/v1/driver.js'); + +module.exports = driver; diff --git a/drivers/SDM630-p1mode/driver.settings.compose.json b/drivers/SDM630-p1mode/driver.settings.compose.json new file mode 100644 index 00000000..fb0f7f36 --- /dev/null +++ b/drivers/SDM630-p1mode/driver.settings.compose.json @@ -0,0 +1,16 @@ +[ + { + "id": "polling_interval", + "type": "number", + "label": { "en": "Polling interval" }, + "value": 10, + "unit": { "en": "s" } + }, + { + "id": "url", + "type": "text", + "label": { "en": "url", + "nl": "url" + } + } +] \ No newline at end of file diff --git a/drivers/SDM630-p1mode/pair/start.html b/drivers/SDM630-p1mode/pair/start.html new file mode 100644 index 00000000..633815c6 --- /dev/null +++ b/drivers/SDM630-p1mode/pair/start.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/drivers/SDM630/assets/icon.svg b/drivers/SDM630/assets/icon.svg new file mode 100644 index 00000000..2dba88f5 --- /dev/null +++ b/drivers/SDM630/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/SDM630/assets/images/large.png b/drivers/SDM630/assets/images/large.png new file mode 100644 index 00000000..058db062 Binary files /dev/null and b/drivers/SDM630/assets/images/large.png differ diff --git a/drivers/SDM630/assets/images/small.png b/drivers/SDM630/assets/images/small.png new file mode 100644 index 00000000..e3883a2d Binary files /dev/null and b/drivers/SDM630/assets/images/small.png differ diff --git a/drivers/SDM630/device.js b/drivers/SDM630/device.js new file mode 100644 index 00000000..b3c254d1 --- /dev/null +++ b/drivers/SDM630/device.js @@ -0,0 +1,346 @@ +'use strict'; + +const Homey = require('homey'); +const fetchWithTimeout = require('../../includes/utils/fetchWithTimeout'); +const http = require('http'); + + + +/** + * Shared keep‑alive agent (blijft) + */ +const agent = new http.Agent({ + keepAlive: true, + keepAliveMsecs: 11000 +}); + +/** + * Stable capability updater — deletion‑safe + */ +async function updateCapability(device, capability, value) { + try { + const current = device.getCapabilityValue(capability); + + // --- SAFE REMOVE --- + // Removal is allowed only when: + // 1) the new value is null + // 2) the current value in Homey is also null + + if (value == null && current == null) { + if (device.hasCapability(capability)) { + await device.removeCapability(capability); + device.log(`🗑️ Removed capability "${capability}"`); + } + return; + } + + // --- ADD IF MISSING --- + if (!device.hasCapability(capability)) { + try { + await device.addCapability(capability); + device.log(`➕ Added capability "${capability}"`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + } else { + throw err; + } + } + } + + // --- UPDATE --- + if (current !== value) { + await device.setCapabilityValue(capability, value); + } + + } catch (err) { + if (err.message === 'device_not_found') { + device.log(`⚠️ Skipping capability "${capability}" — device not found`); + return; + } + device.error(`❌ Failed updateCapability("${capability}")`, err); + } +} + +module.exports = class HomeWizardEnergyDevice630 extends Homey.Device { + + async onInit() { + + this._debugLogs = []; + + const settings = this.getSettings(); + + if (settings.polling_interval == null) { + await this.setSettings({ polling_interval: 10 }); + } + + const interval = Math.max(settings.polling_interval, 2); + const offset = Math.floor(Math.random() * interval * 1000); + + if (this.onPollInterval) clearInterval(this.onPollInterval); + + setTimeout(() => { + this.onPoll().catch(this.error); + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + }, offset); + + if (this.getClass() === 'sensor') { + this.setClass('socket'); + } + } + + onDeleted() { + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + } + + /** + * Discovery + */ + onDiscoveryAvailable(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this.setAvailable(); + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this._debugLog(`Discovery address changed: ${this.url}`); + this.setAvailable(); + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this.setAvailable(); + } + + /** + * Debug logger (batched writes) + */ + _debugLog(msg) { + try { + if (!this._debugBuffer) this._debugBuffer = []; + const ts = new Date().toLocaleString('nl-NL', { hour12: false, timeZone: 'Europe/Amsterdam' }); + const driverName = this.driver.id; + const deviceName = this.getName(); + const safeMsg = typeof msg === 'string' ? msg : (msg instanceof Error ? msg.message : JSON.stringify(msg)); + const line = `${ts} [${driverName}] [${deviceName}] ${safeMsg}`; + this._debugBuffer.push(line); + if (this._debugBuffer.length > 20) this._debugBuffer.shift(); + if (!this._debugFlushTimeout) { + this._debugFlushTimeout = setTimeout(() => { + this._flushDebugLogs(); + this._debugFlushTimeout = null; + }, 5000); + } + } catch (err) { + this.error('Failed to write debug logs:', err.message || err); + } +} +_flushDebugLogs() { + if (!this._debugBuffer || this._debugBuffer.length === 0) return; + try { + const logs = this.homey.settings.get('debug_logs') || []; + logs.push(...this._debugBuffer); + if (logs.length > 500) logs.splice(0, logs.length - 500); + this.homey.settings.set('debug_logs', logs); + this._debugBuffer = []; + } catch (err) { + this.error('Failed to flush debug logs:', err.message || err); + } +} + + /** + * Cloud toggles + */ + async setCloudOn() { + if (!this.url) return; + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: true }) + }); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } catch (err) { + this._debugLog(`Cloud ON failed: ${err.code || ''} ${err.message || err}`); + this.error('Failed to enable cloud:', err); + } + } + + async setCloudOff() { + if (!this.url) return; + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: false }) + }); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } catch (err) { + this._debugLog(`Cloud OFF failed: ${err.code || ''} ${err.message || err}`); + this.error('Failed to disable cloud:', err); + } + } + + /** + * GET /data + */ + async onPoll() { + const settings = this.getSettings(); + + if (!this.url) { + if (settings.url) { + this.url = settings.url; + } else { + await this.setUnavailable('Missing URL'); + return; + } + } + + try { + + const res = await fetchWithTimeout(`${this.url}/data`, { + agent, + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + const data = await res.json(); + if (!data || typeof data !== 'object') throw new Error('Invalid JSON'); + + const tasks = []; + + // 1‑fase + totaal + tasks.push(updateCapability(this, 'measure_power', data.active_power_w)); + tasks.push(updateCapability(this, 'meter_power.consumed.t1', data.total_power_import_t1_kwh)); + tasks.push(updateCapability(this, 'rssi', data.wifi_strength)); + + if (data.total_power_export_t1_kwh > 1) { + tasks.push(updateCapability(this, 'meter_power.produced.t1', data.total_power_export_t1_kwh)); + } + + const net = data.total_power_import_t1_kwh - data.total_power_export_t1_kwh; + tasks.push(updateCapability(this, 'meter_power', net)); + + // 3‑fase power + tasks.push(updateCapability(this, 'measure_power.l1', data.active_power_l1_w)); + tasks.push(updateCapability(this, 'measure_power.l2', data.active_power_l2_w)); + tasks.push(updateCapability(this, 'measure_power.l3', data.active_power_l3_w)); + + // 3‑fase voltage + tasks.push(updateCapability(this, 'measure_voltage.l1', data.active_voltage_l1_v)); + tasks.push(updateCapability(this, 'measure_voltage.l2', data.active_voltage_l2_v)); + tasks.push(updateCapability(this, 'measure_voltage.l3', data.active_voltage_l3_v)); + + // 3‑fase current + tasks.push(updateCapability(this, 'measure_current.l1', data.active_current_l1_a)); + tasks.push(updateCapability(this, 'measure_current.l2', data.active_current_l2_a)); + tasks.push(updateCapability(this, 'measure_current.l3', data.active_current_l3_a)); + + // --- Phase energy meters (derived kWh) --- + const intervalSec = Math.max(settings.polling_interval, 2); + + // --- Local day detection (NO UTC) --- + const todayKey = new Date().toLocaleDateString('nl-NL', { + timeZone: 'Europe/Amsterdam' + }); + + const lastDayKey = this.getStoreValue('day_date'); + + // Daily reset when local calendar day changes + if (lastDayKey !== todayKey) { + await this.setStoreValue('day_l1', 0); + await this.setStoreValue('day_l2', 0); + await this.setStoreValue('day_l3', 0); + await this.setStoreValue('day_date', todayKey); + this.log('Daily phase energy counters reset (local day change)'); + } + + // Initialize total energy store values if missing + if (this.getStoreValue('meter_l1') == null) await this.setStoreValue('meter_l1', 0); + if (this.getStoreValue('meter_l2') == null) await this.setStoreValue('meter_l2', 0); + if (this.getStoreValue('meter_l3') == null) await this.setStoreValue('meter_l3', 0); + + // Initialize daily energy store values if missing + if (this.getStoreValue('day_l1') == null) await this.setStoreValue('day_l1', 0); + if (this.getStoreValue('day_l2') == null) await this.setStoreValue('day_l2', 0); + if (this.getStoreValue('day_l3') == null) await this.setStoreValue('day_l3', 0); + + // Convert W → kWh increment (can be negative = export) + const incL1 = (data.active_power_l1_w || 0) * (intervalSec / 3600); + const incL2 = (data.active_power_l2_w || 0) * (intervalSec / 3600); + const incL3 = (data.active_power_l3_w || 0) * (intervalSec / 3600); + + // Update total kWh + const newL1 = this.getStoreValue('meter_l1') + incL1; + const newL2 = this.getStoreValue('meter_l2') + incL2; + const newL3 = this.getStoreValue('meter_l3') + incL3; + + await this.setStoreValue('meter_l1', newL1); + await this.setStoreValue('meter_l2', newL2); + await this.setStoreValue('meter_l3', newL3); + + // Update daily kWh + const newDayL1 = this.getStoreValue('day_l1') + incL1; + const newDayL2 = this.getStoreValue('day_l2') + incL2; + const newDayL3 = this.getStoreValue('day_l3') + incL3; + + await this.setStoreValue('day_l1', newDayL1); + await this.setStoreValue('day_l2', newDayL2); + await this.setStoreValue('day_l3', newDayL3); + + // Update capabilities (total) + tasks.push(updateCapability(this, 'meter_power.l1', newL1)); + tasks.push(updateCapability(this, 'meter_power.l2', newL2)); + tasks.push(updateCapability(this, 'meter_power.l3', newL3)); + + // Update capabilities (daily) + tasks.push(updateCapability(this, 'meter_power.day.l1', newDayL1)); + tasks.push(updateCapability(this, 'meter_power.day.l2', newDayL2)); + tasks.push(updateCapability(this, 'meter_power.day.l3', newDayL3)); + + + + await Promise.allSettled(tasks); + + await this.setAvailable(); + + } catch (err) { + this._debugLog(`Poll failed: ${err.message}`); + this.setUnavailable(err.message || 'Polling error').catch(this.error); + } + } + + /** + * Settings handler + */ + onSettings(event) { + const { newSettings, changedKeys } = event; + + for (const key of changedKeys) { + + if (key === 'polling_interval') { + const interval = Math.max(newSettings.polling_interval, 2); + + if (this.onPollInterval) clearInterval(this.onPollInterval); + + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + } + + if (key === 'cloud') { + if (newSettings.cloud == 1) this.setCloudOn(); + else this.setCloudOff(); + } + } + } +}; diff --git a/drivers/SDM630/driver.compose.json b/drivers/SDM630/driver.compose.json new file mode 100644 index 00000000..75fc0e21 --- /dev/null +++ b/drivers/SDM630/driver.compose.json @@ -0,0 +1,179 @@ +{ + "name": { + "en": "kWh Meter (3 phase)" + }, + "images": { + "large": "drivers/SDM630/assets/images/large.png", + "small": "drivers/SDM630/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM630", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "meter_power.l1", + "meter_power.l2", + "meter_power.l3", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "meter_power.day.l1", + "meter_power.day.l2", + "meter_power.day.l3", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3", + "rssi" + ], + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "meter_power.day.l1": { + "decimals": 3, + "title": { + "en": "Daily usage Phase 1", + "nl": "Dagverbruik Fase 1" + } + }, + "meter_power.day.l2": { + "decimals": 3, + "title": { + "en": "Daily usage Phase 2", + "nl": "Dagverbruik Fase 2" + } + }, + "meter_power.day.l3": { + "decimals": 3, + "title": { + "en": "Daily usage Phase 3", + "nl": "Dagverbruik Fase 3" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "meter_power.l1": { + "decimals": 3, + "title": { + "en": "Total usage KWh Phase 1", + "nl": "Totaal verbruik KWh Fase 1" + } + }, + "meter_power.l2": { + "decimals": 3, + "title": { + "en": "Total usage KWh Phase 2", + "nl": "Totaal verbruik KWh Fase 2" + } + }, + "meter_power.l3": { + "decimals": 3, + "title": { + "en": "Total usage KWh Phase 3", + "nl": "Totaal verbruik KWh Fase 3" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + } + }, + "measure_voltage.l2": { + "title": { + "en": "Current Voltage phase 2", + "nl": "Huidig Voltage fase 2" + } + }, + "measure_voltage.l3": { + "title": { + "en": "Current Voltage phase 3", + "nl": "Huidig Voltage fase 3" + } + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/SDM630/driver.js b/drivers/SDM630/driver.js new file mode 100644 index 00000000..a1a0dc29 --- /dev/null +++ b/drivers/SDM630/driver.js @@ -0,0 +1,5 @@ +'use strict'; + +const driver = require('../../includes/v1/driver.js'); + +module.exports = driver; diff --git a/drivers/SDM630/driver.settings.compose.json b/drivers/SDM630/driver.settings.compose.json new file mode 100644 index 00000000..857734d9 --- /dev/null +++ b/drivers/SDM630/driver.settings.compose.json @@ -0,0 +1,25 @@ +[ + { + "id": "polling_interval", + "type": "number", + "label": { "en": "Polling interval" }, + "value": 10, + "min": 1, + "unit": { "en": "s" } + }, + { + "id": "cloud", + "type": "number", + "label": { "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { "en": "url", + "nl": "url" + } + } +] \ No newline at end of file diff --git a/drivers/SDM630/pair/start.html b/drivers/SDM630/pair/start.html new file mode 100644 index 00000000..633815c6 --- /dev/null +++ b/drivers/SDM630/pair/start.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/drivers/SDM630_v2/assets/icon.svg b/drivers/SDM630_v2/assets/icon.svg new file mode 100644 index 00000000..2dba88f5 --- /dev/null +++ b/drivers/SDM630_v2/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/SDM630_v2/assets/images/large.png b/drivers/SDM630_v2/assets/images/large.png new file mode 100644 index 00000000..058db062 Binary files /dev/null and b/drivers/SDM630_v2/assets/images/large.png differ diff --git a/drivers/SDM630_v2/assets/images/small.png b/drivers/SDM630_v2/assets/images/small.png new file mode 100644 index 00000000..e3883a2d Binary files /dev/null and b/drivers/SDM630_v2/assets/images/small.png differ diff --git a/drivers/SDM630_v2/device.js b/drivers/SDM630_v2/device.js new file mode 100644 index 00000000..aff5e276 --- /dev/null +++ b/drivers/SDM630_v2/device.js @@ -0,0 +1,513 @@ +'use strict'; + +const Homey = require('homey'); +const api = require('../../includes/v2/Api'); + +// const POLL_INTERVAL = 1000 * 1; // 1 seconds + +function normalizeBatteryMode(data) { + const knownModes = [ + 'zero', + 'standby', + 'to_full', + 'zero_charge_only', + 'zero_discharge_only' + ]; + + let rawMode = data.mode; + + if (typeof rawMode === 'string') { + rawMode = rawMode.trim(); + try { rawMode = JSON.parse(rawMode); } + catch { rawMode = rawMode.replace(/^["']+|["']+$/g, ''); } + } + + if (knownModes.includes(rawMode)) return rawMode; + + if (Array.isArray(data.permissions)) { + const perms = [...data.permissions].sort().join(','); + if (perms === '') return 'standby'; + if (perms === 'charge_allowed,discharge_allowed') return 'zero'; + if (perms === 'charge_allowed') return 'zero_charge_only'; + if (perms === 'discharge_allowed') return 'zero_discharge_only'; + } + + return 'standby'; +} + + + +module.exports = class HomeWizardEnergyDevice630V2 extends Homey.Device { + + async onInit() { + + // await this.setUnavailable(`${this.getName()} ${this.homey.__('device.init')}`); + + this.token = await this.getStoreValue('token'); + //this.log('Token:', this.token); + + await this._updateCapabilities(); + await this._registerCapabilityListeners(); + + const settings = this.getSettings(); + this.log('Settings for SDM630 apiv2: ', settings.polling_interval); + + // Check if polling interval is set in settings else set default value + if (settings.polling_interval === undefined) { + settings.polling_interval = 10; // Default to 10 second if not set + await this.setSettings({ + // Update settings in Homey + polling_interval: 10, + }); + } + + // Register flow card listeners only once (prevent "already registered" warnings) + if (!this.homey.app._flowListenersRegistered_SDM630) { + this.homey.app._flowListenersRegistered_SDM630 = true; + + // Condition Card + const ConditionCardCheckBatteryMode = this.homey.flow.getConditionCard('check-battery-mode'); + ConditionCardCheckBatteryMode.registerRunListener(async (args, state) => { + // this.log('CheckBatteryModeCard'); + + return new Promise(async (resolve, reject) => { + try { + const response = await api.getMode(this.url, this.token); // NEEDS TESTING WITH SDM230 and BATTERY + + if (!response) { + this.log('Invalid response, returning false'); + return resolve(false); + } + + this.log('Retrieved mode:', response.mode); + const normalized = normalizeBatteryMode(response); + return resolve(args.mode === normalized); + + + } catch (error) { + this.log('Error retrieving mode:', error); + return resolve(false); // Or reject(error), depending on your error-handling approach + } + }); + }); + + // + // ✅ SDM630 Battery Mode Action Cards + // + + // Zero mode + this.homey.flow.getActionCard('sdm630-set-battery-to-zero-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Zero Mode'); + + try { + const response = await api.setMode(this.url, this.token, 'zero'); + + if (!response) { + this.log('Invalid response, returning false'); + return false; + } + + const normalized = normalizeBatteryMode(response); + this.log('Set mode to zero:', normalized); + return normalized; + + } catch (error) { + this.error('Error set mode to zero:', error); + return false; + } + }); + + + // Standby mode + this.homey.flow.getActionCard('sdm630-set-battery-to-standby-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Standby Mode'); + + try { + const response = await api.setMode(this.url, this.token, 'standby'); + + if (!response) { + this.log('Invalid response, returning false'); + return false; + } + + const normalized = normalizeBatteryMode(response); + this.log('Set mode to standby:', normalized); + return normalized; + + } catch (error) { + this.error('Error set mode to standby:', error); + return false; + } + }); + + + // Full charge mode + this.homey.flow.getActionCard('sdm630-set-battery-to-full-charge-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Full Charge Mode'); + + try { + const response = await api.setMode(this.url, this.token, 'to_full'); + + if (!response) { + this.log('Invalid response, returning false'); + return false; + } + + const normalized = normalizeBatteryMode(response); + this.log('Set mode to full charge:', normalized); + return normalized; + + } catch (error) { + this.error('Error set mode to full charge:', error); + return false; + } + }); + + + // Zero charge only + this.homey.flow.getActionCard('sdm630-set-battery-to-zero-charge-only-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Zero Charge Only Mode'); + + try { + const response = await api.setMode(this.url, this.token, 'zero_charge_only'); + + if (!response) { + this.log('Invalid response, returning false'); + return false; + } + + const normalized = normalizeBatteryMode(response); + this.log('Set mode to zero_charge_only:', normalized); + return normalized; + + } catch (error) { + this.error('Error set mode to zero_charge_only:', error); + return false; + } + }); + + + // Zero discharge only + this.homey.flow.getActionCard('sdm630-set-battery-to-zero-discharge-only-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Zero Discharge Only Mode'); + + try { + const response = await api.setMode(this.url, this.token, 'zero_discharge_only'); + + if (!response) { + this.log('Invalid response, returning false'); + return false; + } + + const normalized = normalizeBatteryMode(response); + this.log('Set mode to zero_discharge_only:', normalized); + return normalized; + + } catch (error) { + this.error('Error set mode to zero_discharge_only:', error); + return false; + } + }); + + } // End of _flowListenersRegistered_SDM630 guard + + this.onPollInterval = setInterval(this.onPoll.bind(this), 1000 * settings.polling_interval); + + this._triggerFlowPrevious = {}; + + /* + const ActionCardChangeBatteryMode = this.homey.flow.getActionCard('change-battery-mode') + ActionCardChangeBatteryMode.registerRunListener(async (args, state) => { + this.log('ChangeBatteryModeCard change to:', args); + + if (!this.url) { + return false; + } + + return new Promise(async (resolve, reject) => { + try { + const response = await api.setMode(this.url, this.token, args.mode); // NEEDS TESTING WITH P1 and BATTERY + + if (!response || typeof response.mode === 'undefined') { + this.log('Invalid response, returning false'); + return resolve(false); + } + + this.log('Set mode:', response.mode); + return resolve(response.mode); // Returns the mode value + } catch (error) { + this.log('Error set mode:', error); + return resolve(false); // Or reject(error), depending on your error-handling approach + } + }); + }); + */ + + } + + onDeleted() { + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + } + + onDiscoveryAvailable(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.onPoll(); + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.log('onDiscoveryAddressChanged'); + this.onPoll(); + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.setAvailable(); + this.onPoll(); + } + + /** + * Helper function to update capabilities configuration. + * This function is called when the device is initialized. + */ + async _updateCapabilities() { + if (!this.hasCapability('identify')) { + try { + await this.addCapability('identify'); + this.log(`created capability identify for ${this.getName()}`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log(`Capability already exists: identify — ignoring`); + } else { + this.error(err); + } + } + } + + // Remove capabilities that are not needed + if (this.hasCapability('measure_power.power_w')) { + await this.removeCapability('measure_power.power_w').catch(this.error); + this.log(`removed capability measure_power.power_w for ${this.getName()}`); + } + } + + /** + * Helper function to register capability listeners. + * This function is called when the device is initialized. + */ + async _registerCapabilityListeners() { + this.registerCapabilityListener('identify', async (value) => { + await api.identify(this.url, this.token); + }); + } + + /** + * Helper function for 'optional' capabilities. + * This function is called when the device is initialized. + * It will create the capability if it doesn't exist. + * + * We do not remove capabilities here, as we assume the user may want to keep them. + * Besides that we assume that the P1 Meter is connected to a smart meter that does not change often. + * + * @param {string} capability The capability to set + * @param {*} value The value to set + * @returns {Promise} A promise that resolves when the capability is set + */ + async _setCapabilityValue(capability, value) { + // Test if value is undefined, if so, we don't set the capability + if (value === undefined) { + return; + } + + // Create a new capability if it doesn't exist + if (!this.hasCapability(capability)) { + try { + await this.addCapability(capability); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log(`Capability already exists: ${capability} — ignoring`); + } else { + this.error(err); + } + } + } + + // Set the capability value + await this.setCapabilityValue(capability, value).catch(this.error); + } + + /** + * Helper function to trigger flows on change. + * This function is called when the device is initialized. + * + * We use this function to trigger flows when the value changes. + * We store the previous value in a variable. + * + * @param {*} flow_id Flow ID name + * @param {*} value The value to check for changes + * @returns {Promise} A promise that resolves when the flow is triggered + */ + async _triggerFlowOnChange(flow_id, value) { + + // Ignore if value is undefined + if (value === undefined) { + return; + } + + // Check if the value is undefined + // If so, we assume this is the first time we are setting the value + // We cannot trust the the 'trigger' function to be called with the correct value + if (this._triggerFlowPrevious[flow_id] === undefined) { + this._triggerFlowPrevious[flow_id] = value; + return; + } + + // Return of the value is the same as the previous value + if (this._triggerFlowPrevious[flow_id] === value) { + + // We don't need to trigger the flow + return; + } + + // It is a bit 'costly' to get the flow card every time + // But we can assume the trigger does not change often + const flow = this.homey.flow.getDeviceTriggerCard(flow_id); + if (flow === undefined) { + this.error('Flow not found'); + return; + } + + // Update value and trigger the flow + this._triggerFlowPrevious[flow_id] = value; + flow.trigger(this, { [flow_id]: value }).catch(this.error); + } + +async onPoll() { + try { + const settings = this.getSettings(); + + // 1. Restore URL if runtime is empty + if (!this.url) { + if (settings.url) { + this.url = settings.url; + } else { + await this.setUnavailable('Missing URL'); + return; + } + } + + // 2. Sync settings if discovery changed the URL + if (this.url && this.url !== settings.url) { + await this.setSettings({ url: this.url }).catch(this.error); + } + + // Refresh token if missing + if (!this.token) { + this.token = await this.getStoreValue('token'); + } + + // --- Main API calls --- + const data = await api.getMeasurement(this.url, this.token); + + const setCapabilityPromises = []; + + // Power (total + per phase) + setCapabilityPromises.push(this._setCapabilityValue('measure_power', data.power_w)); + setCapabilityPromises.push(this._setCapabilityValue('measure_power.l1', data.power_l1_w)); + setCapabilityPromises.push(this._setCapabilityValue('measure_power.l2', data.power_l2_w)); + setCapabilityPromises.push(this._setCapabilityValue('measure_power.l3', data.power_l3_w)); + + // Import / Export + setCapabilityPromises.push(this._setCapabilityValue('meter_power.import', data.energy_import_kwh)); + if (data.energy_export_kwh !== 0) { + setCapabilityPromises.push(this._setCapabilityValue('meter_power.export', data.energy_export_kwh)); + } + + // Aggregated meter_power + if (!this.hasCapability('meter_power')) { + await this.addCapability('meter_power').catch(this.error); + } + if (data.energy_import_kwh !== undefined) { + const calcValue = data.energy_import_kwh - data.energy_export_kwh; + if (this.getCapabilityValue('meter_power') !== calcValue) { + setCapabilityPromises.push(this._setCapabilityValue('meter_power', calcValue)); + } + } + + // Voltage per phase + setCapabilityPromises.push(this._setCapabilityValue('measure_voltage.l1', data.voltage_l1_v)); + setCapabilityPromises.push(this._setCapabilityValue('measure_voltage.l2', data.voltage_l2_v)); + setCapabilityPromises.push(this._setCapabilityValue('measure_voltage.l3', data.voltage_l3_v)); + + // Current (total + per phase) + setCapabilityPromises.push(this._setCapabilityValue('measure_current', data.current_a)); + setCapabilityPromises.push(this._setCapabilityValue('measure_current.l1', data.current_l1_a)); + setCapabilityPromises.push(this._setCapabilityValue('measure_current.l2', data.current_l2_a)); + setCapabilityPromises.push(this._setCapabilityValue('measure_current.l3', data.current_l3_a)); + + await Promise.allSettled(setCapabilityPromises); + + // For battery mode + + const batteryMode = await api.getMode(this.url, this.token); + + if (batteryMode !== undefined) { + const normalized = normalizeBatteryMode(batteryMode); + + if (settings.mode !== normalized) { + await this.setSettings({ mode: normalized }); + } + + await this._setCapabilityValue('measure_power.battery_group_power_w', batteryMode.power_w ?? null); + await this._setCapabilityValue('measure_power.battery_group_target_power_w', batteryMode.target_power_w ?? null); + await this._setCapabilityValue('measure_power.battery_group_max_consumption_w', batteryMode.max_consumption_w ?? null); + await this._setCapabilityValue('measure_power.battery_group_max_production_w', batteryMode.max_production_w ?? null); + + // ✅ Flow triggers MUST be inside this block + // await this._triggerFlowOnChange('battery_mode', normalized); + await this._triggerFlowOnChange('battery_mode_changed_SDM630_v2', normalized); + // await this._triggerFlowOnChange('measure_power.battery_group_power_w', batteryMode.power_w ?? null); + } + + // If everything succeeded + await this.setAvailable(); + + } catch (err) { + this.error('Polling failed:', err); + await this.setUnavailable(err).catch(this.error); + } +} + + onSettings(MySettings) { + this.log('Settings updated'); + this.log('Settings:', MySettings); + // Update interval polling + if ('polling_interval' in MySettings.oldSettings + && MySettings.oldSettings.polling_interval !== MySettings.newSettings.polling_interval + ) { + this.log('Polling_interval for P1 changed to:', MySettings.newSettings.polling_interval); + clearInterval(this.onPollInterval); + // this.onPollInterval = setInterval(this.onPoll.bind(this), MySettings.newSettings.polling_interval * 1000); + this.onPollInterval = setInterval(this.onPoll.bind(this), 1000 * this.getSettings().polling_interval); + } + if ('mode' in MySettings.oldSettings + && MySettings.oldSettings.mode !== MySettings.newSettings.mode + ) { + this.log('Mode for Plugin Battery via SDM230 advanced settings changed to:', MySettings.newSettings.mode); + api.setMode(this.url, this.token, MySettings.newSettings.mode); + } + // return true; + } + +}; diff --git a/drivers/SDM630_v2/driver.compose.json b/drivers/SDM630_v2/driver.compose.json new file mode 100644 index 00000000..e8ae0c20 --- /dev/null +++ b/drivers/SDM630_v2/driver.compose.json @@ -0,0 +1,159 @@ +{ + "name": { + "en": "kWh Meter 3P (APIv2)" + }, + "images": { + "large": "drivers/SDM630_v2/assets/images/large.png", + "small": "drivers/SDM630_v2/assets/images/small.png" + }, + "class": "socket", + "discovery": "SDM630_v2", + "platforms": [ + "local" + ], + "capabilities": [ + "measure_power", + "meter_power", + "meter_power.import", + "meter_power.export", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "measure_current", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3" + ], + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "meter_power.import": { + "decimals": 3, + "title": { + "en": "Total usage", + "nl": "Totaal gebruik" + } + }, + "meter_power.export": { + "decimals": 3, + "title": { + "en": "Total deliver", + "nl": "Totaal teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "measure_power.battery_group_power_w": { + "title": { + "en": "Battery group Current combined Power", + "nl": "Battery groep Huidig samengesteld vermogen" + } + }, + "measure_power.battery_group_target_power_w": { + "title": { + "en": "Battery group Target Power", + "nl": "Battery groep Doel vermogen" + } + }, + "measure_power.battery_group_max_consumption_w": { + "title": { + "en": "Battery group Max allowed Consumption Power", + "nl": "Battery groep Max toegestaand gebruiksvermogen" + } + }, + "measure_power.battery_group_max_production_w": { + "title": { + "en": "Battery group Max allowed Production Power", + "nl": "Battery groep Max toegestaand leveringssvermogen" + } + } + }, + "pair": [ + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "authorize" + }, + "options": { "singular": true } + }, + { + "id": "authorize", + "navigation": + { + "prev": "list_devices" + } + } + ], + "id": "SDM630_v2", + "settings": [ + { + "id": "polling_interval", + "type": "number", + "label": { + "en": "Polling interval" + }, + "value": 10, + "min": 1, + "unit": { + "en": "s" + } + } + ] +} \ No newline at end of file diff --git a/drivers/SDM630_v2/driver.flow.compose.json b/drivers/SDM630_v2/driver.flow.compose.json new file mode 100644 index 00000000..cd14352e --- /dev/null +++ b/drivers/SDM630_v2/driver.flow.compose.json @@ -0,0 +1,72 @@ +{"triggers": [ + { + "id": "battery_mode_changed_SDM630_v2", + "title": { "en": "Battery mode changed" }, + "titleFormatted": { "en": "Battery mode changed" }, + "args": [], + "tokens": [] + } + ], + "actions": [ + { + "id": "sdm630-set-battery-to-full-charge-mode", + "title": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "titleFormatted": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "args": [] + }, + { + "id": "sdm630-set-battery-to-zero-mode", + "title": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "titleFormatted": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "args": [] + }, + { + "id": "sdm630-set-battery-to-standby-mode", + "title": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "titleFormatted": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "args": [] + }, + { + "id": "sdm630-set-battery-to-zero-charge-only-mode", + "title": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Alleen Opladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Alleen Opladen modus" + }, + "args": [] + }, + { + "id": "sdm630-set-battery-to-zero-discharge-only-mode", + "title": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Alleen Ontladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Alleen Ontladen modus" + }, + "args": [] + } + ] +} diff --git a/drivers/SDM630_v2/driver.js b/drivers/SDM630_v2/driver.js new file mode 100644 index 00000000..fa129193 --- /dev/null +++ b/drivers/SDM630_v2/driver.js @@ -0,0 +1,6 @@ +'use strict'; + +const Homey = require('homey'); +const driver = require('../../includes/v2/Driver'); + +module.exports = driver; diff --git a/drivers/SDM630_v2/pair/authorize.html b/drivers/SDM630_v2/pair/authorize.html new file mode 100644 index 00000000..acf2a741 --- /dev/null +++ b/drivers/SDM630_v2/pair/authorize.html @@ -0,0 +1,48 @@ +
+

+

+

+

+ +
+ + diff --git a/drivers/battery-policy/assets/icon.svg b/drivers/battery-policy/assets/icon.svg new file mode 100644 index 00000000..40b00204 --- /dev/null +++ b/drivers/battery-policy/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/battery-policy/assets/images/large.png b/drivers/battery-policy/assets/images/large.png new file mode 100644 index 00000000..78fa22a6 Binary files /dev/null and b/drivers/battery-policy/assets/images/large.png differ diff --git a/drivers/battery-policy/assets/images/small.png b/drivers/battery-policy/assets/images/small.png new file mode 100644 index 00000000..b6aa955d Binary files /dev/null and b/drivers/battery-policy/assets/images/small.png differ diff --git a/drivers/battery-policy/assets/planning.html b/drivers/battery-policy/assets/planning.html new file mode 100644 index 00000000..ec6c3298 --- /dev/null +++ b/drivers/battery-policy/assets/planning.html @@ -0,0 +1,342 @@ + + + + + + Battery Planning + + + +
+

Batterij Planning - 24 Uur

+ +
+ +
+ +
+

Legenda

+
+
+
+
Volledig laden
+
+
+
☀️
+
Alleen PV laden
+
+
+
🔋
+
Ontladen
+
+
+
⏸️
+
Standby
+
+
+
⏹️
+
Zero/Passthrough
+
+
+
+
Lage prijs
+
+
+
+
Hoge prijs
+
+
+
+
Huidig uur
+
+
+
+ +
+ +
+
+ + + + diff --git a/drivers/battery-policy/device.js b/drivers/battery-policy/device.js new file mode 100644 index 00000000..175ad0d7 --- /dev/null +++ b/drivers/battery-policy/device.js @@ -0,0 +1,2003 @@ +'use strict'; + +const Homey = require('homey'); +const WeatherForecaster = require('../../lib/weather-forecaster'); +const PolicyEngine = require('../../lib/policy-engine'); +const TariffManager = require('../../lib/tariff-manager'); +const LearningEngine = require('../../lib/learning-engine'); +const EfficiencyEstimator = require('../../lib/efficiency-estimator'); +const OptimizationEngine = require('../../lib/optimization-engine'); + +const debug = false; + +function _memMB(label) { + try { + const hs = require('v8').getHeapStatistics(); + const heap = (hs.used_heap_size / 1024 / 1024).toFixed(1); + const tot = (hs.total_heap_size / 1024 / 1024).toFixed(1); + console.log(`[MEM][BatteryPolicy] ${label}: heap=${heap}/${tot}MB`); + } catch (_) { + console.log(`[MEM][BatteryPolicy] ${label}: unavailable`); + } +} + +class BatteryPolicyDevice extends Homey.Device { + + async onInit() { + this.log('BatteryPolicyDevice initialized'); + _memMB('onInit-start'); + + // Components + this.learningEngine = new LearningEngine(this.homey, this); + await this.learningEngine.initialize(); + _memMB('after-learningEngine.initialize'); + + this.weatherForecaster = new WeatherForecaster(this.homey, this.learningEngine); + this.policyEngine = new PolicyEngine(this.homey, this.getSettings()); + this.tariffManager = new TariffManager(this.homey, this.getSettings()); + _memMB('after-engines-created'); + this.explainabilityEngine = null; // lazy-loaded on first policy check + this.chartGenerator = null; // lazy-loaded on first chart request + this.efficiencyEstimator = new EfficiencyEstimator(this.homey); + this.optimizationEngine = new OptimizationEngine(this.getSettings()); + + + // State + this.p1Device = null; + this.weatherData = null; + this.lastRecommendation = null; + this._lastPvEstimateW = 0; // For EMA smoothing + this._pvProductionW = null; // User-provided PV production via flow card + this._pvProductionTimestamp = null; // When the PV data was last updated + this._pvActualHourly = null; // Accumulator for chart: {date, hourly[], sums[], counts[]} + this._pvState = false; // Track PV state with hysteresis + this._lastPvPolicyRun = null; // Debounce PV-triggered policy runs + + await this._initializeCapabilities(); + _memMB('after-initializeCapabilities'); + this._registerCapabilityListeners(); + + // Connect P1 after short delay + this.homey.setTimeout(() => { + this._connectP1Device().catch(err => this.error(err)); + }, 1500); + + // Schedule periodic checks + this._schedulePolicyCheck(); + + // Migrate legacy weather_location (city name) to weather_latitude/weather_longitude + await this._migrateWeatherLocation(); + + // Weather fetch only in dynamic + if (this.getSettings().tariff_type === 'dynamic') { + this._updateWeather() + .then(() => _memMB('after-weather-fetch')) + .catch(err => this.error('Initial weather fetch failed:', err)); + + // Schedule periodic price refresh (every 30 minutes) + this._schedulePriceRefresh(); + } + + // Push device settings immediately so planning page has correct values after restart + // (normally pushed on every _runPolicyCheck, but that runs with a delay) + const s = this.getSettings(); + this.homey.settings.set('device_settings', { + max_charge_price: s.max_charge_price || 0.19, + min_discharge_price: s.min_discharge_price || 0.22, + min_soc: s.min_soc || 10, + max_soc: s.max_soc || 95, + battery_efficiency: s.battery_efficiency || 0.78, + min_profit_margin: s.min_profit_margin || 0.01, + tariff_type: s.tariff_type || 'dynamic', + policy_interval: s.policy_interval || 15, + pv_capacity_w: s.pv_capacity_w || 0, + pv_estimation_enabled: s.pv_estimation_enabled || false, + }); + + this.log('BatteryPolicyDevice ready'); + _memMB('onInit-done'); + } + + async _initializeCapabilities() { + const tariffType = this.getSettings().tariff_type || 'dynamic'; + + const defaults = { + policy_mode: tariffType === 'dynamic' ? 'balanced' : 'balanced-fixed', + auto_apply: true, + recommended_mode: 'preserve', + sun_score: 0, + predicted_sun_hours: 0, + confidence_score: 0, + explanation_summary: 'Initializing policy engine...', + policy_debug_price: '-', + policy_debug_top3low: '-', + policy_debug_top3high: '-', + policy_debug_sun: '-', + policy_debug_learning: '-', + battery_soc_mirror: 50, + grid_power_mirror: 0, + battery_rte: 0.78, + last_update: new Date().toISOString(), + active_mode: 'unknown', + override_until: null, + weather_override: 'auto' + }; + + for (const [capability, defaultValue] of Object.entries(defaults)) { + if (!this.hasCapability(capability)) { + await this.addCapability(capability).catch(err => { + if (err && err.code === 409) return; + this.error(`Failed to add capability ${capability}:`, err); + }); + } + + const current = this.getCapabilityValue(capability); + + if (capability === 'auto_apply' && current === false) { + this.log('ℹ️ Forcing auto_apply to true (was false)'); + await this.setCapabilityValue(capability, true).catch(err => + this.error(`Failed to set ${capability}:`, err) + ); + } else if (capability === 'policy_mode' && current === 'balanced') { + // Migrate old 'balanced' to type-specific mode + const newMode = tariffType === 'dynamic' ? 'balanced' : 'balanced-fixed'; + this.log(`ℹ️ Migrating policy_mode 'balanced' to '${newMode}' based on tariff type`); + await this.setCapabilityValue(capability, newMode).catch(err => + this.error(`Failed to migrate ${capability}:`, err) + ); + } else if (current === null || current === undefined) { + await this.setCapabilityValue(capability, defaultValue).catch(err => + this.error(`Failed to set ${capability}:`, err) + ); + } + } + } + + _registerCapabilityListeners() { + + // POLICY ENABLED / DISABLED + this.registerCapabilityListener('policy_enabled', async (value) => { + const current = this.getCapabilityValue('policy_enabled'); + + if (current === value) { + this.log(`Policy state unchanged (${value}), ignoring sync event`); + return value; + } + + this.log(`Policy ${value ? 'enabled' : 'disabled'}`); + + if (value) { + await this._runPolicyCheck(); + } + + return value; + }); + + // POLICY MODE + this.registerCapabilityListener('policy_mode', async (value) => { + const current = this.getCapabilityValue('policy_mode'); + + if (current === value) { + this.log(`Policy mode unchanged (${value}), ignoring sync event`); + return value; + } + + this.log(`Policy mode changed to: ${value}`); + this.policyEngine.updateSettings({ policy_mode: value }); + await this._runPolicyCheck(); + return value; + }); + + // AUTO APPLY + this.registerCapabilityListener('auto_apply', async (value) => { + const current = this.getCapabilityValue('auto_apply'); + + if (current === value) { + this.log(`Auto-apply unchanged (${value}), ignoring sync event`); + return value; + } + + this.log(`Auto-apply ${value ? 'enabled' : 'disabled'}`); + + if (value && this.lastRecommendation) { + const applyMode = this.lastRecommendation.hwMode || this.lastRecommendation.policyMode; + await this._applyRecommendation(applyMode, this.lastRecommendation.confidence); + } + + return value; + }); + + // WEATHER OVERRIDE + this.registerCapabilityListener('weather_override', async (value) => { + const settings = this.getSettings(); + const current = this.getCapabilityValue('weather_override'); + + if (current === value) { + this.log(`Weather override unchanged (${value}), ignoring sync event`); + return value; + } + + if (settings.tariff_type !== 'dynamic') { + this.log('Weather override ignored (fixed tariff)'); + return value; + } + + this.log(`Weather override changed to: ${value}`); + await this._runPolicyCheck(); + return value; + }); + + } + + /** + * Connect to P1 (energy_v2) + */ + async _connectP1Device() { + const p1DeviceId = this.getSetting('p1_device_id'); + + if (!p1DeviceId) { + this.error('No P1 device configured'); + return; + } + + try { + const driver = this.homey.drivers.getDriver('energy_v2'); + if (!driver) { + this.error('P1 driver (energy_v2) not found'); + return; + } + + this.p1Device = driver.getDevice({ id: p1DeviceId }); + + if (!this.p1Device) { + this.error('P1 device not found'); + return; + } + + this.log(`Connected to P1 device: ${this.p1Device.getName()}`); + + // Remove stale listeners from a previous connection (e.g. reconnect) + this._cleanupP1Listeners(); + + // Single battery_event handler + this._onBatteryEvent = (payload) => { + this._lastBatteryTargetW = payload.target_power_w ?? 0; + this._lastBatteryEventTs = Date.now(); + this.log(`🔌 Battery target event → target=${this._lastBatteryTargetW}W`); + }; + this.p1Device.on('battery_event', this._onBatteryEvent); + + this._setupP1Listeners(); + + // Seed RTE from hardware meters immediately at startup + try { + const battDriver = this.homey.drivers.getDriver('plugin_battery'); + if (battDriver) { + let totalImport = 0, totalExport = 0; + for (const dev of battDriver.getDevices()) { + totalImport += dev.getCapabilityValue('meter_power.import') || 0; + totalExport += dev.getCapabilityValue('meter_power.export') || 0; + } + const newRte = this.efficiencyEstimator.updateFromMeters(totalImport, totalExport); + if (newRte) this.policyEngine.updateSettings({ battery_efficiency: newRte }); + } + } catch (e) { /* driver not available yet */ } + + // Restore last known mode immediately so the battery doesn't sit in firmware + // default (zero_charge_only) during the gap between restart and first policy run. + try { + const modeHistory = this.homey.settings.get('policy_mode_history'); + if (Array.isArray(modeHistory) && modeHistory.length > 0) { + const lastEntry = modeHistory[modeHistory.length - 1]; + const lastMode = lastEntry?.hwMode; + if (lastMode) { + this.log(`🔄 Restoring last known mode on startup: ${lastMode}`); + await this._applyRecommendation(lastMode, 100); + } + } + } catch (e) { + this.log('Could not restore last mode on startup:', e.message); + } + + await this._runPolicyCheck(); + + } catch (error) { + this.error('Failed to connect to P1 device:', error); + } + } + + /** + * Listen for capability changes on P1 device and mirror them in real-time + */ + _setupP1Listeners() { + if (!this.p1Device) return; + + if (this._p1PollInterval) { + this.homey.clearInterval(this._p1PollInterval); + } + + this._p1PollInterval = this.homey.setInterval(async () => { + if (!this.p1Device) return; + + try { + + // DEBUG: Log raw capability values from P1 +const rawSoc = this.p1Device.getCapabilityValue('battery_group_average_soc'); +const rawGrid = this.p1Device.getCapabilityValue('measure_power'); +const rawBattCap = this.p1Device.getCapabilityValue('measure_power.battery_group_power_w'); + +if (debug) this.log( + `🐛 [DEBUG/setup] Raw P1 caps → soc=${rawSoc}, grid=${rawGrid}, battCap=${rawBattCap}` +); + + + const soc = + this.p1Device.getCapabilityValue('battery_group_average_soc') ?? + 50; + + const gridPower = + this.p1Device.getCapabilityValue('measure_power') ?? 0; + + let batteryPower = + this.p1Device.getCapabilityValue('measure_power.battery_group_power_w'); + + if (batteryPower === null || batteryPower === undefined) { + // fallback op target_power_w (als je die ooit krijgt) + if (Date.now() - (this._lastBatteryEventTs ?? 0) < 10000) { + batteryPower = this._lastBatteryTargetW; + } else { + batteryPower = 0; + } + } + + if (debug) this.log(`🐛 batteryPower resolved → ${batteryPower}W`); + if (debug) this.log(`🐛 gridPower value → ${gridPower}W`); + + await this._updateBatteryCostModel({ + batteryPower, + gridPower, + pvState: this._pvState, + soc: soc + }); + + // Efficiency learning — use soc from P1 (battery_group_average_soc), not measure_battery + if (debug) this.log(`[Efficiency] About to update with grid=${gridPower}W, batt=${batteryPower}W, soc=${soc}`); + this.efficiencyEstimator.update( + { gridPower, batteryPower }, + { battery_power: batteryPower, stateOfCharge: soc }, + this.getCapabilityValue('active_mode') || null + ); + + // Add this logging every 5 minutes: + if (this.efficiencyEstimator.state) { + const s = this.efficiencyEstimator.state; + // Log progress every 5 min (every 20th call at 15s interval) + this._effLogCounter = (this._effLogCounter || 0) + 1; + if (this._effLogCounter % 20 === 0) { + const chargedWh = (s.totalChargeKwh * 1000).toFixed(0); + const dischargedWh = (s.totalDischargeKwh * 1000).toFixed(0); + const balance = s.totalDischargeKwh > 0 + ? (s.totalChargeKwh / s.totalDischargeKwh).toFixed(2) : '—'; + this.log( + `[RTE] learning: charged=${chargedWh}Wh / 1000Wh, discharged=${dischargedWh}Wh / 1000Wh, ` + + `balance=${balance} (>1.4 = wacht), current RTE=${( s.efficiency * 100).toFixed(1)}%` + ); + } + + // Update RTE from hardware meters every hour (240 × 15s) + if (this._effLogCounter % 240 === 0) { + try { + const battDriver = this.homey.drivers.getDriver('plugin_battery'); + if (battDriver) { + let totalImport = 0, totalExport = 0; + for (const dev of battDriver.getDevices()) { + totalImport += dev.getCapabilityValue('meter_power.import') || 0; + totalExport += dev.getCapabilityValue('meter_power.export') || 0; + } + const newRte = this.efficiencyEstimator.updateFromMeters(totalImport, totalExport); + if (newRte) this.policyEngine.updateSettings({ battery_efficiency: newRte }); + } + } catch (e) { /* driver not available */ } + } + + // Log RTE insights every 4h (every 960th call at 15s interval) + if (this._effLogCounter % 960 === 0) { + const insights = this.efficiencyEstimator.getEfficiencyInsights(); + if (insights) { + const pw = insights.rteByPower; + const m = insights.rteByMode; + this.log( + `[RTE] Insights (${insights.cycleCount} cycli) per modus: ` + + Object.entries(m).map(([k, v]) => `${k}=${v.rte}% (${v.n}x)`).join(', ') + ); + this.log(`[RTE] Advies: ${insights.recommendation}`); + } + } + } + + + const currentSoc = this.getCapabilityValue('battery_soc_mirror'); + const currentPower = this.getCapabilityValue('grid_power_mirror'); + + // Mirror SoC + if (currentSoc !== soc) { + await this.setCapabilityValue('battery_soc_mirror', soc); + this.log(`🔄 SoC updated: ${currentSoc}% → ${soc}%`); + } + + // Mirror grid power + if (currentPower !== gridPower) { + await this.setCapabilityValue('grid_power_mirror', gridPower); + } + + // ------------------------------------------------------ + // 📊 LEARNING: Record consumption patterns + // ------------------------------------------------------ + if (gridPower > 0) { + // Only record import (consumption), not export + await this.learningEngine.recordConsumption(gridPower).catch(err => + this.error('Learning consumption recording failed:', err) + ); + } + + // ------------------------------------------------------ + // ⭐ REALTIME PV STATE DETECTION (dual-mode) + // ------------------------------------------------------ + // Detects PV in two scenarios: + // 1. EXPORT MODE: Grid exporting surplus (gridPower < -200W) + // 2. CONSUMPTION MODE: Active PV being consumed (sun ≥40% AND daytime AND grid balanced) + // + // This handles the zero_charge_only case with daytime loads where grid ~0W + // but PV is actively producing and being consumed (washing machine, tumble dryer, etc.) + // + // CRITICAL: Account for battery charging when detecting PV state + // If battery is charging, that power would be exported if battery was in standby + const PV_DEBOUNCE_MS = 5 * 60 * 1000; // 5 minutes between PV-triggered runs + // ✅ HYSTERESIS THRESHOLDS: Different values for ON vs OFF to prevent bouncing + const PV_EXPORT_ON = -200; // Turn ON: Clear export < -200W + const PV_EXPORT_OFF = -150; // Turn OFF: Must rise above -150W to deactivate export mode + const PV_GRID_MIN_ON = -100; // Turn ON: Consumption mode starts at -100W + const PV_GRID_MAX_ON = 200; // Turn ON: Consumption mode ends at +200W + const PV_GRID_MIN_OFF = -150; // Turn OFF: Wider range to prevent bouncing (-150W) + const PV_GRID_MAX_OFF = 250; // Turn OFF: Wider range to prevent bouncing (+250W) + const PV_SUN_THRESHOLD = 40; // Sun score ≥40% indicates active PV + const PV_DAYLIGHT_START = 7; // 7 AM + const PV_DAYLIGHT_END = 18; // 6 PM + + // Get current sun score and time + const currentHour = new Date().getHours(); + const isDaylight = currentHour >= PV_DAYLIGHT_START && currentHour < PV_DAYLIGHT_END; + const sunScore = this.getCapabilityValue('sun_score') ?? 0; + const hasSunlight = sunScore >= PV_SUN_THRESHOLD; + + // Calculate "virtual export" = what grid would be if battery was in standby + // If battery is charging (+800W), that PV energy would export to grid instead + // So subtract charging power: grid=-1100W, batt=+800W → virtual=-1900W + // If battery is discharging (-800W), that's already reflected in grid reading + const virtualGridPower = batteryPower > 0 + ? gridPower - batteryPower // Charging: subtract to show true export potential + : gridPower; // Discharging/idle: grid reading is accurate + + // ✅ HYSTERESIS LOGIC: Use different thresholds based on current state + let hasExport, hasActivePVConsumption; + + if (this._pvState) { + // Currently ON: Use wider thresholds to stay ON (prevent false OFF) + hasExport = virtualGridPower < PV_EXPORT_OFF; + hasActivePVConsumption = isDaylight && hasSunlight && + virtualGridPower >= PV_GRID_MIN_OFF && virtualGridPower <= PV_GRID_MAX_OFF; + } else { + // Currently OFF: Use stricter thresholds to turn ON (prevent false ON) + hasExport = virtualGridPower < PV_EXPORT_ON; + hasActivePVConsumption = isDaylight && hasSunlight && + virtualGridPower >= PV_GRID_MIN_ON && virtualGridPower <= PV_GRID_MAX_ON; + } + + // PV is active if EITHER condition is true + const pvNowActive = hasExport || hasActivePVConsumption; + + if (!this._pvState && pvNowActive) { + // PV state OFF → ON + this._pvState = true; + const now = Date.now(); + const reason = hasExport + ? `export (virtual=${virtualGridPower.toFixed(1)}W [grid=${gridPower}W - batt=${batteryPower}W] < ${PV_EXPORT_ON}W)` + : `consumption (sun=${sunScore}%, virtual=${virtualGridPower.toFixed(1)}W, daytime=${isDaylight})`; + + if (!this._lastPvPolicyRun || now - this._lastPvPolicyRun > PV_DEBOUNCE_MS) { + this._lastPvPolicyRun = now; + this.log(`⚡ PV state changed (OFF → ON) via ${reason} → running policy`); + this._runPolicyCheck().catch(err => this.error(err)); + } else { + this.log(`⚡ PV state changed (OFF → ON) via ${reason} → debounced (last run ${Math.round((now - this._lastPvPolicyRun) / 1000)}s ago)`); + } + } else if (this._pvState && !pvNowActive) { + // PV state ON → OFF + this._pvState = false; + const now = Date.now(); + const reason = !hasSunlight + ? `sun gone (${sunScore}% < ${PV_SUN_THRESHOLD}%)` + : !isDaylight + ? `night (${currentHour}:00, outside ${PV_DAYLIGHT_START}–${PV_DAYLIGHT_END})` + : `grid unbalanced (virtual=${virtualGridPower.toFixed(1)}W [grid=${gridPower}W - batt=${batteryPower}W], outside ${PV_GRID_MIN_OFF}–${PV_GRID_MAX_OFF}W)`; + + if (!this._lastPvPolicyRun || now - this._lastPvPolicyRun > PV_DEBOUNCE_MS) { + this._lastPvPolicyRun = now; + this.log(`⚡ PV state changed (ON → OFF) via ${reason} → running policy`); + this._runPolicyCheck().catch(err => this.error(err)); + } else { + this.log(`⚡ PV state changed (ON → OFF) via ${reason} → debounced (last run ${Math.round((now - this._lastPvPolicyRun) / 1000)}s ago)`); + } + } + // Otherwise: no state change, no spam + + } catch (err) { + this.error('Error polling P1 capabilities:', err); + } + // ✅ CPU FIX: Increased from 5s to 15s - heavy work (capability reads/writes, calculations) + }, 15000); + + this.log('✅ P1 capability polling started (15s interval)'); + } + + _schedulePolicyCheck() { + const intervalMinutes = this.getSetting('policy_interval') || 15; + const intervalMs = intervalMinutes * 60 * 1000; + + // Clear any existing timers + if (this.policyCheckInterval) { + this.homey.clearInterval(this.policyCheckInterval); + } + if (this._hourBoundaryTimeout) { + this.homey.clearTimeout(this._hourBoundaryTimeout); + } + + // 1) Regular interval as fallback between hour boundaries + this.policyCheckInterval = this.homey.setInterval( + async () => { + if (this.getCapabilityValue('policy_enabled')) { + await this._runPolicyCheck(); + } + }, + intervalMs + ); + + // 2) Align to hour boundaries — prices change on the hour. + // Schedule a run ~5s after each full hour to catch new prices immediately. + this._scheduleHourBoundary(); + + this.log(`Policy check scheduled every ${intervalMinutes} minutes + aligned to hour boundaries`); + } + + _scheduleHourBoundary() { + const now = Date.now(); + const nextHour = new Date(now); + nextHour.setMinutes(0, 5, 0); // 5 seconds past the hour + nextHour.setHours(nextHour.getHours() + 1); + const msUntilNextHour = nextHour.getTime() - now; + + this._hourBoundaryTimeout = this.homey.setTimeout(async () => { + if (this.getCapabilityValue('policy_enabled')) { + this.log(`⏰ Hour boundary reached (${new Date().getHours()}:00) → running policy check`); + await this._runPolicyCheck().catch(err => this.error('Hour-boundary policy check failed:', err)); + } + // Schedule the next hour boundary + this._scheduleHourBoundary(); + }, msUntilNextHour); + } + + _schedulePriceRefresh() { + // Adaptive interval: 15 min during price-release window (14:00–16:00 CET), + // 30 min otherwise. kwhprice.eu publishes tomorrow's prices at ~13:15 CET. + const getRefreshInterval = () => { + const hour = new Date().getHours(); + return (hour >= 14 && hour <= 16) ? 15 * 60 * 1000 : 30 * 60 * 1000; + }; + + const scheduleNext = () => { + if (this.priceRefreshTimeout) { + this.homey.clearTimeout(this.priceRefreshTimeout); + } + + this.priceRefreshTimeout = this.homey.setTimeout( + async () => { + const settings = this.getSettings(); + + if (settings.enable_dynamic_pricing && this.tariffManager.dynamicProvider) { + const now = new Date(); + this.log(`🔄 Refreshing prices... (${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')})`); + + try { + // Force-refresh the merged provider (fetches Xadi + KwhPrice concurrently) + await this.tariffManager.mergedProvider.fetchPrices(true); + const priceCount = this.tariffManager.mergedProvider.cache?.length || 0; + const sources = this.tariffManager.mergedProvider.lastFetchSources.join('+'); + const days = priceCount > 24 ? 'today + tomorrow' : 'today only'; + this.log(`✅ Prices refreshed: ${priceCount}h (${days}, sources: ${sources})`); + + if (priceCount > 0 && this.getCapabilityValue('policy_enabled')) { + // Always recompute optimizer after price refresh — new data may include + // tomorrow's prices (96→192 slots) that change the optimal schedule. + this.optimizationEngine.updateSettings({}); + await this._runPolicyCheck(); + } + } catch (err) { + this.error('❌ Price refresh failed:', err); + } + } + + scheduleNext(); + }, + getRefreshInterval() + ); + }; + + scheduleNext(); + this.log(`Price refresh scheduled (adaptive: ${getRefreshInterval() / 60000}min, frequent 14:00–16:00)`); + } + + async _updateWeather() { + try { + const settings = this.getSettings(); + + if (settings.tariff_type !== 'dynamic') { + this.weatherData = null; + + await this.setCapabilityValue('sun_score', 0).catch(this.error); + await this.setCapabilityValue('predicted_sun_hours', 0).catch(this.error); + + this.log('Weather skipped (fixed tariff)'); + return; + } + + const loc = this._getLocationFromSetting(); + if (!loc) return; + + const { latitude, longitude } = loc; + + const devSettings = this.getSettings(); + const pvTilt = devSettings.pv_estimation_enabled && typeof devSettings.pv_tilt === 'number' ? devSettings.pv_tilt : null; + const pvAzimuth = devSettings.pv_estimation_enabled && typeof devSettings.pv_azimuth === 'number' ? devSettings.pv_azimuth : null; + + this.weatherData = await this.weatherForecaster.fetchForecast(latitude, longitude, pvTilt, pvAzimuth); + + // Bereken verwachte PV-productie vandaag (kWh) op basis van straling + piekvermogen + const pvCapW = devSettings.pv_capacity_w || 0; + const PR = devSettings.pv_performance_ratio || 0.75; + if (Array.isArray(this.weatherData.dailyProfiles)) { + const todayDate = new Date().toISOString().slice(0, 10); + const todayProfiles = this.weatherData.dailyProfiles.filter(h => h.time.toISOString().startsWith(todayDate)); + const yfs = this.learningEngine?.getSolarYieldFactorsSmoothed(); + const learnedSlots = this.learningEngine?.getSolarLearnedSlotCount() ?? 0; + + const now = new Date(); + const futureProfiles = todayProfiles.filter(h => h.time > now); + + let todayKwh, remainingKwh; + if (learnedSlots >= 10) { + // Learned model: sum(radiation × yieldFactor) / 1000 — no pvCapW or PR needed + todayKwh = todayProfiles.reduce((sum, h) => { + const slotIndex = h.time.getUTCHours() * 4; + const yf = yfs[slotIndex] ?? 0; + return sum + h.radiationWm2 * yf; + }, 0) / 1000; + remainingKwh = futureProfiles.reduce((sum, h) => { + const slotIndex = h.time.getUTCHours() * 4; + const yf = yfs[slotIndex] ?? 0; + return sum + h.radiationWm2 * yf; + }, 0) / 1000; + this.log(`☀️ PV forecast (learned, ${learnedSlots} slots): ${todayKwh.toFixed(1)} kWh today, ${remainingKwh.toFixed(1)} kWh remaining`); + } else if (pvCapW > 0) { + // Fallback: configured capacity × performance ratio + todayKwh = todayProfiles.reduce((sum, h) => sum + pvCapW * PR * (h.radiationWm2 / 1000), 0) / 1000; + remainingKwh = futureProfiles.reduce((sum, h) => sum + pvCapW * PR * (h.radiationWm2 / 1000), 0) / 1000; + this.log(`☀️ PV forecast (fallback PR=${PR}, ${learnedSlots} slots learned): ${todayKwh.toFixed(1)} kWh today, ${remainingKwh.toFixed(1)} kWh remaining`); + } else { + todayKwh = null; + remainingKwh = null; + } + this.weatherData.pvKwhToday = todayKwh !== null ? Math.round(todayKwh * 10) / 10 : null; + this.weatherData.pvKwhRemaining = remainingKwh !== null ? Math.round(remainingKwh * 10) / 10 : null; + } else { + this.weatherData.pvKwhToday = null; + } + + const sunScore = this.weatherForecaster.calculateSunScore(this.weatherData); + + await this.setCapabilityValue('sun_score', sunScore); + await this.setCapabilityValue( + 'predicted_sun_hours', + parseFloat(this.weatherData.sunshineNext4Hours.toFixed(1)) + ); + + this.log('Weather updated:', { + sun4h: this.weatherData.sunshineNext4Hours, + sunScore + }); + + // Invalidate optimizer — new PV forecast may change the optimal charge schedule. + this.optimizationEngine.updateSettings({}); + + } catch (error) { + this.error('Weather update failed:', error); + } + } + + async _migrateWeatherLocation() { + const settings = this.getSettings(); + if (settings.weather_latitude && settings.weather_latitude !== 0) return; // already migrated + + const oldLoc = settings.weather_location; + if (!oldLoc || oldLoc.trim() === '') return; // nothing to migrate + + try { + let lat, lon; + + if (oldLoc.includes(',')) { + const [a, b] = oldLoc.split(',').map(v => parseFloat(v.trim())); + if (!isNaN(a) && !isNaN(b)) { lat = a; lon = b; } + } else { + const geo = await this.weatherForecaster.lookupCity(oldLoc.trim()); + if (geo) { lat = geo.latitude; lon = geo.longitude; } + } + + if (lat != null && lon != null) { + await this.setSettings({ weather_latitude: lat, weather_longitude: lon }); + this.log(`Migrated weather_location "${oldLoc}" → lat=${lat}, lon=${lon}`); + } else { + this.error(`Could not migrate weather_location "${oldLoc}" — user must re-enter coordinates`); + } + } catch (err) { + this.error('Weather location migration failed:', err); + } + } + + _getLocationFromSetting() { + const settings = this.getSettings(); + + if (settings.tariff_type !== 'dynamic') { + return null; + } + + const lat = settings.weather_latitude; + const lon = settings.weather_longitude; + + if (!lat || !lon || lat === 0 || lon === 0) { + this.error('Weather location not set (dynamic mode)'); + return null; + } + + return { latitude: lat, longitude: lon }; + } + + async _runPolicyCheck() { + try { + if (!this.getCapabilityValue('policy_enabled')) { + this.log('Policy disabled, skipping check'); + return; + } + + const overrideUntil = this.getStoreValue('override_until'); + if (overrideUntil && new Date(overrideUntil) > new Date()) { + this.log('Manual override active, skipping policy check'); + return; + } + + const inputs = await this._gatherInputs(); + if (!inputs.battery || inputs.battery.stateOfCharge === undefined) { + this.log('Skipping policy check — battery state not ready'); + return; + } + + // Recompute optimizer schedule if stale (lazy, every ~90 min or after price update) + if (this.optimizationEngine.isStale() && inputs.tariff) { + this._recomputeOptimizer(inputs); + } + inputs.optimizer = this.optimizationEngine; + + const result = this.policyEngine.calculatePolicy(inputs); + + // ------------------------------------------------------ + // 📊 LEARNING: Apply confidence adjustment based on history + // ------------------------------------------------------ + const confidenceAdjustment = this.learningEngine.getConfidenceAdjustment( + result.hwMode || result.policyMode, + { + soc: inputs.battery?.stateOfCharge ?? 0, + sun4h: inputs.weather?.sun4h ?? 0 + } + ); + + if (confidenceAdjustment !== 0) { + const originalConfidence = result.confidence; + result.confidence = Math.max(0, Math.min(100, result.confidence + confidenceAdjustment)); + this.log(`📊 Learning adjusted confidence: ${originalConfidence} → ${result.confidence} (${confidenceAdjustment > 0 ? '+' : ''}${confidenceAdjustment})`); + } + + if (!this.explainabilityEngine) { + this.explainabilityEngine = new (require('../../lib/explainability-engine'))(this.homey); + _memMB('after-lazy-load-explainability'); + } + const explanation = this.explainabilityEngine.generateExplanation( + result, + inputs, + result.scores + ); + + this.homey.api.realtime('explainability_update', explanation); + + this.homey.settings.set('policy_explainability', explanation); + + this.log('Saving explainability length:', JSON.stringify(explanation).length); + + const recommended = result.hwMode || result.policyMode || 'standby'; + + // Push planning data to app settings for the settings page + const batterySOC = inputs.battery?.stateOfCharge ?? 50; + const policyMode = this.getCapabilityValue('policy_mode') || 'balanced'; + const planningData = { + batterySOC, + policyMode, + recommendedMode: recommended, + currentMode: recommended, // will be overwritten below with actual HW mode + maxDischargePowerW: inputs.battery?.maxDischargePowerW || 800, + maxChargePowerW: inputs.battery?.maxChargePowerW || 800, + totalCapacityKwh: inputs.battery?.totalCapacityKwh || null, + batteryCount: Math.max(1, Math.round((inputs.battery?.totalCapacityKwh ?? 2.688) / 2.688)), + pvLearnedSlots: this.learningEngine?.getSolarLearnedSlotCount() ?? 0, + lastUpdate: new Date().toISOString() + }; + this.homey.settings.set('battery_policy_state', planningData); + + // Push device settings to app settings so planning page can read them + // (device settings are not accessible via Homey.get() in the settings page) + this.homey.settings.set('device_settings', { + max_charge_price: this.getSetting('max_charge_price') || 0.19, + min_discharge_price: this.getSetting('min_discharge_price') || 0.22, + min_soc: this.getSetting('min_soc') || 10, + max_soc: this.getSetting('max_soc') || 95, + battery_efficiency: this.getSetting('battery_efficiency') || 0.78, + min_profit_margin: this.getSetting('min_profit_margin') || 0.01, + tariff_type: this.getSetting('tariff_type') || 'dynamic', + policy_interval: this.getSetting('policy_interval') || 15, + pv_capacity_w: this.getSetting('pv_capacity_w') || 0, + pv_estimation_enabled: this.getSetting('pv_estimation_enabled') || false, + pv_performance_ratio: this.getSetting('pv_performance_ratio') || 0.75, + }); + // debug_top3 writes moved to _gatherInputs (single write) + + // Update battery RTE display + // Use learned efficiency with safety bounds (learned can be from old data) + let currentRte = this.efficiencyEstimator.getEfficiency(); + + // Safety: Cap at realistic range for LFP batteries (AC-AC typically 70-97%) + // If learned value is unrealistic, fall back to configured value + const configuredRte = this.getSetting('battery_efficiency') || 0.78; + if (currentRte < 0.50 || currentRte > 0.97) { + this.log(`⚠️ Learned RTE ${(currentRte * 100).toFixed(1)}% outside realistic range for LFP, using configured ${(configuredRte * 100).toFixed(1)}%`); + currentRte = configuredRte; + // Reset the estimator to configured value + this.efficiencyEstimator.reset(configuredRte); + } + + await this.setCapabilityValue('battery_rte', parseFloat((currentRte * 100).toFixed(1))).catch(this.error); + this.homey.settings.set('battery_efficiency_effective', currentRte); + + await this.setCapabilityValue('recommended_mode', recommended); + + await this.setCapabilityValue('confidence_score', result.confidence); + // explanation_summary shows the ACTIVE mode, not the recommended mode + const currentActiveMode = this.getCapabilityValue('active_mode') || recommended; + const activeSummary = this.explainabilityEngine._generateShortSummary({ hwMode: currentActiveMode }, inputs); + await this.setCapabilityValue('explanation_summary', activeSummary); + await this.setCapabilityValue('last_update', new Date().toISOString()); + + const previousMode = this.lastRecommendation?.hwMode || this.lastRecommendation?.policyMode; + const currentMode = result.hwMode || result.policyMode; + const modeChanged = previousMode !== currentMode; + + if (modeChanged && this.getSetting('enable_policy_notifications')) { + try { + await this.homey.notifications.createNotification({ + excerpt: explanation.summary + }); + } catch (err) { + this.error('Failed to send policy notification:', err); + } + } + + this.lastRecommendation = result; + + this.log('Policy check complete:', { + mode: currentMode, + confidence: result.confidence, + summary: explanation.summary + }); + + // ------------------------------------------------------ + // 📊 LEARNING: Record policy decision + // ------------------------------------------------------ + await this.learningEngine.recordPolicyDecision(currentMode, { + soc: inputs.battery?.stateOfCharge ?? 0, + price: inputs.tariff?.currentPrice ?? 0, + sun4h: inputs.weather?.sun4h ?? 0, + confidence: result.confidence + }).catch(err => this.error('Learning policy recording failed:', err)); + + // Chart generation disabled — skip to save memory + + await this._triggerRecommendationChanged(result, explanation); + + const autoApplyEnabled = this.getCapabilityValue('auto_apply'); + this.log(`Auto-apply status: ${autoApplyEnabled ? 'ENABLED' : 'DISABLED'}`); + + if (autoApplyEnabled) { + const applyMode = result.hwMode || result.policyMode; + this.log(`📋 Policy recommendation: ${result.policyMode} → HW mode: ${applyMode}`); + this.log(`📊 Scores: charge=${result.scores?.charge}, discharge=${result.scores?.discharge}, preserve=${result.scores?.preserve}`); + this.log(`🎯 Attempting to apply: ${applyMode} (confidence: ${result.confidence}%)`); + + const minConfidence = this.getSetting('min_confidence_threshold') ?? 60; + const applied = await this._applyRecommendation(applyMode, result.confidence); + + if (applied) { + this.log(`✅ Successfully applied: ${applyMode}`); + + // Append to mode history for planning UI + try { + const modeHistory = this.homey.settings.get('policy_mode_history') || []; + const currentPrice = result.tariff?.currentPrice ?? null; + const currentSoc = this.getCapabilityValue('measure_battery') ?? null; + modeHistory.push({ + ts: new Date().toISOString(), + hwMode: applyMode, + price: currentPrice, + soc: currentSoc, + maxChargePrice: this.getSetting('max_charge_price'), + minDischargePrice: this.getSetting('min_discharge_price') + }); + // Keep last 96 entries (24h at 15min intervals) + if (modeHistory.length > 96) modeHistory.splice(0, modeHistory.length - 96); + this.homey.settings.set('policy_mode_history', modeHistory); + } catch (e) { + this.error('Failed to save mode history:', e); + } + } else { + if (result.confidence < minConfidence) { + this.log(`⏸️ Not applied: confidence ${result.confidence.toFixed(1)}% below threshold ${minConfidence}%`); + } else { + this.log(`⚠️ Failed to apply recommendation — check P1 connection`); + } + } + } else { + this.log('Auto-apply disabled — recommendation not applied'); + } + + // Update active_mode to reflect the hardware's current actual mode + if (this.p1Device) { + const actualHwMode = this.p1Device.getCapabilityValue('battery_group_charge_mode'); + if (actualHwMode) { + await this.setCapabilityValue('active_mode', actualHwMode).catch(this.error); + // Patch currentMode in already-saved planningData (avoid 2nd settings.set) + planningData.currentMode = actualHwMode; + this.homey.settings.set('battery_policy_state', planningData); + + // Always sync explanation_summary to the actual hardware mode + const hwActiveSummary = this.explainabilityEngine._generateShortSummary( + { hwMode: actualHwMode }, + inputs + ); + await this.setCapabilityValue('explanation_summary', hwActiveSummary).catch(this.error); + } + } + + } catch (error) { + this.error('Policy check failed:', error); + await this.setCapabilityValue('explanation_summary', + `Error: ${error.message}` + ); + } + } + + /** + * (Re)compute the OptimizationEngine schedule from the current inputs. + * Called lazily in _runPolicyCheck whenever the schedule is stale. + */ + _recomputeOptimizer(inputs) { + // Prefer 15-min prices for finer-grained optimization; fall back to hourly + const now = new Date(); + const raw15min = inputs.tariff?.allPrices15min; + const rawPrices = (raw15min?.length > 0) + ? raw15min.filter(p => new Date(p.timestamp) >= now) + : (inputs.tariff?.allPrices || inputs.tariff?.next24Hours); + const prices = rawPrices; + if (!prices || prices.length === 0) return; + + // Slot duration in ms (15 min = 900_000, 1 hour = 3_600_000) + const slotMs = (prices.length >= 2) + ? (new Date(prices[1].timestamp) - new Date(prices[0].timestamp)) + : 3_600_000; + + const soc = inputs.battery?.stateOfCharge ?? 50; + const capacityKwh = inputs.battery?.totalCapacityKwh; + if (!capacityKwh || capacityKwh <= 0) return; + + const maxChargePowerW = inputs.battery?.maxChargePowerW || 800; + const maxDischargePowerW = inputs.battery?.maxDischargePowerW || 800; + + // Build per-slot PV power estimate from radiation forecast. + // Prefer the learned per-slot yield factors (W per W/m²) — no pvCapW or PR needed. + // Falls back to configured capacity × PR when insufficient data (<10 learned slots). + let pvForecast = null; + const pvCapacityW = inputs.settings?.pv_capacity_w || 0; + const pvPR = inputs.settings?.pv_performance_ratio || 0.75; + const yfs = this.learningEngine?.getSolarYieldFactorsSmoothed(); + const learnedSlots = this.learningEngine?.getSolarLearnedSlotCount() ?? 0; + + if (Array.isArray(inputs.weather?.hourlyForecast)) { + + pvForecast = inputs.weather.hourlyForecast + .filter(h => typeof h.radiationWm2 === 'number') + .map(h => { + const d = h.time instanceof Date ? h.time : new Date(h.time); + // Yield factor slot index: hour*4 (hourly data always on the hour, minutes=0) + const slotIdx = d.getUTCHours() * 4; + const rawPvW = learnedSlots >= 10 + ? Math.round(h.radiationWm2 * (yfs[slotIdx] ?? 0)) + : pvCapacityW > 0 ? Math.round(pvCapacityW * pvPR * (h.radiationWm2 / 1000)) : 0; + // Cap at installed system capacity — learned yield factors can overshoot on + // exceptional days, but the inverter/system can never exceed its rated peak. + const pvW = pvCapacityW > 0 ? Math.min(rawPvW, pvCapacityW) : rawPvW; + return { timestamp: d.toISOString(), pvPowerW: pvW }; + }) + .filter(h => h.pvPowerW > 0 || pvCapacityW > 0); + } + + // Learned round-trip efficiency from efficiencyEstimator + let learnedRte = this.efficiencyEstimator?.getEfficiency() ?? null; + if (learnedRte != null && (learnedRte < 0.50 || learnedRte > 0.97)) learnedRte = null; + + // 24h consumption forecast from learning engine, floored by baseload when available. + // BaseloadMonitor is optional (requires P1 baseload feature to be active). + const baseloadW = this.homey.app?.baseloadMonitor?.currentBaseload ?? 0; + let consumptionWPerSlot = null; + if (this.learningEngine) { + const now = new Date(); + consumptionWPerSlot = []; + for (let h = 0; h < prices.length; h++) { + // Use the actual price slot timestamp so consumption aligns with the right hour, + // not now + h * slotMs which drifts when prices don't start exactly at 'now'. + const futureTime = new Date(prices[h].timestamp); + const learned = this.learningEngine.getPredictedConsumption(futureTime) ?? 0; + // Use baseload only when learning has no data yet for this slot (learned = 0). + // Learned values already include baseload (recorded from grid import). + consumptionWPerSlot.push(learned > 0 ? learned : baseloadW); + } + } + + const slotLabel = slotMs === 900_000 ? '15-min' : '1h'; + this.log(`🔮 Optimizer: recomputing schedule (${prices.length} × ${slotLabel} slots, SoC ${soc}%, ${capacityKwh}kWh, PV ${pvCapacityW}W peak, RTE ${learnedRte != null ? (learnedRte * 100).toFixed(0) + '%' : 'default'})`); + const respectMinMax = (inputs.settings?.policy_mode === 'balanced-dynamic') + ? false + : inputs.settings?.respect_minmax !== false; + const minDischargePrice = respectMinMax + ? (inputs.settings?.min_discharge_price ?? 0) + : (inputs.settings?.opportunistic_discharge_floor ?? 0.20); + this.optimizationEngine.compute(prices, soc, capacityKwh, maxChargePowerW, maxDischargePowerW, pvForecast, learnedRte, consumptionWPerSlot, minDischargePrice); + + // Persist planning schedule for the settings UI (single source of truth). + // Frontend reads 'policy_optimizer_schedule' and renders it directly — no re-simulation. + const slots = this.optimizationEngine._schedule?.slots; + if (slots?.length > 0) { + const planningSchedule = this.policyEngine.buildPlanningSchedule(slots, pvForecast ?? null); + this.homey.settings.set('policy_optimizer_schedule', planningSchedule); + } + + // Persist hourly PV forecast so the settings chart uses the same values as the optimizer. + // Keyed by Amsterdam local hour (0-23) for the next 48h (today + tomorrow). + if (Array.isArray(pvForecast) && pvForecast.length > 0) { + const now = new Date(); + const nowAmsDate = now.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const nowAmsHour = parseInt(now.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + const pvForecastByDay = [{}, {}]; + + // Future hours: one pvForecast entry per hour — map directly by Amsterdam local hour. + for (const { timestamp, pvPowerW } of pvForecast) { + const t = new Date(timestamp); + const tDate = t.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const tHour = parseInt(t.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + const dayIdx = tDate === nowAmsDate ? 0 : 1; + if (dayIdx === 0 || tDate > nowAmsDate) { + pvForecastByDay[dayIdx][tHour] = pvPowerW; + } + } + + // Past hours today: apply the same learned yield factors to dailyProfiles radiation so + // the chart line is consistent across the full day. Without this, past hours fall back to + // pvCapW × PR × radiation/1000 which underestimates partial-sunrise slots (e.g. the first + // morning slot after DST where the sun rose mid-hour but learned data knows the real yield). + if (learnedSlots >= 10 && yfs && Array.isArray(inputs.weather?.dailyProfiles)) { + for (const h of inputs.weather.dailyProfiles) { + const d = h.time instanceof Date ? h.time : new Date(h.time); + const hDate = d.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + if (hDate !== nowAmsDate) continue; + const hHour = parseInt(d.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + if (hHour >= nowAmsHour) continue; // only truly past hours + if (pvForecastByDay[0][hHour] != null) continue; // don't overwrite future slots + const rawPvW = Math.round(h.radiationWm2 * (yfs[d.getUTCHours() * 4] ?? 0)); + pvForecastByDay[0][hHour] = pvCapacityW > 0 ? Math.min(rawPvW, pvCapacityW) : rawPvW; + } + } + + this.homey.settings.set('policy_pv_forecast_hourly', pvForecastByDay); + } + } + + /** + * Update PV production from flow card (user-provided data) + * @param {number} powerW - PV production in watts + */ + _updatePvProduction(powerW) { + this._pvProductionW = powerW; + this._pvProductionTimestamp = Date.now(); + + // Feed live measurement into the solar yield-factor learner. + // Requires radiation data from the latest weather fetch. + const radiation = this._getInterpolatedRadiation(Date.now()); + if (radiation !== null && this.learningEngine) { + this.learningEngine.updateSolarYieldFactor(new Date(), powerW, radiation); + } + + // Accumulate actual PV per Amsterdam hour for planning chart display. + const nowAms = new Date(); + const todayStr = nowAms.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const amsHour = parseInt(nowAms.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + + if (!this._pvActualHourly || this._pvActualHourly.date !== todayStr) { + const saved = this.homey.settings.get('policy_pv_actual_today'); + if (saved && saved.date === todayStr && Array.isArray(saved.hourly)) { + this._pvActualHourly = { + date: todayStr, + hourly: saved.hourly, + sums: saved.sums || new Array(24).fill(0), + counts: saved.counts || new Array(24).fill(0), + }; + } else { + this._pvActualHourly = { + date: todayStr, + hourly: new Array(24).fill(null), + sums: new Array(24).fill(0), + counts: new Array(24).fill(0), + }; + } + } + + this._pvActualHourly.sums[amsHour] += powerW; + this._pvActualHourly.counts[amsHour] += 1; + this._pvActualHourly.hourly[amsHour] = Math.round( + this._pvActualHourly.sums[amsHour] / this._pvActualHourly.counts[amsHour] + ); + + this.homey.settings.set('policy_pv_actual_today', { + date: this._pvActualHourly.date, + hourly: this._pvActualHourly.hourly, + sums: this._pvActualHourly.sums, + counts: this._pvActualHourly.counts, + }); + } + + /** + * Interpolate radiation (W/m²) from hourly weather forecast at a given moment. + * Returns null when no weather data is available. + */ + _getInterpolatedRadiation(nowMs) { + const forecast = this.weatherData?.hourlyForecast; + if (!Array.isArray(forecast) || forecast.length === 0) return null; + + let prev = null, next = null; + for (const h of forecast) { + const t = h.time instanceof Date ? h.time.getTime() : new Date(h.time).getTime(); + if (t <= nowMs) prev = { t, r: h.radiationWm2 }; + else if (!next) { next = { t, r: h.radiationWm2 }; break; } + } + if (!prev && !next) return null; + if (!prev) return next.r; + if (!next) return prev.r; + + const ratio = (nowMs - prev.t) / (next.t - prev.t); + return prev.r + (next.r - prev.r) * ratio; + } + + /** + * Estimate PV production using grid power analysis + sun model + * @param {Object} ctx - Context with gridPower, batteryPower, sunScore + * @returns {number} Estimated PV production in watts + */ + _estimatePvProduction(ctx) { + const settings = this.getSettings(); + + // Priority 1: User-provided data via flow card (most accurate) + if (this._pvProductionW !== null && this._pvProductionTimestamp) { + const age = Date.now() - this._pvProductionTimestamp; + const maxAge = 5 * 60 * 1000; // 5 minutes + + if (age < maxAge) { + if (settings.enable_logging) { + this.log(`PV from flow card: ${this._pvProductionW}W (age: ${Math.round(age/1000)}s)`); + } + return this._pvProductionW; + } else { + // Data too old, clear it + this._pvProductionW = null; + this._pvProductionTimestamp = null; + } + } + + // Priority 2: Estimation (fallback when no flow data) + // Feature disabled or no capacity configured + if (!settings.pv_estimation_enabled || !settings.pv_capacity_w || settings.pv_capacity_w <= 0) { + return 0; + } + + const grid = ctx.gridPower ?? 0; // positive = import, negative = export + const batt = ctx.batteryPower ?? 0; // positive = charging, negative = discharging + const sunScore = ctx.sunScore ?? 0; // 0..100 + const pvCap = settings.pv_capacity_w; + const alpha = 0.4; // EMA smoothing factor + + // Sun-based model: scale capacity by sun intensity + const sunFactor = Math.max(0, Math.min(1, sunScore / 100)); + const pvModel = Math.round(pvCap * sunFactor); + + let pvFromGrid = 0; + const exportThreshold = -75; // Grid exporting when below this + + if (grid < exportThreshold) { + // Grid is exporting: PV must be producing more than household consumption + // Household load = export + any battery discharge + const exportPower = Math.abs(grid); + const batteryDischarge = batt < 0 ? Math.abs(batt) : 0; + pvFromGrid = exportPower + batteryDischarge; + + // When exporting, PV must also be covering any battery charge + const batteryCharge = batt > 0 ? batt : 0; + pvFromGrid += batteryCharge; + } else if (grid > 0 && batt > 100 && sunScore > 0) { + // Grid importing + battery charging: PV might be contributing + // This is conservative: only count if battery is actively charging + // Real PV = battery charge power (assuming zero-charge-only mode) + pvFromGrid = batt; + } + + // Use the stronger signal (measured export trumps model) + const rawEstimate = Math.max(pvModel, pvFromGrid); + + // EMA smoothing to avoid oscillation from clouds + const estimate = this._lastPvEstimateW + ? Math.round((alpha * rawEstimate) + ((1 - alpha) * this._lastPvEstimateW)) + : rawEstimate; + + this._lastPvEstimateW = estimate; + + // ------------------------------------------------------ + // 📊 LEARNING: Apply learned PV accuracy adjustment + // ------------------------------------------------------ + const learningMultiplier = this.learningEngine.getPvAdjustmentMultiplier(); + const adjustedEstimate = Math.round(estimate * learningMultiplier); + + if (settings.enable_logging && adjustedEstimate > 0) { + this.log(`PV estimate: ${adjustedEstimate}W (raw: ${estimate}W, model: ${pvModel}W, fromGrid: ${pvFromGrid}W, sun: ${sunScore}%, learning: ${learningMultiplier.toFixed(2)}x)`); + } + + return Math.max(0, adjustedEstimate); + } + + async _gatherInputs() { + const settings = { ...this.getSettings() }; + + // Override battery_efficiency with learned meter-based RTE when available, + // so policy engine, explainability, and settings page all use the same value. + const learnedRte = this.efficiencyEstimator?.getEfficiency() ?? null; + if (learnedRte && learnedRte > 0.50 && learnedRte < 0.99) { + settings.battery_efficiency = learnedRte; + } + + let weatherData = null; + + if (settings.tariff_type === 'dynamic') { + if ( + !this.weatherData || + !this.weatherData.fetchedAt || + Date.now() - this.weatherData.fetchedAt > ((settings.weather_update_interval || 1) * 60 * 60 * 1000) + ) { + await this._updateWeather(); + } + + weatherData = this.weatherData || this.weatherForecaster._getDefaultForecast(); + + const weatherOverride = this.getCapabilityValue('weather_override'); + if (weatherOverride !== 'auto') { + this.log(`🌦️ Applying weather override: ${weatherOverride}`); + weatherData = this._applyWeatherOverride(weatherData, weatherOverride); + } + } + + const batteryState = await this._getBatteryState(); + const tariffInfo = this.tariffManager.getCurrentTariff(batteryState.gridPower); + + const debugPrice = tariffInfo?.currentPrice ?? 'n/a'; + const debugTopLow = Array.isArray(tariffInfo?.top3Lowest) + ? tariffInfo.top3Lowest.map(p => `${String(p.hour).padStart(2, '0')}:00€${p.price.toFixed(2)}`).join(', ') + : 'n/a'; + const debugTopHigh = Array.isArray(tariffInfo?.top3Highest) + ? tariffInfo.top3Highest.map(p => `${String(p.hour).padStart(2, '0')}:00€${p.price.toFixed(2)}`).join(', ') + : 'n/a'; + const debugSun4h = Number(weatherData?.sunshineNext4Hours ?? 0).toFixed(1); + const debugSun8h = Number(weatherData?.sunshineNext8Hours ?? 0).toFixed(1); + const debugSunToday = Number(weatherData?.sunshineTodayRemaining ?? 0).toFixed(1); + const debugSunTomorrow = Number(weatherData?.sunshineTomorrow ?? 0).toFixed(1); + + const debugRate = tariffInfo?.currentRate ?? 'n/a'; + const now = new Date().toISOString().slice(11, 16); // HH:MM format + const debugPriceText = `price=${debugPrice} rate=${debugRate} @${now}`; + const debugTopLowText = `low=[${debugTopLow}] @${now}`; + const debugTopHighText = `high=[${debugTopHigh}] @${now}`; + const debugSunText = `4h=${debugSun4h} 8h=${debugSun8h} today=${debugSunToday} tmw=${debugSunTomorrow} @${now}`; + + // Learning statistics + const learningStats = this.learningEngine.getStatistics(); + const rteInsights = this.efficiencyEstimator.getEfficiencyInsights(); + const rteModeSummary = rteInsights + ? Object.entries(rteInsights.rteByMode).map(([k, v]) => `${k}=${v.rte}%(${v.n}x)`).join(' ') + : `rte=${(this.efficiencyEstimator.getEfficiency() * 100).toFixed(1)}% (<5 cycli)`; + const debugLearningText = `days=${learningStats.days_tracking} samples=${learningStats.total_samples} coverage=${learningStats.pattern_coverage}% pv_acc=${learningStats.pv_accuracy}% | rte: ${rteModeSummary} @${now}`; + + await this.setCapabilityValue('policy_debug_price', debugPriceText).catch(this.error); + await this.setCapabilityValue('policy_debug_top3low', debugTopLowText).catch(this.error); + await this.setCapabilityValue('policy_debug_top3high', debugTopHighText).catch(this.error); + await this.setCapabilityValue('policy_debug_sun', debugSunText).catch(this.error); + await this.setCapabilityValue('policy_debug_learning', debugLearningText).catch(this.error); + + // Push debug data to app settings for planning view + this.homey.settings.set('policy_debug_top3low', debugTopLowText); + this.homey.settings.set('policy_debug_top3high', debugTopHighText); + + // NOTE: policy_all_prices is written by TariffManager._getDynamicTariff() every 5 min + // — no need to duplicate here + + // Push weather forecast with hourly radiation data for PV visualization. + // Prefer dailyProfiles (full 24h including past hours) over hourlyForecast (future only). + const weatherSource = weatherData?.dailyProfiles ?? weatherData?.hourlyForecast; + if (weatherSource && Array.isArray(weatherSource)) { + const nowAmsDate = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const hourlyWeather = weatherSource.map(h => { + const t = new Date(h.time); + const hAmsDate = t.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + return { + hour: parseInt(t.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10), + day: hAmsDate > nowAmsDate ? 1 : 0, + sunshine: h.sunshine, + cloudCover: h.cloudCover, + radiationWm2: h.radiationWm2, + weatherCode: h.weatherCode ?? 0 + }; + }); + this.homey.settings.set('policy_weather_hourly', hourlyWeather); + if (weatherData.fetchedAt) { + this.homey.settings.set('policy_weather_fetched_at', new Date(weatherData.fetchedAt).toISOString()); + } + } + + // Estimate PV production using grid analysis + sun model + // Use next-4h sunshine only — tomorrow's forecast must not inflate the current PV estimate + const sunScore = weatherData + ? Math.min(100, Math.round((weatherData.sunshineNext4Hours / 4) * 100)) + : 0; + const sun = { gfs: sunScore, harmonie: sunScore }; + const pvEstimateW = this._estimatePvProduction({ + gridPower: batteryState.gridPower, + batteryPower: batteryState.groupPower, + sunScore + }); + + const p1 = { + resolved_gridPower: batteryState.gridPower, + battery_power: batteryState.groupPower, + pv_power_estimated: pvEstimateW + }; + + // ------------------------------------------------------ + // ⭐ BATTERY COST MODEL INPUTS + // ------------------------------------------------------ + const batteryAvgCost = this._costAvg ?? (await this.getStoreValue('battery_avg_cost') || 0); + const batteryEnergyKwh = this._costEnergy ?? (await this.getStoreValue('battery_energy_kwh') || 0); + + const batteryEfficiency = Math.min(Math.max(this.efficiencyEstimator.getEfficiency() || 0.78, 0.5), 1.0); + + // Break-even prijs (€/kWh) + const breakEven = batteryAvgCost > 0 + ? batteryAvgCost / batteryEfficiency + : 0; + + + return { + weather: (settings.tariff_type === 'dynamic') ? weatherData : null, + battery: batteryState, + tariff: tariffInfo, + time: new Date(), + policyMode: this.getCapabilityValue('policy_mode'), + settings, + p1, + sun, + batteryEfficiency: this.efficiencyEstimator.getEfficiency(), + + // ⭐ NEW: Battery cost model + batteryCost: { + avgCost: batteryAvgCost, + energyKwh: batteryEnergyKwh, + breakEven + }, + previousHwMode: this.lastRecommendation?.hwMode ?? null + }; + + } + + _applyWeatherOverride(weatherData, override) { + const modified = { ...weatherData }; + + switch (override) { + case 'sunny': + modified.sunshineNext4Hours = 4; + modified.sunshineNext8Hours = 6; + modified.sunshineTodayRemaining = 5; + modified.sunshineTomorrow = 7; + modified.cloudCover = 0; + modified.precipitationProbability = 0; + break; + + case 'cloudy': + modified.sunshineNext4Hours = 0.5; + modified.sunshineNext8Hours = 1; + modified.sunshineTodayRemaining = 1; + modified.sunshineTomorrow = 2; + modified.cloudCover = 80; + modified.precipitationProbability = 20; + break; + + case 'rainy': + modified.sunshineNext4Hours = 0; + modified.sunshineNext8Hours = 0; + modified.sunshineTodayRemaining = 0; + modified.sunshineTomorrow = 0; + modified.cloudCover = 100; + modified.precipitationProbability = 90; + break; + + default: + return weatherData; + } + + return modified; + } + + async _getBatteryState() { + const fallback = { + stateOfCharge: 50, + health: 100, + cycles: 0, + gridPower: 0, + mode: 'standby', + groupPower: 0, + maxDischargePowerW: 800, + maxChargePowerW: 800, + battery_group_max_discharge_power_w: 800 + }; + + if (!this.p1Device) { + this.error('No P1 device available, using fallback battery state'); + return fallback; + } + + try { + const soc = + this.p1Device.getCapabilityValue('battery_group_average_soc') ?? + 50; + + const gridPower = + this.p1Device.getCapabilityValue('measure_power') ?? 0; + + const groupMode = + this.p1Device.getCapabilityValue('battery_group_charge_mode') ?? + 'standby'; + + const groupPower = + this.p1Device.getCapabilityValue('measure_power.battery_group_power_w') ?? + 0; + + const totalCapacity = + this.p1Device.getCapabilityValue('battery_group_total_capacity_kwh') ?? + null; + + // Estimate number of units from total capacity (each unit = 2.688 kWh @ 800 W) + const unitCount = totalCapacity ? Math.max(1, Math.round(totalCapacity / 2.688)) : 1; + const unitFallbackW = unitCount * 800; + + // ✅ NEW: Get max production and consumption power + const maxProduction = + this.p1Device.getCapabilityValue('measure_power.battery_group_max_production_w') ?? + unitFallbackW; + + const maxConsumption = + this.p1Device.getCapabilityValue('measure_power.battery_group_max_consumption_w') ?? + unitFallbackW; + + await this.setCapabilityValue('battery_soc_mirror', soc).catch(this.error); + await this.setCapabilityValue('grid_power_mirror', gridPower).catch(this.error); + + return { + stateOfCharge: soc, + health: 100, + cycles: 0, + gridPower, + mode: groupMode, + groupPower, + totalCapacityKwh: totalCapacity, + // ✅ NEW: Provide max discharge and charge power + maxDischargePowerW: maxProduction, + maxChargePowerW: maxConsumption, + battery_group_max_discharge_power_w: maxProduction + }; + + } catch (error) { + this.error('Failed to get battery state from P1:', error); + return fallback; + } + } + + async _applyRecommendation(mode, confidence) { + const minConfidence = this.getSetting('min_confidence_threshold') || 55; + + if (confidence < minConfidence) { + this.log(`Confidence ${confidence}% below threshold ${minConfidence}%, not applying`); + return false; + } + + if (!this.p1Device) { + this.error('No P1 device available to apply mode'); + return false; + } + + try { + // ⭐ Alleen echte HomeWizard modes + let targetMode = null; + + if (mode === 'zero_charge_only') { + targetMode = 'zero_charge_only'; + } else if (mode === 'zero_discharge_only') { + targetMode = 'zero_discharge_only'; + } else if (mode === 'to_full') { + targetMode = 'to_full'; + } else if (mode === 'standby') { + targetMode = 'standby'; + } else if (mode === 'zero') { + targetMode = 'zero'; + } else { + // Fallback: nooit niet‑bestaande modes sturen + this.log(`⚠️ Unknown logical mode "${mode}", falling back to standby`); + targetMode = 'standby'; + } + + // ⭐ Lees de ECHTE batterij-mode + const actualMode = this.p1Device.getCapabilityValue('battery_group_charge_mode'); + + this.log(`🔍 Actual HW mode: ${actualMode}, desired: ${targetMode}`); + + // ⭐ Als al correct → niets doen + if (actualMode === targetMode) { + this.log(`ℹ️ Battery already in correct HW mode (${actualMode}), no change needed`); + return true; + } + + // ⭐ Mode zetten + this.log(`🔄 Changing battery mode: ${actualMode} → ${targetMode} (confidence: ${confidence}%)`); + const result = await this.p1Device.setBatteryGroupMode(targetMode); + + if (result) { + this.log(`✅ Battery mode successfully changed to: ${targetMode}`); + await this._triggerModeApplied(targetMode, confidence); + return true; + } else { + this.log(`❌ setBatteryGroupMode returned false`); + return false; + } + + } catch (error) { + this.error('❌ Failed to apply recommendation to P1:', error); + return false; + } + } + + async _triggerRecommendationChanged(result, explanation) { + const trigger = this.homey.flow.getDeviceTriggerCard('policy_recommendation_changed'); + if (trigger) { + await trigger.trigger(this, { + mode: result.hwMode || result.policyMode, + confidence: result.confidence, + reason: explanation.summary + }).catch(this.error); + } + } + + async _triggerModeApplied(mode, confidence) { + const trigger = this.homey.flow.getDeviceTriggerCard('policy_mode_applied'); + if (trigger) { + await trigger.trigger(this, { + mode, + confidence + }).catch(this.error); + } + } + + async _triggerOverrideSet(duration) { + const trigger = this.homey.flow.getDeviceTriggerCard('policy_override_set'); + if (trigger) { + await trigger.trigger(this, { + duration + }).catch(this.error); + } + } + + async onSettings({ oldSettings, newSettings, changedKeys }) { + this.log('Settings changed:', changedKeys); + + // Validate + if (changedKeys.includes('max_charge_price')) { + const maxCharge = newSettings.max_charge_price; + const minDischarge = newSettings.min_discharge_price || oldSettings.min_discharge_price; + + if (maxCharge >= minDischarge) { + throw new Error(`max_charge_price (€${maxCharge}) must be less than min_discharge_price (€${minDischarge})`); + } + } + + // Update timeline user notification if thresholds changed + if (changedKeys.includes('max_charge_price') || changedKeys.includes('min_discharge_price')) { + await this.homey.notifications.createNotification({ + excerpt: `Battery thresholds updated: charge ≤€${newSettings.max_charge_price}, discharge ≥€${newSettings.min_discharge_price}` + }); + } + + // Update internal modules + this.policyEngine.updateSettings(newSettings); + this.tariffManager.updateSettings(newSettings); + this.optimizationEngine.updateSettings(newSettings); + + // Push updated settings immediately so planning page reflects the change + this.homey.settings.set('device_settings', { + max_charge_price: newSettings.max_charge_price || 0.19, + min_discharge_price: newSettings.min_discharge_price || 0.22, + min_soc: newSettings.min_soc || 10, + max_soc: newSettings.max_soc || 95, + battery_efficiency: newSettings.battery_efficiency || 0.78, + min_profit_margin: newSettings.min_profit_margin || 0.01, + tariff_type: newSettings.tariff_type || 'dynamic', + policy_interval: newSettings.policy_interval || 15, + pv_capacity_w: newSettings.pv_capacity_w || 0, + pv_estimation_enabled: newSettings.pv_estimation_enabled || false, + }); + + // Handle interval change + if (changedKeys.includes('policy_interval')) { + this._schedulePolicyCheck(); + } + + // Weather update + if (changedKeys.some(k => ['weather_latitude', 'weather_longitude', 'pv_tilt', 'pv_azimuth'].includes(k))) { + this.weatherForecaster.invalidateCache(); + this.homey.setTimeout(() => { + this._updateWeather().catch(err => this.error(err)); + }, 10); + } + + // P1 reconnect + if (changedKeys.includes('p1_device_id')) { + this.homey.setTimeout(() => { + this._connectP1Device().catch(err => this.error(err)); + }, 10); + } + + // Dynamic pricing refresh + if ( + changedKeys.includes('enable_dynamic_pricing') || + changedKeys.includes('tariff_type') + ) { + if (newSettings.tariff_type === 'dynamic' && newSettings.enable_dynamic_pricing) { + this._schedulePriceRefresh(); + } else if (this.priceRefreshTimeout) { + this.homey.clearTimeout(this.priceRefreshTimeout); + this.priceRefreshTimeout = null; + this.log('Price refresh stopped (dynamic pricing disabled)'); + } + } + + // ✅ FIX: Add threshold settings to policy run triggers + const requiresPolicyRun = + changedKeys.includes('policy_interval') || + changedKeys.includes('weather_latitude') || + changedKeys.includes('weather_longitude') || + changedKeys.includes('p1_device_id') || + changedKeys.includes('enable_dynamic_pricing') || + changedKeys.includes('tariff_type') || + changedKeys.includes('max_charge_price') || // ← ADD THIS + changedKeys.includes('min_discharge_price') || // ← ADD THIS + changedKeys.includes('min_soc') || // ← ADD THIS (affects planning) + changedKeys.includes('max_soc') || // ← ADD THIS (affects planning) + changedKeys.includes('battery_efficiency') || // ← ADD THIS (affects break-even) + changedKeys.includes('min_profit_margin'); // ← ADD THIS (affects spread calc) + + if (requiresPolicyRun) { + // Push device_settings to app settings IMMEDIATELY (before policy run) + // This ensures settings.html sees the new values when it refreshes + this.homey.settings.set('device_settings', { + max_charge_price: newSettings.max_charge_price || 0.19, + min_discharge_price: newSettings.min_discharge_price || 0.22, + min_soc: newSettings.min_soc || 10, + max_soc: newSettings.max_soc || 95, + battery_efficiency: newSettings.battery_efficiency || 0.78, + min_profit_margin: newSettings.min_profit_margin || 0.01, + tariff_type: newSettings.tariff_type || 'dynamic', + policy_interval: newSettings.policy_interval || 15, + pv_capacity_w: newSettings.pv_capacity_w || 0, + pv_estimation_enabled: newSettings.pv_estimation_enabled || false, + }); + + // Then run policy with new settings + this.homey.setTimeout(() => { + this._runPolicyCheck().catch(err => this.error(err)); + }, 500); + } +} + + + /** + * Remove event listeners from the P1 device to prevent leaks on reconnect/uninit. + */ + _cleanupP1Listeners() { + if (this.p1Device && this._onBatteryEvent) { + this.p1Device.removeListener('battery_event', this._onBatteryEvent); + } + this._onBatteryEvent = null; + } + + async onUninit() { + // Cleanup event listeners + this._cleanupP1Listeners(); + + // Cleanup intervals and timers when app stops/crashes + if (this.policyCheckInterval) { + this.homey.clearInterval(this.policyCheckInterval); + this.policyCheckInterval = null; + } + + if (this._hourBoundaryTimeout) { + this.homey.clearTimeout(this._hourBoundaryTimeout); + this._hourBoundaryTimeout = null; + } + + if (this.priceRefreshTimeout) { + this.homey.clearTimeout(this.priceRefreshTimeout); + this.priceRefreshTimeout = null; + } + + if (this._p1PollInterval) { + this.homey.clearInterval(this._p1PollInterval); + this._p1PollInterval = null; + } + } + + async onDeleted() { + this.log('BatteryPolicyDevice deleted'); + + // Call onUninit to cleanup timers + await this.onUninit(); + + // Clear app-level settings written by this device + // (prevents stale data if device is re-added) + const settingsToClean = [ + 'battery_policy_state', + 'policy_explainability', + 'policy_all_prices', + 'policy_all_prices_15min', + 'policy_debug_top3low', + 'policy_debug_top3high', + 'policy_weather_hourly', + 'policy_mode_history', + 'policy_optimizer_schedule', + 'device_settings', + ]; + for (const key of settingsToClean) { + try { this.homey.settings.unset(key); } catch (_) {} + } + + // Clear p1Device reference + this.p1Device = null; + + this.log('BatteryPolicyDevice cleanup complete'); + } + + /** + * Update planning chart camera image + * @param {Object} inputs - Policy inputs (battery, tariff, weather) + * @param {Object} result - Policy result with recommendation + */ + async _updatePlanningChart(inputs, result) { + try { + const currentHour = parseInt( + new Date().toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }) + , 10); + const language = this.homey.i18n.getLanguage(); + + // Get hourly prices for next 24 hours + const prices = []; + if (inputs.tariff?.top3Low && inputs.tariff?.top3High) { + // Generate price array from available data + const allPrices = this.tariffManager.dynamicProvider?.cache || []; + for (let h = 0; h < 24; h++) { + const priceData = allPrices.find(p => new Date(p.time).getHours() === h); + prices.push({ + hour: h, + price: priceData?.price || inputs.tariff.currentPrice || 0.25 + }); + } + } else { + // Fallback: flat rate + for (let h = 0; h < 24; h++) { + prices.push({ hour: h, price: 0.25 }); + } + } + + // Generate mode forecast for next 24 hours + // Simplified: current mode for all hours (can be enhanced with actual planning) + const modes = []; + const currentMode = result.hwMode || result.policyMode || 'standby'; + for (let h = 0; h < 24; h++) { + modes.push({ hour: h, mode: currentMode }); + } + + // PV forecast (simplified, could use weather hourly data) + const pvForecast = []; + const sun4h = inputs.weather?.sun4h || 0; + // Simple bell curve for daylight hours + for (let h = 0; h < 24; h++) { + let kw = 0; + if (h >= 8 && h <= 17 && sun4h > 0) { + // Peak at noon + const distance = Math.abs(h - 12.5); + kw = Math.max(0, sun4h * (1 - distance / 5)); + } + pvForecast.push({ hour: h, kw }); + } + + // SoC projection (simple linear discharge/charge based on current mode) + const socProjection = []; + let projectedSoC = inputs.battery?.stateOfCharge || 50; + for (let h = 0; h < 24; h++) { + socProjection.push({ hour: h, soc: Math.max(20, Math.min(100, projectedSoC)) }); + + // Simple projection: discharge 2%/h at night, charge 5%/h during PV + if (h >= 18 || h <= 6) { + projectedSoC -= 2; // Discharge + } else if (pvForecast[h].kw > 1) { + projectedSoC += 5; // Charge from PV + } + } + + // Generate chart + const chartData = { + prices, + modes, + pvForecast, + socProjection, + currentHour, + language + }; + + if (!this.chartGenerator) { + this.chartGenerator = new (require('../../lib/battery-chart-generator'))(this.homey); + } + const imageBuffer = this.chartGenerator.generateChart(chartData); + + if (!imageBuffer) { + // Chart generation disabled (canvas not installed) + return; + } + + // Update camera image + const image = await this.homey.images.createImage(); + await image.setBuffer(imageBuffer); + await this.setCameraImage('planning', this.homey.__('camera.planning_title'), image); + + this.log('📊 Planning chart updated successfully'); + + } catch (err) { + this.error('Failed to update planning chart:', err); + } + } + + + async _updateBatteryCostModel({ batteryPower, gridPower, pvState, soc }) { + const intervalSeconds = 15; // polling interval is 15s (every 20th call = 5 min log) + const deltaKwh = (batteryPower / 1000) * (intervalSeconds / 3600); + + // If battery is physically empty, wipe stale cost tracking in persistent store + const minSoc = this.getSetting('min_soc') ?? 0; + if (soc !== null && soc <= Math.max(minSoc, 1)) { + if ((this._costEnergy || 0) > 0 || (this._costAvg || 0) > 0) { + this.log(`💰 CostModel RESET: SoC ${soc}% <= ${Math.max(minSoc, 1)}% → clearing stale energy`); + this._costEnergy = 0; + this._costAvg = 0; + await this.setStoreValue('battery_energy_kwh', 0); + await this.setStoreValue('battery_avg_cost', 0); + } + return; + } + + // Initialize in-memory accumulators from store on first call + if (this._costEnergy === undefined) { + this._costEnergy = await this.getStoreValue('battery_energy_kwh') || 0; + this._costAvg = await this.getStoreValue('battery_avg_cost') || 0; + } + + // Log every 60s (every 12th call) + this._costModelCallCount = (this._costModelCallCount || 0) + 1; + if (this._costModelCallCount % 12 === 0) { + this.log(`💰 CostModel: batteryPower=${batteryPower}W, deltaKwh=${deltaKwh.toFixed(6)}, energy=${this._costEnergy.toFixed(3)}kWh, avgCost=€${this._costAvg.toFixed(4)}, pvState=${pvState}`); + } + + if (Math.abs(deltaKwh) < 0.000001) return; // effectively zero + + let costNew; + + if (batteryPower > 10) { + // Charging + if (pvState) { + const pvMode = await this.getStoreValue('pv_cost_mode') || 'hybrid'; + const feedIn = await this.getStoreValue('feed_in_tariff') || 0.10; + + if (pvMode === 'free') costNew = 0; + else if (pvMode === 'feedin') costNew = feedIn; + else costNew = feedIn < 0.08 ? 0 : feedIn; + } else { + // Grid charging + const tariff = this.tariffManager.getCurrentTariff(gridPower); + costNew = tariff.currentPrice; + } + + const Enew = this._costEnergy + deltaKwh; + const avgNew = ((this._costAvg * this._costEnergy) + (costNew * deltaKwh)) / Enew; + + this._costEnergy = Enew; + this._costAvg = avgNew; + + if (debug) this.log(`💰 CostModel charge: +${deltaKwh.toFixed(5)}kWh @ €${costNew?.toFixed(4)}, avgCost now €${avgNew.toFixed(4)}, total ${Enew.toFixed(3)}kWh`); + + } else if (batteryPower < -10) { + // Discharging + this._costEnergy = Math.max(0, this._costEnergy + deltaKwh); + + if (this._costModelCallCount % 12 === 0) { + if (debug) this.log(`💰 CostModel discharge: ${deltaKwh.toFixed(5)}kWh, total ${this._costEnergy.toFixed(3)}kWh`); + } + } + + // Persist to store every 2 minutes (every 8th call) instead of every 15s + if (this._costModelCallCount % 8 === 0) { + await this.setStoreValue('battery_energy_kwh', this._costEnergy); + await this.setStoreValue('battery_avg_cost', this._costAvg); + } + } + + +} + +module.exports = BatteryPolicyDevice; \ No newline at end of file diff --git a/drivers/battery-policy/device_settings.html b/drivers/battery-policy/device_settings.html new file mode 100644 index 00000000..5dd20226 --- /dev/null +++ b/drivers/battery-policy/device_settings.html @@ -0,0 +1,486 @@ + + + + + + + + + + +
+

+
+
+ +
+
+
⏳ Planning laden...
+
+ +
+

+
+
+ +
+
⏳ Uren laden...
+
+
+ + + + diff --git a/drivers/battery-policy/driver.compose.json b/drivers/battery-policy/driver.compose.json new file mode 100644 index 00000000..61982eb7 --- /dev/null +++ b/drivers/battery-policy/driver.compose.json @@ -0,0 +1,82 @@ +{ + "id": "battery-policy", + "name": { + "en": "Battery Policy Manager" + }, + "class": "other", + "capabilities": [ + "policy_enabled", + "policy_mode", + "auto_apply", + "recommended_mode", + "active_mode", + "sun_score", + "predicted_sun_hours", + "confidence_score", + "explanation_summary", + "policy_debug_price", + "policy_debug_top3low", + "policy_debug_top3high", + "policy_debug_sun", + "policy_debug_learning", + "battery_soc_mirror", + "grid_power_mirror", + "battery_rte", + "last_update", + "override_until", + "weather_override" + ], + "pair": [ + { + "id": "select_battery", + "template": "list_devices", + "navigation": { + "next": "configure_policy" + }, + "options": { + "singular": true, + "title": { + "en": "Select Battery Device" + }, + "instruction": { + "en": "Select the HomeWizard battery device to manage" + } + } + }, + { + "id": "configure_policy", + "template": "add_devices", + "navigation": { + "prev": "select_battery" + } + } + ], + "settings": [ + { + "type": "group", + "label": { + "en": "24-Hour Planning", + "nl": "24-Uurs Planning" + }, + "children": [ + { + "id": "view_planning", + "type": "label", + "label": { + "en": "View Planning", + "nl": "Planning Bekijken" + }, + "hint": { + "en": "Open the 24-hour battery planning view to see hourly prices, modes, and projections", + "nl": "Open de 24-uurs batterij planning om prijzen, modi en projecties per uur te zien" + }, + "value": "📊" + } + ] + } + ], + "images": { + "large": "drivers/battery-policy/assets/images/large.png", + "small": "drivers/battery-policy/assets/images/small.png" + } +} \ No newline at end of file diff --git a/drivers/battery-policy/driver.flow.compose.json b/drivers/battery-policy/driver.flow.compose.json new file mode 100644 index 00000000..3aeaee7a --- /dev/null +++ b/drivers/battery-policy/driver.flow.compose.json @@ -0,0 +1,246 @@ +{ + "triggers": [ + { + "id": "policy_recommendation_changed", + "title": { "en": "Recommendation changed" }, + "titleFormatted": { "en": "Recommendation changed" }, + "hint": { "en": "Triggers when the policy engine changes its recommendation" }, + "tokens": [ + { + "name": "mode", + "type": "string", + "title": { "en": "Mode" }, + "example": { "en": "charge" } + }, + { + "name": "confidence", + "type": "number", + "title": { "en": "Confidence" }, + "example": 75 + }, + { + "name": "reason", + "type": "string", + "title": { "en": "Reason" }, + "example": { "en": "Strong sunlight expected" } + } + ] + }, + { + "id": "policy_mode_applied", + "title": { "en": "Mode applied to battery" }, + "titleFormatted": { "en": "Mode applied to battery" }, + "hint": { "en": "Triggers when the policy engine applies a mode to the battery" }, + "tokens": [ + { + "name": "mode", + "type": "string", + "title": { "en": "Mode" }, + "example": { "en": "discharge" } + }, + { + "name": "confidence", + "type": "number", + "title": { "en": "Confidence" }, + "example": 85 + } + ] + }, + { + "id": "policy_override_set", + "title": { "en": "Manual override activated" }, + "titleFormatted": { "en": "Manual override activated" }, + "hint": { "en": "Triggers when manual override is set" }, + "tokens": [ + { + "name": "duration", + "type": "number", + "title": { "en": "Duration" }, + "example": 60 + } + ] + } + ], + + "conditions": [ + { + "id": "policy_is_enabled", + "title": { "en": "Policy is !{{enabled|disabled}}" }, + "titleFormatted": { "en": "Policy is !{{enabled|disabled}}" }, + "hint": { "en": "Check if the policy engine is active" } + }, + { + "id": "confidence_above", + "title": { "en": "Confidence is above" }, + "titleFormatted": { "en": "Confidence is above [[threshold]]" }, + "hint": { "en": "Check if recommendation confidence exceeds threshold" }, + "args": [ + { + "name": "threshold", + "type": "number", + "placeholder": { "en": "70" }, + "min": 0, + "max": 100 + } + ] + }, + { + "id": "recommended_mode_is", + "title": { "en": "Recommended mode is" }, + "titleFormatted": { "en": "Recommended mode is [[mode]]" }, + "hint": { "en": "Check if current recommendation matches a specific mode" }, + "args": [ + { + "name": "mode", + "type": "dropdown", + "values": [ + { "id": "charge", "label": { "en": "Charge" } }, + { "id": "discharge", "label": { "en": "Discharge" } }, + { "id": "preserve", "label": { "en": "Preserve" } } + ] + } + ] + }, + { + "id": "sun_score_above", + "title": { "en": "Sun score is above" }, + "titleFormatted": { "en": "Sun score is above [[threshold]]" }, + "hint": { "en": "Check if sunshine availability exceeds threshold" }, + "args": [ + { + "name": "threshold", + "type": "number", + "placeholder": { "en": "50" }, + "min": 0, + "max": 100 + } + ] + } + ], + + "actions": [ + { + "id": "enable_policy", + "title": { "en": "Enable policy" }, + "titleFormatted": { "en": "Enable policy" }, + "hint": { "en": "Enable the battery policy engine" } + }, + { + "id": "disable_policy", + "title": { "en": "Disable policy" }, + "titleFormatted": { "en": "Disable policy" }, + "hint": { "en": "Disable the battery policy engine" } + }, + { + "id": "set_policy_mode", + "title": { "en": "Set policy mode to" }, + "titleFormatted": { "en": "Set policy mode to [[mode]]" }, + "hint": { "en": "Change the policy optimization mode" }, + "args": [ + { + "name": "mode", + "type": "dropdown", + "values": [ + { "id": "eco", "label": { "en": "Eco" } }, + { "id": "balanced", "label": { "en": "Balanced" } }, + { "id": "aggressive", "label": { "en": "Aggressive" } } + ] + } + ] + }, + { + "id": "enable_auto_apply", + "title": { "en": "Enable auto-apply" }, + "titleFormatted": { "en": "Enable auto-apply" }, + "hint": { "en": "Automatically apply policy recommendations" } + }, + { + "id": "disable_auto_apply", + "title": { "en": "Disable auto-apply" }, + "titleFormatted": { "en": "Disable auto-apply" }, + "hint": { "en": "Stop automatically applying recommendations" } + }, + { + "id": "set_override", + "title": { "en": "Set manual override for" }, + "titleFormatted": { "en": "Set manual override for [[duration]] minutes" }, + "hint": { "en": "Prevent automatic policy changes for a duration" }, + "args": [ + { + "name": "duration", + "type": "number", + "placeholder": { "en": "60" }, + "min": 15, + "max": 1440, + "units": { "en": "minutes" } + } + ] + }, + { + "id": "clear_override", + "title": { "en": "Clear manual override" }, + "titleFormatted": { "en": "Clear manual override" }, + "hint": { "en": "Resume automatic policy management" } + }, + { + "id": "force_policy_check", + "title": { "en": "Run policy check now" }, + "titleFormatted": { "en": "Run policy check now" }, + "hint": { "en": "Force immediate policy recalculation" } + }, + { + "id": "refresh_weather", + "title": { "en": "Refresh weather forecast" }, + "titleFormatted": { "en": "Refresh weather forecast" }, + "hint": { "en": "Fetch latest weather data immediately" } + }, + { + "id": "set_weather_override", + "title": { "en": "Set weather override" }, + "titleFormatted": { "en": "Set weather override to [[override]]" }, + "hint": { "en": "Override the weather forecast for policy decisions" }, + "args": [ + { + "name": "override", + "type": "dropdown", + "title": { "en": "Weather condition" }, + "values": [ + { "id": "auto", "title": { "en": "Auto (use forecast)" } }, + { "id": "sunny", "title": { "en": "Sunny" } }, + { "id": "cloudy", "title": { "en": "Cloudy" } }, + { "id": "rainy", "title": { "en": "Rainy" } } + ] + } + ] + }, + { + "id": "update_pv_production", + "title": { "en": "Update PV production", "nl": "PV-productie bijwerken" }, + "titleFormatted": { "en": "Update PV production to [[power]] watts", "nl": "PV-productie bijwerken naar [[power]] watt" }, + "hint": { + "en": "Provide real-time PV production data from your solar system. This overrides the sun-based estimate.", + "nl": "Geef realtime PV-productiegegevens van uw zonnesysteem. Dit overschrijft de op zon gebaseerde schatting." + }, + "args": [ + { + "name": "power", + "type": "number", + "title": { "en": "PV Power", "nl": "PV-vermogen" }, + "placeholder": { "en": "1500" }, + "min": 0, + "max": 50000, + "units": { "en": "W", "nl": "W" } + } + ] + }, + { + "id": "reset_learning_data", + "title": { "en": "Reset learning data", "nl": "Leergegevens resetten" }, + "titleFormatted": { "en": "Reset all learning data", "nl": "Alle leergegevens resetten" }, + "hint": { + "en": "Clear all learned patterns and start fresh. Use when moving or changing your setup.", + "nl": "Wis alle geleerde patronen en begin opnieuw. Gebruik bij verhuizing of wijziging van uw opstelling." + } + } + ] +} diff --git a/drivers/battery-policy/driver.js b/drivers/battery-policy/driver.js new file mode 100644 index 00000000..507ff5ab --- /dev/null +++ b/drivers/battery-policy/driver.js @@ -0,0 +1,224 @@ +'use strict'; + +const Homey = require('homey'); + +class BatteryPolicyDriver extends Homey.Driver { + + async onInit() { + this.log('BatteryPolicyDriver initialized'); + + // Register flow cards + this._registerFlowCards(); + } + + /** + * Register flow cards + * @private + */ + _registerFlowCards() { + // Trigger: Recommendation changed + this.homey.flow.getDeviceTriggerCard('policy_recommendation_changed') + .registerRunListener(async (args, state) => { + return true; + }); + + // Trigger: Mode applied + this.homey.flow.getDeviceTriggerCard('policy_mode_applied') + .registerRunListener(async (args, state) => { + return true; + }); + + // Trigger: Override set + this.homey.flow.getDeviceTriggerCard('policy_override_set') + .registerRunListener(async (args, state) => { + return true; + }); + + // Condition: Policy enabled + this.homey.flow.getConditionCard('policy_is_enabled') + .registerRunListener(async (args) => { + return args.device.getCapabilityValue('policy_enabled'); + }); + + // Condition: Confidence above threshold + this.homey.flow.getConditionCard('confidence_above') + .registerRunListener(async (args) => { + const confidence = args.device.getCapabilityValue('confidence_score'); + return confidence >= args.threshold; + }); + + // Condition: Recommended mode is + this.homey.flow.getConditionCard('recommended_mode_is') + .registerRunListener(async (args) => { + const mode = args.device.getCapabilityValue('recommended_mode'); + return mode === args.mode; + }); + + // Condition: Sun score above + this.homey.flow.getConditionCard('sun_score_above') + .registerRunListener(async (args) => { + const sunScore = args.device.getCapabilityValue('sun_score'); + return sunScore >= args.threshold; + }); + + // Action: Enable policy + this.homey.flow.getActionCard('enable_policy') + .registerRunListener(async (args) => { + await args.device.setCapabilityValue('policy_enabled', true); + }); + + // Action: Disable policy + this.homey.flow.getActionCard('disable_policy') + .registerRunListener(async (args) => { + await args.device.setCapabilityValue('policy_enabled', false); + }); + + // Action: Set policy mode + this.homey.flow.getActionCard('set_policy_mode') + .registerRunListener(async (args) => { + await args.device.setCapabilityValue('policy_mode', args.mode); + }); + + // Action: Enable auto-apply + this.homey.flow.getActionCard('enable_auto_apply') + .registerRunListener(async (args) => { + await args.device.setCapabilityValue('auto_apply', true); + }); + + // Action: Disable auto-apply + this.homey.flow.getActionCard('disable_auto_apply') + .registerRunListener(async (args) => { + await args.device.setCapabilityValue('auto_apply', false); + }); + + // Action: Set manual override + this.homey.flow.getActionCard('set_override') + .registerRunListener(async (args) => { + await args.device.setManualOverride(args.duration); + }); + + // Action: Clear override + this.homey.flow.getActionCard('clear_override') + .registerRunListener(async (args) => { + await args.device.clearManualOverride(); + }); + + // Action: Force policy check + this.homey.flow.getActionCard('force_policy_check') + .registerRunListener(async (args) => { + await args.device._runPolicyCheck(); + }); + + // Action: Refresh weather + this.homey.flow.getActionCard('refresh_weather') + .registerRunListener(async (args) => { + args.device.weatherForecaster.invalidateCache(); + await args.device._updateWeather(); + }); + + // Action: Set weather override + this.homey.flow.getActionCard('set_weather_override') + .registerRunListener(async (args) => { + await args.device.setCapabilityValue('weather_override', args.override); + }); + + // Action: Update PV production + this.homey.flow.getActionCard('update_pv_production') + .registerRunListener(async (args) => { + const power = Math.max(0, Math.round(args.power || 0)); + args.device._updatePvProduction(power); + args.device.log(`PV production updated via flow: ${power}W`); + }); + + // Action: Reset learning data + this.homey.flow.getActionCard('reset_learning_data') + .registerRunListener(async (args) => { + await args.device.learningEngine.reset(); + args.device.log('Learning data reset via flow card'); + }); + + this.log('Flow cards registered'); + } + + /** + * Pairing sequence + * Koppel aan een P1 (energy_v2) device + */ + async onPair(session) { + let selectedP1Id = null; + + // Stap 1: lijst met P1 devices + session.setHandler('list_devices', async () => { + const driver = this.homey.drivers.getDriver('energy_v2'); + if (!driver) { + this.log('energy_v2 driver not found during pairing'); + return []; + } + + const devices = driver.getDevices(); + + return devices.map(device => ({ + name: `Battery Policy: ${device.getName()}`, + data: { + id: `policy_${device.getData().id}` + }, + settings: { + p1_device_id: device.getData().id + } + })); + }); + + // Stap 2: selectie verwerken (optioneel, maar netjes) + session.setHandler('list_devices_selection', async (devices) => { + if (devices && devices.length > 0) { + selectedP1Id = devices[0].settings.p1_device_id; + this.log('Selected P1 device for policy:', selectedP1Id); + return true; + } + return false; + }); + } + + /** + * Device repair (voor opnieuw koppelen aan P1 device) + */ + async onRepair(session, device) { + session.setHandler('list_devices', async () => { + const driver = this.homey.drivers.getDriver('energy_v2'); + if (!driver) { + this.log('energy_v2 driver not found during repair'); + return []; + } + + const devices = driver.getDevices(); + + return devices.map(p1Device => ({ + name: p1Device.getName(), + data: { + id: p1Device.getData().id + } + })); + }); + + session.setHandler('list_devices_selection', async (devices) => { + if (devices && devices.length > 0) { + const newP1Id = devices[0].data.id; + + await device.setSettings({ + p1_device_id: newP1Id + }); + + // Reconnect naar P1 + if (typeof device._connectP1Device === 'function') { + await device._connectP1Device(); + } + + this.log('Repaired policy device to P1:', newP1Id); + return true; + } + return false; + }); + } +} + +module.exports = BatteryPolicyDriver; diff --git a/drivers/battery-policy/driver.settings.compose.json b/drivers/battery-policy/driver.settings.compose.json new file mode 100644 index 00000000..b44c2c52 --- /dev/null +++ b/drivers/battery-policy/driver.settings.compose.json @@ -0,0 +1,392 @@ +[ + { + "type": "group", + "label": { "en": "Battery Device", "nl": "Batterijapparaat" }, + "children": [ + { + "id": "p1_device_id", + "type": "label", + "label": { "en": "Linked P1 (Energy v2)", "nl": "Gekoppelde P1 (Energy v2)" }, + "value": "Not configured", + "hint": { + "en": "The HomeWizard P1 (energy_v2) device this policy uses for grid & battery group data. Use the repair tool to change.", + "nl": "Het HomeWizard P1‑apparaat (energy_v2) dat deze policy gebruikt voor net‑ en batterijgroepgegevens. Gebruik de reparatietool om dit te wijzigen." + } + } + ] + }, + + { + "type": "group", + "label": { "en": "Policy Behavior", "nl": "Policy‑gedrag" }, + "children": [ + { + "id": "policy_interval", + "type": "number", + "label": { "en": "Policy Check Interval", "nl": "Policy‑controle‑interval" }, + "units": { "en": "minutes", "nl": "minuten" }, + "value": 15, + "min": 5, + "max": 60, + "step": 5 + }, + { + "id": "min_confidence_threshold", + "type": "number", + "label": { "en": "Minimum Confidence", "nl": "Minimale zekerheid" }, + "units": { "en": "%", "nl": "%" }, + "value": 55, + "min": 0, + "max": 100, + "step": 5 + } + ] + }, + + { + "type": "group", + "label": { "en": "Tariff Configuration", "nl": "Tariefinstellingen" }, + "children": [ + { + "id": "tariff_type", + "type": "dropdown", + "label": { "en": "Tariff Type", "nl": "Type tarief" }, + "value": "fixed", + "values": [ + { "id": "fixed", "label": { "en": "Fixed Rate (Peak Shaving)", "nl": "Vast tarief (Peak Shaving)" } }, + { "id": "dynamic", "label": { "en": "Dynamic Pricing", "nl": "Dynamische prijzen" } } + ] + }, + + { + "id": "peak_hours", + "type": "text", + "label": { "en": "Peak Hours", "nl": "Piekuren" }, + "value": "17:00-21:00", + "visible": { "when": "tariff_type", "is": "fixed" } + }, + + { + "id": "enable_dynamic_pricing", + "type": "checkbox", + "label": { "en": "Enable Dynamic Pricing Provider", "nl": "Dynamische prijsprovider inschakelen" }, + "value": false, + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + + { + "id": "respect_minmax", + "type": "checkbox", + "label": { + "en": "Strictly respect min/max prices", + "nl": "Min/max prijzen strikt respecteren" + }, + "hint": { + "en": "When enabled (default), battery only charges/discharges within configured price limits. When disabled, the system can make opportunistic decisions outside these limits when profitable (e.g., charging at €0.17 when future price is €0.30). Recommended: ENABLED for 2026 (with net metering), DISABLED for 2027+ (without net metering).", + "nl": "Indien ingeschakeld (standaard) laadt/ontlaadt de batterij alleen binnen geconfigureerde prijslimieten. Indien uitgeschakeld kan het systeem opportunistische beslissingen nemen buiten deze limieten wanneer winstgevend (bijv. laden op €0,17 wanneer toekomstige prijs €0,30 is). Aanbevolen: INGESCHAKELD voor 2026 (met salderen), UITGESCHAKELD voor 2027+ (zonder salderen)." + }, + "value": true, + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + + { + "id": "opportunistic_charge_multiplier", + "type": "number", + "label": { + "en": "Opportunistic charge spread multiplier", + "nl": "Opportunistisch laden spread vermenigvuldiger" + }, + "hint": { + "en": "Multiplier for min_profit_margin to trigger opportunistic charging above max_charge_price. Formula: spread > (min_profit_margin × multiplier). Higher values = more conservative. Examples: 1.5 (aggressive, €0.015 spread), 2.0 (balanced, €0.02 spread), 3.0 (conservative, €0.03 spread). Only active when 'Strictly respect min/max prices' is DISABLED.", + "nl": "Vermenigvuldiger voor min_profit_margin om opportunistisch laden boven max_charge_price te activeren. Formule: spread > (min_profit_margin × vermenigvuldiger). Hogere waarden = conservatiever. Voorbeelden: 1,5 (agressief, €0,015 spread), 2,0 (gebalanceerd, €0,02 spread), 3,0 (conservatief, €0,03 spread). Alleen actief wanneer 'Min/max prijzen strikt respecteren' is UITGESCHAKELD." + }, + "value": 2.0, + "min": 1.0, + "max": 5.0, + "step": 0.5, + "units": "×", + "visible": { "when": "respect_minmax", "is": false } + }, + + { + "id": "opportunistic_discharge_floor", + "type": "number", + "label": { + "en": "Opportunistic discharge price floor", + "nl": "Opportunistisch ontladen prijsbodem" + }, + "hint": { + "en": "Minimum price required for opportunistic discharge when current price is below min_discharge_price. Lower values = discharge at lower prices (more aggressive). Examples: €0.10 (very aggressive), €0.20 (balanced), €0.30 (conservative). Only active when 'Strictly respect min/max prices' is DISABLED.", + "nl": "Minimale prijs vereist voor opportunistisch ontladen wanneer huidige prijs onder min_discharge_price ligt. Lagere waarden = ontladen bij lagere prijzen (agressiever). Voorbeelden: €0,10 (zeer agressief), €0,20 (gebalanceerd), €0,30 (conservatief). Alleen actief wanneer 'Min/max prijzen strikt respecteren' is UITGESCHAKELD." + }, + "value": 0.20, + "min": 0.10, + "max": 0.35, + "step": 0.01, + "units": "€/kWh", + "visible": { "when": "respect_minmax", "is": false } + }, + + { + "id": "opportunistic_discharge_spread_threshold", + "type": "number", + "label": { + "en": "Opportunistic discharge spread threshold", + "nl": "Opportunistisch ontladen spread drempel" + }, + "hint": { + "en": "Maximum negative spread to allow opportunistic discharge (when no better future price expected). More negative = stricter (less willing to discharge early). Formula: (future_max_price × efficiency) - current_price < threshold. Examples: -€0.10 (very strict), -€0.05 (balanced), -€0.01 (aggressive). Only active when 'Strictly respect min/max prices' is DISABLED.", + "nl": "Maximale negatieve spread om opportunistisch ontladen toe te staan (wanneer geen betere toekomstige prijs verwacht). Negatiever = strenger (minder bereid vroeg te ontladen). Formule: (toekomstige_max_prijs × efficiëntie) - huidige_prijs < drempel. Voorbeelden: -€0,10 (zeer streng), -€0,05 (gebalanceerd), -€0,01 (agressief). Alleen actief wanneer 'Min/max prijzen strikt respecteren' is UITGESCHAKELD." + }, + "value": -0.05, + "min": -0.10, + "max": -0.01, + "step": 0.01, + "units": "€/kWh", + "visible": { "when": "respect_minmax", "is": false } + }, + + { + "id": "max_charge_price", + "type": "number", + "label": { "en": "Max Charge Price (€/kWh)", "nl": "Maximale laadprijs (€/kWh)" }, + "value": 0.15, + "min": 0, + "max": 1, + "step": 0.01, + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + + { + "id": "min_discharge_price", + "type": "number", + "label": { "en": "Min Discharge Price (€/kWh)", "nl": "Minimale ontlaadprijs (€/kWh)" }, + "value": 0.30, + "min": 0, + "max": 1, + "step": 0.01, + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + + { + "id": "min_profit_margin", + "type": "number", + "label": { "en": "Min Arbitrage Profit (€/kWh)", "nl": "Minimale arbitragewinst (€/kWh)" }, + "hint": { + "en": "Minimum profit per kWh (after battery losses) before grid charging is attempted. Higher = fewer but more profitable cycles. 0.00 = always charge at cheap hours.", + "nl": "Minimale winst per kWh (na batterijverlies) voordat laden uit het net wordt geprobeerd. Hoger = minder maar winstgevendere cycli. 0,00 = altijd laden bij goedkope uren." + }, + "value": 0.01, + "min": 0, + "max": 0.15, + "step": 0.005, + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + + { + "id": "battery_efficiency", + "type": "number", + "label": { "en": "Battery Efficiency (RTE)", "nl": "Batterij efficiëntie (RTE)" }, + "hint": { + "en": "Round-trip efficiency: the percentage of energy you get back from what you put in. The system learns the actual efficiency from real charge/discharge cycles and will override this value automatically. Typical measured values for HomeWizard: 0.72-0.82 (72-82%).", + "nl": "Round-trip efficiëntie: het percentage energie dat je terugkrijgt van wat je erin stopt. Het systeem leert de werkelijke efficiëntie van echte laad/ontlaad cycli en overschrijft deze waarde automatisch. Typische gemeten waarden voor HomeWizard: 0,72-0,82 (72-82%)." + }, + "value": 0.78, + "min": 0.50, + "max": 0.97, + "step": 0.01, + "units": "RTE", + "visible": { "when": "tariff_type", "is": "dynamic" } + } + ] + }, + + { + "type": "group", + "label": { "en": "Weather Forecasting", "nl": "Weersvoorspelling" }, + "visible": { "when": "tariff_type", "is": "dynamic" }, + "children": [ + { + "id": "weather_latitude", + "type": "number", + "label": { "en": "Latitude", "nl": "Breedtegraad" }, + "hint": { "en": "Latitude for solar forecasting (e.g. 52.370).", "nl": "Breedtegraad voor zonne-energievoorspelling (bijv. 52.370)." }, + "value": 0, + "min": -90, + "max": 90, + "step": 0.001 + }, + { + "id": "weather_longitude", + "type": "number", + "label": { "en": "Longitude", "nl": "Lengtegraad" }, + "hint": { "en": "Longitude for solar forecasting (e.g. 4.895).", "nl": "Lengtegraad voor zonne-energievoorspelling (bijv. 4.895)." }, + "value": 0, + "min": -180, + "max": 180, + "step": 0.001 + }, + { + "id": "weather_update_interval", + "type": "number", + "label": { "en": "Weather Update Interval", "nl": "Update‑interval weersvoorspelling" }, + "units": { "en": "hours", "nl": "uur" }, + "value": 3, + "min": 1, + "max": 24, + "step": 1 + } + ] + }, + + { + "type": "group", + "label": { "en": "Battery Limits", "nl": "Batterijlimieten" }, + "children": [ + { + "id": "min_soc", + "type": "number", + "label": { "en": "Minimum Battery %", "nl": "Minimale batterij‑%" }, + "hint": { + "en": "0% means no minimum — battery is allowed to fully discharge. HomeWizard firmware protects the battery hardware at 0-100%.", + "nl": "0% betekent geen minimum — de batterij mag volledig ontladen. HomeWizard firmware beschermt de batterij hardware bij 0-100%." + }, + "units": { "en": "%", "nl": "%" }, + "value": 0, + "min": 0, + "max": 50, + "step": 5, + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + { + "id": "max_soc", + "type": "number", + "label": { "en": "Maximum Battery %", "nl": "Maximale batterij‑%" }, + "units": { "en": "%", "nl": "%" }, + "value": 100, + "min": 80, + "max": 100, + "step": 5, + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + { + "id": "preserve_cycles", + "type": "checkbox", + "label": { "en": "Preserve Battery Cycles", "nl": "Batterijcycli sparen" }, + "value": true, + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + { + "id": "cycle_cost_per_kwh", + "type": "number", + "label": { "en": "Battery Cycle Cost (€/kWh)", "nl": "Batterij cycluskosten (€/kWh)" }, + "hint": { + "en": "Degradation cost per kWh discharged. The optimizer only cycles the battery when the price spread exceeds this cost. Formula: battery_price / (rated_cycles × usable_kWh). Example for 1× HomeWizard battery: €1200 / (6000 × 2.7 kWh) = €0.074.", + "nl": "Slijtagekosten per kWh ontladen. De optimizer cycleert de batterij alleen wanneer het prijsverschil deze kosten overstijgt. Formule: batterijprijs / (nominale_cycli × bruikbare_kWh). Voorbeeld voor 1× HomeWizard batterij: €1200 / (6000 × 2,7 kWh) = €0,074." + }, + "value": 0.075, + "min": 0.00, + "max": 0.15, + "step": 0.005, + "units": "€/kWh", + "visible": { "when": "tariff_type", "is": "dynamic" } + } + ] + }, + + { + "type": "group", + "label": { "en": "PV Estimation", "nl": "PV‑schatting" }, + "children": [ + { + "id": "pv_estimation_enabled", + "type": "checkbox", + "label": { "en": "Enable PV Estimation", "nl": "PV‑schatting inschakelen" }, + "hint": { + "en": "Estimate PV production using grid measurements (no privileged access needed).", + "nl": "Schat PV‑productie met behulp van netmetingen (geen verhoogde toegang nodig)." + }, + "value": false + }, + { + "id": "pv_capacity_w", + "type": "number", + "label": { "en": "PV Peak Capacity (W)", "nl": "PV‑piekvermogen (W)" }, + "hint": { + "en": "Your solar system's peak power rating in watts (e.g., 3500 for 3.5kWp).", + "nl": "Het piekvermogen van uw zonnesysteem in watt (bijv. 3500 voor 3,5kWp)." + }, + "units": { "en": "W", "nl": "W" }, + "value": 0, + "min": 0, + "max": 20000, + "step": 100, + "visible": { "when": "pv_estimation_enabled", "is": true } + }, + { + "id": "pv_tilt", + "type": "number", + "label": { "en": "Panel Tilt (°)", "nl": "Paneel helling (°)" }, + "hint": { + "en": "Roof/panel tilt angle in degrees. 0° = flat (horizontal), 90° = vertical. Typical roof: 30–40°.", + "nl": "Helling van het dak/paneel in graden. 0° = plat (horizontaal), 90° = verticaal. Typisch dak: 30–40°." + }, + "units": "°", + "value": 35, + "min": 0, + "max": 90, + "step": 5, + "visible": { "when": "pv_estimation_enabled", "is": true } + }, + { + "id": "pv_azimuth", + "type": "number", + "label": { "en": "Panel Azimuth (°)", "nl": "Paneel azimuth (°)" }, + "hint": { + "en": "Panel facing direction: -90° = East, 0° = South, 90° = West. E.g. southwest = 45°.", + "nl": "Richting panelen: -90° = Oost, 0° = Zuid, 90° = West. Bijv. zuidwest = 45°." + }, + "units": "°", + "value": 0, + "min": -90, + "max": 90, + "step": 5, + "visible": { "when": "pv_estimation_enabled", "is": true } + }, + { + "id": "pv_performance_ratio", + "type": "number", + "label": { "en": "Performance Ratio (PR)", "nl": "Prestatieratio (PR)" }, + "hint": { + "en": "Fraction of theoretical PV output actually delivered, accounting for inverter losses, wiring, temperature, and soiling. Typical range: 0.65–0.80. Lower this value if the planning page consistently overestimates your actual production.", + "nl": "Fractie van het theoretisch PV-vermogen dat daadwerkelijk wordt geleverd, rekening houdend met omvormerverlies, bedrading, temperatuur en vervuiling. Typisch bereik: 0,65–0,80. Verlaag deze waarde als de planningspagina uw werkelijke productie stelselmatig overschat." + }, + "units": "PR", + "value": 0.75, + "min": 0.50, + "max": 0.90, + "step": 0.01, + "visible": { "when": "pv_estimation_enabled", "is": true } + } + ] + }, + + { + "type": "group", + "label": { "en": "Advanced", "nl": "Geavanceerd" }, + "children": [ + { + "id": "enable_logging", + "type": "checkbox", + "label": { "en": "Enable Detailed Logging", "nl": "Gedetailleerde logging inschakelen" }, + "value": false + }, + { + "id": "enable_policy_notifications", + "type": "checkbox", + "label": { "en": "Post policy decisions to timeline", "nl": "Policy‑beslissingen naar tijdlijn sturen" }, + "value": false + } + ] + } +] diff --git a/drivers/cloud_p1/assets/icon.svg b/drivers/cloud_p1/assets/icon.svg new file mode 100644 index 00000000..e1bc3269 --- /dev/null +++ b/drivers/cloud_p1/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/cloud_p1/assets/images/large.png b/drivers/cloud_p1/assets/images/large.png new file mode 100644 index 00000000..6358f0b7 Binary files /dev/null and b/drivers/cloud_p1/assets/images/large.png differ diff --git a/drivers/cloud_p1/assets/images/small.png b/drivers/cloud_p1/assets/images/small.png new file mode 100644 index 00000000..721d7b06 Binary files /dev/null and b/drivers/cloud_p1/assets/images/small.png differ diff --git a/drivers/cloud_p1/device.js b/drivers/cloud_p1/device.js new file mode 100644 index 00000000..a4475761 --- /dev/null +++ b/drivers/cloud_p1/device.js @@ -0,0 +1,486 @@ +/* + * HomeWizard Cloud P1 Device Driver + * + * Based on HomeWizard Cloud API research and documentation by Sven Serlier + * Original repository: https://github.com/smarthomesven/homey-homewizard-energy-cloud + * + * Copyright (c) 2026 Jeroen Tebbens and contributors to com.homewizard + * Cloud API research (c) 2025 Sven Serlier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +'use strict'; + +const { Device } = require('homey'); +const HomeWizardCloudAPI = require('../../lib/homewizard-cloud-api'); + +const debug = false; + +class CloudP1Device extends Device { + + /** + * onInit is called when the device is initialized. + */ + async onInit() { + this.log('CloudP1Device has been initialized'); + + // Get device data + this.deviceId = this.getData().id; + this.settings = this.getSettings(); + + // Initialize cloud API client + this.cloudAPI = null; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 10; + + // Track last update to detect stale data + this.lastUpdate = null; + this.staleDataTimeout = null; + + // Update rate monitoring to prevent spam + this.updateCount = 0; + this.updateRateWindow = 10000; // 10 seconds + this.updateRateThreshold = 8; // More than 8 updates in 10s = too fast (< 1.25s average) + this.updateRateTimer = null; + this.spamDetected = false; + this.spamLogged = false; + + // Initialize capabilities if needed + await this.initializeCapabilities(); + + // Connect to cloud + await this.connectToCloud(); + + // Register capability listeners + this.registerCapabilityListeners(); + + this.log(`Cloud P1 device initialized: ${this.getName()} (${this.deviceId})`); + } + + /** + * Initialize device capabilities + */ + async initializeCapabilities() { + // Ensure all required capabilities exist + const requiredCapabilities = [ + 'measure_power', + 'meter_power', + 'meter_power.returned', + 'meter_power.peak', + 'meter_power.offpeak', + 'meter_power.producedPeak', + 'meter_power.producedOffpeak', + 'measure_voltage.l1', + 'measure_current.l1', + 'meter_gas' + ]; + + for (const capability of requiredCapabilities) { + if (!this.hasCapability(capability)) { + try { + await this.addCapability(capability); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log(`Capability already exists: ${capability} — ignoring`); + } else { + this.error(`Failed to add capability ${capability}:`, err); + } + } + } + } + + // Check if device has 3 phases based on settings + const threePhases = this.getSetting('number_of_phases') === 3; + + if (threePhases) { + const phaseCapabilities = [ + 'measure_power.l2', + 'measure_power.l3', + 'measure_voltage.l2', + 'measure_voltage.l3', + 'measure_current.l2', + 'measure_current.l3' + ]; + + for (const capability of phaseCapabilities) { + if (!this.hasCapability(capability)) { + try { + await this.addCapability(capability); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log(`Capability already exists: ${capability} — ignoring`); + } else { + this.error(`Failed to add capability ${capability}:`, err); + } + } + } + } + } + } + + /** + * Connect to HomeWizard cloud + */ + async connectToCloud() { + try { + const email = this.getSetting('cloud_email'); + const password = this.getSetting('cloud_password'); + + if (!email || !password) { + throw new Error('Cloud credentials not configured'); + } + + // Create cloud API instance + this.cloudAPI = new HomeWizardCloudAPI({ + email: email, + password: password + }); + + // Set up event listeners + this.setupCloudEventListeners(); + + // Authenticate + await this.cloudAPI.authenticate(); + this.log('Successfully authenticated with HomeWizard cloud'); + + // Connect to main WebSocket + await this.cloudAPI.connectMainWebSocket(); + + // Subscribe to this device + this.cloudAPI.subscribeToDevice(this.deviceId); + + // Note: Realtime WebSocket (1-second updates) is not used + // to avoid potential cloud-side issues and excessive update rates + + // Mark device as available + await this.setAvailable(); + this.reconnectAttempts = 0; + + // Set up stale data detection + this.setupStaleDataDetection(); + + } catch (error) { + this.error('Failed to connect to cloud:', error); + await this.setUnavailable(`Cloud connection failed: ${error.message}`); + + // Schedule reconnect + this.scheduleReconnect(); + } + } + + /** + * Set up cloud API event listeners + */ + setupCloudEventListeners() { + // Handle full device updates + this.cloudAPI.on('device_update', (deviceData) => { + if (deviceData.device === this.deviceId) { + this.log('[MAIN WS] Full device update received'); + this.handleDeviceUpdate(deviceData); + } + }); + + // Handle incremental updates (JSON patches) + this.cloudAPI.on('device_patch', (patchData) => { + if (patchData.deviceId === this.deviceId) { + if (debug) this.log('[MAIN WS] JSON patch received'); + this.handleDeviceUpdate(patchData.state); + } + }); + + // Handle connection issues + this.cloudAPI.on('mainws_closed', () => { + this.log('Main WebSocket closed'); + this.setWarning('Cloud connection lost, reconnecting...').catch(this.error); + }); + + this.cloudAPI.on('mainws_connected', () => { + this.log('Main WebSocket reconnected'); + this.unsetWarning().catch(this.error); + }); + + this.cloudAPI.on('mainws_error', (error) => { + this.error('Main WebSocket error:', error); + }); + } + + /** + * Handle device update from cloud + */ + handleDeviceUpdate(deviceData) { + try { + this.lastUpdate = Date.now(); + + // Monitor update rate and potentially unsubscribe if spam detected + //this.monitorUpdateRate(); + + const state = deviceData.state; + + if (!state) { + this.error('Device update missing state data'); + return; + } + + // Clear any warnings - we're receiving data successfully + this.unsetWarning().catch(this.error); + + // Update online status + if (deviceData.online !== undefined) { + if (deviceData.online) { + this.setAvailable(); + } else { + this.setUnavailable('Device is offline'); + } + } + + // Update power measurements + if (state.active_power_w !== null && state.active_power_w !== undefined) { + this.setCapabilityValue('measure_power', state.active_power_w).catch(this.error); + } + + // Update energy meters (import) + const tariff1 = state.total_power_import_t1_kwh || 0; + const tariff2 = state.total_power_import_t2_kwh || 0; + + this.setCapabilityValue('meter_power.peak', tariff1).catch(this.error); + this.setCapabilityValue('meter_power.offpeak', tariff2).catch(this.error); + this.setCapabilityValue('meter_power', tariff1 + tariff2).catch(this.error); + + // Update energy meters (export) + const exportTariff1 = state.total_power_export_t1_kwh || 0; + const exportTariff2 = state.total_power_export_t2_kwh || 0; + + this.setCapabilityValue('meter_power.producedPeak', exportTariff1).catch(this.error); + this.setCapabilityValue('meter_power.producedOffpeak', exportTariff2).catch(this.error); + this.setCapabilityValue('meter_power.returned', exportTariff1 + exportTariff2).catch(this.error); + + // Update voltage and current (L1) + if (state.active_voltage_l1_v !== null && state.active_voltage_l1_v !== undefined) { + this.setCapabilityValue('measure_voltage.l1', state.active_voltage_l1_v).catch(this.error); + } + + if (state.active_current_l1_a !== null && state.active_current_l1_a !== undefined) { + this.setCapabilityValue('measure_current.l1', state.active_current_l1_a).catch(this.error); + } + + // Update phase-specific measurements if 3-phase + if (this.getSetting('number_of_phases') === 3) { + // Phase 2 + if (state.active_power_l2_w !== null) { + this.setCapabilityValue('measure_power.l2', state.active_power_l2_w).catch(this.error); + } + if (state.active_voltage_l2_v !== null) { + this.setCapabilityValue('measure_voltage.l2', state.active_voltage_l2_v).catch(this.error); + } + if (state.active_current_l2_a !== null) { + this.setCapabilityValue('measure_current.l2', state.active_current_l2_a).catch(this.error); + } + + // Phase 3 + if (state.active_power_l3_w !== null) { + this.setCapabilityValue('measure_power.l3', state.active_power_l3_w).catch(this.error); + } + if (state.active_voltage_l3_v !== null) { + this.setCapabilityValue('measure_voltage.l3', state.active_voltage_l3_v).catch(this.error); + } + if (state.active_current_l3_a !== null) { + this.setCapabilityValue('measure_current.l3', state.active_current_l3_a).catch(this.error); + } + } + + // Update gas meter + if (state.total_gas_m3 !== null && state.total_gas_m3 !== undefined) { + this.setCapabilityValue('meter_gas', state.total_gas_m3).catch(this.error); + } + + // Store WiFi strength for diagnostics + if (deviceData.wifi_strength !== undefined) { + this.setSettings({ wifi_strength: deviceData.wifi_strength }).catch(this.error); + } + + if (debug) this.log('Device updated successfully'); + + } catch (error) { + this.error('Failed to handle device update:', error); + } + } + + /** + * Monitor update rate and stop spam + */ + monitorUpdateRate() { + // Increment update counter + this.updateCount++; + + // Start timer on first update + if (!this.updateRateTimer) { + this.updateRateTimer = setTimeout(() => { + // Check if we exceeded threshold + if (this.updateCount > this.updateRateThreshold && !this.spamDetected) { + this.spamDetected = true; + const updatesPerSecond = (this.updateCount / (this.updateRateWindow / 1000)).toFixed(2); + this.log(`⚠️ Excessive update rate detected: ${this.updateCount} updates in ${this.updateRateWindow/1000}s (${updatesPerSecond}/s)`); + this.log('Unsubscribing from device to stop spam. Will retry in 60 seconds...'); + + // Unsubscribe from this device to stop the updates + if (this.cloudAPI) { + this.cloudAPI.unsubscribeFromDevice(this.deviceId); + } + + // Set warning + this.setWarning('Excessive updates detected. Paused for 60 seconds.').catch(this.error); + + // Resubscribe after 60 seconds + setTimeout(() => { + this.log('Resubscribing to device after spam cooldown...'); + if (this.cloudAPI) { + // Re-add to subscribed devices set + this.cloudAPI.subscribedDevices.add(this.deviceId); + // Send subscribe message + this.cloudAPI._sendSubscribeMessage(this.deviceId); + } + this.spamDetected = false; + this.spamLogged = false; + this.unsetWarning().catch(this.error); + }, 60000); // 60 seconds + } + + // Reset counter + this.updateCount = 0; + this.updateRateTimer = null; + }, this.updateRateWindow); + } + } + + /** + * Set up stale data detection + */ + setupStaleDataDetection() { + // Clear existing timeout + if (this.staleDataTimeout) { + clearTimeout(this.staleDataTimeout); + } + + // Check for stale data every 3 minutes + this.staleDataTimeout = setInterval(() => { + const timeSinceUpdate = Date.now() - (this.lastUpdate || 0); + const maxStaleTime = 180000; // 3 minutes + + if (timeSinceUpdate > maxStaleTime) { + this.log('Data appears stale, marking device as unavailable'); + this.setUnavailable('No recent data from cloud'); + + // Forceer een harde WebSocket reset + if (this.cloudAPI && this.cloudAPI.mainWs) { + this.log('Data stale → forcing WebSocket reconnect'); + try { + this.cloudAPI.mainWs.terminate(); // hard close + } catch (err) { + this.error('Failed to terminate WS:', err); + } + } + } + + }, 60000); // Check every minute + } + + /** + * Schedule reconnection attempt + */ + scheduleReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + this.error('Max reconnect attempts reached'); + this.setUnavailable('Unable to connect to cloud after multiple attempts'); + return; + } + + const delay = Math.min(5000 * Math.pow(2, this.reconnectAttempts), 60000); + this.reconnectAttempts++; + + this.log(`Scheduling reconnect attempt ${this.reconnectAttempts} in ${delay}ms`); + + setTimeout(async () => { + await this.connectToCloud(); + }, delay); + } + + /** + * Register capability listeners + */ + registerCapabilityListeners() { + // Currently, P1 meters don't have controllable capabilities via cloud API + // This is a placeholder for future functionality + } + + /** + * onAdded is called when the user adds the device, called just after pairing. + */ + async onAdded() { + this.log('CloudP1Device has been added'); + } + + /** + * onSettings is called when the user updates the device's settings. + */ + async onSettings({ oldSettings, newSettings, changedKeys }) { + this.log('CloudP1Device settings were changed'); + + // If credentials changed, reconnect + if (changedKeys.includes('cloud_email') || changedKeys.includes('cloud_password')) { + this.log('Cloud credentials changed, reconnecting...'); + if (this.cloudAPI) { + this.cloudAPI.disconnect(); + } + await this.connectToCloud(); + } + } + + /** + * onRenamed is called when the user updates the device's name. + */ + async onRenamed(name) { + this.log('CloudP1Device was renamed to:', name); + } + + /** + * onUninit is called when the app stops/crashes + */ + async onUninit() { + // Clean up timers + if (this.staleDataTimeout) { + clearInterval(this.staleDataTimeout); + this.staleDataTimeout = null; + } + + if (this.updateRateTimer) { + clearTimeout(this.updateRateTimer); + this.updateRateTimer = null; + } + + if (this.cloudAPI) { + this.cloudAPI.disconnect(); + } + } + + /** + * onDeleted is called when the user deleted the device. + */ + async onDeleted() { + this.log('CloudP1Device has been deleted'); + + // Unsubscribe from device (only on explicit deletion) + if (this.cloudAPI) { + this.cloudAPI.unsubscribeFromDevice(this.deviceId); + } + + // Call onUninit to cleanup timers + await this.onUninit(); + } + +} + +module.exports = CloudP1Device; \ No newline at end of file diff --git a/drivers/cloud_p1/driver.compose.json b/drivers/cloud_p1/driver.compose.json new file mode 100644 index 00000000..45c2da97 --- /dev/null +++ b/drivers/cloud_p1/driver.compose.json @@ -0,0 +1,129 @@ +{ + "name": { + "en": "P1 Meter (Cloud)", + "nl": "P1 Meter (Cloud)" + }, + "class": "sensor", + + "capabilities": [], + + "capabilitiesOptions": { + "meter_power.peak": { + "title": { + "en": "Power meter tariff 1", + "nl": "Energiemeter tarief 1" + } + }, + "meter_power.offpeak": { + "title": { + "en": "Power meter tariff 2", + "nl": "Energiemeter tarief 2" + } + }, + "meter_power.producedPeak": { + "title": { + "en": "Production tariff 1", + "nl": "Productie tarief 1" + } + }, + "meter_power.producedOffpeak": { + "title": { + "en": "Production tariff 2", + "nl": "Productie tarief 2" + } + }, + "meter_power.returned": { + "title": { + "en": "Returned Power", + "nl": "Teruggeleverde Energie" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Voltage L1", + "nl": "Spanning L1" + } + }, + "measure_current.l1": { + "title": { + "en": "Current L1", + "nl": "Stroom L1" + } + } + }, + + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power", + "cumulativeExportedCapability": "meter_power.returned" + }, + + "platforms": ["local"], + "connectivity": ["cloud"], + + "images": { + "small": "{{driverAssetsPath}}/images/small.png", + "large": "{{driverAssetsPath}}/images/large.png", + "xlarge": "{{driverAssetsPath}}/images/xlarge.png" + }, + + "pair": [ + { "id": "login" } + ], + + "repair": [ + { "id": "login" } + ], + + "settings": [ + { + "type": "group", + "label": { + "en": "Cloud Connection", + "nl": "Cloud Verbinding" + }, + "children": [ + { + "id": "cloud_email", + "type": "text", + "label": { "en": "Email", "nl": "E-mail" }, + "value": "", + "hint": { + "en": "Your HomeWizard Energy account email", + "nl": "Uw HomeWizard Energy account e-mail" + } + }, + { + "id": "cloud_password", + "type": "password", + "label": { "en": "Password", "nl": "Wachtwoord" }, + "value": "", + "hint": { + "en": "Your HomeWizard Energy account password", + "nl": "Uw HomeWizard Energy account wachtwoord" + } + }, + { + "id": "location_id", + "type": "text", + "label": { "en": "Location ID", "nl": "Locatie ID" }, + "value": "", + "hint": { + "en": "Internal location identifier", + "nl": "Interne locatie-identificatie" + } + }, + { + "id": "location_name", + "type": "text", + "label": { "en": "Location Name", "nl": "Locatie Naam" }, + "value": "", + "hint": { + "en": "Name of your home in HomeWizard Energy app", + "nl": "Naam van uw woning in HomeWizard Energy app" + } + } + ] + } + ] +} diff --git a/drivers/cloud_p1/driver.js b/drivers/cloud_p1/driver.js new file mode 100644 index 00000000..713078ce --- /dev/null +++ b/drivers/cloud_p1/driver.js @@ -0,0 +1,264 @@ +/* + * HomeWizard Cloud P1 Driver + * + * Based on HomeWizard Cloud API research and documentation by Sven Serlier + * Original repository: https://github.com/smarthomesven/homey-homewizard-energy-cloud + * + * Copyright (c) 2026 Jeroen Tebbens and contributors to com.homewizard + * Cloud API research (c) 2025 Sven Serlier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +'use strict'; + +const { Driver } = require('homey'); +const HomeWizardCloudAPI = require('../../lib/homewizard-cloud-api'); + +class CloudP1Driver extends Driver { + + /** + * onInit is called when the driver is initialized. + */ + async onInit() { + this.log('CloudP1Driver has been initialized'); + } + + /** + * onPairListDevices is called when the user starts pairing + */ + async onPairListDevices() { + this.log('onPairListDevices called'); + + // Return empty array - devices will be discovered through pair flow + return []; + } + + /** + * onInit is called when the driver is initialized. + */ + async onInit() { + this.log('CloudP1Driver has been initialized'); + + // Store active pairing sessions at driver level + this.pairingSessions = new Map(); + } + + /** + * onPair is called when a user wants to pair a device + */ + async onPair(session) { + this.log('Pairing session started'); + + // Create a unique session ID + const sessionId = Date.now().toString(); + + // Store session data at driver level so it persists across views + const sessionData = { + cloudAPI: null, + locations: [], + credentials: { + email: null, + password: null + } + }; + + this.pairingSessions.set(sessionId, sessionData); + this.log(`Created pairing session: ${sessionId}`); + + // TEST HANDLER - to verify emit is working + session.setHandler('test', async () => { + this.log('TEST HANDLER CALLED - emit is working!'); + return { success: true, message: 'Test successful' }; + }); + + // Step 1: Get cloud credentials + session.setHandler('cloud_login', async (data) => { + try { + const { email, password } = data; + + if (!email || !password) { + throw new Error('Email and password are required'); + } + + const sd = this.pairingSessions.get(sessionId); + + // Store credentials for later use + sd.credentials.email = email; + sd.credentials.password = password; + + // Create cloud API instance + sd.cloudAPI = new HomeWizardCloudAPI({ + email: email, + password: password + }); + + // Authenticate + await sd.cloudAPI.authenticate(); + this.log('Successfully authenticated'); + + return { success: true }; + + } catch (error) { + this.error('Cloud login failed:', error); + throw new Error(`Authentication failed: ${error.message}`); + } + }); + + // Step 2: Get locations (homes) + session.setHandler('list_locations', async () => { + try { + this.log('list_locations handler called'); + + const sd = this.pairingSessions.get(sessionId); + + if (!sd || !sd.cloudAPI) { + this.error('CloudAPI not initialized'); + throw new Error('Not authenticated. Please login first.'); + } + + this.log('Fetching locations from cloud...'); + sd.locations = await sd.cloudAPI.getLocations(); + this.log(`Found ${sd.locations.length} location(s)`); + + // Format locations for display + const formattedLocations = sd.locations.map(location => ({ + id: location.id.toString(), + name: location.name, + location: location.location, + deviceCount: location.devices ? location.devices.length : 0 + })); + + this.log('Formatted locations:', JSON.stringify(formattedLocations, null, 2)); + return formattedLocations; + + } catch (error) { + this.error('Failed to get locations:', error); + this.error('Error stack:', error.stack); + throw new Error(`Failed to retrieve locations: ${error.message}`); + } + }); + + // Step 3: Get devices for selected location + session.setHandler('list_devices_for_location', async (data) => { + try { + const { locationId } = data; + + const sd = this.pairingSessions.get(sessionId); + + if (!sd || !sd.cloudAPI) { + throw new Error('Not authenticated. Please login first.'); + } + + const location = sd.locations.find(loc => loc.id.toString() === locationId); + + if (!location) { + throw new Error('Location not found'); + } + + // Filter for P1 dongles only + const p1Devices = (location.devices || []).filter(device => + device.type === 'p1dongle' + ); + + this.log(`Found ${p1Devices.length} P1 device(s) in location ${location.name}`); + + // Format devices for display + return p1Devices.map(device => ({ + name: device.name || 'P1 Meter', + data: { + id: device.device_id + }, + settings: { + cloud_email: sd.credentials.email, + cloud_password: sd.credentials.password, + location_id: locationId, + location_name: location.name, + number_of_phases: 1 + }, + store: { + device_type: device.type, + created: device.created, + modified: device.modified + } + })); + + } catch (error) { + this.error('Failed to get devices:', error); + throw new Error(`Failed to retrieve devices: ${error.message}`); + } + }); + + // Clean up on pair session disconnect + session.setHandler('disconnect', async () => { + this.log('Pairing session ended'); + + const sd = this.pairingSessions.get(sessionId); + if (sd && sd.cloudAPI) { + sd.cloudAPI.disconnect(); + } + + // Clean up session data + this.pairingSessions.delete(sessionId); + this.log(`Cleaned up pairing session: ${sessionId}`); + }); + } + + /** + * onRepair is called when a user wants to repair a device + */ + async onRepair(session, device) { + this.log('Repair session started for device:', device.getName()); + + let cloudAPI = null; + + // Step 1: Get new cloud credentials + session.setHandler('cloud_login', async (data) => { + try { + const { email, password } = data; + + if (!email || !password) { + throw new Error('Email and password are required'); + } + + // Create cloud API instance + cloudAPI = new HomeWizardCloudAPI({ + email: email, + password: password + }); + + // Authenticate + await cloudAPI.authenticate(); + this.log('Successfully authenticated during repair'); + + // Update device settings + await device.setSettings({ + cloud_email: email, + cloud_password: password + }); + + return { success: true }; + + } catch (error) { + this.error('Cloud login failed during repair:', error); + throw new Error(`Authentication failed: ${error.message}`); + } + }); + + // Clean up on repair session disconnect + session.setHandler('disconnect', async () => { + this.log('Repair session ended'); + + if (cloudAPI) { + cloudAPI.disconnect(); + cloudAPI = null; + } + }); + } + +} + +module.exports = CloudP1Driver; \ No newline at end of file diff --git a/drivers/cloud_p1/pair/list_locations.html b/drivers/cloud_p1/pair/list_locations.html new file mode 100644 index 00000000..e52c2362 --- /dev/null +++ b/drivers/cloud_p1/pair/list_locations.html @@ -0,0 +1,315 @@ + + + + + + + + +
+

Select Your Home

+

Choose which home contains the devices you want to add.

+
+ +
+
+
Loading your homes...
+ + + + +
+ +
+ + + + \ No newline at end of file diff --git a/drivers/cloud_p1/pair/login.html b/drivers/cloud_p1/pair/login.html new file mode 100644 index 00000000..550a3156 --- /dev/null +++ b/drivers/cloud_p1/pair/login.html @@ -0,0 +1,279 @@ + + + + + + + +
+

HomeWizard Cloud Login

+

Enter your HomeWizard Energy account credentials.

+ +
+ +
+
+ + +
+ +
+ + +
+ + +
+
+ + +
+
+ Select your home +
+ +

You have no homes! Please create a home in the HomeWizard Energy app first.

+

Failed to load homes. Please try again.

+ + +
+ + + + \ No newline at end of file diff --git a/drivers/cloud_watermeter/assets/icon.svg b/drivers/cloud_watermeter/assets/icon.svg new file mode 100644 index 00000000..e0f8ed06 --- /dev/null +++ b/drivers/cloud_watermeter/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/cloud_watermeter/assets/images/large.png b/drivers/cloud_watermeter/assets/images/large.png new file mode 100644 index 00000000..66a529d6 Binary files /dev/null and b/drivers/cloud_watermeter/assets/images/large.png differ diff --git a/drivers/cloud_watermeter/assets/images/small.png b/drivers/cloud_watermeter/assets/images/small.png new file mode 100644 index 00000000..2646976e Binary files /dev/null and b/drivers/cloud_watermeter/assets/images/small.png differ diff --git a/drivers/cloud_watermeter/device.js b/drivers/cloud_watermeter/device.js new file mode 100644 index 00000000..3aec7a66 --- /dev/null +++ b/drivers/cloud_watermeter/device.js @@ -0,0 +1,361 @@ +'use strict'; + +const Homey = require('homey'); +const fetch = require('node-fetch'); + +const API_BASE_URL = 'https://api.homewizardeasyonline.com/v1'; +const TSDB_URL = 'https://tsdb-reader.homewizard.com'; +const TOKEN_REFRESH_MARGIN = 60; // seconds +const MAX_RETRY_ATTEMPTS = 5; +const INITIAL_RETRY_DELAY = 30000; // 30 seconds +const debug = false + +module.exports = class HomeWizardCloudWatermeterDevice extends Homey.Device { + + async onInit() { + this.log('Cloud Watermeter initialized:', this.getName()); + + // Get stored credentials + this.username = this.getStoreValue('username'); + this.password = this.getStoreValue('password'); + this.token = this.getStoreValue('token'); + this.tokenExpiresAt = this.getStoreValue('token_expires_at'); + this.deviceIdentifier = this.getStoreValue('identifier'); + this.homeId = this.getStoreValue('homeId'); + + // Set polling interval (every 15 minutes) + this.pollInterval = this.getSetting('poll_interval') || 900; // 15 minutes default + + // Initialize retry tracking + this.retryAttempt = 0; + + // Initialize cumulative meter if not exists + if (!this.getStoreValue('cumulative_water')) { + await this.setStoreValue('cumulative_water', 0); + } + + // Track last processed date to detect day changes + if (!this.getStoreValue('last_date')) { + await this.setStoreValue('last_date', new Date().toDateString()); + } + + // Add meter_water.daily capability if it doesn't exist + // Safe add: guard against race / 409 errors + if (!this.hasCapability('meter_water.daily')) { + try { + await this.addCapability('meter_water.daily'); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log('Capability already exists: meter_water.daily — ignoring'); + } else { + throw err; + } + } + } + + // Initial data fetch + await this.fetchWaterData(); + + // Start polling + this.startPolling(); + + this.log(`Device initialized: ${this.deviceIdentifier}`); + } + + /** + * Start polling for water data + */ + startPolling() { + if (this.pollingTimer) { + this.homey.clearInterval(this.pollingTimer); + } + + this.pollingTimer = this.homey.setInterval( + async () => { + await this.fetchWaterData(); + }, + this.pollInterval * 1000 + ); + } + + /** + * Calculate exponential backoff delay + * @param {number} attempt - Current retry attempt (0-based) + * @returns {number} Delay in milliseconds + */ + calculateBackoffDelay(attempt) { + // Exponential backoff 30s, 60s, 120s, 240s, etc. + const delay = INITIAL_RETRY_DELAY * Math.pow(2, attempt); + // Add jitter (random 0-20% variation) to avoid thundering herd + const jitter = delay * 0.2 * Math.random(); + return Math.floor(delay + jitter); + } + + /** + * Sleep for specified milliseconds + * @param {number} ms - Milliseconds to sleep + */ + sleep(ms) { + return new Promise(resolve => this.homey.setTimeout(resolve, ms)); + } + + /** + * Ensure we have a valid token, refresh if needed + * @returns {Promise} Valid access token + */ + async ensureToken() { + const now = Date.now(); + + if (!this.token || now >= this.tokenExpiresAt) { + this.log('Token expired or missing, refreshing...'); + await this.authenticate(); + } + + return this.token; + } + + /** + * Authenticate and get new token + */ + async authenticate() { + const url = `${API_BASE_URL}/auth/account/token`; + const credentials = Buffer.from(`${this.username}:${this.password}`).toString('base64'); + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Basic ${credentials}`, + 'User-Agent': 'HomeWizardHomey/1.0', + }, + timeout: 10000, + }); + + if (!response.ok) { + throw new Error(`Authentication failed: ${response.status}`); + } + + const data = await response.json(); + + this.token = data.access_token; + this.tokenExpiresAt = Date.now() + ((data.expires_in || 3600) - TOKEN_REFRESH_MARGIN) * 1000; + + // Store for persistence + await this.setStoreValue('token', this.token); + await this.setStoreValue('token_expires_at', this.tokenExpiresAt); + + this.log('Token refreshed successfully'); + } catch (err) { + this.error('Authentication failed:', err.message); + await this.setUnavailable(this.homey.__('errors.auth_failed')); + throw err; + } + } + + /** + * Fetch water consumption data from TSDB with exponential backoff + */ + async fetchWaterData() { + let attempt = 0; + + while (attempt < MAX_RETRY_ATTEMPTS) { + try { + const token = await this.ensureToken(); + const now = new Date(); + const timezone = this.homey.clock.getTimezone(); + + const url = `${TSDB_URL}/devices/date/${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`; + + const payload = { + devices: [ + { + identifier: this.deviceIdentifier, + measurementType: 'water', + }, + ], + type: 'water', + values: true, + wattage: false, + gb: '15m', + tz: timezone, + fill: 'linear', + three_phases: false, + }; + + if (attempt > 0) { + this.log(`Fetching water data from TSDB (retry ${attempt}/${MAX_RETRY_ATTEMPTS})...`); + } else { + if (debug) this.log(`Fetching water data from TSDB...`); + } + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'HomeWizardHomey/1.0', + }, + body: JSON.stringify(payload), + timeout: 10000, + }); + + if (!response.ok) { + const errorText = await response.text(); + + // Check if it's a retryable error (5xx or rate limiting) + if (response.status >= 500 || response.status === 429) { + throw new Error(`TSDB request failed (retryable): ${response.status} - ${errorText}`); + } + + // Non-retryable error (4xx except 429) + this.error(`TSDB request failed (non-retryable): ${response.status} - ${errorText}`); + await this.setUnavailable(`API error: ${response.status}`); + return; + } + + const data = await response.json(); + if (debug) this.log(`TSDB data received: ${data.values?.length || 0} datapoints`); + + // Process the data + await this.processWaterData(data); + + // Mark device as available + if (!this.getAvailable()) { + await this.setAvailable(); + } + + // Reset retry counter on success + this.retryAttempt = 0; + return; + + } catch (err) { + this.error(`Error fetching water data (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}):`, err.message); + + attempt++; + + if (attempt >= MAX_RETRY_ATTEMPTS) { + this.error('Max retry attempts reached, giving up'); + await this.setUnavailable(err.message); + return; + } + + // Calculate backoff delay + const delay = this.calculateBackoffDelay(attempt - 1); + this.log(`Retrying in ${(delay / 1000).toFixed(1)}s...`); + + // Wait before retry + await this.sleep(delay); + } + } + } + + /** + * Process and update water consumption data + * @param {Object} data - TSDB response data + */ +async processWaterData(data) { + if (!data || !data.values || data.values.length === 0) { + this.log('No water data available'); + return; + } + + const today = new Date().toDateString(); + const lastDate = this.getStoreValue('last_date'); + const previousDailyUsage = this.getStoreValue('previous_daily_usage') || 0; + + // Find the latest non-zero water value + let latestWaterValue = null; + let dailyTotal = 0; + + // Iterate through values to find the most recent reading and calculate daily total + for (let i = data.values.length - 1; i >= 0; i--) { + const datapoint = data.values[i]; + + if (datapoint.water !== null && datapoint.water !== undefined) { + // Sum up all water usage for the day (these are liters per interval) + dailyTotal += datapoint.water; + + // Get the latest reading if we haven't found one yet + if (latestWaterValue === null && datapoint.water > 0) { + latestWaterValue = datapoint.water; + if (debug) this.log(`Latest water reading: ${latestWaterValue}L at ${datapoint.time}`); + } + } + } + + // Convert daily total from liters to m³ + const dailyTotalM3 = dailyTotal / 1000; + + if (debug) this.log(`Daily water usage: ${dailyTotalM3.toFixed(3)} m³ (${dailyTotal.toFixed(1)}L)`); + + // Check if day changed - if so, add previous day's total to cumulative + if (lastDate !== today) { + if (debug) this.log(`Day changed from ${lastDate} to ${today}`); + + // Add previous day's usage to cumulative total + const cumulativeWater = this.getStoreValue('cumulative_water') || 0; + const newCumulative = cumulativeWater + previousDailyUsage; + + await this.setStoreValue('cumulative_water', newCumulative); + await this.setStoreValue('last_date', today); + + if (debug) this.log(`Added ${previousDailyUsage.toFixed(3)} m³ to cumulative. New total: ${newCumulative.toFixed(3)} m³`); + } + + // Store current daily usage for next day rollover + await this.setStoreValue('previous_daily_usage', dailyTotalM3); + + // Update daily water usage capability + if (this.hasCapability('meter_water.daily')) { + await this.setCapabilityValue('meter_water.daily', dailyTotalM3); + if (debug) this.log(`Daily water meter updated: ${dailyTotalM3.toFixed(3)} m³`); + } + + // Update cumulative water usage capability (including manual offset) + if (this.hasCapability('meter_water')) { + const cumulativeWater = this.getStoreValue('cumulative_water') || 0; + const manualOffset = parseFloat(this.getSetting('manual_offset')) || 0; + const totalWater = cumulativeWater + dailyTotalM3 + manualOffset; + + await this.setCapabilityValue('meter_water', totalWater); + if (debug) this.log(`Cumulative water meter updated: ${totalWater.toFixed(3)} m³ (cumulative: ${cumulativeWater.toFixed(3)} m³, daily: ${dailyTotalM3.toFixed(3)} m³, offset: ${manualOffset.toFixed(3)} m³)`); + } +} + + /** + * Handle settings changes + */ + async onSettings({ oldSettings, newSettings, changedKeys }) { + if (changedKeys.includes('poll_interval')) { + this.pollInterval = newSettings.poll_interval; + this.log(`Polling interval changed to ${this.pollInterval}s`); + this.startPolling(); // Restart with new interval + } + + if (changedKeys.includes('manual_offset')) { + this.log(`Manual offset changed to ${newSettings.manual_offset} m³`); + // Trigger an update to reflect the new offset + await this.fetchWaterData(); + } + } + + /** + * Clean up on device deletion + */ + async onDeleted() { + this.log('Cloud Watermeter deleted'); + + if (this.pollingTimer) { + this.homey.clearInterval(this.pollingTimer); + } + } + + /** + * Clean up on device unavailable + */ + async onUninit() { + if (this.pollingTimer) { + this.homey.clearInterval(this.pollingTimer); + } + } +}; \ No newline at end of file diff --git a/drivers/cloud_watermeter/driver.compose.json b/drivers/cloud_watermeter/driver.compose.json new file mode 100644 index 00000000..0423bf9e --- /dev/null +++ b/drivers/cloud_watermeter/driver.compose.json @@ -0,0 +1,102 @@ +{ + "id": "cloud-watermeter", + "name": { + "en": "Watermeter (cloud)", + "nl": "Watermeter (cloud)" + }, + "class": "sensor", + "platforms": [ "local"], + "capabilities": [ + "meter_water", + "meter_water.daily" + ], + "capabilitiesOptions": { + "meter_water.daily": { + "decimals": 3, + "title": { + "en": "Water usage today", + "nl": "Waterverbruik vandaag" + } + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Water usage total", + "nl": "Waterverbruik totaal" + } + } + }, + "energy": { + "cumulative": true + }, + "images": { + "small": "{{driverAssetsPath}}/images/small.png", + "large": "{{driverAssetsPath}}/images/large.png", + "xlarge": "{{driverAssetsPath}}/images/xlarge.png" + }, + "pair": [ + { + "id": "login", + "template": "login_credentials", + "options": { + "title": { + "en": "Login to HomeWizard", + "nl": "Inloggen bij HomeWizard" + }, + "usernameLabel": { + "en": "Email", + "nl": "E-mail" + }, + "usernamePlaceholder": { + "en": "your@email.com", + "nl": "jouw@email.com" + }, + "passwordLabel": { + "en": "Password", + "nl": "Wachtwoord" + }, + "passwordPlaceholder": { + "en": "Password", + "nl": "Wachtwoord" + } + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "settings": [ + { + "type": "group", + "label": { + "en": "General settings", + "nl": "Algemene instellingen" + }, + "children": [ + { + "id": "poll_interval", + "type": "number", + "label": { + "en": "Polling interval (seconds)", + "nl": "Polling interval (seconden)" + }, + "value": 900, + "min": 900, + "max": 3600, + "hint": { + "en": "How often to fetch data from HomeWizard Cloud (default: 900s / 15 min)", + "nl": "Hoe vaak data ophalen van HomeWizard Cloud (standaard: 900s / 15 min)" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/drivers/cloud_watermeter/driver.js b/drivers/cloud_watermeter/driver.js new file mode 100644 index 00000000..cdc05525 --- /dev/null +++ b/drivers/cloud_watermeter/driver.js @@ -0,0 +1,291 @@ +'use strict'; + +const Homey = require('homey'); +const fetch = require('node-fetch'); + +const API_BASE_URL = 'https://api.homewizardeasyonline.com/v1'; +const HOMES_API_URL = 'https://homes.api.homewizard.com'; +const GRAPHQL_URL = 'https://api.homewizard.energy/v1/graphql'; +const TSDB_URL = 'https://tsdb-reader.homewizard.com'; +const TOKEN_REFRESH_MARGIN = 60; // seconds before expiry to refresh + +module.exports = class HomeWizardCloudWatermeterDriver extends Homey.Driver { + + async onInit() { + this.log('HomeWizard Cloud Watermeter driver initialized'); + } + + /** + * Authenticate with HomeWizard Cloud API + * @param {string} username - HomeWizard account email + * @param {string} password - HomeWizard account password + * @returns {Promise} Token data with access_token and expires_in + */ + async authenticate(username, password) { + const url = `${API_BASE_URL}/auth/account/token`; + + const credentials = Buffer.from(`${username}:${password}`).toString('base64'); + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Basic ${credentials}`, + 'User-Agent': 'HomeWizardHomey/1.0', + }, + timeout: 10000, + }); + + if (!response.ok) { + throw new Error(`Authentication failed: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + + return { + access_token: data.access_token, + expires_at: Date.now() + ((data.expires_in || 3600) - TOKEN_REFRESH_MARGIN) * 1000, + }; + } catch (err) { + this.error('Authentication error:', err.message); + throw new Error(this.homey.__('errors.auth_failed')); + } + } + + /** + * Get list of locations (homes) for the account + * @param {string} token - Bearer token + * @returns {Promise} List of locations + */ + async getLocations(token) { + const url = `${HOMES_API_URL}/locations`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'HomeWizardHomey/1.0', + }, + timeout: 10000, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch locations: ${response.status}`); + } + + return await response.json(); + } catch (err) { + this.error('Error fetching locations:', err.message); + return []; + } + } + + /** + * Fetch devices for a specific home using GraphQL (renamed to avoid + * overriding Homey.Driver.getDevices()). + * @param {string} token - Bearer token + * @param {number} homeId - Home ID + * @returns {Promise} GraphQL response with devices + */ + async fetchDevicesForHome(token, homeId) { + const payload = { + operationName: 'DeviceList', + variables: { + homeId: homeId, + }, + query: `query DeviceList($homeId: Int!) { + home(id: $homeId) { + devices { + identifier + name + wifiStrength + ... on CloudDevice { + type + model + hardwareVersion + onlineState + } + } + } + }`, + }; + + return await this.callGraphQL(token, payload); + } + + /** + * Call GraphQL endpoint + * @param {string} token - Bearer token + * @param {Object} payload - GraphQL query payload + * @returns {Promise} GraphQL response + */ + async callGraphQL(token, payload) { + try { + const response = await fetch(GRAPHQL_URL, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'HomeWizardHomey/1.0', + }, + body: JSON.stringify(payload), + timeout: 10000, + }); + + if (!response.ok) { + throw new Error(`GraphQL request failed: ${response.status}`); + } + + return await response.json(); + } catch (err) { + this.error('GraphQL error:', err.message); + return null; + } + } + + /** + * Get time-series database data for water measurements + * @param {string} token - Bearer token + * @param {string} deviceIdentifier - Device identifier + * @param {Date} date - Date to fetch data for + * @param {string} timezone - Timezone string (e.g., 'Europe/Amsterdam') + * @returns {Promise} TSDB data + */ + async getTSDBData(token, deviceIdentifier, date, timezone = 'Europe/Amsterdam') { + const dateStr = date.toISOString().split('T')[0].replace(/-/g, '/'); + const url = `${TSDB_URL}/devices/date/${dateStr}`; + + const payload = { + devices: [ + { + identifier: deviceIdentifier, + measurementType: 'water', + }, + ], + type: 'water', + values: true, + wattage: true, + gb: '15m', + tz: timezone, + fill: 'linear', + three_phases: false, + }; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'HomeWizardHomey/1.0', + }, + body: JSON.stringify(payload), + timeout: 10000, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`TSDB request failed: ${response.status} - ${text}`); + } + + return await response.json(); + } catch (err) { + this.error('TSDB error:', err.message); + return null; + } + } + + /** + * Pairing flow - authenticate and discover devices + */ + async onPair(session) { + let username = ''; + let password = ''; + let tokenData = null; + + session.setHandler('login', async (data) => { + username = data.username; + password = data.password; + + try { + // Authenticate + tokenData = await this.authenticate(username, password); + this.log('Authentication successful'); + return true; + } catch (err) { + this.error('Login failed:', err.message); + throw new Error(this.homey.__('errors.invalid_credentials')); + } + }); + + session.setHandler('list_devices', async () => { + if (!tokenData) { + throw new Error('Not authenticated'); + } + + const devices = await this.discoverDevices(tokenData, username, password); + return devices; + }); + } + + /** + * Discover watermeter devices + */ + async discoverDevices(tokenData, username, password) { + try { + this.log('Fetching locations...'); + const locations = await this.getLocations(tokenData.access_token); + + if (!locations || locations.length === 0) { + return []; + } + + const devices = []; + + for (const location of locations) { + this.log(`Fetching devices for location: ${location.id} (${location.name || 'unnamed'})`); + const devicesData = await this.fetchDevicesForHome(tokenData.access_token, location.id); + + if (devicesData?.data?.home?.devices) { + this.log(`Found ${devicesData.data.home.devices.length} total devices`); + + const watermeters = devicesData.data.home.devices.filter( + device => device.type === 'watermeter' || device.model?.includes('WTR') + ); + + this.log(`Filtered to ${watermeters.length} watermeter(s)`); + + for (const device of watermeters) { + devices.push({ + name: device.name || `Watermeter (${device.identifier})`, + data: { + id: device.identifier, + }, + store: { + username: username, + password: password, + token: tokenData.access_token, + token_expires_at: tokenData.expires_at, + identifier: device.identifier, + homeId: location.id, + }, + }); + } + } + } + + if (devices.length === 0) { + return []; + } + + this.log(`Returning ${devices.length} watermeter(s) for pairing`); + return devices; + } catch (err) { + this.error('Device discovery failed:', err.message); + this.error('Stack trace:', err.stack); + throw err; + } + } +}; \ No newline at end of file diff --git a/drivers/cloud_watermeter/driver.settings.compose.json b/drivers/cloud_watermeter/driver.settings.compose.json new file mode 100644 index 00000000..0f3c1e98 --- /dev/null +++ b/drivers/cloud_watermeter/driver.settings.compose.json @@ -0,0 +1,39 @@ +[ + { + "type": "group", + "label": { + "en": "General settings", + "nl": "Algemene instellingen" + }, + "children": [ + { + "id": "poll_interval", + "type": "number", + "label": { + "en": "Polling interval (seconds)", + "nl": "Polling interval (seconden)" + }, + "value": 900, + "min": 300, + "max": 3600, + "hint": { + "en": "How often to fetch data from HomeWizard Cloud (default: 900s / 15 min)", + "nl": "Hoe vaak data ophalen van HomeWizard Cloud (standaard: 900s / 15 min)" + } + }, + { + "id": "manual_offset", + "type": "number", + "label": { + "en": "Manual offset (m³)", + "nl": "Handmatige correctie (m³)" + }, + "value": 0, + "hint": { + "en": "Add or subtract from the total meter reading. Use this to match your water company's meter reading.", + "nl": "Tel op of trek af van de totale meterstand. Gebruik dit om overeen te komen met de meterstand van je waterbedrijf." + } + } + ] + } +] \ No newline at end of file diff --git a/drivers/cloud_watermeter/pair/login.html b/drivers/cloud_watermeter/pair/login.html new file mode 100644 index 00000000..37498de3 --- /dev/null +++ b/drivers/cloud_watermeter/pair/login.html @@ -0,0 +1,145 @@ + + + + + + + + +
+ Enter your HomeWizard Energy account credentials to connect your cloud-based watermeter. +
+ +
+
+ + +
+ +
+ + +
+ + + +
+
+ + + + \ No newline at end of file diff --git a/drivers/energy/assets/icon.svg b/drivers/energy/assets/icon.svg new file mode 100644 index 00000000..e1bc3269 --- /dev/null +++ b/drivers/energy/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/energy/assets/images/large.png b/drivers/energy/assets/images/large.png new file mode 100644 index 00000000..6358f0b7 Binary files /dev/null and b/drivers/energy/assets/images/large.png differ diff --git a/drivers/energy/assets/images/small.png b/drivers/energy/assets/images/small.png new file mode 100644 index 00000000..721d7b06 Binary files /dev/null and b/drivers/energy/assets/images/small.png differ diff --git a/drivers/energy/device.js b/drivers/energy/device.js new file mode 100644 index 00000000..67f42111 --- /dev/null +++ b/drivers/energy/device.js @@ -0,0 +1,1306 @@ +'use strict'; + +const Homey = require('homey'); +const fetchWithTimeout = require('../../includes/utils/fetchWithTimeout'); +const BaseloadMonitor = require('../../includes/utils/baseloadMonitor'); +const http = require('http'); + + +// All phase‑dependent capabilities (L2/L3/T3) +const PHASE_CAPS = [ + 'measure_power.l2', 'measure_power.l3', + 'measure_voltage.l2', 'measure_voltage.l3', + 'measure_current.l2', 'measure_current.l3', + 'net_load_phase2_pct', 'net_load_phase3_pct', + 'voltage_sag_l2', 'voltage_sag_l3', + 'voltage_swell_l2', 'voltage_swell_l3', + 'meter_power.consumed.t3', 'meter_power.produced.t3' +]; + + + + +async function updateCapability(device, capability, value) { + try { + const current = device.getCapabilityValue(capability); + + // --- SAFE REMOVE --- + // Removal is allowed only when: + // 1) the new value is null + // 2) the current value in Homey is also null + + if (value == null && current == null) { + if (device.hasCapability(capability)) { + await device.removeCapability(capability); + device.log(`🗑️ Removed capability "${capability}"`); + } + return; + } + + // --- ADD IF MISSING --- + if (!device.hasCapability(capability)) { + try { + await device.addCapability(capability); + device.log(`➕ Added capability "${capability}"`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + } else { + throw err; + } + } + } + + // --- UPDATE --- + if (current !== value) { + await device.setCapabilityValue(capability, value); + } + + } catch (err) { + if (err.message === 'device_not_found') { + device.log(`⚠️ Skipping capability "${capability}" — device not found`); + return; + } + device.error(`❌ Failed updateCapability("${capability}")`, err); + } +} + + +/** + * Safe add capability helper — avoids race 409 errors + */ +async function safeAddCapability(device, capability) { + try { + if (!device.hasCapability(capability)) { + await device.addCapability(capability); + device.log(`➕ Safely added capability "${capability}"`); + } + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + return; + } + throw err; + } +} + + + +function getWifiQuality(percent) { + if (percent >= 80) return 'Excellent / Strong'; + if (percent >= 60) return 'Moderate'; + if (percent >= 40) return 'Weak'; + if (percent >= 20) return 'Poor'; + if (percent > 0) return 'Unusable'; + return 'Unusable'; +} + +module.exports = class HomeWizardEnergyDevice extends Homey.Device { + + async onInit() { + this._lastSamples = {}; // mini-cache + this._deleted = false; + this._pollErrorCount = 0; + + this.agent = new http.Agent({ + keepAlive: true, + keepAliveMsecs: 10000 + }); + + // Get effective URL (manual IP overrides discovery) + this.url = this._getEffectiveURL(); + + await updateCapability(this, 'connection_error', 'No errors'); + await updateCapability(this, 'alarm_connectivity', false); + + // Remove legacy capabilities once + for (const cap of ['net_load_phase1', 'net_load_phase2', 'net_load_phase3']) { + if (this.hasCapability(cap)) { + await this.removeCapability(cap).catch(this.error); + } + } + + const settings = this.getSettings(); + + this._overloadThreshold = settings.phase_overload_threshold ?? 97; + this._overloadReset = settings.phase_overload_reset ?? 85; + + if (!settings.polling_interval) { + await this.setSettings({ polling_interval: 10 }); + } + + if (settings.phase_capacity == null) { + await this.setSettings({ phase_capacity: 40 }); + } + + if (settings.number_of_phases == null) { + await this.setSettings({ number_of_phases: 1 }); + } + + if (settings.show_gas === undefined || settings.show_gas === null) { + await this.setSettings({ show_gas: true }); + } + + // Initial phase count (user setting or autodetect later) + this._phases = Number(this.getSettings().number_of_phases) || 1; + + // Clean slate: if 1 phase → remove all L2/L3/T3 + if (this._phases === 1) { + for (const cap of PHASE_CAPS) { + if (this.hasCapability(cap)) { + await this.removeCapability(cap).catch(this.error); + } + } + } + + // If 3 phases → ensure all L2/L3/T3 exist + if (this._phases === 3) { + for (const cap of PHASE_CAPS) { + if (!this.hasCapability(cap)) { + await safeAddCapability(this, cap).catch(this.error); + } + } + } + + // Autodetect counter for 1 → 3 phases promotion + this._phaseDetectCount = 0; + + // Gas capabilities are settings-driven, not payload-driven + if (!settings.show_gas) { + for (const cap of ['meter_gas', 'measure_gas', 'meter_gas.daily']) { + if (this.hasCapability(cap)) { + await this.removeCapability(cap).catch(this.error); + } + } + } + + const interval = Math.max(this.getSettings().polling_interval || 10, 2); + const offset = Math.floor(Math.random() * interval * 1000); + + if (this.onPollInterval) clearInterval(this.onPollInterval); + + // First poll offset + setTimeout(() => { + if (this._deleted) return; + this.onPoll().catch(this.error); + + // Daarna vaste interval zonder lock + this.onPollInterval = setInterval(() => { + if (!this._deleted) { + this.onPoll().catch(this.error); + } + }, interval * 1000); + + }, offset); + + + this._flowTriggerTariff = this.homey.flow.getDeviceTriggerCard('tariff_changed'); + this._flowTriggerImport = this.homey.flow.getDeviceTriggerCard('import_changed'); + this._flowTriggerExport = this.homey.flow.getDeviceTriggerCard('export_changed'); + this._flowTriggerVoltageRestored = this.homey.flow.getDeviceTriggerCard('voltage_restored_v1'); + this._flowTriggerPowerRestored = this.homey.flow.getDeviceTriggerCard('power_restored_v1'); + + // Track voltage state for restoration detection + this._voltageState = { + l1: { abnormal: false, lastAbnormalTime: null }, + l2: { abnormal: false, lastAbnormalTime: null }, + l3: { abnormal: false, lastAbnormalTime: null } + }; + + // Track power state for restoration detection + this._powerState = { + offline: false, + offlineStartTime: null + }; + + this.registerCapabilityListener('identify', async () => { + await this.onIdentify(); + }); + + // Baseload monitor wiring + this._baseloadNotificationsEnabled = this.getSetting('baseload_notifications') ?? true; + this._phaseOverloadNotificationsEnabled = this.getSetting('phase_overload_notifications') ?? true; + + this._phaseOverloadState = { + l1: { highCount: 0, notified: false }, + l2: { highCount: 0, notified: false }, + l3: { highCount: 0, notified: false }, + }; + + const app = this.homey.app; + if (!app.baseloadMonitor) { + app.baseloadMonitor = new BaseloadMonitor(this.homey); + } + + app.baseloadMonitor.registerP1Device(this); + app.baseloadMonitor.trySetMaster(this); + app.baseloadMonitor.setNotificationsEnabledForDevice(this, this._baseloadNotificationsEnabled); + } + + // mini-cache helper + _hasChanged(key, value) { + const prev = this._lastSamples[key]; + if (prev === value) return false; + this._lastSamples[key] = value; + return true; + } + + onDeleted() { + this._deleted = true; + + const app = this.homey.app; + if (app.baseloadMonitor) { + app.baseloadMonitor.unregisterP1Device(this); + } + + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + } + + flowTriggerTariff(device, tokens) { + this._flowTriggerTariff.trigger(device, tokens).catch(this.error); + } + + flowTriggerImport(device, tokens) { + this._flowTriggerImport.trigger(device, tokens).catch(this.error); + } + + flowTriggerExport(device, tokens) { + this._flowTriggerExport.trigger(device, tokens).catch(this.error); + } + + _onNewPowerValue(power) { + const app = this.homey.app; + if (app.baseloadMonitor) { + let batteryPower = null; + try { + const battDriver = this.homey.drivers.getDriver('plugin_battery'); + if (battDriver) { + let total = 0; + for (const dev of battDriver.getDevices()) { + total += dev.getCapabilityValue('measure_power') || 0; + } + if (total !== 0) batteryPower = total; + } + } catch (_) {} + app.baseloadMonitor.updatePowerFromDevice(this, power, batteryPower); + } + } + + async onIdentify() { + if (!this.url) return; + + try { + const res = await fetchWithTimeout(`${this.url}/identify`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!res || !res.ok) { + await updateCapability(this, 'connection_error', res ? String(res.status) : 'fetch failed'); + await updateCapability(this, 'alarm_connectivity', true); + throw new Error(res ? res.statusText : 'Unknown error during fetch'); + } + + } catch (err) { + this.error(err); + throw new Error('Network error during onIdentify'); + } + } + +/** + * Get effective URL - manual IP overrides discovery + * @returns {string} URL to use for API calls + */ +_getEffectiveURL() { + const manualIP = this.getSetting('manual_ip'); + if (manualIP) { + this.log(`🔧 Using manual IP: ${manualIP}`); + // Energy v1 uses http and port 80 with /api path + return `http://${manualIP}/api`; + } + + const settings = this.getSettings(); + if (settings.url) { + return settings.url; + } + + return null; +} + +/** + * Reconnect with manual IP after repair flow + * @param {string} ip - The manual IP address + */ +async reconnectWithManualIP(ip) { + this.log(`🔧 Reconnecting with manual IP: ${ip}`); + this.url = `http://${ip}/api`; + // Energy v1 uses polling, will reconnect on next poll automatically + this.log('🔁 Manual IP set, will use on next poll cycle'); +} + +onDiscoveryAvailable(discoveryResult) { + if (this._deleted) return; + + // Check if manual IP is set - if so, ignore discovery + const manualIP = this.getSetting('manual_ip'); + if (manualIP) { + this.log(`🌐 Discovery: Manual IP (${manualIP}) is set — ignoring discovery`); + return; + } + + if (!discoveryResult?.address || !discoveryResult?.port || !discoveryResult?.txt?.path) { + this.log('Invalid discovery result'); + return; + } + + const newUrl = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + + // Only update if the URL actually changed + if (this.url !== newUrl) { + this.url = newUrl; + this.log(`Discovered device URL: ${this.url}`); + } + + this.setAvailable(); +} + + +onDiscoveryAddressChanged(discoveryResult) { + if (this._deleted) return; + + // Check if manual IP is set - if so, ignore discovery + const manualIP = this.getSetting('manual_ip'); + if (manualIP) { + this.log(`🌐 AddressChanged: Manual IP (${manualIP}) is set — ignoring discovery`); + return; + } + + const newUrl = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + + // Only update if the URL actually changed + if (this.url !== newUrl) { + this.url = newUrl; + this.log(`URL updated: ${this.url}`); + this._debugLog(`Discovery address changed: ${this.url}`); + } +} + + +onDiscoveryLastSeenChanged(discoveryResult) { + if (this._deleted) return; + + const newUrl = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + + // Only update if the URL actually changed + if (this.url !== newUrl) { + this.url = newUrl; + this.log(`URL restored: ${this.url}`); + } + + this.setAvailable(); +} + + + async setCloudOn() { + if (!this.url) return; + + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: true }) + }); + + if (!res || !res.ok) { + await updateCapability(this, 'connection_error', res ? `HTTP ${res.status}` : 'fetch failed'); + await updateCapability(this, 'alarm_connectivity', true); + throw new Error(res ? res.statusText : 'Unknown error during fetch'); + } + + } catch (err) { + this.error(err); + throw new Error('Network error during setCloudOn'); + } + } + + async setCloudOff() { + if (!this.url) return; + + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: false }) + }); + + if (!res || !res.ok) { + await updateCapability(this, 'connection_error', res ? `HTTP ${res.status}` : 'fetch failed'); + await updateCapability(this, 'alarm_connectivity', true); + throw new Error(res ? res.statusText : 'Unknown error during fetch'); + } + + } catch (err) { + this.error(err); + throw new Error('Network error during setCloudOff'); + } + } + + /** + * Debug logger (batched writes) + */ + _debugLog(msg) { + try { + if (!this._debugBuffer) this._debugBuffer = []; + const ts = new Date().toLocaleString('nl-NL', { hour12: false, timeZone: 'Europe/Amsterdam' }); + const driverName = this.driver.id; + const deviceName = this.getName(); + const safeMsg = typeof msg === 'string' ? msg : (msg instanceof Error ? msg.message : JSON.stringify(msg)); + const line = `${ts} [${driverName}] [${deviceName}] ${safeMsg}`; + this._debugBuffer.push(line); + if (this._debugBuffer.length > 20) this._debugBuffer.shift(); + if (!this._debugFlushTimeout) { + this._debugFlushTimeout = setTimeout(() => { + this._flushDebugLogs(); + this._debugFlushTimeout = null; + }, 5000); + } + } catch (err) { + this.error('Failed to write debug logs:', err.message || err); + } +} +_flushDebugLogs() { + if (!this._debugBuffer || this._debugBuffer.length === 0) return; + try { + const logs = this.homey.settings.get('debug_logs') || []; + logs.push(...this._debugBuffer); + if (logs.length > 500) logs.splice(0, logs.length - 500); + this.homey.settings.set('debug_logs', logs); + this._debugBuffer = []; + } catch (err) { + this.error('Failed to flush debug logs:', err.message || err); + } +} + +async onPoll() { + if (this._deleted) return; + + const settings = this.getSettings(); + + // --- EARLY RETURN SAFE --- + if (!await this._prepareUrl(settings)) { + return; + } + + // + // --- BACKOFF DURING ERRORS --- + // + if (this._pollErrorCount > 0) { + const delayMs = Math.min(60000, this._pollErrorCount * 2000); // max 60s + this._debugLog(`Backoff active: waiting ${delayMs}ms due to ${this._pollErrorCount} errors`); + await new Promise(r => setTimeout(r, delayMs)); + } + + let data, nowLocal, homeyLang; + + // + // --- FETCH DATA --- + // + try { + const t = this._getLocalTimeAndLang(); + nowLocal = t.nowLocal; + homeyLang = t.homeyLang; + data = await this._fetchData(); + + // Succes → reset error counter + this._pollErrorCount = 0; + + } catch (err) { + this._pollErrorCount++; + this._handlePollError(err); + return; + } + + // + // ⚡ ELECTRICITY FIRST + // + try { + const tasks = []; + + this._processCorePowerAndWifi(data, tasks); + await this._processTariffAndFlows(data, tasks); + this._processExportAndNetImport(data, tasks); + await this._processImportExportFlows(data, tasks); + this._processBelgiumMonthlyPeak(data, tasks); + this._processPhase1MetricsAndOverload(data, tasks, settings, homeyLang); + this._processPhases2And3(data, tasks, settings, homeyLang); + this._processT3ImportExport(data, tasks); + this._processUrlSync(tasks, settings); + + await Promise.allSettled(tasks); + + // Check for voltage and power restoration + this._checkVoltageRestoration(data); + this._checkPowerRestoration(data); + + await updateCapability(this, 'connection_error', 'No errors'); + await updateCapability(this, 'alarm_connectivity', false); + await this.setAvailable(); + + } catch (err) { + this.error('Electricity processing failed:', err); + } + + // + // 💧 GAS/WATER + // + try { + this._processGasSourceSelection(data); + + const gasTasks = []; + + await this._processMidnightDailyReset(data, gasTasks, settings, nowLocal); + this._processExternalWater(data, gasTasks); + this._processGasLiveValue(data, gasTasks, settings); + this._processGasDelta(data, gasTasks, settings, nowLocal); + this._processDailyTotals(data, gasTasks, settings); + + await Promise.allSettled(gasTasks); + + } catch (err) { + this.error('Gas/Water processing failed:', err); + } +} + + + + async _prepareUrl(settings) { + if (!this.url) { + if (settings.url) { + this.url = settings.url; + this.log(`Restored URL from settings: ${this.url}`); + } else { + //await this.setUnavailable('Missing URL'); + this._debugLog(`Missing URL in settings for device "${this.getName()}"`); + this.log('Polling skipped: missing URL in settings'); + updateCapability(this, 'alarm_connectivity', true).catch(this.error); + return false; + } + } + return true; + } + + _getLocalTimeAndLang() { + const tz = this.homey.clock.getTimezone(); + const now = new Date(); + const iso = now.toLocaleString('sv-SE', { timeZone: tz }); + const nowLocal = new Date(iso); + const homeyLang = this.homey.i18n.getLanguage(); + return { now, nowLocal, homeyLang }; + } + + async _fetchData() { + const res = await fetchWithTimeout(`${this.url}/data`, { + agent: this.agent, + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (!res || !res.ok) { + await updateCapability(this, 'connection_error', 'Fetch error'); + await updateCapability(this, 'alarm_connectivity', true); + throw new Error(res ? res.statusText : 'Unknown error during fetch'); + } + + let text; + let data; + + try { + text = await res.text(); + data = JSON.parse(text); + } catch (err) { + this.error('JSON parse error:', err.message, 'Body:', text?.slice(0, 200)); + throw new Error('Invalid JSON'); + } + + if (!data || typeof data !== 'object') { + throw new Error('Invalid JSON'); + } + + return data; + } + + _processGasSourceSelection(data) { + let gasValue = null; + let gasTimestamp = null; + + if (Array.isArray(data.external)) { + const gasMeters = data.external + .filter(e => e.type === 'gas_meter' && e.value != null && e.timestamp != null); + + if (gasMeters.length > 0) { + gasMeters.sort((a, b) => b.timestamp - a.timestamp); + gasValue = gasMeters[0].value; + gasTimestamp = gasMeters[0].timestamp; + } + } + + if (gasValue == null && data.total_gas_m3 != null) { + gasValue = data.total_gas_m3; + gasTimestamp = data.gas_timestamp; + } + + data._gasValue = gasValue; + data._gasTimestamp = gasTimestamp; + } + + async _processPhaseAutodetect(data, tasks) { + const hasRealL2 = typeof data.active_current_l2_a === 'number' && data.active_current_l2_a !== 0; + const hasRealL3 = typeof data.active_current_l3_a === 'number' && data.active_current_l3_a !== 0; + + if (this._phases === 1 && (hasRealL2 || hasRealL3)) { + this._phaseDetectCount++; + + if (this._phaseDetectCount >= 5) { + this._phases = 3; + await this.setSettings({ number_of_phases: 3 }).catch(this.error); + + for (const cap of PHASE_CAPS) { + if (!this.hasCapability(cap)) { + await safeAddCapability(this, cap).catch(this.error); + } + } + + this.log('Autodetect: promoted to 3 phases'); + } + } else { + this._phaseDetectCount = 0; + } + } + + async _processMidnightDailyReset(data, tasks, settings, nowLocal) { + // Format today's date as YYYY-MM-DD + const today = nowLocal.toISOString().slice(0, 10); + + // Read last reset date + const lastReset = await this.getStoreValue('last_reset_date'); + + // First run or new day → perform reset + if (lastReset !== today) { + + // Reset electricity baseline + if (data.total_power_import_kwh !== undefined) { + tasks.push( + this.setStoreValue('meter_start_day', data.total_power_import_kwh) + .catch(this.error) + ); + } + + // Reset gas baseline + if (settings.show_gas && data._gasValue !== undefined) { + tasks.push( + this.setStoreValue('gasmeter_start_day', data._gasValue) + ); + } + + // Store today's date so we don't reset again + tasks.push( + this.setStoreValue('last_reset_date', today) + ); + + return; + } + + // If baseline missing (e.g. after reinstall), initialize it + const meterStartDay = await this.getStoreValue('meter_start_day'); + if (!meterStartDay && data.total_power_import_kwh !== undefined) { + tasks.push( + this.setStoreValue('meter_start_day', data.total_power_import_kwh) + .catch(this.error) + ); + } + + if (settings.show_gas) { + const gasStartDay = await this.getStoreValue('gasmeter_start_day'); + if (!gasStartDay && data._gasValue !== undefined) { + tasks.push( + this.setStoreValue('gasmeter_start_day', data._gasValue) + ); + } + } +} + + +async _processGasDelta(data, tasks, settings, nowLocal) { + if (!settings.show_gas || (nowLocal.getMinutes() % 5 !== 0)) return; + + try { + const prevTs = await this.getStoreValue('gasmeter_previous_reading_timestamp'); + + if (prevTs == null) { + tasks.push( + this.setStoreValue('gasmeter_previous_reading_timestamp', data._gasTimestamp) + .catch(err => this.error('Store error (ts init):', err.message)) + ); + return; + } + + if (data._gasValue != null && prevTs !== data._gasTimestamp) { + const prevReading = await this.getStoreValue('gasmeter_previous_reading'); + + if (prevReading != null) { + const gasDelta = data._gasValue - prevReading; + + // Minimum delta of 0.01 to avoid noise, and only update if changed since last time + if (gasDelta >= 0.01 && this._hasChanged('measure_gas_delta', gasDelta)) { + tasks.push(updateCapability(this, 'measure_gas', gasDelta)); + } + } + + tasks.push( + this.setStoreValue('gasmeter_previous_reading', data._gasValue) + .catch(err => this.error('Store error (reading):', err.message)) + ); + + tasks.push( + this.setStoreValue('gasmeter_previous_reading_timestamp', data._gasTimestamp) + .catch(err => this.error('Store error (ts update):', err.message)) + ); + } + } catch (err) { + this.error('Unhandled gas delta error:', err.message); + } +} + + + async _processDailyTotals(data, tasks, settings) { + const meterStart = await this.getStoreValue('meter_start_day'); + if (meterStart != null && data.total_power_import_kwh != null) { + const dailyImport = data.total_power_import_kwh - meterStart; + if (this._hasChanged('meter_power.daily', dailyImport)) { + tasks.push(updateCapability(this, 'meter_power.daily', dailyImport)); + } + } + + if (settings.show_gas) { + const gasStart = await this.getStoreValue('gasmeter_start_day'); + if (data._gasValue != null && gasStart != null) { + const gasDiff = data._gasValue - gasStart; + if (this._hasChanged('meter_gas.daily', gasDiff)) { + tasks.push(updateCapability(this, 'meter_gas.daily', gasDiff)); + } + } + } + } + + _processCorePowerAndWifi(data, tasks) { + if (this._hasChanged('measure_power', data.active_power_w)) { + tasks.push(updateCapability(this, 'measure_power', data.active_power_w)); + this._onNewPowerValue(data.active_power_w); + } + + if (this._hasChanged('rssi', data.wifi_strength)) { + tasks.push(updateCapability(this, 'rssi', data.wifi_strength)); + } + + if (this._hasChanged('tariff', data.active_tariff)) { + tasks.push(updateCapability(this, 'tariff', data.active_tariff)); + } + + tasks.push(updateCapability(this, 'identify', 'identify')); + + if (this._hasChanged('meter_power.consumed.t1', data.total_power_import_t1_kwh)) { + tasks.push(updateCapability(this, 'meter_power.consumed.t1', data.total_power_import_t1_kwh)); + } + if (this._hasChanged('meter_power.consumed.t2', data.total_power_import_t2_kwh)) { + tasks.push(updateCapability(this, 'meter_power.consumed.t2', data.total_power_import_t2_kwh)); + } + if (this._hasChanged('meter_power.consumed', data.total_power_import_kwh)) { + tasks.push(updateCapability(this, 'meter_power.consumed', data.total_power_import_kwh)); + } + + const wifiQuality = getWifiQuality(data.wifi_strength); + if (this._hasChanged('wifi_quality', wifiQuality)) { + tasks.push(updateCapability(this, 'wifi_quality', wifiQuality)); + } + } + + async _processTariffAndFlows(data, tasks) { + const lastTariff = await this.getStoreValue('last_active_tariff'); + const currentTariff = data.active_tariff; + if (typeof currentTariff === 'number' && currentTariff !== lastTariff) { + this.flowTriggerTariff(this, { tariff_changed: currentTariff }); + tasks.push(this.setStoreValue('last_active_tariff', currentTariff).catch(this.error)); + } + } + + _processGasLiveValue(data, tasks, settings) { + if (settings.show_gas && data._gasValue != null && this._hasChanged('meter_gas', data._gasValue)) { + tasks.push(updateCapability(this, 'meter_gas', data._gasValue)); + } + } + + _processExportAndNetImport(data, tasks) { + if (data.total_power_export_kwh > 1 || data.total_power_export_t2_kwh > 1) { + if (this._hasChanged('meter_power.produced.t1', data.total_power_export_t1_kwh)) { + tasks.push(updateCapability(this, 'meter_power.produced.t1', data.total_power_export_t1_kwh)); + } + if (this._hasChanged('meter_power.produced.t2', data.total_power_export_t2_kwh)) { + tasks.push(updateCapability(this, 'meter_power.produced.t2', data.total_power_export_t2_kwh)); + } + } + + const netImport = data.total_power_import_kwh === undefined + ? (data.total_power_import_t1_kwh + data.total_power_import_t2_kwh) - + (data.total_power_export_t1_kwh + data.total_power_export_t2_kwh) + : data.total_power_import_kwh - data.total_power_export_kwh; + + if (this._hasChanged('meter_power', netImport)) { + tasks.push(updateCapability(this, 'meter_power', netImport)); + } + + if (data.total_power_import_kwh !== undefined && + this._hasChanged('meter_power.returned', data.total_power_export_kwh)) { + tasks.push(updateCapability(this, 'meter_power.returned', data.total_power_export_kwh)); + } + } + + async _processImportExportFlows(data, tasks) { + const lastImport = await this.getStoreValue('last_total_import_kwh'); + const currentImport = data.total_power_import_kwh; + if (typeof currentImport === 'number' && currentImport !== lastImport) { + this.flowTriggerImport(this, { import_changed: currentImport }); + tasks.push(this.setStoreValue('last_total_import_kwh', currentImport).catch(this.error)); + } + + const lastExport = await this.getStoreValue('last_total_export_kwh'); + const currentExport = data.total_power_export_kwh; + if (typeof currentExport === 'number' && currentExport !== lastExport) { + this.flowTriggerExport(this, { export_changed: currentExport }); + tasks.push(this.setStoreValue('last_total_export_kwh', currentExport).catch(this.error)); + } + } + + _processBelgiumMonthlyPeak(data, tasks) { + if (this._hasChanged('measure_power.montly_power_peak', data.montly_power_peak_w)) { + tasks.push(updateCapability(this, 'measure_power.montly_power_peak', data.montly_power_peak_w)); + } + } + + _processPhase1MetricsAndOverload(data, tasks, settings, homeyLang) { + if (data.active_voltage_l1_v !== undefined && + this._hasChanged('measure_voltage.l1', data.active_voltage_l1_v)) { + tasks.push(updateCapability(this, 'measure_voltage.l1', data.active_voltage_l1_v)); + } + if (data.active_current_l1_a !== undefined && + this._hasChanged('measure_current.l1', data.active_current_l1_a)) { + tasks.push(updateCapability(this, 'measure_current.l1', data.active_current_l1_a)); + } + // Legacy measure_current (mirror of L1) + if (data.active_current_l1_a !== undefined && + this._hasChanged('measure_current', data.active_current_l1_a)) { + tasks.push(updateCapability(this, 'measure_current', data.active_current_l1_a)); + } + + if (data.active_power_l1_w !== undefined && + this._hasChanged('measure_power.l1', data.active_power_l1_w)) { + tasks.push(updateCapability(this, 'measure_power.l1', data.active_power_l1_w)); + } + + if (data.long_power_fail_count !== undefined && + this._hasChanged('long_power_fail_count', data.long_power_fail_count)) { + tasks.push(updateCapability(this, 'long_power_fail_count', data.long_power_fail_count)); + + // Trigger flow card for long power failure + this.homey.flow.getDeviceTriggerCard('long_power_fail_detected_v1') + .trigger(this, { count: data.long_power_fail_count }) + .catch(this.error); + } + + if (data.voltage_sag_l1_count !== undefined && + this._hasChanged('voltage_sag_l1', data.voltage_sag_l1_count)) { + tasks.push(updateCapability(this, 'voltage_sag_l1', data.voltage_sag_l1_count)); + + // Trigger flow card for voltage sag + this.homey.flow.getDeviceTriggerCard('voltage_sag_detected_v1') + .trigger(this, { + phase_l1: data.voltage_sag_l1_count || 0, + phase_l2: this.getCapabilityValue('voltage_sag_l2') || 0, + phase_l3: this.getCapabilityValue('voltage_sag_l3') || 0 + }) + .catch(this.error); + } + + if (data.voltage_swell_l1_count !== undefined && + this._hasChanged('voltage_swell_l1', data.voltage_swell_l1_count)) { + tasks.push(updateCapability(this, 'voltage_swell_l1', data.voltage_swell_l1_count)); + + // Trigger flow card for voltage swell + this.homey.flow.getDeviceTriggerCard('voltage_swell_detected_v1') + .trigger(this, { + phase_l1: data.voltage_swell_l1_count || 0, + phase_l2: this.getCapabilityValue('voltage_swell_l2') || 0, + phase_l3: this.getCapabilityValue('voltage_swell_l3') || 0 + }) + .catch(this.error); + } + + if (data.active_current_l1_a !== undefined) { + const load1 = Math.abs((data.active_current_l1_a / settings.phase_capacity) * 100); + if (this._hasChanged('net_load_phase1_pct', load1)) { + tasks.push(updateCapability(this, 'net_load_phase1_pct', load1)); + this._handlePhaseOverload('l1', load1, homeyLang); + } + } + } + + _processPhases2And3(data, tasks, settings, homeyLang) { + if (this._phases === 3 && (data.active_current_l2_a !== undefined || data.active_current_l3_a !== undefined)) { + + if (data.voltage_sag_l2_count !== undefined && + this._hasChanged('voltage_sag_l2', data.voltage_sag_l2_count)) { + tasks.push(updateCapability(this, 'voltage_sag_l2', data.voltage_sag_l2_count)); + + // Trigger flow card for voltage sag + this.homey.flow.getDeviceTriggerCard('voltage_sag_detected_v1') + .trigger(this, { + phase_l1: this.getCapabilityValue('voltage_sag_l1') || 0, + phase_l2: data.voltage_sag_l2_count || 0, + phase_l3: this.getCapabilityValue('voltage_sag_l3') || 0 + }) + .catch(this.error); + } + if (data.voltage_sag_l3_count !== undefined && + this._hasChanged('voltage_sag_l3', data.voltage_sag_l3_count)) { + tasks.push(updateCapability(this, 'voltage_sag_l3', data.voltage_sag_l3_count)); + + // Trigger flow card for voltage sag + this.homey.flow.getDeviceTriggerCard('voltage_sag_detected_v1') + .trigger(this, { + phase_l1: this.getCapabilityValue('voltage_sag_l1') || 0, + phase_l2: this.getCapabilityValue('voltage_sag_l2') || 0, + phase_l3: data.voltage_sag_l3_count || 0 + }) + .catch(this.error); + } + if (data.voltage_swell_l2_count !== undefined && + this._hasChanged('voltage_swell_l2', data.voltage_swell_l2_count)) { + tasks.push(updateCapability(this, 'voltage_swell_l2', data.voltage_swell_l2_count)); + + // Trigger flow card for voltage swell + this.homey.flow.getDeviceTriggerCard('voltage_swell_detected_v1') + .trigger(this, { + phase_l1: this.getCapabilityValue('voltage_swell_l1') || 0, + phase_l2: data.voltage_swell_l2_count || 0, + phase_l3: this.getCapabilityValue('voltage_swell_l3') || 0 + }) + .catch(this.error); + } + if (data.voltage_swell_l3_count !== undefined && + this._hasChanged('voltage_swell_l3', data.voltage_swell_l3_count)) { + tasks.push(updateCapability(this, 'voltage_swell_l3', data.voltage_swell_l3_count)); + + // Trigger flow card for voltage swell + this.homey.flow.getDeviceTriggerCard('voltage_swell_detected_v1') + .trigger(this, { + phase_l1: this.getCapabilityValue('voltage_swell_l1') || 0, + phase_l2: this.getCapabilityValue('voltage_swell_l2') || 0, + phase_l3: data.voltage_swell_l3_count || 0 + }) + .catch(this.error); + } + + if (data.active_power_l2_w !== undefined && + this._hasChanged('measure_power.l2', data.active_power_l2_w)) { + tasks.push(updateCapability(this, 'measure_power.l2', data.active_power_l2_w)); + } + if (data.active_power_l3_w !== undefined && + this._hasChanged('measure_power.l3', data.active_power_l3_w)) { + tasks.push(updateCapability(this, 'measure_power.l3', data.active_power_l3_w)); + } + + if (data.active_voltage_l2_v !== undefined && + this._hasChanged('measure_voltage.l2', data.active_voltage_l2_v)) { + tasks.push(updateCapability(this, 'measure_voltage.l2', data.active_voltage_l2_v)); + } + if (data.active_voltage_l3_v !== undefined && + this._hasChanged('measure_voltage.l3', data.active_voltage_l3_v)) { + tasks.push(updateCapability(this, 'measure_voltage.l3', data.active_voltage_l3_v)); + } + + if (data.active_current_l2_a !== undefined) { + const load2 = Math.abs((data.active_current_l2_a / settings.phase_capacity) * 100); + if (this._hasChanged('measure_current.l2', data.active_current_l2_a)) { + tasks.push(updateCapability(this, 'measure_current.l2', data.active_current_l2_a)); + } + if (this._hasChanged('net_load_phase2_pct', load2)) { + tasks.push(updateCapability(this, 'net_load_phase2_pct', load2)); + this._handlePhaseOverload('l2', load2, homeyLang); + } + } + + if (data.active_current_l3_a !== undefined) { + const load3 = Math.abs((data.active_current_l3_a / settings.phase_capacity) * 100); + if (this._hasChanged('measure_current.l3', data.active_current_l3_a)) { + tasks.push(updateCapability(this, 'measure_current.l3', data.active_current_l3_a)); + } + if (this._hasChanged('net_load_phase3_pct', load3)) { + tasks.push(updateCapability(this, 'net_load_phase3_pct', load3)); + this._handlePhaseOverload('l3', load3, homeyLang); + } + } + } + } + + _processT3ImportExport(data, tasks) { + if (this._phases === 3) { + if (this._hasChanged('meter_power.consumed.t3', data.total_power_import_t3_kwh)) { + tasks.push(updateCapability(this, 'meter_power.consumed.t3', data.total_power_import_t3_kwh)); + } + if (this._hasChanged('meter_power.produced.t3', data.total_power_export_t3_kwh)) { + tasks.push(updateCapability(this, 'meter_power.produced.t3', data.total_power_export_t3_kwh)); + } + } + } + + _processExternalWater(data, tasks) { + const externalData = data.external; + if (Array.isArray(externalData)) { + const latestWater = externalData.reduce((prev, current) => { + if (current.type === 'water_meter') { + return !prev || current.timestamp > prev.timestamp ? current : prev; + } + return prev; + }, null); + + if (latestWater && latestWater.value != null && + this._hasChanged('meter_water', latestWater.value)) { + tasks.push(updateCapability(this, 'meter_water', latestWater.value)); + } + } + } + + _processUrlSync(tasks, settings) { + if (this.url !== settings.url) { + this.log(`Energy - Updating settings url from ${settings.url} → ${this.url}`); + tasks.push(this.setSettings({ url: this.url }).catch(this.error)); + } + } + +_handlePollError(err) { + const msg = err.message || 'Polling error'; + + // Capability updates alleen bij de eerste fout of elke 10 fouten + if (this._pollErrorCount === 1 || this._pollErrorCount % 10 === 0) { + updateCapability(this, 'connection_error', msg).catch(this.error); + updateCapability(this, 'alarm_connectivity', true).catch(this.error); + } + + // Logging beperken: alleen elke 5 fouten loggen + if (this._pollErrorCount % 5 === 1) { + this.log(`Poll failed (${this._pollErrorCount}): ${msg}`); + } + + // Debug log alleen bij eerste fout + if (this._pollErrorCount === 1) { + this._debugLog(`Poll failed: ${msg}`); + } +} + + + _handlePhaseOverload(phaseKey, loadPct, lang) { + if (!this._phaseOverloadNotificationsEnabled) return; + + const state = this._phaseOverloadState[phaseKey]; + if (!state) return; + + const threshold = this._overloadThreshold ?? 97; + const reset = this._overloadReset ?? 85; + + if (loadPct > threshold) { + state.highCount++; + + if (!state.notified && state.highCount >= 3) { + const phaseNum = phaseKey.replace('l', ''); + const msg = lang === 'nl' + ? `Fase ${phaseNum} overbelast (${loadPct.toFixed(0)}%)` + : `Phase ${phaseNum} overloaded (${loadPct.toFixed(0)}%)`; + + this.homey.notifications.createNotification({ excerpt: msg }).catch(this.error); + state.notified = true; + } + + } else if (loadPct < reset) { + state.highCount = 0; + state.notified = false; + } + } + + async onSettings(event) { + const { newSettings, changedKeys } = event; + this.log('Settings updated', changedKeys); + + for (const key of changedKeys) { + + if (key === 'polling_interval') { + const interval = newSettings.polling_interval; + if (typeof interval === 'number' && interval > 0) { + if (this.onPollInterval) clearInterval(this.onPollInterval); + this.onPollInterval = setInterval(this.onPoll.bind(this), interval * 1000); + } else { + this.log('Invalid polling interval:', interval); + } + } + + if (key === 'cloud') { + try { + if (newSettings.cloud == 1) await this.setCloudOn(); + else await this.setCloudOff(); + } catch (err) { + this.error('Failed to update cloud connection:', err); + } + } + + if (key === 'baseload_notifications') { + this._baseloadNotificationsEnabled = newSettings.baseload_notifications; + const app = this.homey.app; + if (app.baseloadMonitor) { + app.baseloadMonitor.setNotificationsEnabledForDevice(this, this._baseloadNotificationsEnabled); + } + this.log('Baseload notifications changed to:', this._baseloadNotificationsEnabled); + } + + if (key === 'phase_overload_notifications') { + this._phaseOverloadNotificationsEnabled = newSettings.phase_overload_notifications; + this.log('Phase overload notifications changed to:', this._phaseOverloadNotificationsEnabled); + } + + if (key === 'show_gas') { + const showGas = newSettings.show_gas; + if (!showGas) { + for (const cap of ['meter_gas', 'measure_gas', 'meter_gas.daily']) { + if (this.hasCapability(cap)) { + await this.removeCapability(cap).catch(this.error); + } + } + } + } + + if (key === 'phase_overload_threshold') { + this._overloadThreshold = newSettings.phase_overload_threshold; + this.log('Phase overload threshold changed to:', this._overloadThreshold); + } + + if (key === 'phase_overload_reset') { + this._overloadReset = newSettings.phase_overload_reset; + this.log('Phase overload reset changed to:', this._overloadReset); + } + + if (key === 'number_of_phases') { + // Manual override: keep capabilities in sync with explicit phase setting + this._phases = newSettings.number_of_phases; + + if (this._phases === 1) { + for (const cap of PHASE_CAPS) { + if (this.hasCapability(cap)) { + await this.removeCapability(cap).catch(this.error); + } + } + } + + if (this._phases === 3) { + for (const cap of PHASE_CAPS) { + if (!this.hasCapability(cap)) { + await safeAddCapability(this, cap).catch(this.error); + } + } + } + } + + } + } + + /** + * Check if voltage has been restored to normal range after sag/swell + * @param {Object} data - measurement data + */ + _checkVoltageRestoration(data) { + if (!this._voltageState || !this._flowTriggerVoltageRestored) return; + + // Voltage normal range (230V ±10% = 207-253V) + const VOLTAGE_MIN = 207; + const VOLTAGE_MAX = 253; + + const phases = [ + { name: 'l1', voltage: data.active_voltage_l1_v }, + { name: 'l2', voltage: data.active_voltage_l2_v }, + { name: 'l3', voltage: data.active_voltage_l3_v } + ]; + + phases.forEach(({ name, voltage }) => { + if (voltage == null) return; + + const state = this._voltageState[name]; + const isNormal = voltage >= VOLTAGE_MIN && voltage <= VOLTAGE_MAX; + + // Detect restoration: was abnormal, now normal + if (state.abnormal && isNormal) { + const phaseName = name.toUpperCase(); + this.log(`Voltage restored on ${phaseName}: ${voltage}V`); + + this._flowTriggerVoltageRestored.trigger(this, { + phase: phaseName, + voltage: Math.round(voltage) + }).catch(this.error); + + state.abnormal = false; + state.lastAbnormalTime = null; + } + // Track abnormal state + else if (!state.abnormal && !isNormal) { + state.abnormal = true; + state.lastAbnormalTime = Date.now(); + } + }); + } + + /** + * Check if power has been restored after being offline + * @param {Object} data - measurement data + */ + _checkPowerRestoration(data) { + if (!this._powerState || !this._flowTriggerPowerRestored) return; + + // Consider online if we have active power reading or any voltage + const hasActivePower = data.active_power_w != null && data.active_power_w !== 0; + const hasVoltage = data.active_voltage_l1_v != null || data.active_voltage_l2_v != null || data.active_voltage_l3_v != null; + const isOnline = hasActivePower || hasVoltage; + + // Detect restoration: was offline, now online + if (this._powerState.offline && isOnline) { + const offlineDuration = this._powerState.offlineStartTime + ? Math.round((Date.now() - this._powerState.offlineStartTime) / 1000) + : 0; + + this.log(`Power restored after ${offlineDuration} seconds offline`); + + this._flowTriggerPowerRestored.trigger(this, { + offline_duration: offlineDuration + }).catch(this.error); + + this._powerState.offline = false; + this._powerState.offlineStartTime = null; + } + // Track offline state + else if (!this._powerState.offline && !isOnline) { + this._powerState.offline = true; + this._powerState.offlineStartTime = Date.now(); + } + } + +}; diff --git a/drivers/energy/driver.compose.json b/drivers/energy/driver.compose.json new file mode 100644 index 00000000..27d6968f --- /dev/null +++ b/drivers/energy/driver.compose.json @@ -0,0 +1,273 @@ +{ + "name": { + "en": "P1 Meter" + }, + "images": { + "large": "drivers/energy/assets/images/large.png", + "small": "drivers/energy/assets/images/small.png" + }, + "class": "sensor", + "discovery": "energy", + "platforms": [ + "local" + ], + "capabilities": [ + "identify", + "measure_power", + "meter_gas", + "measure_gas", + "meter_water", + "meter_power", + "meter_power.consumed.t1", + "meter_power.produced.t1", + "meter_power.consumed.t2", + "meter_power.produced.t2", + "meter_power.consumed.t3", + "meter_power.produced.t3", + "meter_power.consumed", + "meter_power.returned", + "meter_power.daily", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "net_load_phase1", + "net_load_phase2", + "net_load_phase3", + "net_load_phase1_pct", + "net_load_phase2_pct", + "net_load_phase3_pct", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3", + "measure_power.montly_power_peak", + "rssi", + "wifi_quality", + "tariff", + "long_power_fail_count", + "voltage_sag_l1", + "voltage_sag_l2", + "voltage_sag_l3", + "voltage_swell_l1", + "voltage_swell_l2", + "voltage_swell_l3", + "connection_error", + "measure_frequency", + "alarm_connectivity" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.consumed", + "cumulativeExportedCapability": "meter_power.returned" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + } + }, + "meter_gas": { + "decimals": 3, + "title": { + "en": "Gasmeter", + "nl": "Gasmeter" + } + }, + "meter_gas.daily": { + "decimals": 3, + "title": { + "en": "Day Usage Gas", + "nl": "Dag verbruik Gas" + } + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Watermeter", + "nl": "Watermeter" + } + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "net_load_phase1_pct" :{ + "title": { + "en": "Phase 1 load %", + "nl": "Fase 1 belasting %" + } + }, + "net_load_phase2_pct" :{ + "title": { + "en": "Phase 2 load %", + "nl": "Fase 2 belasting %" + } + }, + "net_load_phase3_pct" :{ + "title": { + "en": "Phase 3 load %", + "nl": "Fase 3 belasting %" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + } + }, + "measure_voltage.l2": { + "title": { + "en": "Current Voltage phase 2", + "nl": "Huidig Voltage fase 2" + } + }, + "measure_voltage.l3": { + "title": { + "en": "Current Voltage phase 3", + "nl": "Huidig Voltage fase 3" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power.consumed.t2": { + "decimals": 3, + "title": { + "en": "Total t2 usage", + "nl": "Totaal t2 gebruik" + } + }, + "meter_power.produced.t2": { + "decimals": 3, + "title": { + "en": "Total t2 deliver", + "nl": "Totaal t2 teruglevering" + } + }, + "meter_power.consumed.t3": { + "decimals": 3, + "title": { + "en": "Total t3 usage", + "nl": "Totaal t3 gebruik" + } + }, + "meter_power.produced.t3": { + "decimals": 3, + "title": { + "en": "Total t3 deliver", + "nl": "Totaal t3 teruglevering" + } + }, + "meter_power.consumed": { + "decimals": 3, + "title": { + "en": "Sum Consumed", + "nl": "Som gebruik" + } + }, + "meter_power.returned": { + "decimals": 3, + "title": { + "en": "Sum returned", + "nl": "Som teruglevering" + } + }, + "meter_power.daily": { + "decimals": 3, + "title": { + "en": "Daily usage", + "nl": "Dag verbruik" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "measure_frequency": { + "decimals": 3, + "title": { + "en": "Current Frequency", + "nl": "Huidige Frequentie" + } + }, + "measure_power.montly_power_peak": { + "title": { + "en": "Monthly Power Peak", + "nl": "Maandelijks piekvermogen" + } + } + }, + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "repair": [ + { + "id": "manual_ip" + } + ] +} diff --git a/drivers/energy/driver.flow.compose.json b/drivers/energy/driver.flow.compose.json new file mode 100644 index 00000000..ab777ca0 --- /dev/null +++ b/drivers/energy/driver.flow.compose.json @@ -0,0 +1,218 @@ +{ + "triggers": [ + { + "id": "tariff_changed", + "title": { + "en": "Peak / Normal Tariff changed", + "nl": "Dal / Normaal Tarief veranderd" + }, + "args": [], + "tokens": [ + { + "name": "tariff_changed", + "type": "number", + "title": { + "en": "tariff", + "nl": "tarief" + }, + "example": 1 + } + ] + }, + { + "id": "import_changed", + "title": { + "en": "Total used changed", + "nl": "Som gebruik veranderd" + }, + "args": [], + "tokens": [ + { + "name": "import_changed", + "type": "number", + "title": { + "en": "import", + "nl": "import" + }, + "example": 1 + } + ] + }, + { + "id": "export_changed", + "title": { + "en": "Total delivered changed", + "nl": "Som teruglevering veranderd" + }, + "args": [], + "tokens": [ + { + "name": "export_changed", + "type": "number", + "title": { + "en": "export", + "nl": "export" + }, + "example": 1 + } + ] + }, + { + "id": "voltage_sag_detected_v1", + "title": { + "en": "Voltage sag detected", + "nl": "Spanningsdip gedetecteerd" + }, + "hint": { + "en": "Triggered when a voltage sag is detected on any phase", + "nl": "Wordt geactiveerd wanneer een spanningsdip wordt gedetecteerd op een willekeurige fase" + }, + "tokens": [ + { + "name": "phase_l1", + "type": "number", + "title": { + "en": "Phase L1 count", + "nl": "Fase L1 aantal" + }, + "example": 5 + }, + { + "name": "phase_l2", + "type": "number", + "title": { + "en": "Phase L2 count", + "nl": "Fase L2 aantal" + }, + "example": 3 + }, + { + "name": "phase_l3", + "type": "number", + "title": { + "en": "Phase L3 count", + "nl": "Fase L3 aantal" + }, + "example": 2 + } + ] + }, + { + "id": "voltage_swell_detected_v1", + "title": { + "en": "Voltage swell detected", + "nl": "Spanningspiek gedetecteerd" + }, + "hint": { + "en": "Triggered when a voltage swell is detected on any phase", + "nl": "Wordt geactiveerd wanneer een spanningspiek wordt gedetecteerd op een willekeurige fase" + }, + "tokens": [ + { + "name": "phase_l1", + "type": "number", + "title": { + "en": "Phase L1 count", + "nl": "Fase L1 aantal" + }, + "example": 2 + }, + { + "name": "phase_l2", + "type": "number", + "title": { + "en": "Phase L2 count", + "nl": "Fase L2 aantal" + }, + "example": 1 + }, + { + "name": "phase_l3", + "type": "number", + "title": { + "en": "Phase L3 count", + "nl": "Fase L3 aantal" + }, + "example": 0 + } + ] + }, + { + "id": "long_power_fail_detected_v1", + "title": { + "en": "Long power failure detected", + "nl": "Lange stroomstoring gedetecteerd" + }, + "hint": { + "en": "Triggered when a long power failure is detected", + "nl": "Wordt geactiveerd wanneer een lange stroomstoring wordt gedetecteerd" + }, + "tokens": [ + { + "name": "count", + "type": "number", + "title": { + "en": "Failure count", + "nl": "Storing aantal" + }, + "example": 1 + } + ] + }, + { + "id": "voltage_restored_v1", + "title": { + "en": "Voltage restored to normal", + "nl": "Spanning hersteld naar normaal" + }, + "hint": { + "en": "Triggers when voltage returns to normal range after a sag or swell", + "nl": "Triggert wanneer spanning terugkeert naar normaal bereik na een dip of piek" + }, + "args": [], + "tokens": [ + { + "name": "phase", + "type": "string", + "title": { + "en": "Phase", + "nl": "Fase" + }, + "example": "L1" + }, + { + "name": "voltage", + "type": "number", + "title": { + "en": "Voltage (V)", + "nl": "Spanning (V)" + }, + "example": 230 + } + ] + }, + { + "id": "power_restored_v1", + "title": { + "en": "Power restored", + "nl": "Stroom hersteld" + }, + "hint": { + "en": "Triggers when power is restored after being offline", + "nl": "Triggert wanneer stroom is hersteld na een uitval" + }, + "args": [], + "tokens": [ + { + "name": "offline_duration", + "type": "number", + "title": { + "en": "Offline duration (seconds)", + "nl": "Offline duur (seconden)" + }, + "example": 120 + } + ] + } + ] +} \ No newline at end of file diff --git a/drivers/energy/driver.js b/drivers/energy/driver.js new file mode 100644 index 00000000..a1a0dc29 --- /dev/null +++ b/drivers/energy/driver.js @@ -0,0 +1,5 @@ +'use strict'; + +const driver = require('../../includes/v1/driver.js'); + +module.exports = driver; diff --git a/drivers/energy/driver.settings.compose.json b/drivers/energy/driver.settings.compose.json new file mode 100644 index 00000000..16166b5e --- /dev/null +++ b/drivers/energy/driver.settings.compose.json @@ -0,0 +1,93 @@ +[ + { + "id": "polling_interval", + "type": "number", + "label": { "en": "Polling interval" }, + "value": 10, + "min": 1, + "unit": { "en": "s" } + }, + { + "id": "number_of_phases", + "type": "number", + "label": { "en": "Amount of phase(s)", + "nl": "Aantal fase(s)" }, + "value": 1 + }, + { + "id": "phase_capacity", + "type": "number", + "label": { "en": "Phase capacity A", + "nl": "Fase capaciteit A" }, + "value": 40, + "unit": { "en": "A" } + }, + { + "id": "cloud", + "type": "number", + "label": { "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { "en": "url", + "nl": "url" + } + }, + { + "id": "show_gas", + "type": "checkbox", + "label": { + "en": "Show gas meter", + "nl": "Gas meter weergeven" + }, + "value": true + }, + { + "id": "baseload_notifications", + "type": "checkbox", + "label": { + "en": "Enable baseload notifications", + "nl": "Sluipverbruik meldingen inschakelen" + }, + "value": true + }, + { + "id": "phase_overload_notifications", + "type": "checkbox", + "label": { + "en": "Phase overload notifications", + "nl": "Fase-overbelasting meldingen" + }, + "value": true + }, + { + "id": "phase_overload_threshold", + "type": "number", + "label": { + "en": "Phase overload warning threshold (%)", + "nl": "Fase overbelasting waarschuwing (%)" + }, + "value": 97, + "min": 50, + "max": 120, + "step": 1 +}, +{ + "id": "phase_overload_reset", + "type": "number", + "label": { + "en": "Phase overload reset threshold (%)", + "nl": "Fase overbelasting reset (%)" + }, + "value": 85, + "min": 20, + "max": 100, + "step": 1 +} + + +] \ No newline at end of file diff --git a/drivers/energy/pair/start.html b/drivers/energy/pair/start.html new file mode 100644 index 00000000..633815c6 --- /dev/null +++ b/drivers/energy/pair/start.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/drivers/energy/repair/manual_ip.html b/drivers/energy/repair/manual_ip.html new file mode 100644 index 00000000..f432c2e7 --- /dev/null +++ b/drivers/energy/repair/manual_ip.html @@ -0,0 +1,200 @@ + + + + + + +

+

+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + + + diff --git a/drivers/energy_socket/assets/icon.svg b/drivers/energy_socket/assets/icon.svg new file mode 100644 index 00000000..98805a05 --- /dev/null +++ b/drivers/energy_socket/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/energy_socket/assets/images/large.png b/drivers/energy_socket/assets/images/large.png new file mode 100644 index 00000000..e49913fc Binary files /dev/null and b/drivers/energy_socket/assets/images/large.png differ diff --git a/drivers/energy_socket/assets/images/small.png b/drivers/energy_socket/assets/images/small.png new file mode 100644 index 00000000..dad42d73 Binary files /dev/null and b/drivers/energy_socket/assets/images/small.png differ diff --git a/drivers/energy_socket/device.js b/drivers/energy_socket/device.js new file mode 100644 index 00000000..af42e065 --- /dev/null +++ b/drivers/energy_socket/device.js @@ -0,0 +1,645 @@ +'use strict'; + +const Homey = require('homey'); +const http = require('http'); +const fetchWithTimeout = require('../../includes/utils/fetchWithTimeout'); + +// Eén gedeelde HTTP agent voor alle energy socket devices. +// maxSockets:4 = max 4 gelijktijdige verbindingen over alle devices heen. +// Dit bespaart ~14 Agent-instanties + OS-sockets t.o.v. 1-per-device. +const SHARED_SOCKET_AGENT = new http.Agent({ + keepAlive: true, + keepAliveMsecs: 10000, + maxSockets: 4, + maxFreeSockets: 2, +}); + + + +/** + * Safe capability updater + */ +async function updateCapability(device, capability, value) { + try { + const current = device.getCapabilityValue(capability); + + // --- SAFE REMOVE --- + // Removal is allowed only when: + // 1) the new value is null + // 2) the current value in Homey is also null + + if (value == null && current == null) { + if (device.hasCapability(capability)) { + await device.removeCapability(capability); + device.log(`🗑️ Removed capability "${capability}"`); + } + return; + } + + // --- ADD IF MISSING --- + if (!device.hasCapability(capability)) { + try { + await device.addCapability(capability); + device.log(`➕ Added capability "${capability}"`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + } else { + throw err; + } + } + } + + // --- UPDATE --- + if (current !== value) { + await device.setCapabilityValue(capability, value); + } + + } catch (err) { + if (err.message === 'device_not_found') { + device.log(`⚠️ Skipping capability "${capability}" — device not found`); + return; + } + device.error(`❌ Failed updateCapability("${capability}")`, err); + } +} + +module.exports = class HomeWizardEnergySocketDevice extends Homey.Device { + + async onInit() { + + this._lastStatePoll = 0; + this._debugLogs = []; + this.__deleted = false; + + // ✅ FIX: Connection stability tracking + this._consecutiveFailures = 0; + this._consecutiveSuccesses = 0; + this._isMarkedUnavailable = false; + this._lastSuccessfulPoll = Date.now(); + + // Persistent fetch stats — restored from settings across restarts + const allStoredStats = this.homey.settings.get('fetch_device_stats') || {}; + const stored = allStoredStats[this.getName()] || {}; + this._fetchStats = { + total: stored.total || 0, + ok: stored.ok || 0, + failed: stored.failed || 0, + timeouts: stored.timeouts || 0, + avgResponseMs: stored.avgResponseMs || 0, + lastError: stored.lastError || null, + lastErrorAt: stored.lastErrorAt || null, + since: stored.since || new Date().toISOString(), + // WiFi stats + rssiAvg: stored.rssiAvg || null, + rssiMin: stored.rssiMin || null, + rssiMax: stored.rssiMax || null, + // mDNS stats + lastDiscoveryAt: stored.lastDiscoveryAt || null, + lastDiscoveryEvent: stored.lastDiscoveryEvent || null, + }; + // Flush stats to store every 60s, staggered by device index to prevent thundering herd + // (safeIndex is set below — forward reference is fine since this runs after allDevices lookup) + this._statsFlushTimer = null; // set after safeIndex is known + + this.agent = SHARED_SOCKET_AGENT; + + await updateCapability(this, 'connection_error', 'No errors'); + await updateCapability(this, 'alarm_connectivity', false); + + // Auto-scale interval based on device count to prevent fetchQueue overflow. + // fetchQueue: MAX_CONCURRENT=4, ~1s/request → throughput ~4 req/s + // Each device does 2 req/poll → min interval = ceil(deviceCount / 2) + const allDevices = this.driver.getDevices(); + const deviceCount = allDevices.length; + const myIndex = allDevices.indexOf(this); + const safeIndex = myIndex >= 0 ? myIndex : 0; + + const userInterval = Math.max(this.getSetting('offset_polling') || 10, 2); + const minInterval = Math.max(2, Math.ceil(deviceCount / 2)); + const interval = Math.max(userInterval, minInterval); + + if (interval > userInterval) { + this.log(`⚠️ Polling interval auto-scaled: ${userInterval}s → ${interval}s (${deviceCount} devices)`); + // Only notify once (from the first device) to avoid notification spam on multi-device setups + if (safeIndex === 0) { + this.homey.notifications.createNotification({ + excerpt: `Energy Socket polling auto-scaled naar ${interval}s (${deviceCount} devices). Verhoog je polling-instelling om deze melding te verbergen.`, + }).catch(() => {}); + } + } + + // Deterministic spread: device index determines start offset so devices never poll simultaneously. + // First device starts after 500ms, others evenly spread across the full interval. + const offset = safeIndex === 0 + ? 500 + : Math.round((safeIndex / deviceCount) * interval * 1000); + + this.log(`⏱️ Polling interval ${interval}s (user: ${userInterval}s), spread offset ${Math.round(offset / 1000)}s (device ${safeIndex + 1}/${deviceCount})`); + + // Stagger stats flush: 10s between devices, every 5min (settings writes are ~130ms each) + const flushDelay = 300000 + safeIndex * 10000; + setTimeout(() => { + if (this.__deleted) return; + this._flushFetchStats(); + this._statsFlushTimer = setInterval(() => this._flushFetchStats(), 300000); + }, flushDelay); + + if (this.onPollInterval) clearInterval(this.onPollInterval); + + // Start interval only after first poll completes (avoids double-firing) + setTimeout(() => { + if (this.__deleted) return; + this.log(`🚀 First poll starting (after ${Math.round(offset/1000)}s delay)`); + this.onPoll().catch(this.error); + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + }, offset); + + + if (this.getClass() === 'sensor') { + this.setClass('socket'); + } + + // Capability listeners + this.registerCapabilityListener('onoff', async (value) => { + if (this.getCapabilityValue('locked')) throw new Error('Device is locked'); + await this._putState({ power_on: value }); + }); + + this.registerCapabilityListener('identify', async () => { + await this._putIdentify(); + }); + + this.registerCapabilityListener('dim', async (value) => { + await this._putState({ brightness: Math.round(255 * value) }); + }); + + this.registerCapabilityListener('locked', async (value) => { + await this._putState({ switch_lock: value }); + }); + } + + onUninit() { + // Cleanup intervals and timers when app stops/crashes + this.__deleted = true; + + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + if (this._debugFlushTimeout) { + clearTimeout(this._debugFlushTimeout); + this._debugFlushTimeout = null; + } + if (this._statsFlushTimer) { + clearInterval(this._statsFlushTimer); + this._statsFlushTimer = null; + } + this._flushFetchStats(); + // Gedeelde agent NIET destroyen — die wordt gebruikt door alle energy socket devices + this.agent = null; + } + + onDeleted() { + // Call onUninit to cleanup timers + this.onUninit(); + + // Flush remaining logs before device deletion (only on explicit deletion) + if (this._debugBuffer && this._debugBuffer.length > 0) { + this._flushDebugLogs(); + } + // Clear debug buffer + if (this._debugBuffer) { + this._debugBuffer = null; + } + } + + /** + * Discovery handlers + */ + _trackDiscovery(event) { + if (this._fetchStats) { + this._fetchStats.lastDiscoveryAt = new Date().toISOString(); + this._fetchStats.lastDiscoveryEvent = event; + } + } + + onDiscoveryAvailable(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this._trackDiscovery('available'); + // ✅ FIX: Reset failure counters on rediscovery + this._consecutiveFailures = 0; + this._consecutiveSuccesses = 0; + this.setAvailable(); + this._isMarkedUnavailable = false; + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this._trackDiscovery('address_changed'); + this._debugLog(`Discovery address changed: ${this.url}`); + // ✅ FIX: Reset failure counters on address change + this._consecutiveFailures = 0; + this._consecutiveSuccesses = 0; + this.setAvailable(); + this._isMarkedUnavailable = false; + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this._trackDiscovery('last_seen'); + this.setAvailable(); + this._isMarkedUnavailable = false; + } + + /** + * Debug logger (batched writes to shared app settings) + */ +_debugLog(msg) { + try { + if (!this._debugBuffer) this._debugBuffer = []; + const ts = new Date().toLocaleString('nl-NL', { hour12: false, timeZone: 'Europe/Amsterdam' }); + const driverName = this.driver.id; + const deviceName = this.getName(); + const safeMsg = typeof msg === 'string' ? msg : (msg instanceof Error ? msg.message : JSON.stringify(msg)); + const line = `${ts} [${driverName}] [${deviceName}] ${safeMsg}`; + this._debugBuffer.push(line); + if (this._debugBuffer.length > 20) this._debugBuffer.shift(); + if (!this._debugFlushTimeout) { + this._debugFlushTimeout = setTimeout(() => { + this._flushDebugLogs(); + this._debugFlushTimeout = null; + }, 5000); + } + } catch (err) { + this.error('Failed to write debug logs:', err.message || err); + } +} + +_flushDebugLogs() { + if (!this._debugBuffer || this._debugBuffer.length === 0) return; + try { + const logs = this.homey.settings.get('debug_logs') || []; + logs.push(...this._debugBuffer); + if (logs.length > 500) logs.splice(0, logs.length - 500); + this.homey.settings.set('debug_logs', logs); + this._debugBuffer = []; + } catch (err) { + this.error('Failed to flush debug logs:', err.message || err); + } +} + +_flushFetchStats() { + if (!this._fetchStats) return; + const t0 = Date.now(); + // If the settings entry for this device was cleared (reset button), reset in-memory stats too + try { + const allStats = this.homey.settings.get('fetch_device_stats') || {}; + if (!allStats[this.getName()]) { + this._fetchStats = { + total: 0, ok: 0, failed: 0, timeouts: 0, + avgResponseMs: 0, lastError: null, lastErrorAt: null, + since: new Date().toISOString(), + rssiAvg: null, rssiMin: null, rssiMax: null, + }; + } + allStats[this.getName()] = this._fetchStats; + this.homey.settings.set('fetch_device_stats', allStats); + } catch (_) {} + // setStoreValue is redundant — data is already in homey.settings above + //this.log(`💾 _flushFetchStats took ${Date.now() - t0}ms`); +} + + /** + * PUT /state (pure fetch, geen retries) + */ + async _putState(body) { + if (!this.url) return; + + try { + // ✅ FIX: Longer timeout for poor WiFi (10s instead of 5s) + const res = await fetchWithTimeout(`${this.url}/state`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }, 10000); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + } catch (err) { + this._debugLog(`PUT /state failed: ${err.message}`); + throw new Error('Network error during state update'); + } + } + + /** + * PUT /identify + */ + async _putIdentify() { + if (!this.url) return; + + try { + const res = await fetchWithTimeout(`${this.url}/identify`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' } + }, 10000); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + } catch (err) { + this._debugLog(`PUT /identify failed: ${err.message}`); + throw new Error('Network error during identify'); + } + } + + /** + * PUT /system cloud on/off + */ + async setCloudOn() { + if (!this.url) return; + + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: true }) + }, 10000); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + } catch (err) { + this._debugLog(`Cloud ON failed: ${err.message}`); + throw new Error('Network error during setCloudOn'); + } + } + + async setCloudOff() { + if (!this.url) return; + + try { + const res = await fetchWithTimeout(`${this.url}/system`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cloud_enabled: false }) + }, 10000); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + } catch (err) { + this._debugLog(`Cloud OFF failed: ${err.message}`); + throw new Error('Network error during setCloudOff'); + } + } + + /** + * ✅ FIX: Debounced connection state management + * Only mark unavailable after 3 consecutive failures + * Only mark available after 2 consecutive successes + */ + _handlePollSuccess(elapsedMs, rssi) { + this._consecutiveFailures = 0; + this._consecutiveSuccesses++; + this._lastSuccessfulPoll = Date.now(); + + // Update fetch stats + const s = this._fetchStats; + s.total++; + s.ok++; + if (elapsedMs != null) { + s.avgResponseMs = Math.round(s.avgResponseMs + (elapsedMs - s.avgResponseMs) / s.ok); + } + if (rssi != null) { + s.rssiAvg = s.rssiAvg == null ? rssi : Math.round(s.rssiAvg + (rssi - s.rssiAvg) / s.ok); + if (s.rssiMin == null || rssi < s.rssiMin) s.rssiMin = rssi; + if (s.rssiMax == null || rssi > s.rssiMax) s.rssiMax = rssi; + } + + // Mark available after 2 consecutive successes (prevents flapping) + if (this._consecutiveSuccesses >= 2 && this._isMarkedUnavailable) { + this.log('✅ Connection restored (2 consecutive successes)'); + this.setAvailable().catch(this.error); + this._isMarkedUnavailable = false; + updateCapability(this, 'connection_error', 'No errors').catch(this.error); + updateCapability(this, 'alarm_connectivity', false).catch(this.error); + } else if (!this._isMarkedUnavailable) { + // Already available — alarm_connectivity is already false, only clear error text if needed + if (this._consecutiveFailures > 0 || this._consecutiveSuccesses === 1) { + updateCapability(this, 'connection_error', 'No errors').catch(this.error); + } + } + } + + _handlePollFailure(err) { + this._consecutiveSuccesses = 0; + this._consecutiveFailures++; + + // Update fetch stats + const s = this._fetchStats; + s.total++; + s.failed++; + if (err && (err.message === 'TIMEOUT' || err.name === 'AbortError')) s.timeouts++; + s.lastError = err ? (err.message || String(err)) : 'unknown'; + s.lastErrorAt = new Date().toISOString(); + + const timeSinceLastSuccess = Date.now() - this._lastSuccessfulPoll; + + // Only mark unavailable after 3 consecutive failures AND 90 seconds since last success + // This prevents flapping on temporary WiFi glitches + if (this._consecutiveFailures >= 3 && timeSinceLastSuccess > 90000) { + if (!this._isMarkedUnavailable) { + this.log(`❌ Connection lost after ${this._consecutiveFailures} failures (${Math.round(timeSinceLastSuccess/1000)}s since last success)`); + this.setUnavailable(err.message || 'Polling error').catch(this.error); + this._isMarkedUnavailable = true; + } + updateCapability(this, 'connection_error', err.message || 'Polling error').catch(this.error); + updateCapability(this, 'alarm_connectivity', true).catch(this.error); + } else { + // Still trying - just log the error but don't mark unavailable yet + this._debugLog(`Poll failed (${this._consecutiveFailures}/3): ${err.message}`); + updateCapability(this, 'connection_error', `Retrying (${this._consecutiveFailures}/3): ${err.message}`).catch(this.error); + } + } + + /** + * GET /data + GET /state (with improved error handling) + */ + async onPoll() { + if (this.__deleted) return; + + const settings = this.getSettings(); + const pollStart = Date.now(); + + // URL restore when needed + if (!this.url) { + if (settings.url) { + this.url = settings.url; + } else { + this._handlePollFailure(new Error('Missing URL')); + return; + } + } + + try { + + // ----------------------------- + // GET /data (with retry on timeout) + // ----------------------------- + let data; + let retries = 2; + + while (retries >= 0) { + try { + // ✅ FIX: Longer timeout for poor WiFi (10s instead of 5s) + const res = await fetchWithTimeout(`${this.url}/data`, { + agent: this.agent, + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }, 10000); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + data = await res.json(); + if (!data || typeof data !== 'object') throw new Error('Invalid JSON'); + + break; // Success - exit retry loop + + } catch (err) { + retries--; + if (retries < 0) { + throw err; // All retries exhausted + } + // Wait 1 second before retry + this._debugLog(`/data failed, retrying (${2-retries}/2): ${err.message}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + const offset = Number(this.getSetting('offset_socket')) || 0; + const watt = data.active_power_w + offset; + + const tasks = []; + const cap = (name, value) => { + if (value === undefined || value === null) return; + const cur = this.getCapabilityValue(name); + if (cur !== value) tasks.push(updateCapability(this, name, value)); + }; + + cap('measure_power', watt); + cap('meter_power.consumed.t1', data.total_power_import_t1_kwh); + cap('measure_power.l1', data.active_power_l1_w); + cap('rssi', data.wifi_strength); + + if (data.total_power_export_t1_kwh > 1) { + cap('meter_power.produced.t1', data.total_power_export_t1_kwh); + } + + const net = data.total_power_import_t1_kwh - data.total_power_export_t1_kwh; + cap('meter_power', net); + + cap('measure_voltage', data.active_voltage_v); + cap('measure_current', data.active_current_a); + + // ----------------------------- + // GET /state (max 1× per 30s, non-critical) + // ----------------------------- + const now = Date.now(); + const mustPollState = + !this._lastStatePoll || + (now - this._lastStatePoll) > 30000; + + if (mustPollState) { + this._lastStatePoll = now; + + try { + const resState = await fetchWithTimeout(`${this.url}/state`, { + agent: this.agent, + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }, 10000); + + if (!resState.ok) throw new Error(`HTTP ${resState.status}: ${resState.statusText}`); + + const state = await resState.json(); + if (!state || typeof state !== 'object') throw new Error('Invalid JSON'); + + cap('onoff', state.power_on); + cap('dim', state.brightness / 255); + cap('locked', state.switch_lock); + + } catch (err) { + // ✅ FIX: State poll failure is non-critical - don't count as connection failure + this._debugLog(`State poll failed (non-critical): ${err.message}`); + // Don't update connection_error or alarm_connectivity for state failures + } + } + + if (!this.__deleted && this.url !== settings.url) { + this.setSettings({ url: this.url }).catch(this.error); + } + + if (tasks.length > 0) await Promise.allSettled(tasks); + + // ✅ FIX: Mark as successful poll + this._handlePollSuccess(Date.now() - pollStart, data.wifi_strength); + + } catch (err) { + if (!this.__deleted) { + this._debugLog(`Poll failed: ${err.message}`); + // ✅ FIX: Use debounced failure handler + this._handlePollFailure(err); + } + } +} + + + /** + * Settings handler + */ + async onSettings(oldSettings, newSettings, changedKeys = []) { + + for (const key of changedKeys) { + + if (key === 'offset_socket') { + const cap = 'measure_power'; + const oldVal = Number(oldSettings[key]) || 0; + const newVal = Number(newSettings[key]) || 0; + const delta = newVal - oldVal; + + const current = this.getCapabilityValue(cap) || 0; + await this.setCapabilityValue(cap, current + delta).catch(this.error); + } + + if (key === 'offset_polling') { + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + + const interval = Number(newSettings.offset_polling); + // ✅ CPU FIX: Increased min interval from 2s to 5s (9 devices = high load) + if (interval >= 2) { + this.onPollInterval = setInterval(this.onPoll.bind(this), interval * 1000); + } + } + + if (key === 'cloud') { + try { + if (newSettings.cloud == 1) await this.setCloudOn(); + else await this.setCloudOff(); + } catch (err) { + this.error('Failed to update cloud setting:', err); + } + } + } + } +}; \ No newline at end of file diff --git a/drivers/energy_socket/driver.compose.json b/drivers/energy_socket/driver.compose.json new file mode 100644 index 00000000..2771d4d4 --- /dev/null +++ b/drivers/energy_socket/driver.compose.json @@ -0,0 +1,136 @@ +{ + "name": { + "en": "Energy Socket" + }, + "images": { + "large": "drivers/energy_socket/assets/images/large.png", + "small": "drivers/energy_socket/assets/images/small.png" + }, + "class": "socket", + "discovery": "energy_socket", + "platforms": [ + "local" + ], + "capabilities": [ + "onoff", + "dim", + "identify", + "locked", + "measure_power", + "meter_power", + "meter_power.consumed.t1", + "meter_power.produced.t1", + "measure_power.l1", + "rssi", + "connection_error", + "alarm_connectivity" + ], + "energy": { + "meterPowerImportedCapability": "meter_power.consumed.t1", + "meterPowerExportedCapability": "meter_power.produced.t1" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + }, + "insights": true + } + }, + "settings": [ + { + "type": "group", + "label": { + "en": "Energy Socket settings", + "nl": "Energy Socket instellingen" + }, + "children": [ + { + "id": "offset_socket", + "type": "number", + "label": { + "en": "Offset Watt usage", + "nl": "Compensatie watt gebruik" + }, + "value": 0, + "unit": { + "en": "watt", + "nl": "watt" + } + }, + { + "id": "offset_polling", + "type": "number", + "label": { + "en": "Polling in seconds", + "nl": "Interval in seconden" + }, + "value": 10, + "min": 2 + }, + { + "id": "cloud", + "type": "number", + "label": { "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + } + ] + } + ], + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ] +} diff --git a/drivers/energy_socket/driver.js b/drivers/energy_socket/driver.js new file mode 100644 index 00000000..a1a0dc29 --- /dev/null +++ b/drivers/energy_socket/driver.js @@ -0,0 +1,5 @@ +'use strict'; + +const driver = require('../../includes/v1/driver.js'); + +module.exports = driver; diff --git a/drivers/energy_socket/pair/start.html b/drivers/energy_socket/pair/start.html new file mode 100644 index 00000000..daae7b43 --- /dev/null +++ b/drivers/energy_socket/pair/start.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/drivers/energy_v2/assets/battery.svg b/drivers/energy_v2/assets/battery.svg new file mode 100644 index 00000000..202e8ba5 --- /dev/null +++ b/drivers/energy_v2/assets/battery.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/drivers/energy_v2/assets/icon.svg b/drivers/energy_v2/assets/icon.svg new file mode 100644 index 00000000..e1bc3269 --- /dev/null +++ b/drivers/energy_v2/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/energy_v2/assets/images/large.png b/drivers/energy_v2/assets/images/large.png new file mode 100644 index 00000000..6358f0b7 Binary files /dev/null and b/drivers/energy_v2/assets/images/large.png differ diff --git a/drivers/energy_v2/assets/images/small.png b/drivers/energy_v2/assets/images/small.png new file mode 100644 index 00000000..721d7b06 Binary files /dev/null and b/drivers/energy_v2/assets/images/small.png differ diff --git a/drivers/energy_v2/assets/rssi.svg b/drivers/energy_v2/assets/rssi.svg new file mode 100644 index 00000000..e98392f6 --- /dev/null +++ b/drivers/energy_v2/assets/rssi.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drivers/energy_v2/device.js b/drivers/energy_v2/device.js new file mode 100644 index 00000000..f727815d --- /dev/null +++ b/drivers/energy_v2/device.js @@ -0,0 +1,2656 @@ +/* + * HomeWizard Energy (P1) Driver - APIv2 + * Copyright (C) 2025 Jeroen Tebbens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +'use strict'; + +const Homey = require('homey'); +const https = require('https'); +const fetchWithTimeout = require('../../includes/utils/fetchWithTimeout'); +const api = require('../../includes/v2/Api'); +const WebSocketManager = require('../../includes/v2/Ws'); +const wsDebug = require('../../includes/v2/wsDebug'); +const BaseloadMonitor = require('../../includes/utils/baseloadMonitor'); +const debug = false; // Legacy constant — use this._debugLogging at runtime (toggle via device settings) + +process.on('uncaughtException', (err) => { + console.error('💥 Uncaught Exception:', err); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('💥 Unhandled Rejection at:', promise, 'reason:', reason); +}); + +// Shared HTTPS agent (module-level) — maxSockets begrensd om RSS te beperken bij meerdere devices +const agent = new https.Agent({ + rejectUnauthorized: false, + maxSockets: 2, + maxFreeSockets: 1, +}); + + + +/** + * Helper function to add, remove or update a capability + * @async + * @param {Homey.Device} device The device instance + * @param {string} capability The capability identifier + * @param {any} value The value to set + * @returns {Promise} + */ +async function updateCapability(device, capability, value) { + try { + const current = device.getCapabilityValue(capability); + + // --- SPECIAL CASE: battery_group_charge_mode --- + // This capability is managed exclusively by _updateBatteryGroup(). + if (capability === 'battery_group_charge_mode') { + if (value != null && current !== value) { + if (!device.hasCapability(capability)) { + await safeAddCapability(device, capability); + } + await device.setCapabilityValue(capability, value); + } + return; + } + + // --- SAFE REMOVE --- + if (value == null && current == null) { + if (device.hasCapability(capability)) { + await device.removeCapability(capability); + device.log(`🗑️ Removed capability "${capability}"`); + } + return; + } + + // --- ADD IF MISSING --- + if (!device.hasCapability(capability)) { + try { + await device.addCapability(capability); + device.log(`➕ Added capability "${capability}"`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + } else { + throw err; + } + } + } + + // --- UPDATE --- + if (current !== value) { + await device.setCapabilityValue(capability, value); + } + + } catch (err) { + if (err.message === 'device_not_found') { + device.log(`⚠️ Skipping capability "${capability}" — device not found`); + return; + } + device.error(`❌ Failed updateCapability("${capability}")`, err); + } +} + + +/** + * Safe add capability helper — avoids race 409 errors + */ +async function safeAddCapability(device, capability) { + try { + if (!device.hasCapability(capability)) { + await device.addCapability(capability); + device.log(`➕ Safely added capability "${capability}"`); + } + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + return; + } + throw err; + } +} + + + +async function setStoreValueSafe(device, key, value) { + try { + return await device.setStoreValue(key, value); + } catch (err) { + if (err.message === 'device_not_found') { + device.log(`⚠️ Skipping setStoreValue("${key}") — device not found`); + return null; + } + device.error(`❌ Failed setStoreValue("${key}")`, err); + return null; + } +} + +async function getStoreValueSafe(device, key) { + try { + return await device.getStoreValue(key); + } catch (err) { + if (err.message === 'device_not_found') { + device.log(`⚠️ Skipping getStoreValue("${key}") — device not found`); + return null; + } + device.error(`❌ Failed getStoreValue("${key}")`, err); + return null; + } +} + + +/** + * Helper function to determine WiFi quality + * @param {number} strength The WiFi signal strength + * @returns {string} The quality level ('poor', 'fair', 'good') + */ +function getWifiQuality(strength) { + if (strength >= -30) return 'Excellent'; // Strongest signal + if (strength >= -60) return 'Strong'; // Strong + if (strength >= -70) return 'Moderate'; // Good to Fair + if (strength >= -80) return 'Weak'; // Fair to Weak + if (strength >= -90) return 'Poor'; // Weak to Unusable + return 'Unusable'; // Very poor signal +} + +async function applyMeasurementCapabilities(device, m) { + try { + const now = Date.now(); + + // ✅ CPU FIX: Categorize capabilities by update frequency + // High-frequency (realtime) capabilities: update on every message (but already throttled at WS level to 3s) + const realtimeCapabilities = { + 'measure_power': m.power_w, + 'measure_power.l1': m.power_l1_w, + 'measure_power.l2': m.power_l2_w, + 'measure_power.l3': m.power_l3_w, + }; + + // Medium-frequency capabilities: update every 10 seconds + const mediumFreqCapabilities = { + 'measure_voltage': m.voltage_v, + 'measure_current': m.current_a, + 'measure_frequency': m.frequency_hz, + 'measure_voltage.l1': m.voltage_l1_v, + 'measure_voltage.l2': m.voltage_l2_v, + 'measure_voltage.l3': m.voltage_l3_v, + 'measure_current.l1': m.current_l1_a, + 'measure_current.l2': m.current_l2_a, + 'measure_current.l3': m.current_l3_a, + 'tariff': m.tariff, + }; + + // Low-frequency capabilities: update every 30 seconds + const lowFreqCapabilities = { + 'meter_power.consumed': m.energy_import_kwh, + 'meter_power.returned': m.energy_export_kwh, + 'meter_power.consumed.t1': m.energy_import_t1_kwh, + 'meter_power.produced.t1': m.energy_export_t1_kwh, + 'meter_power.consumed.t2': m.energy_import_t2_kwh, + 'meter_power.produced.t2': m.energy_export_t2_kwh, + 'meter_power.consumed.t3': m.energy_import_t3_kwh, + 'meter_power.produced.t3': m.energy_export_t3_kwh, + 'meter_power.consumed.t4': m.energy_import_t4_kwh, + 'meter_power.produced.t4': m.energy_export_t4_kwh, + 'measure_power.montly_power_peak': m.monthly_power_peak_w, + 'measure_power.average_power_15m_w': m.average_power_15m_w, + }; + + // Very low-frequency capabilities: update every 60 seconds + const veryLowFreqCapabilities = { + 'long_power_fail_count': m.long_power_fail_count, + 'voltage_sag_l1': m.voltage_sag_l1_count, + 'voltage_sag_l2': m.voltage_sag_l2_count, + 'voltage_sag_l3': m.voltage_sag_l3_count, + 'voltage_swell_l1': m.voltage_swell_l1_count, + 'voltage_swell_l2': m.voltage_swell_l2_count, + 'voltage_swell_l3': m.voltage_swell_l3_count, + }; + + // Initialize debounce timestamps if needed + if (!device._lastMediumUpdate) device._lastMediumUpdate = 0; + if (!device._lastLowUpdate) device._lastLowUpdate = 0; + if (!device._lastVeryLowUpdate) device._lastVeryLowUpdate = 0; + + const mappings = { + ...realtimeCapabilities, + ...(now - device._lastMediumUpdate >= 10000 ? mediumFreqCapabilities : {}), + ...(now - device._lastLowUpdate >= 30000 ? lowFreqCapabilities : {}), + ...(now - device._lastVeryLowUpdate >= 60000 ? veryLowFreqCapabilities : {}), + }; + + // Update timestamps + if (now - device._lastMediumUpdate >= 10000) device._lastMediumUpdate = now; + if (now - device._lastLowUpdate >= 30000) device._lastLowUpdate = now; + if (now - device._lastVeryLowUpdate >= 60000) device._lastVeryLowUpdate = now; + + // Collect all capability updates as promises + const tasks = []; + + // Track which capabilities changed for triggering flows + const changed = { + voltage_sag: null, + voltage_swell: null, + long_power_fail: false + }; + + for (const [capability, valueRaw] of Object.entries(mappings)) { + let value = valueRaw; + + // Normalize tariff (critical for triggers) + if (capability === 'tariff' && value != null) { + value = Number(value); + } + + const cur = device.getCapabilityValue(capability); + if (cur !== value) { + tasks.push(updateCapability(device, capability, value ?? null)); + + // Track voltage sag changes + if (capability === 'voltage_sag_l1' && value != null && value !== cur) { + changed.voltage_sag = { phase: 'L1', count: value }; + } else if (capability === 'voltage_sag_l2' && value != null && value !== cur) { + changed.voltage_sag = { phase: 'L2', count: value }; + } else if (capability === 'voltage_sag_l3' && value != null && value !== cur) { + changed.voltage_sag = { phase: 'L3', count: value }; + } + + // Track voltage swell changes + if (capability === 'voltage_swell_l1' && value != null && value !== cur) { + changed.voltage_swell = { phase: 'L1', count: value }; + } else if (capability === 'voltage_swell_l2' && value != null && value !== cur) { + changed.voltage_swell = { phase: 'L2', count: value }; + } else if (capability === 'voltage_swell_l3' && value != null && value !== cur) { + changed.voltage_swell = { phase: 'L3', count: value }; + } + + // Track long power fail changes + if (capability === 'long_power_fail_count' && value != null && value !== cur) { + changed.long_power_fail = value; + } + } + } + + + // Run all updates in parallel + await Promise.allSettled(tasks); + + // Trigger flow cards after updates complete + if (changed.voltage_sag && device._flowTriggerVoltageSag) { + device._flowTriggerVoltageSag.trigger(device, changed.voltage_sag).catch(device.error); + } + if (changed.voltage_swell && device._flowTriggerVoltageSwell) { + device._flowTriggerVoltageSwell.trigger(device, changed.voltage_swell).catch(device.error); + } + if (changed.long_power_fail !== false && device._flowTriggerPowerFail) { + device._flowTriggerPowerFail.trigger(device, { count: changed.long_power_fail }).catch(device.error); + } + + // Check for voltage and power restoration + device._checkVoltageRestoration(m); + device._checkPowerRestoration(m); + + } catch (error) { + device.error('Failed to apply measurement capabilities:', error); + throw error; + } +} + + +/** + * Normalize battery mode from raw payload + * @param {Object} data - battery payload { mode, permissions } + * @returns {string} normalized mode string + */ +function normalizeBatteryMode(data) { + // If already normalized (string), return as-is + if (typeof data === 'string') { + return data.trim(); + } + + // Extract mode + let rawMode = typeof data.mode === 'string' + ? data.mode.trim().replace(/^["']+|["']+$/g, '') + : null; + + const mode = rawMode ? rawMode.toLowerCase() : null; + + // Extract permissions (sorted for deterministic comparison) + const perms = Array.isArray(data.permissions) + ? [...data.permissions].map(p => p.toLowerCase()).sort().join(',') + : null; + + // Direct modes + if (mode === 'standby') return 'standby'; + if (mode === 'to_full') return 'to_full'; + + // Vendor sometimes sends these directly + if (mode === 'zero_charge_only') return 'zero_charge_only'; + if (mode === 'zero_discharge_only') return 'zero_discharge_only'; + + // Normalize "zero" family + if (mode === 'zero') { + switch (perms) { + case 'charge_allowed,discharge_allowed': + return 'zero'; + case 'charge_allowed': + return 'zero_charge_only'; + case 'discharge_allowed': + return 'zero_discharge_only'; + case '': + case null: + return 'zero'; + default: + console.log(`⚠️ Unknown permissions for mode=zero: ${perms}`); + return 'zero'; + } + } + + // Unknown combination + console.log(`⚠️ Unknown battery mode: ${JSON.stringify(data)}`); + return 'standby'; +} + + + + + + + + + + + +module.exports = class HomeWizardEnergyDeviceV2 extends Homey.Device { + +_hashExternal(external) { + if (!Array.isArray(external) || external.length === 0) return 'none'; + + // Only hash if there's actually data to process + let hash = ''; + for (let i = 0; i < external.length; i++) { + const e = external[i]; + const type = e?.type ?? 'unknown'; + const value = e?.value ?? 'null'; + const ts = e?.timestamp ?? 'null'; + hash += (hash ? '|' : '') + `${type}:${value}:${ts}`; + } + return hash; +} + +/** + * Get effective URL - manual IP overrides discovery + * @returns {string} URL to use for API calls + */ +_getEffectiveURL() { + const manualIP = this.getSetting('manual_ip'); + if (manualIP) { + this.log(`🔧 Using manual IP: ${manualIP}`); + return `https://${manualIP}`; + } + + const settings = this.getSettings(); + if (settings.url) { + return settings.url; + } + + return null; +} + +/** + * Reconnect with manual IP after repair flow + * @param {string} ip - The manual IP address + */ +async reconnectWithManualIP(ip) { + this.log(`🔧 Reconnecting with manual IP: ${ip}`); + + this.url = `https://${ip}`; + + // Restart WebSocket if not polling + if (!this.getSettings().use_polling && this.wsManager) { + this.log('🔁 Manual IP: restarting WebSocket'); + this.wsManager.restartWebSocket(); + } else if (this.getSettings().use_polling) { + this.log('🔁 Manual IP: polling mode, will reconnect on next poll'); + } +} + + + async onInit() { + wsDebug.init(this.homey); + this.onPollInterval = null; + this.gridReturnStart = null; + this.batteryErrorTriggered = false; + this._lastFullUpdate = 0; + this._lastDiscoveryIP = null; + + // Add rate limiting state to onInit() - place near the top of onInit() + this._lastBatteryModeChange = 0; + this._batteryModeChangeCooldown = 5000; // 5 seconds minimum between changes + + + this._cache = { + external_last_payload: null, + external_last_result: null, + meter_start_day: null, + gasmeter_start_day: null, + last_gas_delta_minute: null, + gasmeter_previous_reading: null, + gasmeter_previous_reading_timestamp: null, + last_battery_state: null, + }; + + this._cacheDirty = false; + + // Load store values once + for (const key of Object.keys(this._cache)) { + this._cache[key] = await getStoreValueSafe(this, key); + } + + // Get effective URL (manual IP overrides discovery) + this.url = this._getEffectiveURL(); + + + + // await this.setUnavailable(`${this.getName()} ${this.homey.__('device.init')}`); + + await updateCapability(this, 'connection_error', 'No errors').catch(this.error); + + this.token = await getStoreValueSafe(this, 'token'); + //console.log('P1 Token:', this.token); + + await this._updateCapabilities(); + await this._registerCapabilityListeners(); + await this._ensureBatteryCapabilities(); + + // Register with baseload monitor + const app = this.homey.app; + if (!app.baseloadMonitor) { + app.baseloadMonitor = new BaseloadMonitor(this.homey); + } + app.baseloadMonitor.registerP1Device(this); + app.baseloadMonitor.trySetMaster(this); + this._baseloadNotificationsEnabled = this.getSetting('baseload_notifications') ?? true; + app.baseloadMonitor.setNotificationsEnabledForDevice(this, this._baseloadNotificationsEnabled); + + const settings = this.getSettings(); + this.log('Settings for P1 apiv2: ', settings.polling_interval); + + // Check if polling interval is set in settings else set default value + if (settings.polling_interval === undefined) { + settings.polling_interval = 10; // Default to 10 second if not set + await this.setSettings({ + // Update settings in Homey + polling_interval: 10, + }); + } + + if (settings.cloud === undefined) { + settings.cloud = 1; // Default true + await this.setSettings({ + // Update settings in Homey + cloud: 1, + }); + } + + + + // Store flow listener references for cleanup in onDeleted() + this._flowListenerReferences = []; + + // Register flow card listeners only once (prevent "already registered" warnings) + if (!this.homey.app._flowListenersRegistered) { + this.homey.app._flowListenersRegistered = true; + + +// ============================================================================ +// CONDITION CARD - Check Battery Mode +// ============================================================================ + +const ConditionCardCheckBatteryMode = this.homey.flow.getConditionCard('check-battery-mode'); + +ConditionCardCheckBatteryMode.registerRunListener(async ({ device, mode }) => { + if (!device) return false; + + device.log('ConditionCard: Check Battery Mode'); + + try { + const { wsManager, url, token } = device; + + // Prefer WebSocket cache + if (wsManager?.isConnected()) { + const lastBatteryState = device._cacheGet('last_battery_state'); + + if (lastBatteryState) { + const normalized = normalizeBatteryMode(lastBatteryState); + return mode === normalized; + } + } + + // Fallback: HTTP + const response = await api.getMode(url, token); + if (!response || typeof response !== 'object') { + device.log('⚠️ Invalid battery mode response:', response); + return false; + } + + // Update cache + device._cacheSet('last_battery_state', { + mode: response.mode, + permissions: response.permissions, + battery_count: response.battery_count ?? 1 + }); + + // Normalize + const normalized = normalizeBatteryMode(response); + + // Update capability + await updateCapability(device, 'battery_group_charge_mode', normalized); + + // ✅ FIXED: Only trigger flow on actual change + const prev = device._cacheGet('last_battery_mode'); + if (normalized !== prev) { + device.flowTriggerBatteryMode(device, { mode: normalized }); + device._cacheSet('last_battery_mode', normalized); + } + + return mode === normalized; + + } catch (error) { + device?.error('Error retrieving mode:', error); + return false; + } +}); + +// ============================================================================ +// ACTION CARD 1: Set Battery to Zero Mode +// ============================================================================ + +this.homey.flow + .getActionCard('set-battery-to-zero-mode') + .registerRunListener(async ({ device }) => { + if (!device) return false; + + // ✅ RATE LIMITING: Prevent rapid successive calls + const now = Date.now(); + if (now - device._lastBatteryModeChange < device._batteryModeChangeCooldown) { + device.log('⏸️ Battery mode change throttled - cooldown active'); + return 'zero'; + } + device._lastBatteryModeChange = now; + device._cacheSet('last_commanded_mode', 'zero'); + + device.log('ActionCard: Set Battery to Zero Mode'); + + try { + const { wsManager, url, token } = device; + + // Prefer WebSocket + if (wsManager?.isConnected()) { + wsManager.setBatteryMode('zero'); + device.log('Set mode to zero via WebSocket'); + return 'zero'; + } + + // HTTP fallback: set mode + const response = await api.setMode(url, token, 'zero'); + if (!response) { + device.log('Invalid response from setMode()'); + return false; + } + + // Fetch real battery state after setting mode + const modeResponse = await api.getMode(url, token); + if (!modeResponse || typeof modeResponse !== 'object') { + device.log('⚠️ Invalid battery mode response after setMode:', modeResponse); + return false; + } + + // Update cache + device._cacheSet('last_battery_state', { + mode: modeResponse.mode, + permissions: modeResponse.permissions, + battery_count: modeResponse.battery_count ?? 1 + }); + + // Normalize + const normalized = normalizeBatteryMode(modeResponse); + + // Update capability + await updateCapability(device, 'battery_group_charge_mode', normalized); + + // ✅ FIXED: Only trigger flow on actual change + const prev = device._cacheGet('last_battery_mode'); + if (normalized !== prev) { + device.flowTriggerBatteryMode(device, { mode: normalized }); + device._cacheSet('last_battery_mode', normalized); + } + + device.log('Set mode to zero via HTTP'); + return 'zero'; + + } catch (error) { + device.error('Error set mode to zero:', error); + return false; + } + }); + +// ============================================================================ +// ACTION CARD 2: Set Battery to Standby Mode +// ============================================================================ + +this.homey.flow + .getActionCard('set-battery-to-standby-mode') + .registerRunListener(async ({ device }) => { + if (!device) return false; + + // ✅ RATE LIMITING: Prevent rapid successive calls + const now = Date.now(); + if (now - device._lastBatteryModeChange < device._batteryModeChangeCooldown) { + device.log('⏸️ Battery mode change throttled - cooldown active'); + return 'standby'; + } + device._lastBatteryModeChange = now; + device._cacheSet('last_commanded_mode', 'standby'); + + device.log('ActionCard: Set Battery to Standby Mode'); + + try { + const { wsManager, url, token } = device; + + // Prefer WebSocket + if (wsManager?.isConnected()) { + wsManager.setBatteryMode('standby'); + device.log('Set mode to standby via WebSocket'); + return 'standby'; + } + + // HTTP fallback: set mode + const response = await api.setMode(url, token, 'standby'); + if (!response) return false; + + // Fetch real battery state + const modeResponse = await api.getMode(url, token); + if (!modeResponse || typeof modeResponse !== 'object') return false; + + // Update cache + device._cacheSet('last_battery_state', { + mode: modeResponse.mode, + permissions: modeResponse.permissions, + battery_count: modeResponse.battery_count ?? 1 + }); + + // Normalize + const normalized = normalizeBatteryMode(modeResponse); + + // Update capability + await updateCapability(device, 'battery_group_charge_mode', normalized); + + // ✅ FIXED: Only trigger flow on actual change + const prev = device._cacheGet('last_battery_mode'); + if (normalized !== prev) { + device.flowTriggerBatteryMode(device, { mode: normalized }); + device._cacheSet('last_battery_mode', normalized); + } + + device.log('Set mode to standby via HTTP'); + return 'standby'; + + } catch (error) { + device.error('Error set mode to standby:', error); + return false; + } + }); + +// ============================================================================ +// ACTION CARD 3: Set Battery to Full Charge Mode +// ============================================================================ + +this.homey.flow + .getActionCard('set-battery-to-full-charge-mode') + .registerRunListener(async ({ device }) => { + if (!device) return false; + + // ✅ RATE LIMITING: Prevent rapid successive calls + const now = Date.now(); + if (now - device._lastBatteryModeChange < device._batteryModeChangeCooldown) { + device.log('⏸️ Battery mode change throttled - cooldown active'); + return 'to_full'; + } + device._lastBatteryModeChange = now; + device._cacheSet('last_commanded_mode', 'to_full'); + + device.log('ActionCard: Set Battery to Full Charge Mode'); + + try { + const { wsManager, url, token } = device; + + // Prefer WebSocket + if (wsManager?.isConnected()) { + wsManager.setBatteryMode('to_full'); + device.log('Set mode to full charge via WebSocket'); + return 'to_full'; + } + + // HTTP fallback + const response = await api.setMode(url, token, 'to_full'); + if (!response) return false; + + // Fetch real battery state + const modeResponse = await api.getMode(url, token); + if (!modeResponse || typeof modeResponse !== 'object') return false; + + // Update cache + device._cacheSet('last_battery_state', { + mode: modeResponse.mode, + permissions: modeResponse.permissions, + battery_count: modeResponse.battery_count ?? 1 + }); + + // Normalize + const normalized = normalizeBatteryMode(modeResponse); + + // Update capability + await updateCapability(device, 'battery_group_charge_mode', normalized); + + // ✅ FIXED: Only trigger flow on actual change + const prev = device._cacheGet('last_battery_mode'); + if (normalized !== prev) { + device.flowTriggerBatteryMode(device, { mode: normalized }); + device._cacheSet('last_battery_mode', normalized); + } + + device.log('Set mode to full charge via HTTP'); + return 'to_full'; + + } catch (error) { + device.error('Error set mode to full charge:', error); + return false; + } + }); + +// ============================================================================ +// ACTION CARD 4: Set Battery to Zero Charge Only Mode +// ============================================================================ + +this.homey.flow + .getActionCard('set-battery-to-zero-charge-only-mode') + .registerRunListener(async ({ device }) => { + if (!device) return false; + + // ✅ RATE LIMITING: Prevent rapid successive calls + const now = Date.now(); + if (now - device._lastBatteryModeChange < device._batteryModeChangeCooldown) { + device.log('⏸️ Battery mode change throttled - cooldown active'); + return 'zero_charge_only'; + } + device._lastBatteryModeChange = now; + device._cacheSet('last_commanded_mode', 'zero_charge_only'); + + device.log('ActionCard: Set Battery to Zero Charge Only Mode'); + + try { + const { wsManager, url, token } = device; + + // Prefer WebSocket + if (wsManager?.isConnected()) { + wsManager.setBatteryMode('zero_charge_only'); + device.log('Set mode to zero_charge_only via WebSocket'); + return 'zero_charge_only'; + } + + // HTTP fallback + const response = await api.setMode(url, token, 'zero_charge_only'); + if (!response) return false; + + // Fetch real battery state + const modeResponse = await api.getMode(url, token); + if (!modeResponse || typeof modeResponse !== 'object') return false; + + // Update cache + device._cacheSet('last_battery_state', { + mode: modeResponse.mode, + permissions: modeResponse.permissions, + battery_count: modeResponse.battery_count ?? 1 + }); + + // Normalize + const normalized = normalizeBatteryMode(modeResponse); + + // Update capability + await updateCapability(device, 'battery_group_charge_mode', normalized); + + // ✅ FIXED: Only trigger flow on actual change + const prev = device._cacheGet('last_battery_mode'); + if (normalized !== prev) { + device.flowTriggerBatteryMode(device, { mode: normalized }); + device._cacheSet('last_battery_mode', normalized); + } + + device.log('Set mode to zero_charge_only via HTTP'); + return 'zero_charge_only'; + + } catch (error) { + device.error('Error set mode to zero_charge_only:', error); + return false; + } + }); + +// ============================================================================ +// ACTION CARD 5: Set Battery to Zero Discharge Only Mode +// ============================================================================ + +this.homey.flow + .getActionCard('set-battery-to-zero-discharge-only-mode') + .registerRunListener(async ({ device }) => { + if (!device) return false; + + // ✅ RATE LIMITING: Prevent rapid successive calls + const now = Date.now(); + if (now - device._lastBatteryModeChange < device._batteryModeChangeCooldown) { + device.log('⏸️ Battery mode change throttled - cooldown active'); + return 'zero_discharge_only'; + } + device._lastBatteryModeChange = now; + device._cacheSet('last_commanded_mode', 'zero_discharge_only'); + + device.log('ActionCard: Set Battery to Zero Discharge Only Mode'); + + try { + const { wsManager, url, token } = device; + + // Prefer WebSocket + if (wsManager?.isConnected()) { + wsManager.setBatteryMode('zero_discharge_only'); + device.log('Set mode to zero_discharge_only via WebSocket'); + return 'zero_discharge_only'; + } + + // HTTP fallback + const response = await api.setMode(url, token, 'zero_discharge_only'); + if (!response) return false; + + // Fetch real battery state + const modeResponse = await api.getMode(url, token); + if (!modeResponse || typeof modeResponse !== 'object') return false; + + // Update cache + device._cacheSet('last_battery_state', { + mode: modeResponse.mode, + permissions: modeResponse.permissions, + battery_count: modeResponse.battery_count ?? 1 + }); + + // Normalize + const normalized = normalizeBatteryMode(modeResponse); + + // Update capability + await updateCapability(device, 'battery_group_charge_mode', normalized); + + // ✅ FIXED: Only trigger flow on actual change + const prev = device._cacheGet('last_battery_mode'); + if (normalized !== prev) { + device.flowTriggerBatteryMode(device, { mode: normalized }); + device._cacheSet('last_battery_mode', normalized); + } + + device.log('Set mode to zero_discharge_only via HTTP'); + return 'zero_discharge_only'; + + } catch (error) { + device.error('Error set mode to zero_discharge_only:', error); + return false; + } + }); + + + + + + + + } // End of _flowListenersRegistered guard + + // this.flowTriggerBatteryMode + + this._flowTriggerBatteryMode = this.homey.flow.getDeviceTriggerCard('battery_mode_changed'); + this._flowTriggerTariff = this.homey.flow.getDeviceTriggerCard('tariff_changed_v2'); + this._flowTriggerImport = this.homey.flow.getDeviceTriggerCard('import_changed_v2'); + this._flowTriggerExport = this.homey.flow.getDeviceTriggerCard('export_changed_v2'); + this._flowTriggerVoltageSag = this.homey.flow.getDeviceTriggerCard('voltage_sag_detected'); + this._flowTriggerVoltageSwell = this.homey.flow.getDeviceTriggerCard('voltage_swell_detected'); + this._flowTriggerPowerFail = this.homey.flow.getDeviceTriggerCard('long_power_fail_detected'); + this._flowTriggerVoltageRestored = this.homey.flow.getDeviceTriggerCard('voltage_restored'); + this._flowTriggerPowerRestored = this.homey.flow.getDeviceTriggerCard('power_restored'); + + // Track voltage state for restoration detection + this._voltageState = { + l1: { abnormal: false, lastAbnormalTime: null }, + l2: { abnormal: false, lastAbnormalTime: null }, + l3: { abnormal: false, lastAbnormalTime: null } + }; + + // Track power state for restoration detection + this._powerState = { + offline: false, + offlineStartTime: null + }; + + + + this._triggerFlowPrevious = {}; + + // Bind handler functions ONCE to avoid creating new function objects on every reconnect (memory leak) + this._boundHandleMeasurement = this._handleMeasurement.bind(this); + this._boundHandleSystem = this._handleSystem.bind(this); + this._boundHandleBatteries = this._handleBatteries.bind(this); + this._boundLog = this.log.bind(this); + this._boundError = this.error.bind(this); + this._boundSetAvailable = this.setAvailable.bind(this); + this._boundGetSetting = this.getSetting.bind(this); + + // this.onPollInterval = setInterval(this.onPoll.bind(this), 1000 * settings.polling_interval); + + this.pollingEnabled = !!settings.use_polling; + + // ✅ Debug logging via settings (toggle in device settings without redeploy) + this._debugLogging = this.getSetting('debug_logging') ?? false; + if (this._debugLogging) this.log('🐛 Debug logging enabled via settings'); + + if (this.pollingEnabled) { + this.log('⚙️ Polling enabled via settings'); + this.startPolling(); + } else { + this.wsManager = new WebSocketManager({ + device: this, + url: this.url, + token: this.token, + log: this._boundLog, + error: this._boundError, + setAvailable: this._boundSetAvailable, + getSetting: this._boundGetSetting, + handleMeasurement: this._boundHandleMeasurement, + handleSystem: this._boundHandleSystem, + handleBatteries: this._boundHandleBatteries, + measurementThrottleMs: (this.getSetting('ws_throttle_ms') || 2) * 1000, + onJournalEvent: (type, deviceId, data) => { + if (type === 'snapshot') wsDebug.snapshot(deviceId, data); + else wsDebug.log(type, deviceId, typeof data === 'string' ? data : JSON.stringify(data)); + }, + }); + + this.wsManager.start(); + } + + if (debug) this._debugInterval = setInterval(() => { + this.log( + 'CPU diag:', + 'ws=', this.wsManager?.isConnected(), + 'poll=', this.pollingEnabled, + 'batteryGroup=', this._phaseOverloadNotificationsEnabled, + 'external=', !!this._cache.external_last_payload, + 'lastWS=', Date.now() - (this.wsManager?.lastMeasurementAt || 0) + ); + }, 60000); // Reduced frequency: every 60s instead of 10s + + + // 🕒 Driver-side watchdog + // 🕒 Driver-side watchdog (ORIGINEEL) + this._wsWatchdog = setInterval(() => { + const staleMeasurement = Date.now() - (this.wsManager?.lastMeasurementAt || 0); + + if (!this.getSettings().use_polling) { + if (staleMeasurement > 190000) { + this.log(`🕒 P1 watchdog: measurement stale >3min (${staleMeasurement}ms), restarting WS`); + this.wsManager?.restartWebSocket(); + } + } + + }, 60000); // check every minute + + + // Overload notification true/false + this._phaseOverloadNotificationsEnabled = this.getSetting('phase_overload_notifications') ?? true; + + this._phaseOverloadState = { + l1: { highCount: 0, notified: false }, + l2: { highCount: 0, notified: false }, + l3: { highCount: 0, notified: false } + }; + + this._cacheFlushInterval = setInterval(async () => { + if (!this._cacheDirty) return; + this._cacheDirty = false; + + // Batch all store operations in parallel instead of sequential awaits + const storePromises = Object.entries(this._cache).map( + ([key, value]) => setStoreValueSafe(this, key, value) + ); + await Promise.all(storePromises).catch(this.error); + }, 30000); + + this._batteryGroupInterval = setInterval(() => { + this._updateBatteryGroup().catch(this.error); + }, 60000); // reduced from 10s to 60s for CPU efficiency + + this._dailyInterval = setInterval(() => { + this._updateDaily().catch(this.error); + }, 60000); // elke minuut + + + } + + _cacheGet(key) { + return this._cache[key]; + } + + _cacheSet(key, value) { + this._cache[key] = (value === undefined ? null : value); + this._cacheDirty = true; + } + + /** + * Public API: Set battery group mode + * Can be called from other drivers (e.g. BatteryPolicyDevice) + */ +async setBatteryGroupMode(targetMode) { + this.log(`🔋 setBatteryGroupMode(${targetMode}) called`); + + // ✅ ADD RATE LIMITING HERE: + const now = Date.now(); + if (now - this._lastBatteryModeChange < this._batteryModeChangeCooldown) { + this.log('⏸️ setBatteryGroupMode throttled - cooldown active'); + return true; // Return success, don't spam the device + } + this._lastBatteryModeChange = now; + this._cacheSet('last_commanded_mode', targetMode); + + try { + const { wsManager, url, token } = this; + + // --- Prefer WebSocket --- + if (wsManager?.isConnected()) { + this.log(`🔌 Setting mode via WebSocket: ${targetMode}`); + wsManager.setBatteryMode(targetMode); + // Add delay for WebSocket command to be processed + await new Promise(resolve => setTimeout(resolve, 500)); + } else { + // --- HTTP fallback --- + this.log(`🌐 Setting mode via HTTP: ${targetMode}`); + const response = await api.setMode(url, token, targetMode); + if (!response) { + this.error('❌ HTTP setMode returned invalid response'); + return false; + } + } + + // --- Fetch updated mode from device with retry --- + let modeResponse; + let retries = 3; + while (retries > 0) { + try { + modeResponse = await api.getMode(url, token); + if (modeResponse) break; + } catch (err) { + this.log(`⚠️ getMode attempt failed (${retries} retries left):`, err.message); + await new Promise(resolve => setTimeout(resolve, 300)); + retries--; + } + } + + if (!modeResponse) { + this.error('❌ Failed to fetch mode after setting'); + return false; + } + + // api.getMode returns a string, so normalize it directly + const normalized = modeResponse; + + // --- Update cache --- + this._cacheSet('last_battery_mode', normalized); + + // --- Update capability --- + await updateCapability(this, 'battery_group_charge_mode', normalized); + + // --- Trigger flow if changed --- + const prev = this._cacheGet('last_battery_mode'); + if (normalized !== prev) { + this.flowTriggerBatteryMode(this, { mode: normalized }); + this._cacheSet('last_battery_mode', normalized); + } + + this.log(`✅ Battery group mode applied: ${normalized}`); + return true; + + } catch (err) { + this.error('❌ Failed to set battery group mode:', err); + return false; + } +} + + + + + flowTriggerBatteryMode(device, tokens) { + this._flowTriggerBatteryMode.trigger(device, tokens).catch(this.error); + } + + + flowTriggerTariff(device, value) { + // this.log(`⚡ Triggering tariff change with value:`, value); + this._flowTriggerTariff.trigger(device, { tariff: value }).catch(this.error); + } + + flowTriggerImport(device, value) { + // this.log(`📥 Triggering import change with value:`, value); + this._flowTriggerImport.trigger(device, { import: value }).catch(this.error); + } + + flowTriggerExport(device, value) { + // this.log(`📤 Triggering export change with value:`, value); + this._flowTriggerExport.trigger(device, { export: value }).catch(this.error); + } + +_getRealtimePluginBatteryData() { + const driver = this.homey.drivers.getDriver('plugin_battery'); + if (!driver) return []; + + const devices = driver.getDevices(); + const result = []; + + for (const dev of devices) { + const id = dev.getData()?.id; + if (!id) continue; + + // Explicit realtime values + const soc = (typeof dev._lastSoC === 'number') + ? dev._lastSoC // 0% is valid + : null; + + const power = (typeof dev._lastPower === 'number') + ? dev._lastPower + : null; + + const capacity = (typeof dev._lastCapacity === 'number' && dev._lastCapacity > 0) + ? dev._lastCapacity + : null; + + const cycles = (typeof dev._lastCycles === 'number') + ? dev._lastCycles + : null; + + result.push({ + id, + soc, + power, + capacity, + cycles, + }); + } + + return result; +} + + +_mergeBatterySources(realtime, group) { + const merged = []; + + for (const rt of realtime) { + const g = group[rt.id] || {}; + + // Normalize capacity to exact unit spec (2688 Wh/unit). + // Firmware rounds to 2.8 kWh; we convert to unit count and back for precision. + const UNIT_KWH = 2.688; + const rawCapacity = (typeof rt.capacity === 'number' && rt.capacity > 0) + ? rt.capacity + : (typeof g.capacity_kwh === 'number' && g.capacity_kwh > 0) + ? g.capacity_kwh + : UNIT_KWH; + const unitCount = Math.max(1, Math.round(rawCapacity / UNIT_KWH)); + const capacity = unitCount * UNIT_KWH; + + const soc = (typeof rt.soc === 'number') + ? rt.soc // realtime 0% is valid + : (typeof g.soc_pct === 'number') + ? g.soc_pct + : 0; + + const power = (typeof rt.power === 'number') + ? rt.power + : (typeof g.power_w === 'number') + ? g.power_w + : 0; + + const cycles = (typeof rt.cycles === 'number') + ? rt.cycles + : (typeof g.cycles === 'number') + ? g.cycles + : 0; + + merged.push({ + id: rt.id, + capacity_kwh: capacity, + soc_pct: soc, + power_w: power, + cycles: cycles, + }); + } + + return merged; +} + + + + +async _updateBatteryGroup() { + const dataObj = this.getData(); + if (!dataObj?.id) return; + + // 1. Realtime pluginBattery data + const realtime = this._getRealtimePluginBatteryData(); + + // 2. Fallback batteryGroup data (cached) + const cachedGroup = this._cacheGet('pluginBatteryGroup_cache'); + const group = cachedGroup || (this.homey.settings.get('pluginBatteryGroup') || {}); + + // Refresh cache every 60s + if (!this._lastBatteryGroupCacheUpdate || Date.now() - this._lastBatteryGroupCacheUpdate > 60000) { + this._cacheSet('pluginBatteryGroup_cache', group); + this._lastBatteryGroupCacheUpdate = Date.now(); + } + + // 3. Merge both sources + const batteries = this._mergeBatterySources(realtime, group); + + const realtimeCount = realtime.length; + const fallbackCount = Object.keys(group).length; + + // 4. Vendor battery_count gate (soft) + const vendorCount = this._cacheGet('last_battery_state')?.battery_count; + + // --- Only remove capabilities if ALL sources agree there is no battery --- + if (vendorCount === 0 && fallbackCount === 0) { + if (debug) this.log('🔋 No battery detected — removing battery capabilities'); + + const caps = [ + 'measure_power.battery_group_power_w', + 'measure_power.battery_group_target_power_w', + 'measure_power.battery_group_max_consumption_w', + 'measure_power.battery_group_max_production_w', + 'battery_group_total_capacity_kwh', + 'battery_group_average_soc', + 'battery_group_state', + 'battery_group_charge_mode' + ]; + + for (const cap of caps) { + if (this.hasCapability(cap)) { + this.removeCapability(cap).catch(this.error); + } + } + + return; + } + + // --- If we have ANY batteries, continue --- + if (batteries.length === 0) return; + + + // 5. Weighted SoC calculation + let totalCapacity = 0; + let weightedSoC = 0; + let totalPower = 0; + + for (const b of batteries) { + const cap = (typeof b.capacity_kwh === 'number' && b.capacity_kwh > 0) + ? b.capacity_kwh + : 1; + + const soc = (typeof b.soc_pct === 'number') + ? b.soc_pct + : 0; + + const power = (typeof b.power_w === 'number') + ? b.power_w + : 0; + + totalCapacity += cap; + weightedSoC += cap * soc; + totalPower += power; + } + + const averageSoC = totalCapacity > 0 + ? Math.round(weightedSoC / totalCapacity) + : 0; + + const chargeState = + totalPower > 20 ? 'charging' : + totalPower < -20 ? 'discharging' : + 'idle'; + + // 6. Update capabilities + await Promise.allSettled([ + updateCapability(this, 'battery_group_total_capacity_kwh', totalCapacity), + updateCapability(this, 'battery_group_average_soc', averageSoC), + updateCapability(this, 'battery_group_state', chargeState), + ]); + + // 7. Vendor-native charge mode update + const lastVendorState = this._cacheGet('last_battery_state'); + + if (lastVendorState && typeof lastVendorState === 'object') { + const normalized = normalizeBatteryMode(lastVendorState); + + // Alleen updaten als de waarde echt veranderd is + const prev = this.getCapabilityValue('battery_group_charge_mode'); + if (prev !== normalized) { + await updateCapability(this, 'battery_group_charge_mode', normalized); + + // Cache bijwerken + this._cacheSet('last_battery_mode', normalized); + + // Flow triggeren + this.flowTriggerBatteryMode(this, { mode: normalized }); + + if (debug) this.log(`🔋 Updated battery_group_charge_mode → ${normalized}`); + } + } +} + + + + + + +_processBatteryGroupChargeMode(data, tasks) { + const group = data.battery_group; + if (!group || !group.charge_mode) return; + + const mode = this.normalizeBatteryMode(group.charge_mode); + + if (this._hasChanged('battery_group_charge_mode', mode)) { + tasks.push(updateCapability(this, 'battery_group_charge_mode', mode)); + } +} + + + +async _updateDaily() { + if (!this._validateMeasurementContext()) return; + + const showGas = this.getSetting('show_gas') === true; + const m = this._cacheGet('last_measurement'); + if (!m) return; + + const nowLocal = this._getLocalTimeSafe(); + const hour = nowLocal.getHours(); + const minute = nowLocal.getMinutes(); + + this._dailyMidnightReset(m, showGas, hour, minute); + await this._dailyElectricity(m); + await this._dailyGas(m, showGas); + await this._dailyGasDelta(showGas, minute); +} + +_getLocalTimeSafe() { + // ✅ CPU FIX: Cache the Intl.DateTimeFormat instance — creating it per call + // is expensive (loads IANA timezone DB each time, called 1440×/day) + if (!this._tzFormatter) { + this._tzFormatter = new Intl.DateTimeFormat('sv-SE', { + timeZone: 'Europe/Brussels', + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', + hour12: false, + }); + } + const parts = this._tzFormatter.formatToParts(new Date()); + const p = {}; + for (const { type, value } of parts) p[type] = value; + return new Date(`${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}`); +} + +_dailyMidnightReset(m, showGas, hour, minute) { + if (hour === 0 && minute === 0) { + if (typeof m.energy_import_kwh === 'number') { + this._cacheSet('meter_start_day', m.energy_import_kwh); + } + + const lastExternal = this._cacheGet('external_last_result'); + const gas = lastExternal?.gas; + + if (showGas && typeof gas?.value === 'number') { + this._cacheSet('gasmeter_start_day', gas.value); + } + } +} + +async _dailyElectricity(m) { + const meterStart = this._cacheGet('meter_start_day'); + if (meterStart != null && typeof m.energy_import_kwh === 'number') { + const dailyImport = m.energy_import_kwh - meterStart; + const cur = this.getCapabilityValue('meter_power.daily'); + if (cur !== dailyImport) { + await updateCapability(this, 'meter_power.daily', dailyImport).catch(this.error); + } + } +} + +async _dailyGas(m, showGas) { + if (!showGas) return; + + const lastExternal = this._cacheGet('external_last_result'); + const gas = lastExternal?.gas; + const gasStart = this._cacheGet('gasmeter_start_day'); + + if (gas?.value != null && gasStart != null) { + const gasDiff = gas.value - gasStart; + const cur = this.getCapabilityValue('meter_gas.daily'); + if (cur !== gasDiff) { + await updateCapability(this, 'meter_gas.daily', gasDiff).catch(this.error); + } + } +} + +async _dailyGasDelta(showGas, minute) { + if (!showGas || minute % 5 !== 0) return; + + const lastMinute = this._cacheGet('last_gas_delta_minute'); + if (lastMinute === minute) return; + + this._cacheSet('last_gas_delta_minute', minute); + + const lastExternal = this._cacheGet('external_last_result'); + const gas = lastExternal?.gas; + if (!gas || typeof gas.value !== 'number') return; + + const prevTimestamp = this._cacheGet('gasmeter_previous_reading_timestamp'); + + if (prevTimestamp == null) { + this._cacheSet('gasmeter_previous_reading_timestamp', gas.timestamp); + return; + } + + if (gas.timestamp === prevTimestamp) return; + + const prevReading = this._cacheGet('gasmeter_previous_reading'); + + if (typeof prevReading === 'number') { + const delta = gas.value - prevReading; + if (delta >= 0) { + const cur = this.getCapabilityValue('measure_gas'); + if (cur !== delta) { + await updateCapability(this, 'measure_gas', delta).catch(this.error); + } + } + } + + this._cacheSet('gasmeter_previous_reading', gas.value); + this._cacheSet('gasmeter_previous_reading_timestamp', gas.timestamp); +} + + +async _handleExternalMeters(external) { + const tasks = []; + + // Single pass through external meters - extract latest for each type + const latest = {}; + let gasExists = false; + let waterExists = false; + + for (const meter of (external ?? [])) { + if (meter.type === 'gas_meter') { + gasExists = true; + if (meter.value != null && meter.timestamp != null) { + const current = latest['gas_meter']; + if (!current || meter.timestamp > current.timestamp) { + latest['gas_meter'] = meter; + } + } + } else if (meter.type === 'water_meter') { + waterExists = true; + if (meter.value != null && meter.timestamp != null) { + const current = latest['water_meter']; + if (!current || meter.timestamp > current.timestamp) { + latest['water_meter'] = meter; + } + } + } + } + + const gas = latest['gas_meter']; + const water = latest['water_meter']; + + // GAS CAPABILITY MANAGEMENT (structural) + if (gasExists && !this.hasCapability('meter_gas')) { + tasks.push(safeAddCapability(this, 'meter_gas').catch(this.error)); + } + if (!gasExists && this.hasCapability('meter_gas')) { + tasks.push(this.removeCapability('meter_gas').catch(this.error)); + this.log('Removed meter_gas — no gas meter found.'); + } + + // GAS VALUE UPDATE (data) + if (gasExists && gas && this.getCapabilityValue('meter_gas') !== gas.value) { + tasks.push(this.setCapabilityValue('meter_gas', gas.value).catch(this.error)); + } + + // WATER CAPABILITY MANAGEMENT (structural) + if (waterExists && !this.hasCapability('meter_water')) { + tasks.push(safeAddCapability(this, 'meter_water').catch(this.error)); + } + if (!waterExists && this.hasCapability('meter_water')) { + tasks.push(this.removeCapability('meter_water').catch(this.error)); + this.log('Removed meter_water — no water meter found.'); + } + + // WATER VALUE UPDATE (data) + if (waterExists && water && this.getCapabilityValue('meter_water') !== water.value) { + tasks.push(this.setCapabilityValue('meter_water', water.value).catch(this.error)); + } + + await Promise.all(tasks); + + return { gas, water }; +} + +async _handleMeasurement(m) { + if (!this._validateMeasurementContext()) return; + + const now = Date.now(); + const settings = this.getSettings(); + const showGas = settings.show_gas === true; + + // Safely get language, default to 'en' if app instance is destroyed + let homeyLang = 'en'; + try { + homeyLang = this.homey.i18n.getLanguage(); + } catch (err) { + this.log('⚠️ Could not get language (app destroyed?), defaulting to English'); + } + + this._measurementCache(m, now); + const tasks = []; + + this._measurementPower(m, tasks); + this._measurementPhases(m, tasks, settings, homeyLang); + this._measurementFullRefresh(m, tasks, now); + this._measurementFlows(m, now); + this._measurementNetPower(m, tasks); + + const { gas, water } = await this._measurementExternalMeters(m, tasks); + await this._measurementGasWater(gas, water, tasks, showGas); + + if (tasks.length > 0) { + await Promise.allSettled(tasks); + } +} + +_validateMeasurementContext() { + if (this.__deleted) return false; + + const dataObj = this.getData(); + if (!dataObj || !dataObj.id) { + this.log('⚠️ Ignoring measurement: device no longer exists'); + return false; + } + + // Check if app instance is still valid (not destroyed) + if (!this.homey) { + this.log('⚠️ Ignoring measurement: app instance has been destroyed'); + return false; + } + + return true; +} + +_measurementCache(m, now) { + this._cacheSet('last_measurement', m); + this.lastMeasurementAt = now; +} + +_measurementPower(m, tasks) { + const cap = (name, value) => { + const cur = this.getCapabilityValue(name); + if (cur !== value) { + tasks.push(updateCapability(this, name, value).catch(this.error)); + } + }; + + const currentPower = this.getCapabilityValue('measure_power'); + if (currentPower !== m.power_w) { + cap('measure_power', m.power_w); + cap('measure_power.l1', m.power_l1_w); + cap('measure_power.l2', m.power_l2_w); + cap('measure_power.l3', m.power_l3_w); + } + + // Always feed baseload monitor, even when power unchanged. + // If battery holds grid at 0W constantly, the value never changes but we + // still need periodic samples for the night window. + this._onNewPowerValue(m.power_w); +} + +_onNewPowerValue(gridPower) { + const app = this.homey.app; + if (app.baseloadMonitor) { + // Primary: plugin_battery devices (read directly from battery API — most accurate) + let batteryPower = null; + let batterySource = null; + try { + const battDriver = this.homey.drivers.getDriver('plugin_battery'); + if (battDriver) { + let total = 0; + let count = 0; + for (const dev of battDriver.getDevices()) { + total += dev.getCapabilityValue('measure_power') || 0; + count++; + } + if (count > 0) { + batteryPower = total; + batterySource = `plugin_battery(${count})`; + } + } + } catch (_) {} + // Fallback: P1 API capability (only when no plugin_battery devices present) + if (batterySource === null) { + const p1batt = this.getCapabilityValue('measure_power.battery_group_power_w'); + if (typeof p1batt === 'number') { + batteryPower = p1batt; + batterySource = 'p1api'; + } + } + app.baseloadMonitor.updatePowerFromDevice(this, gridPower, batteryPower); + } +} + +_measurementPhases(m, tasks, settings, homeyLang) { + const cap = (name, value) => { + const cur = this.getCapabilityValue(name); + if (cur !== value) { + tasks.push(updateCapability(this, name, value).catch(this.error)); + } + }; + + if (m.current_l1_a !== undefined) { + const load1 = Math.abs((m.current_l1_a / settings.grid_phase_amps) * 100); + cap('net_load_phase1_pct', load1); + this._handlePhaseOverload('l1', load1, homeyLang); + } + + if (m.current_l2_a !== undefined) { + const load2 = Math.abs((m.current_l2_a / settings.grid_phase_amps) * 100); + cap('net_load_phase2_pct', load2); + this._handlePhaseOverload('l2', load2, homeyLang); + } + + if (m.current_l3_a !== undefined) { + const load3 = Math.abs((m.current_l3_a / settings.grid_phase_amps) * 100); + cap('net_load_phase3_pct', load3); + this._handlePhaseOverload('l3', load3, homeyLang); + } +} + +_measurementFullRefresh(m, tasks, now) { + // ✅ CPU FIX: Raised from 10s to 30s — applyMeasurementCapabilities iterates + // over ~25 capabilities and calls getCapabilityValue() on each; at 10s that + // was 6×/min of 25+ Homey API calls. 30s cuts this work by 66%. + if (!this._lastFullUpdate || now - this._lastFullUpdate > 30000) { + tasks.push(applyMeasurementCapabilities(this, m).catch(this.error)); + this._lastFullUpdate = now; + } +} + +_measurementFlows(m, now) { + // ✅ CPU FIX: Rate-limit flow triggers to 60s + // energy_import_kwh changes every second at any load → was firing 12×/min + if (!this._lastFlowTrigger || now - this._lastFlowTrigger > 60000) { + + if (typeof m.energy_import_kwh === 'number' && + this._triggerFlowPrevious.import !== m.energy_import_kwh) { + this._triggerFlowPrevious.import = m.energy_import_kwh; + this.flowTriggerImport(this, m.energy_import_kwh); + } + + if (typeof m.energy_export_kwh === 'number' && + this._triggerFlowPrevious.export !== m.energy_export_kwh) { + this._triggerFlowPrevious.export = m.energy_export_kwh; + this.flowTriggerExport(this, m.energy_export_kwh); + } + + if (typeof m.tariff !== 'undefined') { + const newTariff = Number(m.tariff); + const prevTariff = this._triggerFlowPrevious.tariff; + + if (prevTariff !== newTariff) { + this._triggerFlowPrevious.tariff = newTariff; + this.flowTriggerTariff(this, newTariff); + } + } + + + this._lastFlowTrigger = now; + } +} + +_measurementNetPower(m, tasks) { + if (m.energy_import_kwh !== undefined && m.energy_export_kwh !== undefined) { + const net = m.energy_import_kwh - m.energy_export_kwh; + const cur = this.getCapabilityValue('meter_power'); + if (cur !== net) { + tasks.push(updateCapability(this, 'meter_power', net).catch(this.error)); + } + } +} + +async _measurementExternalMeters(m, tasks) { + let gas = null; + let water = null; + + const prevHash = this._cacheGet('external_last_hash') ?? null; + const newHash = this._hashExternal(m.external); + + if (prevHash === newHash) { + // Geen verandering → gebruik cache + const lastResult = this._cacheGet('external_last_result'); + gas = lastResult?.gas ?? null; + water = lastResult?.water ?? null; + } else { + // Verandering → opnieuw verwerken + const result = await this._handleExternalMeters(m.external); + gas = result.gas; + water = result.water; + + this._cacheSet('external_last_payload', m.external); + this._cacheSet('external_last_result', result); + this._cacheSet('external_last_hash', newHash); + } + + return { gas, water }; +} + + +async _measurementGasWater(gas, water, tasks, showGas) { + if (!showGas) { + if (this.hasCapability('meter_gas')) tasks.push(this.removeCapability('meter_gas').catch(this.error)); + if (this.hasCapability('measure_gas')) tasks.push(this.removeCapability('measure_gas').catch(this.error)); + if (this.hasCapability('meter_gas.daily')) tasks.push(this.removeCapability('meter_gas.daily').catch(this.error)); + return; + } + + // (No extra logic — everything happens in _handleExternalMeters) +} + + + + +_handleSystem(data) { + // this.log('⚙️ System data received:', data); + if (!this.getData() || !this.getData().id) { + this.log('⚠️ Ignoring system event: device no longer exists'); + return; + } + + // Update wifi rssi and wifi text + if (typeof data.wifi_rssi_db === 'number') { + if (this.hasCapability('rssi')) { + updateCapability(this, 'rssi', data.wifi_rssi_db).catch(this.error); + const wifiQuality = getWifiQuality(data.wifi_rssi_db); + updateCapability(this, 'wifi_quality', wifiQuality).catch(this.error); + } + + } + + +} + +async _ensureBatteryCapabilities() { + const caps = [ + 'measure_power.battery_group_power_w', + 'measure_power.battery_group_target_power_w', + 'measure_power.battery_group_max_consumption_w', + 'measure_power.battery_group_max_production_w', + 'battery_group_total_capacity_kwh', + 'battery_group_average_soc', + 'battery_group_state', + 'battery_group_charge_mode' + ]; + + for (const cap of caps) { + try { + await safeAddCapability(this, cap); + } catch (err) { + this.error(`❌ Failed to ensure capability "${cap}":`, err); + } + } +} + + +async _handleBatteries(data) { + try { + if (debug) this.log('⚡ Battery event data:', data); + + // --- Device existence guard --- + // ✅ CPU FIX: Removed pointless getDriver/getDevice lookup — _handleBatteries + // already runs on the device instance itself, no need to look it up again. + const dataObj = this.getData(); + if (!dataObj?.id) return; + + // --- Normalize payload --- + const battery = Array.isArray(data) ? data[0] : data; + const payload = typeof battery === 'string' + ? { ...data, mode: battery, permissions: [] } + : battery; + + if (debug && payload.battery_count != 0) { + this.log('⚡ Battery event payload:', payload); + } + + // --- Normalize mode --- + const normalizedMode = normalizeBatteryMode(payload); + const lastBatteryMode = this._cacheGet('last_battery_mode'); + + // --- Firmware fallback: to_full but power_w = 0 --- + if (normalizedMode === 'to_full' && (payload.power_w == null || payload.power_w === 0)) { + const prev = this._cacheGet('last_battery_state') || {}; + + const batteryCount = + (typeof payload.battery_count === 'number' && payload.battery_count > 0) + ? payload.battery_count + : (typeof prev.battery_count === 'number' && prev.battery_count > 0) + ? prev.battery_count + : 1; + + const fallbackPower = batteryCount * 800; + + this.log( + `⚠️ Firmware bug detected: power_w=0 in to_full. Applying fallback ${fallbackPower}W (battery_count=${batteryCount})` + ); + + payload.power_w = fallbackPower; + } + + // --- Update capability battery_group_charge_mode --- + try { + await updateCapability(this, 'battery_group_charge_mode', normalizedMode); + } catch (err) { + this.error('❌ Failed to update battery_group_charge_mode:', err); + } + + // --- Trigger flow only on real mode change --- + if (normalizedMode !== lastBatteryMode) { + const lastCommanded = this._cacheGet('last_commanded_mode'); + if (lastCommanded != null && normalizedMode !== lastCommanded) { + this.log(`⚠️ External battery mode override detected! Commanded: ${lastCommanded} → actual: ${normalizedMode}`); + const extKey = 'external_mode_overrides'; + const stored = this.homey.settings.get(extKey) || { count: 0, last: null }; + stored.count += 1; + stored.last = { from: lastCommanded, to: normalizedMode, ts: new Date().toISOString() }; + this.homey.settings.set(extKey, stored); + } + + this.flowTriggerBatteryMode(this); + this._cacheSet('last_battery_mode', normalizedMode); + + // ✅ CPU FIX: fire-and-forget — setSettings writes to persistent storage + // and blocks the event loop if awaited on every battery update + this.setSettings({ mode: normalizedMode }).catch(err => { + this.error('❌ Failed to update setting "mode":', err); + }); + } + + // --- Update battery power capabilities --- + // ✅ CPU FIX: Run in parallel instead of 4 sequential awaits + await Promise.allSettled([ + this._setCapabilityValue('measure_power.battery_group_power_w', payload.power_w ?? 0), + this._setCapabilityValue('measure_power.battery_group_target_power_w', payload.target_power_w ?? 0), + this._setCapabilityValue('measure_power.battery_group_max_consumption_w', payload.max_consumption_w ?? 0), + this._setCapabilityValue('measure_power.battery_group_max_production_w', payload.max_production_w ?? 0), + ]); + + // --- Store raw WS battery state for condition cards --- + const prev = this._cacheGet('last_battery_state') || {}; + + this._cacheSet('last_battery_state', { + mode: payload.mode ?? prev.mode, + permissions: Array.isArray(payload.permissions) + ? payload.permissions + : prev.permissions, + battery_count: (typeof payload.battery_count === 'number') + ? payload.battery_count + : prev.battery_count + }); + + // --- Battery error detection --- + const group = this.homey.settings.get('pluginBatteryGroup') || {}; + const batteries = Object.values(group); + + const isGridReturn = (payload.power_w ?? 0) < -400; + const batteriesPresent = batteries.length > 0; + const shouldBeCharging = (payload.target_power_w ?? 0) > 0; + const isNotStandby = normalizedMode !== 'standby'; + + const now = Date.now(); + + if (isGridReturn && batteriesPresent && shouldBeCharging && isNotStandby) { + if (!this.gridReturnStart) this.gridReturnStart = now; + + const duration = now - this.gridReturnStart; + + if (duration > 30000 && !this.batteryErrorTriggered) { + this.batteryErrorTriggered = true; + + this.log('❌ Battery error: batteries should be charging and grid is receiving power'); + + this.homey.flow + .getDeviceTriggerCard('battery_error_detected') + .trigger(this, {}, { + power: payload.power_w, + target: payload.target_power_w, + mode: normalizedMode, + batteryCount: batteries.length + }) + .catch(this.error); + } + + } else { + this.gridReturnStart = null; + this.batteryErrorTriggered = false; + } + + } catch (err) { + this.error('❌ _handleBatteries failed:', err); + } +} + + + + + + + + startPolling() { + if (this.wsActive || this.onPollInterval) return; + + const interval = this.getSettings().polling_interval || 10; + this.log(`⏱️ Polling gestart met interval: ${interval}s`); + + this.onPollInterval = setInterval(this.onPoll.bind(this), 1000 * interval); + } + + + + onUninit() { + // Cleanup intervals and timers when app stops/crashes + this.__deleted = true; + + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + if (this._wsReconnectTimeout) { + clearTimeout(this._wsReconnectTimeout); + this._wsReconnectTimeout = null; + } + if (this._wsWatchdog) { + clearInterval(this._wsWatchdog); + this._wsWatchdog = null; + } + if (this._cacheFlushInterval) { + clearInterval(this._cacheFlushInterval); + this._cacheFlushInterval = null; + } + if (this._batteryGroupInterval) { + clearInterval(this._batteryGroupInterval); + this._batteryGroupInterval = null; + } + if (this._dailyInterval) { + clearInterval(this._dailyInterval); + this._dailyInterval = null; + } + if (this._debugInterval) { + clearInterval(this._debugInterval); + this._debugInterval = null; + } + if (this.wsManager) { + this.wsManager.stop(); + this.wsManager = null; + } + + // Unregister flow card listeners to prevent memory leak + if (this._flowListenerReferences) { + for (const listener of this._flowListenerReferences) { + try { + listener.unregister?.(); + } catch (_) {} + } + this._flowListenerReferences = null; + } + } + + onDeleted() { + // Unregister from baseload monitor (only on explicit device deletion) + const app = this.homey.app; + if (app.baseloadMonitor) { + app.baseloadMonitor.unregisterP1Device(this); + } + + // Call onUninit to cleanup timers/intervals + this.onUninit(); + } + +async onDiscoveryAvailable(discoveryResult) { + const newIP = discoveryResult.address; + + // Check if manual IP is set - if so, ignore discovery + const manualIP = this.getSetting('manual_ip'); + if (manualIP) { + this.log(`🌐 Discovery: Manual IP (${manualIP}) is set — ignoring discovery IP ${newIP}`); + return; + } + + // Eerste keer discovery → IP opslaan + if (!this._lastDiscoveryIP) { + this._lastDiscoveryIP = newIP; + this.url = `https://${newIP}`; + this.log(`🌐 Discovery: initial IP set to ${newIP}`); + await this.setSettings({ url: this.url }).catch(this.error); + } + + // IP is NIET veranderd → niets doen + if (this._lastDiscoveryIP === newIP) { + this.log(`🌐 Discovery: IP unchanged (${newIP}) — ignoring`); + return; + } + + // IP is WEL veranderd → update + restart + this._lastDiscoveryIP = newIP; + this.url = `https://${newIP}`; + this.log(`🌐 Discovery: IP changed → ${newIP}`); + await this.setSettings({ url: this.url }).catch(this.error); + + // Debounce reconnect + if (this._wsReconnectTimeout) clearTimeout(this._wsReconnectTimeout); + this._wsReconnectTimeout = setTimeout(async () => { + + if (this.pollingEnabled) { + this.log('🔁 Discovery: polling active — skipping WS reconnect'); + return; + } + + // Preflight reachability check + try { + const res = await fetchWithTimeout(`${this.url}/api/system`, { + headers: { Authorization: `Bearer ${this.token}` }, + agent + }, 3000); + + if (!res || typeof res.cloud_enabled === 'undefined') { + this.error(`❌ Discovery: device at ${this.url} unreachable — skipping WS`); + return; + } + + this.log('🔁 Discovery: IP changed & reachable — restarting WebSocket'); + await this.setAvailable(); + this.wsManager?.restartWebSocket(); + + } catch (err) { + this.error(`❌ Discovery preflight failed — ${err.message}`); + } + + }, 500); +} + + + + + +async onDiscoveryAddressChanged(discoveryResult) { + const newIP = discoveryResult.address; + + // Check if manual IP is set - if so, ignore discovery + const manualIP = this.getSetting('manual_ip'); + if (manualIP) { + this.log(`🌐 AddressChanged: Manual IP (${manualIP}) is set — ignoring discovery IP ${newIP}`); + return; + } + + // Only respond if the IP actually changed + if (this._lastDiscoveryIP === newIP) { + this.log(`🌐 AddressChanged: IP unchanged (${newIP}) — ignoring`); + return; + } + + // IP is veranderd → opslaan + settings bijwerken + this._lastDiscoveryIP = newIP; + this.url = `https://${newIP}`; + this.log(`🌐 Address changed — new URL: ${this.url}`); + await this.setSettings({ url: this.url }).catch(this.error); + + // Debounce reconnect + if (this._wsReconnectTimeout) clearTimeout(this._wsReconnectTimeout); + this._wsReconnectTimeout = setTimeout(() => { + if (!this.getSettings().use_polling) { + this.log('🔁 Address change: restarting WebSocket'); + this.wsManager?.restartWebSocket(); + } else { + this.log('🔁 Address change: polling active — skipping WS reconnect'); + } + }, 500); +} + + +async onDiscoveryLastSeenChanged(discoveryResult) { + const newIP = discoveryResult.address; + + // Update IP only if changed + if (this._lastDiscoveryIP !== newIP) { + this._lastDiscoveryIP = newIP; + this.url = `https://${newIP}`; + this.log(`📡 Device seen again — IP updated: ${newIP}`); + await this.setSettings({ url: this.url }).catch(this.error); + } else { + this.log(`📡 Device seen again — IP unchanged (${newIP})`); + } + + await this.setAvailable(); + + // Debounce reconnect + if (this._wsReconnectTimeout) clearTimeout(this._wsReconnectTimeout); + this._wsReconnectTimeout = setTimeout(() => { + + if (this.pollingEnabled) { + this.log('🔁 LastSeen: polling active — skipping WS reconnect'); + return; + } + + // Only restart WS if it is NOT connected + if (!this.wsManager?.isConnected()) { + this.log('🔁 LastSeen: WS not connected → restarting WebSocket'); + this.wsManager?.restartWebSocket(); + } else { + this.log('📡 LastSeen: WS already connected — ignoring'); + } + + }, 500); +} + + + + + + /** + * Helper function to update capabilities configuration. + * This function is called when the device is initialized. + */ + async _updateCapabilities() { + if (!this.hasCapability('identify')) { + await safeAddCapability(this, 'identify').catch(this.error); + console.log(`created capability identify for ${this.getName()}`); + } + + if (!this.hasCapability('measure_power')) { + await safeAddCapability(this, 'measure_power').catch(this.error); + console.log(`created capability measure_power for ${this.getName()}`); + } + + + + // Remove capabilities that are not needed + if (this.hasCapability('measure_power.power_w')) { + await this.removeCapability('measure_power.power_w').catch(this.error); + console.log(`removed capability measure_power.power_w for ${this.getName()}`); + } + + if (this.hasCapability('meter_power.returned.t1')) { + await this.removeCapability('meter_power.returned.t1').catch(this.error); + console.log(`removed capability meter_power.returned.t1 for ${this.getName()}`); + } + + if (this.hasCapability('meter_power.returned.t2')) { + await this.removeCapability('meter_power.returned.t2').catch(this.error); + console.log(`removed capability meter_power.returned.t2 for ${this.getName()}`); + } + + } + + /** + * Helper function to register capability listeners. + * This function is called when the device is initialized. + */ +async _registerCapabilityListeners() { + + // Existing listener + this.registerCapabilityListener('identify', async () => { + await api.identify(this.url, this.token); + }); + + // Battery mode picker listener + this.registerCapabilityListener('battery_group_charge_mode', async (value) => { + // Track capability API mode changes (Homey UI, 3rd party apps, external Flows) + // Does NOT count energy_v2 own action cards or battery-policy (they bypass this listener) + // High counts = likely a 3rd party app repeatedly overriding the battery mode + const _extKey = 'capability_api_mode_changes'; + const _extStored = this.homey.settings.get(_extKey) || { count: 0, last: null }; + _extStored.count += 1; + _extStored.last = { mode: value, ts: new Date().toISOString() }; + this.homey.settings.set(_extKey, _extStored); + wsDebug.log('capability_api_mode_change', this.getData().id, + `mode="${value}" count=${_extStored.count}`); + this.log(`⚠️ capability_api_mode_change #${_extStored.count} → ${value} (Homey capability API: UI, 3rd party app, or external Flow)`); + + // Rate limiting + const now = Date.now(); + if (now - this._lastBatteryModeChange < this._batteryModeChangeCooldown) { + this.log('⏸️ Battery mode change throttled - cooldown active'); + return value; // Return the requested value, don't fail + } + this._lastBatteryModeChange = now; + this._cacheSet('last_commanded_mode', value); + + try { + const { wsManager, url, token } = this; + + // 1. Prefer WebSocket + if (wsManager?.isConnected()) { + wsManager.setBatteryMode(value); + this.log(`Set battery mode via WS → ${value}`); + } else { + // 2. HTTP fallback + const response = await api.setMode(url, token, value); + if (!response) { + this.log(`⚠️ Invalid response from setMode(${value})`); + return false; + } + } + + // 3. Fetch real vendor state + const modeResponse = await api.getMode(url, token); + + if (!modeResponse) { + this.log('⚠️ Invalid battery mode response after UI change:', modeResponse); + return false; + } + + // 4. Normalize (string OR object) + const normalized = normalizeBatteryMode(modeResponse); + + // 5. Update cache in object-safe form + this._cacheSet('last_battery_state', { + mode: typeof modeResponse === 'object' ? modeResponse.mode : normalized, + permissions: typeof modeResponse === 'object' ? modeResponse.permissions : [], + battery_count: typeof modeResponse === 'object' + ? (modeResponse.battery_count ?? 1) + : 1 + }); + + // 6. Update capability to the *real* vendor state + await updateCapability(this, 'battery_group_charge_mode', normalized); + + // 7. Trigger flow if changed + const prev = this._cacheGet('last_battery_mode'); + if (normalized !== prev) { + this.flowTriggerBatteryMode(this, { mode: normalized }); + this._cacheSet('last_battery_mode', normalized); + } + + return normalized; + + } catch (err) { + this.error('❌ Failed to set battery_group_charge_mode via UI:', err); + return false; + } + }); +} + + + /** + * Helper function for 'optional' capabilities. + * This function is called when the device is initialized. + * It will create the capability if it doesn't exist. + * + * We do not remove capabilities here, as we assume the user may want to keep them. + * Besides that we assume that the P1 Meter is connected to a smart meter that does not change often. + * + * @param {string} capability The capability to set + * @param {*} value The value to set + * @returns {Promise} A promise that resolves when the capability is set + */ +async _setCapabilityValue(capability, value) { + if (value === undefined) return; + + // Only update if the capability exists + if (!this.hasCapability(capability)) return; + + await this.setCapabilityValue(capability, value).catch(this.error); +} + + + /** + * Helper function to trigger flows on change. + * This function is called when the device is initialized. + * + * We use this function to trigger flows when the value changes. + * We store the previous value in a variable. + * + * @param {*} flow_id Flow ID name + * @param {*} value The value to check for changes + * @returns {Promise} A promise that resolves when the flow is triggered + */ + async _triggerFlowOnChange(flow_id, value) { + if (!Number.isFinite(value)) { + this.log(`⚠️ Skipping flow "${flow_id}" — invalid or missing value:`, value); + return; + } + + this._triggerFlowPrevious = this._triggerFlowPrevious || {}; + + if (this._triggerFlowPrevious[flow_id] === undefined) { + this._triggerFlowPrevious[flow_id] = value; + // await setStoreValueSafe(this, `last_${flow_id}`, value); + this._cacheSet(`last_${flow_id}`, value); + + return; + } + + if (this._triggerFlowPrevious[flow_id] === value) { + return; + } + + const card = this.homey.flow.getDeviceTriggerCard(flow_id); + if (!card) { + this.error(`❌ Flow card "${flow_id}" not found`); + return; + } + + this._triggerFlowPrevious[flow_id] = value; + + this.log(`🚀 Triggering flow "${flow_id}" with value:`, value); + this.log(`📦 Token payload:`, { [flow_id]: value }); + + await card.trigger(this, {}, { [flow_id]: value }).catch(this.error); + // await setStoreValueSafe(this, `last_${flow_id}`, value); + this._cacheSet(`last_${flow_id}`, value); + } + + + + // onPoll method if websocket is to heavy for Homey unit + async onPoll() { + if (this.__deleted) return; // Skip if device is deleted/uninit + + const settings = this.getSettings(); + + // 1. Restore URL if runtime is empty + if (!this.url) { + if (settings.url) { + this.url = settings.url; + } else { + await this.setUnavailable('Missing URL'); + return; + } + } + + // 2. Sync settings if discovery changed the URL + if (this.url && this.url !== settings.url) { + await this.setSettings({ url: this.url }).catch(this.error); + } + + try { + + const [measurement, system, batteries] = await Promise.all([ + api.getMeasurement(this.url, this.token), + api.getSystem(this.url, this.token), + api.getMode(this.url, this.token), + ]); + + // Reuse websocket based measurement capabilities code + if (measurement) { + await this._handleMeasurement(measurement); + + // Reuse websocket based external measurement capabilities code (gas and water) + if (measurement.external) { + await this._handleExternalMeters(measurement.external); + } + } + + // Reuse websocket based system capabilities code + if (system) { + await this._handleSystem(system); + } + + // console.log(batteries); + // Reuse websocket based battery capabilities code + if (batteries) { + await this._handleBatteries(batteries); + } + + await this.setAvailable(); + + } catch (err) { + if (!this.__deleted) { + this.log(`Polling error: ${err.message}`); + this.setUnavailable(err.message || 'Polling error').catch(this.error); + } + } + } + + _handlePhaseOverload(phaseKey, loadPct, lang) { + const state = this._phaseOverloadState[phaseKey]; + + // Debounce: 3 opeenvolgende samples boven 97% + if (loadPct > 97) { + state.highCount++; + + if (!state.notified && state.highCount >= 3 && this._phaseOverloadNotificationsEnabled) { + const phaseNum = phaseKey.replace('l', ''); // l1 → 1 + const msg = lang === 'nl' + ? `Fase ${phaseNum} overbelast (${loadPct.toFixed(0)}%)` + : `Phase ${phaseNum} overloaded (${loadPct.toFixed(0)}%)`; + + this.homey.notifications.createNotification({ excerpt: msg }).catch(this.error); + state.notified = true; + } + } else { + // Hysterese: reset pas onder 85% + if (loadPct < 85) { + state.highCount = 0; + state.notified = false; + } + } +} + + async onSettings(MySettings) { + this.log('Settings updated'); + this.log('Settings:', MySettings); + // Update interval polling + if ('polling_interval' in MySettings.oldSettings + && MySettings.oldSettings.polling_interval !== MySettings.newSettings.polling_interval + ) { + this.log('Polling_interval for P1 changed to:', MySettings.newSettings.polling_interval); + clearInterval(this.onPollInterval); + // this.onPollInterval = setInterval(this.onPoll.bind(this), MySettings.newSettings.polling_interval * 1000); + this.onPollInterval = setInterval(this.onPoll.bind(this), 1000 * this.getSettings().polling_interval); + } + if ('mode' in MySettings.oldSettings + && MySettings.oldSettings.mode !== MySettings.newSettings.mode + ) { + this.log('Mode for Plugin Battery via P1 advanced settings changed to:', MySettings.newSettings.mode); + try { + await api.setMode(this.url, this.token, MySettings.newSettings.mode); + } catch (err) { + this.log('Failed to set mode:', err.message); + } + } + + if ('cloud' in MySettings.oldSettings + && MySettings.oldSettings.cloud !== MySettings.newSettings.cloud + ) { + this.log('Cloud connection in advanced settings changed to:', MySettings.newSettings.cloud); + + try { + if (MySettings.newSettings.cloud == 1) { + await api.setCloudOn(this.url, this.token); + } else if (MySettings.newSettings.cloud == 0) { + await api.setCloudOff(this.url, this.token); + } + } catch (err) { + this.log('Failed to update cloud setting:', err.message); + } + } + + if (MySettings.changedKeys.includes('use_polling')) { + this.log(`⚙️ use_polling gewijzigd naar: ${MySettings.newSettings.use_polling}`); + + // ⭐ FIX: update runtime flag + this.pollingEnabled = MySettings.newSettings.use_polling; + + if (MySettings.newSettings.use_polling) { + this.wsManager?.stop(); // cleanly stop WebSocket + this.startPolling(); + } else { + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + + if (!this.wsManager) { + this.wsManager = new WebSocketManager({ + url: this.url, + token: this.token, + log: this._boundLog, + error: this._boundError, + setAvailable: this._boundSetAvailable, + getSetting: this._boundGetSetting, + handleMeasurement: this._boundHandleMeasurement, + handleSystem: this._boundHandleSystem, + handleBatteries: this._boundHandleBatteries, + measurementThrottleMs: (this.getSetting('ws_throttle_ms') || 2) * 1000, + onJournalEvent: (type, deviceId, data) => { + if (type === 'snapshot') wsDebug.snapshot(deviceId, data); + else wsDebug.log(type, deviceId, typeof data === 'string' ? data : JSON.stringify(data)); + }, + }); + } + + //this.wsManager.start(); + this.wsManager.resume(); + } + + } + + if ('baseload_notifications' in MySettings.newSettings) { + this._baseloadNotificationsEnabled = MySettings.newSettings.baseload_notifications; + const app = this.homey.app; + if (app.baseloadMonitor) { + app.baseloadMonitor.setNotificationsEnabledForDevice(this, this._baseloadNotificationsEnabled); + } + this.log('Baseload notifications changed to:', this._baseloadNotificationsEnabled); + } + + if ('phase_overload_notifications' in MySettings.newSettings) { + this._phaseOverloadNotificationsEnabled = MySettings.newSettings.phase_overload_notifications; + this.log('Phase overload notifications changed to:', this._phaseOverloadNotificationsEnabled); + } + + if (MySettings.changedKeys.includes('debug_logging')) { + this._debugLogging = MySettings.newSettings.debug_logging ?? false; + this.log(`🐛 Debug logging ${this._debugLogging ? 'enabled' : 'disabled'}`); + // Also toggle WebSocket verbose logging + this.wsManager?.setDebug(this._debugLogging); + } + + if (MySettings.changedKeys.includes('ws_throttle_ms')) { + const throttleSec = MySettings.newSettings.ws_throttle_ms || 2; + this.log(`⚙️ WS measurement throttle changed to ${throttleSec}s — restarting WebSocket`); + // Throttle is set at construction time, so we need a full WS restart + this.wsManager?.restartWebSocket(); + } + + return true; + } + + /** + * Check if voltage has been restored to normal range after sag/swell + * @param {Object} m - measurement data + */ + _checkVoltageRestoration(m) { + if (!this._voltageState || !this._flowTriggerVoltageRestored) return; + + // Voltage normal range (230V ±10% = 207-253V) + const VOLTAGE_MIN = 207; + const VOLTAGE_MAX = 253; + + const phases = [ + { name: 'l1', voltage: m.voltage_l1_v }, + { name: 'l2', voltage: m.voltage_l2_v }, + { name: 'l3', voltage: m.voltage_l3_v } + ]; + + phases.forEach(({ name, voltage }) => { + if (voltage == null) return; + + const state = this._voltageState[name]; + const isNormal = voltage >= VOLTAGE_MIN && voltage <= VOLTAGE_MAX; + + // Detect restoration: was abnormal, now normal + if (state.abnormal && isNormal) { + const phaseName = name.toUpperCase(); + this.log(`Voltage restored on ${phaseName}: ${voltage}V`); + + this._flowTriggerVoltageRestored.trigger(this, { + phase: phaseName, + voltage: Math.round(voltage) + }).catch(this.error); + + state.abnormal = false; + state.lastAbnormalTime = null; + } + // Track abnormal state + else if (!state.abnormal && !isNormal) { + state.abnormal = true; + state.lastAbnormalTime = Date.now(); + } + }); + } + + /** + * Check if power has been restored after being offline + * @param {Object} m - measurement data + */ + _checkPowerRestoration(m) { + if (!this._powerState || !this._flowTriggerPowerRestored) return; + + // Consider online if we have active power reading or any voltage + const hasActivePower = m.active_power_w != null && m.active_power_w !== 0; + const hasVoltage = m.voltage_l1_v != null || m.voltage_l2_v != null || m.voltage_l3_v != null; + const isOnline = hasActivePower || hasVoltage; + + // Detect restoration: was offline, now online + if (this._powerState.offline && isOnline) { + const offlineDuration = this._powerState.offlineStartTime + ? Math.round((Date.now() - this._powerState.offlineStartTime) / 1000) + : 0; + + this.log(`Power restored after ${offlineDuration} seconds offline`); + + this._flowTriggerPowerRestored.trigger(this, { + offline_duration: offlineDuration + }).catch(this.error); + + this._powerState.offline = false; + this._powerState.offlineStartTime = null; + } + // Track offline state + else if (!this._powerState.offline && !isOnline) { + this._powerState.offline = true; + this._powerState.offlineStartTime = Date.now(); + } + } + +}; \ No newline at end of file diff --git a/drivers/energy_v2/driver.compose.json b/drivers/energy_v2/driver.compose.json new file mode 100644 index 00000000..fa2d71ec --- /dev/null +++ b/drivers/energy_v2/driver.compose.json @@ -0,0 +1,383 @@ +{ + "name": { + "en": "P1 Meter (apiv2)" + }, + "images": { + "large": "drivers/energy_v2/assets/images/large.png", + "small": "drivers/energy_v2/assets/images/small.png" + }, + "class": "sensor", + "discovery": "energy_v2", + "platforms": [ + "local" + ], + "capabilities": [ + "identify", + "measure_power", + "meter_gas", + "meter_gas.daily", + "meter_water", + "meter_power", + "meter_power.consumed.t1", + "meter_power.produced.t1", + "meter_power.consumed.t2", + "meter_power.produced.t2", + "meter_power.consumed.t3", + "meter_power.produced.t3", + "meter_power.consumed.t4", + "meter_power.produced.t4", + "meter_power.consumed", + "meter_power.returned", + "meter_power.daily", + "measure_power.l1", + "measure_power.l2", + "measure_power.l3", + "measure_current", + "measure_current.l1", + "measure_current.l2", + "measure_current.l3", + "net_load_phase1_pct", + "net_load_phase2_pct", + "net_load_phase3_pct", + "measure_voltage.l1", + "measure_voltage.l2", + "measure_voltage.l3", + "measure_power.montly_power_peak", + "measure_power.average_power_15m_w", + "tariff", + "long_power_fail_count", + "voltage_sag_l1", + "voltage_sag_l2", + "voltage_sag_l3", + "voltage_swell_l1", + "voltage_swell_l2", + "voltage_swell_l3", + "rssi", + "wifi_quality", + "measure_power.battery_group_power_w", + "measure_power.battery_group_target_power_w", + "measure_power.battery_group_max_consumption_w", + "measure_power.battery_group_max_production_w", + "connection_error", + "battery_group_total_capacity_kwh", + "battery_group_average_soc", + "battery_group_state", + "battery_group_charge_mode" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.consumed", + "cumulativeExportedCapability": "meter_power.returned" + }, + "capabilitiesOptions": { + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + } + }, + "meter_gas": { + "decimals": 3, + "title": { + "en": "Gasmeter", + "nl": "Gasmeter" + } + }, + "meter_gas.daily": { + "decimals": 3, + "title": { + "en": "Day Usage Gas", + "nl": "Dag verbruik Gas" + } + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Watermeter", + "nl": "Watermeter" + } + }, + "measure_power.l1": { + "title": { + "en": "Current usage phase 1", + "nl": "Huidig gebruik fase 1" + } + }, + "measure_power.l2": { + "title": { + "en": "Current usage phase 2", + "nl": "Huidig gebruik fase 2" + } + }, + "measure_power.l3": { + "title": { + "en": "Current usage phase 3", + "nl": "Huidig gebruik fase 3" + } + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + } + }, + "measure_current.l1": { + "title": { + "en": "Current Amp phase 1", + "nl": "Huidig Amp fase 1" + } + }, + "measure_current.l2": { + "title": { + "en": "Current Amp phase 2", + "nl": "Huidig Amp fase 2" + } + }, + "measure_current.l3": { + "title": { + "en": "Current Amp phase 3", + "nl": "Huidig Amp fase 3" + } + }, + "net_load_phase1_pct" :{ + "title": { + "en": "Phase 1 load %", + "nl": "Fase 1 belasting %" + } + }, + "net_load_phase2_pct" :{ + "title": { + "en": "Phase 2 load %", + "nl": "Fase 2 belasting %" + } + }, + "net_load_phase3_pct" :{ + "title": { + "en": "Phase 3 load %", + "nl": "Fase 3 belasting %" + } + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + } + }, + "measure_voltage.l1": { + "title": { + "en": "Current Voltage phase 1", + "nl": "Huidig Voltage fase 1" + } + }, + "measure_voltage.l2": { + "title": { + "en": "Current Voltage phase 2", + "nl": "Huidig Voltage fase 2" + } + }, + "measure_voltage.l3": { + "title": { + "en": "Current Voltage phase 3", + "nl": "Huidig Voltage fase 3" + } + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Total t1 usage", + "nl": "Totaal t1 gebruik" + } + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Total t1 deliver", + "nl": "Totaal t1 teruglevering" + } + }, + "meter_power.consumed.t2": { + "decimals": 3, + "title": { + "en": "Total t2 usage", + "nl": "Totaal t2 gebruik" + } + }, + "meter_power.produced.t2": { + "decimals": 3, + "title": { + "en": "Total t2 deliver", + "nl": "Totaal t2 teruglevering" + } + }, + "meter_power.consumed.t3": { + "decimals": 3, + "title": { + "en": "Total t3 usage", + "nl": "Totaal t3 gebruik" + } + }, + "meter_power.produced.t3": { + "decimals": 3, + "title": { + "en": "Total t3 deliver", + "nl": "Totaal t3 teruglevering" + } + }, + "meter_power.consumed.t4": { + "decimals": 3, + "title": { + "en": "Total t4 usage", + "nl": "Totaal t4 gebruik" + } + }, + "meter_power.produced.t4": { + "decimals": 3, + "title": { + "en": "Total t4 deliver", + "nl": "Totaal t4 teruglevering" + } + }, + "meter_power.consumed": { + "decimals": 3, + "title": { + "en": "Sum Consumed", + "nl": "Som gebruik" + } + }, + "meter_power.returned": { + "decimals": 3, + "title": { + "en": "Sum returned", + "nl": "Som teruglevering" + } + }, + "meter_power.daily": { + "decimals": 3, + "title": { + "en": "Daily usage", + "nl": "Dag verbruik" + } + }, + "meter_power": { + "decimals": 3, + "title": { + "en": "Total usage KWh", + "nl": "Totaal verbruik KWh" + } + }, + "measure_power.montly_power_peak": { + "title": { + "en": "Monthly Power Peak", + "nl": "Maandelijks piekvermogen" + } + }, + "measure_power.average_power_15m_w": { + "title": { + "en": "Active average power over 15 minutes", + "nl": "Actief gemiddeld vermogen over 15 minuten" + } + }, + "measure_power.battery_group_power_w": { + "title": { + "en": "Battery group Current combined Power", + "nl": "Battery groep Huidig samengesteld vermogen" + } + }, + "measure_power.battery_group_target_power_w": { + "title": { + "en": "Battery group Target Power", + "nl": "Battery groep Doel vermogen" + } + }, + "measure_power.battery_group_max_consumption_w": { + "title": { + "en": "Battery group Max allowed Consumption Power", + "nl": "Battery groep Max toegestaand gebruiksvermogen" + } + }, + "measure_power.battery_group_max_production_w": { + "title": { + "en": "Battery group Max allowed Production Power", + "nl": "Battery groep Max toegestaand leveringssvermogen" + } + }, + "battery_group_total_capacity_kwh": { + "type": "number", + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/battery.svg", + "units": { + "en": "kWh" + }, + "title": { + "en": "Battery Group Total Capacity" + }, + "insights": false + }, + "battery_group_average_soc": { + "type": "number", + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/battery.svg", + "units": { + "en": "%" + }, + "title": { + "en": "Battery Group Average SoC" + }, + "insights": true + }, + "battery_group_state": { + "type": "string", + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/battery.svg", + "title": { + "en": "Battery Group Charge state", + "nl": "Battery Groep Laadt status" + }, + "insights": true + }, + "rssi": { + "type": "number", + "title": { + "en": "WiFi Signal", + "nl": "WiFi signaal" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/rssi.svg", + "units": { + "en": "dBm", + "nl": "dBm" + } + } + }, + "pair": [ + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "authorize" + }, + "options": { "singular": true } + }, + { + "id": "authorize", + "navigation": + { + "prev": "list_devices" + } + } + ], + "repair": [ + { + "id": "manual_ip" + } + ] +} diff --git a/drivers/energy_v2/driver.flow.compose.json b/drivers/energy_v2/driver.flow.compose.json new file mode 100644 index 00000000..32f67701 --- /dev/null +++ b/drivers/energy_v2/driver.flow.compose.json @@ -0,0 +1,283 @@ +{ + "actions": [ + { + "id": "set-battery-to-full-charge-mode", + "title": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "titleFormatted": { + "en": "Set battery to Full Charge mode", + "nl": "Zet batterij in Volledig Oplaad modus" + }, + "args": [] + }, + { + "id": "set-battery-to-zero-mode", + "title": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Zero modus" + }, + "titleFormatted": { + "en": "Set battery to Zero mode", + "nl": "Zet batterij in Nul op de meter modus" + }, + "args": [] + }, + { + "id": "set-battery-to-standby-mode", + "title": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "titleFormatted": { + "en": "Set battery to Standby mode", + "nl": "Zet batterij in Standby modus" + }, + "args": [] + }, + { + "id": "set-battery-to-zero-charge-only-mode", + "title": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Nul op de meter alleen opladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Charge Only mode", + "nl": "Zet batterij in Nul op de meter alleen opladen modus" + }, + "args": [] + }, + { + "id": "set-battery-to-zero-discharge-only-mode", + "title": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Nul op de meter, alleen ontladen modus" + }, + "titleFormatted": { + "en": "Set battery to Zero Discharge Only mode", + "nl": "Zet batterij in Nul op de meter, alleen ontladen modus" + }, + "args": [] + } + ], + "triggers": [ + { + "id": "battery_mode_changed", + "title": { "en": "Battery mode changed" }, + "titleFormatted": { "en": "Battery mode changed" }, + "args": [], + "tokens": [] + }, + { + "id": "battery_error_detected", + "title": { + "en": "Battery error detected" + }, + "args": [] + }, + { + "id": "battery_group_state_changed", + "title": { "en": "Battery group state changed" } + }, + { + "id": "tariff_changed_v2", + "title": { + "en": "Peak / Normal Tariff changed", + "nl": "Dal / Normaal Tarief veranderd" + }, + "args": [], + "tokens": [ + { + "name": "tariff", + "type": "number", + "title": { + "en": "tariff", + "nl": "tarief" + }, + "example": 1 + } + ] + }, + { + "id": "import_changed_v2", + "title": { + "en": "Total used changed", + "nl": "Som gebruik veranderd" + }, + "args": [], + "tokens": [ + { + "name": "import", + "type": "number", + "title": { + "en": "import", + "nl": "import" + }, + "example": 1 + } + ] + }, + { + "id": "export_changed_v2", + "title": { + "en": "Total delivered changed", + "nl": "Som teruglevering veranderd" + }, + "args": [], + "tokens": [ + { + "name": "export", + "type": "number", + "title": { + "en": "export", + "nl": "export" + }, + "example": 1 + } + ] + }, + { + "id": "voltage_sag_detected", + "title": { + "en": "Voltage sag detected", + "nl": "Spanningsdip gedetecteerd" + }, + "hint": { + "en": "Triggers when a voltage sag (dip) is detected on any phase", + "nl": "Triggert wanneer een spanningsdip wordt gedetecteerd op een fase" + }, + "args": [], + "tokens": [ + { + "name": "phase", + "type": "string", + "title": { + "en": "Phase", + "nl": "Fase" + }, + "example": "L1" + }, + { + "name": "count", + "type": "number", + "title": { + "en": "Total count", + "nl": "Totaal aantal" + }, + "example": 1 + } + ] + }, + { + "id": "voltage_swell_detected", + "title": { + "en": "Voltage swell detected", + "nl": "Spanningspiek gedetecteerd" + }, + "hint": { + "en": "Triggers when a voltage swell (surge) is detected on any phase", + "nl": "Triggert wanneer een spanningspiek wordt gedetecteerd op een fase" + }, + "args": [], + "tokens": [ + { + "name": "phase", + "type": "string", + "title": { + "en": "Phase", + "nl": "Fase" + }, + "example": "L1" + }, + { + "name": "count", + "type": "number", + "title": { + "en": "Total count", + "nl": "Totaal aantal" + }, + "example": 1 + } + ] + }, + { + "id": "long_power_fail_detected", + "title": { + "en": "Long power failure detected", + "nl": "Lange stroomuitval gedetecteerd" + }, + "hint": { + "en": "Triggers when a long power failure is detected (>1 minute)", + "nl": "Triggert wanneer een lange stroomuitval wordt gedetecteerd (>1 minuut)" + }, + "args": [], + "tokens": [ + { + "name": "count", + "type": "number", + "title": { + "en": "Total count", + "nl": "Totaal aantal" + }, + "example": 1 + } + ] + }, + { + "id": "voltage_restored", + "title": { + "en": "Voltage restored to normal", + "nl": "Spanning hersteld naar normaal" + }, + "hint": { + "en": "Triggers when voltage returns to normal range after a sag or swell", + "nl": "Triggert wanneer spanning terugkeert naar normaal bereik na een dip of piek" + }, + "args": [], + "tokens": [ + { + "name": "phase", + "type": "string", + "title": { + "en": "Phase", + "nl": "Fase" + }, + "example": "L1" + }, + { + "name": "voltage", + "type": "number", + "title": { + "en": "Voltage (V)", + "nl": "Spanning (V)" + }, + "example": 230 + } + ] + }, + { + "id": "power_restored", + "title": { + "en": "Power restored", + "nl": "Stroom hersteld" + }, + "hint": { + "en": "Triggers when power is restored after being offline", + "nl": "Triggert wanneer stroom is hersteld na een uitval" + }, + "args": [], + "tokens": [ + { + "name": "offline_duration", + "type": "number", + "title": { + "en": "Offline duration (seconds)", + "nl": "Offline duur (seconden)" + }, + "example": 120 + } + ] + } + ] +} diff --git a/drivers/energy_v2/driver.js b/drivers/energy_v2/driver.js new file mode 100644 index 00000000..fa129193 --- /dev/null +++ b/drivers/energy_v2/driver.js @@ -0,0 +1,6 @@ +'use strict'; + +const Homey = require('homey'); +const driver = require('../../includes/v2/Driver'); + +module.exports = driver; diff --git a/drivers/energy_v2/driver.settings.compose.json b/drivers/energy_v2/driver.settings.compose.json new file mode 100644 index 00000000..ebacfa1a --- /dev/null +++ b/drivers/energy_v2/driver.settings.compose.json @@ -0,0 +1,145 @@ +[ + { + "id": "mode", + "type": "dropdown", + "value": "plugin-battery", + "label": { + "en": "Plugin Battery mode", + "nl": "Plugin‑batterijmodus" + }, + "values": [ + { + "id": "zero", + "label": { + "en": "Zero mode", + "nl": "Nul op de meter" + } + }, + { + "id": "to_full", + "label": { + "en": "Full charge", + "nl": "Volledig opladen" + } + }, + { + "id": "standby", + "label": { + "en": "Standby", + "nl": "Stand‑by" + } + }, + { + "id": "zero_charge_only", + "label": { + "en": "Zero mode, Charge allowed", + "nl": "Nul op de meter, laden toegestaan" + } + }, + { + "id": "zero_discharge_only", + "label": { + "en": "Zero mode, Discharge allowed", + "nl": "Nul op de meter, ontladen toegestaan" + } + } + ] + }, + { + "id": "polling_interval", + "type": "number", + "label": { "en": "Polling interval" }, + "value": 10, + "min": 1, + "unit": { "en": "s" } + }, + { + "id": "grid_phase_amps", + "type": "number", + "label": { "en": "Grid phase Amps", + "nl": "Net fase aansluiting" }, + "value": 40, + "unit" : { "en": "A", + "nl": "A" } + }, + { + "id": "cloud", + "type": "number", + "label": { "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + }, + { + "id": "url", + "type": "text", + "label": { "en": "url", + "nl": "url" + } + }, + { + "id": "use_polling", + "type": "checkbox", + "label": { + "en": "Use polling instead of WebSocket", + "nl": "Gebruik polling in plaats van WebSocket" + }, + "value": true + }, + { + "id": "ws_throttle_ms", + "type": "number", + "label": { + "en": "WebSocket update interval (seconds)", + "nl": "WebSocket update interval (seconden)" + }, + "hint": { + "en": "How often measurements are processed from the WebSocket. Increase to 3 or 4 on older or slower Homey devices to reduce CPU load. Default: 2.", + "nl": "Hoe vaak metingen worden verwerkt via WebSocket. Verhoog naar 3 of 4 op oudere Homey apparaten om CPU-belasting te verminderen. Standaard: 2." + }, + "value": 2, + "min": 2, + "max": 10, + "step": 1 +}, + { + "id": "show_gas", + "type": "checkbox", + "label": { + "en": "Show gas meter", + "nl": "Gas meter weergeven" + }, + "value": true + }, + { + "id": "baseload_notifications", + "type": "checkbox", + "label": { + "en": "Enable baseload notifications", + "nl": "Sluipverbruik meldingen inschakelen" + }, + "value": true + }, + { + "id": "phase_overload_notifications", + "type": "checkbox", + "label": { + "en": "Phase overload notifications", + "nl": "Fase-overbelasting meldingen" + }, + "value": true + }, + { + "id": "debug_logging", + "type": "checkbox", + "label": { + "en": "Enable debug logging", + "nl": "Debug logging inschakelen" + }, + "value": false, + "hint": { + "en": "Enables verbose logging for WebSocket and battery events. Disable after debugging to reduce CPU load.", + "nl": "Schakelt uitgebreide logging in voor WebSocket en batterijgebeurtenissen. Schakel uit na het debuggen om CPU-belasting te verminderen." + } + } +] diff --git a/drivers/energy_v2/pair/authorize.html b/drivers/energy_v2/pair/authorize.html new file mode 100644 index 00000000..6b62d7d0 --- /dev/null +++ b/drivers/energy_v2/pair/authorize.html @@ -0,0 +1,48 @@ +
+

+

+

+

+ +
+ + diff --git a/drivers/energy_v2/repair/manual_ip.html b/drivers/energy_v2/repair/manual_ip.html new file mode 100644 index 00000000..f432c2e7 --- /dev/null +++ b/drivers/energy_v2/repair/manual_ip.html @@ -0,0 +1,200 @@ + + + + + + +

+

+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + + + diff --git a/drivers/energylink/assets/icon.svg b/drivers/energylink/assets/icon.svg new file mode 100644 index 00000000..3ac01a32 --- /dev/null +++ b/drivers/energylink/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/energylink/device.js b/drivers/energylink/device.js new file mode 100644 index 00000000..f057ebf2 --- /dev/null +++ b/drivers/energylink/device.js @@ -0,0 +1,298 @@ +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); + +const debug = false; + +class HomeWizardEnergylink extends Homey.Device { + + async onInit() { + + this.startPolling(); + + // Flow triggers + this._flowTriggerPowerUsed = this.homey.flow.getDeviceTriggerCard('power_used_changed'); + this._flowTriggerPowerNetto = this.homey.flow.getDeviceTriggerCard('power_netto_changed'); + this._flowTriggerPowerS1 = this.homey.flow.getDeviceTriggerCard('power_s1_changed'); + this._flowTriggerMeterPowerS1 = this.homey.flow.getDeviceTriggerCard('meter_power_s1_changed'); + this._flowTriggerPowerS2 = this.homey.flow.getDeviceTriggerCard('power_s2_changed'); + this._flowTriggerMeterPowerS2 = this.homey.flow.getDeviceTriggerCard('meter_power_s2_changed'); + this._flowTriggerMeterPowerUsed = this.homey.flow.getDeviceTriggerCard('meter_power_used_changed'); + this._flowTriggerMeterPowerAggregated = this.homey.flow.getDeviceTriggerCard('meter_power_aggregated_changed'); + this._flowTriggerMeterReturnT1 = this.homey.flow.getDeviceTriggerCard('meter_return_t1_changed'); + this._flowTriggerMeterReturnT2 = this.homey.flow.getDeviceTriggerCard('meter_return_t2_changed'); + } + + startPolling() { + + // Clear previous intervals + if (this.refreshIntervalId) clearInterval(this.refreshIntervalId); + if (this.refreshIntervalIdReadings) clearInterval(this.refreshIntervalIdReadings); + + // Status polling every 20 seconds + this.refreshIntervalId = setInterval(() => { + if (debug) this.log('-- EnergyLink Status Polling --'); + + if (this.getSetting('homewizard_id')) { + this.getStatus(); + } + }, 20 * 1000); + + // Readings polling every 60 seconds + this.refreshIntervalIdReadings = setInterval(() => { + if (debug) this.log('-- EnergyLink Readings Polling --'); + + if (this.getSetting('homewizard_id')) { + this.getReadings(); + } + }, 60 * 1000); + } + + // ----------------------------- + // STATUS POLLING + // ----------------------------- + async getStatus() { + + const homewizard_id = this.getSetting('homewizard_id'); + if (!homewizard_id) return; + + try { + const callback = await homewizard.getDeviceData(homewizard_id, 'energylinks'); + + // Safe guard: must be array with at least 1 entry + if (!Array.isArray(callback) || callback.length === 0) { + this.setUnavailable('No EnergyLink data available'); + return; + } + + const entry = callback[0]; + if (!entry) return; + + this.setAvailable().catch(this.error); + + const promises = []; + + // ----------------------------- + // BASIC VALUES + // ----------------------------- + const value_s1 = entry.t1; + const value_s2 = entry.t2; + // const factor_s1 = entry.c1 ?? 1; + // const factor_s2 = entry.c2 ?? 1; + + const energy_current_cons = entry.used?.po ?? 0; + const energy_daytotal_cons = entry.used?.dayTotal ?? 0; + const energy_daytotal_aggr = entry.aggregate?.dayTotal ?? 0; + const energy_current_netto = entry.aggregate?.po ?? 0; + + // ----------------------------- + // GAS (optional) + // ----------------------------- + try { + const gas_daytotal_cons = entry.gas?.dayTotal; + if (gas_daytotal_cons != null) { + promises.push(this.setCapabilityValue('meter_gas.today', gas_daytotal_cons).catch(this.error)); + } + } catch (_) { + this.log('No gas information available'); + } + + // ----------------------------- + // ELECTRICITY (common) + // ----------------------------- + promises.push(this.setCapabilityValue('measure_power.used', energy_current_cons).catch(this.error)); + promises.push(this.setCapabilityValue('measure_power', energy_current_netto).catch(this.error)); + promises.push(this.setCapabilityValue('measure_power.netto', energy_current_netto).catch(this.error)); + promises.push(this.setCapabilityValue('meter_power.used', energy_daytotal_cons).catch(this.error)); + promises.push(this.setCapabilityValue('meter_power.aggr', energy_daytotal_aggr).catch(this.error)); + + // ----------------------------- + // SOLAR / WATER / OTHER / CAR + // ----------------------------- + let solar_current_prod = 0; + let solar_daytotal_prod = 0; + + let water_current_cons = 0; + let water_daytotal_cons = 0; + + // S1 solar + if (value_s1 === 'solar') { + const po = entry.s1?.po ?? 0; + const dt = entry.s1?.dayTotal ?? 0; + + solar_current_prod += po; + solar_daytotal_prod += dt; + + if (this.hasCapability('meter_power.s1other')) { + promises.push(this.removeCapability('meter_power.s1other').catch(this.error)); + promises.push(this.removeCapability('measure_power.s1other').catch(this.error)); + } + } + + // S2 solar + if (value_s2 === 'solar') { + const po = entry.s2?.po ?? 0; + const dt = entry.s2?.dayTotal ?? 0; + + if (!this.hasCapability('measure_power.s2')) { + await this.addCapability('measure_power.s2').catch(this.error); + await this.addCapability('meter_power.s2').catch(this.error); + } + + promises.push(this.setCapabilityValue('measure_power.s2', po).catch(this.error)); + promises.push(this.setCapabilityValue('meter_power.s2', dt).catch(this.error)); + + solar_current_prod += po; + solar_daytotal_prod += dt; + + if (this.hasCapability('meter_power.s2other')) { + promises.push(this.removeCapability('meter_power.s2other').catch(this.error)); + promises.push(this.removeCapability('measure_power.s2other').catch(this.error)); + } + } + + // Apply solar totals + if (value_s1 === 'solar' || value_s2 === 'solar') { + promises.push(this.setCapabilityValue('measure_power.s1', solar_current_prod).catch(this.error)); + promises.push(this.setCapabilityValue('meter_power.s1', solar_daytotal_prod).catch(this.error)); + } + + // S1 water + if (value_s1 === 'water') { + water_current_cons = entry.s1?.po ?? 0; + water_daytotal_cons = (entry.s1?.dayTotal ?? 0) / 1000; + + promises.push(this.setCapabilityValue('meter_water', water_daytotal_cons).catch(this.error)); + promises.push(this.setCapabilityValue('measure_water', water_current_cons).catch(this.error)); + } + + // S2 water + if (value_s2 === 'water') { + water_current_cons = entry.s2?.po ?? 0; + water_daytotal_cons = (entry.s2?.dayTotal ?? 0) / 1000; + + promises.push(this.setCapabilityOptions('meter_water', { decimals: 3 }).catch(this.error)); + promises.push(this.setCapabilityValue('meter_water', water_daytotal_cons).catch(this.error)); + promises.push(this.setCapabilityValue('measure_water', water_current_cons).catch(this.error)); + } + + // S1 other/car + if (value_s1 === 'other' || value_s1 === 'car') { + const po = entry.s1?.po ?? 0; + const dt = entry.s1?.dayTotal ?? 0; + + promises.push(this.setCapabilityValue('meter_power.s1other', dt).catch(this.error)); + promises.push(this.setCapabilityValue('measure_power.s1other', po).catch(this.error)); + } + + // S2 other/car + if (value_s2 === 'other' || value_s2 === 'car') { + const po = entry.s2?.po ?? 0; + const dt = entry.s2?.dayTotal ?? 0; + + promises.push(this.setCapabilityValue('meter_power.s2other', dt).catch(this.error)); + promises.push(this.setCapabilityValue('measure_power.s2other', po).catch(this.error)); + } + + // ----------------------------- + // FLOW TRIGGERS (safe) + // ----------------------------- + if (energy_current_cons != null && + energy_current_cons !== this.getStoreValue('last_measure_power_used')) { + + promises.push(this._flowTriggerPowerUsed.trigger(this, { power_used: energy_current_cons })); + this.setStoreValue('last_measure_power_used', energy_current_cons); + } + + if (energy_current_netto != null && + energy_current_netto !== this.getStoreValue('last_measure_power_netto')) { + + promises.push(this._flowTriggerPowerNetto.trigger(this, { netto_power_used: energy_current_netto })); + this.setStoreValue('last_measure_power_netto', energy_current_netto); + } + + // Execute all updates + await Promise.allSettled(promises); + + this.setAvailable().catch(this.error); + + } catch (err) { + this.log('ERROR EnergyLink getStatus', err); + this.setUnavailable(err); + } + } + + // ----------------------------- + // READINGS POLLING + // ----------------------------- + async getReadings() { + + const homewizard_id = this.getSetting('homewizard_id'); + if (!homewizard_id) return; + + try { + const callback = await homewizard.getDeviceData(homewizard_id, 'energylink_el'); + + // Must have at least 3 entries + if (!Array.isArray(callback) || callback.length < 3) { + return; + } + + this.setAvailable().catch(this.error); + + const gas = callback[2]?.consumed ?? 0; + const cons_t1 = callback[0]?.consumed ?? 0; + const prod_t1 = callback[0]?.produced ?? 0; + const cons_t2 = callback[1]?.consumed ?? 0; + let prod_t2 = callback[1]?.produced ?? 0; + + if (prod_t2 < 0) prod_t2 = -prod_t2; + + const aggregated = (cons_t1 + cons_t2) - (prod_t1 + prod_t2); + + // Ensure capabilities exist + if (!this.hasCapability('meter_power')) { + await this.addCapability('meter_power').catch(this.error); + } + if (!this.hasCapability('meter_gas')) { + await this.addCapability('meter_gas').catch(this.error); + } + + // Update values + this.setCapabilityValue('meter_gas.reading', gas).catch(this.error); + this.setCapabilityValue('meter_gas', gas).catch(this.error); + this.setCapabilityValue('meter_power', aggregated).catch(this.error); + this.setCapabilityValue('meter_power.consumed.t1', cons_t1).catch(this.error); + this.setCapabilityValue('meter_power.produced.t1', prod_t1).catch(this.error); + this.setCapabilityValue('meter_power.consumed.t2', cons_t2).catch(this.error); + this.setCapabilityValue('meter_power.produced.t2', prod_t2).catch(this.error); + + // Flow triggers + if (prod_t1 != null && prod_t1 !== this.getStoreValue('last_meter_return_t1')) { + this._flowTriggerMeterReturnT1.trigger(this, { meter_power_produced_t1: prod_t1 }); + this.setStoreValue('last_meter_return_t1', prod_t1); + } + + if (prod_t2 != null && prod_t2 !== this.getStoreValue('last_meter_return_t2')) { + this._flowTriggerMeterReturnT2.trigger(this, { meter_power_produced_t2: prod_t2 }); + this.setStoreValue('last_meter_return_t2', prod_t2); + } + + } catch (err) { + this.log('ERROR EnergyLink getReadings', err); + this.setUnavailable(err); + } + } + + onDeleted() { + const deviceId = this.getData().id; + homewizard.removeDevice(deviceId); + + clearInterval(this.refreshIntervalId); + clearInterval(this.refreshIntervalIdReadings); + this.log('-- EnergyLink Polling Stopped --'); + } +} + +module.exports = HomeWizardEnergylink; diff --git a/drivers/energylink/driver.compose.json b/drivers/energylink/driver.compose.json new file mode 100644 index 00000000..341dd810 --- /dev/null +++ b/drivers/energylink/driver.compose.json @@ -0,0 +1,221 @@ +{ + "name": { + "en": "Energylink", + "nl": "Energylink" + }, + "images": { + "large": "drivers/energylink/assets/images/large.jpg", + "small": "drivers/energylink/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "platforms": [ "local"], + "capabilities": [ + "measure_power", + "meter_power.used", + "meter_power.aggr", + "meter_power.s1", + "meter_power.s2", + "meter_power", + "measure_power.used", + "measure_power.netto", + "measure_power.s1", + "measure_power.s2", + "measure_power.s1other", + "meter_power.s1other", + "measure_power.s2other", + "meter_power.s2other", + "meter_gas.today", + "meter_gas.reading", + "meter_water", + "measure_water", + "meter_power.consumed.t1", + "meter_power.produced.t1", + "meter_power.consumed.t2", + "meter_power.produced.t2" + ], + "energy": { + "cumulative": true, + "cumulativeImportedCapability": "meter_power.used", + "cumulativeExportedCapability": "meter_power.s1" + }, + "capabilitiesOptions": { + "meter_power.used": { + "decimals": 3, + "title": { + "en": "Day usage", + "nl": "Dag gebruik" + }, + "insights": true + }, + "meter_power.aggr": { + "decimals": 3, + "title": { + "en": "Overall usage", + "nl": "Netto gebruik" + }, + "insights": true + }, + "meter_power.s1": { + "decimals": 3, + "title": { + "en": "Day production S1", + "nl": "Dag opbrengst S1" + }, + "insights": true + }, + "meter_power.s2": { + "decimals": 3, + "title": { + "en": "Day production S2", + "nl": "Dag opbrengst S2" + }, + "insights": true + }, + "measure_power.s1other": { + "title": { + "en": "Power current S1 other", + "nl": "Huidig vermogen S1 other" + }, + "insights": true + }, + "meter_power.s1other": { + "decimals": 3, + "title": { + "en": "Day usage S1 other", + "nl": "Dag gebruik S1 other" + }, + "insights": true + }, + "measure_power.used": { + "title": { + "en": "Power current", + "nl": "Huidig vermogen" + }, + "insights": true + }, + "measure_power.s2other": { + "title": { + "en": "Power current S2 other", + "nl": "Huidig vermogen S2 other" + }, + "insights": true + }, + "meter_power.s2other": { + "decimals": 3, + "title": { + "en": "Day usage S2 other", + "nl": "Dag gebruik S2 other" + }, + "insights": true + }, + "measure_power.netto": { + "title": { + "en": "Netto Power current", + "nl": "Netto Huidig vermogen" + } + }, + "measure_power.s1": { + "title": { + "en": "Solar current S1", + "nl": "Huidige opbrengst S1" + }, + "insights": true + }, + "measure_power.s2": { + "title": { + "en": "Solar current S2", + "nl": "Huidige opbrengst S2" + }, + "insights": true + }, + "meter_gas.today": { + "decimals": 3, + "title": { + "en": "Gas", + "nl": "Gas" + }, + "insights": true + }, + "meter_gas.reading": { + "decimals": 3, + "title": { + "en": "Meter reading gas", + "nl": "Meterstand Gas" + }, + "insights": true + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Water Total", + "nl": "Water Totaal" + }, + "insights": true + }, + "measure_water": { + "title": { + "en": "Water l./m", + "nl": "Water l./m" + }, + "insights": true + }, + "meter_power.consumed.t1": { + "decimals": 3, + "title": { + "en": "Meter reading low", + "nl": "Stand laag tarief" + }, + "insights": true + }, + "meter_power.produced.t1": { + "decimals": 3, + "title": { + "en": "Meter reading produced low", + "nl": "Stand terug levering laag" + }, + "insights": true + }, + "meter_power.consumed.t2": { + "decimals": 3, + "title": { + "en": "Meter reading normal", + "nl": "Stand verbruik normaal" + }, + "insights": true + }, + "meter_power.produced.t2": { + "decimals": 3, + "title": { + "en": "Meter reading produced normal", + "nl": "Stand terug levering normaal" + }, + "insights": true + }, + "meter_power": { + "title": { + "en": "Aggregated meter", + "nl": "Geaggregeerde meterstand" + }, + "insights": true + } + }, + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/energylink/driver.flow.compose.json b/drivers/energylink/driver.flow.compose.json new file mode 100644 index 00000000..09fa61cf --- /dev/null +++ b/drivers/energylink/driver.flow.compose.json @@ -0,0 +1,194 @@ +{ + "triggers": [ + { + "id": "power_used_changed", + "title": { + "en": "Power used changed", + "nl": "Huidig vermogen veranderd" + }, + "args": [], + "tokens": [ + { + "name": "power_used", + "type": "number", + "title": { + "en": "Watt", + "nl": "Watt" + }, + "example": 15 + } + ] + }, + { + "id": "power_s1_changed", + "title": { + "en": "Power production changed", + "nl": "Huidige productie veranderd" + }, + "args": [], + "tokens": [ + { + "name": "power_s1", + "type": "number", + "title": { + "en": "Watt", + "nl": "Watt" + }, + "example": 15 + } + ] + }, + { + "id": "power_s2_changed", + "title": { + "en": "Power usage S2 changed", + "nl": "Huidige gebruik S2 veranderd" + }, + "args": [], + "tokens": [ + { + "name": "power_s2", + "type": "number", + "title": { + "en": "Watt", + "nl": "Watt" + }, + "example": 15 + } + ] + }, + { + "id": "power_netto_changed", + "title": { + "en": "Daily netto usage changed", + "nl": "Dag netto verbruik veranderd" + }, + "args": [], + "tokens": [ + { + "name": "netto_power_used", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_power_used_changed", + "title": { + "en": "Daily usage changed", + "nl": "Dag verbruik veranderd" + }, + "args": [], + "tokens": [ + { + "name": "power_daytotal_used", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_power_aggregated_changed", + "title": { + "en": "Overall usage changed", + "nl": "Netto verbruik veranderd" + }, + "args": [], + "tokens": [ + { + "name": "power_daytotal_aggr", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_power_s1_changed", + "title": { + "en": "Daily production changed", + "nl": "Dag productie veranderd" + }, + "args": [], + "tokens": [ + { + "name": "power_daytotal_s1", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_power_s2_changed", + "title": { + "en": "Daily usage S2 changed", + "nl": "Dag gebruik S2 veranderd" + }, + "args": [], + "tokens": [ + { + "name": "power_daytotal_s2", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_return_t1_changed", + "title": { + "en": "Meter return t1 changed", + "nl": "Meter teruglevering t1 veranderd" + }, + "args": [], + "tokens": [ + { + "name": "meter_power_produced_t1", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + }, + { + "id": "meter_return_t2_changed", + "title": { + "en": "Meter return t2 changed", + "nl": "Meter teruglevering t2 veranderd" + }, + "args": [], + "tokens": [ + { + "name": "meter_power_produced_t2", + "type": "number", + "title": { + "en": "kWh", + "nl": "kWh" + }, + "example": 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/drivers/energylink/driver.js b/drivers/energylink/driver.js index e5f2a0e8..e754562c 100755 --- a/drivers/energylink/driver.js +++ b/drivers/energylink/driver.js @@ -1,200 +1,82 @@ -var devices = []; -var homewizard = require('./../../includes/homewizard.js'); -var refreshIntervalId = 0; - -// SETTINGS -module.exports.settings = function( device_data, newSettingsObj, oldSettingsObj, changedKeysArr, callback ) { - Homey.log ('Changed settings: ' + JSON.stringify(device_data) + ' / ' + JSON.stringify(newSettingsObj) + ' / old = ' + JSON.stringify(oldSettingsObj)); - try { - changedKeysArr.forEach(function (key) { - devices[device_data.id].settings[key] = newSettingsObj[key]; - }); - callback(null, true); - } catch (error) { - callback(error); - } -}; - -module.exports.pair = function( socket ) { - socket.on('get_homewizards', function () { - homewizard.getDevices(function(homewizard_devices) { - - Homey.log(homewizard_devices); - var hw_devices = {}; - Object.keys(homewizard_devices).forEach(function(key) { - hw_devices[key] = homewizard_devices[key]; - }); - - socket.emit('hw_devices', hw_devices); - }); +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); + +const devices = {}; + +class HomeWizardEnergyLink extends Homey.Driver { + + onInit() { + // Driver initialized + } + + async onPair(socket) { + + // Show initial view + await socket.showView('start'); + + // View change logging + socket.setHandler('showView', (viewId) => { + this.log(`View: ${viewId}`); }); - - socket.on('manual_add', function (device, callback) { - if (device.settings.homewizard_id.indexOf('HW_') === -1 && device.settings.homewizard_id.indexOf('HW') === 0) { - //true - Homey.log('Energylink added ' + device.data.id); - devices[device.data.id] = { - id: device.data.id, - name: device.name, - settings: device.settings, - }; - callback( null, devices ); - socket.emit("success", device); - startPolling(); - } else { - socket.emit("error", "No valid HomeWizard found, re-pair if problem persists"); + + // Request list of EnergyLinks from all HomeWizard controllers + socket.setHandler('get_energylinks', async () => { + const fetchedDevices = homewizard.self.devices || {}; + const energyLinkList = []; + + this.log('[PAIRING] Fetched devices:', Object.keys(fetchedDevices)); + + Object.keys(fetchedDevices).forEach(hwId => { + const device = fetchedDevices[hwId]; + this.log(`[PAIRING] Device ${hwId} polldata:`, device.polldata ? 'exists' : 'missing'); + + const energylinks = device.polldata?.energylinks || []; + this.log(`[PAIRING] Device ${hwId} energylinks:`, energylinks); + + // Energylinks is een array + if (Array.isArray(energylinks) && energylinks.length > 0) { + energylinks.forEach(el => { + energyLinkList.push({ + homewizard_id: hwId, + energylink_id: el.id, + name: el.name || 'EnergyLink', + hw_name: device.name || device.settings?.homewizard_ip || hwId, + hw_ip: device.settings?.homewizard_ip + }); + }); } + }); + + this.log('[PAIRING] EnergyLinks found:', energyLinkList.length, energyLinkList); + socket.emit('energylink_list', energyLinkList); }); - - socket.on('disconnect', function(){ - console.log("User aborted pairing, or pairing is finished"); - }); -} -module.exports.init = function(devices_data, callback) { - devices_data.forEach(function initdevice(device) { - Homey.log('add device: ' + JSON.stringify(device)); - devices[device.id] = device; - module.exports.getSettings(device, function(err, settings){ - devices[device.id].settings = settings; - }); + // Manual add + socket.setHandler('manual_add', async (device) => { + const hwId = device.settings.homewizard_id; + + if (!hwId || hwId === '') { + socket.emit('error', 'No HomeWizard selected'); + return; + } + + this.log(`EnergyLink added ${device.data.id} on HomeWizard ${hwId}`); + + devices[device.data.id] = { + id: device.data.id, + name: device.name, + settings: device.settings, + }; + + socket.emit('success', device); }); - if (Object.keys(devices).length > 0) { - startPolling(); - } - Homey.log('Energylink driver init done'); - - callback (null, true); -}; - -module.exports.deleted = function( device_data ) { - clearInterval(refreshIntervalId); - console.log("--Stopped Polling Energy Link--"); - devices = []; - Homey.log('deleted: ' + JSON.stringify(device_data)); -}; - -module.exports.capabilities = { - "measure_power.used": { - get: function (device_data, callback) { - var device = devices[device_data.id]; - - if (device === undefined) { - callback(null, 0); - } else { - callback(null, device.last_measure_power_used); - } - } - }, - "measure_power.s1": { - get: function (device_data, callback) { - var device = devices[device_data.id]; - - if (device === undefined) { - callback(null, 0); - } else { - callback(null, device.last_measure_power_s1); - } - } - }, - "meter_power.used": { - get: function (device_data, callback) { - var device = devices[device_data.id]; - - if (device === undefined) { - callback(null, 0); - } else { - callback(null, device.last_meter_power_used); - } - } - }, - "meter_power.s1": { - get: function (device_data, callback) { - var device = devices[device_data.id]; - - if (device === undefined) { - callback(null, 0); - } else { - callback(null, device.last_meter_power_s1); - } - } - }, - meter_gas: { - get: function (device_data, callback) { - var device = devices[device_data.id]; - - if (device === undefined) { - callback(null, 0); - } else { - callback(null, device.last_meter_gas); - } - } - } -}; - -// Start polling -function startPolling() { - refreshIntervalId = setInterval(function () { - console.log("--Start Energylink Polling-- "); - Object.keys(devices).forEach(function (device_id) { - getStatus(device_id); + + socket.setHandler('disconnect', () => { + this.log('Pairing aborted or finished'); }); - }, 1000 * 10); + } } -function getStatus(device_id) { - if(devices[device_id].settings.homewizard_id !== undefined ) { - var homewizard_id = devices[device_id].settings.homewizard_id; - homewizard.getDeviceData(homewizard_id, 'energylinks', function(callback) { - if (Object.keys(callback).length > 0) { - try { - module.exports.setAvailable({id: device_id}); - var energy_current_cons = ( callback[0].used.po ); // WATTS Energy used JSON $energylink[0]['used']['po'] - var energy_current_prod = ( callback[0].s1.po ); // WATTS Energy produced via S1 $energylink[0]['s1']['po'] - var energy_daytotal_cons = ( callback[0].used.dayTotal ); // KWH Energy used JSON $energylink[0]['used']['po'] - var energy_daytotal_prod = ( callback[0].s1.dayTotal ); // KWH Energy produced via S1 $energylink[0]['s1']['po'] - var gas_daytotal_cons = ( callback[0].gas.dayTotal ); // m3 Energy produced via S1 $energylink[0]['gas']['dayTotal'] - - - // Consumed elec current - module.exports.realtime( { id: device_id }, "measure_power.used", energy_current_cons ); - // Consumed elec total day - module.exports.realtime( { id: device_id }, "meter_power.used", energy_daytotal_cons ); - // Produced elec current - module.exports.realtime( { id: device_id }, "measure_power.s1", energy_current_prod ); - // Produced elec total day - module.exports.realtime( { id: device_id }, "meter_power.s1", energy_daytotal_prod ); - // Consumed gas - module.exports.realtime( { id: device_id }, "meter_gas", gas_daytotal_cons ); - - // Trigger flows - if (energy_current_cons != devices[device_id].last_measure_power_used) { - console.log("Current Power - "+ energy_current_cons); - Homey.manager('flow').triggerDevice('power_used_changed', { power_used: energy_current_cons }, null, { id: device_id } ); - } - if (energy_current_prod != devices[device_id].last_measure_power_s1) { - console.log("Current S1 - "+ energy_current_prod); - Homey.manager('flow').triggerDevice('power_s1_changed', { power_s1: energy_current_prod }, null, { id: device_id } ); - } - if (energy_daytotal_cons != devices[device_id].last_meter_power_used) { - console.log("Used Daytotal- "+ energy_daytotal_cons); - Homey.manager('flow').triggerDevice('meter_power_used_changed', { power_daytotal_used: energy_daytotal_cons }, null, { id: device_id }); - } - if (energy_daytotal_prod != devices[device_id].last_meter_power_s1) { - console.log("S1 Daytotal- "+ energy_daytotal_prod); - Homey.manager('flow').triggerDevice('meter_power_s1_changed', { power_daytotal_s1: energy_daytotal_prod }, null, { id: device_id }); - } - } - catch(err) { - // Error with Energylink no data in Energylink - console.log ("No Energylink found"); - module.exports.setUnavailable({id: device_id}, "No Energylink found" ); - } - } - }); - } else { - Homey.log('Removed Energylink '+ device_id +' (wrong settings)'); - module.exports.setUnavailable({id: device_id}, "No Energylink found" ); - clearInterval(refreshIntervalId); - } -} +module.exports = HomeWizardEnergyLink; diff --git a/drivers/energylink/pair/start.html b/drivers/energylink/pair/start.html index 9308b3ee..65774702 100755 --- a/drivers/energylink/pair/start.html +++ b/drivers/energylink/pair/start.html @@ -1,84 +1,104 @@ +

-
- -
-
-

- +
+ +
+
+

+ diff --git a/drivers/heatlink/assets/icon.svg b/drivers/heatlink/assets/icon.svg new file mode 100644 index 00000000..14d493af --- /dev/null +++ b/drivers/heatlink/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/heatlink/device.js b/drivers/heatlink/device.js new file mode 100644 index 00000000..6d53cf57 --- /dev/null +++ b/drivers/heatlink/device.js @@ -0,0 +1,263 @@ +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); + +const debug = false; + +function callnewAsync(device_id, uri_part, { + timeout = 5000, + retries = 2, + retryDelay = 3000 +} = {}) { + + return new Promise((resolve, reject) => { + let attempts = 0; + + const attempt = () => { + attempts++; + + let finished = false; + const timeoutId = setTimeout(() => { + if (finished) return; + finished = true; + + if (attempts <= retries) { + return setTimeout(attempt, retryDelay); + } + + return reject(new Error(`Timeout after ${timeout}ms`)); + }, timeout); + + homewizard.callnew(device_id, uri_part, (err, result) => { + if (finished) return; + finished = true; + clearTimeout(timeoutId); + + if (err) { + if (attempts <= retries) { + return setTimeout(attempt, retryDelay); + } + return reject(err); + } + + return resolve(result); + }); + }; + + attempt(); + }); +} + +class HomeWizardHeatlink extends Homey.Device { + + + async retrySetTarget(homewizard_id, temperature, { + maxAttempts = 5, + delay = 3000 + } = {}) { + + const path = `/hl/0/settarget/${temperature}`; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + + try { + this.log(`Attempt ${attempt}/${maxAttempts}: ${path}`); + await callnewAsync(homewizard_id, path); + return true; // success + } + + catch (err) { + const msg = err?.message || String(err); + + // Circuit breaker open → wait and retry + if (msg.includes('circuit_open')) { + this.log(`Circuit open, retrying in ${delay}ms...`); + await new Promise(r => setTimeout(r, delay)); + continue; + } + + // Real error → stop immediately + this.log(`Non-retryable error: ${msg}`); + throw err; + } + } + + throw new Error('Failed after retries (circuit still open)'); + } + + + + async onInit() { + + this.log(`Heatlink init: ${this.getName()}`); + + this.startPolling(); + + this.registerCapabilityListener('target_temperature', async (temperature) => { + if (!temperature) return false; + + if (temperature < 5) temperature = 5; + else if (temperature > 35) temperature = 35; + + temperature = Math.round(temperature.toFixed(1) * 2) / 2; + + const homewizard_id = this.getSetting('homewizard_id'); + + try { + const ok = await this.retrySetTarget(homewizard_id, temperature); + + if (ok) { + this.log('settarget target_temperature -> true'); + await this.setStoreValue('setTemperature', temperature); + return true; + } + + } catch (err) { + this.log('ERR settarget target_temperature -> false'); + this.error(`Heatlink ${this.getName()} settarget failed: ${err.message}`); + + await this.setStoreValue('setTemperature', 0); + this.getStatus().catch(this.error); + return false; + } + }); + + + + } + + startPolling() { + + if (this.refreshIntervalId) { + clearInterval(this.refreshIntervalId); + } + + this.refreshIntervalId = setInterval(() => { + if (debug) this.log('--Heatlink Poll--'); + this.getStatus(); + }, 20000); // 20 sec + } + + async getStatus() { + + const homewizard_id = this.getSetting('homewizard_id'); + if (!homewizard_id) { + this.log('HW ID not found'); + return; + } + + try { + // ❗ getDeviceData is async (in-memory) + const callback = await homewizard.getDeviceData(homewizard_id, 'heatlinks'); + + if (!callback || Object.keys(callback).length === 0) { + if (debug) this.log('No heatlink data yet'); + return; + } + + this.setAvailable().catch(this.error); + + const promises = []; + + const rte = (callback[0].rte.toFixed(1) * 2) / 2; + const rsp = (callback[0].rsp.toFixed(1) * 2) / 2; + const tte = (callback[0].tte.toFixed(1) * 2) / 2; + const wte = (callback[0].wte.toFixed(1) * 2) / 2; + + if (this.getStoreValue('temperature') != rte) { + promises.push(this.setCapabilityValue('measure_temperature', rte).catch(this.error)); + this.setStoreValue('temperature', rte).catch(this.error); + } + + if (this.getStoreValue('thermTemperature') != rsp) { + if (this.getStoreValue('setTemperature') === 0) { + promises.push(this.setCapabilityValue('target_temperature', rsp).catch(this.error)); + } + this.setStoreValue('thermTemperature', rsp).catch(this.error); + } + + const override = await this.getStoreValue('setTemperature'); + + // If override is active but Heatlink reports a different tte → clear override + if (override > 0 && tte !== override) { + this.log('Heatlink rejected override, clearing override flag'); + await this.setStoreValue('setTemperature', 0).catch(this.error); + + // Immediately sync Homey to real Heatlink value + promises.push(this.setCapabilityValue('target_temperature', rsp).catch(this.error)); + } + + + if (override > 0) { + // Override active → Homey must follow the override + if (tte === override) { + // Heatlink has accepted the override + promises.push(this.setCapabilityValue('target_temperature', tte).catch(this.error)); + } else { + // Heatlink has NOT accepted the override yet + // Do NOT overwrite Homey with stale tte + // Let polling try again later + } + } else { + // No override → follow thermostat setpoint (rsp) + promises.push(this.setCapabilityValue('target_temperature', rsp).catch(this.error)); + } + + + if (!this.hasCapability('measure_temperature.boiler')) { + promises.push(this.addCapability('measure_temperature.boiler').catch(this.error)); + } else { + promises.push(this.setCapabilityValue('measure_temperature.boiler', wte).catch(this.error)); + } + + if (!this.hasCapability('measure_temperature.heatlink')) { + promises.push(this.addCapability('measure_temperature.heatlink').catch(this.error)); + } else { + promises.push(this.setCapabilityValue('measure_temperature.heatlink', tte).catch(this.error)); + } + + if (!this.hasCapability('central_heating_flame')) { + promises.push(this.addCapability('central_heating_flame').catch(this.error)); + } else { + promises.push(this.setCapabilityValue('central_heating_flame', callback[0].heating === 'on').catch(this.error)); + } + + if (!this.hasCapability('central_heating_pump')) { + promises.push(this.addCapability('central_heating_pump').catch(this.error)); + } else { + promises.push(this.setCapabilityValue('central_heating_pump', callback[0].pump === 'on').catch(this.error)); + } + + if (!this.hasCapability('warm_water')) { + promises.push(this.addCapability('warm_water').catch(this.error)); + } else { + promises.push(this.setCapabilityValue('warm_water', callback[0].dhw === 'on').catch(this.error)); + } + + if (!this.hasCapability('measure_pressure')) { + promises.push(this.addCapability('measure_pressure').catch(this.error)); + } else { + promises.push(this.setCapabilityValue('measure_pressure', callback[0].wp).catch(this.error)); + } + + await Promise.allSettled(promises); + + } catch (error) { + this.log('Heatlink data error', error); + this.setUnavailable(error).catch(this.error); + } + } + + onDeleted() { + const deviceId = this.getData().id; + homewizard.removeDevice(deviceId); + + if (this.refreshIntervalId) { + clearInterval(this.refreshIntervalId); + } + this.log(`Heatlink deleted: ${this.getName()}`); + } +} + +module.exports = HomeWizardHeatlink; diff --git a/drivers/heatlink/driver.compose.json b/drivers/heatlink/driver.compose.json new file mode 100644 index 00000000..dc2c461a --- /dev/null +++ b/drivers/heatlink/driver.compose.json @@ -0,0 +1,66 @@ +{ + "name": { + "en": "Heatlink", + "nl": "Heatlink" + }, + "images": { + "large": "drivers/heatlink/assets/images/large.jpg", + "small": "drivers/heatlink/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "thermostat", + "platforms": [ "local"], + "capabilities": [ + "measure_temperature", + "target_temperature", + "measure_temperature.heatlink", + "measure_temperature.boiler", + "central_heating_pump", + "central_heating_flame", + "warm_water", + "measure_pressure" + ], + "capabilitiesOptions": { + "measure_temperature.heatlink": { + "title": { + "en": "Heatlink target temperature", + "nl": "Heatlink doel temperatuur" + } + }, + "measure_temperature.boiler": { + "title": { + "en": "Boiler temperature", + "nl": "Ketel temperatuur" + } + }, + "measure_pressure": { + "decimals": 1, + "title": { + "en": "Water pressure", + "nl": "Waterdruk" + }, + "units": { + "en": "Bar", + "nl": "Bar" + } + } + }, + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/heatlink/driver.js b/drivers/heatlink/driver.js index 3ae92c70..15c1dc2e 100755 --- a/drivers/heatlink/driver.js +++ b/drivers/heatlink/driver.js @@ -1,192 +1,131 @@ -var devices = []; -var homewizard = require('./../../includes/homewizard.js'); -var refreshIntervalId = 0; - -// SETTINGS -module.exports.settings = function( device_data, newSettingsObj, oldSettingsObj, changedKeysArr, callback ) { - Homey.log ('Changed settings: ' + JSON.stringify(device_data) + ' / ' + JSON.stringify(newSettingsObj) + ' / old = ' + JSON.stringify(oldSettingsObj)); - try { - changedKeysArr.forEach(function (key) { - devices[device_data.id].settings[key] = newSettingsObj[key]; - }); - callback(null, true); - } catch (error) { - callback(error); - } -}; - -module.exports.pair = function( socket ) { - socket.on('get_homewizards', function () { - homewizard.getDevices(function(homewizard_devices) { - - Homey.log(homewizard_devices); - var hw_devices = {}; - Object.keys(homewizard_devices).forEach(function(key) { - hw_devices[key] = homewizard_devices[key]; - }); - - socket.emit('hw_devices', hw_devices); - }); - }); - - socket.on('manual_add', function (device, callback) { - if (device.settings.homewizard_id.indexOf('HW_') === -1 && device.settings.homewizard_id.indexOf('HW') === 0) { - //true - Homey.log('HeatLink added ' + device.data.id); - devices[device.data.id] = { - id: device.data.id, - name: device.name, - settings: device.settings, - }; - callback( null, devices ); - socket.emit("success", device); - startPolling(); - } else { - socket.emit("error", "No valid HomeWizard found, re-pair if problem persists"); +'use strict'; + +const Homey = require('homey'); + +// const { ManagerDrivers } = require('homey'); +// const driver = ManagerDrivers.getDriver('homewizard'); +const devices = {}; +const homewizard = require('../../includes/legacy/homewizard.js'); + +let homewizard_devices; + +function callnewAsync(device_id, uri_part, { + timeout = 5000, + retries = 2, + retryDelay = 3000 +} = {}) { + + return new Promise((resolve, reject) => { + + let attempts = 0; + + const attempt = () => { + attempts++; + + let timeoutId; + let finished = false; + + // Timeout mechanisme + timeoutId = setTimeout(() => { + if (finished) return; + finished = true; + + if (attempts <= retries) { + return setTimeout(attempt, retryDelay); } - }); - - socket.on('disconnect', function(){ - console.log("User aborted pairing, or pairing is finished"); - }); + + return reject(new Error(`Timeout after ${timeout}ms`)); + }, timeout); + + // De echte call + homewizard.callnew(device_id, uri_part, (err, result) => { + if (finished) return; + finished = true; + clearTimeout(timeoutId); + + if (err) { + if (attempts <= retries) { + return setTimeout(attempt, retryDelay); + } + return reject(err); + } + + return resolve(result); + }); + }; + + attempt(); + }); } -module.exports.init = function(devices_data, callback) { - devices_data.forEach(function initdevice(device) { - Homey.log('add device: ' + JSON.stringify(device)); - devices[device.id] = device; - module.exports.getSettings(device, function(err, settings){ - devices[device.id].settings = settings; - }); - - }); - if (Object.keys(devices).length > 0) { - startPolling(); - } - Homey.log('Heatlink driver init done'); - - callback (null, true); -}; - -module.exports.deleted = function( device_data ) { - clearInterval(refreshIntervalId); - Homey.log("--Stopped Polling--"); - devices = []; - Homey.log('deleted: ' + JSON.stringify(device_data)); -}; - - -module.exports.capabilities = { - - measure_temperature: { - get: function (device, callback) { - if (device instanceof Error) return callback(device); - console.log("measure_temperature"); - getStatus(device.id); - newvalue = devices[device.id].temperature; - // Callback ambient temperature - callback(null, newvalue); - } - }, - target_temperature: { - - get: function (device, callback) { - if (device instanceof Error) return callback(device); - console.log("target_temperature:get"); - // Retrieve updated data - getStatus(device.id); - if (devices[device.id].setTemperature !== 0) { - newvalue = devices[device.id].setTemperature; - } else { - newvalue = devices[device.id].thermTemperature; - } - callback(null, newvalue); - }, - - set: function (device, temperature, callback) { - if (device instanceof Error) return callback(device); - // Catch faulty trigger and max/min temp - if (!temperature) { - callback(true, temperature); +class HomeWizardHeatlink extends Homey.Driver { + + onInit() { + //this.log('HomeWizard Heatlink has been inited'); + + this.homey.flow.getActionCard('heatlink_off') + // .register() + .registerRunListener(async (args) => { + if (!args.device) return false; + + try { + await callnewAsync(args.device.getData().id, '/hl/0/settarget/0'); + this.log('flowCardAction heatlink_off -> returned true'); + return true; + } catch (err) { + this.log('ERR flowCardAction heatlink_off -> returned false: ', err.message); return false; } - else if (temperature < 5) { - temperature = 5; - } - else if (temperature > 35) { - temperature = 35; + }); + } + + async onPair(socket) { + socket.setHandler('get_heatlinks', async () => { + const fetchedDevices = homewizard.self.devices || {}; + const heatLinkList = []; + + Object.keys(fetchedDevices).forEach(hwId => { + const device = fetchedDevices[hwId]; + const heatlinks = device.polldata?.heatlinks || []; + + if (Array.isArray(heatlinks) && heatlinks.length > 0) { + heatLinkList.push({ + homewizard_id: hwId, + name: device.name || device.settings?.homewizard_ip || hwId, + ip: device.settings?.homewizard_ip, + heatlink_name: heatlinks[0].name || 'HeatLink' + }); } - temperature = Math.round(temperature.toFixed(1) * 2) / 2; - var url = '/hl/0/settarget/'+temperature; - console.log(url); - var homewizard_id = devices[device.id].settings.homewizard_id; - homewizard.call(homewizard_id, '/hl/0/settarget/'+temperature, function(err, response) { - console.log(err); - if (callback) callback(err, temperature); - }); - } - }, -}; - -function getStatus(device_id) { - if(devices[device_id].settings.homewizard_id !== undefined ) { - var homewizard_id = devices[device_id].settings.homewizard_id; - homewizard.getDeviceData(homewizard_id, 'heatlinks', function(callback) { - if (Object.keys(callback).length > 0) { - try { - var rte = (callback[0].rte.toFixed(1) * 2) / 2; - var rsp = (callback[0].rsp.toFixed(1) * 2) / 2; - var tte = (callback[0].tte.toFixed(1) * 2) / 2; - - //Check current temperature - if (devices[device_id].temperature != rte) { - console.log("New RTE - "+ rte); - module.exports.realtime( { id: device_id }, "measure_temperature", rte ); - devices[device_id].temperature = rte; - } else { - console.log("RTE: no change"); - } - - //Check thermostat temperature - if (devices[device_id].thermTemperature != rsp) { - console.log("New RSP - "+ rsp); - if (devices[device_id].setTemperature === 0) { - module.exports.realtime( { id: device_id }, "target_temperature", rsp ); - } - devices[device_id].thermTemperature = rsp; - } else { - console.log("RSP: no change"); - } - - //Check heatlink set temperature - if (devices[device_id].setTemperature != tte) { - console.log("New TTE - "+ tte); - if (tte > 0) { - module.exports.realtime( { id: device_id }, "target_temperature", tte ); - } else { - module.exports.realtime( { id: device_id }, "target_temperature", devices[device_id].thermTemperature ); - } - devices[device_id].setTemperature = tte; - } else { - console.log("TTE: no change"); - } - } catch(err) { - console.log ("Heatlink data corrupt"); - } - } - }); - } else { - Homey.log('Removed Heatlink '+ device_id +' (old settings)'); - module.exports.setUnavailable({id: device_id}, "No Heatlink found" ); - clearInterval(refreshIntervalId); - } - } - - function startPolling() { - refreshIntervalId = setInterval(function () { - Homey.log("--Start Heatlink Polling-- "); - Object.keys(devices).forEach(function (device_id) { - getStatus(device_id); }); - }, 1000 * 10); - } \ No newline at end of file + + this.log('HomeWizard devices with HeatLinks:', heatLinkList.length); + socket.emit('heatlink_list', heatLinkList); + }); + + socket.setHandler('manual_add', async (device) => { + const hwId = device.settings.homewizard_id; + + if (!hwId || hwId === '') { + socket.emit('error', 'No HomeWizard selected'); + return; + } + + this.log(`HeatLink added ${device.data.id} on HomeWizard ${hwId}`); + devices[device.data.id] = { + id: device.data.id, + name: device.name, + settings: device.settings, + }; + socket.emit('success', device); + }); + + socket.setHandler('disconnect', () => { + this.log('User aborted pairing, or pairing is finished'); + }); + } + +} + +module.exports = HomeWizardHeatlink; + + diff --git a/drivers/heatlink/pair/start.html b/drivers/heatlink/pair/start.html index 4e34acb4..fb0106af 100755 --- a/drivers/heatlink/pair/start.html +++ b/drivers/heatlink/pair/start.html @@ -1,70 +1,76 @@

- - + +
- - + +

@@ -86,6 +94,6 @@

- + \ No newline at end of file diff --git a/drivers/kakusensors/assets/icon.svg b/drivers/kakusensors/assets/icon.svg new file mode 100644 index 00000000..d59da4d3 --- /dev/null +++ b/drivers/kakusensors/assets/icon.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drivers/kakusensors/assets/images/large.jpg b/drivers/kakusensors/assets/images/large.jpg new file mode 100644 index 00000000..f46ba736 Binary files /dev/null and b/drivers/kakusensors/assets/images/large.jpg differ diff --git a/drivers/kakusensors/assets/images/small.jpg b/drivers/kakusensors/assets/images/small.jpg new file mode 100644 index 00000000..d9885a47 Binary files /dev/null and b/drivers/kakusensors/assets/images/small.jpg differ diff --git a/drivers/kakusensors/device.js b/drivers/kakusensors/device.js new file mode 100644 index 00000000..b6bd2642 --- /dev/null +++ b/drivers/kakusensors/device.js @@ -0,0 +1,165 @@ +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); + +const debug = false; + +class HomeWizardKakusensors extends Homey.Device { + + async onInit() { + + if (debug) this.log(`Init Kakusensor ${this.getName()}`); + + this.startPolling(); + } + + startPolling() { + if (this.refreshIntervalId) clearInterval(this.refreshIntervalId); + + this.refreshIntervalId = setInterval(() => { + this.poll(); + }, 20000); + } + + async poll() { + const hwId = this.getSetting('homewizard_id'); + const sensorId = this.getSetting('kakusensors_id'); + const sensorType = this.getSetting('kakusensor_type'); + + if (!hwId || !sensorId) return; + + try { + const sensors = await homewizard.getDeviceData(hwId, 'kakusensors'); + if (!Array.isArray(sensors)) return; + + const entry = sensors.find(s => s.id == sensorId); + if (!entry) return; + + const status = entry.status === 'yes'; + + // Motion + if (sensorType === 'motion') { + if (!this.hasCapability('alarm_motion')) { + try { + await this.addCapability('alarm_motion'); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log('Capability already exists: alarm_motion — ignoring'); + } else { + throw err; + } + } + } + if (this.getCapabilityValue('alarm_motion') !== status) { + await this.setCapabilityValue('alarm_motion', status); + } + } + + // Smoke + if (sensorType === 'smoke' || sensorType === 'smoke868') { + if (!this.hasCapability('alarm_smoke')) { + try { + await this.addCapability('alarm_smoke'); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log('Capability already exists: alarm_smoke — ignoring'); + } else { + throw err; + } + } + } + if (this.getCapabilityValue('alarm_smoke') !== status) { + await this.setCapabilityValue('alarm_smoke', status); + } + } + + // Water leakage + if (sensorType === 'leakage') { + if (!this.hasCapability('alarm_water')) { + try { + await this.addCapability('alarm_water'); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log('Capability already exists: alarm_water — ignoring'); + } else { + throw err; + } + } + } + if (this.getCapabilityValue('alarm_water') !== status) { + await this.setCapabilityValue('alarm_water', status); + } + } + + // Contact + if (sensorType === 'contact' || sensorType === 'contact868') { + if (!this.hasCapability('alarm_contact')) { + try { + await this.addCapability('alarm_contact'); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log('Capability already exists: alarm_contact — ignoring'); + } else { + throw err; + } + } + } + if (this.getCapabilityValue('alarm_contact') !== status) { + await this.setCapabilityValue('alarm_contact', status); + } + } + + // Doorbell + if (sensorType === 'doorbell') { + if (!this.hasCapability('alarm_generic')) { + try { + await this.addCapability('alarm_generic'); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log('Capability already exists: alarm_generic — ignoring'); + } else { + throw err; + } + } + } + if (this.getCapabilityValue('alarm_generic') !== status) { + await this.setCapabilityValue('alarm_generic', status); + } + } + + // Battery (optioneel) + if (entry.lowBattery !== undefined) { + const low = entry.lowBattery === 'yes'; + if (!this.hasCapability('alarm_battery')) { + try { + await this.addCapability('alarm_battery'); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log('Capability already exists: alarm_battery — ignoring'); + } else { + throw err; + } + } + } + if (this.getCapabilityValue('alarm_battery') !== low) { + await this.setCapabilityValue('alarm_battery', low); + } + } + + this.setAvailable().catch(() => {}); + + } catch (err) { + this.log('Kakusensor poll error:', err); + } + } + + onDeleted() { + const deviceId = this.getData().id; + homewizard.removeDevice(deviceId); + + if (this.refreshIntervalId) clearInterval(this.refreshIntervalId); + } +} + +module.exports = HomeWizardKakusensors; diff --git a/drivers/kakusensors/driver.compose.json b/drivers/kakusensors/driver.compose.json new file mode 100644 index 00000000..018d1708 --- /dev/null +++ b/drivers/kakusensors/driver.compose.json @@ -0,0 +1,32 @@ +{ + "name": { + "en": "Smoke, Motion and door senors", + "nl": "Rook, beweging en deur sensors" + }, + "images": { + "large": "drivers/kakusensors/assets/images/large.jpg", + "small": "drivers/kakusensors/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "platforms": [ "local"], + "capabilities": [], + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/kakusensors/driver.js b/drivers/kakusensors/driver.js new file mode 100644 index 00000000..d9df4289 --- /dev/null +++ b/drivers/kakusensors/driver.js @@ -0,0 +1,63 @@ +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); + +class HomeWizardKakusensors extends Homey.Driver { + + onInit() { + // this.log('HomeWizard Kakusensors driver inited'); + } + + async onPair(socket) { + + await socket.showView('start'); + + socket.setHandler('get_kakusensors', async () => { + const fetchedDevices = homewizard.self.devices || {}; + const sensorList = []; + const hwIds = Object.keys(fetchedDevices); + + await Promise.all( + hwIds.map(hwId => { + return new Promise(resolve => { + homewizard.callnew(hwId, '/get-sensors', (err, response) => { + if (err || !response) return resolve(); + + const kakusensors = response.kakusensors || []; + kakusensors.forEach(sensor => { + sensorList.push({ + id: sensor.id, + name: sensor.name, + type: sensor.type, + homewizard_id: hwId + }); + }); + + resolve(); + }); + }); + }) + ); + + this.log('[PAIRING] Kakusensor list:', sensorList); + socket.emit('kakusensor_list', sensorList); + }); + + socket.setHandler('manual_add', async (device) => { + const hwId = device.settings.homewizard_id; + const sensorId = device.settings.kakusensors_id; + + if (!hwId || sensorId === undefined) { + socket.emit('error', this.homey.__("settings.selection_error")); + return; + } + + socket.emit('success', device); + }); + + socket.setHandler('disconnect', () => {}); + } +} + +module.exports = HomeWizardKakusensors; diff --git a/drivers/kakusensors/pair/start.html b/drivers/kakusensors/pair/start.html new file mode 100644 index 00000000..3bc9dc64 --- /dev/null +++ b/drivers/kakusensors/pair/start.html @@ -0,0 +1,108 @@ + + + + + + + +

+
+ +
+
+

+ + + diff --git a/drivers/plugin_battery/assets/clock.svg b/drivers/plugin_battery/assets/clock.svg new file mode 100644 index 00000000..d7c59d99 --- /dev/null +++ b/drivers/plugin_battery/assets/clock.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/drivers/plugin_battery/assets/cycle.svg b/drivers/plugin_battery/assets/cycle.svg new file mode 100644 index 00000000..8c7360f1 --- /dev/null +++ b/drivers/plugin_battery/assets/cycle.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/drivers/plugin_battery/assets/icon.svg b/drivers/plugin_battery/assets/icon.svg new file mode 100644 index 00000000..40b00204 --- /dev/null +++ b/drivers/plugin_battery/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/plugin_battery/assets/images/large.png b/drivers/plugin_battery/assets/images/large.png new file mode 100644 index 00000000..78fa22a6 Binary files /dev/null and b/drivers/plugin_battery/assets/images/large.png differ diff --git a/drivers/plugin_battery/assets/images/small.png b/drivers/plugin_battery/assets/images/small.png new file mode 100644 index 00000000..b6aa955d Binary files /dev/null and b/drivers/plugin_battery/assets/images/small.png differ diff --git a/drivers/plugin_battery/assets/rssi.svg b/drivers/plugin_battery/assets/rssi.svg new file mode 100644 index 00000000..e98392f6 --- /dev/null +++ b/drivers/plugin_battery/assets/rssi.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drivers/plugin_battery/assets/tariff.svg b/drivers/plugin_battery/assets/tariff.svg new file mode 100644 index 00000000..ca38a2b8 --- /dev/null +++ b/drivers/plugin_battery/assets/tariff.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/drivers/plugin_battery/assets/widget/README.md b/drivers/plugin_battery/assets/widget/README.md new file mode 100644 index 00000000..72451164 --- /dev/null +++ b/drivers/plugin_battery/assets/widget/README.md @@ -0,0 +1,95 @@ +# Plugin Battery State of Charge Widget + +## Overview + +A beautiful, responsive widget that displays the Plugin Battery's state of charge (SoC) as a percentage. The widget provides real-time visual feedback with a color-coded battery icon that changes based on the charge level. + +## Features + +- **Real-time Percentage Display**: Shows current battery charge level as a large, easy-to-read percentage +- **Visual Battery Icon**: Animated battery icon with dynamic fill based on charge level +- **Color Coding**: + - Green (50-100%): Healthy charge + - Orange (20-50%): Medium charge + - Red (0-20%): Low battery warning +- **Connection Status**: Displays whether the device is connected +- **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices +- **Smooth Animations**: Transitions smoothly when battery level changes + +## Widget Details + +### HTML File +- **Location**: `drivers/plugin_battery/assets/widget/soc_widget.html` +- **Capability Used**: `measure_battery` (state of charge percentage) +- **Update Frequency**: Real-time as battery level changes + +### Configuration + +The widget is configured in `driver.compose.json` with: +- **ID**: `battery_soc_widget` +- **Template**: Generic widget template +- **Supported Capability**: `measure_battery` (0-100%) + +### Trigger Tokens + +The widget provides the following trigger token: +- **state_of_charge**: Returns the current battery charge percentage (0-100) + +## Display Elements + +1. **Title**: "Battery State of Charge" +2. **Battery Icon**: Visual representation with: + - Battery body showing fill level + - Terminal at the top + - Color-coded fill (green → orange → red) +3. **Percentage Value**: Large, bold display of current charge level +4. **Status Indicator**: Shows connection status with indicator light +5. **Label**: "Charge Level" text + +## Integration with Homey + +The widget automatically: +- Updates whenever the `measure_battery` capability value changes +- Reflects real-time battery state from your Plugin Battery device +- Displays connection status based on device availability + +## Usage + +1. Add this widget to your Homey dashboard +2. Select your Plugin Battery device +3. The widget will display the current state of charge percentage +4. Watch the battery icon fill/empty as the charge level changes + +## Styling Notes + +- **Font**: System fonts (SF Pro Display, Segoe UI, Roboto) +- **Color Scheme**: + - Background gradient: Purple (#667eea to #764ba2) + - Widget background: Clean white + - Text: Dark gray (#333) for readability +- **Border Radius**: 24px for modern, rounded corners +- **Shadow**: Soft shadow for depth + +## Responsive Breakpoints + +- **Desktop**: Full 300px width widget +- **Tablet**: Scales appropriately +- **Mobile** (<480px): Compact layout with adjusted font sizes + +## Future Enhancements (Optional) + +Potential improvements for future versions: +- Time-to-full estimate display +- Time-to-empty estimate display +- Charging/discharging rate indicator +- Historical trend graph +- Temperature display +- Multi-language support (EN, NL already configured) + +## Localization + +The widget supports multiple languages configured in `driver.compose.json`: +- **English (en)** +- **Dutch (nl)** + +All labels and descriptions are localized. diff --git a/drivers/plugin_battery/device.js b/drivers/plugin_battery/device.js new file mode 100644 index 00000000..48b66354 --- /dev/null +++ b/drivers/plugin_battery/device.js @@ -0,0 +1,1108 @@ +'use strict'; + +const Homey = require('homey'); +const fetch = require('node-fetch'); +const https = require('https'); +const WebSocketManager = require('../../includes/v2/Ws'); +const wsDebug = require('../../includes/v2/wsDebug'); +const api = require('../../includes/v2/Api'); +const debug = false; + +// Per-device HTTPS agent is created in onInit() and destroyed in onUninit() + + +// --------------------------------------------------------- +// estimateBatteryKWh (unchanged) +// --------------------------------------------------------- +function estimateBatteryKWh(loadPct, cycles, inverterEfficiency) { + const nominalCapacity = 2.688; // HW battery spec: 2688 Wh per unit + const referenceCycles = 6000; + const referenceDegradation = 0.7; + + const degradationRate = (1 - referenceDegradation) / referenceCycles; + let degradationFactor = 1 - (degradationRate * cycles); + degradationFactor = Math.max(degradationFactor, 0); + + if (inverterEfficiency < 0.75) inverterEfficiency = 0.75; + + return nominalCapacity * inverterEfficiency * (loadPct / 100) * degradationFactor; +} + +// --------------------------------------------------------- +// checkSoCDrift (unchanged) +// --------------------------------------------------------- +function checkSoCDrift({ + previousSoC, + previousTimestamp, + currentSoC, + currentPowerW, + batteryCapacityWh = 2470 +}) { + // Geen vorige meting → geen drift + if (previousSoC === undefined || previousTimestamp === undefined) { + return { drift: false }; + } + + const now = Date.now(); + const deltaTimeMin = (now - previousTimestamp) / 60000; + + // Moet lang genoeg duren om cloudy PV noise te filteren + // BMS calibratie: 45min @ 75W + 15min @ 800W = sustained pattern + if (deltaTimeMin < 20) { + return { drift: false }; + } + + // SoC moet stuck zijn op 0% (zowel vorige als huidige meting) + if (previousSoC !== 0 || currentSoC !== 0) { + return { drift: false }; + } + + // Must be actively charging (not idle or discharging) + if (currentPowerW < 50) { + return { drift: false }; + } + + // BMS calibration signatures: + // Phase 1: ~75W for 45min+ (50-150W range to catch variations) + // Phase 2: ~800W for 15min+ (700-900W range for full power phase) + const isLowPowerCalibration = currentPowerW >= 50 && currentPowerW <= 150 && deltaTimeMin >= 20; + const isHighPowerCalibration = currentPowerW >= 700 && currentPowerW <= 900 && deltaTimeMin >= 20; + + // 20+ minuten laden terwijl SoC stuck op 0% → zeer waarschijnlijk BMS drift + // (cloudy PV zou niet zo consistent zijn over 20min+) + if (isLowPowerCalibration || isHighPowerCalibration) { + return { drift: true }; + } + + return { drift: false }; +} + + + + + +// --------------------------------------------------------- +// getWifiQuality (unchanged) +// --------------------------------------------------------- +function getWifiQuality(strength) { + if (typeof strength !== 'number') return 'Unknown'; + if (strength >= -30) return 'Excellent'; + if (strength >= -60) return 'Strong'; + if (strength >= -70) return 'Moderate'; + if (strength >= -80) return 'Weak'; + if (strength >= -90) return 'Poor'; + return 'Unusable'; +} + +// --------------------------------------------------------- +// updateCapability (unchanged) +// --------------------------------------------------------- +async function updateCapability(device, capability, value) { + const current = device.getCapabilityValue(capability); + + if (value === undefined || value === null) return; + + if (!device.hasCapability(capability)) { + try { + await device.addCapability(capability); + device.log(`➕ Added capability "${capability}"`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + } else { + device.error(err); + } + } + } + + if (current !== value) { + await device.setCapabilityValue(capability, value).catch(device.error); + } +} + + +// --------------------------------------------------------- +// fetchWithTimeout (unchanged) +// --------------------------------------------------------- + +function fetchWithTimeout(url, options = {}, timeoutMs = 5000) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + return fetch(url, { ...options, signal: controller.signal }) + .finally(() => clearTimeout(timeout)); +} + + +// --------------------------------------------------------- +// DEVICE CLASS +// --------------------------------------------------------- +module.exports = class HomeWizardPluginBattery extends Homey.Device { + + async onInit() { + wsDebug.init(this.homey); + + this._pollErrorCount = 0; + this._lastPollAt = 0; + + // Per-device HTTPS agent — destroyed in onUninit() to prevent socket accumulation + this._httpsAgent = new https.Agent({ + rejectUnauthorized: false, + keepAlive: true, + keepAliveMsecs: 15000, + maxSockets: 2, + maxFreeSockets: 2, + }); + + if (this.hasCapability('battery_soc')) { + this.removeCapability('battery_soc').catch(this.error); + } + + await this._updateCapabilities(); + await this._registerCapabilityListeners(); + + this.previousChargingState = null; + this.previousTimeToEmpty = null; + this.previousStateOfCharge = null; + this._prevTimeToFull = this.getCapabilityValue('time_to_full') ?? 0; + this._prevTimeToEmpty = this.getCapabilityValue('time_to_empty') ?? 0; + this._lastDiscoveryIP = null; + + this.token = await this.getStoreValue('token'); + + const settings = { use_polling: false, ...this.getSettings() }; + this.log('Plugin Battery settings:', settings); + + if (!this.url && settings.url) { + this.url = settings.url; + this.log(`Restored URL from settings: ${this.url}`); + } + + if (settings.polling_interval == null) { + await this.setSettings({ polling_interval: 10 }); + settings.polling_interval = 10; + } + + // Stop old WS if present + if (this.wsManager) { + this.wsManager.stop(); + this.wsManager = null; + } + + // Bind handler functions ONCE to avoid creating new function objects on every reconnect (memory leak) + this._boundLog = this.log.bind(this); + this._boundError = this.error.bind(this); + this._boundSetAvailable = this.setAvailable.bind(this); + this._boundGetSetting = this.getSetting.bind(this); + this._boundHandleMeasurement = (data) => { + this.lastWsMeasurementAt = Date.now(); + this._handleMeasurement(data); + }; + this._boundHandleSystem = this._handleSystem.bind(this); + + // ----------------------------------------------------- + // SELECT DATA SOURCE + // ----------------------------------------------------- + if (settings.use_polling) { + const intervalSec = settings.polling_interval || 10; + // ✅ CPU FIX: Stagger startup across devices (spread over 0-30s) + // Prevents thundering herd: all 3 batteries firing first TLS poll simultaneously + const startupDelay = Math.floor(Math.random() * 30000); + this.log(`⏱️ Polling enabled at init, interval ${intervalSec}s, startup delay ${Math.round(startupDelay/1000)}s`); + + this._startupPollTimeout = setTimeout(() => { + if (this.__deleted) return; + this._startupPollTimeout = null; + this.onPoll().catch(this.error); + this.onPollInterval = setInterval( + this.onPoll.bind(this), + intervalSec * 1000 + ); + }, startupDelay); + + } else { + this.log('🔌 WebSocket enabled at init'); + + this.wsManager = new WebSocketManager({ + device: this, + url: this.url, + token: this.token, + log: this._boundLog, + error: this._boundError, + setAvailable: this._boundSetAvailable, + getSetting: this._boundGetSetting, + handleMeasurement: this._boundHandleMeasurement, + handleSystem: this._boundHandleSystem, + measurementThrottleMs: 5000, // ✅ CPU FIX: 5s for battery (was 2s) — 4 devices × 30/min instead of 4 × 120/min + onJournalEvent: (type, deviceId, data) => { + if (type === 'snapshot') wsDebug.snapshot(deviceId, data); + else wsDebug.log(type, deviceId, typeof data === 'string' ? data : JSON.stringify(data)); + }, + }); + + this.wsManager.start(); + + // Idle watchdog + this._wsIdleWatchdog = setInterval(() => { + const last = this.lastWsMeasurementAt || 0; + const diff = Date.now() - last; + + if (diff > 10 * 60 * 1000) { + this.log(`🕒 WS idle for ${diff}ms → fallback poll`); + this._fallbackPoll(); + } + }, 60000); + + // Stale WS watchdog + this._wsWatchdog = setInterval(() => { + const staleMs = Date.now() - (this.wsManager?.lastMeasurementAt || 0); + if (!this.getSettings().use_polling && staleMs > 190000) { + this.log(`🕒 WS stale >3min (${staleMs}ms), restarting`); + this.wsManager?.restartWebSocket(); + } + }, 60000); + + // Battery group updater (reduced from 10s to 60s to lower CPU usage) + this._batteryGroupInterval = setInterval(() => { + this._updateBatteryGroup(); + }, 60000); + } + } + + onUninit() { + // Cleanup intervals and timers when app stops/crashes + this.__deleted = true; + + if (this._startupPollTimeout) { + clearTimeout(this._startupPollTimeout); + this._startupPollTimeout = null; + } + if (this._wsWatchdog) { + clearInterval(this._wsWatchdog); + this._wsWatchdog = null; + } + if (this._wsIdleWatchdog) { + clearInterval(this._wsIdleWatchdog); + this._wsIdleWatchdog = null; + } + if (this._wsReconnectTimeout) { + clearTimeout(this._wsReconnectTimeout); + this._wsReconnectTimeout = null; + } + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + if (this._batteryGroupInterval) { + clearInterval(this._batteryGroupInterval); + this._batteryGroupInterval = null; + } + + if (this.wsManager) { + this.wsManager.stop(); + this.wsManager = null; + } + + // Destroy HTTPS agent to close all keep-alive TLS sockets + if (this._httpsAgent) { + this._httpsAgent.destroy(); + this._httpsAgent = null; + } + } + + onDeleted() { + // Call onUninit to cleanup timers/intervals + this.onUninit(); + + // Remove from battery group (only on explicit device deletion) + const batteryId = this.getData().id; + const group = this.homey.settings.get('pluginBatteryGroup') || {}; + if (group[batteryId]) { + delete group[batteryId]; + this.homey.settings.set('pluginBatteryGroup', group); + this.log(`Battery ${batteryId} removed from pluginBatteryGroup`); + } + } + + /** + * Discovery handlers + */ + async onDiscoveryAvailable(discoveryResult) { + const newIP = discoveryResult.address; + + if (!this._lastDiscoveryIP) { + this._lastDiscoveryIP = newIP; + this.url = `https://${newIP}`; + this.log(`🌐 Discovery: initial IP ${newIP}`); + await this.setSettings({ url: this.url }).catch(this.error); + return; + } + + if (this._lastDiscoveryIP === newIP) { + this.log(`🌐 Discovery: IP unchanged (${newIP})`); + return; + } + + this._lastDiscoveryIP = newIP; + this.url = `https://${newIP}`; + this.log(`🌐 Discovery: IP changed → ${newIP}`); + await this.setSettings({ url: this.url }).catch(this.error); + + this._rebuildWebSocketDebounced(); + } + + async onDiscoveryAddressChanged(discoveryResult) { + const newIP = discoveryResult.address; + + if (this._lastDiscoveryIP === newIP) { + this.log(`🌐 AddressChanged: IP unchanged (${newIP})`); + return; + } + + this._lastDiscoveryIP = newIP; + this.url = `https://${newIP}`; + this.log(`🌐 Address changed → ${newIP}`); + await this.setSettings({ url: this.url }).catch(this.error); + + this._rebuildWebSocketDebounced(); + } + + async onDiscoveryLastSeenChanged(discoveryResult) { + const newIP = discoveryResult.address; + + if (this._lastDiscoveryIP !== newIP) { + this._lastDiscoveryIP = newIP; + this.url = `https://${newIP}`; + this.log(`📡 LastSeen: IP updated → ${newIP}`); + await this.setSettings({ url: this.url }).catch(this.error); + } else { + this.log(`📡 LastSeen: IP unchanged (${newIP})`); + } + + await this.setAvailable(); + + if (!this.getSettings().use_polling && !this.wsManager?.isConnected()) { + this._rebuildWebSocketDebounced(); + } + } + + /** + * Debounced WebSocket rebuild + */ + _rebuildWebSocketDebounced() { + if (this._wsReconnectTimeout) clearTimeout(this._wsReconnectTimeout); + + this._wsReconnectTimeout = setTimeout(() => { + if (this.getSettings().use_polling) { + this.log('🔁 Polling active → skip WS rebuild'); + return; + } + + this.log('🔁 Rebuilding WebSocket'); + + if (this.wsManager) { + this.wsManager.stop(); + this.wsManager = null; + } + + this.wsManager = new WebSocketManager({ + device: this, + url: this.url, + token: this.token, + log: this._boundLog, + error: this._boundError, + setAvailable: this._boundSetAvailable, + getSetting: this._boundGetSetting, + handleMeasurement: this._boundHandleMeasurement, + handleSystem: this._boundHandleSystem, + measurementThrottleMs: 5000, // ✅ CPU FIX: 5s for battery (was 2s) — 4 devices × 30/min instead of 4 × 120/min + onJournalEvent: (type, deviceId, data) => { + if (type === 'snapshot') wsDebug.snapshot(deviceId, data); + else wsDebug.log(type, deviceId, typeof data === 'string' ? data : JSON.stringify(data)); + }, + }); + + this.wsManager.start(); + + }, 500); + } + /** + * Handle incoming measurement payloads from WS. + * Maps fields to capabilities, updates shared group info and triggers flows. + */ + async _handleMeasurement(data) { + if (!this.getData() || !this.getData().id) { + this.log('⚠️ Ignoring measurement: device no longer exists'); + return; + } + + const devId = this.getData().id; + if (debug) this.log('HANDLE MEASUREMENT:', data); + + try { + const now = Date.now(); + this.lastMeasurementAt = now; + + const BATTERY_CAPACITY_WH = 2470; + + // ✅ CPU FIX: Batch all capability updates to avoid blocking event loop + const capabilityUpdates = []; + + // --------------------------------------------------------- + // 1. REALTIME capabilities — only power changes rapidly enough to warrant every 2s + // ✅ CPU FIX: voltage, current, frequency moved to 5s tier (was realtime) + // With 4 battery devices at 2s throttle, these were firing 120×/min combined + // --------------------------------------------------------- + const realtimeCaps = [ + ['measure_power', data.power_w], + ]; + + for (const [cap, val] of realtimeCaps) { + const cur = this.getCapabilityValue(cap); + if (cur !== val) { + capabilityUpdates.push(updateCapability(this, cap, val)); + } + } + + // --------------------------------------------------------- + // 2. SOC + slow electrical debounced (max 1× per 5 sec) + // --------------------------------------------------------- + if (!this._socLastUpdate || now - this._socLastUpdate > 5000) { + const slowElecCaps = [ + ['measure_voltage', data.voltage_v], + ['measure_current', data.current_a], + ['measure_frequency', data.frequency_hz], + ]; + for (const [cap, val] of slowElecCaps) { + const cur = this.getCapabilityValue(cap); + if (cur !== val) capabilityUpdates.push(updateCapability(this, cap, val)); + } + + const cur = this.getCapabilityValue('measure_battery'); + if (cur !== data.state_of_charge_pct) { + capabilityUpdates.push(updateCapability(this, 'measure_battery', data.state_of_charge_pct)); + capabilityUpdates.push(updateCapability(this, 'measure_soc', data.state_of_charge_pct)); + } + this._socLastUpdate = now; + } + + // --------------------------------------------------------- + // 3. Import/export debounced (max 1× per 10 sec) + // --------------------------------------------------------- + if (!this._energyLastUpdate || now - this._energyLastUpdate > 10000) { + const imp = this.getCapabilityValue('meter_power.import'); + const exp = this.getCapabilityValue('meter_power.export'); + + if (imp !== data.energy_import_kwh) { + capabilityUpdates.push(updateCapability(this, 'meter_power.import', data.energy_import_kwh)); + } + if (exp !== data.energy_export_kwh) { + capabilityUpdates.push(updateCapability(this, 'meter_power.export', data.energy_export_kwh)); + } + + this._energyLastUpdate = now; + } + + // --------------------------------------------------------- + // 4. Cycles debounced (max 1× per 60 sec) + // --------------------------------------------------------- + if (!this._cyclesLastUpdate || now - this._cyclesLastUpdate > 60000) { + const cur = this.getCapabilityValue('cycles'); + if (cur !== data.cycles) { + capabilityUpdates.push(updateCapability(this, 'cycles', data.cycles)); + } + this._cyclesLastUpdate = now; + } + + // --------------------------------------------------------- + // 5. Charging state (realtime, only on change) + // --------------------------------------------------------- + let chargingState; + if (data.power_w > 10) chargingState = 'charging'; + else if (data.power_w < -10) chargingState = 'discharging'; + else chargingState = 'idle'; + + if (chargingState !== this.previousChargingState) { + capabilityUpdates.push(updateCapability(this, 'battery_charging_state', chargingState)); + this.previousChargingState = chargingState; + + this.homey.flow + .getDeviceTriggerCard('battery_state_changed') + .trigger(this, { state: chargingState }) + .catch(this.error); + } + + // --------------------------------------------------------- + // 6. Time to full / empty + // --------------------------------------------------------- + if (typeof data.state_of_charge_pct === 'number' && typeof data.power_w === 'number') { + + const current_capacity = BATTERY_CAPACITY_WH * (data.state_of_charge_pct / 100); + + // Charging + if (data.power_w > 10) { + const remaining = BATTERY_CAPACITY_WH - current_capacity; + let ttf = Math.round((remaining / data.power_w) * 60); + + if (Math.abs(this._prevTimeToFull - ttf) >= 5) { + capabilityUpdates.push(updateCapability(this, 'time_to_full', ttf)); + this._prevTimeToFull = ttf; + } + + if (this._prevTimeToEmpty !== 0) { + capabilityUpdates.push(updateCapability(this, 'time_to_empty', 0)); + this._prevTimeToEmpty = 0; + } + } + + // Discharging + else if (data.power_w < -10) { + let tte = Math.round((current_capacity / Math.abs(data.power_w)) * 60); + + if (Math.abs(this._prevTimeToEmpty - tte) >= 5) { + capabilityUpdates.push(updateCapability(this, 'time_to_empty', tte)); + this._prevTimeToEmpty = tte; + } + + if (this._prevTimeToFull !== 0) { + capabilityUpdates.push(updateCapability(this, 'time_to_full', 0)); + this._prevTimeToFull = 0; + } + } + + // Idle + else { + if (this._prevTimeToFull !== 0) { + capabilityUpdates.push(updateCapability(this, 'time_to_full', 0)); + this._prevTimeToFull = 0; + } + if (this._prevTimeToEmpty !== 0) { + capabilityUpdates.push(updateCapability(this, 'time_to_empty', 0)); + this._prevTimeToEmpty = 0; + } + } + } + + // --------------------------------------------------------- + // 7. Estimate KWh (max 1× per 30 sec) + // --------------------------------------------------------- + if (!this._estimateLastUpdate || now - this._estimateLastUpdate > 30000) { + const inverterEfficiency = (data.energy_import_kwh > 0) + ? data.energy_export_kwh / data.energy_import_kwh + : 0.75; + + const estimate_kwh = estimateBatteryKWh( + data.state_of_charge_pct, + data.cycles, + inverterEfficiency + ); + + const rounded = Math.round(estimate_kwh * 100) / 100; + if (this.getCapabilityValue('estimate_kwh') !== rounded) { + capabilityUpdates.push(updateCapability(this, 'estimate_kwh', rounded)); + } + + this._estimateLastUpdate = now; + } + + // ✅ Execute all capability updates in parallel (non-blocking) + if (capabilityUpdates.length > 0) { + Promise.allSettled(capabilityUpdates).catch(err => { + this.error(`❌ Capability update batch error:`, err); + }); + } + + // --------------------------------------------------------- + // 8. Drift detection (1× per 5 min) + // --------------------------------------------------------- + if (!this._driftLastUpdate || now - this._driftLastUpdate > 300000) // 5 min + { + // 1. Eerste measurement initialiseren + if (Math.abs(data.power_w) > 10 && (this.previousSoC === undefined || this.previousTimestamp === undefined)) { + this.previousSoC = data.state_of_charge_pct; + this.previousTimestamp = now; + this._driftLastUpdate = now; + return; // drift kan nog niet berekend worden + } + + // 2. Nu pas drift berekenen + const driftResult = checkSoCDrift({ + previousSoC: this.previousSoC, + previousTimestamp: this.previousTimestamp, + currentSoC: data.state_of_charge_pct, + currentPowerW: data.power_w, + currentTimestamp: now, + batteryCapacityWh: this.getSetting('battery_capacity_wh') || 2470 + }); + + // 3. Update previousSoC/timestamp na drift-check + this.previousSoC = data.state_of_charge_pct; + this.previousTimestamp = now; + + + // 4. Drift events + if (driftResult.drift && !this.driftActive) { + this.driftActive = true; + this.log(`⚠️ SoC drift detected`); + this.homey.flow + .getDeviceTriggerCard('battery_soc_drift_detected') + .trigger(this) + .catch(this.error); + } + + if (!driftResult.drift && this.driftActive) { + this.driftActive = false; + this.log('✅ SoC drift resolved.'); + } + + this._driftLastUpdate = now; + } + + // --------------------------------------------------------- + // 9. Store latest values for group updater + // --------------------------------------------------------- + this._lastPower = data.power_w; + this._lastSoC = data.state_of_charge_pct; + this._lastCycles = data.cycles; + + } catch (err) { + this.error(`❌ _handleMeasurement crashed for device ${devId}:`, err?.stack || err); + } + } + + + /** + * Handle system events (wifi, cloud, etc.) + */ + _handleSystem(data) { + if (!this.getData() || !this.getData().id) { + this.log('⚠️ Ignoring system event: device no longer exists'); + return; + } + + const now = Date.now(); + + // --------------------------------------------------------- + // 1. WiFi RSSI (debounced: max 1× per 5 sec) + // --------------------------------------------------------- + if (typeof data.wifi_rssi_db === 'number') { + if (!this._wifiLastUpdate || now - this._wifiLastUpdate > 5000) { + const curRssi = this.getCapabilityValue('rssi'); + if (curRssi !== data.wifi_rssi_db) { + updateCapability(this, 'rssi', data.wifi_rssi_db); + } + + const quality = getWifiQuality(data.wifi_rssi_db); + const curQuality = this.getCapabilityValue('wifi_quality'); + if (curQuality !== quality) { + updateCapability(this, 'wifi_quality', quality); + } + + this._wifiLastUpdate = now; + } + } + + // --------------------------------------------------------- + // LED brightness → Homey dim (0–1) + // --------------------------------------------------------- + if (typeof data.status_led_brightness_pct === 'number') { + if (!this._dimLastUpdate || now - this._dimLastUpdate > 5000) { + const dimValue = data.status_led_brightness_pct / 100; // 0–100 → 0–1 + + if (this.getCapabilityValue('dim') !== dimValue) { + updateCapability(this, 'dim', dimValue); + } + if (this.getCapabilityValue('led_brightness_pct') !== data.status_led_brightness_pct) { + updateCapability(this, 'led_brightness_pct', data.status_led_brightness_pct); + } + + this._dimLastUpdate = now; + } + } + +} + + + + /** + * Ensure required capabilities exist. + */ + async _updateCapabilities() { + const caps = [ + 'identify', + 'dim', + 'led_brightness_pct', + 'meter_power.import', + 'meter_power.export', + 'measure_power', + 'measure_voltage', + 'measure_current', + 'measure_battery', + 'battery_charging_state', + 'cycles', + 'time_to_empty', + 'time_to_full', + 'rssi', + 'wifi_quality', + 'estimate_kwh' + ]; + + for (const cap of caps) { + if (!this.hasCapability(cap)) { + try { + await this.addCapability(cap); + this.log(`Created capability ${cap} for ${this.getName()}`); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + this.log(`Capability already exists: ${cap} — ignoring`); + } else { + this.error(`Failed to add capability ${cap}:`, err); + } + } + } + } + } + +async _fetchFallbackSoC() { + try { + const res = await fetchWithTimeout(`${this.url}/api/measurement`, { + headers: { + Authorization: `Bearer ${this.token}`, + 'X-Api-Version': '2' + }, + agent: this._httpsAgent + }); + + if (!res.ok) { + this.log(`⚠️ Fallback SoC fetch failed: ${res.status}`); + return null; + } + + const data = await res.json(); + if (typeof data.state_of_charge_pct === 'number') { + this.log(`✅ Fallback SoC available: ${data.state_of_charge_pct}%`); + return data.state_of_charge_pct; + } else { + this.log(`⚠️ Fallback SoC not present in API response`); + return null; + } + } catch (err) { + this.error('Fallback SoC fetch error:', err.message); + return null; + } +} + + + + /** + * Update battery group every 10 seconds + */ +async _updateBatteryGroup() { + if (this.__deleted) return; // Skip if device is deleted/uninit + + const batteryId = this.getData()?.id; + if (!batteryId) return; + + // 1. Skip during polling errors (backoff active) + if (this._pollErrorCount > 0) { + return; + } + + // 2. Skip if WS updated SoC recently (< 30s) + if (this._lastSoC && Date.now() - this._energyLastUpdate < 30000) { + return; + } + + // 3. Throttle: only run every 30 seconds + if (!this._lastGroupUpdate || Date.now() - this._lastGroupUpdate < 30000) { + return; + } + this._lastGroupUpdate = Date.now(); + + // 4. Fetch SoC via API (fallback) + let apiSoc = null; + try { + const res = await fetchWithTimeout(`${this.url}/api/measurement`, { + headers: { + Authorization: `Bearer ${this.token}`, + 'X-Api-Version': '2' + }, + agent: this._httpsAgent + }); + + if (res.ok) { + const data = await res.json(); + if (typeof data.state_of_charge_pct === 'number') { + apiSoc = data.state_of_charge_pct; + } + } + } catch (err) { + // Do NOT increment pollErrorCount here + // This is a soft fallback, not core polling + this.log('Battery group API fetch failed:', err.message); + return; + } + + // 5. Determine final SoC + let soc = (typeof this._lastSoC === 'number') ? this._lastSoC : null; + if (typeof apiSoc === 'number') soc = apiSoc; + if (typeof soc !== 'number' || soc < 0 || soc > 100) soc = 0; + + // 6. Update group + const info = { + id: batteryId, + capacity_kwh: 2.688, + cycles: this._lastCycles ?? 0, + power_w: this._lastPower ?? 0, + soc_pct: Math.round(soc), + updated_at: Date.now() + }; + + let group = this.homey.settings.get('pluginBatteryGroup') || {}; + const prev = JSON.stringify(group[batteryId]); + const next = JSON.stringify(info); + + if (prev !== next) { + group[batteryId] = info; + this.homey.settings.set('pluginBatteryGroup', group); + } +} + + + + + + + /** + * Fallback poll (pure fetch) + */ + async _fallbackPoll() { + if (this.__deleted) return; // Skip if device is deleted/uninit + if (this._pollErrorCount > 0) return; // geen fallback tijdens errors + try { + const measurementRes = await fetchWithTimeout(`${this.url}/api/measurement`, { + headers: { + Authorization: `Bearer ${this.token}`, + 'X-Api-Version': '2' + }, + agent: this._httpsAgent + }); + + if (measurementRes.ok) { + const measurement = await measurementRes.json(); + this._handleMeasurement(measurement); + } + + const systemRes = await fetchWithTimeout(`${this.url}/api/system`, { + headers: { + Authorization: `Bearer ${this.token}`, + 'X-Api-Version': '2' + }, + agent: this._httpsAgent + }); + + if (systemRes.ok) { + const system = await systemRes.json(); + this._handleSystem(system); + } + + this.log('📡 Fallback poll completed'); + + } catch (err) { + if (!this.__deleted) { + this.error('Fallback poll error:', err.message); + } + } + } + + /** + * Polling (pure fetch, no timeout wrapper) + */ + async onPoll() { + if (this.__deleted) return; // Skip if device is deleted/uninit + + const now = Date.now(); + + // Exponential backoff bij fouten + if (this._pollErrorCount > 0) { + const delayMs = Math.min(60000, this._pollErrorCount * 2000); + const sinceLast = now - this._lastPollAt; + + if (sinceLast < delayMs) { + return; // skip poll + } + } + + this._lastPollAt = now; + + try { + const measurementRes = await fetchWithTimeout(`${this.url}/api/measurement`, { + headers: { + Authorization: `Bearer ${this.token}`, + 'X-Api-Version': '2' + }, + agent: this._httpsAgent + }); + + if (measurementRes.ok) { + const measurement = await measurementRes.json(); + this._handleMeasurement(measurement); + this._pollErrorCount = 0; // reset bij succes + } else { + this._pollErrorCount++; + if (this._pollErrorCount % 5 === 1) { + this.log(`Polling measurement failed (${measurementRes.status})`); + } + return; + } + + const systemRes = await fetchWithTimeout(`${this.url}/api/system`, { + headers: { + Authorization: `Bearer ${this.token}`, + 'X-Api-Version': '2' + }, + agent: this._httpsAgent + }); + + if (systemRes.ok) { + const system = await systemRes.json(); + this._handleSystem(system); + } else { + this._pollErrorCount++; + if (this._pollErrorCount % 5 === 1) { + this.log(`Polling system failed (${systemRes.status})`); + } + } + + } catch (err) { + if (!this.__deleted) { + this._pollErrorCount++; + if (this._pollErrorCount % 5 === 1) { + this.error('Polling error:', err.message); + } + } + } +} + + + + +/** + * Register capability listeners. + */ +async _registerCapabilityListeners() { + + // IDENTIFY + this.registerCapabilityListener('identify', async () => { + await api.identify(this.url, this.token); + }); + + // LED BRIGHTNESS + this.registerCapabilityListener('dim', async (value) => { + // value is 0–1 → API wants 0–100 + const brightness = Math.round(value * 100); + + try { + await api.setLedBrightness(this.url, this.token, brightness); + this.log(`LED brightness set to ${brightness}%`); + } catch (err) { + this.error('LED brightness set error:', err.message); + throw new Error('Failed to set LED brightness'); + } + }); + +} + + + + /** + * Settings handler — switching between WS and polling. + */ + async onSettings({ oldSettings = {}, newSettings = {}, changedKeys = [] } = {}) { + this.log('Plugin Battery Settings updated', newSettings, changedKeys); + + const oldUsePolling = oldSettings.use_polling; + const newUsePolling = newSettings.use_polling; + + const oldInterval = oldSettings.polling_interval; + const newInterval = newSettings.polling_interval; + + // --------------------------------------------------------- + // 1. use_polling toggled → switch between WS and polling + // --------------------------------------------------------- + if (changedKeys.includes('use_polling')) { + if (newUsePolling) { + // SWITCH TO POLLING + this.log('⚙️ Switching to POLLING mode'); + + // Stop WebSocket + if (this.wsManager) { + this.log('🔌 Stopping WebSocket (polling enabled)'); + this.wsManager.stop(); + this.wsManager = null; + } + + // Start polling + const intervalSec = newInterval || newSettings.polling_interval || 10; + if (this.onPollInterval) clearInterval(this.onPollInterval); + this.onPollInterval = setInterval(this.onPoll.bind(this), intervalSec * 1000); + + this.log(`⏱️ Polling enabled, interval ${intervalSec}s`); + + } else { + // SWITCH TO WEBSOCKET + this.log('⚙️ Switching to WEBSOCKET mode'); + + // Stop polling + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + this.log('⏹️ Polling stopped'); + } + + // FULL REBUILD of WebSocketManager + if (this.wsManager) { + this.wsManager.stop(); + this.wsManager = null; + } + + this.wsManager = new WebSocketManager({ + device: this, + url: this.url, + token: this.token, + log: this._boundLog, + error: this._boundError, + setAvailable: this._boundSetAvailable, + getSetting: this._boundGetSetting, + handleMeasurement: this._boundHandleMeasurement, + handleSystem: this._boundHandleSystem, + measurementThrottleMs: 5000, // ✅ CPU FIX: 5s for battery (was 2s) + onJournalEvent: (type, deviceId, data) => { + if (type === 'snapshot') wsDebug.snapshot(deviceId, data); + else wsDebug.log(type, deviceId, typeof data === 'string' ? data : JSON.stringify(data)); + }, + }); + + this.log('🔌 Starting WebSocket (polling disabled)'); + this.wsManager.start(); + } + } + + // --------------------------------------------------------- + // 2. Polling interval changed → restart polling if active + // --------------------------------------------------------- + if (changedKeys.includes('polling_interval')) { + const intervalSec = newInterval || newSettings.polling_interval || 10; + + if (newSettings.use_polling) { + if (this.onPollInterval) clearInterval(this.onPollInterval); + this.onPollInterval = setInterval(this.onPoll.bind(this), intervalSec * 1000); + this.log(`⏱️ Polling interval updated to ${intervalSec}s`); + } else { + this.log('⏱️ Polling interval changed, but polling is disabled'); + } + } + + return true; + } +}; \ No newline at end of file diff --git a/drivers/plugin_battery/driver.compose.json b/drivers/plugin_battery/driver.compose.json new file mode 100644 index 00000000..a10130f6 --- /dev/null +++ b/drivers/plugin_battery/driver.compose.json @@ -0,0 +1,218 @@ +{ + "name": { + "en": "Plugin Battery" + }, + "images": { + "large": "drivers/plugin_battery/assets/images/large.png", + "small": "drivers/plugin_battery/assets/images/small.png" + }, + "class": "battery", + "discovery": "plugin_battery", + "platforms": [ + "local" + ], + "capabilities": [ + "identify", + "dim", + "led_brightness_pct", + "meter_power.import", + "meter_power.export", + "measure_battery", + "battery_charging_state", + "measure_soc", + "measure_power", + "measure_current", + "measure_voltage", + "measure_frequency", + "cycles", + "rssi", + "time_to_full", + "time_to_empty", + "estimate_kwh" + ], + "energy": { + "homeBattery": true, + "meterPowerImportedCapability": "meter_power.import", + "meterPowerExportedCapability": "meter_power.export" + }, + "capabilitiesOptions": { + "dim": { + "title": { + "en": "LED Brightness", + "nl": "LED Helderheid" + } + }, + "measure_power": { + "title": { + "en": "Current usage", + "nl": "Huidig vermogen" + } + }, + "meter_power.import": { + "decimals": 3, + "title": { + "en": "Total battery import", + "nl": "Totaal batterij import" + } + }, + "meter_power.export": { + "decimals": 3, + "title": { + "en": "Total Battery Export", + "nl": "Totaal Batterij export" + } + }, + "measure_voltage": { + "title": { + "en": "Current Voltage", + "nl": "Huidig Voltage" + } + }, + "measure_current": { + "title": { + "en": "Current Amp", + "nl": "Huidig Amp" + } + }, + "measure_battery": { + "title": { + "en": "Battery Level", + "nl": "Batterij niveau" + } + }, + "measure_frequency": { + "decimals": 3, + "title": { + "en": "Current Frequency", + "nl": "Huidige Frequentie" + } + }, + "battery_charging_state": { + "title": { + "en": "Battery State", + "nl": "Batterij status" + } + }, + "measure_soc": { + "type": "number", + "title": { + "en": "Battery Level", + "nl": "Batterijniveau" + }, + "desc": { + "en": "Current battery state of charge", + "nl": "Huidige batterij laadstatus" + }, + "units": { + "en": "%", + "nl": "%" + }, + "min": 0, + "max": 100, + "decimals": 0, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" + }, + "rssi": { + "type": "number", + "title": { + "en": "WiFi Signal", + "nl": "WiFi signaal" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/rssi.svg", + "units": { + "en": "dBm", + "nl": "dBm" + } + }, + "time_to_full": { + "type": "number", + "title": { + "en": "Time until full charge", + "nl": "Tijd tot vol geladen" + }, + "getable": true, + "setable": true, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/clock.svg", + "units": { + "en": "min", + "nl": "min" + } + }, + "time_to_empty": { + "type": "number", + "title": { + "en": "Time until discharged", + "nl": "Tijd tot ontladen" + }, + "getable": true, + "setable": true, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/clock.svg", + "units": { + "en": "min", + "nl": "min" + } + }, + "estimate_kwh": { + "type": "number", + "title": { + "en": "Est. kWh in battery", + "nl": "Geschatte kWh in batterij" + }, + "getable": true, + "setable": true, + "insights": true, + "uiComponent": "sensor", + "icon": "assets/tariff.svg", + "units": { + "en": "kWh", + "nl": "kWh" + } + } + }, + "widgets": [ + { + "id": "battery_soc_widget", + "name": { + "en": "Battery State of Charge", + "nl": "Batterij laadniveau" + }, + "description": { + "en": "Display battery percentage charge level", + "nl": "Toon batterij laadpercentage" + }, + "template": "generic", + "class": "battery", + "capabilities": [ + "measure_battery" + ] + } + ], + "pair": [ + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "authorize" + }, + "options": { "singular": true } + }, + { + "id": "authorize", + "navigation": + { + "prev": "list_devices" + } + } + ] +} diff --git a/drivers/plugin_battery/driver.flow.compose.json b/drivers/plugin_battery/driver.flow.compose.json new file mode 100644 index 00000000..0b655595 --- /dev/null +++ b/drivers/plugin_battery/driver.flow.compose.json @@ -0,0 +1,65 @@ +{ + "triggers": [ + { + "id": "battery_state_changed", + "title": { "en": "Battery state changed" }, + "args": [ + { + "name": "state", + "type": "dropdown", + "title": { "en": "Charging state" }, + "values": [ + { "id": "charging", "name": { "en": "Charging" } }, + { "id": "discharging", "name": { "en": "Discharging" } }, + { "id": "idle", "name": { "en": "Idle" } } + ] + } + ], + "titleFormatted": { + "en": "Battery state changed to [[state]]" + } + }, + { + "id": "battery_low_runtime", + "title": { "en": "Battery time to empty is low" }, + "args": [ + { + "name": "minutes", + "type": "number", + "title": { "en": "Minutes remaining" } + } + ], + "titleFormatted": { + "en": "Battery time to empty is [[minutes]] minutes" + } + }, + { + "id": "battery_full", + "title": { "en": "Battery is fully charged" } + }, + { + "id": "battery_soc_drift_detected", + "title": { + "en": "Battery SoC Drift Detected" + }, + "args": [], + "titleFormatted": { + "en": "Battery SoC drift detected", + "nl": "Batterij SoC-afwijking gedetecteerd" + } + + }, + { + "id": "net_frequency_out_of_range", + "title": { + "en": "Network frequency out of range", + "nl": "Netwerkfrequentie buiten bereik" + }, + "args": [], + "titleFormatted": { + "en": "Network frequency is out of range", + "nl": "Netwerkfrequentie is buiten bereik" + } + } + ] +} diff --git a/drivers/plugin_battery/driver.js b/drivers/plugin_battery/driver.js new file mode 100644 index 00000000..fa129193 --- /dev/null +++ b/drivers/plugin_battery/driver.js @@ -0,0 +1,6 @@ +'use strict'; + +const Homey = require('homey'); +const driver = require('../../includes/v2/Driver'); + +module.exports = driver; diff --git a/drivers/plugin_battery/driver.settings.compose.json b/drivers/plugin_battery/driver.settings.compose.json new file mode 100644 index 00000000..82edbc56 --- /dev/null +++ b/drivers/plugin_battery/driver.settings.compose.json @@ -0,0 +1,25 @@ +[ + { + "id": "polling_interval", + "type": "number", + "label": { "en": "Polling interval" }, + "value": 10, + "unit": { "en": "s" } + }, + { + "id": "url", + "type": "text", + "label": { "en": "url", + "nl": "url" + } + }, + { + "id": "use_polling", + "type": "checkbox", + "label": { + "en": "Use polling instead of WebSocket", + "nl": "Gebruik polling in plaats van WebSocket" + }, + "value": true + } +] \ No newline at end of file diff --git a/drivers/plugin_battery/pair/authorize.html b/drivers/plugin_battery/pair/authorize.html new file mode 100644 index 00000000..e9efb9d4 --- /dev/null +++ b/drivers/plugin_battery/pair/authorize.html @@ -0,0 +1,48 @@ +
+

+

+

+

+ +
+ + diff --git a/drivers/rainmeter/assets/icon.svg b/drivers/rainmeter/assets/icon.svg new file mode 100644 index 00000000..72f7fb5c --- /dev/null +++ b/drivers/rainmeter/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/rainmeter/assets/images/large.jpg b/drivers/rainmeter/assets/images/large.jpg new file mode 100644 index 00000000..38564b34 Binary files /dev/null and b/drivers/rainmeter/assets/images/large.jpg differ diff --git a/drivers/rainmeter/assets/images/meter.svg b/drivers/rainmeter/assets/images/meter.svg new file mode 100644 index 00000000..cf9411ac Binary files /dev/null and b/drivers/rainmeter/assets/images/meter.svg differ diff --git a/drivers/rainmeter/assets/images/small.jpg b/drivers/rainmeter/assets/images/small.jpg new file mode 100644 index 00000000..9574d024 Binary files /dev/null and b/drivers/rainmeter/assets/images/small.jpg differ diff --git a/drivers/rainmeter/device.js b/drivers/rainmeter/device.js new file mode 100644 index 00000000..37650eb3 --- /dev/null +++ b/drivers/rainmeter/device.js @@ -0,0 +1,191 @@ +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); +// const { ManagerDrivers } = require('homey'); +// const driver = ManagerDrivers.getDriver('rainmeter'); + +let refreshIntervalId; +const devices = {}; +// var temperature; + +class HomeWizardRainmeter extends Homey.Device { + + async onInit() { + + // await this.setUnavailable(`${this.getName()} ${this.homey.__('device.init')}`); + + // this.log(`HomeWizard Rainmeter ${this.getName()} has been inited`); + + const devices = this.homey.drivers.getDriver('rainmeter').getDevices(); + devices.forEach((device) => { + this.log(`add device: ${JSON.stringify(device.getName())}`); + + devices[device.getData().id] = device; + devices[device.getData().id].settings = device.getSettings(); + }); + + this.startPolling(); + + this._flowTriggerValueChanged = this.homey.flow.getDeviceTriggerCard('rainmeter_value_changed'); + + } + + flowTriggerValueChanged(device, tokens) { + this._flowTriggerValueChanged.trigger(device, tokens).catch(this.error); + } + + startPolling() { + + // Clear interval + if (this.refreshIntervalId) { + clearInterval(this.refreshIntervalId); + } + + // Start polling for thermometer + this.refreshIntervalId = setInterval(() => { + // this.log("--Start Rainmeter Polling-- "); + + this.getStatus(); + + }, 1000 * 20); + + } + + async getStatus() { + Promise.resolve() + .then(async () => { + + const me = this; + + if (this.getSetting('homewizard_id') !== undefined) { + const homewizard_id = this.getSetting('homewizard_id'); + const callback = await homewizard.getDeviceData(homewizard_id, 'rainmeters'); + + if (Object.keys(callback).length > 0) { + try { + // me.setAvailable(); + + // Check Battery + if (callback[0].lowBattery != undefined && callback[0].lowBattery != null) { + if (!this.hasCapability('alarm_battery')) { + await this.addCapability('alarm_battery').catch(me.error); + } + + const lowBattery_temp = callback[0].lowBattery; + const lowBattery_status = lowBattery_temp == 'yes'; + + if (this.getCapabilityValue('alarm_battery') != lowBattery_status) { + // if (debug) { this.log("New status - " + lowBattery_status); } + await this.setCapabilityValue('alarm_battery', lowBattery_status).catch(me.error); + } + } else if (this.hasCapability('alarm_battery')) { + await this.removeCapability('alarm_battery').catch(me.error); + } + + const rain_daytotal = callback[0].mm; // Total Rain in mm used JSON $rainmeters[0]['mm'] + const rain_last3h = callback[0]['3h']; // Last 3 hours rain in mm used JSON $rainmeters[0]['3h'] + + // Rain last 3 hours + if (typeof rain_last3h === 'number' && !isNaN(rain_last3h)) { + await me.setCapabilityValue('measure_rain.last3h', rain_last3h).catch(me.error); + } else { + this.log('Skipping measure_rain.last3h → invalid value:', rain_last3h); + } + + // Rain total day + if (typeof rain_daytotal === 'number' && !isNaN(rain_daytotal)) { + await me.setCapabilityValue('measure_rain.total', rain_daytotal).catch(me.error); + } else { + this.log('Skipping measure_rain.total → invalid value:', rain_daytotal); + } + + + // Trigger flows + if (rain_daytotal != me.getStoreValue('last_raintotal') && rain_daytotal != 0 && rain_daytotal != undefined && rain_daytotal != null) { + me.flowTriggerValueChanged(me, { rainmeter_changed: rain_daytotal }); + await me.setStoreValue('last_raintotal', rain_daytotal).catch(me.error); // Update last_raintotal + } + } catch (err) { + this.log('ERROR RainMeter getStatus ', err); + me.setUnavailable(); + } + } + } else { + this.log('Rainmeter settings not found, stop polling set unavailable'); + // this.setUnavailable(); + + // Only clear interval when the unavailable device is the only device on this driver + // This will prevent stopping the polling when a user has 1 device with old settings and 1 with new + // In the event that a user has multiple devices with old settings, this function will get called every 10 seconds, but that should not be a problem + } + }) + .then(() => { + this.setAvailable().catch(this.error); + }) + .catch((err) => { + this.error(err); + this.setUnavailable(err).catch(this.error); + }); + } + + /* + getStatus() { + + var me = this; + + if(this.getSetting('homewizard_id') !== undefined ) { + var homewizard_id = this.getSetting('homewizard_id'); + + homewizard.getDeviceData(homewizard_id, 'rainmeters', function(callback) { + if (Object.keys(callback).length > 0) { + try { + me.setAvailable(); + + var rain_daytotal = ( callback[0].mm ); // Total Rain in mm used JSON $rainmeters[0]['mm'] + var rain_last3h = ( callback[0]['3h'] ); // Last 3 hours rain in mm used JSON $rainmeters[0]['3h'] + // Rain last 3 hours + me.setCapabilityValue("measure_rain.last3h", rain_last3h ).catch(me.error); + // Rain total day + me.setCapabilityValue("measure_rain.total", rain_daytotal ).catch(me.error); + + // Trigger flows + if (rain_daytotal != me.getStoreValue("last_raintotal") && rain_daytotal != 0 && rain_daytotal != undefined && rain_daytotal != null) { + //this.log("Current Total Rainfall - "+ rain_daytotal); + me.flowTriggerValueChanged(me, {rainmeter_changed: rain_daytotal}) + me.setStoreValue("last_raintotal",rain_daytotal); // Update last_raintotal + } + + } catch (err) { + this.log('ERROR RainMeter getStatus ', err); + me.setUnavailable(); + } + } + }); + } else { + this.log('Rainmeter settings not found, stop polling set unavailable'); + this.setUnavailable(); + + // Only clear interval when the unavailable device is the only device on this driver + // This will prevent stopping the polling when a user has 1 device with old settings and 1 with new + // In the event that a user has multiple devices with old settings this function will get called every 10 seconds but that should not be a problem + + } + } + */ + + onDeleted() { + const deviceId = this.getData().id; + homewizard.removeDevice(deviceId); + + if (Object.keys(devices).length === 0) { + clearInterval(refreshIntervalId); + this.log('--Stopped Polling--'); + } + + this.log(`deleted: ${JSON.stringify(this)}`); + } + +} + +module.exports = HomeWizardRainmeter; diff --git a/drivers/rainmeter/driver.compose.json b/drivers/rainmeter/driver.compose.json new file mode 100644 index 00000000..8073fc57 --- /dev/null +++ b/drivers/rainmeter/driver.compose.json @@ -0,0 +1,49 @@ +{ + "name": { + "en": "Rainmeter", + "nl": "Regen meter" + }, + "images": { + "large": "drivers/rainmeter/assets/images/large.jpg", + "small": "drivers/rainmeter/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "platforms": [ "local"], + "capabilities": [ + "measure_rain.last3h", + "measure_rain.total" + ], + "capabilitiesOptions": { + "measure_rain.last3h": { + "title": { + "en": "Last 3 hours rain", + "nl": "Laatste 3 uur regen" + } + }, + "measure_rain.total": { + "title": { + "en": "Rainfall today", + "nl": "Regenval vandaag" + } + } + }, + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/rainmeter/driver.flow.compose.json b/drivers/rainmeter/driver.flow.compose.json new file mode 100644 index 00000000..57e4e3d6 --- /dev/null +++ b/drivers/rainmeter/driver.flow.compose.json @@ -0,0 +1,23 @@ +{ + "triggers": [ + { + "id": "rainmeter_value_changed", + "title": { + "en": "Rainmeter value changed", + "nl": "Regenmeter waarde veranderd" + }, + "args": [], + "tokens": [ + { + "name": "rainmeter_changed", + "type": "number", + "title": { + "en": "mm", + "nl": "mm" + }, + "example": 15 + } + ] + } + ] +} \ No newline at end of file diff --git a/drivers/rainmeter/driver.js b/drivers/rainmeter/driver.js new file mode 100644 index 00000000..3b7000fb --- /dev/null +++ b/drivers/rainmeter/driver.js @@ -0,0 +1,64 @@ +'use strict'; + +const Homey = require('homey'); + +const devices = {}; +const homewizard = require('../../includes/legacy/homewizard.js'); + +let homewizard_devices; + +class HomeWizardRainmeter extends Homey.Driver { + + onInit() { + // this.log('HomeWizard Rainmeter has been inited'); + } + + async onPair(socket) { + socket.setHandler('get_homewizards', async () => { + homewizard_devices = this.homey.drivers.getDriver('homewizard').getDevices(); + + return new Promise((resolve) => { + homewizard.getDevices((homewizard_devices) => { + const hw_devices = {}; + + Object.keys(homewizard_devices).forEach((key) => { + hw_devices[key] = { + id: key, + name: homewizard_devices[key].name, + settings: homewizard_devices[key].settings + }; + }); + + this.log('HomeWizard devices found:', Object.keys(hw_devices).length); + socket.emit('hw_devices', hw_devices); + resolve(hw_devices); + }); + }); + }); + + socket.setHandler('manual_add', async (device) => { + const hwId = device.settings.homewizard_id; + + if (!hwId || hwId === '') { + socket.emit('error', 'No HomeWizard selected'); + return; + } + + this.log(`Rainmeter added ${device.data.id} on HomeWizard ${hwId}`); + devices[device.data.id] = { + id: device.data.id, + name: device.name, + settings: device.settings, + }; + socket.emit('success', device); + return devices; + }); + + socket.setHandler('disconnect', () => { + this.log('User aborted pairing, or pairing is finished'); + }); + } + +} + +module.exports = HomeWizardRainmeter; diff --git a/drivers/rainmeter/pair/start.html b/drivers/rainmeter/pair/start.html new file mode 100644 index 00000000..af80f7f8 --- /dev/null +++ b/drivers/rainmeter/pair/start.html @@ -0,0 +1,85 @@ + + }); +} + + + + + + + +

+
+ +
+
+

+ + + diff --git a/drivers/thermometer/assets/icon.svg b/drivers/thermometer/assets/icon.svg new file mode 100644 index 00000000..6e3b7b88 --- /dev/null +++ b/drivers/thermometer/assets/icon.svg @@ -0,0 +1,7 @@ + + + temperature + + + + \ No newline at end of file diff --git a/drivers/thermometer/assets/images/large.jpg b/drivers/thermometer/assets/images/large.jpg new file mode 100644 index 00000000..4d233459 Binary files /dev/null and b/drivers/thermometer/assets/images/large.jpg differ diff --git a/drivers/thermometer/assets/images/small.jpg b/drivers/thermometer/assets/images/small.jpg new file mode 100644 index 00000000..f85588b3 Binary files /dev/null and b/drivers/thermometer/assets/images/small.jpg differ diff --git a/drivers/thermometer/device.js b/drivers/thermometer/device.js new file mode 100644 index 00000000..f1d0efec --- /dev/null +++ b/drivers/thermometer/device.js @@ -0,0 +1,198 @@ +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); + +// const { ManagerDrivers } = require('homey'); +// const driver = ManagerDrivers.getDriver('thermometer'); + +let refreshIntervalId; +// const devices = {}; +// const thermometers = {}; +const debug = false; + +class HomeWizardThermometer extends Homey.Device { + + async onInit() { + + // await this.setUnavailable(`${this.getName()} ${this.homey.__('device.init')}`); + + const devices = this.homey.drivers.getDriver('thermometer').getDevices(); + + devices.forEach((device) => { + if (debug) { this.log(`add device: ${JSON.stringify(device.getName())}`); } + devices[device.getData().id] = device; + devices[device.getData().id].settings = device.getSettings(); + }); + + if (Object.keys(devices).length > 0) { + this.startPolling(devices); + } + } + + startPolling(devices) { + + // Clear interval + if (this.refreshIntervalId) { + clearInterval(this.refreshIntervalId); + } + + // Start polling for thermometer + this.refreshIntervalId = setInterval(() => { + if (debug) { this.log('--Start Thermometer Polling-- '); } + + this.getStatus(devices); + + }, 1000 * 20); + + } + + async getStatus(devices) { + try { + const promises = devices.map(async (device) => { // parallel processing using Promise.all + if (device.settings.homewizard_id !== undefined) { + const { homewizard_id } = device.settings; + const { thermometer_id } = device.settings; + + const result = await homewizard.getDeviceData(homewizard_id, 'thermometers'); + + if (Object.keys(result).length > 0) { + for (const index2 in result) { + if ( + result[index2].id == thermometer_id + && result[index2].te != undefined + && result[index2].hu != undefined + && typeof result[index2].te != 'undefined' + && typeof result[index2].hu != 'undefined' + ) { + let te = (result[index2].te.toFixed(1) * 2) / 2; + let hu = (result[index2].hu.toFixed(1) * 2) / 2; + + // First adjust retrieved temperature with offset + const offset_temp = device.getSetting('offset_temperature'); + te += offset_temp; + + // Check current temperature + if (device.getCapabilityValue('measure_temperature') != te) { + if (debug) { this.log(`New TE - ${te}`); } + await device.setCapabilityValue('measure_temperature', te).catch(this.error); + await device.setStoreValue('lastTempUpdate', Date.now()).catch(this.error); + // Reset trigger state + await device.setStoreValue('unchangedTriggered', false).catch(this.error); + } + + // Check trigger condition + const last = await device.getStoreValue('lastTempUpdate'); + if (last) { + const diffHours = (Date.now() - last) / 1000 / 3600; + + const triggerCard = this.homey.flow.getDeviceTriggerCard('temp_not_changed_trigger'); + + // Haal ingestelde uren op uit device settings of store + const hours = device.getSetting('temp_not_changed_hours') + ?? await device.getStoreValue('temp_not_changed_hours'); + + if (hours && diffHours >= hours) { + const alreadyTriggered = await device.getStoreValue('unchangedTriggered'); + + if (!alreadyTriggered) { + await triggerCard.trigger(device, { hours }).catch(this.error); + await device.setStoreValue('unchangedTriggered', true).catch(this.error); + } + } + } + + + // First adjust retrieved humidity with offset + const offset_hu = device.getSetting('offset_humidity'); + hu += offset_hu; + + // Check current humidity + if (device.getCapabilityValue('measure_humidity') != hu) { + if (debug) { this.log(`New HU - ${hu}`); } + await device.setCapabilityValue('measure_humidity', hu).catch(this.error); + } + + if (result[index2].lowBattery != undefined && result[index2].lowBattery != null) { + if (!device.hasCapability('alarm_battery')) { + await device.addCapability('alarm_battery').catch(this.error); + } + + const lowBattery_temp = result[index2].lowBattery; + const lowBattery_status = lowBattery_temp == 'yes'; + + if (device.getCapabilityValue('alarm_battery') != lowBattery_status) { + if (debug) { this.log(`New status - ${lowBattery_status}`); } + await device.setCapabilityValue('alarm_battery', lowBattery_status).catch(this.error); + } + } else if (device.hasCapability('alarm_battery')) { + await device.removeCapability('alarm_battery').catch(this.error); + } + } + } + } + } + }); + + await Promise.all(promises); + + await this.setAvailable().catch(this.error); + } catch (err) { + this.error(err); + await this.setUnavailable(err).catch(this.error); + } + } + + onDeleted() { + const deviceId = this.getData().id; + homewizard.removeDevice(deviceId); + + if (Object.keys(devices).length === 0) { + clearInterval(refreshIntervalId); + if (debug) { this.log('--Stopped Polling--'); } + } + + this.log(`deleted: ${JSON.stringify(this)}`); + } + + // Catch offset updates + onSettings(oldSettings, newSettings, changedKeys) { + this.log('Settings updated'); + + // Defensieve check: Homey geeft soms geen array terug + if (!Array.isArray(changedKeys)) { + this.error('changedKeys is not iterable:', changedKeys); + return; + } + + for (const key of changedKeys) { + if (key.startsWith('offset_')) { + const cap = `measure_${key.slice(7)}`; + const value = this.getCapabilityValue(cap); + const delta = newSettings[key] - oldSettings[key]; + + this.log('Updating value of', cap, 'from', value, 'to', value + delta); + + this.setCapabilityValue(cap, value + delta) + .catch((err) => this.error(err)); + } + } +} + + + updateValue(cap, value) { + // add offset if defined + this.log('Updating value of', this.id, 'with capability', cap, 'to', value); + const cap_offset = cap.replace('measure', 'offset'); + const offset = this.getSetting(cap_offset); + this.log(cap_offset, offset); + if (offset != null) { + value += offset; + } + this.setCapabilityValue(cap, value) + .catch((err) => this.error(err)); + } + +} + +module.exports = HomeWizardThermometer; diff --git a/drivers/thermometer/driver.compose.json b/drivers/thermometer/driver.compose.json new file mode 100644 index 00000000..85b2b677 --- /dev/null +++ b/drivers/thermometer/driver.compose.json @@ -0,0 +1,69 @@ +{ + "name": { + "en": "Thermometer", + "nl": "Thermometer" + }, + "images": { + "large": "drivers/thermometer/assets/images/large.jpg", + "small": "drivers/thermometer/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "capabilities": [ + "measure_temperature", + "measure_humidity" + ], + "energy": { + "batteries": [ + "AA", + "AA" + ] + }, + "settings": [ + { + "type": "group", + "label": { + "en": "Sensor offset", + "nl": "Sensor compensatie" + }, + "children": [ + { + "id": "offset_temperature", + "type": "number", + "label": { + "en": "Temperature", + "nl": "Temperatuur" + }, + "value": 0 + }, + { + "id": "offset_humidity", + "type": "number", + "label": { + "en": "Humidity", + "nl": "Vochtigheid" + }, + "value": 0 + } + ] + } + ], + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/thermometer/driver.flow.compose.json b/drivers/thermometer/driver.flow.compose.json new file mode 100644 index 00000000..c383d266 --- /dev/null +++ b/drivers/thermometer/driver.flow.compose.json @@ -0,0 +1,52 @@ +{ + "conditions": [ + { + "id": "temp_not_changed_hours", + "title": { + "en": "Temperature not changed for", + "nl": "Temperatuur niet veranderd sinds" + }, + "titleFormatted": { + "en": "Temperature not changed for [[hours]] hours", + "nl": "Temperatuur niet veranderd sinds [[hours]] uren" + }, + "hint": { + "en": "Checks if the temperature has not changed for the given number of hours.", + "nl": "Controleert of de temperatuur niet veranderd is gedurende het opgegeven aantal uren." + }, + "args": [ + { + "name": "hours", + "type": "number", + "title": { + "en": "Hours", + "nl": "Uren" + } + } + ] + } + ], + "triggers": [ + { + "id": "temp_not_changed_trigger", + "title": { + "en": "Temperature unchanged for X hours", + "nl": "Temperatuur niet veranderd sinds X uur" + }, + "titleFormatted": { + "en": "Temperature unchanged for [[hours]] hours", + "nl": "Temperatuur niet veranderd sinds [[hours]] uren" + }, + "args": [ + { + "name": "hours", + "type": "number", + "title": { + "en": "Hours", + "nl": "Uren" + } + } + ] + } + ] +} diff --git a/drivers/thermometer/driver.js b/drivers/thermometer/driver.js new file mode 100755 index 00000000..3559bd44 --- /dev/null +++ b/drivers/thermometer/driver.js @@ -0,0 +1,104 @@ +'use strict'; + +const Homey = require('homey'); + +const devices = {}; +const homewizard = require('../../includes/legacy/homewizard.js'); + + +class HomeWizardThermometer extends Homey.Driver { + + onInit() { + // this.log('HomeWizard Thermometer has been inited'); + } + + async onPair(socket) { + let homewizard_devices; + // socket.on('get_homewizards', function () { + await socket.setHandler('get_homewizards', async () => { + const fetchedDevices = homewizard.self.devices || {}; + const thermometerList = []; + + const hwIds = Object.keys(fetchedDevices); + + // We wachten op ALLE /get-sensors calls + await Promise.all( + hwIds.map(hwId => { + return new Promise(resolve => { + homewizard.callnew(hwId, '/get-sensors', (err, response) => { + if (err || !response) return resolve(); + + const thermometers = response.thermometers || []; + thermometers.forEach(t => { + thermometerList.push({ + id: t.id, + name: t.name, + homewizard_id: hwId + }); + }); + + resolve(); + }); + }); + }) + ); + + this.log('[PAIRING] Emitting thermometer list:', thermometerList); + socket.emit('thermometer_list', thermometerList); + }); + + + + await socket.setHandler('manual_add', async (device) => { + const hwId = device.settings.homewizard_id; + const sensorId = device.settings.thermometer_id; + + if (!hwId || sensorId === undefined) { + socket.emit('error', this.homey.__("settings.selection_error")); + return; + } + + // Zoek thermometer opnieuw via /get-sensors + homewizard.callnew(hwId, '/get-sensors', (err, response) => { + if (err || !response) { + socket.emit('error', this.homey.__("settings.fetch_error")); + return; + } + + const selected = (response.thermometers || []).find(t => t.id == sensorId); + if (!selected) { + socket.emit('error', this.homey.__("settings.thermometer_notfound_error")); + return; + } + + // Naam opslaan + device.settings.thermometer_name = selected.name; + + devices[device.data.id] = { + id: device.data.id, + name: device.name, + settings: device.settings, + }; + + socket.emit('success', device); + }); +}); + + + + await socket.setHandler('disconnect', () => { + this.log('User aborted pairing, or pairing is finished'); + }); + } + + onPairListDevices(data, callback) { + const devices = [ + + ]; + + callback(null, devices); + } + +} + +module.exports = HomeWizardThermometer; diff --git a/drivers/thermometer/flow/conditions/temp_not_changed_hours.js b/drivers/thermometer/flow/conditions/temp_not_changed_hours.js new file mode 100644 index 00000000..79b02d4f --- /dev/null +++ b/drivers/thermometer/flow/conditions/temp_not_changed_hours.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + async run({ device, hours }) { + const last = await device.getStoreValue('lastTempUpdate'); + if (!last) return false; + + const diffHours = (Date.now() - last) / 1000 / 3600; + return diffHours >= hours; + } +}; diff --git a/drivers/thermometer/flow/triggers/temp_not_changed_trigger.js b/drivers/thermometer/flow/triggers/temp_not_changed_trigger.js new file mode 100644 index 00000000..6e1ad50b --- /dev/null +++ b/drivers/thermometer/flow/triggers/temp_not_changed_trigger.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + async runListener(args, state) { + // Homey uses runListener for triggers + // We match on hours so the trigger only fires if the user sets that number + return args.hours === state.hours; + } +}; diff --git a/drivers/thermometer/pair/start.html b/drivers/thermometer/pair/start.html new file mode 100755 index 00000000..9569f5e0 --- /dev/null +++ b/drivers/thermometer/pair/start.html @@ -0,0 +1,90 @@ + + + + + + + + +

+
+ + +
+
+

+ + + diff --git a/drivers/watermeter/assets/icon.svg b/drivers/watermeter/assets/icon.svg new file mode 100644 index 00000000..e0f8ed06 --- /dev/null +++ b/drivers/watermeter/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/watermeter/assets/images/large.png b/drivers/watermeter/assets/images/large.png new file mode 100644 index 00000000..66a529d6 Binary files /dev/null and b/drivers/watermeter/assets/images/large.png differ diff --git a/drivers/watermeter/assets/images/small.png b/drivers/watermeter/assets/images/small.png new file mode 100644 index 00000000..2646976e Binary files /dev/null and b/drivers/watermeter/assets/images/small.png differ diff --git a/drivers/watermeter/device.js b/drivers/watermeter/device.js new file mode 100644 index 00000000..dd85bd25 --- /dev/null +++ b/drivers/watermeter/device.js @@ -0,0 +1,291 @@ +'use strict'; + +const Homey = require('homey'); +const fetchWithTimeout = require('../../includes/utils/fetchWithTimeout'); +const http = require('http'); + + + +/** + * Safe capability updater + */ +async function updateCapability(device, capability, value) { + const current = device.getCapabilityValue(capability); + + if (value === undefined || value === null) return; + + if (!device.hasCapability(capability)) { + try { + await device.addCapability(capability); + } catch (err) { + if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) { + device.log(`Capability already exists: ${capability} — ignoring`); + } else { + device.error(err); + } + } + } + + if (current !== value) { + await device.setCapabilityValue(capability, value).catch(device.error); + } +} + +module.exports = class HomeWizardEnergyWatermeterDevice extends Homey.Device { + + async onInit() { + + this._debugLogs = []; + this.__deleted = false; + + this.agent = new http.Agent({ + keepAlive: true, + keepAliveMsecs: 10000, + maxSockets: 2, + maxFreeSockets: 2, + }); + + + const settings = this.getSettings(); + + if (settings.offset_polling == null) { + await this.setSettings({ offset_polling: 10 }); + } + + if (settings.offset_water == null) { + await this.setSettings({ offset_water: 0 }); + } + + const interval = Math.max(settings.offset_polling, 2); + const offset = Math.floor(Math.random() * interval * 1000); + + if (this.onPollInterval) clearInterval(this.onPollInterval); + + this._startupPollTimeout = setTimeout(() => { + if (this.__deleted) return; + this._startupPollTimeout = null; + this.onPoll().catch(this.error); + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + }, offset); + + const requiredCaps = [ + 'measure_water', + 'meter_water', + 'meter_water.daily', + 'identify', + 'rssi' + ]; + + for (const cap of requiredCaps) { + if (!this.hasCapability(cap)) { + await this.addCapability(cap).catch(this.error); + } + } + + this.registerCapabilityListener('identify', async () => { + await this.onIdentify(); + }); + } + + onUninit() { + this.__deleted = true; + + if (this._startupPollTimeout) { + clearTimeout(this._startupPollTimeout); + this._startupPollTimeout = null; + } + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + if (this._debugFlushTimeout) { + clearTimeout(this._debugFlushTimeout); + this._debugFlushTimeout = null; + } + if (this.agent) { + this.agent.destroy(); + this.agent = null; + } + } + + onDeleted() { + this.onUninit(); + } + + /** + * Discovery — simpel gehouden + */ + onDiscoveryAvailable(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this.setAvailable(); + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this._debugLog(`🔄 Discovery address changed: ${this.url}`); + this.setAvailable(); + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `http://${discoveryResult.address}:${discoveryResult.port}${discoveryResult.txt.path}`; + this.setAvailable(); + } + + /** + * Per-device debug logger (batched writes) + */ +_debugLog(msg) { + try { + if (!this._debugBuffer) this._debugBuffer = []; + const ts = new Date().toLocaleString('nl-NL', { hour12: false, timeZone: 'Europe/Amsterdam' }); + const driverName = this.driver.id; + const deviceName = this.getName(); + const safeMsg = typeof msg === 'string' ? msg : (msg instanceof Error ? msg.message : JSON.stringify(msg)); + const line = `${ts} [${driverName}] [${deviceName}] ${safeMsg}`; + this._debugBuffer.push(line); + if (this._debugBuffer.length > 20) this._debugBuffer.shift(); + if (!this._debugFlushTimeout) { + this._debugFlushTimeout = setTimeout(() => { + this._flushDebugLogs(); + this._debugFlushTimeout = null; + }, 5000); + } + } catch (err) { + this.error('Failed to write debug logs:', err.message || err); + } +} +_flushDebugLogs() { + if (!this._debugBuffer || this._debugBuffer.length === 0) return; + try { + const logs = this.homey.settings.get('debug_logs') || []; + logs.push(...this._debugBuffer); + if (logs.length > 500) logs.splice(0, logs.length - 500); + this.homey.settings.set('debug_logs', logs); + this._debugBuffer = []; + } catch (err) { + this.error('Failed to flush debug logs:', err.message || err); + } +} + + /** + * PUT /identify — zonder timeout wrapper + */ + async onIdentify() { + if (!this.url) return; + + try { + const res = await fetchWithTimeout(`${this.url}/identify`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' } + }); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + } catch (err) { + this._debugLog(`Identify failed: ${err.code || ''} ${err.message || err}`); + this.error('Identify failed:', err); + throw new Error('Network error during identify'); + } + } + + /** + * GET /data + */ + async onPoll() { + if (this.__deleted) return; + + const settings = this.getSettings(); + + if (!this.url) { + if (settings.url) { + this.url = settings.url; + } else { + await this.setUnavailable('Missing URL'); + return; + } + } + + try { + + const res = await fetchWithTimeout(`${this.url}/data`, { + agent: this.agent, + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); + + const text = await res.text(); + const data = JSON.parse(text); + + // --- Capability updates --- + const offsetWater = + data.total_liter_offset_m3 === 0 || data.total_liter_offset_m3 === '0' + ? settings.offset_water + : data.total_liter_offset_m3; + + const totalM3 = data.total_liter_m3 + offsetWater; + + await updateCapability(this, 'measure_water', data.active_liter_lpm); + await updateCapability(this, 'meter_water', totalM3); + await updateCapability(this, 'rssi', data.wifi_strength); + + // --- Daily baseline --- + const dailyStart = await this._ensureDailyBaseline(totalM3); + const dailyUsage = Math.max(0, totalM3 - dailyStart); + + await updateCapability(this, 'meter_water.daily', dailyUsage); + + await this.setAvailable(); + + } catch (err) { + this._debugLog(`❌ ${err.code || ''} ${err.message || err}`); + this.error('Polling failed:', err); + this.setUnavailable(err.message || 'Polling error').catch(this.error); + } + } + + /** + * Daily baseline logic — deletion‑safe + */ + async _ensureDailyBaseline(totalM3) { + const today = new Date().toISOString().slice(0, 10); + + const storedDate = await this.getStoreValue('dailyStartDate'); + const storedValue = await this.getStoreValue('dailyStartM3'); + + if (storedDate !== today || storedValue == null) { + await this.setStoreValue('dailyStartDate', today); + await this.setStoreValue('dailyStartM3', totalM3); + return totalM3; + } + + return storedValue; + } + + onSettings(event) { + const { newSettings, changedKeys } = event; + + for (const key of changedKeys) { + + if (key === 'offset_polling') { + const interval = newSettings.offset_polling; + + if (typeof interval === 'number' && interval > 0) { + if (this.onPollInterval) clearInterval(this.onPollInterval); + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + } + } + + if (key === 'cloud') { + if (newSettings.cloud == 1) this.setCloudOn?.(); + else this.setCloudOff?.(); + } + } + } +}; diff --git a/drivers/watermeter/driver.compose.json b/drivers/watermeter/driver.compose.json new file mode 100644 index 00000000..ea9857fa --- /dev/null +++ b/drivers/watermeter/driver.compose.json @@ -0,0 +1,111 @@ +{ + "name": { + "en": "Watermeter" + }, + "images": { + "large": "drivers/watermeter/assets/images/large.png", + "small": "drivers/watermeter/assets/images/small.png" + }, + "class": "sensor", + "discovery": "watermeter", + "platforms": [ + "local" + ], + "capabilities": [ + "identify", + "measure_water", + "meter_water", + "rssi" + ], + "energy": { + "cumulative": true + }, + "capabilitiesOptions": { + "measure_water": { + "type": "number", + "title": { + "en": "Water L/min", + "nl": "Water L/min" + }, + "units": { + "en": "L/min" + }, + "desc": { + "en": "Water flow in Liters per minute (L/min)", + "nl": "Waterdoorstroming in Liters per minuut (L/min)" + }, + "chartType": "stepLine", + "decimals": 1, + "getable": true, + "setable": false + }, + "meter_water": { + "decimals": 3, + "title": { + "en": "Total usage", + "nl": "Totaal verbruik" + } + }, + "meter_water.daily": { + "decimals": 3, + "title": { "en": "Daily water usage", "nl": "Dagverbruik water" }, + "units": { "en": "m³", "nl": "m³" } + } + }, + "settings": [ + { + "type": "group", + "label": { + "en": "Watermeter offset", + "nl": "Watermeter compensatie" + }, + "children": [ + { + "id": "offset_water", + "type": "number", + "label": { + "en": "Offset watermeter m3", + "nl": "compensatie watermeter m3" + }, + "value": 0 + }, + { + "id": "offset_polling", + "type": "number", + "label": { + "en": "Polling in seconds", + "nl": "Interval in seconden" + }, + "value": 10 + }, + { + "id": "cloud", + "type": "number", + "label": { "en": "Cloud connection 1=on 0=off", + "nl": "Cloud verbinding 1=actief 0=uit" + }, + "value": 1 + } + ] + } + ], + "pair": [ + { + "id": "start", + "navigation": { + "next": "list_devices" + } + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ] +} diff --git a/drivers/watermeter/driver.js b/drivers/watermeter/driver.js new file mode 100644 index 00000000..a1a0dc29 --- /dev/null +++ b/drivers/watermeter/driver.js @@ -0,0 +1,5 @@ +'use strict'; + +const driver = require('../../includes/v1/driver.js'); + +module.exports = driver; diff --git a/drivers/watermeter/pair/start.html b/drivers/watermeter/pair/start.html new file mode 100644 index 00000000..aee174a2 --- /dev/null +++ b/drivers/watermeter/pair/start.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/drivers/wattcher/assets/icon.svg b/drivers/wattcher/assets/icon.svg new file mode 100644 index 00000000..4db0faef --- /dev/null +++ b/drivers/wattcher/assets/icon.svg @@ -0,0 +1,45 @@ + +image/svg+xml diff --git a/drivers/wattcher/device.js b/drivers/wattcher/device.js new file mode 100644 index 00000000..d7bd31fc --- /dev/null +++ b/drivers/wattcher/device.js @@ -0,0 +1,109 @@ +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); + +// const { ManagerDrivers } = require('homey'); +// const driver = ManagerDrivers.getDriver('wattcher'); + +let refreshIntervalId; +const devices = {}; + + +class HomeWizardWattcher extends Homey.Device { + + async onInit() { + + // await this.setUnavailable(`${this.getName()} ${this.homey.__('device.init')}`); + + this.log(`HomeWizard Wattcher ${this.getName()} has been inited`); + + const devices = this.homey.drivers.getDriver('wattcher').getDevices(); + devices.forEach((device) => { + this.log(`add device: ${JSON.stringify(device.getName())}`); + + devices[device.getData().id] = device; + devices[device.getData().id].settings = device.getSettings(); + }); + + this.startPolling(); + } + + startPolling() { + + // Clear interval + if (this.refreshIntervalId) { + clearInterval(this.refreshIntervalId); + } + + // Start polling for thermometer + this.refreshIntervalId = setInterval(() => { + this.log('--Start Wattcher Polling-- '); + + this.getStatus(); + + }, 1000 * 20); + + } + + async getStatus() { + Promise.resolve() + .then(async () => { + + if (this.getSetting('homewizard_id') !== undefined) { + const homewizard_id = this.getSetting('homewizard_id'); + const callback = await homewizard.getDeviceData(homewizard_id, 'energymeters'); + + if (Object.keys(callback).length > 0) { + try { + // this.log('Start capturing data'); + + const energy_current_cons = callback[0].po; // WATTS Energy used JSON $energymeters[0]['po'] + const energy_daytotal_cons = callback[0].dayTotal; // KWH Energy used JSON $energymeters[0]['dayTotal'] + + // Wattcher elec current + this.setCapabilityValue('measure_power', energy_current_cons).catch(this.error); + // Wattcher elec total day + this.setCapabilityValue('meter_power', energy_daytotal_cons).catch(this.error); + + // this.log('End capturing data'); + // this.log('Wattcher usage- ' + energy_current_cons); + // this.log('Wattcher Daytotal- ' + energy_daytotal_cons); + } catch (err) { + this.log('ERROR Wattcher getStatus ', err); + this.setUnavailable(err); + } + } + } else { + this.log('Wattcher settings not found, stop polling set unavailable'); + this.setUnavailable(); + clearInterval(this.refreshIntervalId); + + // Only clear interval when the unavailable device is the only device on this driver + // This will prevent stopping the polling when a user has 1 device with old settings and 1 with new + // In the event that a user has multiple devices with old settings, this function will get called every 10 seconds, but that should not be a problem + } + }) + .then(() => { + this.setAvailable().catch(this.error); + }) + .catch((err) => { + this.error(err); + this.setUnavailable(err).catch(this.error); + clearInterval(this.refreshIntervalId); + }); + } + + onDeleted() { + + if (Object.keys(devices).length === 0) { + clearInterval(this.refreshIntervalId); + this.log('--Stopped Polling--'); + } + + this.log(`deleted: ${JSON.stringify(this)}`); + } + +} + +module.exports = HomeWizardWattcher; diff --git a/drivers/wattcher/driver.compose.json b/drivers/wattcher/driver.compose.json new file mode 100644 index 00000000..b8ee2b36 --- /dev/null +++ b/drivers/wattcher/driver.compose.json @@ -0,0 +1,49 @@ +{ + "name": { + "en": "Wattcher", + "nl": "Wattcher" + }, + "images": { + "large": "drivers/wattcher/assets/images/large.jpg", + "small": "drivers/wattcher/assets/images/small.jpg" + }, + "platforms": [ + "local" + ], + "class": "sensor", + "platforms": [ "local"], + "capabilities": [ + "measure_power", + "meter_power" + ], + "capabilitiesOptions": { + "meter_power": { + "title": { + "en": "Day usage", + "nl": "Dag totaal" + } + }, + "measure_power": { + "title": { + "en": "Power current", + "nl": "Huidig vermogen" + } + } + }, + "pair": [ + { + "id": "start" + }, + { + "id": "list_my_devices", + "template": "list_devices", + "navigation": { + "next": "add_my_devices" + } + }, + { + "id": "add_my_devices", + "template": "add_devices" + } + ] +} \ No newline at end of file diff --git a/drivers/wattcher/driver.js b/drivers/wattcher/driver.js index 0ac5aa24..9e635d0d 100755 --- a/drivers/wattcher/driver.js +++ b/drivers/wattcher/driver.js @@ -1,143 +1,67 @@ -var devices = []; -var homewizard = require('./../../includes/homewizard.js'); -var refreshIntervalId = 0; +'use strict'; -// SETTINGS -module.exports.settings = function( device_data, newSettingsObj, oldSettingsObj, changedKeysArr, callback ) { - Homey.log ('Changed settings: ' + JSON.stringify(device_data) + ' / ' + JSON.stringify(newSettingsObj) + ' / old = ' + JSON.stringify(oldSettingsObj)); - try { - changedKeysArr.forEach(function (key) { - devices[device_data.id].settings[key] = newSettingsObj[key]; - }); - callback(null, true); - } catch (error) { - callback(error); - } -}; +const Homey = require('homey'); -module.exports.pair = function( socket ) { - socket.on('get_homewizards', function () { - homewizard.getDevices(function(homewizard_devices) { - Homey.log(homewizard_devices); - var hw_devices = {}; - Object.keys(homewizard_devices).forEach(function(key) { - hw_devices[key] = homewizard_devices[key]; - }); - - socket.emit('hw_devices', hw_devices); - }); - }); - - socket.on('manual_add', function (device, callback) { - if (device.settings.homewizard_id.indexOf('HW_') === -1 && device.settings.homewizard_id.indexOf('HW') === 0) { - //true - Homey.log('Wattcher added ' + device.data.id); - devices[device.data.id] = { - id: device.data.id, - name: device.name, - settings: device.settings, - }; - callback( null, devices ); - socket.emit("success", device); - startPolling(); - } else { - socket.emit("error", "No valid HomeWizard found, re-pair if problem persists"); - } - }); - - socket.on('disconnect', function(){ - console.log("User aborted pairing, or pairing is finished"); - }); -} +// const request = require('request'); -module.exports.init = function(devices_data, callback) { - devices_data.forEach(function initdevice(device) { - Homey.log('add device: ' + JSON.stringify(device)); - devices[device.id] = device; - module.exports.getSettings(device, function(err, settings){ - devices[device.id].settings = settings; - }); - }); - if (Object.keys(devices).length > 0) { - startPolling(); - } - Homey.log('Wattcher driver init done'); +// const { ManagerDrivers } = require('homey'); +// const driver = ManagerDrivers.getDriver('homewizard'); +const devices = {}; +const homewizard = require('../../includes/legacy/homewizard.js'); - callback (null, true); -}; +let homewizard_devices; -module.exports.deleted = function( device_data ) { - clearInterval(refreshIntervalId); - console.log("--Stopped Polling Wattcher--"); - devices = []; - Homey.log('deleted: ' + JSON.stringify(device_data)); -}; +class HomeWizardWattcher extends Homey.Driver { -module.exports.capabilities = { - measure_power: { - get: function (device_data, callback) { - var device = devices[device_data.id]; + onInit() { + // this.log('HomeWizard Wattcher has been inited'); + } - if (device === undefined) { - callback(null, 0); - } else { - callback(null, device.power); - } - } - }, - meter_power: { - get: function (device_data, callback) { - var device = devices[device_data.id]; + async onPair(socket) { + socket.setHandler('get_homewizards', async () => { + homewizard_devices = this.homey.drivers.getDriver('homewizard').getDevices(); - if (device === undefined) { - callback(null, 0); - } else { - callback(null, device.energy); - } - } - } - -}; + return new Promise((resolve) => { + homewizard.getDevices((homewizard_devices) => { + const hw_devices = {}; -// Start polling -function startPolling() { - refreshIntervalId = setInterval(function () { - console.log("--Start Wattcher Polling-- "); - Object.keys(devices).forEach(function (device_id) { - getStatus(device_id); - }); - }, 1000 * 10); -} + Object.keys(homewizard_devices).forEach((key) => { + hw_devices[key] = { + id: key, + name: homewizard_devices[key].name, + settings: homewizard_devices[key].settings + }; + }); -function getStatus(device_id) { - if(devices[device_id].settings.homewizard_id !== undefined ) { - var homewizard_id = devices[device_id].settings.homewizard_id; - homewizard.getDeviceData(homewizard_id, 'energymeters', function(callback) { - if (Object.keys(callback).length > 0) { - try { - module.exports.setAvailable({id: device_id}); - var energy_current_cons = ( callback[0].po ); // WATTS Energy used JSON $energymeters[0]['po'] - var energy_daytotal_cons = ( callback[0].dayTotal ); // KWH Energy used JSON $energymeters[0]['dayTotal'] - - // Wattcher elec current - module.exports.realtime( { id: device_id }, "measure_power", energy_current_cons ); - // Wattcher elec total day - module.exports.realtime( { id: device_id }, "meter_power", energy_daytotal_cons ); - - console.log("Wattcher usage- "+ energy_current_cons); - console.log("Wattcher Daytotal- "+ energy_daytotal_cons); - } catch (err) { - // Error with Wattcher no data in Energymeters - console.log ("No Wattcher found"); - module.exports.setUnavailable({id: device_id}, "No Wattcher found" ); - } - } + this.log('HomeWizard devices found:', Object.keys(hw_devices).length); + socket.emit('hw_devices', hw_devices); + resolve(hw_devices); }); - } else { - Homey.log('Removed Wattcher '+ device_id +' (old settings)'); - module.exports.setUnavailable({id: device_id}, "No Wattcher found" ); - clearInterval(refreshIntervalId); - } -} + }); + }); + socket.setHandler('manual_add', async (device) => { + const hwId = device.settings.homewizard_id; + + if (!hwId || hwId === '') { + socket.emit('error', 'No HomeWizard selected'); + return; + } + + this.log(`Wattcher added ${device.data.id} on HomeWizard ${hwId}`); + devices[device.data.id] = { + id: device.data.id, + name: device.name, + settings: device.settings, + }; + socket.emit('success', device); + return devices; + }); + + socket.setHandler('disconnect', () => { + this.log('User aborted pairing, or pairing is finished'); + }); + } +} +module.exports = HomeWizardWattcher; diff --git a/drivers/wattcher/pair/start.html b/drivers/wattcher/pair/start.html index c484957b..248f2e4f 100755 --- a/drivers/wattcher/pair/start.html +++ b/drivers/wattcher/pair/start.html @@ -1,50 +1,64 @@ }); } @@ -52,19 +66,6 @@ + + +

+
+ +
+
+

+ + + diff --git a/hwconfig.exe.zip b/hwconfig.exe.zip new file mode 100644 index 00000000..4c0e7c11 Binary files /dev/null and b/hwconfig.exe.zip differ diff --git a/includes/.eslintignore b/includes/.eslintignore new file mode 100644 index 00000000..8acbdadf --- /dev/null +++ b/includes/.eslintignore @@ -0,0 +1,4 @@ +node_modules +assets +drivers/**/assets +*.log \ No newline at end of file diff --git a/includes/homewizard.js b/includes/homewizard.js deleted file mode 100644 index 62bbdef2..00000000 --- a/includes/homewizard.js +++ /dev/null @@ -1,152 +0,0 @@ -var request = require('request'); - -module.exports = (function(){ - var homewizard = {}; - var self = {}; - self.devices = []; - self.polls = []; - var testdata = {"preset":0,"time":"2016-12-07 20:26","switches":[{"id":0,"type":"dimmer","status":"on","dimlevel":39},{"id":1,"type":"switch","status":"on"},{"id":2,"type":"dimmer","status":"off","dimlevel":0},{"id":3,"type":"switch","status":"off"},{"id":4,"type":"dimmer","status":"off","dimlevel":0},{"id":5,"type":"virtual"},{"id":6,"type":"hue","status":"on","color":{"hue":60,"sat":57,"bri":65}},{"id":7,"type":"virtual"},{"id":8,"type":"virtual"},{"id":9,"type":"switch","status":"on"},{"id":10,"type":"hue","status":"off","color":{"hue":175,"sat":0,"bri":100}},{"id":11,"type":"hue","status":"off","color":{"hue":60,"sat":59,"bri":66}},{"id":12,"type":"virtual"},{"id":13,"type":"hue","status":"off","color":{"hue":68,"sat":88,"bri":10}},{"id":14,"type":"hue","status":"off","color":{"hue":68,"sat":88,"bri":57}},{"id":15,"type":"hue","status":"off","color":{"hue":68,"sat":88,"bri":98}},{"id":16,"type":"hue","status":"off","color":{"hue":68,"sat":88,"bri":19}},{"id":17,"type":"hue","status":"off","color":{"hue":8,"sat":0,"bri":0}},{"id":18,"type":"hue","status":"off","color":{"hue":43,"sat":96,"bri":21}},{"id":19,"type":"hue","status":"on","color":{"hue":307,"sat":99,"bri":18}},{"id":20,"type":"virtual"},{"id":21,"type":"virtual"},{"id":22,"type":"virtual"}],"uvmeters":[],"windmeters":[],"rainmeters":[],"thermometers":[{"id":0,"te":20.3,"hu":47,"favorite":"no"},{"id":1,"te":6.9,"hu":78,"favorite":"no"},{"id":2,"te":20.4,"hu":44,"favorite":"no"},{"id":3,"te":24.7,"hu":43,"favorite":"no"},{"id":4,"te":23.1,"hu":45,"favorite":"no"},{"id":5,"te":7.1,"hu":32,"favorite":"no"}],"weatherdisplays":[], "energymeters":[{"id": 0, "name": "Wattcher", "key": "0", "code": "xxxxxxxxxx", "po": 320, "dayTotal": 5.33, "po+": 2730, "po+t": "6:23", "po-": 120, "po-t": "8:25", "lowBattery": "no", "favorite": "no"}], "energylinks": [{"id":0,"tariff":2,"s1":{"po":114,"dayTotal":0.00,"po+":114,"po+t":"00:01","po-":114,"po-t":"00:01"},"s2":null,"aggregate":{"po":264,"dayTotal":0.00,"po+":264,"po+t":"00:01","po-":264,"po-t":"00:01"},"used":{"po":378,"dayTotal":0.00,"po+":378,"po+t":"00:01","po-":378,"po-t":"00:01"},"gas":{"lastHour":0.23,"dayTotal":0.00},"kwhindex":0.73}], "heatlinks": [{"id": 0, "pump": "off", "heating": "off", "dhw": "off", "rte": 20.230, "rsp": 20.000, "tte": 0.000, "ttm": null, "wp": 1.359, "wte": 0.000, "ofc": 0, "odc": 0}], "kakusensors": [{"id":0,"status":"yes","timestamp":"20:25"},{"id":1,"status":"no","timestamp":"19:17"},{"id":2,"status":"yes","timestamp":"20:25"}]}; - - homewizard.debug = false; - homewizard.debug_devices = []; - homewizard.debug_devices.HW12345 = { - id: 'HW12345', - name: 'HomeWizard', - settings: { - homewizard_ip: '192.168.1.123', - homewizard_pass: 'xxxxx', - homewizard_ledring: true, - } - }; - homewizard.debug_devices_data = [ { id: 'HW12345' }]; - - homewizard.setDevices = function(devices){ - Homey.log('Devices SET!'); - Homey.log(devices); - if (homewizard.debug) { - self.devices = homewizard.debug_devices; - } else { - self.devices = devices; - } - }; - - homewizard.getDevices = function(callback) { - callback(self.devices); - }; - - homewizard.getDeviceData = function(device_id, data_part, callback) { - if (typeof self.devices[device_id] === 'undefined' || typeof self.devices[device_id].polldata === 'undefined' || typeof self.devices[device_id].polldata[data_part] === 'undefined') { - callback([]); - } else { - callback(self.devices[device_id].polldata[data_part]); - } - }; - - homewizard.call = function(device_id, uri_part, callback) { - if (homewizard.debug) { - callback(null, testdata); - } else { - Homey.log('Call device' + device_id); - if (typeof self.devices[device_id].settings !== 'undefined' && typeof self.devices[device_id].settings.homewizard_ip !== 'undefined' && typeof self.devices[device_id].settings.homewizard_pass !== 'undefined') { - var homewizard_ip = self.devices[device_id].settings.homewizard_ip; - var homewizard_pass = self.devices[device_id].settings.homewizard_pass; - request({ - uri: 'http://' + homewizard_ip + '/' + homewizard_pass + uri_part, - method: "GET", - timeout: 10000, - }, function (error, response, body) { - if (response === null || response === undefined) { - callback(false); - return; - } - if (!error && response.statusCode == 200) { - var jsonObject; - try { - jsonObject = JSON.parse(body); - - if (jsonObject.status == 'ok') { - if(typeof callback === 'function') { - callback(null, jsonObject.response); - } - } - } catch (exception) { - Homey.log('JSON: '+ body); - jsonObject = null; - callback(false); - } - } else { - if(typeof callback === 'function') { - callback(false); - } - Homey.log('Error: '+error); - } - }); - } else { - Homey.log('Homewizard '+ device_id +': settings not found!'); - } - } - }; - - homewizard.getScenes = function(args, callback) { - this.call(args.args.device.id, '/gplist', function(err, response) { - if (err === null) { - var output = []; - for (var i = 0, len = response.length; i < len; i++) { - if (response[i].name.toLowerCase().indexOf(args.query.toLowerCase()) !== -1) { - output[output.length] = response[i]; - } - } - if(typeof callback === 'function') { - callback(null, output); - } - } else { - callback(err); // err - } - }); - }; - - homewizard.ledring_pulse = function(device_id, colorName) { - var homewizard_ledring = self.devices[device_id].settings.homewizard_ledring; - if (homewizard_ledring) { - Homey.manager('ledring').animate( - 'pulse', // animation name (choose from loading, pulse, progress, solid) - { - color: colorName, - }, - 'INFORMATIVE', // priority - 3000, // duration - function(err, success) { // callback - if(err) return Homey.error(err); - Homey.log("Ledring pulsing "+colorName); - } - ); - } - }; - - homewizard.startpoll = function() { - self.polls.device_id = setInterval(function () { - homewizard.poll(); - }, 1000 * 10); - }; - - homewizard.poll = function() { - Object.keys(self.devices).forEach(function (device_id) { - if (typeof self.devices[device_id].polldata === 'undefined') { - self.devices[device_id].polldata = []; - } - homewizard.call(device_id, '/get-status', function(err, response) { - if (err === null) { - self.devices[device_id].polldata.preset = response.preset; - self.devices[device_id].polldata.heatlinks = response.heatlinks; - self.devices[device_id].polldata.energylinks = response.energylinks; - self.devices[device_id].polldata.energymeters = response.energymeters; - //self.devices[device_id].polldata.thermometers = response.thermometers; - - Homey.log('HW-Data polled for: '+device_id); - } - }); - }); - }; - - return homewizard; -})(); diff --git a/includes/legacy/fetchLegacyDebug.js b/includes/legacy/fetchLegacyDebug.js new file mode 100644 index 00000000..1db18ac1 --- /dev/null +++ b/includes/legacy/fetchLegacyDebug.js @@ -0,0 +1,86 @@ +'use strict'; + +const Homey = require('homey'); + +module.exports = class FetchLegacyDebug { + constructor(device, size = 100) { + this.device = device; + this.size = size; + + this.deviceId = + device?.id || + device?.getData?.()?.id || + device?.deviceInstance?.getData?.()?.id || + 'unknown-device'; + + this.deviceName = + device?.name || + device?.deviceInstance?.getName?.() || + 'unknown'; + + this.key = `fetchLegacyDebug_${this.deviceId}`; + + this.settings = + Homey && + Homey.settings && + typeof Homey.settings.get === 'function' && + typeof Homey.settings.set === 'function' + ? Homey.settings + : null; + + let stored = []; + if (this.settings) { + try { + stored = this.settings.get(this.key) || []; + } catch (_) {} + } + + this.buffer = Array.isArray(stored) ? stored : []; + + // throttle state + this._lastFlush = 0; + } + + log(entry) { + const iso = new Date().toLocaleString('nl-NL', { timeZone: 'Europe/Amsterdam', hour12: false }); + + this.buffer.push({ + t: iso, + id: this.deviceId, + name: this.deviceName, + ...entry, + }); + + if (this.buffer.length > this.size) { + this.buffer = this.buffer.slice(-this.size); + } + + this.flush(); + } + + flush() { + if (!this.settings) return; + + const now = Date.now(); + if (now - this._lastFlush < 1000) return; // max 1 write per seconde + + this._lastFlush = now; + + try { + this.settings.set(this.key, this.buffer); + } catch (_) {} + } + + get() { + return this.buffer; + } + + clear() { + this.buffer = []; + if (this.settings) { + try { + this.settings.set(this.key, []); + } catch (_) {} + } + } +}; diff --git a/includes/legacy/homewizard.js b/includes/legacy/homewizard.js new file mode 100644 index 00000000..6c5f62d7 --- /dev/null +++ b/includes/legacy/homewizard.js @@ -0,0 +1,464 @@ +'use strict'; + +const Homey = require('homey'); +const http = require('http'); +const FetchLegacyDebug = require('./fetchLegacyDebug'); +const fetch = require('node-fetch'); + +const Homey2023 = Homey.platform === 'local' && Homey.platformVersion === 2; + +module.exports = (function() { + const homewizard = {}; + const self = {}; + // self.devices = []; + self.devices = {}; + self.polls = []; + const debug = false; + + homewizard.setDevices = function(devices) { + self.devices = devices; + + for (const id in self.devices) { + const device = self.devices[id]; + if (!device) continue; + + if (!device.fetchLegacyDebug) { + device.fetchLegacyDebug = new FetchLegacyDebug(device, 100); + } + } + }; + + homewizard.getRandom = function(min, max) { + return Math.random() * (max - min) + min; + }; + + homewizard.getDevices = function(callback) { + callback(self.devices); + }; + + homewizard.getDeviceData = function(device_id, data_part) { + return new Promise((resolve) => { + if ( + typeof self.devices[device_id] === 'undefined' || + typeof self.devices[device_id].polldata === 'undefined' || + typeof self.devices[device_id].polldata[data_part] === 'undefined' + ) { + resolve([]); + } else { + resolve(self.devices[device_id].polldata[data_part]); + } + }); + }; + + function initCircuitBreaker(device) { + if (!device.circuit) { + device.circuit = { + failures: 0, + lastFailure: 0, + openUntil: 0, + threshold: 3, + cooldownMs: 300000 + }; + } + } + + function circuitBreakerAllows(device) { + initCircuitBreaker(device); + const now = Date.now(); + return device.circuit.openUntil <= now; + } + + function circuitBreakerFail(device) { + initCircuitBreaker(device); + const now = Date.now(); + const c = device.circuit; + + c.failures++; + c.lastFailure = now; + + if (c.failures >= c.threshold) { + c.openUntil = now + c.cooldownMs; + + device.fetchLegacyDebug.log({ + type: 'circuit_open', + openUntil: c.openUntil + }); + + if (debug) console.log(`Circuit breaker OPEN for device (cooldown ${c.cooldownMs}ms)`); + } + + } + + function circuitBreakerSuccess(device) { + initCircuitBreaker(device); + const c = device.circuit; + c.failures = 0; + c.openUntil = 0; + } + + function recordResponseTime(device, durationMs) { + if (!device.responseStats) { + device.responseStats = { samples: [], maxSamples: 20 }; + } + + const stats = device.responseStats; + + // Atomic bounds check + push to prevent exceeding maxSamples + if (stats.samples.length >= stats.maxSamples) { + stats.samples.shift(); + } + stats.samples.push(durationMs); + } + + function getAverageResponseTime(device) { + if (!device.responseStats || device.responseStats.samples.length === 0) { + return null; + } + + const arr = device.responseStats.samples; + return arr.reduce((a, b) => a + b, 0) / arr.length; + } + + function getAdaptiveTimeout(device) { + const avg = getAverageResponseTime(device); + + if (!avg || device.responseStats.samples.length < 3) return 7000; + + // Adaptive timeout: 2.5 × avg + const adaptive = avg * 2.5; + + // Hard min/max + return Math.min(Math.max(adaptive, 7000), 12000); + } + + + homewizard.setDeviceInstance = function(device_id, deviceInstance) { + if (!self.devices[device_id]) { + // optioneel: in debug zien dat er iets mis is + if (debug) console.log(`[homewizard.setDeviceInstance] Unknown device_id: ${device_id}`); + return; + } + + self.devices[device_id].deviceInstance = deviceInstance; + }; + + /** + * Remove a device from the legacy polling system and clean up references + * Call this from device onDeleted() handlers + */ + homewizard.removeDevice = function(device_id) { + if (!self.devices[device_id]) return; + + const device = self.devices[device_id]; + + // Stop polling this device + if (self.polls[device_id]) { + clearInterval(self.polls[device_id]); + self.polls[device_id] = null; + } + + // Clear fetchLegacyDebug reference + if (device.fetchLegacyDebug) { + device.fetchLegacyDebug.clear(); + device.fetchLegacyDebug = null; + } + + // Clear polldata + if (device.polldata) { + device.polldata = null; + } + + // Clear response stats + if (device.responseStats) { + device.responseStats.samples = []; + device.responseStats = null; + } + + // Clear circuit breaker + if (device.circuit) { + device.circuit = null; + } + + // Remove from global device map + delete self.devices[device_id]; + + if (debug) console.log(`[homewizard.removeDevice] Cleaned up device ${device_id}`); + }; + + + + // --------------------------------------------------------------------------- + // 🟩 LEGACY PARSER + // --------------------------------------------------------------------------- + + homewizard.callnew = async function(device_id, uri_part, callback) { + let timeout = null; + let controller = null; + let callbackCalled = false; + + const device = self.devices[device_id]; + if (!device || !device.settings) { + try { + device?.fetchLegacyDebug?.log({ + type: 'settings_missing', + url: null + }); + } catch (_) {} + return callback('settings_missing', []); + } + + if (!device.fetchLegacyDebug) { + device.fetchLegacyDebug = new FetchLegacyDebug(device, 100); + } + + const { homewizard_ip, homewizard_pass } = device.settings; + const url = `http://${homewizard_ip}/${homewizard_pass}${uri_part}`; + + if (!circuitBreakerAllows(device)) { + return callback('circuit_open', []); + } + + // Wrap callback to ensure it's only called once and to clean up references + const safeCallback = (err, data) => { + if (callbackCalled) return; + callbackCalled = true; + + // Clear references immediately to allow GC + clearTimeout(timeout); + timeout = null; + controller = null; + + callback(err, data); + }; + + const timeoutDuration = getAdaptiveTimeout(device); + const start = Date.now(); + + try { + controller = new AbortController(); + const { signal } = controller; + + timeout = setTimeout(() => { + controller.abort(); + }, timeoutDuration); + + const response = await fetch(url, { + signal, + follow: 0, + redirect: 'error', + headers: { 'Content-Type': 'application/json' } + }); + + clearTimeout(timeout); + + const duration = Date.now() - start; + recordResponseTime(device, duration); + circuitBreakerSuccess(device); + + if (response.status !== 200) { + device.fetchLegacyDebug.log({ + type: 'http_error', + url, + ms: duration, + status: response.status + }); + return safeCallback('http_error', []); + } + + const text = await response.text(); + + // Alleen nog bij debug=true, niet in productie + if (debug) { + device.fetchLegacyDebug.log({ + type: 'raw_response', + url, + ms: duration, + status: response.status, + body: text.slice(0, 300) + }); + } + + + let jsonData; + try { + jsonData = JSON.parse(text); + } catch (e) { + device.fetchLegacyDebug.log({ + type: 'json_parse_error', + url, + ms: duration, + error: e.message, + bodySnippet: text.slice(0, 300) + }); + return safeCallback('json_parse_error', []); + } + + if (jsonData.status === 'ok' && jsonData.response) { + return safeCallback(null, jsonData.response); + } + + if (jsonData.status === 'ok' && !jsonData.response) { + // Treat as success for write-only endpoints + if ( + uri_part.startsWith('/hl/0/settarget') || + uri_part.startsWith('/preset/') + ) { + return safeCallback(null, jsonData); + } + + device.fetchLegacyDebug.log({ + type: 'empty_response', + url, + ms: duration, + payload: jsonData + }); + return safeCallback('empty_response', []); + } + + + + + device.fetchLegacyDebug.log({ + type: 'invalid_data', + url, + ms: duration, + payload: jsonData + }); + return safeCallback('invalid_data', []); + + } catch (error) { + clearTimeout(timeout); + + const duration = Date.now() - start; + recordResponseTime(device, duration); + circuitBreakerFail(device); + + if (error.name === 'AbortError') { + device.fetchLegacyDebug.log({ + type: 'timeout', + url, + ms: duration, + timeout: timeoutDuration + }); + // keine "user aborted" log, keine dubbele entry + return safeCallback('timeout', []); + } + + device.fetchLegacyDebug.log({ + type: 'error', + url, + ms: duration, + error: error.message || error, + code: error.code || null + }); + + return safeCallback(error, []); + } +}; + + + // --------------------------------------------------------------------------- + + if (!Homey2023) { + homewizard.ledring_pulse = function(device_id, colorName) { + const { homewizard_ledring } = self.devices[device_id].settings; + if (homewizard_ledring) { + Homey.manager('ledring').animate( + 'pulse', + { color: colorName }, + 'INFORMATIVE', + 3000, + (err) => { + if (err) return Homey.error(err); + console.log(`Ledring pulsing ${colorName}`); + } + ); + } + }; + } + + homewizard.startpoll = function() { + homewizard.poll().catch(() => {}); + + for (const device_id in self.devices) { + const device = self.devices[device_id]; + if (!device) continue; + + const HARD_MIN_POLL_SEC = 15; + + const userIntervalSec = device?.settings?.poll_interval || 30; + const adaptiveTimeoutMs = getAdaptiveTimeout(device); + const adaptivePollSec = Math.ceil((adaptiveTimeoutMs + 1000) / 1000); + + const effectivePollSec = Math.max( + userIntervalSec, + HARD_MIN_POLL_SEC, + adaptivePollSec + ); + + + if (self.polls[device_id]) { + clearInterval(self.polls[device_id]); + } + + self.polls[device_id] = setInterval(async () => { + try { + await homewizard.poll(device_id); + } catch (_) {} + }, effectivePollSec * 1000); + } + }; + + homewizard.poll = async function(device_id = null) { + const list = device_id ? [device_id] : Object.keys(self.devices); + + for (const id of list) { + if (!self.devices[id]) continue; + + if (!self.devices[id].polldata) { + self.devices[id].polldata = []; + } + + let response; + try { + response = await new Promise((resolve, reject) => { + homewizard.callnew(id, '/get-sensors', (err, response) => { + if (err == null) resolve(response); + else reject(err); + }); + }); + } catch (_) { + continue; + } + + if (!response) continue; + + self.devices[id].polldata.preset = response.preset; + self.devices[id].polldata.heatlinks = response.heatlinks; + self.devices[id].polldata.energylinks = response.energylinks; + self.devices[id].polldata.energymeters = response.energymeters; + self.devices[id].polldata.thermometers = response.thermometers; + self.devices[id].polldata.rainmeters = response.rainmeters; + self.devices[id].polldata.windmeters = response.windmeters; + self.devices[id].polldata.kakusensors = response.kakusensors; + + if (Object.keys(response.energylinks).length !== 0) { + try { + const response2 = await new Promise((resolve, reject) => { + homewizard.callnew(id, '/el/get/0/readings', (err2, response2) => { + if (err2 == null) resolve(response2); + else reject(err2); + }); + }); + + if (response2) { + self.devices[id].polldata.energylink_el = response2; + } + } catch (_) {} + } + } + }; + + homewizard.self = self; + return homewizard; +}()); diff --git a/includes/utils/baseloadMonitor.js b/includes/utils/baseloadMonitor.js new file mode 100644 index 00000000..fdb92a1d --- /dev/null +++ b/includes/utils/baseloadMonitor.js @@ -0,0 +1,658 @@ +/* + * HomeWizard Baseload Monitor (Sluipverbruik) + * Copyright (C) 2025 Jeroen Tebbens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +'use strict'; + +class BaseloadMonitor { + constructor(homey) { + this.homey = homey; + + this.nightStartHour = 1; + this.nightEndHour = 5; + this.maxNights = 30; + + // Original thresholds - these work well for most households + // The key insight: fridge cycles (50-300W, 30-120min) are normal and not tracked as invalid + this.highPlateauThreshold = 800; + this.highPlateauMinDuration = 900000; + this.negativeMinDuration = 300000; + this.nearZeroMargin = 80; + this.nearZeroMinDuration = 600000; + this.oscillationWindow = 300000; + this.oscillationAmplitude = 500; + this.pvStartupEarliest = 5; + this.pvStartupLatest = 8; + + this.fridgeMinPower = 50; + this.fridgeMaxPower = 300; + this.fridgeMinDuration = 1800000; + this.fridgeMaxDuration = 7200000; + + this.devices = new Set(); + this.master = null; + this.enabled = false; + + this.currentNightSamples = []; + this.nightInvalid = false; + this.flags = {}; + this.nightHistory = []; + this.currentBaseload = null; + + this._nightTimer = null; + this._nightEndTimer = null; + + this.deviceNotificationPrefs = new Map(); + this.defaultNotificationsEnabled = false; + this.invalidNightCounter = 0; + + this._loadState(); + } + + registerP1Device(device) { + this.devices.add(device); + if (!this.enabled) this.start(); + } + + unregisterP1Device(device) { + this.devices.delete(device); + if (this.master === device) this.master = null; + if (this.devices.size === 0) this.stop(); + } + + trySetMaster(device) { + if (!this.master) this.master = device; + } + + updatePowerFromDevice(device, power, batteryPower = null) { + if (device === this.master) this.updatePower(power, batteryPower); + } + + start() { + if (this.enabled) return; + this.enabled = true; + this._scheduleNightWindow(); + } + + stop() { + this.enabled = false; + this._clearNightTimers(); + this._resetNightState(); + } + + updatePower(power, batteryPower = null) { + if (!this.enabled || typeof power !== 'number') return; + const now = new Date(); + if (!this._isInNightWindow(now)) return; + + // Battery-aware: subtract battery power from grid to get true household consumption. + // batteryPower > 0 = charging (grid includes charge current → subtract) + // batteryPower < 0 = discharging (grid reduced by discharge → subtract negative = add back) + // In both cases: householdPower = gridPower - batteryPower + let householdPower = power; + if (typeof batteryPower === 'number' && batteryPower !== 0) { + householdPower = power - batteryPower; + } + // Clamp: household power can never be negative (meter rounding / timing mismatch) + if (householdPower < 0) householdPower = 0; + + this._processNightSample(now, householdPower, power, batteryPower); + } + + _isInNightWindow(d) { + const h = d.getHours(); + return h >= this.nightStartHour && h < this.nightEndHour; + } + + _isWithinCurrentNightWindow(now = new Date()) { + const s = new Date(now), e = new Date(now); + s.setHours(this.nightStartHour,0,0,0); + e.setHours(this.nightEndHour,0,0,0); + return now >= s && now < e; + } + + _scheduleNightWindow() { + this._clearNightTimers(); + const now = new Date(); + if (this._isWithinCurrentNightWindow(now)) return this._onNightStartFromRecovery(now); + const next = new Date(now); + next.setHours(this.nightStartHour,0,0,0); + if (next <= now) next.setDate(next.getDate()+1); + this._nightTimer = this.homey.setTimeout(()=>this._onNightStart(), next-now); + } + + _onNightStartFromRecovery(now) { + if (!this.enabled) return this._scheduleNightWindow(); + this._resetNightState(); + const end = new Date(now); + end.setHours(this.nightEndHour,0,0,0); + this._nightEndTimer = this.homey.setTimeout(()=>this._onNightEnd(), Math.max(0,end-now)); + } + + _clearNightTimers() { + if (this._nightTimer) this.homey.clearTimeout(this._nightTimer); + if (this._nightEndTimer) this.homey.clearTimeout(this._nightEndTimer); + this._nightTimer = this._nightEndTimer = null; + } + + _onNightStart() { + if (!this.enabled) return this._scheduleNightWindow(); + this._resetNightState(); + const dur = (this.nightEndHour - this.nightStartHour)*3600000; + this._nightEndTimer = this.homey.setTimeout(()=>this._onNightEnd(), dur); + } + + _onNightEnd() { + this.homey.clearTimeout(this._nightEndTimer); + this._nightEndTimer = null; + if (!this.enabled) return this._scheduleNightWindow(); + this._finalizeNight(); + this._scheduleNightWindow(); + } + + _resetNightState() { + this.currentNightSamples = []; + this.nightInvalid = false; + this.flags = { + sawHighPlateau:false, + sawNegativeLong:false, + sawNearZeroLong:false, + sawOscillation:false, + sawPVStartup:false, + }; + } + + _processNightSample(ts, power, rawGridPower = null, batteryPower = null) { + // Throttle: store at most 1 sample per 30 seconds. + // Duration-based detection methods work correctly at any interval; + // 30s resolution is more than enough for 5–15 min detection windows. + const nowMs = ts && ts.getTime ? ts.getTime() : (typeof ts === 'number' ? ts : Date.now()); + const lastSample = this.currentNightSamples.at(-1); + const lastMs = lastSample + ? (lastSample.ts && lastSample.ts.getTime ? lastSample.ts.getTime() : lastSample.ts) + : -Infinity; + + if (nowMs - lastMs >= 30000) { + this.currentNightSamples.push({ ts, power, rawGridPower, batteryPower }); + } + + if (power < 0) return; // export: don't trigger plateau/zero detection + + // Only re-run expensive detections every 30 seconds to avoid CPU overhead + // Each update would otherwise trigger full array scans + const lastCheck = this._lastDetectionCheck || 0; + + if (nowMs - lastCheck >= 30000) { + this._lastDetectionCheck = nowMs; + this._detectHighPlateau(); + this._detectNegativeLong(); + this._detectNearZeroLong(); + this._detectOscillation(); + this._detectPVStartup(); + } + } + + + _detectHighPlateau() { + if (this.currentNightSamples.length<2) return; + const avg = this._avg(this.currentNightSamples.map(s=>s.power)); + const base = this.currentBaseload||100; + if (avg>base+this.highPlateauThreshold && + this._durAbove(base+this.highPlateauThreshold)>=this.highPlateauMinDuration) { + this.flags.sawHighPlateau=true; this.nightInvalid=true; + } + } + + _detectNegativeLong() { + if (this._durBelow(0)>=this.negativeMinDuration) { + this.flags.sawNegativeLong=true; this.nightInvalid=true; + } + } + + _detectNearZeroLong() { + // Near-zero detection is meant to catch grid balancing, not normal low-baseload households + // To avoid false positives from fridge cycles in low-baseload homes: + // Only flag if CONTINUOUS near-zero for >= nearZeroMinDuration (10 minutes). + // Time-based logic works correctly at any sample interval. + + let maxConsecutiveMs = 0; + let currentStreakMs = 0; + let lastTs = null; + let skippedBatteryKnown = 0; + + for (const s of this.currentNightSamples) { + const ts = s.ts && s.ts.getTime ? s.ts.getTime() : s.ts; + // If no battery data was provided and grid is near-zero, skip this sample: + // we cannot distinguish genuine near-zero from battery compensation. + const unknownBattery = s.batteryPower === null || s.batteryPower === undefined; + const rawNearZero = Math.abs(s.rawGridPower ?? s.power) < this.nearZeroMargin; + if (unknownBattery && rawNearZero) { + // Can't distinguish battery compensation from true near-zero — skip. + lastTs = ts; + continue; + } + if (!unknownBattery) { + // Battery power is known → householdPower is a real measurement, not grid + // balancing noise. Even a 50W baseload looks "near-zero" against the 80W + // margin. Don't contribute to the near-zero streak. + skippedBatteryKnown++; + lastTs = ts; + continue; + } + if (Math.abs(s.power) < this.nearZeroMargin) { + if (lastTs !== null) currentStreakMs += ts - lastTs; + } else { + maxConsecutiveMs = Math.max(maxConsecutiveMs, currentStreakMs); + currentStreakMs = 0; + } + lastTs = ts; + } + maxConsecutiveMs = Math.max(maxConsecutiveMs, currentStreakMs); + + if (maxConsecutiveMs >= this.nearZeroMinDuration) { + this.flags.sawNearZeroLong = true; + this.nightInvalid = true; + } + } + + _detectOscillation() { + const w = this._lastSamples(this.oscillationWindow); + if (w.length<4) return; + + // Trim 1 outlier from each end before computing range. + // A single bad sample (e.g. battery mode-transition measurement lag) must not + // invalidate the night; only sustained oscillation should. + const sorted = w.map(s => s.power).sort((a, b) => a - b); + const lo = sorted[1]; + const hi = sorted[sorted.length - 2]; + + if (hi - lo >= this.oscillationAmplitude) { + this.flags.sawOscillation=true; + this.nightInvalid=true; + } + } + + _detectPVStartup() { + const last = this.currentNightSamples.at(-1); + if (!last) return; + const h = last.ts.getHours(); + if (h>=this.pvStartupEarliest && h<=this.pvStartupLatest && last.power<0) { + this.flags.sawPVStartup=true; this.nightInvalid=true; + } + } + + _detectFridgeCycles(samples) { + let c=0,inC=false,start=null,last=null; + for (const s of samples) { + const w = s.power>=this.fridgeMinPower && s.power<=this.fridgeMaxPower; + if (!inC && w) {inC=true; start=s.ts;} + else if (inC && !w) { + const d=s.ts-start; + if (d>=this.fridgeMinDuration && d<=this.fridgeMaxDuration) c++; + inC=false; start=null; + } + last=s.ts; + } + if (inC && start && last-start>=this.fridgeMinDuration && last-start<=this.fridgeMaxDuration) c++; + return c; + } + + _finalizeNight() { + const dateKey = new Date().toISOString().slice(0,10); + const cycles = this._detectFridgeCycles(this.currentNightSamples); + + // [DEBUG] One-line night summary for diagnosing battery correction + { + const total = this.currentNightSamples.length; + const withBatt = this.currentNightSamples.filter(s => s.batteryPower !== null && s.batteryPower !== undefined).length; + const avgGrid = total ? Math.round(this.currentNightSamples.reduce((a,s)=>(a + (s.rawGridPower ?? s.power)),0) / total) : null; + const avgHousehold = total ? Math.round(this.currentNightSamples.reduce((a,s)=>a+s.power,0) / total) : null; + console.log(`[BaseloadMonitor] night ${dateKey}: ${total} samples, ${withBatt} with battery data, avgGrid=${avgGrid}W, avgHousehold=${avgHousehold}W, invalid=${this.nightInvalid}${this.nightInvalid ? ` (${Object.entries(this.flags).filter(([,v])=>v).map(([k])=>k).join(',')})` : ''}`); + } + + if (this.currentNightSamples.length===0) { + this._push(dateKey,null,true,{fridgeCycles:cycles}); + this._notify('night_no_samples'); + return; + } + + const labels = { + sawHighPlateau:{nl:'hoog verbruik',en:'high consumption'}, + sawNegativeLong:{nl:'negatief vermogen',en:'negative power'}, + sawNearZeroLong:{nl:'balanceren rond 0W',en:'near-zero balancing'}, + sawOscillation:{nl:'fluctuaties',en:'oscillation'}, + sawPVStartup:{nl:'PV opstart',en:'PV startup'}, + }; + const lang = this._lang(); + + if (this.nightInvalid) { + this.invalidNightCounter++; + const reasons = Object.entries(this.flags).filter(([,v])=>v) + .map(([k])=>labels[k][lang]).join(', ') || (lang==='nl'?'onbekend':'unknown'); + + if (this.invalidNightCounter>=3) { + this._notify('night_invalid',{reasons}); + this.invalidNightCounter=0; + } + + this._push(dateKey,null,true,{fridgeCycles:cycles}); + + const valid = this.nightHistory.slice(-7).filter(n=>!n.invalid && typeof n.avg==='number'); + if (!valid.length) { + const fb = this._fallback(); + if (fb!==null) { + this.currentBaseload=fb; + this._save(); + this._notify('baseload_fallback',{fallback:fb.toFixed(0)}); + } + } + return; + } + + this.invalidNightCounter=0; + + // Detect nights where battery compensation masked consumption: + // >50% of samples have no battery data and near-zero household power. + // These are unreliable — skip silently without triggering notifications. + const uncorrectedNearZero = this.currentNightSamples.filter(s => + (s.batteryPower === null || s.batteryPower === undefined) && + Math.abs(s.rawGridPower ?? s.power) < this.nearZeroMargin).length; + if (uncorrectedNearZero / this.currentNightSamples.length > 0.5) { + this._push(dateKey, null, true, { fridgeCycles: cycles, batteryMasked: true }); + return; + } + + const valid = this.nightHistory.slice(-7).filter(n=>!n.invalid && typeof n.avg==='number'); + if (!valid.length) { + const fb = this._fallback(); + if (fb!==null) { + this.currentBaseload=fb; + this._save(); + this._notify('baseload_fallback',{fallback:fb.toFixed(0)}); + } + } + + const avg = this._avg(this.currentNightSamples.map(s=>s.power)); + this._push(dateKey,avg,false,{fridgeCycles:cycles}); + + const old = this.currentBaseload; + this.currentBaseload = this._computeSmartBaseload(); + this._save(); + + if (old && this.currentBaseload) { + const diff = Math.abs(this.currentBaseload-old); + const pct = diff/old*100; + if (diff>50 && pct>20) { + this._notify('baseload_changed',{ + current:this.currentBaseload.toFixed(0), + previous:old.toFixed(0) + }); + } + } + } + + _lang() { + try {return this.homey.i18n.getLanguage().startsWith('nl')?'nl':'en';} + catch{return'en';} + } + + _downsampleSamples(samples, intervalMs = 30000) { + if (!samples.length) return []; + const result = []; + let lastKeptTs = -Infinity; + for (const s of samples) { + const ts = s.ts && s.ts.getTime ? s.ts.getTime() : (typeof s.ts === 'number' ? s.ts : 0); + if (ts - lastKeptTs >= intervalMs) { + // Strip rawGridPower/batteryPower from history — only power is needed for stats + result.push({ ts, power: s.power }); + lastKeptTs = ts; + } + } + return result; + } + + _push(date,avg,invalid,meta={}) { + // Downsample before storing: keep 1 sample per 30s instead of 1 per second. + // currentNightSamples stays at full resolution for real-time detection; + // history only needs statistical resolution (halved from ~14,400 → ~480/night). + const samples = this._downsampleSamples(this.currentNightSamples, 30000); + this.nightHistory.push({date,avg,invalid,samples,...meta}); + if (this.nightHistory.length>this.maxNights) + this.nightHistory.splice(0,this.nightHistory.length-this.maxNights); + } + + _compute() { + const v = this.nightHistory.filter(n=>!n.invalid && typeof n.avg==='number').map(n=>n.avg); + if (!v.length) {this._notify('baseload_unavailable'); return this.currentBaseload||null;} + + // Simple selection sort for first 3 values instead of full sort + const count = Math.min(3, v.length); + const sorted = []; + + for (let i = 0; i < count; i++) { + let minIdx = 0; + for (let j = 1; j < v.length; j++) { + if (v[j] < v[minIdx] && !sorted.includes(j)) minIdx = j; + } + sorted.push(v[minIdx]); + } + + return this._avg(sorted); + } + + /** + * Smart baseload calculation that filters out EV charging and heat pump cycles + * Strategy: + * 1. Get all valid nights + * 2. For each night, filter samples to exclude obvious non-baseload (>1kW) + * 3. Take median of lowest 50% of filtered samples per night + * 4. Average the 3 lowest night medians + * + * This is robust against: + * - EV charging (typically 1.4-7kW) + * - Heat pump cycles (typically 2-3kW) + * - Brief high consumption spikes + */ + _computeSmartBaseload() { + const validNights = this.nightHistory.filter(n => !n.invalid && Array.isArray(n.samples) && n.samples.length > 0); + + if (!validNights.length) { + this._notify('baseload_unavailable'); + return this.currentBaseload || null; + } + + const nightMedians = []; + + for (const night of validNights) { + // Filter out obvious non-baseload consumption (EV charging, heat pumps, etc.) + // Keep only samples that look like true baseload (<1000W) + const baseloadSamples = night.samples + .map(s => s.power) + .filter(p => typeof p === 'number' && p >= 0 && p < 1000); + + if (baseloadSamples.length < 10) continue; // Need at least 10 samples for reliable median + + // Sort to find median of lowest 50% + baseloadSamples.sort((a, b) => a - b); + const halfPoint = Math.floor(baseloadSamples.length / 2); + const lowestHalf = baseloadSamples.slice(0, halfPoint); + + if (lowestHalf.length > 0) { + // Median of lowest half + const medianIdx = Math.floor(lowestHalf.length / 2); + nightMedians.push(lowestHalf[medianIdx]); + } + } + + if (!nightMedians.length) { + // Fallback to old method if smart filtering yields nothing + return this._compute(); + } + + // Take average of 3 lowest night medians + nightMedians.sort((a, b) => a - b); + const count = Math.min(3, nightMedians.length); + const lowest = nightMedians.slice(0, count); + + return this._avg(lowest); + } + + _avg(a) {return a.length?a.reduce((x,y)=>x+y,0)/a.length:null;} + + /** + * Calculate monthly and yearly cost estimate for current baseload + * @param {number} avgPricePerKwh - Average electricity price in €/kWh + * @returns {object} { baseloadW, monthlyKwh, monthlyCost, yearlyCost } + */ + getMonthlyEstimate(avgPricePerKwh = 0.25) { + if (!this.currentBaseload || typeof avgPricePerKwh !== 'number') { + return { baseloadW: null, monthlyKwh: null, monthlyCost: null, yearlyCost: null }; + } + + const baseloadW = this.currentBaseload; + const monthlyKwh = (baseloadW / 1000) * 24 * 30; // W to kW, 24h/day, 30 days + const monthlyCost = monthlyKwh * avgPricePerKwh; + const yearlyCost = monthlyCost * 12; + + return { + baseloadW: Math.round(baseloadW), + monthlyKwh: Math.round(monthlyKwh * 10) / 10, + monthlyCost: Math.round(monthlyCost * 100) / 100, + yearlyCost: Math.round(yearlyCost * 100) / 100 + }; + } + + _durAbove(t) { + let ms=0; + for (let i=1;it && c.power>t) ms+=c.ts-p.ts; + } + return ms; + } + + _durBelow(t) { + let ms=0; + for (let i=1;i= 0; i--) { + const s = this.currentNightSamples[i]; + if (s.ts <= threshold) break; + result.unshift(s); + } + return result; + } + + _fallback() { + const r=[]; + for (const n of this.nightHistory.slice(-7)) if (Array.isArray(n.samples)) r.push(...n.samples); + const p=[]; + for (const s of r) { + // Filter: only non-negative values < 1000W (same logic as _computeSmartBaseload) + if (typeof s.power === 'number' && s.power >= 0 && s.power < 1000) p.push(s.power); + } + if (!p.length) return null; + + // Partial sort for bottom 10% instead of full sort + const take=Math.max(3,Math.floor(p.length*0.1)); + const minVals = []; + + for (let i = 0; i < take && i < p.length; i++) { + let minIdx = i; + for (let j = i + 1; j < p.length; j++) { + if (p[j] < p[minIdx]) minIdx = j; + } + [p[i], p[minIdx]] = [p[minIdx], p[i]]; + minVals.push(p[i]); + } + + return this._avg(minVals); + } + + _save() { + this.homey.settings.set('baseload_state',{ + nightHistory:this.nightHistory, + currentBaseload:this.currentBaseload, + deviceNotificationPrefs:Array.from(this.deviceNotificationPrefs.entries()), + invalidNightCounter:this.invalidNightCounter + }); + } + + _loadState() { + const s=this.homey.settings.get('baseload_state'); + if (!s) return; + if (Array.isArray(s.nightHistory)) this.nightHistory=s.nightHistory; + if (typeof s.currentBaseload==='number') this.currentBaseload=s.currentBaseload; + if (Array.isArray(s.deviceNotificationPrefs)) this.deviceNotificationPrefs=new Map(s.deviceNotificationPrefs); + if (typeof s.invalidNightCounter==='number') this.invalidNightCounter=s.invalidNightCounter; + } + + setNotificationsEnabledForDevice(device,enabled) { + this.deviceNotificationPrefs.set(device.getId(),enabled); + this._save(); + } + + async _notify(key,vars={}) { + if (!this.master) return; + const pref=this.deviceNotificationPrefs.get(this.master.getId()); + const enabled=(pref!==undefined)?pref:this.defaultNotificationsEnabled; + if (!enabled) return; + + const lang=this._lang(); + const msg={ + night_invalid:{ + nl:`Baseload niet bijgewerkt: nacht bevatte fluctuaties (${vars.reasons}).`, + en:`Baseload not updated: night contained fluctuations (${vars.reasons}).` + }, + night_no_samples:{ + nl:`Baseload niet bijgewerkt: geen gegevens ontvangen.`, + en:`Baseload not updated: no data received.` + }, + baseload_changed:{ + nl:`Baseload gewijzigd: ${vars.current} W (was ${vars.previous} W).`, + en:`Baseload changed: ${vars.current} W (was ${vars.previous} W).` + }, + baseload_unavailable:{ + nl:`Baseload niet beschikbaar: geen geldige nachten.`, + en:`Baseload unavailable: no valid nights.` + }, + baseload_fallback:{ + nl:`Baseload (fallback): ${vars.fallback} W.`, + en:`Fallback baseload: ${vars.fallback} W.` + } + }[key]?.[lang]; + + if (!msg) return; + try {await this.homey.notifications.createNotification({excerpt:msg});} + catch(e){this.homey.error('Notification failed:',e);} + } +} + +module.exports = BaseloadMonitor; diff --git a/includes/utils/fetchQueue.js b/includes/utils/fetchQueue.js new file mode 100644 index 00000000..03ab4207 --- /dev/null +++ b/includes/utils/fetchQueue.js @@ -0,0 +1,102 @@ +const fetch = require('node-fetch'); + +const queue = []; +let active = 0; + +const MAX_CONCURRENT = 4; +const MIN_DELAY = 200; +const MAX_QUEUE = 100; + +function log(...args) { + const ts = new Date().toISOString(); + console.log(ts, '[fetchQueue]', ...args); +} + + +function processQueue() { + if (active >= MAX_CONCURRENT) return; + if (queue.length === 0) return; + + const job = queue.shift(); + const { url, opts, resolve, reject, retry } = job; + + active++; + + const controller = new AbortController(); + const timeoutMs = opts.timeout || 5000; + + const timeout = setTimeout(() => { + controller.abort(); + log(`timeout: ${url}`); + }, timeoutMs); + + fetch(url, { ...opts, signal: controller.signal }) + .then(resolve) + .catch(err => { + if (err.name === 'AbortError') { + log(`timeout (abort): ${url}`); + } else { + log(`error on ${url}: ${err.message}`); + } + + if (!retry) { + log(`retrying once: ${url}`); + + setTimeout(() => { + // Retry must respect MAX_QUEUE and duplicate rules + const key = `${url}|${opts.method || 'GET'}`; + + if (queue.length >= MAX_QUEUE) { + return reject(new Error(`Queue overflow during retry: ${key}`)); + } + + if (!queue.some(j => `${j.url}|${j.opts.method || 'GET'}` === key)) { + queue.push({ url, opts, resolve, reject, retry: true }); + } + + setImmediate(processQueue); + }, 1000); + + } else { + log(`final fail: ${url}`); + reject(err); + } + }) + .finally(() => { + clearTimeout(timeout); + active = Math.max(0, active - 1); + + // Always schedule next job with jitter + setTimeout(() => { + setImmediate(processQueue); + }, MIN_DELAY + Math.floor(Math.random() * 100)); + }); +} + +function queuedFetch(url, opts = {}) { + return new Promise((resolve, reject) => { + const key = `${url}|${opts.method || 'GET'}`; + + // Duplicate suppression + if (queue.some(job => `${job.url}|${job.opts.method || 'GET'}` === key)) { + return reject(new Error(`Duplicate request suppressed`)); + } + + // Backpressure + if (queue.length >= MAX_QUEUE) { + return reject(new Error(`Queue overflow: ${queue.length} jobs`)); + } + + queue.push({ url, opts, resolve, reject, retry: false }); + + // Always schedule processing on next tick to avoid starvation + setImmediate(processQueue); + }); +} + +queuedFetch.stats = () => ({ + active, + pending: queue.length, +}); + +module.exports = queuedFetch; diff --git a/includes/utils/fetchWithTimeout.js b/includes/utils/fetchWithTimeout.js new file mode 100644 index 00000000..bfe53ecd --- /dev/null +++ b/includes/utils/fetchWithTimeout.js @@ -0,0 +1,31 @@ +'use strict'; + +const fetch = require('node-fetch'); + +/** + * fetch() with a hard timeout. + * + * Returns a Response (same as fetch), but rejects with Error('TIMEOUT') + * if the request has not resolved within `timeoutMs` milliseconds. + * + * @param {string} url + * @param {object} [options={}] node-fetch options (headers, method, body, agent, …) + * @param {number} [timeoutMs=5000] + * @returns {Promise} + */ +async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const res = await fetch(url, { ...options, signal: controller.signal }); + clearTimeout(timer); + return res; + } catch (err) { + clearTimeout(timer); + if (err.name === 'AbortError') throw new Error('TIMEOUT'); + throw err; + } +} + +module.exports = fetchWithTimeout; diff --git a/includes/v1/driver.js b/includes/v1/driver.js new file mode 100644 index 00000000..1ad20ee3 --- /dev/null +++ b/includes/v1/driver.js @@ -0,0 +1,175 @@ +'use strict'; + +const Homey = require('homey'); +const fetchWithTimeout = require('../utils/fetchWithTimeout'); + + +module.exports = class HomeWizardEnergyWatermeterDriver extends Homey.Driver { + +logDiscovery(status, detail = null) { + const dbg = this.homey.settings.get('debug_discovery') || {}; + + dbg.lastStatus = status; // 'ok', 'error', 'timeout', 'not_found' + dbg.lastDetail = detail ? String(detail) : null; + dbg.lastUpdate = new Date().toLocaleString('nl-NL', { timeZone: 'Europe/Amsterdam', hour12: false }), + + this.homey.settings.set('debug_discovery', dbg); +} + + + +/** + * Discovers available devices and returns them for pairing. + * + * @async + * @returns {Promise} List of discovered devices with name and ID. + */ + async onPairListDevices() { + + const discoveryStrategy = this.getDiscoveryStrategy(); + this.logDiscovery('start', 'Beginning mDNS discovery'); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + const discoveryResults = discoveryStrategy.getDiscoveryResults(); + const numberOfDiscoveryResults = Object.keys(discoveryResults).length; + //console.log('Discovered devices:', discoveryResults); Error: Circular Reference "device" + + console.log( + '[DISCOVERY]', + Object.values(discoveryResults).map(r => ({ + id: r.id, + address: r.address, + port: r.port, + product: r.txt?.product_name, + serial: r.txt?.serial, + })) + ); + + + const devices = []; + const results = Object.values(discoveryResults); + + // Al-gepaarde devices hoeven niet opnieuw geverifieerd te worden via HTTP — + // we kennen hun naam al. Alleen nieuwe (nog niet gepaarde) devices fetchen. + // Dit voorkomt een RSS-piek van 16+ gelijktijdige fetches bij grote setups. + const pairedMap = new Map( + this.getDevices().map(d => [d.getData().id, d.getName()]) + ); + + for (const r of results) { + if (pairedMap.has(r.id)) { + devices.push({ name: pairedMap.get(r.id), data: { id: r.id } }); + } + } + + const newResults = results.filter(r => !pairedMap.has(r.id)); + console.log(`[DISCOVERY] ${pairedMap.size} al-gepaird (geen fetch), ${newResults.length} nieuw te verifiëren`); + + const CONCURRENCY = 2; + for (let i = 0; i < newResults.length; i += CONCURRENCY) { + const batch = newResults.slice(i, i + CONCURRENCY); + await Promise.all(batch.map(async (discoveryResult) => { + try { + const url = `http://${discoveryResult.address}:${discoveryResult.port}/api`; + const res = await fetchWithTimeout(url, {}, 2000); + if (!res.ok) throw new Error(res.statusText); + + const data = await res.json(); + + const productName = typeof data.product_name === 'string' && data.product_name + ? data.product_name + : (data.product_type || 'HomeWizard Device'); + + this.logDiscovery('ok', `Found ${productName} at ${discoveryResult.address}`); + + let name = productName; + if (numberOfDiscoveryResults > 1) { + name = `${productName} (${data.serial || discoveryResult.id})`; + } + + devices.push({ + name, + data: { id: discoveryResult.id }, + }); + + } catch (err) { + console.log(`Discovery failed for ${discoveryResult.id}:`, err.message); + this.logDiscovery('error', err.message); + } + })); + } + + if (devices.length === 0) { + this.logDiscovery('not_found', 'No devices responded to mDNS'); + throw new Error(this.homey.__('pair.no_devices_found')); + } + + return devices; +} + +async onRepair(session, device) { + console.log('[REPAIR] Starting repair session for device:', device.getName()); + + // Get current manual IP if set + session.setHandler('get_current_ip', async () => { + const manualIP = device.getSetting('manual_ip'); + const discoveryIP = device.getStoreValue('address'); + return { + manual_ip: manualIP || '', + discovery_ip: discoveryIP || this.homey.__('repair.unknown'), + using_manual: !!manualIP + }; + }); + + // Validate and set manual IP + session.setHandler('set_manual_ip', async (data) => { + const ip = data.ip?.trim(); + + // Clear manual IP if requested + if (data.clear) { + await device.setSettings({ manual_ip: '' }); + console.log('[REPAIR] Manual IP cleared, returning to mDNS discovery'); + return { success: true }; + } + + // Validate IP format + if (!ip || !/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) { + throw new Error(this.homey.__('repair.invalid_ip')); + } + + // Test connection to device + try { + const response = await fetchWithTimeout(`http://${ip}/api`, { method: 'GET' }, 5000); + + if (!response.ok) { + throw new Error(this.homey.__('repair.connection_failed')); + } + + const apiData = await response.json(); + + // Verify it's the same device by serial number + if (apiData.serial && apiData.serial !== device.getData().id) { + throw new Error(this.homey.__('repair.wrong_device')); + } + + // Save manual IP + await device.setSettings({ manual_ip: ip }); + console.log('[REPAIR] Manual IP set to:', ip); + + // Trigger device reconnection if it has the method + if (typeof device.reconnectWithManualIP === 'function') { + await device.reconnectWithManualIP(ip); + } + + return { success: true }; + + } catch (error) { + console.error('[REPAIR] Connection test failed:', error.message); + throw new Error(this.homey.__('repair.connection_failed') + ': ' + error.message); + } + }); +} + + +}; diff --git a/includes/v2/Api.js b/includes/v2/Api.js new file mode 100644 index 00000000..4d2f3de1 --- /dev/null +++ b/includes/v2/Api.js @@ -0,0 +1,199 @@ +'use strict'; + +const https = require('https'); +const fetchWithTimeout = require('../utils/fetchWithTimeout'); + +module.exports = (function () { + const api = {}; + + const http_agent = new https.Agent({ + keepAlive: true, + keepAliveMsecs: 11000, + rejectUnauthorized: false, + }); + + + /** + * Pure fetch → always returns parsed JSON or throws + */ + async function fetchJSON(url, opts = {}) { + const res = await fetchWithTimeout(url, { agent: http_agent, ...opts }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + const text = await res.text(); + try { + return JSON.parse(text); + } catch { + return text; + } + } + + // ------------------------- + // IDENTIFY + // ------------------------- + api.identify = async function (url, token) { + const data = await fetchJSON(`${url}/api/system/identify`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + } + }); + + if (typeof data !== 'object') { + throw new Error('Invalid response format'); + } + }; + + // ------------------------- + // MEASUREMENT + // ------------------------- + api.getMeasurement = async function (url, token) { + return fetchJSON(`${url}/api/measurement`, { + headers: { Authorization: `Bearer ${token}` } + }); + }; + + // ------------------------- + // SYSTEM + // ------------------------- + api.getSystem = async function (url, token) { + return fetchJSON(`${url}/api/system`, { + headers: { Authorization: `Bearer ${token}` } + }); + }; + + // ------------------------- + // INFO + // ------------------------- + api.getInfo = async function (url, token) { + return fetchJSON(`${url}/api`, { + headers: { Authorization: `Bearer ${token}` } + }); + }; + + // ------------------------- + // GET MODE + // ------------------------- + api.getMode = async function (url, token) { + const data = await fetchJSON(`${url}/api/batteries`, { + headers: { Authorization: `Bearer ${token}` } + }); + + if (Array.isArray(data.permissions)) { + const perms = [...data.permissions].sort().join(','); + + if (data.mode === 'to_full') return 'to_full'; + + switch (perms) { + case '': + return 'standby'; + case 'charge_allowed,discharge_allowed': + return 'zero'; + case 'charge_allowed': + return 'zero_charge_only'; + case 'discharge_allowed': + return 'zero_discharge_only'; + default: + throw new Error(`Unknown permissions combination: ${JSON.stringify(data.permissions)}`); + } + } + + return data.mode; + }; + + // ------------------------- + // SET MODE (no retries) + // ------------------------- + api.setMode = async function (url, token, selectedMode) { + let body; + + switch (selectedMode) { + case 'standby': + body = { mode: 'standby', permissions: [] }; + break; + case 'zero': + body = { mode: 'zero', permissions: ['charge_allowed', 'discharge_allowed'] }; + break; + case 'zero_charge_only': + body = { mode: 'zero', permissions: ['charge_allowed'] }; + break; + case 'zero_discharge_only': + body = { mode: 'zero', permissions: ['discharge_allowed'] }; + break; + case 'to_full': + body = { mode: 'to_full' }; + break; + default: + body = { mode: selectedMode }; + } + + return fetchJSON(`${url}/api/batteries`, { + method: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body) + }); + }; + + // ------------------------- + // CLOUD ON/OFF (no retries) + // ------------------------- + api.setCloudOn = async function (url, token) { + return api._setCloud(url, token, true); + }; + + api.setCloudOff = async function (url, token) { + return api._setCloud(url, token, false); + }; + + api._setCloud = async function (url, token, enabled) { + return fetchJSON(`${url}/api/system`, { + method: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ cloud_enabled: enabled }) + }); + }; + + // GET LED BRIGHTNESS +api.getLedBrightness = async function (url, token) { + const data = await fetchJSON(`${url}/api/system`, { + headers: { Authorization: `Bearer ${token}` } + }); + + if (typeof data.status_led_brightness_pct === 'number') { + return data.status_led_brightness_pct / 100; // Homey expects 0–1 + } + + throw new Error('LED brightness not present in system response'); +}; + + + + // SET LED BRIGHTNESS +api.setLedBrightness = async function (url, token, brightnessPct) { + return fetchJSON(`${url}/api/system`, { + method: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + status_led_brightness_pct: brightnessPct + }) + }); +}; + + + + + return api; +}()); diff --git a/includes/v2/Driver.js b/includes/v2/Driver.js new file mode 100644 index 00000000..b1cf13c5 --- /dev/null +++ b/includes/v2/Driver.js @@ -0,0 +1,257 @@ +'use strict'; + +const Homey = require('homey'); +const fetch = require('node-fetch'); + +const https = require('https'); + +/** + * Helper method to request a token from the HomeWizard Energy device + * + * @param {string} address + * @returns {string|null} token or null if the button has not been pressed yet + * @throws {Error} When response is not 200 or token is not present + */ +async function requestToken(address) { + const payload = { + name: `local/homey_${Math.random().toString(16).substr(2, 6)}`, + }; + + console.log('Trying to get token...'); + + // The request... + const response = await fetch(`https:/${address}/api/user`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Api-Version': '2', + }, + body: JSON.stringify(payload), + agent: new (https.Agent)({ rejectUnauthorized: false }), + }); + + // See if we get an unauthorized response, meaning the button has not been pressed yet + if (response.status == 403) { + console.log('Button not pressed yet...'); + return null; + } + + // Some error checking + if (response.status != 200) { + throw new Error(response.statusText); + } + + const result = await response.json(); + + if (result.token === undefined) { + throw new Error('No token received'); + } + + return result.token; +} + +module.exports = class HomeWizardEnergyDriverV2 extends Homey.Driver { + + logDiscovery(status, detail = null) { + const dbg = this.homey.settings.get('debug_discovery') || {}; + + dbg.lastStatus = status; // 'ok', 'error', 'not_found' + dbg.lastDetail = detail ? String(detail) : null; + dbg.lastUpdate = new Date().toLocaleString('nl-NL', { timeZone: 'Europe/Amsterdam', hour12: false }), + + this.homey.settings.set('debug_discovery', dbg); + } + + + async onPair(session) { + + // Initialize variables to prevent undefined errors + this.interval = null; + this.timeout = null; + this.devices = []; + + // First screen, get list of devices. + session.setHandler('list_devices', async () => { + + const discoveryStrategy = this.getDiscoveryStrategy(); + const discoveryResults = discoveryStrategy.getDiscoveryResults(); + + // console.log('Discovered devices:', discoveryResults); Error: Circular Reference "device" + console.log( + '[DISCOVERY]', + Object.values(discoveryResults).map(r => ({ + id: r.id, + address: r.address, + port: r.port, + product: r.txt?.product_name, + serial: r.txt?.serial, + })) + ); + + + if (!discoveryResults || Object.keys(discoveryResults).length === 0) { + this.logDiscovery('not_found', 'No devices found via mDNS'); + + // Throw helpful error to guide users with mDNS/network issues + throw new Error(this.homey.__('pair.no_devices_found')); + } else { + this.logDiscovery('ok', `Found ${Object.keys(discoveryResults).length} devices`); + } + + + // Return list of devices, we do not test if device is reachable as we trust the discovery results + const devices = []; + for (const discoveryResult of Object.values(discoveryResults)) { + + devices.push({ + name: `${discoveryResult.txt.product_name} (${discoveryResult.txt.serial.substr(6)})`, + data: { + id: discoveryResult.txt.serial, + }, + store: { + address: discoveryResult.address, // Used for the authorize step, not _really_ needed later on + }, + }); + } + + return devices; + }); + + // Undocumented event, triggered when the user selects a device + // This is a list of devices. We only expect exactly one device to be selected, + // as enforced by the singular option in driver.compose.json + session.setHandler('list_devices_selection', async (data) => { + this.selectedDevice = data[0]; + }); + + // This event is triggered when the authorize screen is shown or when the user presses retry action + session.setHandler('try_authorize', async (duration) => { + try { + // Check if any previous timers are running and stop them + if (this.interval !== null) { + clearInterval(this.interval); + clearTimeout(this.timeout); + } + + // Try obtaining the token at intervals + this.interval = setInterval(async () => { + console.debug('Checking for button press...'); + + let token = null; + + try { + token = await requestToken(this.selectedDevice.store.address); + } + catch (error) { + console.error('Error while trying to get token: ', error); + this.logDiscovery('error', `Token request failed: ${error.message}`); + + try { + await session.emit('error', error.message); + } catch (e) { + console.error('Pair session already closed:', e.message); + } + clearInterval(this.interval); + clearTimeout(this.timeout); + return; + } + + if (token) { + clearInterval(this.interval); + clearTimeout(this.timeout); + this.selectedDevice.store.token = token; + try { + await session.emit('create', this.selectedDevice); + } catch (e) { + console.error('Pair session already closed:', e.message); + } + } + }, 2000); // Check every 2 seconds + + // Stop trying after a certain duration (use setTimeout, not setInterval) + this.timeout = setTimeout(async () => { + clearInterval(this.interval); + clearTimeout(this.timeout); + console.log('Timeout!'); + this.logDiscovery('error', 'Authorization timeout'); + + try { + await session.emit('authorize_timeout'); + } catch (e) { + console.error('Pair session already closed:', e.message); + } + }, duration); + + } catch (error) { + console.log('Pair Session Timeout error', error); + } + }); + } + + async onRepair(session, device) { + console.log('[REPAIR] Starting repair session for device:', device.getName()); + + // Get current manual IP if set + session.setHandler('get_current_ip', async () => { + const manualIP = device.getSetting('manual_ip'); + const discoveryIP = device.getStoreValue('address'); + return { + manual_ip: manualIP || '', + discovery_ip: discoveryIP || this.homey.__('repair.unknown'), + using_manual: !!manualIP + }; + }); + + // Validate and set manual IP + session.setHandler('set_manual_ip', async (data) => { + const ip = data.ip?.trim(); + + // Clear manual IP if requested + if (data.clear) { + await device.setSettings({ manual_ip: '' }); + console.log('[REPAIR] Manual IP cleared, returning to mDNS discovery'); + return { success: true }; + } + + // Validate IP format + if (!ip || !/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) { + throw new Error(this.homey.__('repair.invalid_ip')); + } + + // Test connection to device + try { + const response = await fetch(`http://${ip}/api`, { + method: 'GET', + timeout: 5000 + }); + + if (!response.ok) { + throw new Error(this.homey.__('repair.connection_failed')); + } + + const data = await response.json(); + + // Verify it's the same device by serial number + if (data.serial && data.serial !== device.getData().id) { + throw new Error(this.homey.__('repair.wrong_device')); + } + + // Save manual IP + await device.setSettings({ manual_ip: ip }); + console.log('[REPAIR] Manual IP set to:', ip); + + // Trigger device reconnection if it has the method + if (typeof device.reconnectWithManualIP === 'function') { + await device.reconnectWithManualIP(ip); + } + + return { success: true }; + + } catch (error) { + console.error('[REPAIR] Connection test failed:', error.message); + throw new Error(this.homey.__('repair.connection_failed') + ': ' + error.message); + } + }); + } + +}; diff --git a/includes/v2/Ws.js b/includes/v2/Ws.js new file mode 100644 index 00000000..432a3d26 --- /dev/null +++ b/includes/v2/Ws.js @@ -0,0 +1,843 @@ +/* + * HomeWizard WebSocket Manager + * Copyright (C) 2025 Jeroen Tebbens + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +'use strict'; + +const https = require('https'); +const WebSocket = require('ws'); +const fetchWithTimeout = require('../../includes/utils/fetchWithTimeout'); + +const SHARED_AGENT = new https.Agent({ + keepAlive: true, + keepAliveMsecs: 11000, + maxSockets: 4, + maxFreeSockets: 2, + rejectUnauthorized: false, + timeout: 10000 +}); + +/** + * WebSocketManager + * + * Manages a resilient WebSocket connection to a HomeWizard device. + * Responsibilities: + * - Open and authorize the WebSocket connection + * - Subscribe to topics: system, measurement, batteries + * - Reconnect with exponential backoff on errors/close + * - Monitor heartbeat to detect stalls and zombies + * - Throttle incoming data to prevent CPU overload + * - Expose start/stop/restart and helper checks + * + * Constructor expects callbacks and helpers from the device instance: + * - device: device reference (for optimistic setBatteryMode update) + * - url: base http(s) url of the device + * - token: bearer token for authorization + * - log, error: logging functions + * - setAvailable: mark device available + * - getSetting: read device settings + * - handleMeasurement, handleSystem, handleBatteries: data callbacks + */ +class WebSocketManager { + constructor({ device, url, token, log, error, setAvailable, getSetting, handleMeasurement, handleSystem, handleBatteries, onJournalEvent, measurementThrottleMs }) { + this.device = device; + this.url = url; + this.token = token; + this.log = log; + this.error = error; + this.setAvailable = setAvailable; + this.getSetting = getSetting; + + this._handleMeasurement = handleMeasurement; + this._handleSystem = handleSystem; + this._handleBatteries = handleBatteries; + this._onJournalEvent = onJournalEvent || null; + this._deviceId = device?.getData?.()?.id || 'unknown'; + + // WebSocket instance and state + this.ws = null; + this.wsActive = false; + this.reconnectAttempts = 0; + this.lastMeasurementAt = Date.now(); + + // Reconnect / restart guards + this.reconnecting = false; + this._restartCooldown = 0; + this._stopped = false; + + this._timers = new Set(); + this.pongReceived = true; + + // Throttle: measurements arrive every ~1s, process at most every 2s + // ✅ CPU FIX: configurable via constructor (plugin_battery uses 5s, energy_v2 uses 2s) + this._lastMeasurementProcessedAt = 0; + this._measurementThrottleMs = (typeof measurementThrottleMs === 'number' && measurementThrottleMs > 0) + ? measurementThrottleMs + : 2000; + this._pendingMeasurement = null; + this._pendingMeasurementTimer = null; + + // Throttle: system (wifi rssi etc.) — handler does capability writes + this._lastSystemProcessedAt = 0; + this._systemThrottleMs = 30000; // ✅ CPU FIX: raised from 10s to 30s — WiFi RSSI doesn't need 10s updates + + // Throttle: batteries — handler does capability writes + flow triggers + // ✅ CPU FIX: raised from 5s to 30s — HomeWizard firmware pushes batteries + // topic on EVERY measurement (1/s). Battery mode changes are rare; 30s is plenty. + this._lastBatteriesProcessedAt = 0; + this._batteriesThrottleMs = 30000; + this._pendingBatteries = null; + this._pendingBatteriesTimer = null; + + // Debug: verbose per-message logging (toggle via device setting 'ws_debug') + this._debug = false; + + // Uptime & timing + this._startedAt = Date.now(); + this._lastHandlerDurationMs = { measurement: 0, system: 0, batteries: 0 }; + this._maxHandlerDurationMs = { measurement: 0, system: 0, batteries: 0 }; + + // Reconnect rate detection (ring buffer of last 20 reconnect timestamps) + this._reconnectTimestamps = []; + + // Stats counters for getStats() + this._stats = { + messagesReceived: 0, + measurementsProcessed: 0, + measurementsDropped: 0, + systemProcessed: 0, + systemDropped: 0, + batteriesProcessed: 0, + batteriesDeferred: 0, + reconnects: 0, + lastConnectedAt: null, + lastDisconnectedAt: null, + handlerErrors: 0, + }; + } + + /** + * Write a critical event to the persistent journal. + * Called at key lifecycle points so events survive app kills. + */ + _journal(type, message) { + try { this._onJournalEvent?.(type, this._deviceId, message); } + catch (e) { /* never let journal break the WS flow */ } + } + + /** + * Throttled journal: only write one event per type per 10 minutes. + * Reduces noise for expected repeated events like preflight_fail. + */ + _journalThrottled(type, message) { + if (!this._journalThrottleMap) this._journalThrottleMap = {}; + const now = Date.now(); + const last = this._journalThrottleMap[type] || 0; + if (now - last > 600000) { + this._journal(type, message); + this._journalThrottleMap[type] = now; + } + } + + /** + * Persist current stats snapshot for post-crash diagnostics. + * Called periodically from the 30s health-check timer. + */ + _persistSnapshot() { + try { this._onJournalEvent?.('snapshot', this._deviceId, this.getStats()); } + catch (e) { /* ignore */ } + } + + _safeSetTimeout(fn, ms) { + const id = setTimeout(() => { + this._timers.delete(id); + if (this._stopped) return; + fn(); + }, ms); + this._timers.add(id); + return id; + } + + _safeSetInterval(fn, ms) { + const id = setInterval(() => { + if (this._stopped) return; + fn(); + }, ms); + this._timers.add(id); + return id; + } + + _clearTimers() { + for (const id of this._timers) { + clearTimeout(id); + clearInterval(id); + } + this._timers.clear(); + } + + /** + * Start or restart the WebSocket connection. + * Runs a preflight check to verify device reachability before connecting. + */ + async start() { + if (this._stopped) { + this.log('⚠️ WebSocket is stopped — use resume() to restart'); + return; + } + + // Skip if already connecting + if (this.ws && this.ws.readyState === WebSocket.CONNECTING) { + this.log('⏸️ WebSocket is already connecting — skipping start'); + return; + } + + // Clean up existing socket + if (this.ws) { + try { + if (this.ws.readyState === WebSocket.OPEN) this.ws.terminate(); + else this.ws.close(); + } catch (err) { + this.error('❌ Failed to clean up WebSocket:', err); + } + this.ws = null; + this.wsActive = false; + } + + // Allow URL from settings if not provided at construction + const settingsUrl = this.getSetting('url'); + if (!this.url && settingsUrl) this.url = settingsUrl; + if (!this.token || !this.url) { + this.error('❌ Missing token or URL — cannot start WebSocket'); + return; + } + + // Preflight: verify device is reachable + try { + const httpRes = await fetchWithTimeout(`${this.url}/api/system`, { + headers: { Authorization: `Bearer ${this.token}` }, + agent: SHARED_AGENT + }, 3000); + const res = httpRes.ok ? await httpRes.json() : null; + if (!res || typeof res.cloud_enabled === 'undefined') { + this.error(`❌ Device unreachable at ${this.url} — skipping WebSocket`); + this._journalThrottled('preflight_fail', `Device unreachable at ${this.url}`); + this._scheduleReconnect(); + return; + } + } catch (err) { + this.error(`❌ Preflight check failed: ${err.message}`); + this._journalThrottled('preflight_fail', err.message); + this._scheduleReconnect(); + return; + } + + const wsUrl = this.url.replace(/^http(s)?:\/\//, 'wss://') + '/api/ws'; + + // Create standard WebSocket + try { + this.ws = new WebSocket(wsUrl, { + agent: SHARED_AGENT, + perMessageDeflate: false, + maxPayload: 512 * 1024, + handshakeTimeout: 5000 + }); + } catch (err) { + this.error('❌ Failed to create WebSocket:', err); + this.wsActive = false; + return; + } + + this._safeSend = (obj) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false; + try { + const buffered = this.ws._socket?.bufferSize || this.ws.bufferedAmount || 0; + if (buffered > 512 * 1024) { + this.log(`⚠️ Skipping send — buffered ${buffered}`); + return false; + } + this.ws.send(JSON.stringify(obj)); + return true; + } catch (err) { + this.error('❌ safeSend failed:', err); + return false; + } + }; + + // ──────────────────────── open ──────────────────────── + this.ws.on('open', () => { + if (this._stopped) return; + this.wsActive = true; + this.lastMeasurementAt = Date.now(); + this.reconnectAttempts = 0; + this._stats.lastConnectedAt = new Date().toISOString(); + this.log('🔌 WebSocket opened — authorizing...'); + this._journal('open', 'WebSocket opened'); + + if (this.ws._socket) this.ws._socket.setKeepAlive(true, 30000); + + this.pongReceived = true; + + // Single 30s health-check: ping/pong + heartbeat + zombie detection + this._safeSetInterval(() => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; + + const now = Date.now(); + const idle = now - this.lastMeasurementAt; + + // Periodically persist stats snapshot (every ~5 min = every 10th tick of 30s) + if (!this._healthTick) this._healthTick = 0; + this._healthTick++; + if (this._healthTick % 10 === 0) this._persistSnapshot(); + + // No pong reply AND no data for 60s → zombie + if (!this.pongReceived && idle > 60000) { + this.log('🧨 No pong & idle — force closing zombie WebSocket'); + this._journal('zombie', `No pong & idle ${Math.round(idle / 1000)}s — terminating`); + try { this.ws.terminate(); } catch (e) {} + this.ws = null; + this.wsActive = false; + this._scheduleReconnect(); + return; + } + + // Request battery status if idle > 60s + if (idle > 60000) { + this._safeSend({ type: 'batteries' }); + } + + // No data for 3 minutes → force-close zombie (device may still respond to pings but stopped streaming) + if (idle > 180000) { + this.log(`💤 No measurement in 3min (${Math.round(idle / 1000)}s) — force closing zombie WebSocket`); + this._journal('zombie', `Idle ${Math.round(idle / 1000)}s, no measurements — force restart`); + try { this.ws.terminate(); } catch (e) {} + this.ws = null; + this.wsActive = false; + this._scheduleReconnect(); + return; + } + + this.pongReceived = false; + try { this.ws.ping(); } catch (e) { this.error('ping failed', e); } + }, 30000); + + // Authorize via message + const maxRetries = 30; + let retries = 0; + const tryAuthorize = () => { + if (this._stopped || !this.ws) return; + if (this.ws.readyState === WebSocket.OPEN) { + this.log('🔐 Sending WebSocket authorization'); + this._safeSend({ type: 'authorization', data: this.token }); + } else if (retries < maxRetries) { + retries++; + this._safeSetTimeout(tryAuthorize, 100); + } else { + this.error('❌ WebSocket failed to open after timeout — giving up'); + this.ws.terminate(); + this.wsActive = false; + } + }; + tryAuthorize(); + }); + + // ──────────────────────── pong ──────────────────────── + this.ws.on('pong', () => { + this.pongReceived = true; + // Do NOT update lastMeasurementAt here — only actual measurement data should reset the idle timer. + // Updating on pong would mask a zombie: device alive at TCP level but stopped streaming data. + }); + + // ──────────────────────── message ──────────────────────── + this.ws.on('message', (msg) => { + if (this._stopped) return; + + let data; + try { data = JSON.parse(msg.toString()); } + catch (err) { this.error('❌ Failed to parse WS message:', err); return; } + + this._stats.messagesReceived++; + if (this._debug) this.log(`[WS-DBG] type=${data.type}`); + + if (data.type === 'authorized') { + this.log('✅ WebSocket authorized'); + this._journal('authorized', 'WebSocket authorized — subscribing'); + this.lastMeasurementAt = Date.now(); + this._subscribeTopics(); + } + else if (data.type === 'measurement') { + this._onMeasurement(data.data || {}); + } + else if (data.type === 'system') { + this._onSystem(data.data || {}); + } + else if (data.type === 'batteries') { + this._onBatteries(data.data || {}); + } + }); + + // ──────────────────────── error ──────────────────────── + this.ws.on('error', (err) => { + if (this._stopped) return; + this.error(`❌ WebSocket error: ${err.code || ''} ${err.message || err}`); + this._journal('error', `${err.code || ''} ${err.message || err}`); + this.wsActive = false; + this._stats.lastDisconnectedAt = new Date().toISOString(); + this._scheduleReconnect(); + }); + + // ──────────────────────── close ──────────────────────── + this.ws.on('close', () => { + if (this._stopped) return; + this.log('🔌 WebSocket closed — retrying'); + this._journal('close', 'WebSocket closed'); + this.wsActive = false; + this._stats.lastDisconnectedAt = new Date().toISOString(); + this._scheduleReconnect(); + }); + } + + // ──────────── Throttled message handlers ──────────── + + /** + * Measurement: process immediately if throttle window passed, + * otherwise store latest and schedule a deferred flush. + */ + _onMeasurement(payload) { + const now = Date.now(); + if (now - this._lastMeasurementProcessedAt >= this._measurementThrottleMs) { + this._lastMeasurementProcessedAt = now; + this.lastMeasurementAt = now; + this._stats.measurementsProcessed++; + const t0 = Date.now(); + try { this._handleMeasurement?.(payload); } + catch (e) { this.error('❌ Measurement handler error:', e); this._stats.handlerErrors++; } + this._trackHandlerTime('measurement', Date.now() - t0); + } else { + this._stats.measurementsDropped++; + this._pendingMeasurement = payload; + if (!this._pendingMeasurementTimer) { + const remaining = this._measurementThrottleMs - (now - this._lastMeasurementProcessedAt); + this._pendingMeasurementTimer = this._safeSetTimeout(() => { + this._pendingMeasurementTimer = null; + if (this._stopped || !this._pendingMeasurement) return; + const pending = this._pendingMeasurement; + this._pendingMeasurement = null; + this._lastMeasurementProcessedAt = Date.now(); + this.lastMeasurementAt = Date.now(); + const t0 = Date.now(); + try { this._handleMeasurement?.(pending); } + catch (e) { this.error('❌ Deferred measurement error:', e); this._stats.handlerErrors++; } + this._trackHandlerTime('measurement', Date.now() - t0); + }, remaining); + } + } + } + + /** + * System: process if throttle window passed, drop intermediate. + * Only carries wifi rssi — not critical to flush. + */ + _onSystem(payload) { + const now = Date.now(); + if (now - this._lastSystemProcessedAt >= this._systemThrottleMs) { + this._lastSystemProcessedAt = now; + this._stats.systemProcessed++; + if (this._debug) this.log(`[WS-DBG] system processed`); + const t0 = Date.now(); + try { this._handleSystem?.(payload); } + catch (e) { this.error('❌ System handler error:', e); this._stats.handlerErrors++; } + this._trackHandlerTime('system', Date.now() - t0); + } else { + this._stats.systemDropped++; + } + } + + /** + * Batteries: process if throttle window passed, otherwise store + * latest and schedule a deferred flush so mode changes are not lost. + */ + _onBatteries(payload) { + const now = Date.now(); + this._pendingBatteries = payload; + if (now - this._lastBatteriesProcessedAt >= this._batteriesThrottleMs) { + this._lastBatteriesProcessedAt = now; + const bat = this._pendingBatteries; + this._pendingBatteries = null; + if (this._pendingBatteriesTimer) { + clearTimeout(this._pendingBatteriesTimer); + this._pendingBatteriesTimer = null; + } + this._stats.batteriesProcessed++; + if (this._debug) this.log(`[WS-DBG] batteries processed: mode=${bat?.mode}`); + const t0b = Date.now(); + try { this._handleBatteries?.(bat); } + catch (e) { this.error('❌ Batteries handler error:', e); this._stats.handlerErrors++; } + this._trackHandlerTime('batteries', Date.now() - t0b); + } else if (!this._pendingBatteriesTimer) { + this._stats.batteriesDeferred++; + const remaining = this._batteriesThrottleMs - (now - this._lastBatteriesProcessedAt); + this._pendingBatteriesTimer = this._safeSetTimeout(() => { + this._pendingBatteriesTimer = null; + if (this._stopped || !this._pendingBatteries) return; + const bat = this._pendingBatteries; + this._pendingBatteries = null; + this._lastBatteriesProcessedAt = Date.now(); + const t0d = Date.now(); + try { this._handleBatteries?.(bat); } + catch (e) { this.error('❌ Deferred batteries error:', e); this._stats.handlerErrors++; } + this._trackHandlerTime('batteries', Date.now() - t0d); + }, remaining); + } + } + + // ──────────── Reconnect / lifecycle ──────────── + + _scheduleReconnect() { + if (this._stopped || this.reconnecting) return; + if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { + this.log('⏸️ Reconnect suppressed — socket OPEN or CONNECTING'); + return; + } + this.reconnecting = true; + this.reconnectAttempts++; + this._stats.reconnects++; + this._reconnectTimestamps.push(Date.now()); + if (this._reconnectTimestamps.length > 20) this._reconnectTimestamps.shift(); + const base = 5000 * this.reconnectAttempts; + const delay = Math.min(base, 180000); // cap at 3 minutes + const jitter = delay * (0.9 + Math.random() * 0.2); + this.log(`🔁 WS reconnect in ${Math.round(jitter / 1000)}s`); + this._journal('reconnect', `Attempt #${this.reconnectAttempts} in ${Math.round(jitter / 1000)}s`); + this._persistSnapshot(); // snapshot before potential crash + this._safeSetTimeout(() => { + this.reconnecting = false; + if (this._stopped) return; + this.restartWebSocket(); + }, jitter); + } + + stop() { + this._stopped = true; + this._clearTimers(); + this.reconnecting = false; + if (this.ws) { + try { + if (this.ws.readyState === WebSocket.OPEN) this.ws.close(); + else this.ws.terminate(); + } catch (err) { + this.error('❌ Error closing WebSocket:', err); + } + this.ws = null; + this.wsActive = false; + } + this._pendingMeasurement = null; + this._pendingBatteries = null; + this._pendingMeasurementTimer = null; + this._pendingBatteriesTimer = null; + } + + async resume() { + if (!this._stopped) return; + this._stopped = false; + this.reconnectAttempts = 0; + this.reconnecting = false; + await this.start(); + } + + _subscribeTopics() { + ['system', 'measurement', 'batteries'].forEach(topic => { + this._safeSend({ type: 'subscribe', data: topic }); + }); + this.wsActive = true; + this.setAvailable().catch(this.error); + } + + _startHeartbeatMonitor() { + // Merged into single 30s health-check in start() — kept for API compat + } + + isConnected() { + return !this._stopped && this.ws && this.ws.readyState === WebSocket.OPEN; + } + + /** + * Return a snapshot of internal state for diagnostics. + * Call from device code: this.wsManager.getStats() + */ + getStats() { + const now = Date.now(); + return { + connected: this.isConnected(), + wsActive: this.wsActive, + stopped: this._stopped, + reconnecting: this.reconnecting, + reconnectAttempts: this.reconnectAttempts, + idleMs: now - this.lastMeasurementAt, + timersActive: this._timers.size, + throttle: { + measurement: { + lastProcessedAgo: now - this._lastMeasurementProcessedAt, + pending: !!this._pendingMeasurement, + timerActive: !!this._pendingMeasurementTimer, + }, + system: { + lastProcessedAgo: now - this._lastSystemProcessedAt, + }, + batteries: { + lastProcessedAgo: now - this._lastBatteriesProcessedAt, + pending: !!this._pendingBatteries, + timerActive: !!this._pendingBatteriesTimer, + }, + }, + counters: { ...this._stats }, + uptimeMs: now - this._startedAt, + handlerTiming: { + last: { ...this._lastHandlerDurationMs }, + max: { ...this._maxHandlerDurationMs }, + }, + reconnectRate: this._getReconnectRate(), + }; + } + + /** + * Track how long a handler callback took. + * If a handler exceeds 250ms that's a CPU warning sign. + */ + _trackHandlerTime(name, ms) { + this._lastHandlerDurationMs[name] = ms; + if (ms > this._maxHandlerDurationMs[name]) { + this._maxHandlerDurationMs[name] = ms; + } + // Log slow handlers (> 250ms) — these are CPU hogs + // Throttle: only journal once per handler per 5 min to reduce noise + if (ms > 250) { + this.log(`⚠️ Slow ${name} handler: ${ms}ms`); + if (!this._slowHandlerThrottle) this._slowHandlerThrottle = {}; + const now = Date.now(); + const lastLogged = this._slowHandlerThrottle[name] || 0; + if (now - lastLogged > 300000) { + this._journal('slow_handler', `${name} took ${ms}ms`); + this._slowHandlerThrottle[name] = now; + } + } + } + + /** + * Calculate reconnect rate from recent timestamps. + * Returns { count, windowMs, perMinute } or null if no reconnects. + */ + _getReconnectRate() { + if (this._reconnectTimestamps.length < 2) return null; + const first = this._reconnectTimestamps[0]; + const last = this._reconnectTimestamps[this._reconnectTimestamps.length - 1]; + const windowMs = last - first; + if (windowMs <= 0) return null; + const count = this._reconnectTimestamps.length; + return { + count, + windowMs, + perMinute: Math.round((count / (windowMs / 60000)) * 10) / 10, + }; + } + + /** + * Generate a plain-text diagnostic report that users can copy-paste + * and share. Designed for post-crash analysis. + */ + getDiagnosticReport() { + const stats = this.getStats(); + const now = new Date(); + const uptime = Math.round(stats.uptimeMs / 1000); + const uptimeStr = uptime > 3600 + ? `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m` + : `${Math.floor(uptime / 60)}m ${uptime % 60}s`; + + const lines = []; + lines.push(`═══ WebSocket Diagnostic Report ═══`); + lines.push(`Generated: ${now.toISOString()}`); + lines.push(`Device: ${this._deviceId}`); + lines.push(`URL: ${this.url || 'none'}`); + lines.push(`Uptime: ${uptimeStr}`); + lines.push(``); + lines.push(`── Connection ──`); + lines.push(`Connected: ${stats.connected}`); + lines.push(`WS Active: ${stats.wsActive}`); + lines.push(`Stopped: ${stats.stopped}`); + lines.push(`Reconnecting: ${stats.reconnecting}`); + lines.push(`Reconnect attempts: ${stats.reconnectAttempts}`); + lines.push(`Idle: ${Math.round(stats.idleMs / 1000)}s`); + lines.push(`Active timers: ${stats.timersActive}`); + lines.push(``); + lines.push(`── Counters ──`); + const c = stats.counters; + lines.push(`Messages received: ${c.messagesReceived}`); + lines.push(`Measurements: ${c.measurementsProcessed} processed, ${c.measurementsDropped} throttled`); + lines.push(`System: ${c.systemProcessed} processed, ${c.systemDropped} throttled`); + lines.push(`Batteries: ${c.batteriesProcessed} processed, ${c.batteriesDeferred} deferred`); + lines.push(`Reconnects: ${c.reconnects}`); + lines.push(`Handler errors: ${c.handlerErrors}`); + lines.push(`Last connected: ${c.lastConnectedAt || 'never'}`); + lines.push(`Last disconnected: ${c.lastDisconnectedAt || 'never'}`); + lines.push(``); + lines.push(`── Handler Timing (ms) ──`); + lines.push(`Last: meas=${stats.handlerTiming.last.measurement} sys=${stats.handlerTiming.last.system} bat=${stats.handlerTiming.last.batteries}`); + lines.push(`Max: meas=${stats.handlerTiming.max.measurement} sys=${stats.handlerTiming.max.system} bat=${stats.handlerTiming.max.batteries}`); + + // Anomaly detection + const anomalies = []; + if (stats.handlerTiming.max.measurement > 100) anomalies.push(`🔴 Measurement handler slow (max ${stats.handlerTiming.max.measurement}ms)`); + if (stats.handlerTiming.max.system > 100) anomalies.push(`🔴 System handler slow (max ${stats.handlerTiming.max.system}ms)`); + if (stats.handlerTiming.max.batteries > 100) anomalies.push(`🔴 Batteries handler slow (max ${stats.handlerTiming.max.batteries}ms)`); + if (c.handlerErrors > 0) anomalies.push(`🟡 ${c.handlerErrors} handler errors occurred`); + + const rate = stats.reconnectRate; + if (rate && rate.perMinute > 2) anomalies.push(`🔴 Rapid reconnects: ${rate.perMinute}/min (${rate.count} in ${Math.round(rate.windowMs / 1000)}s)`); + else if (rate && rate.perMinute > 0.5) anomalies.push(`🟡 Elevated reconnects: ${rate.perMinute}/min`); + + if (c.reconnects > 10 && uptime < 600) anomalies.push(`🔴 ${c.reconnects} reconnects in ${uptimeStr} — reconnect storm`); + if (stats.idleMs > 180000 && stats.connected) anomalies.push(`🟡 Connected but idle for ${Math.round(stats.idleMs / 1000)}s — stale connection?`); + + const msgRate = uptime > 0 ? (c.messagesReceived / uptime) : 0; + if (msgRate > 5) anomalies.push(`🟡 High message rate: ${msgRate.toFixed(1)}/s`); + + if (anomalies.length > 0) { + lines.push(``); + lines.push(`── ⚠ Anomalies Detected ──`); + anomalies.forEach(a => lines.push(a)); + } else { + lines.push(``); + lines.push(`── ✅ No anomalies detected ──`); + } + + lines.push(``); + lines.push(`── Throttle ──`); + lines.push(`Measurement: last ${Math.round(stats.throttle.measurement.lastProcessedAgo / 1000)}s ago, pending=${stats.throttle.measurement.pending}`); + lines.push(`System: last ${Math.round(stats.throttle.system.lastProcessedAgo / 1000)}s ago`); + lines.push(`Batteries: last ${Math.round(stats.throttle.batteries.lastProcessedAgo / 1000)}s ago, pending=${stats.throttle.batteries.pending}`); + + return lines.join('\n'); + } + + /** + * Enable or disable verbose debug logging at runtime. + */ + setDebug(enabled) { + this._debug = !!enabled; + this.log(`🔧 WS debug ${this._debug ? 'ON' : 'OFF'}`); + } + + restartWebSocket() { + if (this._stopped) return; + if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) { + this.log('⏸️ Socket is OPEN or CONNECTING — skipping restart'); + return; + } + + const now = Date.now(); + if (now - this._restartCooldown < 3000) { + this.log('⏸️ Skipping restart — cooldown active'); + return; + } + + this._restartCooldown = now; + this._clearTimers(); + this._resetWebSocket(); + this.start(); + } + + _resetWebSocket() { + if (!this.ws) return; + const state = this.ws.readyState; + if (state === WebSocket.CONNECTING) { + this.log('⏸️ WebSocket is still connecting — skipping termination'); + return; + } + try { + if (state === WebSocket.OPEN) { + this.log('🔄 Terminating active WebSocket'); + this.ws.terminate(); + } else { + this.log('🔄 Closing inactive WebSocket'); + this.ws.close(); + } + } catch (err) { + this.error('❌ Failed to reset WebSocket:', err); + } + this.ws = null; + this.wsActive = false; + } + + // ──────────── Battery control ──────────── + + setBatteryMode(mode) { + if (!this.isConnected()) { + const errMsg = `❌ Cannot set battery mode to "${mode}" — WebSocket not connected`; + this.error(errMsg); + throw new Error(errMsg); + } + + let payloadData; + switch (mode) { + case 'standby': + payloadData = { mode: 'standby', permissions: [] }; + break; + case 'zero': + payloadData = { mode: 'zero', permissions: ['charge_allowed', 'discharge_allowed'] }; + break; + case 'zero_charge_only': + payloadData = { mode: 'zero', permissions: ['charge_allowed'] }; + break; + case 'zero_discharge_only': + payloadData = { mode: 'zero', permissions: ['discharge_allowed'] }; + break; + case 'to_full': + payloadData = { mode: 'to_full' }; + break; + default: + this.error(`❌ Unknown battery mode: "${mode}"`); + throw new Error(`Unknown battery mode: "${mode}"`); + } + + const payload = { type: 'batteries', data: { ...payloadData } }; + this.log(`🔋 WS → setBatteryMode("${mode}")`); + this._journal('mode_change', `setBatteryMode("${mode}")`); + + try { + this._safeSend(payload); + this.log('✅ Battery mode command sent'); + // ✅ CPU FIX: Optimistic update via setImmediate to avoid blocking the send path. + // Previously called _handleBatteries synchronously which triggered capability + // writes + setSettings() in the same tick as the WS send. + setImmediate(() => { + if (!this._stopped) { + this.device?._handleBatteries?.(payload.data); + } + }); + } catch (err) { + this.error(`❌ Failed to send battery mode command: ${err.message}`); + throw err; + } + } + + requestBatteryStatus() { + if (!this.isConnected()) { + this.log('⚠️ Cannot request battery status — WebSocket not connected'); + return; + } + this.log('🔋 Requesting battery status via WebSocket'); + this._safeSend({ type: 'batteries' }); + } + + setCloud(enabled) { + if (!this.isConnected()) throw new Error('WebSocket not connected'); + this._safeSend({ type: 'system', data: { cloud_enabled: enabled } }); + } +} + +module.exports = WebSocketManager; \ No newline at end of file diff --git a/includes/v2/wsDebug.js b/includes/v2/wsDebug.js new file mode 100644 index 00000000..1754a025 --- /dev/null +++ b/includes/v2/wsDebug.js @@ -0,0 +1,128 @@ +'use strict'; + +/** + * wsDebug — Crash-resilient WebSocket diagnostics + * + * Writes critical events + periodic stats snapshots to Homey persistent + * settings so they survive app kills (CPU/memory limit, SIGKILL). + * + * Settings keys: + * ws_journal – ring buffer of last 50 critical events + * ws_snapshots – last stats snapshot per device (keyed by deviceId) + * + * The old 'debug_ws' key is still written for backward compatibility + * with existing settings page code. + */ + +let HomeyRef = null; + +// Throttle settings writes: at most once per 5 seconds per key +const _lastWrite = {}; +const MIN_WRITE_INTERVAL_MS = 5000; + +function _ts() { + return new Date().toLocaleString('nl-NL', { + timeZone: 'Europe/Amsterdam', + hour12: false, + }); +} + +module.exports = { + + init(homeyInstance) { + HomeyRef = homeyInstance; + }, + + /** + * Log a critical event to the persistent journal. + * Only call for events that matter: connect, disconnect, error, + * reconnect, mode_change, preflight_fail — NOT every message. + */ + log(type, deviceId, message) { + if (!HomeyRef) return; + + try { + const entry = { ts: _ts(), type, deviceId, message }; + + const stored = HomeyRef.settings.get('ws_journal'); + const journal = Array.isArray(stored) ? stored : []; + journal.push(entry); + + // Keep last 50 entries + const trimmed = journal.slice(-50); + + // Throttle: don't write more than once per 5s + const now = Date.now(); + if (!_lastWrite.journal || now - _lastWrite.journal >= MIN_WRITE_INTERVAL_MS) { + _lastWrite.journal = now; + HomeyRef.settings.set('ws_journal', trimmed); + // Backward compat: also write to old key + HomeyRef.settings.set('debug_ws', trimmed); + } + } catch (err) { + console.error('wsDebug.log failed:', err.message); + } + }, + + /** + * Persist a stats snapshot for a device. + * Called periodically (every ~5 min) from Ws.js. + * Survives app crash — the last snapshot before the kill is readable + * from the settings page. + */ + snapshot(deviceId, stats) { + if (!HomeyRef || !deviceId) return; + + try { + const now = Date.now(); + // Throttle snapshots to once per 30s per device + const key = `snap_${deviceId}`; + if (_lastWrite[key] && now - _lastWrite[key] < 30000) return; + _lastWrite[key] = now; + + const stored = HomeyRef.settings.get('ws_snapshots') || {}; + stored[deviceId] = { + ts: _ts(), + timestamp: now, + ...stats, + }; + HomeyRef.settings.set('ws_snapshots', stored); + } catch (err) { + console.error('wsDebug.snapshot failed:', err.message); + } + }, + + /** + * Read the full journal (for settings page or programmatic access). + */ + getJournal() { + if (!HomeyRef) return []; + try { + return HomeyRef.settings.get('ws_journal') || []; + } catch { return []; } + }, + + /** + * Read all device snapshots. + */ + getSnapshots() { + if (!HomeyRef) return {}; + try { + return HomeyRef.settings.get('ws_snapshots') || {}; + } catch { return {}; } + }, + + /** + * Clear journal + snapshots (e.g. from settings page button). + */ + clear() { + if (!HomeyRef) return; + try { + HomeyRef.settings.set('ws_journal', []); + HomeyRef.settings.set('ws_snapshots', {}); + HomeyRef.settings.set('debug_ws', []); + } catch (err) { + console.error('wsDebug.clear failed:', err.message); + } + }, +}; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 00000000..89747b43 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "checkJs": false, + "allowJs": true, + "noEmit": true, + "strict": false, // disables strict type checks + "baseUrl": ".", // enables relative imports + "types": ["node"], + "sourceMap": true + }, + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/launch.json b/launch.json new file mode 100644 index 00000000..ed43b0cf --- /dev/null +++ b/launch.json @@ -0,0 +1,10 @@ +{ + "type": "node", + "request": "attach", + "restart": true, + "name": "Attach HA to Homey Pro LAN", + "address": "192.168.1.12", + "port": 9225, + "localRoot": "${workspaceFolder}", + "remoteRoot": "/app/" +} \ No newline at end of file diff --git a/lib/battery-chart-generator.js b/lib/battery-chart-generator.js new file mode 100644 index 00000000..a54c9586 --- /dev/null +++ b/lib/battery-chart-generator.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Battery Policy Chart Generator (Placeholder) + * Chart generation disabled - users should use the HTML planning view in app settings + * See HomeWizard app settings → Battery Planning tab + */ +class BatteryChartGenerator { + constructor(homey) { + this.homey = homey; + this.enabled = false; + this.homey.log('📊 Chart generation: Use Battery Planning view in app settings (Settings → Battery Planning tab)'); + } + + /** + * Generate chart - returns null (feature disabled) + * @returns {null} + */ + generateChart(data) { + // Chart generation not available - users should access Battery Planning via app settings + return null; + } +} + +module.exports = BatteryChartGenerator; diff --git a/lib/efficiency-estimator.js b/lib/efficiency-estimator.js new file mode 100644 index 00000000..1068db60 --- /dev/null +++ b/lib/efficiency-estimator.js @@ -0,0 +1,329 @@ +'use strict'; + +class EfficiencyEstimator { + constructor(homey) { + this.homey = homey; + + this.state = this.homey.settings.get('efficiency_state') || { + totalChargeKwh: 0, + totalDischargeKwh: 0, + efficiency: 0.78, // Default 78% RTE (conservative start, learning engine corrects this) + lastTimestamp: null, + lastSoc: null, + // Per-cycle metadata for insight analysis + chargePowerSum: 0, + chargePowerSamples: 0, + dischargePowerSum: 0, + dischargePowerSamples: 0, + cycles: [] // last 60 completed cycles + }; + } + + save() { + this.homey.settings.set('efficiency_state', this.state); + } + + update(p1, battery, activeMode = null) { + if (!p1 || !battery) return; + + const now = Date.now(); + + if (!this.state.lastTimestamp) { + this.state.lastTimestamp = now; + this.state.lastSoc = battery.stateOfCharge ?? null; + return; + } + + const dtHours = (now - this.state.lastTimestamp) / 3600000; + this.state.lastTimestamp = now; + + if (dtHours <= 0 || dtHours > 1) return; + + const grid = p1.gridPower ?? 0; + const power = battery.battery_power ?? 0; + const soc = battery.stateOfCharge ?? null; + + if (Math.abs(power) <= 100) return; // filter inverter standby draw (~50-80W) that doesn't charge cells + + // ✅ FIX: Reset counters on SoC=0 but PRESERVE learned efficiency + // Hysteresis: only trigger if SoC drops from >2% to 0% (prevents glitches) + if (soc === 0 && this.state.lastSoc > 2) { + this.homey.log( + `[Efficiency] SoC dropped to 0% (from ${this.state.lastSoc}%) → ` + + `clearing charge/discharge counters (preserving learned RTE ${(this.state.efficiency * 100).toFixed(1)}%)` + ); + this.state.totalChargeKwh = 0; + this.state.totalDischargeKwh = 0; + // ✅ DO NOT reset efficiency - only reset counters for new cycle + this.state.lastSoc = soc; + this.save(); + return; + } + + // ✅ Battery-side RTE: use battery_power directly + let accumulated = false; + if (power > 100) { + const chargeKw = power / 1000; + // If we're starting a fresh charge from near-empty but have orphaned discharge + // data from a previous cycle (no paired charge), clear it to avoid cross-cycle + // contamination (e.g. 1100Wh discharge / 1000Wh charge → bogus 110% RTE). + // NOTE: soc must be a real number (not null) for this guard to fire. + if (this.state.totalDischargeKwh > 0 && this.state.totalChargeKwh === 0 + && typeof soc === 'number' && soc <= 5) { + this.homey.log(`[Efficiency] Clearing orphaned discharge data (${(this.state.totalDischargeKwh * 1000).toFixed(0)}Wh) from previous cycle`); + this.state.totalDischargeKwh = 0; + } + this.state.totalChargeKwh += chargeKw * dtHours; + this.state.chargePowerSum = (this.state.chargePowerSum || 0) + power; + this.state.chargePowerSamples = (this.state.chargePowerSamples || 0) + 1; + // Track dominant charge mode (by energy weight: more Wh in a mode = more votes) + if (activeMode) { + this.state.chargeModeVotes = this.state.chargeModeVotes || {}; + this.state.chargeModeVotes[activeMode] = (this.state.chargeModeVotes[activeMode] || 0) + (chargeKw * dtHours); + } + accumulated = true; + } + + if (power < -100) { + const dischargeKw = Math.abs(power) / 1000; + this.state.totalDischargeKwh += dischargeKw * dtHours; + this.state.dischargePowerSum = (this.state.dischargePowerSum || 0) + Math.abs(power); + this.state.dischargePowerSamples = (this.state.dischargePowerSamples || 0) + 1; + accumulated = true; + } + + // Persist counters periodically so a restart doesn't lose partial-cycle progress. + // ~1 min cadence (every 4th update at 15s polling interval) to avoid excess writes. + if (accumulated) { + this._saveCounter = (this._saveCounter || 0) + 1; + if (this._saveCounter % 4 === 0) this.save(); + } + + this.state.lastSoc = soc; + + // Learn after enough data (≥1.0 kWh each way — ~75min at 800W) + if ( + this.state.totalChargeKwh >= 1.0 && + this.state.totalDischargeKwh >= 1.0 + ) { + // Balance check: prevent partial-cycle bias where PV charges much more + // than the discharge window captured (e.g. 1.5kWh charge / 1.0kWh discharge = 67% RTE). + // Threshold 1.45: allows measurements down to 1/1.45 = 69% true RTE, + // consistent with the 70% floor below. Filters out badly skewed windows + // where charge far outpaces discharge (charge:discharge > 1.45:1). + const balanceRatio = this.state.totalChargeKwh / this.state.totalDischargeKwh; + if (balanceRatio > 1.45) { + // Discharge hasn't caught up yet — wait for more discharge before measuring + return; + } + + const newEff = + this.state.totalDischargeKwh / this.state.totalChargeKwh; + + const oldEff = this.state.efficiency; + + if (newEff >= 0.70 && newEff <= 0.97) { + // Valid RTE for LFP battery (AC-AC typically 85-95%, allow 70-97% window) + this.state.efficiency = (oldEff * 0.95) + (newEff * 0.05); + + const avgChargePower = this.state.chargePowerSamples > 0 + ? Math.round(this.state.chargePowerSum / this.state.chargePowerSamples) : 0; + const avgDischargePower = this.state.dischargePowerSamples > 0 + ? Math.round(this.state.dischargePowerSum / this.state.dischargePowerSamples) : 0; + + // Dominant charge mode = mode with most kWh during this cycle + const votes = this.state.chargeModeVotes || {}; + const dominantMode = Object.keys(votes).sort((a, b) => votes[b] - votes[a])[0] || null; + + const chargedWh = (this.state.totalChargeKwh * 1000).toFixed(0); + const dischargedWh = (this.state.totalDischargeKwh * 1000).toFixed(0); + this.homey.log( + `[Efficiency] ✅ Learning cycle complete: ` + + `charged=${chargedWh}Wh @ avg ${avgChargePower}W, ` + + `discharged=${dischargedWh}Wh @ avg ${avgDischargePower}W, ` + + `measured=${(newEff * 100).toFixed(1)}%, ` + + `learned RTE: ${(oldEff * 100).toFixed(1)}% → ${(this.state.efficiency * 100).toFixed(1)}%` + ); + + // Store cycle for insight analysis + this.state.cycles = this.state.cycles || []; + this.state.cycles.push({ + rte: newEff, + avgChargePower, + avgDischargePower, + chargedWh: Math.round(this.state.totalChargeKwh * 1000), + mode: dominantMode, + month: new Date().getMonth() + 1, + ts: Date.now() + }); + if (this.state.cycles.length > 60) { + this.state.cycles = this.state.cycles.slice(-60); + } + + // Log insights directly after each cycle (once enough data) + const insights = this.getEfficiencyInsights(); + if (insights) { + const m = insights.rteByMode; + this.homey.log( + `[Efficiency] 📊 Modus-vergelijking: ` + + Object.entries(m).map(([k, v]) => `${k}=${v.rte}% (${v.n}x)`).join(', ') + ); + this.homey.log(`[Efficiency] 💡 ${insights.recommendation}`); + } + + this.state.totalChargeKwh = 0; + this.state.totalDischargeKwh = 0; + this.state.chargePowerSum = 0; + this.state.chargePowerSamples = 0; + this.state.dischargePowerSum = 0; + this.state.dischargePowerSamples = 0; + this.state.chargeModeVotes = {}; + + } else if (newEff > 0.97) { + // Unrealistically high for LFP (>97% AC-AC is physically impossible) — discard + this.homey.log( + `[Efficiency] ⚠️ RTE measurement ${(newEff * 100).toFixed(1)}% too high (>97% impossible for AC-AC) → ` + + `discarding cycle (charged=${(this.state.totalChargeKwh * 1000).toFixed(0)}Wh, ` + + `discharged=${(this.state.totalDischargeKwh * 1000).toFixed(0)}Wh)` + ); + this.state.totalChargeKwh = 0; + this.state.totalDischargeKwh = 0; + } else if (newEff < 0.70 && this.state.totalChargeKwh > 2.0) { + // Implausibly low for LFP (<70% AC-AC) — likely partial cycle or cross-cycle contamination, discard + this.homey.log( + `[Efficiency] ⚠️ RTE measurement ${(newEff * 100).toFixed(1)}% implausibly low for LFP after ${(this.state.totalChargeKwh * 1000).toFixed(0)}Wh → discarding` + ); + this.state.totalChargeKwh = 0; + this.state.totalDischargeKwh = 0; + } else if (this.state.totalChargeKwh > 10.0 || this.state.totalDischargeKwh > 10.0) { + // Counters very stale (>10 kWh) without valid result — prevent unbounded growth + this.homey.log( + `[Efficiency] ⚠️ RTE counters stale (charge=${this.state.totalChargeKwh.toFixed(1)}kWh, ` + + `discharge=${this.state.totalDischargeKwh.toFixed(1)}kWh) → resetting` + ); + this.state.totalChargeKwh = 0; + this.state.totalDischargeKwh = 0; + } + // else: still accumulating (ratio not yet in valid range) + + this.save(); + } + } + + getEfficiency() { + return this.state.efficiency ?? 0.78; + } + + /** + * Update RTE directly from cumulative hardware meter values. + * More accurate than cycle-based estimation — uses authoritative import/export kWh. + * Only updates if both values are large enough (>20 kWh) for statistical stability. + */ + updateFromMeters(totalImportKwh, totalExportKwh) { + if (!totalImportKwh || !totalExportKwh) return; + if (totalImportKwh < 20 || totalExportKwh < 20) return; // not enough data yet + + const meterRte = totalExportKwh / totalImportKwh; + if (meterRte < 0.50 || meterRte > 0.99) return; // sanity check + + const oldEff = this.state.efficiency; + if (Math.abs(meterRte - oldEff) > 0.001) { + this.state.efficiency = meterRte; + this.save(); + this.homey.log( + `[Efficiency] 📊 Meter-based RTE: import=${totalImportKwh.toFixed(1)}kWh, ` + + `export=${totalExportKwh.toFixed(1)}kWh → ` + + `${(oldEff * 100).toFixed(1)}% → ${(meterRte * 100).toFixed(1)}%` + ); + return meterRte; // signal caller that value changed + } + return null; // no change + } + + // ✅ Reset to configured value + reset(configuredEff = 0.78) { + this.homey.log( + `[Efficiency] Manual reset: ${(this.state.efficiency * 100).toFixed(1)}% → ${(configuredEff * 100).toFixed(0)}%` + ); + this.state.totalChargeKwh = 0; + this.state.totalDischargeKwh = 0; + this.state.efficiency = configuredEff; + this.save(); + } + /** + * Analyse of lager laadvermogen betere RTE geeft. + * Geeft inzicht per vermogensbucket en seizoen. + */ + getEfficiencyInsights() { + const cycles = this.state.cycles || []; + if (cycles.length < 5) return null; + + // Bucket cycles by average charge power: low (<300W), mid (300-600W), high (>600W) + const buckets = { low: [], mid: [], high: [] }; + for (const c of cycles) { + if (c.avgChargePower < 300) buckets.low.push(c.rte); + else if (c.avgChargePower < 600) buckets.mid.push(c.rte); + else buckets.high.push(c.rte); + } + + const avg = arr => arr.length ? arr.reduce((s, v) => s + v, 0) / arr.length : null; + + const rteByPower = { + low: avg(buckets.low) ? { rte: +(avg(buckets.low) * 100).toFixed(1), n: buckets.low.length } : null, + mid: avg(buckets.mid) ? { rte: +(avg(buckets.mid) * 100).toFixed(1), n: buckets.mid.length } : null, + high: avg(buckets.high) ? { rte: +(avg(buckets.high) * 100).toFixed(1), n: buckets.high.length } : null, + }; + + // Seasonal: group by month (winter=nov-feb, spring=mar-may, summer=jun-aug, autumn=sep-oct) + const season = m => m <= 2 || m === 12 ? 'winter' : m <= 5 ? 'spring' : m <= 8 ? 'summer' : 'autumn'; + const bySeasonMap = {}; + for (const c of cycles) { + const s = season(c.month); + bySeasonMap[s] = bySeasonMap[s] || []; + bySeasonMap[s].push(c.rte); + } + const rteBySeason = {}; + for (const [s, arr] of Object.entries(bySeasonMap)) { + rteBySeason[s] = { rte: +(avg(arr) * 100).toFixed(1), n: arr.length }; + } + + // RTE per charge mode (zero_charge_only, to_full, standby, etc.) + const byMode = {}; + for (const c of cycles) { + if (!c.mode) continue; + byMode[c.mode] = byMode[c.mode] || []; + byMode[c.mode].push(c.rte); + } + const rteByMode = {}; + for (const [mode, arr] of Object.entries(byMode)) { + rteByMode[mode] = { rte: +(avg(arr) * 100).toFixed(1), n: arr.length }; + } + + // Recommendation: mode-based first, power-based as fallback + let recommendation = null; + const zco = rteByMode['zero_charge_only']; + const full = rteByMode['to_full']; + if (zco && full && zco.n >= 3 && full.n >= 3) { + const diff = zco.rte - full.rte; + if (diff > 1.5) { + recommendation = `zero_charge_only geeft ${diff.toFixed(1)}% hogere RTE dan to_full (${zco.rte}% vs ${full.rte}%). PV-overflow laden is efficiënter — prefereer dit boven vol nettarief laden.`; + } else if (diff < -1.5) { + recommendation = `to_full geeft ${(-diff).toFixed(1)}% hogere RTE dan zero_charge_only (${full.rte}% vs ${zco.rte}%). Geen voordeel bij langzamer laden in jouw situatie.`; + } else { + recommendation = `zero_charge_only (${zco.rte}%) en to_full (${full.rte}%) geven vergelijkbare RTE — laadstrategie heeft weinig effect op efficiëntie.`; + } + } else { + const lowRte = avg(buckets.low); + const highRte = avg(buckets.high); + if (lowRte && highRte && (lowRte - highRte) > 0.02) { + recommendation = `Lager laadvermogen (<300W) geeft ${((lowRte - highRte) * 100).toFixed(1)}% hogere RTE. Nog onvoldoende modus-data (zco=${zco?.n || 0}x, full=${full?.n || 0}x).`; + } else { + recommendation = `Nog onvoldoende data voor modus-vergelijking (zco=${zco?.n || 0}x, full=${full?.n || 0}x — minimaal 3x elk nodig).`; + } + } + + return { rteByPower, rteByMode, rteBySeason, recommendation, cycleCount: cycles.length }; + } +} + +module.exports = EfficiencyEstimator; \ No newline at end of file diff --git a/lib/energyzero-provider.js b/lib/energyzero-provider.js new file mode 100644 index 00000000..2a3e15d5 --- /dev/null +++ b/lib/energyzero-provider.js @@ -0,0 +1,260 @@ +'use strict'; + +const fetchWithTimeout = require('../includes/utils/fetchWithTimeout'); + +/** + * EnergyZero Day-Ahead Prices Provider + * + * FIX: VAT/markup order was wrong: + * WRONG: (spot × 1.21) + markup → misses VAT on markup (€0.023/kWh too low!) + * CORRECT: (spot + markup) × 1.21 → matches Xadi exactly + * + * This explains why EnergyZero "looked off" vs Xadi - it was consistently + * €0.023/kWh lower (= markup × VAT = 0.11 × 0.21). + */ +class EnergyZeroProvider { + constructor(homey, options = {}) { + this.homey = homey; + this.cache = null; + this.cacheExpiry = null; + this.log = homey.log.bind(homey); + this.error = homey.error.bind(homey); + + this.markup = options.markup || 0.11; + this.log(`EnergyZero provider initialized with markup: €${this.markup}/kWh`); + + this._loadCache(); + } + + async _loadCache() { + try { + const cached = await this.homey.settings.get('energyzero_cache'); + if (cached && cached.expiry > Date.now()) { + this.cache = cached.prices.map(p => ({ + ...p, + timestamp: new Date(p.timestamp) + })); + this.cacheExpiry = cached.expiry; + this.log(`Loaded ${this.cache.length} cached prices from storage (expires in ${Math.round((this.cacheExpiry - Date.now()) / 60000)}min)`); + } + } catch (error) { + this.log('Failed to load cached prices:', error.message); + } + } + + async _saveCache() { + try { + await this.homey.settings.set('energyzero_cache', { + prices: this.cache, + expiry: this.cacheExpiry, + savedAt: Date.now() + }); + } catch (error) { + this.error('Failed to save price cache:', error.message); + } + } + + async fetchPrices() { + if (this.cache && this.cacheExpiry > Date.now()) { + this.log('Using cached EnergyZero prices'); + return this.cache; + } + + const now = new Date(); + + const testDate = now.toLocaleString('en-US', { + timeZone: 'Europe/Amsterdam', + timeZoneName: 'short' + }); + const isDST = testDate.includes('CEST'); + const offsetHours = isDST ? 2 : 1; + + const cetDateStr = now.toLocaleString('en-CA', { + timeZone: 'Europe/Amsterdam', + year: 'numeric', + month: '2-digit', + day: '2-digit' + }).split(',')[0]; + + const [year, month, day] = cetDateStr.split('-').map(Number); + + const fromDateObj = new Date(Date.UTC(year, month - 1, day - 1, 24 - offsetHours, 0, 0)); + const tillDateObj = new Date(fromDateObj.getTime() + 48 * 60 * 60 * 1000); + + const fromDate = fromDateObj.toISOString().split('T')[0]; + const tillDate = tillDateObj.toISOString().split('T')[0]; + + // inclBtw=false: API returns raw spot prices (despite the param name being confusing) + // We apply VAT ourselves in the correct order: (spot + markup) × 1.21 + const url = `https://api.energyzero.nl/v1/energyprices?fromDate=${fromDate}T00:00:00.000Z&tillDate=${tillDate}T00:00:00.000Z&interval=4&usageType=1&inclBtw=false`; + + this.log(`Fetching EnergyZero prices for CET dates ${fromDate} to ${tillDate}`); + + try { + const response = await fetchWithTimeout(url, { + headers: { 'Accept': 'application/json' } + }, 10000); + + if (!response.ok) { + throw new Error(`EnergyZero API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + + if (!data.Prices || !Array.isArray(data.Prices)) { + throw new Error('Invalid response format from EnergyZero API'); + } + + this.cache = data.Prices.map((item, index) => { + const timestamp = new Date(item.readingDate); + const spot = item.price; // Raw spot price, ex VAT, ex markup + + // FIX: Correct order matches how energy suppliers actually charge: + // VAT applies to the total (spot + markup), not just the spot price + // This matches Xadi's formula exactly: (spot + markup) × 1.21 + const finalPrice = (spot + this.markup) * 1.21; + + if (index === 0) { + this.log(`EZ price calc: (€${spot.toFixed(5)} spot + €${this.markup} markup) × 1.21 VAT = €${finalPrice.toFixed(5)}`); + } + + const cetHour = parseInt(timestamp.toLocaleString('en-US', { + timeZone: 'Europe/Amsterdam', + hour: '2-digit', + hour12: false + })); + + return { + timestamp, + price: finalPrice, + originalPrice: spot, + hour: cetHour, + readingDate: item.readingDate + }; + }).sort((a, b) => a.timestamp - b.timestamp); + + this.cacheExpiry = Date.now() + 60 * 60 * 1000; + await this._saveCache(); + + this.log(`Fetched ${this.cache.length} hourly prices from EnergyZero`); + + if (this.cache.length > 0) { + const first = this.cache[0]; + const last = this.cache[this.cache.length - 1]; + this.log(`Price range: ${first.timestamp.toISOString()} (${first.hour}:00 CET, €${first.price.toFixed(4)}) to ${last.timestamp.toISOString()} (${last.hour}:00 CET, €${last.price.toFixed(4)})`); + } + + return this.cache; + + } catch (error) { + this.error('Failed to fetch EnergyZero prices:', error); + if (this.cache) { + this.log('Returning stale cache due to fetch error'); + return this.cache; + } + throw error; + } + } + + getCurrentRate() { + if (!this.cache || this.cache.length === 0) return 'standard'; + + const now = new Date(); + let currentPrice = this.cache.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + const end = new Date(start.getTime() + 3600 * 1000); + return now >= start && now < end; + }); + + if (!currentPrice) { + const currentHourCET = parseInt(now.toLocaleString('en-US', { + timeZone: 'Europe/Amsterdam', + hour: '2-digit', + hour12: false + })); + currentPrice = this.cache.find(p => p.hour === currentHourCET); + } + + if (!currentPrice) currentPrice = this.cache[0]; + + const prices = this.cache.map(p => p.price); + const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length; + const stdDev = this._calculateStdDev(prices, avgPrice); + const price = currentPrice.price; + + if (price <= avgPrice - stdDev * 0.5) return 'low'; + if (price >= avgPrice + stdDev * 0.5) return 'peak'; + return 'standard'; + } + + getNextRateChange() { + if (!this.cache || this.cache.length === 0) return null; + const now = new Date(); + const nextHour = this.cache.find(p => { + const timestamp = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + return timestamp > now; + }); + return nextHour ? nextHour.timestamp : null; + } + + getCurrentPrice() { + if (!this.cache || this.cache.length === 0) return null; + const now = new Date(); + const currentPrice = this.cache.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + const end = new Date(start.getTime() + 3600 * 1000); + return now >= start && now < end; + }); + return currentPrice ? currentPrice.price : null; + } + + getPriceStatistics() { + if (!this.cache || this.cache.length === 0) return { avg: null, min: null, max: null, stdDev: null }; + const prices = this.cache.map(p => p.price); + const avg = prices.reduce((a, b) => a + b, 0) / prices.length; + return { + avg, + min: Math.min(...prices), + max: Math.max(...prices), + stdDev: this._calculateStdDev(prices, avg) + }; + } + + getTop3Cheapest() { + if (!this.cache || this.cache.length === 0) return []; + return [...this.cache] + .sort((a, b) => a.price - b.price) + .slice(0, 3) + .map(p => ({ hour: p.hour, price: p.price, timestamp: p.timestamp })); + } + + getTop3MostExpensive() { + if (!this.cache || this.cache.length === 0) return []; + return [...this.cache] + .sort((a, b) => b.price - a.price) + .slice(0, 3) + .map(p => ({ hour: p.hour, price: p.price, timestamp: p.timestamp })); + } + + _calculateStdDev(values, mean) { + const squareDiffs = values.map(value => Math.pow(value - mean, 2)); + return Math.sqrt(squareDiffs.reduce((a, b) => a + b, 0) / values.length); + } + + hasPrices() { + return this.cache && this.cache.length > 0; + } + + getAllHourlyPrices() { + if (!this.cache || this.cache.length === 0) return []; + const now = new Date(); + return this.cache.map(p => ({ + hour: p.hour, + index: Math.floor((p.timestamp - now) / (1000 * 60 * 60)), + price: p.price, + timestamp: p.timestamp + })); + } +} + +module.exports = EnergyZeroProvider; \ No newline at end of file diff --git a/lib/explainability-engine.js b/lib/explainability-engine.js new file mode 100644 index 00000000..cd1f8090 --- /dev/null +++ b/lib/explainability-engine.js @@ -0,0 +1,1086 @@ +'use strict'; + +class ExplainabilityEngine { + constructor(homey) { + this.homey = homey; + this.log = homey.log.bind(homey); + } + + generateExplanation(recommendation, inputs, scores) { + const policyMode = recommendation.policyMode || recommendation.mode; + const hwMode = recommendation.hwMode || null; + + const normalized = { + policyMode, + hwMode, + confidence: recommendation.confidence + }; + + const reasons = []; + const warnings = []; + + const settings = inputs.settings || {}; + const tariffType = settings.tariff_type || 'fixed'; + + // Ensure explanation uses the same price as the planning view + const providerPrice = inputs.priceProvider?.getPriceForTimestamp(inputs.time)?.price; + inputs.effectivePrice = providerPrice ?? inputs.tariff?.currentPrice ?? null; + + // Mirror policy-engine: cap at configured & 0.95 to prevent drift, but use learned if lower + const configuredEff = settings.battery_efficiency || 0.75; + const learnedEff = inputs.batteryEfficiency ?? configuredEff; + inputs.batteryEfficiency = Math.min(configuredEff, learnedEff, 0.95); + + // CORE REASONS + this._addBatteryReasons(reasons, warnings, inputs.battery, settings, tariffType, inputs); + this._addLoadAwarenessReasons(reasons, inputs); + this._addTariffReasons(reasons, inputs.tariff, settings, tariffType, inputs); + this._addPVReasons(reasons, inputs); + this._addArbitrageReasons(reasons, inputs); + this._addDelayChargeReasons(reasons, inputs, normalized); + + if (tariffType === 'dynamic') { + this._addWeatherReasons(reasons, inputs.weather); + this._addSunForecastReasons(reasons, inputs); + this._addEfficiencyReasons(reasons, inputs); + } + + this._addPeakShavingReasons(reasons, inputs); + this._addTimeReasons(reasons, inputs.time); + this._addModeSpecificReasons(reasons, inputs, normalized); + this._addConfidenceReason(reasons, recommendation, inputs); + + // Score-reasons explain WHY policyMode won (charge/discharge/preserve). + // hwMode may differ due to mapping constraints (e.g. charge → standby when price too high). + // Filter on policyMode so score-reasons stay visible, then prepend mapping explanation. + const MODE_SUPPORTED = { + zero_charge_only: 'charge', to_full: 'charge', + zero_discharge_only: 'discharge', + standby: 'preserve', zero: 'preserve', preserve: 'preserve' + }; + const policyCategory = normalized.policyMode; // 'charge' | 'discharge' | 'preserve' + const hwCategory = MODE_SUPPORTED[normalized.hwMode] ?? null; + + const mappingReason = this._buildMappingReason(normalized, inputs, scores); + if (mappingReason) reasons.unshift(mappingReason); + + const filteredReasons = policyCategory + ? reasons.filter(r => r.supportedMode === policyCategory || r.supportedMode === null) + : reasons; + + // Sort by impact (mapping reason stays first via 'critical' impact) + filteredReasons.sort((a, b) => this._impactWeight(b.impact) - this._impactWeight(a.impact)); + + const summary = this._generateSummary(recommendation, filteredReasons, inputs); + const shortSummary = this._generateShortSummary(recommendation, inputs); + + return { + recommendation: hwMode || policyMode, + policyMode, + hwMode, + confidence: recommendation.confidence, + reasons: filteredReasons.slice(0, 8), + warnings, + scores, + summary, + shortSummary, + timestamp: new Date().toISOString(), + // ✅ Add battery cost and efficiency to output for UI display + batteryCost: inputs.batteryCost, + batteryEfficiency: inputs.batteryEfficiency, + effectivePrice: inputs.effectivePrice + }; + } + + // ... (rest of the methods stay the same until _generateShortSummary) + + _generateShortSummary(recommendation, inputs) { + const hw = recommendation.hwMode; + const price = inputs?.effectivePrice; + const soc = inputs?.battery?.stateOfCharge; + const sun = inputs?.weather?.sunshineNext4Hours; + const rte = inputs?.batteryEfficiency; // ✅ Now uses configured 0.75, not learned 0.99 + + const avg = inputs?.batteryCost?.avgCost ?? null; + const breakEven = inputs?.batteryCost?.breakEven ?? null; + + const MODE_SHORT_NL = { + zero_charge_only: 'Zon‑laden', + zero_discharge_only: 'Ontladen', + to_full: 'Vol‑laden', + standby: 'Stand‑by', + zero: 'Net‑0', + + charge: 'Laden', + discharge: 'Ontladen', + preserve: 'Bewaren' + }; + + const label = MODE_SHORT_NL[hw] || hw?.toUpperCase() || '?'; + const parts = [label]; + + // SoC + if (typeof soc === 'number') { + parts.push(`${soc}%`); + } + + // Huidige prijs + if (typeof price === 'number') { + parts.push(`€${price.toFixed(3)}`); + } + + // ⭐ Arbitrage-indicator + if (avg !== null && breakEven !== null && typeof price === 'number') { + if (price > breakEven + 0.01) { + parts.push(`+€${(price - breakEven).toFixed(3)}/kWh`); // prijs boven break-even + } else if (price < breakEven - 0.01) { + parts.push(`-€${(breakEven - price).toFixed(3)}/kWh`); // prijs onder break-even + } else { + parts.push('≈BE'); // rond break-even + } + } + + // Zon + if (typeof sun === 'number' && sun >= 1) { + parts.push(`☀${sun.toFixed(1)}h`); + } + + // ✅ FIX: RTE should now show 75% instead of 99% + if (typeof rte === 'number') { + parts.push(`RTE ${(rte * 100).toFixed(0)}%`); + } + + return parts.join(' '); + } + + // ... (rest of methods unchanged - copy from original file) + + _addBatteryReasons(reasons, warnings, battery, settings, tariffType, inputs) { + const soc = battery?.stateOfCharge ?? 50; + const minSoc = settings.min_soc ?? null; + const maxSoc = settings.max_soc ?? 95; + + // NOTE: HomeWizard firmware handles 0-100% protection + // Explainability reflects strategy, not safety limits. + // Mirror policy-engine: no hardcoded floor — respect user's min_soc setting + const zeroModeThreshold = minSoc ?? 0; + + if (soc === 0) { + // Mirror policy-engine: at soc=0 export may be more profitable than storing + const _price = inputs.effectivePrice ?? null; + const _eff = settings.battery_efficiency ?? 0.75; + const _pricesArr = inputs.tariff?.allPrices || inputs.tariff?.next24Hours || []; + const _now = new Date(); + const _futurePrices = _pricesArr + .filter(h => h.timestamp ? new Date(h.timestamp) > _now : (h.index ?? 0) >= 1) + .map(h => h.price).filter(p => typeof p === 'number' && p > 0); + const _maxFuture = _futurePrices.length ? Math.max(..._futurePrices) : null; + const _storeValue = _maxFuture !== null ? _maxFuture * _eff : null; + const _exportWins = _price !== null && _storeValue !== null && _price > _storeValue; + + if (_exportWins) { + reasons.push({ + icon: '⚡', + category: 'battery', + text: `Batterij op 0% — export naar net winstgevender (€${_price.toFixed(3)} > max €${_maxFuture.toFixed(3)} × ${_eff.toFixed(2)} = €${_storeValue.toFixed(3)}). Stand-by aanbevolen.`, + impact: 'critical', + sentiment: 'positive', + supportedMode: 'preserve' + }); + } else { + reasons.push({ + icon: '🔧', + category: 'battery', + text: `Batterij op 0% — HomeWizard firmware calibratie mogelijk actief, laden prioriteit.`, + impact: 'critical', + sentiment: 'negative', + supportedMode: 'charge' + }); + } + warnings.push(`Batterij op 0% (mogelijk firmware calibratie)`); + return; + } + + if (soc <= zeroModeThreshold) { + reasons.push({ + icon: '🛑', + category: 'battery', + text: `Batterij zeer laag (${soc}%) — ontladen niet toegestaan.`, + impact: 'high', + sentiment: 'negative', + supportedMode: 'charge' + }); + warnings.push(`Batterij zeer laag (${soc}%)`); + return; + } + + if (typeof minSoc === 'number' && soc < minSoc) { + reasons.push({ + icon: '🔋', + category: 'battery', + text: `Batterij onder ingestelde minimumwaarde (${soc}% < ${minSoc}%).`, + impact: 'high', + sentiment: 'negative', + supportedMode: 'charge' + }); + warnings.push(`Batterij onder ingestelde minimumwaarde (${minSoc}%)`); + return; + } + + if (soc >= maxSoc) { + const fullText = maxSoc >= 100 + ? `Batterij volledig geladen (${soc}%).` + : `Batterij boven ingestelde maximumwaarde (${soc}% ≥ ${maxSoc}%).`; + const fullWarning = maxSoc >= 100 + ? `Batterij volledig geladen (${soc}%)` + : `Batterij boven ingestelde maximumwaarde (${maxSoc}%)`; + reasons.push({ + icon: '🔋', + category: 'battery', + text: fullText, + impact: 'high', + sentiment: 'positive', + supportedMode: 'discharge' + }); + warnings.push(fullWarning); + return; + } + + if (soc > zeroModeThreshold && soc <= 10) { + reasons.push({ + icon: '⚠️', + category: 'battery', + text: `Batterij erg laag (${soc}%) — laden aanbevolen.`, + impact: 'high', + sentiment: 'negative', + supportedMode: 'charge' + }); + return; + } + + if (soc > 10 && soc <= 30) { + reasons.push({ + icon: '⚠️', + category: 'battery', + text: `Batterij laag (${soc}%) — behouden voor dure uren.`, + impact: 'high', + sentiment: 'neutral', + supportedMode: 'preserve' + }); + return; + } + + reasons.push({ + icon: '🔋', + category: 'battery', + text: `Batterij in normaal bereik (${soc}%).`, + impact: 'low', + sentiment: 'neutral', + supportedMode: null + }); + } + + _addLoadAwarenessReasons(reasons, inputs) { + const battery = inputs.battery; + const p1 = inputs.p1; + + if (!battery || !p1) return; + + const maxDischarge = battery.maxDischargePowerW || + (battery.totalCapacityKwh + ? Math.max(1, Math.round(battery.totalCapacityKwh / 2.688)) * 800 + : 800); + + const gridPower = p1.resolved_gridPower ?? 0; + const batteryPower = p1.battery_power ?? 0; + const dischargeNow = batteryPower < 0 ? Math.abs(batteryPower) : 0; + const currentLoad = gridPower > 0 ? gridPower + dischargeNow : 0; + + if (currentLoad === 0) return; + + const canCover = currentLoad <= maxDischarge; + const coverageRatio = currentLoad > 0 ? Math.min(currentLoad / maxDischarge, 1.0) : 0; + + const actualDischargeText = dischargeNow > 0 ? ` [nu: ${Math.round(dischargeNow)}W]` : ''; + + if (canCover) { + reasons.push({ + icon: '✅', + category: 'load', + text: `Batterij kan huidige belasting volledig dekken (load: ${currentLoad}W, capaciteit: ${maxDischarge}W${actualDischargeText}).`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'discharge' + }); + } else { + const uncovered = currentLoad - maxDischarge; + reasons.push({ + icon: '⚠️', + category: 'load', + text: `Batterij kan slechts ${Math.round(coverageRatio * 100)}% dekken (capaciteit: ${maxDischarge}W, load: ${currentLoad}W${actualDischargeText} — ${uncovered}W blijft van net).`, + impact: 'high', + sentiment: 'negative', + supportedMode: 'discharge' + }); + } + } + + _addTariffReasons(reasons, tariff, settings, tariffType, inputs) { + if (!tariff) return; + + if (tariffType === 'fixed') { + reasons.push({ + icon: '💰', + category: 'tariff', + text: 'Vast tarief actief — focus op peak‑shaving.', + impact: 'low', + sentiment: 'neutral', + supportedMode: 'discharge' + }); + return; + } + + const price = inputs.effectivePrice; + const maxChargePrice = settings.max_charge_price || 0; + const minDischargePrice = settings.min_discharge_price || 0; + const fmt = (v) => typeof v === 'number' ? `€${v.toFixed(3)}` : 'onbekend'; + + const isTop3Cheap = Array.isArray(tariff.top3Lowest) && + tariff.top3Lowest.some(p => Math.abs((p.price ?? p) - price) < 0.00001); + + const isTop3Expensive = Array.isArray(tariff.top3Highest) && + tariff.top3Highest.some(p => Math.abs((p.price ?? p) - price) < 0.00001); + + if (isTop3Expensive) { + const battery = inputs.battery; + const p1 = inputs.p1; + const maxDischarge = battery?.maxDischargePowerW || 800; + const currentLoad = p1?.resolved_gridPower > 0 ? p1.resolved_gridPower : 0; + + if (currentLoad > 0) { + const savingsPerHour = (Math.min(currentLoad, maxDischarge) / 1000) * price; + reasons.push({ + icon: '🔥', + category: 'tariff', + text: `TOP-3 DUURSTE UUR (${fmt(price)}) — besparing €${savingsPerHour.toFixed(2)}/uur door batterij te gebruiken.`, + impact: 'critical', + sentiment: 'positive', + supportedMode: 'discharge' + }); + } else { + reasons.push({ + icon: '🔥', + category: 'tariff', + text: `TOP-3 DUURSTE UUR (${fmt(price)}) — batterij beschermen voor wanneer belasting komt.`, + impact: 'critical', + sentiment: 'positive', + supportedMode: 'preserve' + }); + } + return; + } + + if (isTop3Cheap) { + const EFFICIENCY = inputs.batteryEfficiency ?? 0.75; + const futurePrice = Array.isArray(tariff.top3Highest) && tariff.top3Highest[0] + ? (tariff.top3Highest[0].price ?? tariff.top3Highest[0]) + : minDischargePrice; + + const profit = (futurePrice * EFFICIENCY) - price; + + if (profit > 0.05) { + reasons.push({ + icon: '⚡', + category: 'tariff', + text: `TOP-3 GOEDKOOPSTE UUR (${fmt(price)}) — winst €${profit.toFixed(3)}/kWh bij RTE ${(EFFICIENCY * 100).toFixed(0)}%.`, + impact: 'critical', + sentiment: 'positive', + supportedMode: 'charge' + }); + } else { + reasons.push({ + icon: '⚠️', + category: 'tariff', + text: `TOP-3 GOEDKOOPSTE UUR (${fmt(price)}) maar marginale winst (€${profit.toFixed(3)}/kWh na verlies).`, + impact: 'medium', + sentiment: 'neutral', + supportedMode: 'preserve' + }); + } + return; + } + + if (typeof price === 'number' && price <= maxChargePrice) { + const minProfitable = price * 1.25; + if (minDischargePrice >= minProfitable) { + reasons.push({ + icon: '💰', + category: 'tariff', + text: `Stroom goedkoop (${fmt(price)}) — winstgevend bij ontladen op €${minDischargePrice.toFixed(3)}.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'charge' + }); + } else { + reasons.push({ + icon: '⚠️', + category: 'tariff', + text: `Stroom goedkoop (${fmt(price)}) maar spread te klein — moet €${minProfitable.toFixed(3)}+ worden voor winst.`, + impact: 'medium', + sentiment: 'neutral', + supportedMode: 'preserve' + }); + } + } + + if (typeof price === 'number' && price >= minDischargePrice) { + reasons.push({ + icon: '💰', + category: 'tariff', + text: `Stroomprijs hoog (${fmt(price)}) — ontladen voorkomt dure netstroom.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'discharge' + }); + } + + if (typeof price === 'number' && price <= 0.05) { + reasons.push({ + icon: '⚡', + category: 'tariff', + text: `Extreem lage prijs (${fmt(price)}) — zelfs na 25% verlies winstgevend.`, + impact: 'critical', + sentiment: 'positive', + supportedMode: 'charge' + }); + } + + if (typeof price === 'number' && price >= 0.40) { + reasons.push({ + icon: '🔥', + category: 'tariff', + text: `Extreem hoge prijs (${fmt(price)}) — maximale besparing door batterij.`, + impact: 'critical', + sentiment: 'positive', + supportedMode: 'discharge' + }); + } + } + + _addArbitrageReasons(reasons, inputs) { + const cost = inputs.batteryCost; + const price = inputs.effectivePrice; + + if (!cost || typeof price !== 'number') return; + + const avg = cost.avgCost; + const breakEven = cost.breakEven; + + if (avg <= 0) return; + + const fmt = (v) => `€${v.toFixed(3)}`; + + const minDischargePrice = inputs.settings?.min_discharge_price ?? 0; + const respectMinMax = (inputs.settings?.policy_mode === 'balanced-dynamic') + ? false + : inputs.settings?.respect_minmax !== false; + const oppDischargeFloor = inputs.settings?.opportunistic_discharge_floor ?? 0.20; + const effectiveDischargeFloor = respectMinMax ? minDischargePrice : oppDischargeFloor; + + if (price > breakEven + 0.01) { + if (effectiveDischargeFloor > 0 && price < effectiveDischargeFloor) { + const blockReason = respectMinMax + ? `onder min. ontlaadprijs (${fmt(minDischargePrice)})` + : `onder opportunistische drempel (${fmt(oppDischargeFloor)})`; + reasons.push({ + icon: '⚖️', + category: 'arbitrage', + impact: 'medium', + sentiment: 'neutral', + text: `Prijs (${fmt(price)}) boven break‑even (${fmt(breakEven)}) maar ${blockReason} — ontladen geblokkeerd.`, + supportedMode: 'preserve' + }); + } else { + reasons.push({ + icon: '💰', + category: 'arbitrage', + impact: 'high', + sentiment: 'positive', + text: `Ontladen is winstgevend: huidige prijs (${fmt(price)}) ligt boven break‑even (${fmt(breakEven)}).`, + supportedMode: 'discharge' + }); + } + return; + } + + if (price < breakEven - 0.01) { + reasons.push({ + icon: '📉', + category: 'arbitrage', + impact: 'high', + sentiment: 'positive', + text: `Laden is goedkoop: huidige prijs (${fmt(price)}) ligt onder jouw break-even prijs (${fmt(breakEven)}).`, + supportedMode: 'charge' + }); + return; + } + + reasons.push({ + icon: '⚖️', + category: 'arbitrage', + impact: 'medium', + sentiment: 'neutral', + text: `Prijs ligt rond break‑even (${fmt(price)} ≈ ${fmt(breakEven)}) — batterij behouden.`, + supportedMode: 'preserve' + }); + } + + _addPVReasons(reasons, inputs) { + const p1 = inputs?.p1; + if (!p1) { + reasons.push({ + icon: '🌥️', + category: 'pv', + text: `Geen PV‑gegevens beschikbaar.`, + impact: 'low', + sentiment: 'neutral', + supportedMode: null + }); + return; + } + + const gridPower = p1.resolved_gridPower ?? 0; + const batteryPower = p1.battery_power ?? 0; + + if (gridPower < -150) { + const exportPower = Math.abs(gridPower); + // Mirror policy-engine PV OVERSCHOT: compare export-now vs store-for-later + const _price = inputs.effectivePrice ?? null; + const _eff = (inputs.settings?.battery_efficiency) ?? 0.75; + const _pricesArr = inputs.tariff?.allPrices || inputs.tariff?.next24Hours || []; + const _now = new Date(); + const _futurePrices = _pricesArr + .filter(h => h.timestamp ? new Date(h.timestamp) > _now : (h.index ?? 0) >= 1) + .map(h => h.price).filter(p => typeof p === 'number' && p > 0); + const _maxFuture = _futurePrices.length ? Math.max(..._futurePrices) : null; + const _storeValue = _maxFuture !== null ? _maxFuture * _eff : null; + const _exportWins = _price !== null && _storeValue !== null && _price > _storeValue; + + if (_exportWins) { + const delayNote = inputs._delayCharge ? `, batterij laadt later bij goedkopere prijzen` : ''; + reasons.push({ + icon: '⚡', + category: 'pv', + text: `PV-overschot (${exportPower}W export) — export naar net winstgevender (€${_price.toFixed(3)} > €${_maxFuture.toFixed(3)} × ${_eff.toFixed(2)} = €${_storeValue.toFixed(3)})${delayNote}.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'preserve' + }); + } else { + reasons.push({ + icon: '☀️', + category: 'pv', + text: `PV-overschot gedetecteerd (${exportPower}W export) — opslaan winstgevender dan exporteren nu.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'charge' + }); + } + return; + } + + // Mirror policy-engine PV detection: virtual export = gridPower - batteryPower + const virtualExport = gridPower - batteryPower; + if (batteryPower > 150 && (gridPower <= 0 || virtualExport < -200)) { + const exportPart = virtualExport < -200 ? `, ${Math.round(Math.abs(virtualExport))}W virtueel export` : ''; + reasons.push({ + icon: '🔋', + category: 'pv', + text: `Batterij wordt geladen door PV (${Math.round(batteryPower)}W${exportPart}) — geen conversieverlies.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'charge' + }); + return; + } + + const weather = inputs.weather || {}; + const sunTomorrow = Number(weather.sunshineTomorrow ?? 0); + const soc = inputs.battery?.stateOfCharge ?? 50; + const pvNowW = Math.round(inputs.p1?.pv_power_estimated ?? 0); + const pvNowLabel = pvNowW > 50 ? `PV beperkt (${pvNowW}W)` : 'Geen PV nu'; + + if (sunTomorrow >= 4.0 && soc > 50) { + reasons.push({ + icon: '🌅', + category: 'pv', + text: `${pvNowLabel}, maar ${sunTomorrow.toFixed(1)}h zon morgen — batterij heeft vrije zonenergie (${soc}%), gebruik deze nu.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'discharge' + }); + } else { + reasons.push({ + icon: '🌥️', + category: 'pv', + text: `Geen PV-opwek — grid laden kost 25% conversieverlies.`, + impact: 'medium', + sentiment: 'neutral', + supportedMode: 'preserve' + }); + } + } + + _addWeatherReasons(reasons, weather) { + if (!weather) return; + + const sun4h = weather.sunshineNext4Hours || 0; + const sun8h = weather.sunshineNext8Hours || 0; + const sunToday = weather.sunshineTodayRemaining || 0; + const sunTomorrow = weather.sunshineTomorrow || 0; + + // Use remaining PV when available (more useful than full-day total mid-day) + const pvKwhRemaining = weather.pvKwhRemaining ?? null; + const pvKwhToday = weather.pvKwhToday ?? null; + const pvKwhLabel = pvKwhRemaining != null + ? `~${pvKwhRemaining} kWh resterend` + : pvKwhToday != null + ? `~${pvKwhToday} kWh vandaag` + : null; + const pvKwhSuffix = pvKwhLabel ? `, ${pvKwhLabel}` : ''; + + if (sun4h >= 2) { + reasons.push({ + icon: '☀️', + category: 'weather', + text: `Sterke zon komende 4 uur (${sun4h.toFixed(1)}h zonuren${pvKwhSuffix}) — batterij kan gratis geladen worden.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'charge' + }); + } else if (sun4h >= 1) { + reasons.push({ + icon: '🌤️', + category: 'weather', + text: `Matige zon komende 4 uur (${sun4h.toFixed(1)}h zonuren${pvKwhSuffix}).`, + impact: 'medium', + sentiment: 'neutral', + supportedMode: 'charge' + }); + } + + if (sun8h >= 3 && sun4h < 2) { + reasons.push({ + icon: '🌤️', + category: 'weather', + text: `Zon later vandaag verwacht (${sun8h.toFixed(1)}h over 4-8 uur${pvKwhSuffix}).`, + impact: 'medium', + sentiment: 'positive', + supportedMode: 'preserve' + }); + } else if (sunToday >= 2 && sun4h < 1) { + reasons.push({ + icon: '⛅', + category: 'weather', + text: `Zon verwacht later vandaag (${sunToday.toFixed(1)}h zonuren resterend${pvKwhSuffix}).`, + impact: 'medium', + sentiment: 'positive', + supportedMode: 'preserve' + }); + } else if (sun4h < 1) { + // Bewolkt/regen — geen noemenswaardige zon verwacht + const pvLabel = pvKwhLabel ?? 'weinig PV'; + const sunLabel = sunToday > 0 ? `${sunToday.toFixed(1)}h zon resterend` : 'geen zonuren verwacht'; + reasons.push({ + icon: '🌧️', + category: 'weather', + text: `Bewolkt/regen vandaag — ${pvLabel} PV (${sunLabel}).`, + impact: 'low', + sentiment: 'neutral', + supportedMode: 'preserve' + }); + } + + if (sunTomorrow >= 4) { + reasons.push({ + icon: '🌅', + category: 'weather', + text: `Goede zon morgen verwacht (${sunTomorrow.toFixed(1)}h) — batterij kan dan gratis laden.`, + impact: 'low', + sentiment: 'positive', + supportedMode: 'preserve' + }); + } + } + + _addSunForecastReasons(reasons, inputs) { + const weather = inputs.weather; + const sun = inputs.sun; + const tariff = inputs.tariff; + + if (!weather || !tariff) return; + + const price = tariff.currentPrice; + const maxChargePrice = inputs.settings?.max_charge_price || 0; + + const isCheap = price <= maxChargePrice; + if (!isCheap) return; + + const sun4h = weather.sunshineNext4Hours || 0; + const sunToday = weather.sunshineTodayRemaining || 0; + + const gfsScore = sun?.gfs || 0; + const harmonieScore = sun?.harmonie || 0; + const avgModelScore = (gfsScore + harmonieScore) / 2; + + const totalSun = Math.max(sun4h, sunToday, avgModelScore / 10); + + if (totalSun >= 3) { + reasons.push({ + icon: '🌞', + category: 'sun_forecast', + text: `Goede zon verwacht (${totalSun.toFixed(1)}h) — grid laden overslaan, wacht op gratis PV.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'preserve' + }); + } else if (totalSun < 2) { + reasons.push({ + icon: '☁️', + category: 'sun_forecast', + text: `Weinig zon verwacht (${totalSun.toFixed(1)}h) — grid laden nodig voor dure uren.`, + impact: 'medium', + sentiment: 'neutral', + supportedMode: 'charge' + }); + } + } + + _addEfficiencyReasons(reasons, inputs) { + const tariff = inputs.tariff; + const settings = inputs.settings; + const policyMode = inputs.policyMode; + + if (!tariff || !settings) return; + + const price = tariff.currentPrice; + const maxChargePrice = settings.max_charge_price || 0; + const minDischargePrice = settings.min_discharge_price || 0; + + if (policyMode === 'charge' || (price && price <= maxChargePrice)) { + const EFFICIENCY = inputs.batteryEfficiency ?? 0.75; + const minProfitable = price / EFFICIENCY; + + if (minDischargePrice < minProfitable) { + const loss = price * (1 - EFFICIENCY); + reasons.push({ + icon: '⚠️', + category: 'RTE', + text: `RTE-verlies ${((1 - EFFICIENCY) * 100).toFixed(0)}% (€${loss.toFixed(3)}/kWh) — spread te klein voor winst.`, + impact: 'medium', + sentiment: 'negative', + supportedMode: 'preserve' + }); + } + } + } + + _addDelayChargeReasons(reasons, inputs, recommendation) { + if (!inputs._delayCharge) return; + + const price = inputs.effectivePrice; + const fmt = (v) => typeof v === 'number' ? `€${v.toFixed(3)}` : 'onbekend'; + const hwMode = recommendation?.hwMode; + + // If PV-overschot reason already covers the export story, skip the redundant strategy line + const pvAlreadyExplained = reasons.some(r => r.category === 'pv' && r.supportedMode === 'preserve'); + + if (hwMode === 'zero_discharge_only') { + reasons.push({ + icon: '☀️', + category: 'strategy', + text: `PV-export + ontladen: zonne-energie gaat naar net (${fmt(price)}/kWh) terwijl batterij huishoudelijk verbruik dekt. Batterij laadt later bij goedkopere uren.`, + impact: 'critical', + sentiment: 'positive', + supportedMode: 'discharge' + }); + } else if (!pvAlreadyExplained) { + reasons.push({ + icon: '☀️', + category: 'strategy', + text: `Batterij laadt later bij goedkopere prijzen (nu ${fmt(price)}/kWh).`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'preserve' + }); + } + } + + _addPeakShavingReasons(reasons, inputs) { + if (inputs.settings.tariff_type !== 'fixed') return; + + const time = inputs.time; + const settings = inputs.settings; + + if (!time || !settings.peak_hours) return; + + const hour = time.getHours(); + const [startHour, endHour] = settings.peak_hours.split('-').map(h => parseInt(h, 10)); + + if (hour >= startHour && hour < endHour) { + reasons.push({ + icon: '📊', + category: 'peak', + text: `Piekuren (${startHour}:00-${endHour}:00) — batterij gebruiken om netpieken te scheren.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'discharge' + }); + } + } + + _addTimeReasons(reasons, time) { + if (!time) return; + + const hour = time.getHours(); + + if (hour >= 17 && hour < 22) { + reasons.push({ + icon: '⏰', + category: 'time', + text: `Avonduren (${hour}:00) — hogere huishoudelijke consumptie verwacht.`, + impact: 'medium', + sentiment: 'neutral', + supportedMode: 'discharge' + }); + } + + if (hour >= 2 && hour < 6) { + reasons.push({ + icon: '🌙', + category: 'time', + text: `Nachturen (${hour}:00) — vaak goedkope tarieven voor laden.`, + impact: 'low', + sentiment: 'positive', + supportedMode: 'charge' + }); + } + } + + _addConfidenceReason(reasons, recommendation, inputs) { + const c = Math.round(recommendation.confidence); + const MODE_LABELS_NL = { + zero_charge_only: 'Zon-laden', + zero_discharge_only: 'Ontladen (zon-export)', + to_full: 'Vol-laden', + standby: 'Stand-by', + zero: 'Net-0', + charge: 'Laden', + discharge: 'Ontladen', + preserve: 'Bewaren' + }; + const mode = recommendation.hwMode || recommendation.policyMode; + const modeLabel = MODE_LABELS_NL[mode] || mode || '?'; + + const MODE_SUPPORTED = { + zero_charge_only: 'charge', to_full: 'charge', + zero_discharge_only: 'discharge', + standby: 'preserve', zero: 'preserve', preserve: 'preserve' + }; + const supportedMode = MODE_SUPPORTED[mode] ?? null; + + const prev = inputs?.previousHwMode ?? null; + const prevLabel = prev ? (MODE_LABELS_NL[prev] || prev) : null; + const transition = prevLabel && prev !== mode ? ` (was: ${prevLabel})` : ''; + + if (c >= 90) { + reasons.push({ + icon: '🎯', + category: 'confidence', + text: `Zeer hoge zekerheid (${c}%) voor keuze: ${modeLabel}${transition}.`, + impact: 'low', + sentiment: 'positive', + supportedMode + }); + } else if (c >= 70) { + reasons.push({ + icon: '🎯', + category: 'confidence', + text: `Hoge zekerheid (${c}%) voor keuze: ${modeLabel}${transition}.`, + impact: 'low', + sentiment: 'positive', + supportedMode + }); + } else if (c >= 50) { + reasons.push({ + icon: '🎯', + category: 'confidence', + text: `Redelijke zekerheid (${c}%) voor keuze: ${modeLabel}${transition} — meerdere factoren in balans.`, + impact: 'low', + sentiment: 'neutral', + supportedMode + }); + } else { + reasons.push({ + icon: '⚠️', + category: 'confidence', + text: `Lage zekerheid (${c}%) voor keuze: ${modeLabel}${transition} — scores lagen dicht bij elkaar.`, + impact: 'low', + sentiment: 'neutral', + supportedMode + }); + } + } + + _addModeSpecificReasons(reasons, inputs, recommendation) { + const hwMode = recommendation.hwMode; + const price = inputs.effectivePrice; + const fmt = (v) => typeof v === 'number' ? `€${v.toFixed(3)}` : '?'; + + if (hwMode === 'zero_charge_only') { + // Only add if no meaningful charge-supporting reason exists yet + const hasChargeReason = reasons.some(r => r.supportedMode === 'charge' && r.impact !== 'low'); + if (!hasChargeReason) { + const minDischargePrice = inputs.settings?.min_discharge_price ?? 0; + const maxChargePrice = inputs.settings?.max_charge_price ?? 0; + const respectMinMax = (inputs.settings?.policy_mode === 'balanced-dynamic') + ? false + : inputs.settings?.respect_minmax !== false; + const oppDischargeFloor = inputs.settings?.opportunistic_discharge_floor ?? 0.20; + const effectiveDischargeFloor = respectMinMax ? minDischargePrice : oppDischargeFloor; + + const parts = []; + if (effectiveDischargeFloor > 0 && typeof price === 'number' && price < effectiveDischargeFloor) { + const blockLabel = respectMinMax + ? `min. ontlaadprijs ${fmt(minDischargePrice)}` + : `opportunistische drempel ${fmt(oppDischargeFloor)}`; + parts.push(`ontladen geblokkeerd (prijs ${fmt(price)} < ${blockLabel})`); + } + if (maxChargePrice > 0 && typeof price === 'number' && price > maxChargePrice) { + parts.push(`grid laden te duur (${fmt(price)} > max. laadprijs ${fmt(maxChargePrice)})`); + } + + const text = parts.length > 0 + ? `Modus Zon-laden actief — ${parts.join(', ')}. Batterij wacht op gratis PV-opwek.` + : `Modus Zon-laden actief — batterij accepteert alleen gratis PV-opwek.`; + + reasons.push({ + icon: '🌤️', + category: 'mode', + text, + impact: 'high', + sentiment: 'positive', + supportedMode: 'charge' + }); + } + } + } + + /** + * When policyMode and hwMode diverge (e.g. charge wins score but becomes standby + * because price > max_charge_price and no PV), generate an explanation for the gap. + */ + _buildMappingReason(normalized, inputs, scores) { + const policyMode = normalized.policyMode; + const hwMode = normalized.hwMode; + + // No divergence + if (policyMode === 'preserve' && (hwMode === 'standby' || hwMode === 'zero')) return null; + if (policyMode === 'charge' && (hwMode === 'to_full' || hwMode === 'zero_charge_only')) return null; + if (policyMode === 'discharge' && hwMode === 'zero_discharge_only') return null; + + const price = inputs.effectivePrice; + const maxChargePrice = inputs.settings?.max_charge_price ?? 0; + const minDischarge = inputs.settings?.min_discharge_price ?? 0; + const fmt = v => typeof v === 'number' ? `€${v.toFixed(3)}` : '?'; + const scoreCharge = scores?.charge ?? 0; + const scoreDischarge = scores?.discharge ?? 0; + + const HW_LABELS = { + standby: 'Standby', zero_charge_only: 'Zon-laden', to_full: 'Vol-laden', + zero_discharge_only: 'Ontladen', zero: 'Net-0' + }; + const hwLabel = HW_LABELS[hwMode] || hwMode; + + if (policyMode === 'charge' && (hwMode === 'standby' || hwMode === 'zero_charge_only')) { + const parts = []; + if (typeof price === 'number' && price > maxChargePrice) { + parts.push(`stroomprijs ${fmt(price)} > max laadprijs ${fmt(maxChargePrice)}`); + } + if (hwMode === 'standby') parts.push('geen PV beschikbaar'); + return { + icon: '⏳', + category: 'mapping', + text: `Laden wint (score ${scoreCharge}) maar ${parts.join(' en ')} → ${hwLabel}: wacht op betere conditie.`, + impact: 'critical', + sentiment: 'neutral', + supportedMode: 'charge' + }; + } + + if (policyMode === 'discharge' && hwMode === 'standby') { + const respectMinMax = (inputs.settings?.policy_mode === 'balanced-dynamic') + ? false + : inputs.settings?.respect_minmax !== false; + const oppDischargeFloor = inputs.settings?.opportunistic_discharge_floor ?? 0.20; + const effectiveFloor = respectMinMax ? minDischarge : oppDischargeFloor; + + const parts = []; + if (typeof price === 'number' && effectiveFloor > 0 && price < effectiveFloor) { + const floorLabel = respectMinMax + ? `min ontlaadprijs ${fmt(minDischarge)}` + : `opportunistische drempel ${fmt(oppDischargeFloor)}`; + parts.push(`prijs ${fmt(price)} < ${floorLabel}`); + } else { + parts.push('ontladen niet winstgevend genoeg'); + } + return { + icon: '⏳', + category: 'mapping', + text: `Ontladen wint (score ${scoreDischarge}) maar ${parts.join(' en ')} → ${hwLabel}.`, + impact: 'critical', + sentiment: 'neutral', + supportedMode: 'discharge' + }; + } + + return null; + } + + _generateSummary(recommendation, reasons, inputs) { + const hw = recommendation.hwMode; + const policy = recommendation.policyMode; + + const MODE_LABELS_NL = { + zero_charge_only: 'batterij PV-only te laten laden', + zero_discharge_only: 'batterij te gebruiken voor huishoudelijk verbruik', + to_full: 'batterij van het net te laden', + standby: 'batterij stand-by te zetten', + zero: 'net-0 te handhaven', + + charge: 'batterij te laden', + discharge: 'batterij te ontladen', + preserve: 'batterij te beschermen' + }; + + const action = MODE_LABELS_NL[hw] || MODE_LABELS_NL[policy] || 'batterij te beheren'; + + const topReasons = reasons + .filter(r => ['critical', 'high', 'medium'].includes(r.impact)) + .slice(0, 3) + .map(r => r.text.toLowerCase().replace(/\.$/, '')) + .join(', '); + + if (topReasons) { + return `Advies: ${action}. Reden: ${topReasons}.`; + } + + return `Advies: ${action}.`; + } + + _impactWeight(impact) { + return { critical: 4, high: 3, medium: 2, low: 1 }[impact] || 0; + } +} + +module.exports = ExplainabilityEngine; \ No newline at end of file diff --git a/lib/homewizard-cloud-api.js b/lib/homewizard-cloud-api.js new file mode 100644 index 00000000..c33766ed --- /dev/null +++ b/lib/homewizard-cloud-api.js @@ -0,0 +1,648 @@ +/* + * HomeWizard Cloud API Client + * + * Based on HomeWizard Cloud API research and documentation by Sven Serlier + * Original repository: https://github.com/smarthomesven/homey-homewizard-energy-cloud + * + * Copyright (c) 2026 Jeroen Tebbens and contributors to com.homewizard + * Cloud API research (c) 2025 Sven Serlier + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +'use strict'; + +const https = require('https'); +const WebSocket = require('ws'); +const { EventEmitter } = require('events'); + +/** + * HomeWizard Cloud API Client + * Handles authentication and WebSocket connections to HomeWizard cloud services + */ +class HomeWizardCloudAPI extends EventEmitter { + constructor(options = {}) { + super(); + + this.email = options.email; + this.password = options.password; + this.accessToken = null; + this.tokenExpiry = null; + this.accountInfo = null; + + // WebSocket connections + this.mainWs = null; + this.realtimeWs = null; + + // Device state cache + this.deviceStates = new Map(); + + // Track subscribed devices for reconnection + this.subscribedDevices = new Set(); + + // Configuration + this.reconnectInterval = 10000; + this.messageId = 0; + + this._fastModeStart = null; + this._fastModeRecovery = false; + this._lastFastModeRecovery = null; + + this._lastPatchReceived = null; + this._lastPatchTs = null; + + this._watchdogInterval = null; + + this._forceResubscribeAfterRecovery = false; + + this._startWatchdog(); + + } + + /** + * Authenticate and get access token + */ + async authenticate() { + return new Promise((resolve, reject) => { + const credentials = Buffer.from(`${this.email}:${this.password}`).toString('base64'); + + const options = { + hostname: 'api.homewizardeasyonline.com', + path: '/v1/auth/account/token?include=account', + method: 'GET', + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const response = JSON.parse(data); + + if (res.statusCode === 200) { + this.accessToken = response.access_token; + this.tokenExpiry = Date.now() + (response.expires_in * 1000); + this.accountInfo = response.account; + + this.emit('authenticated', this.accountInfo); + resolve(response); + } else { + reject(new Error(`Authentication failed: ${res.statusCode} - ${data}`)); + } + } catch (error) { + reject(new Error(`Failed to parse authentication response: ${error.message}`)); + } + }); + }); + + req.on('error', (error) => { + reject(new Error(`Authentication request failed: ${error.message}`)); + }); + + req.end(); + }); + } + + /** + * Check if access token is still valid + */ + isTokenValid() { + return this.accessToken && this.tokenExpiry && Date.now() < this.tokenExpiry; + } + + /** + * Get user's homes/locations + */ + async getLocations() { + if (!this.isTokenValid()) { + await this.authenticate(); + } + + return new Promise((resolve, reject) => { + const options = { + hostname: 'homes.api.homewizard.com', + path: '/locations', + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json' + } + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const locations = JSON.parse(data); + + if (res.statusCode === 200) { + resolve(locations); + } else { + reject(new Error(`Failed to get locations: ${res.statusCode} - ${data}`)); + } + } catch (error) { + reject(new Error(`Failed to parse locations response: ${error.message}`)); + } + }); + }); + + req.on('error', (error) => { + reject(new Error(`Get locations request failed: ${error.message}`)); + }); + + req.end(); + }); + } + + /** + * Connect to main WebSocket for device updates (every 40 seconds) + */ + async connectMainWebSocket() { + if (!this.isTokenValid()) { + await this.authenticate(); + } + + return new Promise((resolve, reject) => { + this.mainWs = new WebSocket('wss://energy-app-ws.homewizard.com/ws/'); + + let authenticated = false; + + this.mainWs.on('open', () => { + this.log('Main WebSocket connected'); + + const helloMessage = { + type: 'hello', + message_id: ++this.messageId, + compatibility: 5, + os: 'homey', + source: 'com.homewizard', + token: this.accessToken, + version: 'com.homewizard/3.0.0' + }; + + // FIX: wait till socket really opened + if (this.mainWs.readyState === WebSocket.OPEN) { + this.mainWs.send(JSON.stringify(helloMessage)); + } else { + this.mainWs.once('open', () => { + this.mainWs.send(JSON.stringify(helloMessage)); + }); + } + }); + + + this.mainWs.on('message', (data) => { + try { + const message = JSON.parse(data.toString()); + + // STREAM DEBUG + if (message.type === 'response' && message.details && message.details.includes('Stream ID')) { + const streamId = message.details.split('Stream ID: ')[1]; + if (!this._lastStreamId) { + this._lastStreamId = streamId; + this.log(`[DEBUG] First stream ID: ${streamId}`); + } else if (this._lastStreamId !== streamId) { + this.log(`[DEBUG] New stream ID detected: ${streamId} (previous: ${this._lastStreamId})`); + this._lastStreamId = streamId; + } + } + + // AUTH + if (message.type === 'response' && !authenticated) { + if (message.status === 200) { + authenticated = true; + this.log('WebSocket authenticated successfully'); + this.emit('mainws_connected'); + + if (this.subscribedDevices.size > 0) { + this.log(`Re-subscribing to ${this.subscribedDevices.size} device(s)...`); + setTimeout(() => { + for (const deviceId of this.subscribedDevices) { + this._sendSubscribeMessage(deviceId); + } + }, 1000); + } + + resolve(); + } else { + reject(new Error(`Authentication failed: ${message.status} - ${message.details}`)); + } + } + + // FAST‑MODE DETECTOR + if (message.type === 'json_patch') { + + // Tijdens lockout géén fast-mode detectie meer + if (this._fastModeRecovery) { + return; + } + + const now = Date.now(); + this._lastPatchReceived = now; + + if (!this._lastPatchTs) this._lastPatchTs = now; + const diff = now - this._lastPatchTs; + this._lastPatchTs = now; + + const isFast = diff < 1500; + + if (isFast) { + if (!this._fastModeStart) { + this._fastModeStart = now; + this.log('[FAST-MODE] Detected fast updates, monitoring...'); + } + } else { + this._fastModeStart = null; + } + + if (this._fastModeStart && now - this._fastModeStart > 5000) { + this.log('[FAST-MODE] Cloud stuck in fast-mode → forcing reconnect'); + + this._fastModeStart = null; + this._fastModeRecovery = true; + this._forceResubscribeAfterRecovery = true; + this._lastFastModeRecovery = Date.now(); + + try { + this.mainWs.close(); + } catch (e) { + this.error('Error closing WS during fast-mode recovery:', e); + } + + return; + } + } + + this._handleMainWebSocketMessage(message); + + } catch (error) { + this.error('Failed to parse WebSocket message:', error); + } + }); + + this.mainWs.on('error', (error) => { + this.error('Main WebSocket error:', error); + this.emit('mainws_error', error); + if (!authenticated) { + reject(error); + } + }); + + this.mainWs.on('close', () => { + this.log('Main WebSocket closed'); + this.emit('mainws_closed'); + + authenticated = false; + + // Fast-mode state resetten bij elke close + this._fastModeStart = null; + this._lastPatchTs = null; + this._lastPatchReceived = null; + + // FAST‑MODE LOCKOUT + if (this._fastModeRecovery) { + this.log('Main WebSocket closed due to fast-mode recovery'); + + const sinceRecovery = Date.now() - (this._lastFastModeRecovery || 0); + const lockout = 2 * 60 * 1000; + const wait = Math.max(0, lockout - sinceRecovery); + + this.log(`[FAST-MODE] Enforcing 2-minute lockout, waiting ${wait}ms before reconnect...`); + + // recovery-vlag pas na het plannen van reconnect uit + this._fastModeRecovery = false; + + setTimeout(() => { + this.connectMainWebSocket().catch(err => { + this.error('Reconnect after lockout failed:', err); + }); + }, wait); + + return; + } + + // NORMAL RECONNECT + const jitter = Math.floor(Math.random() * 2000); + const cooldown = this.reconnectInterval + 10000 + jitter; + + this.log(`Reconnecting main WebSocket after ${cooldown}ms cooldown...`); + + setTimeout(() => { + this.connectMainWebSocket().catch((error) => { + this.error('Failed to reconnect main WebSocket:', error); + }); + }, cooldown); + }); + + }); +} + + + + + /** + * Connect to realtime WebSocket for second-by-second updates + */ + async connectRealtimeWebSocket(deviceId, threePhases = false) { + if (!this.isTokenValid()) { + await this.authenticate(); + } + + return new Promise((resolve, reject) => { + this.realtimeWs = new WebSocket('wss://tsdb-reader.homewizard.com/devices/date/now'); + + this.realtimeWs.on('open', () => { + this.log('Realtime WebSocket connected'); + + // Send subscription message + const subscribeMessage = { + token: this.accessToken, + type: 'main_connection', + devices: [{ + identifier: deviceId, + measurementType: 'main_connection' + }], + three_phases: threePhases + }; + + this.realtimeWs.send(JSON.stringify(subscribeMessage)); + this.emit('realtimews_connected'); + resolve(); + }); + + this.realtimeWs.on('message', (data) => { + try { + const message = JSON.parse(data.toString()); + this._handleRealtimeWebSocketMessage(message, deviceId); + } catch (error) { + this.error('Failed to parse realtime WebSocket message:', error); + } + }); + + this.realtimeWs.on('error', (error) => { + this.error('Realtime WebSocket error:', error); + this.emit('realtimews_error', error); + }); + + this.realtimeWs.on('close', () => { + this.log('Realtime WebSocket closed, attempting to reconnect...'); + this.emit('realtimews_closed'); + + setTimeout(() => { + this.connectRealtimeWebSocket(deviceId, threePhases).catch((error) => { + this.error('Failed to reconnect realtime WebSocket:', error); + }); + }, this.reconnectInterval); + }); + }); + } + + /** + * Send subscribe message (internal helper) + */ + _sendSubscribeMessage(deviceId) { + if (!this.mainWs || this.mainWs.readyState !== WebSocket.OPEN) { + this.error(`Cannot subscribe to ${deviceId}: WebSocket not open`); + return; + } + + const subscribeMessage = { + type: 'subscribe_device', + device: deviceId, + message_id: ++this.messageId + }; + + this.mainWs.send(JSON.stringify(subscribeMessage)); + this.log(`Subscribed to device: ${deviceId}`); + } + + /** + * Subscribe to a device on the main WebSocket + */ + subscribeToDevice(deviceId) { + // Add to tracked devices for auto-resubscribe on reconnect + this.subscribedDevices.add(deviceId); + + // Subscribe now if connected + this._sendSubscribeMessage(deviceId); + } + + /** + * Unsubscribe from a device + */ + unsubscribeFromDevice(deviceId) { + this.subscribedDevices.delete(deviceId); + this.deviceStates.delete(deviceId); + + if (!this.mainWs || this.mainWs.readyState !== WebSocket.OPEN) { + this.log(`WS not open, local unsubscribe only for ${deviceId}`); + return; + } + + const msg = { + type: 'unsubscribe_device', + device: deviceId, + message_id: ++this.messageId, + }; + + this.mainWs.send(JSON.stringify(msg)); + this.log(`Unsubscribe message sent for device: ${deviceId}`); + } + + + /** + * Handle messages from main WebSocket + */ + _handleMainWebSocketMessage(message) { + switch (message.type) { + case 'response': + this.log(`Response received: ${message.status} - ${message.details}`); + break; + + case 'error': + this.error('WebSocket error message:', JSON.stringify(message)); + // Check if this is a critical error that should close connection + if (message.code === 'unauthorized' || message.code === 'forbidden') { + this.emit('mainws_error', new Error(`WebSocket error: ${message.message || 'Unauthorized'}`)); + } + break; + + case 'p1dongle': + case 'energysocket': + case 'watermeter': + // Full device state update + this.deviceStates.set(message.device, message); + this.emit('device_update', message); + break; + + case 'json_patch': + this._lastPatchReceived = Date.now(); // <-- NIEUW + this._applyJsonPatch(message); + break; + + + default: + this.log(`Unknown message type: ${message.type}`); + } + } + + /** + * Apply JSON patch to device state + */ + _applyJsonPatch(patchMessage) { + const deviceId = patchMessage.device; + let deviceState = this.deviceStates.get(deviceId); + + if (!deviceState) { + // This is expected if we've unsubscribed from the device + // Don't log as error to avoid spam + return; + } + + // Apply each patch operation + for (const operation of patchMessage.patch) { + if (operation.op === 'replace') { + // Parse the path (e.g., "/state/active_power_w" -> ["state", "active_power_w"]) + const pathParts = operation.path.split('/').filter(p => p); + + // Navigate to the nested property and update it + let target = deviceState; + for (let i = 0; i < pathParts.length - 1; i++) { + target = target[pathParts[i]]; + } + target[pathParts[pathParts.length - 1]] = operation.value; + } + } + + this.deviceStates.set(deviceId, deviceState); + this.emit('device_patch', { deviceId, patch: patchMessage.patch, state: deviceState }); + } + + /** + * Handle messages from realtime WebSocket + */ + _handleRealtimeWebSocketMessage(message, deviceId) { + if (message.time && message.wattage !== undefined) { + this.emit('realtime_power', { + deviceId, + timestamp: new Date(message.time), + wattage: message.wattage, + wattages: message.wattages + }); + } + } + + _startWatchdog() { + if (this._watchdogInterval) return; + + this._watchdogInterval = setInterval(() => { + const now = Date.now(); + + // Skip tijdens fast-mode recovery + if (this._fastModeRecovery) return; + + // Skip als WS niet open is + if (!this.mainWs || this.mainWs.readyState !== WebSocket.OPEN) return; + + // Skip als we recent een patch kregen + if (this._lastPatchReceived && now - this._lastPatchReceived < 60000) return; + + // Skip als we nog nooit een patch hebben gehad + if (!this._lastPatchReceived) return; + + this.log('[WATCHDOG] No patches for 60s — forcing resubscribe'); + + // Eerst resubscribe + for (const deviceId of this.subscribedDevices) { + this._sendSubscribeMessage(deviceId); + } + + // Na 10s checken of het werkte + setTimeout(() => { + const now2 = Date.now(); + + if (this._lastPatchReceived && now2 - this._lastPatchReceived < 60000) { + this.log('[WATCHDOG] Resubscribe successful'); + return; + } + + this.log('[WATCHDOG] Resubscribe failed — forcing reconnect'); + + try { + if (this.mainWs && this.mainWs.readyState === WebSocket.OPEN) { + this.mainWs.close(); + } + } catch (e) { + this.error('[WATCHDOG] Error closing WS:', e); + } + + this.connectMainWebSocket().catch(err => { + this.error('[WATCHDOG] Reconnect failed:', err); + }); + + }, 10000); + + }, 30000); +} + + + /** + * Get current state of a device + */ + getDeviceState(deviceId) { + return this.deviceStates.get(deviceId); + } + + /** + * Disconnect all WebSocket connections + */ + disconnect() { + if (this.mainWs) { + this.mainWs.close(); + this.mainWs = null; + } + + if (this.realtimeWs) { + this.realtimeWs.close(); + this.realtimeWs = null; + } + + this.deviceStates.clear(); + this.subscribedDevices.clear(); + this.emit('disconnected'); + } + + // Logging helpers + log(...args) { + const ts = new Date().toISOString(); + console.log(`${ts} [log] [HomeWizardCloudAPI]`, ...args); + } + + error(...args) { + const ts = new Date().toISOString(); + console.error(`${ts} [err] [HomeWizardCloudAPI]`, ...args); + } +} + +module.exports = HomeWizardCloudAPI; \ No newline at end of file diff --git a/lib/kwhprice-provider.js b/lib/kwhprice-provider.js new file mode 100644 index 00000000..215ef14b --- /dev/null +++ b/lib/kwhprice-provider.js @@ -0,0 +1,428 @@ +'use strict'; + +const fetchWithTimeout = require('../includes/utils/fetchWithTimeout'); + +/** + * kWhPrice.eu Day-Ahead Prices Provider + * + * ✅ MODIFIED: Now exports BOTH 15-minute intervals AND hourly averages + * + * - cache: Hourly averages (backward compatible) + * - cache15min: Full 15-minute intervals (new) + */ +class KwhPriceProvider { + constructor(homey, options = {}) { + this.homey = homey; + this.cache = null; // Hourly averages (existing) + this.cache15min = null; // ✅ NEW: 15-minute intervals + this.cacheExpiry = null; + this.log = homey.log.bind(homey); + this.error = homey.error.bind(homey); + + this.markup = options.markup || 0.11; + this.log(`KwhPrice provider initialized with markup: €${this.markup}/kWh`); + + this._loadCache(); + } + + // ─── Persistence ───────────────────────────────────────────────────────────── + + async _loadCache() { + try { + const cached = await this.homey.settings.get('kwhprice_cache'); + if (cached && cached.expiry > Date.now()) { + this.cache = cached.prices.map(p => ({ + ...p, + timestamp: new Date(p.timestamp) + })); + // ✅ NEW: Load 15-min cache too + this.cache15min = cached.prices15min?.map(p => ({ + ...p, + timestamp: new Date(p.timestamp) + })) || null; + + this.cacheExpiry = cached.expiry; + this.log( + `Loaded ${this.cache.length} hourly + ${this.cache15min?.length || 0} 15-min prices from storage ` + + `(expires in ${Math.round((this.cacheExpiry - Date.now()) / 60000)}min)` + ); + } + } catch (err) { + this.log('Failed to load cached prices:', err.message); + } + } + + async _saveCache() { + // Cache is saved centrally by MergedPriceProvider — skip individual save + // to reduce settings store pressure and memory churn + } + + // ─── Scraping ───────────────────────────────────────────────────────────────── + + /** + * Parse kwhprice.eu/en/netherlands page. + * + * The page uses Chart.js with data embedded as JavaScript arrays: + * labels: ["00:00-00:15", "00:15-00:30", ...] (96 × 15-min labels) + * data: [0.0571, 0.0623, ...] (96 × spot prices €/kWh) + * + * Only today's data is available (no tomorrow dataset on this page). + * Returns an array of { periodStart: Date, spotEur: number }. + */ + _parseHtml(html, todayAms) { + const slots = []; + const [y, mo, d] = todayAms; + + const todayUtcMidnight = new Date(Date.UTC(y, mo - 1, d)); + const offsetToday = (this._isSummerTime(todayUtcMidnight) ? 2 : 1) * 3600 * 1000; + + const toUtc = (hh, mm) => + new Date(Date.UTC(y, mo - 1, d, hh, mm, 0) - offsetToday); + + // Extract labels array: ["00:00-00:15", ...] + const labelsMatch = html.match(/labels:\s*\[([\s\S]*?)\]/); + if (!labelsMatch) { + this.log('kWhPrice: no Chart.js labels array found in page'); + return []; + } + + // Extract numeric data array (prices only — no strings inside) + const dataMatch = html.match(/\bdata:\s*\[([\d.,\s\n]+)\]/); + if (!dataMatch) { + this.log('kWhPrice: no Chart.js data array found in page'); + return []; + } + + // Parse "HH:MM-HH:MM" labels → start times + const times = [...labelsMatch[1].matchAll(/"(\d{2}):(\d{2})-\d{2}:\d{2}"/g)] + .map(m => ({ hh: parseInt(m[1], 10), mm: parseInt(m[2], 10) })); + + // Parse spot prices + const prices = dataMatch[1] + .split(',') + .map(s => parseFloat(s.trim())) + .filter(n => !isNaN(n)); + + if (times.length === 0 || prices.length === 0) { + this.log(`kWhPrice: parsed ${times.length} times, ${prices.length} prices — skipping`); + return []; + } + + const count = Math.min(times.length, prices.length); + this.log(`kWhPrice layout: Chart.js 15-min intervals [${count} slots today]`); + + for (let i = 0; i < count; i++) { + const { hh, mm } = times[i]; + const spotEur = prices[i]; + if (spotEur > 0) { + slots.push({ periodStart: toUtc(hh, mm), spotEur }); + } + } + + return slots; + } + + /** Rough summer-time check for Europe/Amsterdam. */ + _isSummerTime(date) { + // CEST runs last Sunday March → last Sunday October + const year = date.getUTCFullYear(); + const lastSunMarch = this._lastSunday(year, 2); // month index 2 = March + const lastSunOct = this._lastSunday(year, 9); // month index 9 = October + return date >= lastSunMarch && date < lastSunOct; + } + + _lastSunday(year, monthIndex) { + const d = new Date(Date.UTC(year, monthIndex + 1, 0)); // last day of month + d.setUTCDate(d.getUTCDate() - d.getUTCDay()); // back to Sunday + return d; + } + + // ✅ NEW: Process 15-minute slots WITHOUT averaging + _process15MinSlots(slots) { + return slots.map(slot => { + const finalPrice = (slot.spotEur + this.markup) * 1.21; + + const cetHour = parseInt( + slot.periodStart.toLocaleString('en-US', { + timeZone: 'Europe/Amsterdam', + hour: '2-digit', + hour12: false + }) + ); + + const cetMinute = parseInt( + slot.periodStart.toLocaleString('en-US', { + timeZone: 'Europe/Amsterdam', + minute: '2-digit' + }) + ); + + return { + timestamp: slot.periodStart, + price: finalPrice, + originalPrice: slot.spotEur, + hour: cetHour, + minute: cetMinute, + readingDate: slot.periodStart.toISOString() + }; + }).sort((a, b) => a.timestamp - b.timestamp); + } + + /** Average 15-min slots into hourly buckets (existing method, unchanged). */ + _aggregateToHourly(slots) { + const buckets = {}; + + for (const slot of slots) { + // Key = UTC hour start + const hourKey = new Date( + Date.UTC( + slot.periodStart.getUTCFullYear(), + slot.periodStart.getUTCMonth(), + slot.periodStart.getUTCDate(), + slot.periodStart.getUTCHours() + ) + ).toISOString(); + + if (!buckets[hourKey]) buckets[hourKey] = { sum: 0, count: 0 }; + buckets[hourKey].sum += slot.spotEur; + buckets[hourKey].count += 1; + } + + return Object.entries(buckets) + .map(([isoKey, { sum, count }]) => { + const timestamp = new Date(isoKey); + const spot = sum / count; + const finalPrice = (spot + this.markup) * 1.21; + + const cetHour = parseInt( + timestamp.toLocaleString('en-US', { + timeZone: 'Europe/Amsterdam', + hour: '2-digit', + hour12: false + }) + ); + + return { + timestamp, + price: finalPrice, + originalPrice: spot, + hour: cetHour, + readingDate: timestamp.toISOString() + }; + }) + .sort((a, b) => a.timestamp - b.timestamp); + } + + // ─── Public API (matches EnergyZeroProvider exactly) ───────────────────────── + + async fetchPrices(force = false) { + if (!force && this.cache && this.cacheExpiry > Date.now()) { + this.log('Using cached kWhPrice prices'); + return this.cache; + } + + const now = new Date(); + const amsDateStr = now.toLocaleString('en-CA', { + timeZone: 'Europe/Amsterdam', + year: 'numeric', + month: '2-digit', + day: '2-digit' + }).split(',')[0]; + const todayAms = amsDateStr.split('-').map(Number); // [y, m, d] + + this.log(`Fetching kWhPrice prices for Amsterdam date ${amsDateStr}`); + + try { + const response = await fetchWithTimeout('https://kwhprice.eu/en/netherlands', { + headers: { + 'Accept': 'text/html,application/xhtml+xml', + 'User-Agent': 'Mozilla/5.0 (compatible; dap-energy-app/1.0)' + } + }, 10000); + + if (!response.ok) { + throw new Error(`kWhPrice fetch error: ${response.status} ${response.statusText}`); + } + + const html = await response.text(); + const slots = this._parseHtml(html, todayAms); + + if (slots.length === 0) { + this.log("kWhPrice: page returned no data → using empty list"); + return []; + } + + // ✅ NEW: Store BOTH 15-minute and hourly data + this.cache15min = this._process15MinSlots(slots); + this.cache = this._aggregateToHourly(slots); + this.cacheExpiry = Date.now() + 60 * 60 * 1000; // 1 hour + await this._saveCache(); + + const days = slots.length > 96 ? 'today + tomorrow' : 'today'; + this.log( + `Fetched ${slots.length} 15-min slots (${days}) → ` + + `${this.cache15min.length} 15-min prices + ${this.cache.length} hourly averages` + ); + + if (this.cache.length > 0) { + const first = this.cache[0]; + const last = this.cache[this.cache.length - 1]; + this.log( + `Hourly range: ${first.timestamp.toISOString()} (${first.hour}:00 AMS, €${first.price.toFixed(4)}) ` + + `→ ${last.timestamp.toISOString()} (${last.hour}:00 AMS, €${last.price.toFixed(4)})` + ); + } + + if (this.cache15min.length > 0) { + const first15 = this.cache15min[0]; + const last15 = this.cache15min[this.cache15min.length - 1]; + this.log( + `15-min range: ${first15.timestamp.toISOString()} (${first15.hour}:${String(first15.minute).padStart(2, '0')} AMS, €${first15.price.toFixed(4)}) ` + + `→ ${last15.timestamp.toISOString()} (${last15.hour}:${String(last15.minute).padStart(2, '0')} AMS, €${last15.price.toFixed(4)})` + ); + } + + return this.cache; + + } catch (err) { + this.error('Failed to fetch kWhPrice prices:', err); + if (this.cache) { + this.log('Returning stale cache due to fetch error'); + return this.cache; + } + throw err; + } + } + + // ✅ NEW: Get 15-minute prices + getAll15MinPrices() { + if (!this.cache15min || this.cache15min.length === 0) return []; + const now = new Date(); + return this.cache15min.map((p, idx) => ({ + hour: p.hour, + minute: p.minute, + index: idx, // 0-95 for today, 96-191 for tomorrow + price: p.price, + timestamp: p.timestamp, + hoursFromNow: Math.floor((p.timestamp - now) / (1000 * 60 * 60)) + })); + } + + // ✅ NEW: Get current 15-minute price + getCurrent15MinPrice() { + if (!this.cache15min || this.cache15min.length === 0) return null; + const now = new Date(); + const current = this.cache15min.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + const end = new Date(start.getTime() + 15 * 60 * 1000); // 15 minutes + return now >= start && now < end; + }); + return current ? current.price : null; + } + + getCurrentRate() { + if (!this.cache || this.cache.length === 0) return 'standard'; + + const now = new Date(); + let currentPrice = this.cache.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + const end = new Date(start.getTime() + 3600 * 1000); + return now >= start && now < end; + }); + + if (!currentPrice) { + const currentHourCET = parseInt( + now.toLocaleString('en-US', { + timeZone: 'Europe/Amsterdam', + hour: '2-digit', + hour12: false + }) + ); + currentPrice = this.cache.find(p => p.hour === currentHourCET); + } + + if (!currentPrice) currentPrice = this.cache[0]; + + const prices = this.cache.map(p => p.price); + const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length; + const stdDev = this._calculateStdDev(prices, avgPrice); + const price = currentPrice.price; + + if (price <= avgPrice - stdDev * 0.5) return 'low'; + if (price >= avgPrice + stdDev * 0.5) return 'peak'; + return 'standard'; + } + + getNextRateChange() { + if (!this.cache || this.cache.length === 0) return null; + const now = new Date(); + const next = this.cache.find(p => { + const timestamp = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + return timestamp > now; + }); + return next ? next.timestamp : null; + } + + getCurrentPrice() { + if (!this.cache || this.cache.length === 0) return null; + const now = new Date(); + const current = this.cache.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + const end = new Date(start.getTime() + 3600 * 1000); + return now >= start && now < end; + }); + return current ? current.price : null; + } + + getPriceStatistics() { + if (!this.cache || this.cache.length === 0) { + return { avg: null, min: null, max: null, stdDev: null }; + } + const prices = this.cache.map(p => p.price); + const avg = prices.reduce((a, b) => a + b, 0) / prices.length; + return { + avg, + min: Math.min(...prices), + max: Math.max(...prices), + stdDev: this._calculateStdDev(prices, avg) + }; + } + + getTop3Cheapest() { + if (!this.cache || this.cache.length === 0) return []; + return [...this.cache] + .sort((a, b) => a.price - b.price) + .slice(0, 3) + .map(p => ({ hour: p.hour, price: p.price, timestamp: p.timestamp })); + } + + getTop3MostExpensive() { + if (!this.cache || this.cache.length === 0) return []; + return [...this.cache] + .sort((a, b) => b.price - a.price) + .slice(0, 3) + .map(p => ({ hour: p.hour, price: p.price, timestamp: p.timestamp })); + } + + hasPrices() { + return this.cache && this.cache.length > 0; + } + + getAllHourlyPrices() { + if (!this.cache || this.cache.length === 0) return []; + const now = new Date(); + return this.cache.map(p => ({ + hour: p.hour, + index: Math.floor((p.timestamp - now) / (1000 * 60 * 60)), + price: p.price, + timestamp: p.timestamp + })); + } + + _calculateStdDev(values, mean) { + const squareDiffs = values.map(v => Math.pow(v - mean, 2)); + return Math.sqrt(squareDiffs.reduce((a, b) => a + b, 0) / values.length); + } +} + +module.exports = KwhPriceProvider; \ No newline at end of file diff --git a/lib/learning-engine.js b/lib/learning-engine.js new file mode 100644 index 00000000..29c9f308 --- /dev/null +++ b/lib/learning-engine.js @@ -0,0 +1,584 @@ +'use strict'; + +/** + * LearningEngine - Tracks historical performance and learns patterns + * + * Features: + * - Hourly consumption patterns by day-of-week + * - PV production accuracy tracking + * - Policy decision success rate + * - Adaptive confidence scoring + */ + +const debug = false; + +class LearningEngine { + constructor(homey, device) { + this.homey = homey; + this.device = device; + this.log = (...args) => debug && homey.log('[LearningEngine]', ...args); + } + + /** + * Initialize or load historical data from device store + */ + async initialize() { + const stored = await this.device.getStoreValue('learning_data'); + + if (stored) { + this.data = stored; + this.log('Loaded learning data:', Object.keys(this.data)); + + // One-time reset: if a corrupted bias snapshot pushed the factor above 1.5 and the + // reset has not yet been applied, restore to neutral. The flag prevents repeated resets + // on every restart so a legitimately high factor can still be learned over time. + if ((this.data.radiation_bias_factor ?? 1.0) > 1.5 && !this.data.radiation_bias_reset_v1) { + this.log(`Resetting corrupted radiation_bias_factor (${this.data.radiation_bias_factor?.toFixed(2)}) → 1.0`); + this.data.radiation_bias_factor = 1.0; + this.data.radiation_bias_samples = []; + this.data.radiation_bias_reset_v1 = true; + await this._saveData(); + } + + const biasSamples = (this.data.radiation_bias_samples || []).length; + const biasFactor = this.data.radiation_bias_factor ?? 1.0; + const learnedSlots = this.getSolarLearnedSlotCount(); + const biasActive = biasSamples >= 3 && learnedSlots < 10; + this.homey.log(`[LearningEngine] radiation_bias_factor=${biasFactor.toFixed(3)} (${biasSamples} samples, ${learnedSlots} yield slots — ${biasActive ? 'ACTIVE' : 'inactive: yield factors in use'})`); + + } else { + // Initialize fresh data structure + this.data = { + // Hourly consumption patterns: [day_of_week][hour] = { sum, count, avg } + consumption_patterns: this._initializeConsumptionPatterns(), + + // PV prediction accuracy: track predicted vs actual + pv_predictions: [], + pv_accuracy_score: 1.0, // 1.0 = perfect, adjusts over time + + // Weather radiation forecast bias: ratio of actual vs forecasted W/m² + // < 1.0 = model over-predicts (too optimistic), > 1.0 = under-predicts + radiation_bias_samples: [], // { ratio, timestamp } + radiation_bias_factor: 1.0, // EMA of actual/forecast ratio + + // Per-slot solar yield factors: yieldFactor[slot] = W_actual / (W/m² radiation) + // 96 slots × 15 min = 24h. Absorbs pvCapacity, panel angle, PR, shading in one number. + // null = not yet learned. Requires PV power flow card to be active. + solar_yield_factors: new Array(96).fill(null), + + // Historic max radiation seen per slot — used as dynamic learning threshold. + // Only learn when current radiation > 15% of slot max (avoids dawn/dusk noise). + solar_slot_max_radiation: new Array(96).fill(0), + + // Policy decisions: track outcomes + policy_decisions: [], + policy_success_rate: 1.0, + + // Last updated timestamp + last_updated: Date.now(), + + // Statistics + stats: { + total_samples: 0, + days_tracked: 0, + learning_started: Date.now() + } + }; + + await this._saveData(); + } + } + + /** + * Initialize consumption pattern structure + * 7 days × 24 hours with sum/count/avg + */ + _initializeConsumptionPatterns() { + const patterns = {}; + for (let day = 0; day < 7; day++) { + patterns[day] = {}; + for (let hour = 0; hour < 24; hour++) { + patterns[day][hour] = { sum: 0, count: 0, avg: 0 }; + } + } + return patterns; + } + + /** + * Record actual consumption for learning + * @param {number} powerW - Current grid import power + */ + async recordConsumption(powerW) { + if (powerW < 0) return; // Only track import, not export + + const now = new Date(); + const dayOfWeek = now.getDay(); // 0 = Sunday, 6 = Saturday + const hour = now.getHours(); + + const pattern = this.data.consumption_patterns[dayOfWeek][hour]; + // Use exponential moving average once we have enough data to avoid + // sum/count growing unboundedly (which bloats the store over years). + if (pattern.count < 100) { + pattern.sum += powerW; + pattern.count += 1; + pattern.avg = pattern.sum / pattern.count; + } else { + // EMA with alpha=0.01 — memory-efficient rolling average + const alpha = 0.01; + pattern.avg = alpha * powerW + (1 - alpha) * pattern.avg; + // Keep sum/count in sync so getPredictedConsumption still works; + // cap count at 100 to prevent overflow + pattern.sum = pattern.avg * pattern.count; + } + + this.data.stats.total_samples += 1; + + // Save periodically (every 100 samples) + if (this.data.stats.total_samples % 100 === 0) { + await this._saveData(); + } + } + + /** + * Get predicted consumption for a specific time + * @param {Date} targetTime - Time to predict for + * @returns {number} Predicted power in W + */ + getPredictedConsumption(targetTime = new Date()) { + const dayOfWeek = targetTime.getDay(); + const hour = targetTime.getHours(); + + const pattern = this.data.consumption_patterns[dayOfWeek][hour]; + + // Enough samples for this specific day — use it directly + if (pattern.count >= 4) return pattern.avg; + + // Fallback: average over same day-group (weekend vs weekday) for this hour + // Weekend = Sunday (0) + Saturday (6); weekday = Mon–Fri (1–5) + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + const groupDays = isWeekend ? [0, 6] : [1, 2, 3, 4, 5]; + + let groupSum = 0, groupCount = 0; + for (const d of groupDays) { + const p = this.data.consumption_patterns[d][hour]; + if (p.count > 0) { groupSum += p.avg * p.count; groupCount += p.count; } + } + if (groupCount > 0) return groupSum / groupCount; + + // Last resort: all days combined + let totalSum = 0, totalCount = 0; + for (let d = 0; d < 7; d++) { + const p = this.data.consumption_patterns[d][hour]; + if (p.count > 0) { totalSum += p.avg * p.count; totalCount += p.count; } + } + return totalCount > 0 ? totalSum / totalCount : 0; + } + + /** + * Get consumption prediction confidence + * @param {Date} targetTime - Time to predict for + * @returns {number} Confidence 0-100 + */ + getConsumptionConfidence(targetTime = new Date()) { + const dayOfWeek = targetTime.getDay(); + const hour = targetTime.getHours(); + + const pattern = this.data.consumption_patterns[dayOfWeek][hour]; + + // Per-day confidence (0-10: 0-50%, 10-50: 50-90%, 50+: 90-100%) + const dayConf = (count) => { + if (count === 0) return 0; + if (count < 10) return count * 5; + if (count < 50) return 50 + (count - 10); + return Math.min(100, 90 + (count - 50) * 0.2); + }; + + if (pattern.count >= 4) return dayConf(pattern.count); + + // Fallback to group confidence, capped at 60% (less precise than per-day data) + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + const groupDays = isWeekend ? [0, 6] : [1, 2, 3, 4, 5]; + const groupTotal = groupDays.reduce((sum, d) => sum + this.data.consumption_patterns[d][hour].count, 0); + return Math.min(60, dayConf(groupTotal)); + } + + /** + * Record PV prediction vs actual for learning + * @param {number} predictedW - What we predicted + * @param {number} actualW - What actually happened + */ + async recordPvAccuracy(predictedW, actualW) { + const now = Date.now(); + + // Calculate error percentage + const error = predictedW > 0 + ? Math.abs(actualW - predictedW) / predictedW + : 0; + + this.data.pv_predictions.push({ + timestamp: now, + predicted: predictedW, + actual: actualW, + error: error + }); + + // Keep only last 300 predictions (enough for accuracy calculation, uses slice(-100)) + if (this.data.pv_predictions.length > 300) { + this.data.pv_predictions = this.data.pv_predictions.slice(-300); + } + + // Update accuracy score (exponential moving average) + const accuracy = 1.0 - error; // 1.0 = perfect, 0.0 = completely wrong + const alpha = 0.1; // Smoothing factor + this.data.pv_accuracy_score = + (alpha * accuracy) + ((1 - alpha) * this.data.pv_accuracy_score); + + this.log(`PV accuracy: predicted=${predictedW}W, actual=${actualW}W, error=${(error*100).toFixed(1)}%, score=${this.data.pv_accuracy_score.toFixed(2)}`); + + // Throttle saves: only persist every 10th call (~50 min at 5-min intervals) + this._pvSaveCounter = (this._pvSaveCounter || 0) + 1; + if (this._pvSaveCounter % 10 === 0) await this._saveData(); + } + + /** + * Get PV prediction adjustment multiplier + * @returns {number} Multiplier to apply to predictions (0.5 - 1.5) + */ + getPvAdjustmentMultiplier() { + // If we have no history, trust predictions fully + if (this.data.pv_predictions.length < 10) return 1.0; + + // Calculate average error over recent predictions + const recent = this.data.pv_predictions.slice(-100); + const avgError = recent.reduce((sum, p) => sum + p.error, 0) / recent.length; + + // If consistently over-predicting (actual < predicted), reduce multiplier + // If consistently under-predicting (actual > predicted), increase multiplier + const avgRatio = recent.reduce((sum, p) => { + return sum + (p.predicted > 0 ? p.actual / p.predicted : 1.0); + }, 0) / recent.length; + + // Clamp adjustment between 0.5 and 1.5 + return Math.max(0.5, Math.min(1.5, avgRatio)); + } + + /** + * Record Open-Meteo radiation forecast accuracy for a day. + * Call once per day after comparing yesterday's forecasted vs actual radiation. + * @param {number} forecastAvgWm2 - Average radiation forecasted for yesterday's daylight hours + * @param {number} actualAvgWm2 - Average radiation actually observed (from past_days=1) + */ + async recordRadiationAccuracy(forecastAvgWm2, actualAvgWm2) { + if (forecastAvgWm2 <= 0) return; // no daylight to compare + // Reject samples where the forecast snapshot is suspiciously low — likely a data + // glitch or saved during a period with no valid radiation data. A threshold of 30 W/m² + // is below any real cloudy-day average but above noise/zeros from bad API responses. + if (forecastAvgWm2 < 30) { + this.log(`Radiation bias skipped: forecast avg ${forecastAvgWm2.toFixed(0)} W/m² too low — likely bad snapshot`); + return; + } + + // Cap ratio to avoid single weather-model misses (e.g. predicted cloudy, turned sunny) + // from dominating the EMA. A 3× ratio already signals a major miss. + const ratio = Math.min(actualAvgWm2 / forecastAvgWm2, 3.0); + const now = Date.now(); + + this.data.radiation_bias_samples = this.data.radiation_bias_samples || []; + this.data.radiation_bias_samples.push({ ratio, timestamp: now }); + + // Keep last 30 days of daily samples + if (this.data.radiation_bias_samples.length > 30) { + this.data.radiation_bias_samples = this.data.radiation_bias_samples.slice(-30); + } + + // EMA update (alpha=0.15: slower than PV — weather bias shifts seasonally) + const alpha = 0.15; + const prev = this.data.radiation_bias_factor ?? 1.0; + this.data.radiation_bias_factor = Math.max(0.3, Math.min(2.0, + alpha * ratio + (1 - alpha) * prev + )); + + this.log(`Radiation bias: forecast=${forecastAvgWm2.toFixed(0)}W/m², actual=${actualAvgWm2.toFixed(0)}W/m², ratio=${ratio.toFixed(2)}, factor=${this.data.radiation_bias_factor.toFixed(2)}`); + + await this._saveData(); + } + + /** + * Get the learned radiation bias correction factor. + * Multiply forecasted W/m² values by this before using in planning. + * 1.0 = model is accurate, 0.8 = model over-predicts by 20% + * @returns {number} Correction factor clamped to 0.3–2.0 + */ + getRadiationBiasFactor() { + // Need at least 3 samples before trusting the bias + if (!this.data.radiation_bias_samples || this.data.radiation_bias_samples.length < 3) return 1.0; + // Once yield factors are learned (≥10 slots), they already absorb the relationship between + // Open-Meteo radiation and actual PV output. Applying bias on top double-counts the correction. + if (this.getSolarLearnedSlotCount() >= 10) return 1.0; + return this.data.radiation_bias_factor ?? 1.0; + } + + /** + * Update the per-slot solar yield factor from a live PV power measurement. + * Approach inspired by de Gruijter's SolarLearningStrategy (com.gruijter.powerhour). + * Core concept: yieldFactor = W_actual / (W/m²) — absorbs capacity, orientation, PR and shading. + * Called every time the PV flow card fires; radiation is interpolated from weather data. + * + * @param {Date} timestamp - Current time + * @param {number} powerW - Actual PV production in watts + * @param {number} radiationWm2 - Current radiation (GTI or GHI) in W/m² + */ + updateSolarYieldFactor(timestamp, powerW, radiationWm2) { + if (radiationWm2 < 10) return; // absolute floor — ignore dawn/dusk sensor noise + + const d = new Date(timestamp); + const slotIndex = (d.getUTCHours() * 4) + Math.floor(d.getUTCMinutes() / 15); + + // Dynamic threshold: only learn when radiation is meaningful for this slot. + // Tracks the highest radiation ever seen per slot, learns only above 15% of that. + // Prevents low-quality dawn/dusk samples from corrupting the slot model. + this.data.solar_slot_max_radiation = this.data.solar_slot_max_radiation || new Array(96).fill(0); + if (radiationWm2 > this.data.solar_slot_max_radiation[slotIndex]) { + this.data.solar_slot_max_radiation[slotIndex] = radiationWm2; + } + const dynamicThreshold = Math.max(50, this.data.solar_slot_max_radiation[slotIndex] * 0.15); + if (radiationWm2 < dynamicThreshold) return; + + const yf = Math.max(0, powerW) / radiationWm2; + if (!Number.isFinite(yf) || yf < 0.01 || yf > 500) return; + + this.data.solar_yield_factors = this.data.solar_yield_factors || new Array(96).fill(null); + const old = this.data.solar_yield_factors[slotIndex]; + + // Spike protection: ignore readings >1.25× current global max + const globalMax = Math.max(...this.data.solar_yield_factors.filter(v => v !== null), 0); + if (globalMax > 0 && yf > globalMax * 1.25) { + this.log(`Solar yield slot ${slotIndex}: spike ignored (${yf.toFixed(2)} > 1.25×${globalMax.toFixed(2)})`); + return; + } + + // Outlier rejection: transient cloud causing >80% drop vs learned model + if (old !== null && old > 0 && yf < old * 0.2) { + this.log(`Solar yield slot ${slotIndex}: drop ignored (${yf.toFixed(2)} vs model ${old.toFixed(2)})`); + return; + } + + // Symmetric EMA (α=0.10): unbiased convergence to the typical yield for this slot. + // An asymmetric EMA (fast up, slow down) was tried but caused upward drift toward + // the all-time peak, producing unrealistically high forecasts (e.g. 4.6 kW on 3500 Wp). + const alpha = old === null ? 1.0 : 0.10; + const newYf = old === null ? yf : alpha * yf + (1 - alpha) * old; + + this.data.solar_yield_factors[slotIndex] = newYf; + this.log(`Solar yield slot ${slotIndex}: ${old !== null ? old.toFixed(2) : 'init'} → ${newYf.toFixed(2)} (inst=${yf.toFixed(2)}, P=${Math.round(powerW)}W, R=${Math.round(radiationWm2)}W/m²)`); + + // Fire-and-forget save (not awaited — too frequent to block on) + this._saveData().catch(() => {}); + } + + /** + * Return the learned per-slot yield factors (96 entries, null = not yet learned). + * @returns {Array} + */ + getSolarYieldFactors() { + return this.data.solar_yield_factors || new Array(96).fill(null); + } + + /** + * How many slots have been learned (0–96). Fewer than ~10 means insufficient data. + * @returns {number} + */ + getSolarLearnedSlotCount() { + return (this.data.solar_yield_factors || []).filter(v => v !== null).length; + } + + /** + * Return yield factors smoothed and gap-filled for use in forecasting. + * + * Step 1 — fill null slots: linear interpolation between nearest learned neighbours. + * Slots with no learned neighbours stay 0 (e.g. deep night with no sun). + * Step 2 — 3-pass weighted smoothing (0.25 / 0.5 / 0.25): removes quantisation + * noise while preserving the physical curve shape. + * + * The raw learned values are never modified; this only affects forecast output. + * @returns {Array} 96 numbers (0 where no data and no interpolation possible) + */ + getSolarYieldFactorsSmoothed() { + const raw = this.data.solar_yield_factors || new Array(96).fill(null); + if (raw.filter(v => v !== null).length < 5) return raw.map(v => v ?? 0); + + // Step 1: fill null gaps by linear interpolation + const filled = raw.map((v, i) => { + if (v !== null) return v; + let li = -1, ri = -1; + for (let j = i - 1; j >= 0; j--) { if (raw[j] !== null) { li = j; break; } } + for (let j = i + 1; j < 96; j++) { if (raw[j] !== null) { ri = j; break; } } + if (li < 0 && ri < 0) return 0; + if (li < 0) return raw[ri]; + if (ri < 0) return raw[li]; + const t = (i - li) / (ri - li); + return raw[li] + (raw[ri] - raw[li]) * t; + }); + + // Step 2: 3-pass weighted smoothing + let result = filled; + for (let pass = 0; pass < 3; pass++) { + const s = [...result]; + for (let i = 1; i < 95; i++) { + s[i] = 0.25 * result[i - 1] + 0.5 * result[i] + 0.25 * result[i + 1]; + } + s[0] = 0.75 * result[0] + 0.25 * result[1]; + s[95] = 0.75 * result[95] + 0.25 * result[94]; + result = s; + } + + return result; + } + + /** + * Save a daily radiation forecast snapshot for tomorrow's bias comparison. + * Stored in device store (survives app restarts and redeployments). + * @param {string} dateStr - 'YYYY-MM-DD' UTC date + * @param {number} forecastAvgWm2 - Average forecasted radiation (W/m²) for daylight hours + */ + async saveForecastSnapshot(dateStr, forecastAvgWm2) { + this.data.forecast_snapshots = this.data.forecast_snapshots || {}; + this.data.forecast_snapshots[dateStr] = { forecastAvgWm2, savedAt: Date.now() }; + + // Keep only last 5 days + const keys = Object.keys(this.data.forecast_snapshots).sort(); + if (keys.length > 5) { + for (const old of keys.slice(0, keys.length - 5)) { + delete this.data.forecast_snapshots[old]; + } + } + + await this._saveData(); + } + + /** + * Retrieve a previously saved radiation forecast snapshot. + * @param {string} dateStr - 'YYYY-MM-DD' UTC date + * @returns {{ forecastAvgWm2: number, savedAt: number } | null} + */ + getForecastSnapshot(dateStr) { + return this.data.forecast_snapshots?.[dateStr] ?? null; + } + + /** + * Record policy decision and its outcome + * @param {string} mode - Recommended mode + * @param {Object} context - Decision context + */ + async recordPolicyDecision(mode, context) { + const now = Date.now(); + + this.data.policy_decisions.push({ + timestamp: now, + mode: mode, + soc: context.soc, + price: context.price, + sun_forecast: context.sun4h, + confidence: context.confidence + }); + + // Keep only last 500 decisions (~ 5 days at 15min intervals) + if (this.data.policy_decisions.length > 500) { + this.data.policy_decisions = this.data.policy_decisions.slice(-500); + } + + // Throttle saves: only persist every 5th decision (~75 min at 15-min intervals) + this._policySaveCounter = (this._policySaveCounter || 0) + 1; + if (this._policySaveCounter % 5 === 0) await this._saveData(); + } + + /** + * Get confidence adjustment based on historical success + * @param {string} mode - Proposed mode + * @param {Object} context - Current context + * @returns {number} Confidence adjustment (-20 to +20) + */ + getConfidenceAdjustment(mode, context) { + // If insufficient history, no adjustment + if (this.data.policy_decisions.length < 50) return 0; + + // Find similar past decisions (same mode, similar SoC, similar sun forecast) + const similar = this.data.policy_decisions.filter(d => { + const socMatch = Math.abs(d.soc - context.soc) < 20; // Within 20% + const sunMatch = Math.abs((d.sun_forecast || 0) - (context.sun4h || 0)) < 2; // Within 2h + return d.mode === mode && socMatch && sunMatch; + }); + + if (similar.length < 10) return 0; // Need at least 10 similar cases + + // Calculate average confidence of similar decisions + const avgConfidence = similar.reduce((sum, d) => sum + d.confidence, 0) / similar.length; + + // If we've been consistently confident in similar situations, boost slightly + // If confidence was lower, reduce slightly + const adjustment = (avgConfidence - 70) * 0.1; // Scale to ±3 points + + return Math.max(-20, Math.min(20, adjustment)); + } + + /** + * Get learning statistics for display + */ + getStatistics() { + const daysTracking = (Date.now() - this.data.stats.learning_started) / (1000 * 60 * 60 * 24); + + // Count how many hour-slots have data + let slotsWithData = 0; + for (let day = 0; day < 7; day++) { + for (let hour = 0; hour < 24; hour++) { + if (this.data.consumption_patterns[day][hour].count > 0) { + slotsWithData++; + } + } + } + + return { + days_tracking: Math.floor(daysTracking), + total_samples: this.data.stats.total_samples, + pattern_coverage: Math.round((slotsWithData / 168) * 100), // 168 = 7*24 + pv_predictions: this.data.pv_predictions.length, + pv_accuracy: Math.round(this.data.pv_accuracy_score * 100), + policy_decisions: this.data.policy_decisions.length + }; + } + + /** + * Clear all learning data (reset) + */ + async reset() { + this.data = { + consumption_patterns: this._initializeConsumptionPatterns(), + pv_predictions: [], + pv_accuracy_score: 1.0, + radiation_bias_samples: [], + radiation_bias_factor: 1.0, + policy_decisions: [], + policy_success_rate: 1.0, + last_updated: Date.now(), + stats: { + total_samples: 0, + days_tracked: 0, + learning_started: Date.now() + } + }; + + await this._saveData(); + this.log('Learning data reset'); + } + + /** + * Save data to device store + */ + async _saveData() { + this.data.last_updated = Date.now(); + await this.device.setStoreValue('learning_data', this.data); + this.log('Learning data saved'); + } +} + +module.exports = LearningEngine; diff --git a/lib/merged-price-provider.js b/lib/merged-price-provider.js new file mode 100644 index 00000000..3fda5943 --- /dev/null +++ b/lib/merged-price-provider.js @@ -0,0 +1,335 @@ +'use strict'; + +const XadiProvider = require('./xadi-provider'); +const KwhPriceProvider = require('./kwhprice-provider'); + +/** + * MergedPriceProvider + * + * Fetches from both Xadi and KwhPrice concurrently and merges their hourly + * price data into the most complete possible table. Both sources derive from + * ENTSO-E day-ahead prices, so spot prices are identical when both have a slot. + * + * Merge strategy (per UTC-hour slot): + * - Xadi wins when both sources have the same slot (trusted, server-applied markup) + * - KwhPrice fills any slot Xadi is missing + * - Result is sorted ascending by timestamp + * + * Typical coverage: + * Before ~13:15 CET → today 24h from either/both sources + * After ~13:15 CET → today + tomorrow up to 48h + * + * Exposes the same public interface as EnergyZeroProvider / KwhPriceProvider + * so it is a drop-in for TariffManager.dynamicProvider. + */ +class MergedPriceProvider { + constructor(homey, options = {}) { + this.homey = homey; + this.cache = null; + this.cacheExpiry = null; + this.log = homey.log.bind(homey); + this.error = homey.error.bind(homey); + + this.markup = options.markup || 0.11; + + // Both underlying providers share the same markup so prices are comparable + this.xadi = new XadiProvider(homey); + this.kwhprice = new KwhPriceProvider(homey, { markup: this.markup }); + + // Track which sources contributed to the last fetch + this.lastFetchSources = []; + + this.log(`MergedPriceProvider initialized with markup: €${this.markup}/kWh`); + this._cacheLoadPromise = this._loadCache(); + } + + // ─── Persistence ───────────────────────────────────────────────────────────── + + async _loadCache() { + try { + const cached = await this.homey.settings.get('merged_price_cache'); + if (cached && cached.expiry > Date.now()) { + this.cache = cached.prices.map(p => ({ ...p, timestamp: new Date(p.timestamp) })); + this.cacheExpiry = cached.expiry; + this.lastFetchSources = cached.sources || []; + + // Restore 15-min caches from what was saved — sub-providers' _saveCache() is a + // no-op (saves centrally here), so without this, cache15min is always null after + // restart and getAll15MinPrices() falls back to _expandHourlyTo15Min(). + if (cached.prices15min_kwh?.length > 0) { + this._cached15min_kwh = cached.prices15min_kwh.map(p => ({ + ...p, timestamp: new Date(p.timestamp) + })); + } + if (cached.prices15min_xadi?.length > 0) { + this._cached15min_xadi = cached.prices15min_xadi.map(p => ({ + ...p, timestamp: new Date(p.timestamp) + })); + } + + this.log( + `Loaded ${this.cache.length} merged cached prices ` + + `(sources: ${this.lastFetchSources.join('+')}, ` + + `expires in ${Math.round((this.cacheExpiry - Date.now()) / 60000)}min, ` + + `15-min: ${this._cached15min_kwh?.length || 0} kwh + ${this._cached15min_xadi?.length || 0} xadi)` + ); + } + } catch (err) { + this.log('Failed to load merged cache:', err.message); + } + } + + async _saveCache() { + try { + await this.homey.settings.set('merged_price_cache', { + prices: this.cache, + // Persist sub-provider 15-min caches so they survive restarts. + // Both sub-providers use a no-op _saveCache(), so this is the only place + // their cache15min data is written to persistent storage. + prices15min_kwh: this.kwhprice.cache15min || null, + prices15min_xadi: this.xadi.cache15min || null, + expiry: this.cacheExpiry, + sources: this.lastFetchSources, + savedAt: Date.now() + }); + } catch (err) { + this.error('Failed to save merged cache:', err.message); + } + } + + // ─── Merge logic ───────────────────────────────────────────────────────────── + + /** + * Merge two arrays of hourly price objects. + * Both arrays use the shape: { timestamp: Date, price, originalPrice, hour, readingDate } + * + * Xadi entries take priority; KwhPrice fills gaps. + * Key = UTC hour start (ISO string, truncated to the hour). + */ + _merge(xadiPrices, kwhPrices) { + const slots = new Map(); + + /** + * Snap any timestamp to the UTC hour boundary it belongs to. + * Xadi returns prices at :45 past the hour (15-min interval offset); + * snapping ensures getCurrentPrice()'s [start, start+1h) window is correct + * and that both sources key on the same bucket. + */ + const snapToHour = ts => { + const d = ts instanceof Date ? ts : new Date(ts); + return new Date( + Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours()) + ); + }; + + const normalize = p => ({ + ...p, + timestamp: snapToHour(p.timestamp) + }); + + // Add Xadi first (lower priority — used as fallback for hours KwhPrice doesn't have) + for (const p of xadiPrices) { + const n = normalize(p); + slots.set(n.timestamp.toISOString(), { ...n, _source: 'xadi' }); + } + + // Overwrite with KwhPrice (higher priority — final EPEX auction prices) + for (const p of kwhPrices) { + const n = normalize(p); + slots.set(n.timestamp.toISOString(), { ...n, _source: 'kwhprice' }); + } + + return Array.from(slots.values()) + .sort((a, b) => a.timestamp - b.timestamp); + } + + // ─── Public API ─────────────────────────────────────────────────────────────── + + async fetchPrices(force = false) { + // Wait for the initial settings cache load (constructor calls _loadCache async) + if (this._cacheLoadPromise) { + await this._cacheLoadPromise; + this._cacheLoadPromise = null; + } + if (!force && this.cache && this.cacheExpiry > Date.now()) { + this.log(`Using merged cache (${this.cache.length}h, sources: ${this.lastFetchSources.join('+')})`); + + // Restore sub-provider 15-min caches from the saved data loaded in _loadCache(). + // This ensures getAll15MinPrices() (called by TariffManager) returns native 15-min + // data instead of falling back to _expandHourlyTo15Min() on every restart. + // Sub-providers' own _loadCache() doesn't set cache15min (their _saveCache is no-op), + // so this is the only way to populate them when the hourly cache is still valid. + if (!this.kwhprice.cache15min && this._cached15min_kwh?.length > 0) { + this.kwhprice.cache15min = this._cached15min_kwh; + this.log(`♻️ Restored ${this.kwhprice.cache15min.length} KwhPrice 15-min prices from merged cache`); + } + if (!this.xadi.cache15min && this._cached15min_xadi?.length > 0) { + this.xadi.cache15min = this._cached15min_xadi; + this.log(`♻️ Restored ${this.xadi.cache15min.length} Xadi 15-min prices from merged cache`); + } + + return this.cache; + } + + // Fetch both concurrently; treat each failure independently + const [xadiResult, kwhResult] = await Promise.allSettled([ + this.xadi.fetchPrices(force), + this.kwhprice.fetchPrices(force) + ]); + + const xadiPrices = xadiResult.status === 'fulfilled' ? (xadiResult.value || []) : []; + const kwhPrices = kwhResult.status === 'fulfilled' ? (kwhResult.value || []) : []; + + if (xadiResult.status === 'rejected') { + this.log('⚠️ Xadi fetch failed in merge:', xadiResult.reason?.message); + } + if (kwhResult.status === 'rejected') { + this.log('⚠️ KwhPrice fetch failed in merge:', kwhResult.reason?.message); + } + + if (xadiPrices.length === 0 && kwhPrices.length === 0) { + // Both failed — return stale cache if available + if (this.cache?.length > 0) { + this.log('⚠️ Both sources failed, returning stale merged cache'); + return this.cache; + } + throw new Error('MergedPriceProvider: both Xadi and KwhPrice returned no data'); + } + + this.lastFetchSources = [ + ...(xadiPrices.length > 0 ? ['xadi'] : []), + ...(kwhPrices.length > 0 ? ['kwhprice'] : []) + ]; + + this.cache = this._merge(xadiPrices, kwhPrices); + this.cacheExpiry = Date.now() + 60 * 60 * 1000; // 1 hour + await this._saveCache(); + + // Detailed coverage log + const xadiSlots = this.cache.filter(p => p._source === 'xadi').length; + const kwhSlots = this.cache.filter(p => p._source === 'kwhprice').length; + const totalH = this.cache.length; + const days = totalH > 24 ? 'today + tomorrow' : 'today only'; + + this.log( + `✅ Merged price table: ${totalH}h (${days}) — ` + + `${xadiSlots}h from Xadi, ${kwhSlots}h from KwhPrice` + ); + + if (this.cache.length > 0) { + const first = this.cache[0]; + const last = this.cache[this.cache.length - 1]; + this.log( + `Range: ${first.timestamp instanceof Date ? first.timestamp.toISOString() : first.timestamp} ` + + `(${first.hour}:00) → ` + + `${last.timestamp instanceof Date ? last.timestamp.toISOString() : last.timestamp} ` + + `(${last.hour}:00)` + ); + } + + return this.cache; + } + + getCurrentRate() { + if (!this.cache || this.cache.length === 0) return 'standard'; + + const now = new Date(); + let current = this.cache.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + const end = new Date(start.getTime() + 3600 * 1000); + return now >= start && now < end; + }); + + if (!current) { + const currentHour = parseInt( + now.toLocaleString('en-US', { timeZone: 'Europe/Amsterdam', hour: '2-digit', hour12: false }) + ); + current = this.cache.find(p => p.hour === currentHour); + } + + if (!current) current = this.cache[0]; + + const prices = this.cache.map(p => p.price); + const avg = prices.reduce((a, b) => a + b, 0) / prices.length; + const stdDev = this._calculateStdDev(prices, avg); + const price = current.price; + + if (price <= avg - stdDev * 0.5) return 'low'; + if (price >= avg + stdDev * 0.5) return 'peak'; + return 'standard'; + } + + getNextRateChange() { + if (!this.cache || this.cache.length === 0) return null; + const now = new Date(); + const next = this.cache.find(p => { + const ts = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + return ts > now; + }); + return next ? next.timestamp : null; + } + + getCurrentPrice() { + if (!this.cache || this.cache.length === 0) return null; + const now = new Date(); + const current = this.cache.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + const end = new Date(start.getTime() + 3600 * 1000); + return now >= start && now < end; + }); + return current ? current.price : null; + } + + getPriceStatistics() { + if (!this.cache || this.cache.length === 0) { + return { avg: null, min: null, max: null, stdDev: null }; + } + const prices = this.cache.map(p => p.price); + const avg = prices.reduce((a, b) => a + b, 0) / prices.length; + return { + avg, + min: Math.min(...prices), + max: Math.max(...prices), + stdDev: this._calculateStdDev(prices, avg) + }; + } + + getTop3Cheapest() { + if (!this.cache || this.cache.length === 0) return []; + return [...this.cache] + .sort((a, b) => a.price - b.price) + .slice(0, 3) + .map(p => ({ hour: p.hour, price: p.price, timestamp: p.timestamp })); + } + + getTop3MostExpensive() { + if (!this.cache || this.cache.length === 0) return []; + return [...this.cache] + .sort((a, b) => b.price - a.price) + .slice(0, 3) + .map(p => ({ hour: p.hour, price: p.price, timestamp: p.timestamp })); + } + + hasPrices() { + return !!(this.cache && this.cache.length > 0); + } + + getAllHourlyPrices() { + if (!this.cache || this.cache.length === 0) return []; + const now = new Date(); + return this.cache.map(p => ({ + hour: p.hour, + index: Math.floor((new Date(p.timestamp) - now) / (1000 * 60 * 60)), + price: p.price, + timestamp: p.timestamp, + source: p._source // bonus: callers can see where each hour came from + })); + } + + _calculateStdDev(values, mean) { + const sq = values.map(v => Math.pow(v - mean, 2)); + return Math.sqrt(sq.reduce((a, b) => a + b, 0) / values.length); + } +} + +module.exports = MergedPriceProvider; \ No newline at end of file diff --git a/lib/optimization-engine.js b/lib/optimization-engine.js new file mode 100644 index 00000000..ea3f6620 --- /dev/null +++ b/lib/optimization-engine.js @@ -0,0 +1,264 @@ +'use strict'; + +/** + * OptimizationEngine — 24h DP-based battery scheduling bias layer. + * + * Runs backward induction over the available price horizon to find the + * globally optimal charge / preserve / discharge sequence. The result is + * exposed as a per-slot hint that PolicyEngine adds as a ~60-point bias, + * strong enough to guide but NOT strong enough to override real-time safety + * rules (PV surplus, SoC limits, delay-charge, etc.). + * + * Architecture: + * PolicyEngine keeps all edge-case handling. + * OptimizationEngine provides a 24-h lookahead that the heuristic rules lack. + */ +class OptimizationEngine { + constructor(settings) { + this.RTE = settings.battery_efficiency || 0.75; + this.minSoc = settings.min_soc ?? 0; + this.maxSoc = settings.max_soc ?? 95; + this.cycleCostPerKwh = settings.cycle_cost_per_kwh ?? 0.075; + // NL saldering (net metering) is active until 2027: export earns full retail price. + // Set to actual export/import ratio when saldering ends. + this.exportPriceRatio = settings.export_price_ratio ?? 1.0; + this._schedule = null; // { computedAt: number, slots: [{timestamp, action}] } + } + + /** + * Compute the optimal 24-h schedule via backward-induction DP. + * + * @param {Array<{timestamp: string|Date, price: number}>} prices + * Hourly price slots sorted ascending (allPrices / next24Hours). + * @param {number} currentSoc — current state of charge (0-100 %) + * @param {number} capacityKwh — usable battery capacity in kWh + * @param {number} maxChargePowerW — max charge power in W + * @param {number} maxDischargePowerW — max discharge power in W + * @param {Array<{timestamp: string|Date, pvPowerW: number}>} [pvForecast] + * Optional per-slot PV power estimate (W). Slots with PV reduce effective + * grid-charge cost proportionally — the DP prefers charging during PV hours. + * @param {number|null} [rte] + * Round-trip efficiency override (0–1). Falls back to this.RTE when null. + * @param {Array|null} [consumptionWPerSlot] + * Expected house consumption per slot in W. When provided and exportPriceRatio < 1, + * discharge value is split: local consumption offset at 100% price, export at exportPriceRatio. + * @param {number} [minDischargePrice] + * Minimum price (€/kWh) at which discharge is allowed. Slots below this threshold + * are treated as discharge-blocked so the DP never schedules discharge there. + * Defaults to 0 (no constraint). Must match the policy-engine's min_discharge_price setting. + */ + compute(prices, currentSoc, capacityKwh, maxChargePowerW, maxDischargePowerW, pvForecast, rte = null, consumptionWPerSlot = null, minDischargePrice = 0) { + if (!prices || prices.length === 0 || !capacityKwh || capacityKwh <= 0) return; + + const N = prices.length; + // Auto-detect slot duration from timestamps (supports both 1h and 15-min data) + const slotH = (prices.length >= 2) + ? (new Date(prices[1].timestamp) - new Date(prices[0].timestamp)) / 3_600_000 + : 1; + + const effectiveRte = (rte != null && rte > 0.3 && rte <= 1) ? rte : this.RTE; + const cycleCostPerKwh = this.cycleCostPerKwh ?? 0; + + // SoC delta per full slot (integer %, clamped to at least 1) + const chargeSocDelta = Math.max(1, Math.round( + (maxChargePowerW / 1000) * slotH * effectiveRte * 100 / capacityKwh + )); + + // kWh exchanged per charge slot — used for proportional edge scaling + const chargeKwhFull = (maxChargePowerW / 1000) * slotH; + + // Per-slot effective discharge power: zero_discharge_only tracks house consumption + // (grid ~0W), so the battery only discharges as fast as the house consumes — not at + // max discharge power. Falls back to maxDischargePowerW when no consumption data. + const effectiveDischargePowerW = prices.map((_, t) => { + const consumptionW = Array.isArray(consumptionWPerSlot) && consumptionWPerSlot[t] != null + ? consumptionWPerSlot[t] + : maxDischargePowerW; + return Math.min(maxDischargePowerW, consumptionW); + }); + const perSlotDischargeSocDelta = effectiveDischargePowerW.map(w => + Math.max(1, Math.round((w / 1000) * slotH * 100 / capacityKwh)) + ); + const perSlotDischargeKwhFull = effectiveDischargePowerW.map(w => + (w / 1000) * slotH + ); + + const minSoc = this.minSoc; + const maxSoc = this.maxSoc; + + // Pre-compute per-slot PV coverage (0–1): fraction of charge power covered by PV + const pvCoverage = prices.map(p => { + const pvW = this._getPvForSlot(pvForecast, p.timestamp); + return Math.min(pvW / maxChargePowerW, 1); + }); + + // dp[soc] = max future profit achievable with this SoC from the current slot onward + let dp = new Float64Array(101).fill(0); + + // policy[t][soc] = best action code: 0 = preserve, 1 = charge, 2 = discharge + const policy = Array.from({ length: N }, () => new Uint8Array(101)); + + // pvStrong threshold: matches policy-engine's pvStrong (≥400 W) used in + // _mapActionToHwModeForPlanning. Only above this coverage does the firmware + // run zero_charge_only during preserve — below it the battery stays in standby + // and gains no free SoC from PV. + const pvStrongCoverage = maxChargePowerW > 0 ? 400 / maxChargePowerW : 0.15; + + // ── Backward induction ──────────────────────────────────────────────────── + for (let t = N - 1; t >= 0; t--) { + const price = prices[t].price; + // Grid charge cost reduced by PV coverage (fully free when PV >= charge power) + const effectiveChargeCost = price * (1 - pvCoverage[t]); + const newDp = new Float64Array(101).fill(-1e9); + + + for (let soc = 0; soc <= 100; soc++) { + + // Preserve: firmware runs zero_charge_only only when PV is strong (≥400 W), + // so only apply free SoC gain above that threshold. Weak PV results in + // standby — no free charging. + const pvSocGain = pvCoverage[t] >= pvStrongCoverage + ? Math.round(pvCoverage[t] * chargeSocDelta) + : 0; + const preserveSoc = Math.min(maxSoc, soc + pvSocGain); + const vPreserve = dp[preserveSoc]; + + // Charge: SoC rises; cost is reduced when PV covers part of the charge power. + // Half the cycle cost applies here (wear from charging), regardless of PV coverage. + let vCharge = -1e9; + if (soc < maxSoc) { + const newSoc = Math.min(maxSoc, soc + chargeSocDelta); + const socDelta = newSoc - soc; // may be less than chargeSocDelta at the top + const kwh = chargeKwhFull * socDelta / chargeSocDelta; + vCharge = -(effectiveChargeCost + cycleCostPerKwh * 0.5) * kwh + dp[newSoc]; + } + + // Discharge: SoC falls, avoided grid cost. + // If consumption data is available, discharge beyond local demand is export + // (worth exportPriceRatio of retail price vs. 100% for local consumption offset). + // Block discharge when PV is strong: the HW battery can't discharge AND capture PV + // in the same slot. Also prevents the irrational "discharge at maxSoc, recharge from + // PV for free" pattern — the PV energy used for recharging has an opportunity cost + // (it could export at market price), so such cycles are net-negative after cycle costs. + // Also block discharge when price is below the user's minimum discharge threshold — + // this keeps the DP schedule consistent with _mapActionToHwModeForPlanning which + // shows 'standby' for those slots. Without this guard the DP internally discharges + // but the display shows standby, causing an unexplained SoC drop in the chart. + let vDischarge = -1e9; + if (soc > minSoc && pvCoverage[t] <= 0.5 && price >= minDischargePrice) { + const slotDischargeSocDelta = perSlotDischargeSocDelta[t]; + const slotDischargeKwhFull = perSlotDischargeKwhFull[t]; + const newSoc = Math.max(minSoc, soc - slotDischargeSocDelta); + const socDelta = soc - newSoc; + const kwh = slotDischargeKwhFull * socDelta / slotDischargeSocDelta; + // null = no data (assume full local offset); 0 = learned zero consumption (all export) + const consumptionKwh = Array.isArray(consumptionWPerSlot) && consumptionWPerSlot[t] != null + ? (consumptionWPerSlot[t] / 1000) * slotH + : null; + // With NL saldering (net metering, active until 2027), export earns the full + // retail price — so the consumption split does not reduce discharge value. + // exportPriceRatio defaults to 1.0 and can be lowered post-saldering. + const exportPriceRatio = this.exportPriceRatio ?? 1.0; + let dischargeValue; + if (consumptionKwh != null && exportPriceRatio < 1.0) { + const coveredKwh = Math.min(kwh, consumptionKwh); + const exportKwh = kwh - coveredKwh; + dischargeValue = price * coveredKwh + price * exportPriceRatio * exportKwh; + } else { + dischargeValue = price * kwh; + } + // Half the cycle cost applies on discharge (other half was on charge). + vDischarge = dischargeValue - cycleCostPerKwh * 0.5 * kwh + dp[newSoc]; + } + + // Pick best action + let best = vPreserve, bestAction = 0; + if (vCharge > best) { best = vCharge; bestAction = 1; } + if (vDischarge > best) { best = vDischarge; bestAction = 2; } + + newDp[soc] = best > -1e9 ? best : 0; + policy[t][soc] = bestAction; + } + + dp = newDp; + } + + // ── Forward pass: trace the optimal path from currentSoc ────────────────── + const slots = []; + const ACTIONS = ['preserve', 'charge', 'discharge']; + let soc = Math.max(0, Math.min(100, Math.round(currentSoc))); + + for (let t = 0; t < N; t++) { + const code = policy[t][soc]; + const action = ACTIONS[code]; + slots.push({ timestamp: prices[t].timestamp, action, price: prices[t].price, socProjected: soc }); + + if (code === 1) soc = Math.min(maxSoc, soc + chargeSocDelta); + else if (code === 2) soc = Math.max(minSoc, soc - perSlotDischargeSocDelta[t]); + else if (pvCoverage[t] >= pvStrongCoverage) { + // Preserve + PV strong enough for zero_charge_only: battery charges for free. + // Below pvStrongCoverage the firmware stays in standby — no free SoC gain. + soc = Math.min(maxSoc, soc + Math.round(pvCoverage[t] * chargeSocDelta)); + } + } + + this._schedule = { computedAt: Date.now(), slots }; + } + + /** + * Find the PV power (W) forecast for a given price-slot timestamp. + * Returns 0 when no PV data is available or no matching slot found. + * @private + */ + _getPvForSlot(pvForecast, timestamp) { + if (!Array.isArray(pvForecast) || pvForecast.length === 0) return 0; + const slotMs = new Date(timestamp).getTime(); + let best = 0, bestDist = Infinity; + for (const s of pvForecast) { + const dist = Math.abs(new Date(s.timestamp).getTime() - slotMs); + if (dist < bestDist) { bestDist = dist; best = s.pvPowerW; } + } + return bestDist <= 35 * 60 * 1000 ? best : 0; + } + + /** + * Return the optimal action for the current time slot, or null if unknown. + * Matches the slot whose timestamp is closest to `now`, within ±35 minutes. + * + * @param {Date} now + * @returns {'charge'|'discharge'|'preserve'|null} + */ + getSlot(now) { + if (!this._schedule) return null; + + const nowMs = now.getTime(); + let best = null, bestDist = Infinity; + + for (const slot of this._schedule.slots) { + const dist = Math.abs(new Date(slot.timestamp).getTime() - nowMs); + if (dist < bestDist) { bestDist = dist; best = slot; } + } + + return (best && bestDist <= 35 * 60 * 1000) ? best.action : null; + } + + /** + * True when the schedule is missing or older than maxAgeMs (default 90 min). + * PolicyEngine triggers recomputation when this returns true. + */ + isStale(maxAgeMs = 90 * 60 * 1000) { + return !this._schedule || (Date.now() - this._schedule.computedAt > maxAgeMs); + } + + /** Propagate settings changes and invalidate the cached schedule. */ + updateSettings(newSettings) { + if (newSettings.battery_efficiency != null) this.RTE = newSettings.battery_efficiency; + if (newSettings.min_soc != null) this.minSoc = newSettings.min_soc; + if (newSettings.max_soc != null) this.maxSoc = newSettings.max_soc; + if (newSettings.cycle_cost_per_kwh != null) this.cycleCostPerKwh = newSettings.cycle_cost_per_kwh; + if (newSettings.export_price_ratio != null) this.exportPriceRatio = newSettings.export_price_ratio; + this._schedule = null; // invalidate — will recompute on next policy run + } +} + +module.exports = OptimizationEngine; diff --git a/lib/planner-engine.js b/lib/planner-engine.js new file mode 100644 index 00000000..e6156479 --- /dev/null +++ b/lib/planner-engine.js @@ -0,0 +1,190 @@ +/** + * PlannerEngine - Simplified policy engine for 48-hour planning view + * + * Unlike PolicyEngine which makes real-time decisions with live sensor data, + * PlannerEngine projects recommendations for future hours based only on: + * - Price forecasts + * - Top-3 hour badges + * - Sun forecasts + * - Battery SoC projections + * + * Used by: Settings page planning view (HTML) + */ + +class PlannerEngine { + constructor(settings) { + // Battery specs + this.RTE = settings.battery_efficiency || 0.75; // 75% round-trip efficiency + this.BREAKEVEN_MULTIPLIER = 1 / this.RTE; // 1.333 for 75% RTE + this.MIN_PROFIT_MARGIN = settings.min_profit_margin ?? 0.02; // €0.02/kWh default + + // User thresholds + this.maxChargePrice = settings.max_charge_price || 0.15; + this.minDischargePrice = settings.min_discharge_price || 0.30; + this.minSOC = settings.min_soc || 10; + this.maxSOC = settings.max_soc || 95; + + // Mode + this.tariffType = settings.tariff_type || 'dynamic'; + } + + /** + * Get mode recommendation for a specific hour + * @param {Object} hourData - { hour, price, isPeak, isCheap, hasSun, projectedSOC, hoursFromNow } + * @param {Array} allPrices - Full price array with {price, hour, timestamp, index} + * @returns {string} - Mode: 'charge', 'discharge', 'pv_only', 'standby' + */ + getRecommendationForHour(hourData, allPrices) { + const { hour, price, isPeak, isCheap, hasSun, projectedSOC, hoursFromNow } = hourData; + + // If no price data, can't make recommendation + if (price === null || price === undefined) { + return 'standby'; + } + + // Battery limits + if (projectedSOC >= this.maxSOC) { + return 'discharge'; // Battery full, must discharge + } + if (projectedSOC <= this.minSOC) { + return 'charge'; // Battery empty, must charge + } + + // Peak shaving mode + if (this.tariffType === 'fixed') { + if (isPeak) return 'discharge'; + if (isCheap) return 'charge'; + if (hasSun && hour >= 8 && hour <= 17) return 'pv_only'; + return 'standby'; + } + + // Dynamic pricing mode + + // Priority 1: Top-3 badges (most reliable signals) + if (isPeak) { + // Top-3 expensive hour → discharge to avoid buying at this price + return 'discharge'; + } + + if (isCheap) { + // Top-3 cheap hour → check if profitable vs future + const futureExpensive = this._getFutureExpensiveHours(price, hoursFromNow, allPrices); + if (futureExpensive && futureExpensive.length > 0) { + const avgFuture = futureExpensive.reduce((s, h) => s + h.price, 0) / futureExpensive.length; + const profitPerKwh = (avgFuture * this.RTE) - price; + + if (profitPerKwh > this.MIN_PROFIT_MARGIN) { + return 'charge'; // Profitable arbitrage available + } + } + // Top-3 cheap but no profitable future → wait for PV or better opportunity + return hasSun ? 'pv_only' : 'standby'; + } + + // Priority 2: Threshold checks + if (price <= this.maxChargePrice) { + // Cheap by absolute threshold → check profitability + const futureExpensive = this._getFutureExpensiveHours(price, hoursFromNow, allPrices); + if (futureExpensive && futureExpensive.length > 0) { + const avgFuture = futureExpensive.reduce((s, h) => s + h.price, 0) / futureExpensive.length; + const profitPerKwh = (avgFuture * this.RTE) - price; + + if (profitPerKwh > this.MIN_PROFIT_MARGIN) { + return 'charge'; + } + } + return hasSun ? 'pv_only' : 'standby'; + } + + if (price >= this.minDischargePrice) { + // Expensive by absolute threshold → discharge + return 'discharge'; + } + + // Priority 3: Relative price position + arbitrage check + // Even if price doesn't hit absolute thresholds, check if arbitrage is available + const futureExpensive = this._getFutureExpensiveHours(price, hoursFromNow, allPrices); + if (futureExpensive && futureExpensive.length > 0) { + const avgFuture = futureExpensive.reduce((s, h) => s + h.price, 0) / futureExpensive.length; + const profitPerKwh = (avgFuture * this.RTE) - price; + + // If profitable arbitrage exists and battery not nearly full + if (profitPerKwh > this.MIN_PROFIT_MARGIN && projectedSOC < this.maxSOC - 10) { + return 'charge'; // Grid charge profitable + } + } + + // Priority 4: Sun forecast (daytime hours only) + if (hasSun && hour >= 8 && hour <= 17) { + return 'pv_only'; + } + + // Default: standby (wait for better signal) + return 'standby'; + } + + /** + * Find future hours where price exceeds breakeven threshold + * Same logic as PolicyEngine._getFutureExpensiveHours but simpler + */ + _getFutureExpensiveHours(currentPrice, fromHoursFromNow, allPrices) { + if (!allPrices || allPrices.length === 0) return null; + + // Breakeven: future price must be currentPrice / RTE to be profitable + const breakeven = currentPrice * this.BREAKEVEN_MULTIPLIER; + + // Look 24 hours ahead from fromHoursFromNow + const endHoursFromNow = fromHoursFromNow + 24; + + // Filter to future hours within window that exceed breakeven + const expensiveHours = allPrices.filter(h => { + const hourIndex = h.index ?? h.hoursFromNow ?? 0; + return hourIndex > fromHoursFromNow && + hourIndex <= endHoursFromNow && + h.price >= breakeven; + }); + + return expensiveHours.length > 0 ? expensiveHours : null; + } + + /** + * Get a human-readable explanation of the recommendation + */ + getExplanation(hourData, mode, allPrices) { + const { hour, price, isPeak, isCheap, hasSun, projectedSOC, hoursFromNow } = hourData; + + if (mode === 'discharge') { + if (isPeak) return `Peak hour (€${price.toFixed(3)}) - discharge to avoid buying expensive power`; + if (price >= this.minDischargePrice) return `Expensive (≥€${this.minDischargePrice}) - discharge saves money`; + if (projectedSOC >= this.maxSOC) return `Battery full (${projectedSOC}%) - must discharge`; + return 'Discharge recommended'; + } + + if (mode === 'charge') { + const futureExpensive = this._getFutureExpensiveHours(price, hoursFromNow, allPrices); + if (futureExpensive && futureExpensive.length > 0) { + const avgFuture = futureExpensive.reduce((s, h) => s + h.price, 0) / futureExpensive.length; + const profit = (avgFuture * this.RTE) - price; + return `Cheap (€${price.toFixed(3)}) vs future avg €${avgFuture.toFixed(3)} = +€${profit.toFixed(3)}/kWh profit`; + } + if (isCheap) return `Top-3 cheap hour (€${price.toFixed(3)})`; + if (projectedSOC <= this.minSOC) return `Battery low (${projectedSOC}%) - must charge`; + return 'Charge recommended'; + } + + if (mode === 'pv_only') { + return `Sun expected - charge from PV only`; + } + + // standby + if (!isCheap && !isPeak) { + return `Normal price (€${price.toFixed(3)}) - wait for PV or better opportunity`; + } + return 'Standby'; + } +} + +// Export for use in both Node.js (settings page backend) and browser +if (typeof module !== 'undefined' && module.exports) { + module.exports = PlannerEngine; +} \ No newline at end of file diff --git a/lib/policy-engine.js b/lib/policy-engine.js new file mode 100644 index 00000000..4925a5d7 --- /dev/null +++ b/lib/policy-engine.js @@ -0,0 +1,1696 @@ +'use strict'; + +// ====================================================== +// CHANGES IN THIS VERSION: +// 1-9: (previous changes preserved) +// 10. respect_minmax setting: strict vs dynamic threshold enforcement +// ====================================================== + +const debug = false; + +class PolicyEngine { + constructor(homey, settings) { + this.homey = homey; + this.settings = settings; + this.log = (...args) => homey.log('[PolicyEngine]', ...args); + + this.BATTERY_EFFICIENCY = settings.battery_efficiency || 0.75; + this.BREAKEVEN_MULTIPLIER = 1 / this.BATTERY_EFFICIENCY; + this.MIN_PROFIT_MARGIN = settings.min_profit_margin ?? 0.01; + + this.currentLoad = 0; + this.maxDischarge = 0; + } + + _getDynamicChargePrice(tariff, currentPrice) { + const staticMax = this.settings.max_charge_price || 0.15; + + if (!tariff) return staticMax; + + const pricesArray = tariff.effectivePrices || tariff.allPrices || tariff.next24Hours; + if (!Array.isArray(pricesArray) || pricesArray.length === 0) return staticMax; + + const now = new Date(); + + const futurePrices = pricesArray + .filter(h => { + if (h.timestamp) { + const ts = new Date(h.timestamp); + return ts > now && ts <= new Date(now.getTime() + 24 * 3600_000); + } + const idx = h.index ?? 0; + return idx >= 1 && idx <= 24; + }) + .map(h => h.price) + .filter(p => typeof p === 'number' && p > 0); + + if (futurePrices.length === 0) return staticMax; + + const maxFuturePrice = Math.max(...futurePrices); + const dynamicThreshold = (maxFuturePrice * this.BATTERY_EFFICIENCY) - this.MIN_PROFIT_MARGIN; + + // Cap: never charge above the break-even of the configured discharge price. + // Charging at more than (minDischarge × RTE - margin) means you can't profit + // at minDischargePrice — there's no point. + const minDischarge = this.settings.min_discharge_price || 0.30; + const maxByDischarge = (minDischarge * this.BATTERY_EFFICIENCY) - this.MIN_PROFIT_MARGIN; + + // Effective = opportunistic up to the discharge break-even, but never below staticMax + const effectiveMax = Math.max(staticMax, Math.min(dynamicThreshold, maxByDischarge)); + + this.log(`DynamicChargePrice: maxFuturePrice=€${maxFuturePrice.toFixed(3)}, dynamic=€${dynamicThreshold.toFixed(3)}, static=€${staticMax.toFixed(3)}, maxByDischarge=€${maxByDischarge.toFixed(3)}, effective=€${effectiveMax.toFixed(3)}`); + + return effectiveMax; + } + + calculatePolicy(inputs) { + const scores = { charge: 0, discharge: 0, preserve: 0 }; + + this.log('--- POLICY RUN START ---'); + if (debug) this.log('Inputs:', JSON.stringify(inputs, null, 2)); + + this.maxDischarge = inputs.battery?.maxDischargePowerW || + (inputs.battery?.totalCapacityKwh + ? Math.max(1, Math.round(inputs.battery.totalCapacityKwh / 2.688)) * 800 + : 800); + + const grid = inputs.p1?.resolved_gridPower ?? 0; + const batt = inputs.p1?.battery_power ?? 0; + const dischargeNow = batt < 0 ? Math.abs(batt) : 0; + this.currentLoad = Math.max(0, grid + dischargeNow); + + const batteryCanCover = this.currentLoad <= this.maxDischarge; + const coverageRatio = this.currentLoad > 0 ? Math.min(this.maxDischarge / this.currentLoad, 1.0) : 0; + + this.log(`Battery limits: max=${this.maxDischarge}W, load=${this.currentLoad}W, canCover=${batteryCanCover}, coverage=${Math.round(coverageRatio * 100)}%`); + + inputs.batteryLimits = { + maxDischarge: this.maxDischarge, + currentLoad: this.currentLoad, + canCoverLoad: batteryCanCover, + coverageRatio + }; + + const soc = inputs.battery?.stateOfCharge ?? 50; + const maxSoc = this.settings.max_soc ?? 95; + const minSoc = this.settings.min_soc ?? 0; + + if (inputs.batteryCost) { + // Reset cost model whenever battery is at or below min SoC — firmware cuts power + // flow to 0W when empty, so we can't rely on isDischarging being true + if (soc <= Math.max(minSoc, 1)) { + if (inputs.batteryCost.avgCost !== 0 || inputs.batteryCost.energyKwh !== 0) { + this.log(`[COST][RESET] SoC ${soc}% <= min_soc ${minSoc}% → battery empty, resetting cost model`); + } + inputs.batteryCost.avgCost = 0; + inputs.batteryCost.energyKwh = 0; + inputs.batteryCost.breakEven = 0; + } + } + + inputs.dynamicMaxChargePrice = this._getDynamicChargePrice( + inputs.tariff, inputs.tariff?.currentPrice + ); + + const _cetHour = parseInt(new Date().toLocaleString('en-US', { + hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' + }), 10); + const _isDaylight = _cetHour >= 7 && _cetHour < 20; + + // Detect PV surplus: either exporting to grid OR battery is absorbing PV OR pvEstimate shows production + const _pvSurplusDetected = grid < -200 || batt > 100 || (inputs.p1?.pv_power_estimated ?? 0) >= 200; + + if (_pvSurplusDetected && soc < maxSoc && _isDaylight) { + // Compare: export surplus to grid NOW vs store in battery for later discharge + // When battery is in zero_charge_only, PV goes to battery first → grid stays near 0 + // So we also detect PV via batteryPower and pvEstimate, not just grid export + const _pvCurrentPrice = inputs.tariff?.currentPrice ?? null; + const _pvPricesArray = inputs.tariff?.effectivePrices || inputs.tariff?.allPrices || inputs.tariff?.next24Hours || []; + const _sph = Math.round(1 / (inputs.tariff?.slotHours ?? 1)); // slots per hour + const _pvNow = new Date(); + const _pvFuturePrices = _pvPricesArray + .filter(h => h.timestamp ? new Date(h.timestamp) > _pvNow : (h.index ?? 0) >= 1) + .map(h => h.price) + .filter(p => typeof p === 'number' && p > 0); + const _pvMaxFuture = _pvFuturePrices.length ? Math.max(..._pvFuturePrices) : null; + const _pvStoreValue = _pvMaxFuture !== null ? _pvMaxFuture * this.BATTERY_EFFICIENCY : null; + // Store decision in inputs so Arbitrage block can respect same opportunity-cost logic + inputs._pvExporting = true; + inputs._pvStoreValue = _pvStoreValue; + inputs._pvCurrentPrice = _pvCurrentPrice; + + // ✅ DELAY-CHARGE CHECK: If enough sun is coming later at cheaper prices, + // "export now + charge from free PV later" beats "charge now" by €current/kWh. + // Both paths charge for free (PV), but only the delay path earns grid export revenue too. + const _targetSocForDelay = this.settings.max_soc ?? 95; + const _battCapForDelay = (inputs.battery?.maxChargePowerW + ? Math.max(1, Math.round(inputs.battery.maxChargePowerW / 800)) * 2.688 + : 2.688); + const _toChargeKwhCheck = ((_targetSocForDelay - soc) / 100) * _battCapForDelay; + + let _canDelayCharge = false; + if (_toChargeKwhCheck <= 0) { + // Battery already at target SoC — nothing to delay-charge. Let discharge scoring decide. + this.log(`PV OVERSCHOT: delay-charge skipped — battery already full (SoC ${soc}% >= target ${_targetSocForDelay}%)`); + } else if (_pvCurrentPrice !== null && _pvCurrentPrice > 0) { + // ✅ CRITICAL: Don't delay during excellent arbitrage opportunities + const _dynamicMax = inputs.dynamicMaxChargePrice ?? this.settings.max_charge_price ?? 0.15; + const _maxFuture = _pvMaxFuture ?? 0; + const _spreadProfit = (_maxFuture * this.BATTERY_EFFICIENCY) - _pvCurrentPrice; + const _excellentArbitrage = _spreadProfit >= 0.10; // €0.10/kWh spread is excellent + + // Delay-charge only worthwhile at high export prices (≥ maxCharge + €0.08). + // At low/moderate prices the net benefit is marginal (~€0.15) and the risk of + // staying empty outweighs it. With maxCharge=€0.14 this threshold is €0.22. + const _minDelayPrice = _dynamicMax + 0.08; + + if (_pvCurrentPrice <= _dynamicMax && _excellentArbitrage) { + this.log(`PV OVERSCHOT: delay-charge SKIPPED — current price €${_pvCurrentPrice.toFixed(3)} is cheap + excellent spread €${_spreadProfit.toFixed(3)}/kWh → allow grid charging`); + } else if (_pvCurrentPrice < _minDelayPrice) { + this.log(`PV OVERSCHOT: delay-charge SKIPPED — current price €${_pvCurrentPrice.toFixed(3)} < min €${_minDelayPrice.toFixed(3)} (maxCharge €${_dynamicMax.toFixed(3)} + €0.08) → benefit too small, charge now`); + } else { + // Calculate battery charge requirements first (needed for _cheaperLater check too) + const _battChargePowerW = inputs.battery?.maxChargePowerW || + (inputs.battery?.totalCapacityKwh + ? Math.max(1, Math.round(inputs.battery.totalCapacityKwh / 2.688)) * 800 + : 800); + const _pvCapW = this.settings.pv_capacity_w || 3000; + const _targetSoc = this.settings.max_soc ?? 95; + const _estCapKwh = (_battChargePowerW / 800) * 2.688; + const _toChargeKwh = ((_targetSoc - soc) / 100) * _estCapKwh; + const _hoursNeeded = _toChargeKwh / (_battChargePowerW / 1000); + + // Check: are there significantly cheaper hours coming — AND enough of them to fill the battery? + // 24h window: catches tonight's cheap grid slots (e.g. 22:00 when current time is 09:00) + const _cheapLaterHours = _pvPricesArray.filter(p => + typeof p.index === 'number' && + p.index > 2 * _sph && p.index <= 24 * _sph && + typeof p.price === 'number' && + p.price < _pvCurrentPrice * 0.70 // At least 30% cheaper + ); + const _cheaperLater = _cheapLaterHours.length > 0 && _cheapLaterHours.length >= Math.ceil(_hoursNeeded * _sph); + + if (_cheaperLater) { + // Count remaining hours where radiation is high enough to actually charge the battery. + // Minimum radiation threshold: W/m² needed so that PV output >= battery charge power. + // At 1000 W/m² (STC) a pvCapW system produces full power; scale linearly. + const _pvCanSupport = _pvCapW >= _battChargePowerW * 0.8; + const _minRadiation = _pvCapW > 0 ? (_battChargePowerW / _pvCapW) * 1000 : 0; + const _hourlyForecast = inputs.weather?.hourlyForecast; + const _now = Date.now(); + + let _effectiveSunHours = 0; + if (Array.isArray(_hourlyForecast) && _hourlyForecast.length > 0 && _minRadiation > 0) { + // Count future hours where radiation covers charge power + for (const h of _hourlyForecast) { + const hMs = h.time instanceof Date ? h.time.getTime() : new Date(h.time).getTime(); + if (hMs < _now) continue; // skip past hours + if (typeof h.radiationWm2 === 'number' && h.radiationWm2 >= _minRadiation) { + _effectiveSunHours++; + } + } + } else { + // Fallback: use sunshineTodayRemaining when no hourly radiation data + _effectiveSunHours = inputs.weather?.sunshineTodayRemaining ?? 0; + } + + // Also accept sun score as last-resort fallback (no weather data at all) + const _hasForecast = Array.isArray(_hourlyForecast) && _hourlyForecast.length > 0; + const _sunScore = inputs.sun?.gfs ?? inputs.sun?.harmonie ?? 0; + const _enoughSun = _effectiveSunHours >= _hoursNeeded || (!_hasForecast && _sunScore >= 50); + + // Guard: pvKwhRemaining must cover what the battery still needs. + // Counts as "enough" only when remaining PV comfortably exceeds the charge need + // (1.2× margin for forecast uncertainty and household consumption losses). + // Without this, delay-charge can trap us: we export now, sun fades, + // and the battery can never fill before the evening peak. + const _pvKwhRemaining = inputs.weather?.pvKwhRemaining ?? null; + const _pvCanFill = _pvKwhRemaining === null // no data → trust sun-hour count + || _pvKwhRemaining >= _toChargeKwh * 1.2; + + if (_enoughSun && _pvCanSupport && _pvCanFill) { + _canDelayCharge = true; + const _sunSource = _hasForecast + ? `${_effectiveSunHours}h radiation≥${Math.round(_minRadiation)}W/m²` + : `score ${_sunScore}`; + const _pvRemainingStr = _pvKwhRemaining !== null ? `, PV remaining ${_pvKwhRemaining.toFixed(1)}kWh ≥ ${(_toChargeKwh * 1.2).toFixed(1)}kWh needed` : ''; + this.log(`PV OVERSCHOT: delay-charge possible — ${_sunSource} (need ${_hoursNeeded.toFixed(1)}h for ${_toChargeKwh.toFixed(1)}kWh @ ${_battChargePowerW}W)${_pvRemainingStr}, ${_cheapLaterHours.length} cheap slot(s) (${(_cheapLaterHours.length * 60 / _sph).toFixed(0)}min) coming → export now, charge later`); + } else if (_enoughSun && _pvCanSupport && !_pvCanFill) { + this.log(`PV OVERSCHOT: delay-charge SKIPPED — PV remaining ${_pvKwhRemaining?.toFixed(1)}kWh < ${(_toChargeKwh * 1.2).toFixed(1)}kWh needed (${_toChargeKwh.toFixed(1)}kWh × 1.2 margin) → charge now before sun fades`); + } else { + // No adequate sun — fall back to grid price arbitrage over 24h window. + // If there are enough significantly cheaper grid hours ahead, delay charging. + const _cheap24h = _pvPricesArray.filter(p => + typeof p.index === 'number' && + p.index > 1 * _sph && p.index <= 24 * _sph && + typeof p.price === 'number' && + p.price < _pvCurrentPrice * 0.75 // At least 25% cheaper (grid arbitrage needs wider spread) + ); + if (_cheap24h.length >= Math.ceil(_hoursNeeded * _sph)) { + _canDelayCharge = true; + this.log(`PV OVERSCHOT: delay-charge via grid arbitrage — no adequate sun (${_effectiveSunHours}h < ${_hoursNeeded.toFixed(1)}h needed), but ${_cheap24h.length} cheap grid hour(s) coming in 24h @ ≤€${Math.min(..._cheap24h.map(p => p.price)).toFixed(3)} → wait for cheap grid`); + } else { + this.log(`PV OVERSCHOT: delay-charge SKIPPED — ${_effectiveSunHours}h adequate sun (need ${_hoursNeeded.toFixed(1)}h) and no sufficient cheap grid hours → charge now`); + } + } + } + } + } + + if (_canDelayCharge) { + // Export now is optimal: earn €current export + charge for free from PV later. + // This applies at ANY SoC — the battery firmware handles protection. + // At low SoC there's nothing useful to discharge anyway, standby is best. + inputs._pvStoreWins = false; + inputs._delayCharge = true; + scores.preserve += 200; + scores.charge = 0; + // Don't zero discharge — stored energy can still profitably discharge + // during delay-charge. PV exports to grid, battery discharges to cover load. + // HW mode zero_discharge_only handles both actions simultaneously. + this.log(`PV OVERSCHOT: delay-charge wins → PV exports to grid at €${_pvCurrentPrice?.toFixed(3)}/kWh, charge battery later at cheaper hours (discharge still allowed)`); + } else if (_pvCurrentPrice === null || _pvStoreValue === null || _pvStoreValue > _pvCurrentPrice) { + // Storing beats exporting (or no price data, or can't delay) — charge from free PV + inputs._pvStoreWins = true; + scores.charge += 250; + scores.preserve = -100; + this.log(`PV OVERSCHOT: storing beats exporting (max €${_pvMaxFuture?.toFixed(3)} × ${this.BATTERY_EFFICIENCY} = €${_pvStoreValue?.toFixed(3)} > current €${_pvCurrentPrice?.toFixed(3)}) → force charge`); + } else { + // Exporting now is more profitable — let all solar go to grid, price signals decide + inputs._pvStoreWins = false; + scores.preserve += 20; + this.log(`PV OVERSCHOT: export now more profitable (€${_pvCurrentPrice.toFixed(3)} > max €${_pvMaxFuture.toFixed(3)} × ${this.BATTERY_EFFICIENCY} = €${_pvStoreValue.toFixed(3)}) → standby, PV to grid`); + } + } + + this._applySmartLowSocRule(scores, inputs); + + if (this.settings.tariff_type === 'dynamic') { + this._applyTariffScore(scores, inputs.tariff, inputs.battery, inputs); + this._applyDayAheadStrategy(scores, inputs.tariff, inputs.battery, inputs.time, inputs); + } + + if (this.settings.tariff_type === 'fixed') { + this._applyPeakShavingRules(scores, inputs); + } + + this._applyWeatherForecast(scores, inputs.weather, inputs.tariff, inputs.battery, inputs); + + const pvDetected = this._applyPVReality( + scores, + inputs.p1, + inputs.battery?.mode, + inputs + ); + + this._applyBatteryScore(scores, inputs.battery, pvDetected, inputs); + + this._applyPolicyMode(scores, inputs.policyMode); + + if (inputs.batteryCost?.avgCost > 0) { + const configuredEff = this.settings.battery_efficiency || 0.75; + const learnedEff = inputs.batteryEfficiency ?? configuredEff; + const effectiveEff = Math.min(configuredEff, learnedEff, 0.95); + + const breakEven = inputs.batteryCost.avgCost / effectiveEff; + inputs.batteryCost.breakEven = breakEven; + + const price = inputs.tariff?.currentPrice ?? null; + const maxChargePrice = this.settings.max_charge_price ?? 0.19; + const minDischargePrice = this.settings.min_discharge_price ?? 0.25; + const _arbitrageMinSoc = Math.max(this.settings.min_soc ?? 0, 5); // ✅ FIX: was 1, raised to 5 — consistent with BatteryScore floor + + if (price !== null) { + if (price > breakEven + 0.01 && price >= minDischargePrice) { + if (soc <= _arbitrageMinSoc) { + this.log(`Arbitrage: price €${price.toFixed(3)} > break-even €${breakEven.toFixed(3)} → discharge skipped (SoC ${soc}% at min_soc)`); + } else if (inputs._pvExporting && inputs._pvStoreWins === true) { + // PV OVERSCHOT already determined future store value > current price. + // Discharging existing stored energy now also loses to holding for future peak. + this.log(`Arbitrage: price €${price.toFixed(3)} > break-even €${breakEven.toFixed(3)} BUT PV exporting & future store value €${inputs._pvStoreValue?.toFixed(3)} > current → discharge suppressed, hold for peak`); + scores.preserve += 10; + } else { + // NOTE: delay-charge no longer blocks discharge — stored energy can be + // profitably discharged while PV exports to grid independently. + scores.discharge += 80; + scores.preserve -= 20; + this.log(`Arbitrage: price €${price.toFixed(3)} > break-even €${breakEven.toFixed(3)} AND >= min_discharge €${minDischargePrice.toFixed(3)} → discharge +80${inputs._delayCharge ? ' (delay-charge: PV exports + battery discharges)' : ''}`); + } + } else if (price > breakEven + 0.01 && price < minDischargePrice) { + scores.preserve += 10; + this.log(`Arbitrage: price €${price.toFixed(3)} > break-even but below min_discharge €${minDischargePrice.toFixed(3)} → preserve`); + } else if (price < breakEven - 0.01 && soc < maxSoc && price <= maxChargePrice) { + scores.charge += 80; + scores.preserve -= 20; + this.log(`Arbitrage: price €${price.toFixed(3)} < break-even AND <= max_charge_price €${maxChargePrice} → charge +80`); + } else { + scores.preserve += 10; + this.log(`Arbitrage: price near break-even or above max_charge_price → preserve`); + } + } + } + + // Peak Timing Guard: suppress discharge if better hours are coming + this._applyPeakTimingGuard(scores, inputs); + + // Optimizer bias: 24-h DP lookahead hint (applied before delay-charge cleanup) + this._applyOptimizerBias(scores, inputs); + + // Delay-charge: ensure charge stays zero — PV should export to grid, not charge battery. + // Some downstream rules (DayAhead, Weather) may have re-added charge; clean up here. + if (inputs._delayCharge) { + scores.charge = 0; + } + + // PV covers gap: suppress GRID charging only. + // When PV surplus is already being stored (pvStoreWins), that's PV charging — not wasteful. + // Only apply the grid-charge penalty when pvStoreWins is NOT active. + if (inputs._pvCoversGap && !inputs._pvStoreWins) { + scores.charge = Math.max(0, scores.charge - 100); + scores.preserve = Math.max(scores.preserve, scores.charge + 20); + } + + scores.charge = Math.max(0, scores.charge); + scores.discharge = Math.max(0, scores.discharge); + scores.preserve = Math.max(0, scores.preserve); + + const recommendation = this._selectMode(scores, inputs); + + this.log('Final scores:', scores); + this.log('Recommendation:', recommendation); + this.log('--- POLICY RUN END ---'); + + return { ...recommendation, scores }; + } + + _applySmartLowSocRule(scores, inputs) { + const soc = inputs.battery?.stateOfCharge ?? 50; + + if (soc === 0) { + this.log('SmartLowSoC skipped (SoC = 0)'); + return; + } + + if (soc >= 30) return; + + const price = inputs.tariff?.currentPrice ?? null; + const minDischargePrice = this.settings.min_discharge_price || 0; + if (price !== null && price >= minDischargePrice) { + this.log(`SmartLowSoC: skipped — peak price €${price.toFixed(3)} >= min discharge €${minDischargePrice.toFixed(3)}`); + return; + } + + const sun4h = inputs.weather?.sunshineNext4Hours ?? 0; + const multi = inputs.sun; + + const gfs = multi?.gfs ?? null; + const icon = multi?.harmonie ?? null; + + const arr = [sun4h, gfs, icon].filter(v => typeof v === 'number'); + const avgSun = arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0; + + const noSun = avgSun < 5; + const lightSun = avgSun >= 5 && avgSun < 25; + const strongSun = avgSun >= 25; + + const dynamicMax = inputs.dynamicMaxChargePrice ?? this.settings.max_charge_price ?? 0.15; + const priceLow = price !== null && price <= dynamicMax; + + const futureMax = inputs.tariff?.statistics?.max ?? null; + const eff = this.BATTERY_EFFICIENCY; + const spreadProfitable = price !== null && futureMax !== null && + (futureMax * eff) - price > 0; + + // If the remaining PV forecast (from now until end of day) covers the remaining battery + // capacity, charging from grid is wasteful — the sun will handle it for free. + // Use pvKwhRemaining (future-only) rather than pvKwhToday (full day including past hours) + // so that an already-sunny morning doesn't falsely suppress grid charge in the afternoon. + const pvKwhRemaining = inputs.weather?.pvKwhRemaining ?? inputs.weather?.pvKwhToday ?? null; + const capacityKwh = inputs.battery?.totalCapacityKwh ?? 2.688; + const remainingKwh = capacityKwh * (1 - soc / 100); + // Require 1.5× margin: PV must comfortably exceed the remaining capacity. + // A tight ratio (e.g. 3kWh PV for 2.5kWh remaining) leaves no buffer for + // household consumption or forecast error → charge from grid instead. + const pvCoversGap = pvKwhRemaining !== null && pvKwhRemaining >= remainingKwh * 1.5; + + if (pvCoversGap) { + // PV will charge the battery for free — grid charging wastes money. + // Set a flag so the post-scoring step can enforce preserve after DayAhead/Tariff rules run. + inputs._pvCoversGap = true; + this.log(`SmartLowSoC: PV remaining ${pvKwhRemaining.toFixed(1)}kWh covers remaining capacity ${remainingKwh.toFixed(1)}kWh → flagging _pvCoversGap, skip grid charge`); + return; + } + + if (noSun && !priceLow && !spreadProfitable) { + scores.preserve += 40; + this.log('SmartLowSoC: noSun, no spread → preserve (low SoC)'); + return; + } + + if (lightSun) { + scores.preserve += 40; + scores.discharge += 20; + this.log('SmartLowSoC: lightSun → preserve'); + return; + } + + if (strongSun) { + scores.preserve += 60; + scores.discharge += 10; + this.log('SmartLowSoC: strongSun → preserve'); + return; + } + + if (priceLow || spreadProfitable) { + scores.charge += 60; + scores.preserve += 10; + this.log(`SmartLowSoC: ${priceLow ? 'cheap price' : 'profitable spread'} → charge`); + return; + } + } + + _applyBatteryScore(scores, battery, pvDetected, inputs = {}) { + const soc = battery.stateOfCharge ?? 50; + const maxSoc = this.settings.max_soc ?? 95; + + // NOTE: HomeWizard firmware handles battery protection (0-100% safe range) + // No artificial limits needed here - use full range for optimal planning + + const zeroModeThreshold = this.settings.min_soc ?? 0; + if (soc <= zeroModeThreshold) { + // Prevent discharge — battery near-empty, firmware may be calibrating. + // Do NOT zero preserve: if delay-charge determined exporting is more + // profitable than storing, preserve/standby should still win. + this.log(`BatteryScore: SoC ${soc}% <= ${zeroModeThreshold}% → ZERO MODE (no discharge)`); + scores.discharge = 0; + if (scores.charge <= 0 && scores.preserve <= 0) { + // No price signal and no delay-charge: default to preserve + scores.preserve += 20; + this.log(`BatteryScore: ZERO MODE — no price signal, preserve +20`); + } + return; + } + + if (soc >= maxSoc) { + this.log('BatteryScore: max SoC reached → discharge'); + scores.discharge += 40; + scores.charge = 0; + scores.preserve += 10; + return; + } + + // ✅ FIX: PV trickle-charge hysteresis + // If currently in zero_charge_only (actively charging from weak PV) and SoC is still + // low, block discharge entirely. Switching to discharge now would immediately consume + // the just-captured energy at a net RTE loss — pointless regardless of price. + // Only allow discharge once enough has been stored (hysteresis: min_soc + 20%, min 20%). + const currentHwMode = inputs.battery?.mode ?? null; + const pvTrickleCharging = currentHwMode === 'zero_charge_only' && pvDetected; + const pvTrickleHysteresis = Math.max(zeroModeThreshold + 20, 20); + if (pvTrickleCharging && soc < pvTrickleHysteresis) { + this.log(`BatteryScore: trickle-charging from PV (zero_charge_only), SoC ${soc}% < ${pvTrickleHysteresis}% → blocking discharge (RTE loss prevention)`); + scores.discharge = 0; + scores.charge += 30; + return; + } + + if (pvDetected) { + if (inputs._delayCharge) { + this.log('BatteryScore: PV detected + delay-charge → preserve (PV exports to grid)'); + scores.preserve += 10; + return; + } + const currentPrice = inputs.tariff?.currentPrice ?? null; + const minDischargePrice = this.settings.min_discharge_price || 0.25; + const atPeakPrice = currentPrice !== null && currentPrice >= minDischargePrice; + if (atPeakPrice) { + this.log(`BatteryScore: PV detected (sticky) but price €${currentPrice.toFixed(3)} >= min_discharge €${minDischargePrice.toFixed(3)} → skip charge bonus (discharge favoured)`); + scores.preserve += 5; + return; + } + this.log('BatteryScore: PV detected → prefer charging'); + scores.charge += 40; + scores.preserve += 5; + return; + } + + this.log('BatteryScore: normal range → preserve +10'); + scores.preserve += 10; +} + + + _applyTariffScore(scores, tariff, battery, inputs) { + if (!this.settings.enable_dynamic_pricing || tariff.currentPrice == null) return; + + const price = tariff.currentPrice; + const soc = battery?.stateOfCharge ?? 0; + const maxChargePrice = inputs.dynamicMaxChargePrice ?? this.settings.max_charge_price ?? 0.15; + const minDischargePrice = this.settings.min_discharge_price || 0; + + // ✅ Get respect_minmax setting (default true = strict mode) + // balanced-dynamic overrides to opportunistic mode regardless of the setting — + // designed for post-saldering (NL 2027+) where export price ≠ import price. + const respectMinMax = inputs.policyMode === 'balanced-dynamic' + ? false + : this.settings.respect_minmax !== false; + + // ✅ Get configurable opportunistic parameters + const oppChargeMultiplier = this.settings.opportunistic_charge_multiplier ?? 2.0; + const oppDischargeFloor = this.settings.opportunistic_discharge_floor ?? 0.20; + const oppDischargeSpreadThreshold = this.settings.opportunistic_discharge_spread_threshold ?? -0.05; + + const allPrices = tariff.effectivePrices || tariff.allPrices || tariff.next24Hours || []; + const sph = Math.round(1 / (tariff.slotHours ?? 1)); // slots per hour + const now = new Date(); + + const futurePrices48h = allPrices + .filter(p => { + if (p.timestamp) return new Date(p.timestamp) > now; + return (p.index ?? 0) > 0; + }) + .map(p => p.price) + .filter(p => typeof p === 'number' && p > 0); + + const maxFuturePrice = futurePrices48h.length ? Math.max(...futurePrices48h) : null; + + const cheaperHourComing = allPrices.some(p => + typeof p.index === 'number' && + p.index > 0 && + p.index <= 8 * sph && + typeof p.price === 'number' && + p.price < price - 0.005 + ); + + const spreadProfit = maxFuturePrice !== null + ? (maxFuturePrice * this.BATTERY_EFFICIENCY) - price + : -1; + + const sunExpected = this._getSunExpectation(inputs.weather, inputs.sun, inputs.p1); + + // ------------------------------------------------------------ + // 1. DISCHARGE LOGIC + // ------------------------------------------------------------ + + // ✅ SOLAR TIMING OPTIMIZATION: Prevent PV charging during expensive hours when more PV + cheaper prices coming + // Scenario: Morning with sun + high price (€0.339), but more sun + lower prices coming later + // Strategy: DON'T charge battery from PV now (boost discharge mode → standby/zero_discharge_only) + // Benefit: Saves battery capacity for charging from PV during cheaper hours, PV exports to grid now + // + // ⚠️ GUARD: Skip at LOW SoC when PV is exporting — charging from free surplus beats discharging. + // At HIGH SoC (> 30%), allow the "discharge now + refill from future PV" arbitrage play, + // but only if the existing moreSunLater + capacity checks confirm PV can refill. + // Also skip when SoC is near-empty — there is nothing meaningful to discharge. + const _gridPowerST = inputs.p1?.resolved_gridPower ?? 0; + const _pvExportingToGrid = _gridPowerST < -100; + const _socNearEmpty = soc <= Math.max(this.settings.min_soc ?? 0, 5); // ✅ FIX: was 1, raised to 5 + // Below this SoC, free PV charge-in always wins over discharge-and-refill + const _pvExportLowSocThreshold = 30; + + if (sunExpected.source === 'actual_pv' && price >= minDischargePrice * 0.85 && minDischargePrice > 0) { + // PV is available RIGHT NOW during an expensive period + + if (soc <= _pvExportLowSocThreshold) { + // At low SoC with PV active: standby is optimal — PV exports to grid at current price, + // battery has nothing useful to discharge anyway. Charge later when prices drop. + if (inputs._delayCharge) { + this.log(`Tariff [SOLAR TIMING]: skipped — SoC ${soc}% <= ${_pvExportLowSocThreshold}%, delay-charge active → standby`); + return; + } + this.log(`Tariff [SOLAR TIMING]: skipped — SoC ${soc}% <= ${_pvExportLowSocThreshold}%, PV charging from free solar`); + // fall through to charge logic below + } else if (_socNearEmpty) { + this.log(`Tariff [SOLAR TIMING]: skipped — SoC ${soc}% near-empty, nothing to discharge`); + // fall through to charge logic below + } else { + if (_pvExportingToGrid) { + this.log(`Tariff [SOLAR TIMING]: SoC ${soc}% > ${_pvExportLowSocThreshold}% with PV exporting — evaluating discharge+solar-refill arbitrage`); + } + + // Check if more sun is coming later (beyond current sun) + const moreSunLater = sunExpected.totalHours >= 4 || + (inputs.weather?.sunshineNext8Hours ?? 0) >= 2 || + (inputs.weather?.sunshineTodayRemaining ?? 0) >= 3; + + // Check if cheaper prices are coming (where we could charge from solar instead) + const cheaperPricesLater = allPrices.some(p => + typeof p.index === 'number' && + p.index > 2 * sph && // At least 2 hours from now + p.index <= 12 * sph && // Within 12 hours + typeof p.price === 'number' && + p.price < price * 0.75 // At least 25% cheaper + ); + + if (moreSunLater && cheaperPricesLater) { + // ✅ CAPACITY CHECK: Verify PV system can charge battery in available time + // Battery specs (1 unit = 2.688 kWh @ ~800W, 4 units = 10.752 kWh @ ~3200W) + const batteryChargePowerW = inputs.battery?.maxChargePowerW || + (inputs.battery?.totalCapacityKwh + ? Math.max(1, Math.round(inputs.battery.totalCapacityKwh / 2.688)) * 800 + : 800); + const pvCapacityW = this.settings.pv_capacity_w || 3000; // User-configured PV peak capacity + const currentSoc = soc; + const targetSoc = this.settings.max_soc ?? 95; + + // Estimate battery capacity: 2.688 kWh per 800W unit (HW battery spec: 2688 Wh) + const estimatedBatteryCapacityKwh = (batteryChargePowerW / 800) * 2.688; + const capacityToChargeKwh = ((targetSoc - currentSoc) / 100) * estimatedBatteryCapacityKwh; + + // Time needed to charge (hours) at full battery charge power + const hoursNeededToCharge = capacityToChargeKwh / (batteryChargePowerW / 1000); + + // Check if PV capacity is sufficient for battery charge power + const pvCanSupport = pvCapacityW >= batteryChargePowerW * 0.8; // 80% threshold for real-world conditions + + // Check if enough sun hours remain + const enoughSunTime = sunExpected.totalHours >= hoursNeededToCharge; + + if (pvCanSupport && enoughSunTime) { + // OPTIMAL: Boost discharge mode to prevent charging from PV + // Maps to: standby (no charge/discharge) or zero_discharge_only (discharge to house only) + // Result: PV covers house + exports to grid, battery doesn't charge (saves capacity for later) + const opportunityValue = price - (Math.min(...allPrices.filter(p => p.index > 2 * sph && p.index <= 12 * sph).map(p => p.price)) || price * 0.75); + scores.discharge += 120; + scores.charge = 0; + scores.preserve = 0; + this.log(`Tariff [SOLAR TIMING]: 🌞💰 PV NOW at expensive €${price.toFixed(3)} + more sun later (${sunExpected.totalHours}h) + cheaper prices coming → DISCHARGE +120`); + this.log(` Capacity check: battery ${estimatedBatteryCapacityKwh.toFixed(1)}kWh @ ${batteryChargePowerW}W, PV ${pvCapacityW}W, need ${hoursNeededToCharge.toFixed(1)}h to charge ${capacityToChargeKwh.toFixed(1)}kWh (${currentSoc}%→${targetSoc}%)`); + this.log(` Opportunity: €${opportunityValue.toFixed(3)}/kWh - PV exports to grid now, battery charges later at cheaper prices`); + return; + } else { + const reason = !pvCanSupport + ? `PV capacity ${pvCapacityW}W < battery ${batteryChargePowerW}W needed` + : `sun hours ${sunExpected.totalHours}h < ${hoursNeededToCharge.toFixed(1)}h needed`; + this.log(`Tariff [SOLAR TIMING]: ⚠️ Would delay charging but ${reason} - allowing normal PV charging`); + // Fall through to normal logic + } + } + } // end else (not exporting to grid, not near-empty) + } + + if (respectMinMax) { + // STRICT MODE: Only discharge when price >= minDischargePrice + if (price >= minDischargePrice && minDischargePrice > 0) { + // ✅ NEW: At extreme price peaks, ALWAYS discharge regardless of PV export strategy + const isPricePeak = maxFuturePrice ? price >= maxFuturePrice * 0.90 : false; + + if (!isPricePeak && inputs._pvExporting && inputs._pvStoreWins === true) { + this.log(`Tariff [STRICT]: discharge skipped — PV exporting & future store value €${inputs._pvStoreValue?.toFixed(3)} > current €${price.toFixed(3)} → hold for peak`); + // fall through to charge logic + } else { + const coverableLoad = Math.min(this.currentLoad, this.maxDischarge); + const coverageRatio = this.maxDischarge > 0 ? coverableLoad / this.maxDischarge : 0; + const priceRatio = maxFuturePrice ? price / maxFuturePrice : 1; + + const baseScore = Math.round(100 * priceRatio * Math.max(0.5, coverageRatio)); + + scores.discharge += baseScore; + scores.preserve = 0; + + this.log(`Tariff [STRICT]: discharge hour €${price.toFixed(3)} >= min €${minDischargePrice.toFixed(3)} → discharge +${baseScore}${isPricePeak ? ' (🔴 PRICE PEAK)' : ''}${inputs._delayCharge ? ' (delay-charge: PV exports + battery discharges)' : ''}`); + return; + } + } + } else { + // DYNAMIC MODE: Discharge when profitable, even if below threshold + if (price >= minDischargePrice && minDischargePrice > 0) { + // NOTE: delay-charge no longer blocks discharge (see STRICT comment above) + if (inputs._pvExporting && inputs._pvStoreWins === true) { + // fall through to charge logic + } else { + const coverableLoad = Math.min(this.currentLoad, this.maxDischarge); + const coverageRatio = this.maxDischarge > 0 ? coverableLoad / this.maxDischarge : 0; + const priceRatio = maxFuturePrice ? price / maxFuturePrice : 1; + + const baseScore = Math.round(100 * priceRatio * Math.max(0.5, coverageRatio)); + + scores.discharge += baseScore; + scores.preserve = 0; + + this.log(`Tariff [DYNAMIC]: discharge hour €${price.toFixed(3)} >= min €${minDischargePrice.toFixed(3)} → discharge +${baseScore}${inputs._delayCharge ? ' (delay-charge: PV exports + battery discharges)' : ''}`); + return; + } + } else if (price > oppDischargeFloor && spreadProfit < oppDischargeSpreadThreshold) { + // ✅ Opportunistic discharge: Use configurable parameters + const score = Math.min(60, (price - oppDischargeFloor) * 300); + scores.discharge += score; + scores.preserve -= 10; + this.log(`Tariff [DYNAMIC]: 🎯 opportunistic discharge €${price.toFixed(3)} (floor=€${oppDischargeFloor}, spread=${spreadProfit.toFixed(3)} < ${oppDischargeSpreadThreshold}) → discharge +${score}`); + return; + } + } + + // ------------------------------------------------------------ + // 2. CHARGE LOGIC + // ------------------------------------------------------------ + if (respectMinMax) { + // STRICT MODE: Only charge when price <= maxChargePrice + if (price <= maxChargePrice && maxChargePrice > 0) { + + if (sunExpected.source === 'actual_pv') { + scores.charge += 300; + if (!inputs._delayCharge) scores.preserve = 0; + this.log(`Tariff [STRICT]: cheap €${price.toFixed(3)} + actual PV → CHARGE +300`); + return; + } + + if (cheaperHourComing) { + scores.preserve += 80; + scores.charge = 0; + this.log(`Tariff [STRICT]: cheaper hours coming → preserve (waiting for cheaper window)`); + return; + } + + // ✅ ENHANCED: Check solar arbitrage opportunity + if (sunExpected.goodSunComing && sunExpected.hours <= 6 && sunExpected.totalHours >= 3) { + // Require reliable sun signal: active PV export, near-term forecast (sun4h), or model confidence. + // sunshineTodayRemaining alone is too vague (includes theoretical sun on overcast/rainy days). + const sunSignalReliable = sunExpected.source === 'actual_pv' || + (sunExpected.details?.sun4h ?? 0) > 0 || + (sunExpected.details?.gfs ?? 0) >= 25 || + (sunExpected.details?.harmonie ?? 0) >= 25; + + if (!sunSignalReliable) { + this.log(`Tariff [STRICT]: sun signal not reliable (sun4h=0, gfs=${sunExpected.details?.gfs}, harmonie=${sunExpected.details?.harmonie}) → skip solar arbitrage`); + // fall through to normal charge logic below + } else { + // Calculate solar arbitrage: free solar → discharge at peak (no charge RTE loss) + const DISCHARGE_EFFICIENCY = 0.87; // One-way discharge efficiency (not round-trip) + const solarArbitrageProfit = maxFuturePrice ? maxFuturePrice * DISCHARGE_EFFICIENCY : 0; + + // Calculate grid arbitrage: buy now → discharge at peak (with full RTE loss) + const gridArbitrageProfit = spreadProfit; + + if (solarArbitrageProfit > gridArbitrageProfit + 0.05) { + // Solar arbitrage significantly more profitable → preserve capacity + scores.preserve += 90; + scores.charge = 0; + this.log(`Tariff [STRICT]: 🌞 solar arbitrage €${solarArbitrageProfit.toFixed(3)}/kWh > grid €${gridArbitrageProfit.toFixed(3)}/kWh → PRESERVE for PV in ${sunExpected.hours}h (${sunExpected.totalHours}h total)`); + return; + } else { + // Grid arbitrage still better, but acknowledge sun coming + scores.preserve += 20; + scores.charge -= 10; + this.log(`Tariff [STRICT]: cheap €${price.toFixed(3)} but PV in ${sunExpected.hours}h (solar: €${solarArbitrageProfit.toFixed(3)} vs grid: €${gridArbitrageProfit.toFixed(3)}) → mild preserve`); + return; + } + } + } + + const boost = spreadProfit >= 0.10 ? 80 : 50; + scores.charge += boost; + scores.preserve = 0; + + this.log(`Tariff [STRICT]: cheap €${price.toFixed(3)}, spread €${spreadProfit.toFixed(3)}/kWh → charge +${boost}`); + return; + } + } else { + // DYNAMIC MODE: Charge when profitable, even if slightly above threshold + + if (sunExpected.source === 'actual_pv') { + scores.charge += 300; + if (!inputs._delayCharge) scores.preserve = 0; + this.log(`Tariff [DYNAMIC]: actual PV → CHARGE +300`); + return; + } + + if (price <= maxChargePrice && maxChargePrice > 0) { + + if (cheaperHourComing) { + scores.preserve += 80; + scores.charge = 0; + this.log(`Tariff [DYNAMIC]: cheaper hours coming → preserve`); + return; + } + + // ✅ ENHANCED: Check solar arbitrage opportunity + if (sunExpected.goodSunComing && sunExpected.hours <= 6 && sunExpected.totalHours >= 3) { + // Require reliable sun signal: active PV export, near-term forecast (sun4h), or model confidence. + // sunshineTodayRemaining alone is too vague (includes theoretical sun on overcast/rainy days). + const sunSignalReliable = sunExpected.source === 'actual_pv' || + (sunExpected.details?.sun4h ?? 0) > 0 || + (sunExpected.details?.gfs ?? 0) >= 25 || + (sunExpected.details?.harmonie ?? 0) >= 25; + + if (!sunSignalReliable) { + this.log(`Tariff [DYNAMIC]: sun signal not reliable (sun4h=0, gfs=${sunExpected.details?.gfs}, harmonie=${sunExpected.details?.harmonie}) → skip solar arbitrage`); + // fall through to normal charge logic below + } else { + // Calculate solar arbitrage: free solar → discharge at peak (no charge RTE loss) + const DISCHARGE_EFFICIENCY = 0.87; // One-way discharge efficiency (not round-trip) + const solarArbitrageProfit = maxFuturePrice ? maxFuturePrice * DISCHARGE_EFFICIENCY : 0; + + // Calculate grid arbitrage: buy now → discharge at peak (with full RTE loss) + const gridArbitrageProfit = spreadProfit; + + if (solarArbitrageProfit > gridArbitrageProfit + 0.05) { + // Solar arbitrage significantly more profitable → preserve capacity + scores.preserve += 90; + scores.charge = 0; + this.log(`Tariff [DYNAMIC]: 🌞 solar arbitrage €${solarArbitrageProfit.toFixed(3)}/kWh > grid €${gridArbitrageProfit.toFixed(3)}/kWh → PRESERVE for PV in ${sunExpected.hours}h (${sunExpected.totalHours}h total)`); + return; + } else { + // Grid arbitrage still better, but acknowledge sun coming + scores.preserve += 20; + scores.charge -= 10; + this.log(`Tariff [DYNAMIC]: PV in ${sunExpected.hours}h (solar: €${solarArbitrageProfit.toFixed(3)} vs grid: €${gridArbitrageProfit.toFixed(3)}) → mild preserve`); + return; + } + } + } + + const boost = spreadProfit >= 0.10 ? 80 : 50; + scores.charge += boost; + scores.preserve = 0; + + this.log(`Tariff [DYNAMIC]: cheap €${price.toFixed(3)}, spread €${spreadProfit.toFixed(3)}/kWh → charge +${boost}`); + return; + + } else if (spreadProfit > this.MIN_PROFIT_MARGIN * oppChargeMultiplier) { + // ✅ Opportunistic charge: Use configurable multiplier + const threshold = this.MIN_PROFIT_MARGIN * oppChargeMultiplier; + const boost = Math.min(80, spreadProfit * 400); + scores.charge += boost; + scores.preserve -= 10; + this.log(`Tariff [DYNAMIC]: 🎯 opportunistic charge €${price.toFixed(3)}, exceptional spread €${spreadProfit.toFixed(3)}/kWh (threshold=€${threshold.toFixed(3)}, multiplier=${oppChargeMultiplier}) → charge +${boost}`); + return; + } + } + + // ------------------------------------------------------------ + // 3. NORMAL PRICE RANGE + // ------------------------------------------------------------ + scores.preserve += 5; + this.log(`Tariff: normal price €${price.toFixed(3)} → preserve +5`); + + // ------------------------------------------------------------ + // 4. EXTREME OVERRIDES (always apply) + // ------------------------------------------------------------ + if (price <= 0.05) { + scores.charge += 40; + scores.preserve -= 5; + this.log('Tariff: ultra cheap (≤€0.05) → charge +40'); + } + + if (price >= 0.40) { + scores.discharge += 40; + scores.preserve -= 5; + this.log('Tariff: ultra expensive (≥€0.40) → discharge +40'); + } + + // ------------------------------------------------------------ + // 5. NO SIGNAL FALLBACK + // ------------------------------------------------------------ + if (scores.charge === 0 && scores.discharge === 0 && scores.preserve <= 10 && soc > 0) { + scores.preserve += 5; + this.log('Tariff: no signal → preserve +5'); + } +} + + + _applyDayAheadStrategy(scores, tariff, battery, time, inputs) { + const _daEffective = tariff?.effectivePrices || tariff?.next24Hours; + if (!tariff || !Array.isArray(_daEffective)) return; + + const soc = battery?.stateOfCharge ?? 50; + const maxSoc = this.settings.max_soc ?? 95; + const currentHour = time?.getHours() ?? 0; + const currentPrice = tariff.currentPrice ?? 0; + const dynamicMax = inputs.dynamicMaxChargePrice ?? this.settings.max_charge_price ?? 0.15; + const sph = Math.round(1 / (tariff.slotHours ?? 1)); // slots per hour + const next24 = _daEffective; + + const zeroModeThresholdDA = Math.max(this.settings.min_soc ?? 0, 5); // ✅ FIX: was 1, raised to 5 + if (soc <= zeroModeThresholdDA) { + this.log(`DayAhead: SoC ${soc}% <= ${zeroModeThresholdDA}% → skipping day-ahead logic (ZERO MODE)`); + return; + } + + let nextExpensiveHour = null; + let hoursUntilExpensive = null; + + for (let i = 1; i < next24.length; i++) { + const hourData = next24[i]; + const breakeven = currentPrice * this.BATTERY_EFFICIENCY; + + if (hourData.price >= breakeven + this.MIN_PROFIT_MARGIN) { + nextExpensiveHour = hourData; + hoursUntilExpensive = i / sph; // convert slot index to hours + break; + } + } + + const sunExpected = this._getSunExpectation(inputs.weather, inputs.sun, inputs.p1); + + const minDischargePrice = this.settings.min_discharge_price || 0.25; + const alreadyAtPeakPrice = currentPrice >= minDischargePrice; + + if (nextExpensiveHour && hoursUntilExpensive !== null) { + + if (hoursUntilExpensive <= 4 && soc < 80) { + if (alreadyAtPeakPrice) { + // We're already in a discharge hour — don't charge for a future expensive hour + this.log(`DayAhead: expensive in ${hoursUntilExpensive}h but current €${currentPrice.toFixed(3)} >= min_discharge €${minDischargePrice.toFixed(3)} → skip charge (discharge NOW)`); + } else if (sunExpected.goodSunComing && sunExpected.hours < hoursUntilExpensive) { + scores.preserve += 20; + this.log(`DayAhead: expensive in ${hoursUntilExpensive}h, but PV in ${sunExpected.hours}h → preserve`); + } else { + scores.charge += 30; + this.log(`DayAhead: expensive hour in ${hoursUntilExpensive}h → charge +30`); + if (hoursUntilExpensive <= 0.5 && soc < maxSoc - 5) { + inputs._chargeUrgent = true; // signal mapping: grid charge before imminent peak + } + } + } + + if (hoursUntilExpensive <= 2 && soc >= 80) { + scores.preserve += 15; + this.log(`DayAhead: expensive in ${hoursUntilExpensive}h, SoC ${soc}% → preserve`); + } + + if (hoursUntilExpensive <= 3 && soc < 50) { + if (alreadyAtPeakPrice) { + // Already discharging this hour — skip charge boost + this.log(`DayAhead: low SoC but current €${currentPrice.toFixed(3)} >= min_discharge → skip charge (discharge NOW)`); + } else if (!sunExpected.goodSunComing || sunExpected.hours >= hoursUntilExpensive) { + scores.charge += 20; + this.log(`DayAhead: low SoC ${soc}% with expensive hour in ${hoursUntilExpensive}h → charge +20`); + } + } + } + + if (currentHour >= 12 && currentHour < 16 && soc < 70) { + const priceReasonable = currentPrice <= dynamicMax * 1.2; + + if (!sunExpected.goodSunComing && priceReasonable) { + scores.charge += 25; + this.log(`DayAhead: pre-peak (${currentHour}:00), SoC ${soc}% → charge +25`); + } + } + + const sunTomorrow = inputs.weather?.sunshineTomorrow ?? 0; + + if (sunTomorrow >= 4 && soc >= 40 && currentHour >= 20) { + scores.preserve += 15; + this.log(`DayAhead: strong sun tomorrow (${sunTomorrow}h) → preserve`); + } + + const postPeak = currentHour >= 21 || currentHour < 6; + + if (postPeak && soc < 80) { + const cheapSoon = next24.findIndex(p => + typeof p.index === 'number' && + p.index > 0 && + p.index <= 8 * sph && + typeof p.price === 'number' && + p.price <= dynamicMax + ); + + if (cheapSoon >= 0) { + scores.charge += 40; + this.log(`DayAhead: post-peak, cheap hours in ${cheapSoon}h → charge +40`); + } + } +} + + + _getSunExpectation(weather, sunMulti, p1) { + if (p1) { + const gridPower = p1.resolved_gridPower ?? 0; + const batteryPower = p1.battery_power ?? 0; + + if (gridPower < -100 || (batteryPower > 100 && gridPower <= 0)) { + return { + goodSunComing: true, + totalHours: 99, + hours: 0, + source: 'actual_pv', + details: { gridPower, batteryPower } + }; + } + } + + if (!weather && !sunMulti) { + return { goodSunComing: false, totalHours: 0, hours: null }; + } + + const sun4h = weather?.sunshineNext4Hours ?? 0; + const sun8h = weather?.sunshineNext8Hours ?? 0; + const sunToday = weather?.sunshineTodayRemaining ?? 0; + const sunTomorrow = weather?.sunshineTomorrow ?? 0; + + const gfs = sunMulti?.gfs ?? null; + const harmonie = sunMulti?.harmonie ?? null; + + let totalHours = 0; + let hoursUntil = null; + let goodSunComing = false; + + if (sunToday > 0) { totalHours += sunToday; if (hoursUntil === null) hoursUntil = 0; } + if (sunTomorrow > 0) { totalHours += sunTomorrow; if (hoursUntil === null) hoursUntil = 24 - (new Date().getHours()); } + if (sun4h > 0 && hoursUntil === null) hoursUntil = 2; + if (sun8h > 0 && hoursUntil === null) hoursUntil = 4; + + if (totalHours >= 3) goodSunComing = true; + if (gfs >= 25 || harmonie >= 25) { + goodSunComing = true; + if (hoursUntil === null) hoursUntil = 4; + } + + return { + goodSunComing, + totalHours, + hours: hoursUntil, + details: { sun4h, sun8h, sunToday, sunTomorrow, gfs, harmonie } + }; + } + + _getFutureExpensiveHours(tariff, currentPrice) { + if (!tariff) return null; + + const breakeven = (currentPrice != null) + ? currentPrice * this.BREAKEVEN_MULTIPLIER + : (this.settings.min_discharge_price || 0.25); + + const pricesArray = tariff.allPrices || tariff.next24Hours; + if (!Array.isArray(pricesArray)) return null; + + const now = new Date(); + const lookAheadMs = 24 * 60 * 60 * 1000; + + const expensiveHours = pricesArray.filter((hour, idx) => { + if (hour.timestamp) { + const ts = new Date(hour.timestamp); + return ts > now && + ts <= new Date(now.getTime() + lookAheadMs) && + (hour.price ?? 0) >= breakeven; + } + const hourIndex = hour.index ?? idx; + return hourIndex >= 1 && hourIndex <= 24 && (hour.price ?? 0) >= breakeven; + }); + + const onTheHour = expensiveHours.filter(h => { + const ts = new Date(h.timestamp); + return ts.getMinutes() === 0; + }); + const result = onTheHour.length > 0 ? onTheHour : expensiveHours; + + this.log(`_getFutureExpensiveHours: breakeven=€${breakeven.toFixed(4)}, found ${result.length} profitable hours`); + return result.length > 0 ? result : null; + } + + _applyPVReality(scores, p1, batteryMode, inputs) { + if (!p1) return false; + + const gridPower = p1.resolved_gridPower ?? 0; + const batteryPower = p1.battery_power ?? 0; + const pvEstimate = p1.pv_power_estimated ?? 0; + + const now = Date.now(); + + if (!this._pvStickyUntil) this._pvStickyUntil = 0; + + // When delay-charge is active, PV should export to grid — don't add charge bonuses. + // Still detect PV (return true) and maintain sticky flag for other logic. + const delayChargeActive = inputs._delayCharge === true; + + if (this._pvStickyUntil > now) { + if (delayChargeActive) { + this.log(`PV Reality: sticky PV active + delay-charge → standby (PV exports to grid)`); + } else { + this.log(`PV Reality: sticky PV active → charge allowed`); + scores.charge += 50; + } + return true; + } + + // ONLY set sticky PV if we have actual PV export or grid is strongly exporting. + // Don't use high batteryPower alone — it could be grid-charging in to_full mode! + // A real PV signal requires either gridPower < 0 (net export) or high pvEstimate. + if (batteryPower > 50 && gridPower < -50) { + this._pvStickyUntil = now + 5 * 60 * 1000; + if (delayChargeActive) { + this.log(`💡 PV detected via batteryPower (${batteryPower}W) + export (${Math.abs(gridPower)}W) → sticky 5 min (delay-charge: no charge bonus)`); + } else { + this.log(`💡 PV detected via batteryPower (${batteryPower}W) + export (${Math.abs(gridPower)}W) → sticky 5 min`); + scores.charge += 100; + } + return true; + } + + if (pvEstimate >= 100) { + this._pvStickyUntil = now + 5 * 60 * 1000; + if (delayChargeActive) { + this.log(`💡 PV detected via pvEstimate (${pvEstimate}W) → sticky 5 min (delay-charge: no charge bonus)`); + } else { + this.log(`💡 PV detected via pvEstimate (${pvEstimate}W) → sticky 5 min`); + scores.charge += 80; + } + return true; + } + + if (gridPower < -100) { + const pvSurplus = Math.abs(gridPower); + this._pvStickyUntil = now + 5 * 60 * 1000; + if (delayChargeActive) { + this.log(`💡 PV detected via export (${pvSurplus}W) → sticky 5 min (delay-charge: no charge bonus)`); + } else { + this.log(`💡 PV detected via export (${pvSurplus}W) → sticky 5 min`); + scores.charge += 60; + } + return true; + } + + if (batteryMode === 'zero_charge_only') { + const _cetHour = parseInt(new Date().toLocaleString('en-GB', { + hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' + }), 10); + const _isDaylight = _cetHour >= 7 && _cetHour < 20; + if (_isDaylight) { + this._pvStickyUntil = now + 2 * 60 * 1000; + this.log(`PV Reality: zero_charge_only mode (daylight) → PV assumed`); + scores.charge += 40; + return true; + } + this.log(`PV Reality: zero_charge_only mode but nighttime (${_cetHour}h CET) → not assuming PV`); + } + + if (this.settings.tariff_type === 'dynamic') { + this.log('PV Reality: no PV surplus but dynamic pricing → tariff decides'); + return true; + } + + this.log('PV Reality: no PV → blocking charge'); + return false; +} + + + _applyWeatherForecast(scores, weather, tariff, battery, inputs) { + if (!weather) return; + + const sun4h = Number(weather.sunshineNext4Hours ?? 0); + const sun8h = Number(weather.sunshineNext8Hours ?? 0); + const sunToday = Number(weather.sunshineTodayRemaining ?? 0); + const sunTomorrow = Number(weather.sunshineTomorrow ?? 0); + const soc = battery?.stateOfCharge ?? 50; + const isDynamic = this.settings.tariff_type === 'dynamic'; + + if (sun4h >= 2.0) { + scores.charge -= 25; + scores.preserve += 15; + this.log(`Weather: sun4h >= 2 → preserve (PV coming in ~2h)`); + } + + if (sun4h >= 1.0) { + scores.preserve += 10; + this.log('Weather: sun4h >= 1 → preserve'); + } + + if (sun8h >= 3.0) { + scores.charge -= 20; + this.log('Weather: sun8h >= 3 → avoid grid charging'); + } + + if (sunToday >= 4.0) { + scores.charge -= 15; + scores.preserve += 10; + this.log('Weather: sunToday >= 4 → avoid grid charging'); + } + + if (sunTomorrow >= 4.0) { + scores.charge -= 10; + + if (isDynamic) { + const currentPrice = tariff?.currentPrice || 0; + const minDischargePrice = this.settings.min_discharge_price || 0.25; + + if (currentPrice >= minDischargePrice * 0.85) { + scores.discharge += 25; + this.log('Weather: sunTomorrow >= 4 + expensive hour → BOOST discharge'); + } else { + this.log('Weather: sunTomorrow >= 4 → mild grid charge penalty'); + } + } else { + scores.discharge += 15; + this.log('Weather: sunTomorrow >= 4 → encourage discharge'); + } + } + + if (sunTomorrow >= 6.0) { + scores.charge -= 20; + + if (isDynamic) { + const currentPrice = tariff?.currentPrice || 0; + const minDischargePrice = this.settings.min_discharge_price || 0.25; + + if (currentPrice >= minDischargePrice * 0.75) { + scores.discharge += 35; + this.log('Weather: sunTomorrow >= 6 + moderate/high price → AGGRESSIVE discharge'); + } else { + this.log('Weather: sunTomorrow >= 6 → avoid grid charging, ready to discharge'); + } + } else { + const currentHour = new Date().getHours(); + if (currentHour >= 17) { + scores.discharge += 30; + this.log('Weather: sunTomorrow >= 6 + evening → AGGRESSIVE discharge'); + } else { + scores.discharge += 20; + this.log('Weather: sunTomorrow >= 6 → boost discharge'); + } + } + } + } + + _applyPeakShavingRules(scores, inputs) { + const { p1, time, battery } = inputs; + if (!p1 || !time) return; + + const grid = p1.resolved_gridPower ?? 0; + const batt = p1.battery_power ?? 0; + const dischargeNow = batt < 0 ? Math.abs(batt) : 0; + const trueLoad = grid + dischargeNow; + const maxDischarge = this.maxDischarge; + const coverageRatio = trueLoad > 0 ? Math.min(maxDischarge / trueLoad, 1.0) : 0; + const canFullyCover = trueLoad <= maxDischarge; + const hour = time.getHours(); + const peak = this._parseTimeRange(this.settings.peak_hours); + const inPeak = peak && hour >= peak.startHour && hour < peak.endHour; + + if (inPeak) { + scores.charge = 0; + + if (canFullyCover) { + scores.discharge += 40; + scores.preserve += 5; + this.log(`Peak: battery can fully cover ${trueLoad}W → discharge +40`); + } else { + const partialScore = Math.round(40 * coverageRatio); + scores.discharge += partialScore; + scores.preserve += 5; + this.log(`Peak: battery covers ${Math.round(coverageRatio * 100)}% of ${trueLoad}W → discharge +${partialScore}`); + } + } + + if (trueLoad > maxDischarge * 0.8) { + if (canFullyCover) { + scores.discharge += 30; + scores.preserve -= 5; + this.log(`Peak: high load ${trueLoad}W (coverable) → discharge +30`); + } else { + scores.discharge += 15; + scores.preserve -= 5; + this.log(`Peak: high load ${trueLoad}W (exceeds capacity) → discharge +15`); + } + } + + if (trueLoad < maxDischarge * 0.3) { + scores.preserve += 15; + scores.discharge -= 5; + this.log(`Peak: low load ${trueLoad}W → preserve +15`); + } + + const weather = inputs.weather; + if (weather) { + const sunTomorrow = Number(weather.sunshineTomorrow ?? 0); + const h = time.getHours(); + + if (sunTomorrow >= 5.0 && h >= 17 && h < 23) { + scores.discharge += 20; + scores.preserve -= 10; + this.log(`Peak: evening + ${sunTomorrow}h sun tomorrow → boost discharge`); + } + + const offPeak = this._parseTimeRange(this.settings.off_peak_hours); + const inOffPeak = offPeak && h >= offPeak.startHour && h < offPeak.endHour; + + if (inOffPeak && sunTomorrow >= 4.0) { + scores.charge -= 30; + this.log(`Peak: off-peak but ${sunTomorrow}h sun tomorrow → skip grid charging`); + } + } + + if (trueLoad >= maxDischarge * 0.3 && trueLoad <= maxDischarge * 0.8) { + scores.discharge += 10; + this.log(`Peak: optimal load range ${trueLoad}W → discharge +10`); + } + } + + _parseTimeRange(range) { + if (!range) return null; + const [start, end] = range.split('-').map(s => parseInt(s, 10)); + if (isNaN(start) || isNaN(end)) return null; + return { startHour: start, endHour: end }; + } + + _applyPolicyMode(scores, mode) { + if (mode === 'eco') { + scores.preserve *= 1.3; + scores.charge *= 0.8; + scores.discharge *= 0.8; + this.log('PolicyMode: ECO (reduced cycling to minimize losses)'); + } + + if (mode === 'aggressive') { + scores.charge *= 1.2; + scores.discharge *= 1.2; + scores.preserve *= 0.7; + this.log('PolicyMode: AGGRESSIVE (maximize arbitrage opportunities)'); + } + } + +_mapPolicyToHwMode(policyMode, ctx) { + const tariffType = this.settings.tariff_type; + const soc = ctx.battery?.stateOfCharge ?? 50; + const minSoc = this.settings.min_soc ?? 0; + const maxSoc = this.settings.max_soc ?? 95; + const price = ctx.tariff?.currentPrice ?? null; + const minDischarge = this.settings.min_discharge_price || 0; + const maxChargePrice = this.settings.max_charge_price ?? 0.19; + + // NOTE: HomeWizard firmware handles 0-100% protection + // Only respect user-configured min_soc for strategy, not safety + + if (soc < minSoc) { + this.log(`[MAPPING] SoC ${soc}% < min_soc ${minSoc}% → forcing standby`); + return 'standby'; + } + + const costModelActive = + ctx.batteryCost?.avgCost > 0 && + ctx.batteryCost?.energyKwh >= 0.5; + + if (costModelActive) { + const configuredEff = this.settings.battery_efficiency || 0.75; + const learnedEff = ctx.batteryEfficiency ?? configuredEff; + const effectiveEff = Math.min(configuredEff, learnedEff, 0.95); + + const safeBreakEven = ctx.batteryCost.avgCost / effectiveEff; + ctx.batteryCost.breakEven = safeBreakEven; + + this.log(`[MAPPING] safe break-even €${safeBreakEven.toFixed(3)} (eff=${effectiveEff.toFixed(3)})`); + } + + const profitableToDischarge = + price !== null && + price >= minDischarge && + (!costModelActive || price >= ctx.batteryCost.breakEven); + + const gridPower = ctx.p1?.resolved_gridPower ?? 0; + const batteryPower = ctx.p1?.battery_power ?? 0; + const pvEstimate = ctx.p1?.pv_power_estimated ?? 0; + + // Check sticky PV flag first (set by _applyPVReality with 5min window) + const stickyPvActive = this._pvStickyUntil && (this._pvStickyUntil > Date.now()); + + const actualPvNow = + stickyPvActive || + gridPower < -100 || + pvEstimate >= 100 || + (gridPower <= 0 && batteryPower > 50); // battery charging without grid import → PV surplus + + this.log(`[MAPPING] policyMode=${policyMode}, soc=${soc}, PV=${actualPvNow} (sticky=${stickyPvActive}), price=${price?.toFixed(3)}, maxCharge=€${maxChargePrice}`); + + if (ctx.policyMode === 'zero') { + if (soc <= minSoc) return 'zero_charge_only'; + if (soc >= maxSoc) return 'zero_discharge_only'; + return 'zero'; + } + + if (ctx.policyMode === 'balanced' && tariffType === 'dynamic') { + + // Delay-charge: PV exports to grid for revenue. + // If discharge wins scoring, use zero_discharge_only: battery discharges to cover + // house load while PV independently exports surplus to grid. Both actions are profitable. + // Otherwise standby: PV exports, battery idle. + if (ctx._delayCharge) { + if (policyMode === 'discharge') { + const hwMode = profitableToDischarge ? 'zero_discharge_only' : 'standby'; + this.log(`[MAPPING] delay-charge + discharge → ${hwMode} (PV exports to grid, battery covers house load)`); + return hwMode; + } + this.log(`[MAPPING] delay-charge active → standby (PV exports to grid)`); + return 'standby'; + } + + if (policyMode === 'discharge') { + return profitableToDischarge ? 'zero_discharge_only' : 'standby'; + } + + if (policyMode === 'charge') { + + // When PV is producing strongly (≥400W), prefer zero_charge_only: harvest solar + // surplus without pulling from the grid. Grid charging (to_full) adds unnecessary + // import cost and slightly lower RTE when sun can do the job. + // Only fall back to to_full when PV is weak or absent and the grid price is cheap. + if (actualPvNow && pvEstimate >= 400 && !ctx._chargeUrgent) { + this.log(`[MAPPING][CHARGE] PV strong (${pvEstimate}W) → zero_charge_only (harvest solar, skip grid import)`); + return 'zero_charge_only'; + } + + if (ctx._chargeUrgent && price !== null && price <= maxChargePrice) { + this.log(`[MAPPING][CHARGE] urgent pre-peak (PV ${pvEstimate}W but expensive hour imminent) → to_full`); + return 'to_full'; + } + + // Weak/no PV: use price-based decision + if (price !== null && price <= maxChargePrice) { + this.log(`[MAPPING][CHARGE] PV weak/absent (${pvEstimate}W), price €${price.toFixed(3)} <= max_charge_price €${maxChargePrice} → to_full`); + return 'to_full'; + } + + // Price above ceiling — capture any PV if available, else standby + if (actualPvNow) { + this.log(`[MAPPING][CHARGE] price €${price?.toFixed(3)} > max_charge_price, PV active (${pvEstimate}W) → zero_charge_only`); + return 'zero_charge_only'; + } + + this.log(`[MAPPING][CHARGE] price €${price?.toFixed(3)} > max_charge_price €${maxChargePrice}, no PV → standby`); + return 'standby'; + } + + if (policyMode === 'preserve') { + return actualPvNow ? 'zero_charge_only' : 'standby'; + } + } + + if (tariffType === 'fixed') { + if (policyMode === 'discharge') return 'zero_discharge_only'; + if (policyMode === 'charge') return actualPvNow ? 'zero_charge_only' : 'to_full'; + return actualPvNow ? 'zero_charge_only' : 'standby'; + } + + return actualPvNow ? 'zero_charge_only' : 'standby'; +} + + /** + * Build a frontend-ready planning schedule from the optimizer's slot output. + * Maps optimizer actions to hwModes using PV forecast — not real-time P1 data. + * Called from device.js after each optimizer recompute; result is saved as + * 'policy_optimizer_schedule' Homey setting for the frontend to render. + * + * @param {Array<{timestamp, action, price, socProjected}>} slots + * @param {Array<{timestamp, pvPowerW}>|null} pvForecast + * @returns {Array<{timestamp, action, hwMode, socProjected, price, pvW}>} + */ + buildPlanningSchedule(slots, pvForecast) { + if (!slots || slots.length === 0) return []; + + const tariffType = this.settings.tariff_type || 'dynamic'; + const maxChargePrice = this.settings.max_charge_price ?? 0.19; + const minSoc = this.settings.min_soc ?? 0; + const maxSoc = this.settings.max_soc ?? 95; + + // Normalise policy_mode aliases + const raw = this.settings.policy_mode || 'balanced'; + const userPolicyMode = ['balanced', 'balanced-fixed', 'balanced-dynamic'].includes(raw) + ? 'balanced' : raw; + + // Mirror the DP floor logic: in dynamic mode use opportunistic_discharge_floor + // so the planning display is consistent with what the optimizer actually scheduled. + const respectMinMax = (raw === 'balanced-dynamic') + ? false + : this.settings.respect_minmax !== false; + const minDischargePrice = respectMinMax + ? (this.settings.min_discharge_price || 0) + : (this.settings.opportunistic_discharge_floor ?? 0.20); + + return slots.map(slot => { + const pvW = this._getPvWForTimestamp(slot.timestamp, pvForecast); + const hwMode = this._mapActionToHwModeForPlanning(slot.action, { + price: slot.price, soc: slot.socProjected, + pvW, tariffType, userPolicyMode, + maxChargePrice, minDischargePrice, minSoc, maxSoc, + }); + return { + timestamp: slot.timestamp, + action: slot.action, + hwMode, + socProjected: slot.socProjected, + price: slot.price, + pvW, + }; + }); + } + + _getPvWForTimestamp(timestamp, pvForecast) { + if (!pvForecast || pvForecast.length === 0) return 0; + const tsMs = new Date(timestamp).getTime(); + + // Return 0 outside daylight hours (6:00–21:00 local) — no PV possible at night. + const cetHour = parseInt(new Date(tsMs).toLocaleString('en-GB', { + hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' + }), 10); + if (cetHour < 6 || cetHour >= 21) return 0; + + let best = null, bestDist = Infinity; + for (const f of pvForecast) { + const dist = Math.abs(new Date(f.timestamp).getTime() - tsMs); + if (dist < bestDist) { bestDist = dist; best = f; } + } + // Only use forecast if within 35 minutes — no spillover from distant slots. + return (best && bestDist <= 35 * 60 * 1000) ? (best.pvPowerW ?? 0) : 0; + } + + /** + * Simplified action→hwMode mapping for planning (uses PV forecast, not real-time P1). + * The optimizer's DP already handles optimal timing (no cheaperSlotComing needed here). + */ + _mapActionToHwModeForPlanning(action, { price, soc, pvW, tariffType, userPolicyMode, maxChargePrice, minDischargePrice, minSoc, maxSoc }) { + const pvStrong = pvW >= 400; + const pvPresent = pvW >= 100; + const profitableToDischarge = price !== null && price >= minDischargePrice; + + if (userPolicyMode === 'zero') { + if (soc <= minSoc) return 'zero_charge_only'; + if (soc >= maxSoc) return 'zero_discharge_only'; + return 'zero'; + } + + if (action === 'discharge') { + // If PV is producing and battery isn't full, capturing free solar beats discharging. + // The real-time policy would detect PV and override to zero_charge_only anyway. + if (pvStrong && soc < maxSoc) return 'zero_charge_only'; + return profitableToDischarge ? 'zero_discharge_only' : 'standby'; + } + + if (action === 'charge') { + if (pvStrong) return 'zero_charge_only'; + if (price !== null && price <= maxChargePrice) return 'to_full'; + if (pvPresent) return 'zero_charge_only'; + return 'standby'; + } + + // preserve: only show zero_charge_only when PV is meaningfully strong (≥400W). + // Weak evening sun (e.g. 100W) is not worth a mode switch — show standby instead. + return pvStrong ? 'zero_charge_only' : 'standby'; + } + + /** + * Peak Timing Guard: ensure discharge happens during the most profitable hours. + * With limited battery capacity (e.g. 2.7kWh @ 800W ≈ 3.4h), discharge should + * target the top-N most expensive remaining hours, not just any hour above min_discharge. + */ + _applyPeakTimingGuard(scores, inputs) { + // Only relevant when discharge is winning + if (scores.discharge <= 0) return; + + const price = inputs.tariff?.currentPrice; + if (price == null) return; + + const allPrices = inputs.tariff?.effectivePrices || inputs.tariff?.allPrices || inputs.tariff?.next24Hours || []; + const now = new Date(); + + // Get prices for the remaining hours (next 12h window — practical discharge horizon) + const remainingPrices = allPrices + .filter(p => { + if (p.timestamp) { + const t = new Date(p.timestamp); + return t >= now && t <= new Date(now.getTime() + 12 * 3600_000); + } + return typeof p.index === 'number' && p.index >= 0 && p.index <= 12; + }) + .map(p => p.price) + .filter(p => typeof p === 'number' && p > 0); + + if (remainingPrices.length < 3) return; // Not enough data to judge + + // Calculate battery discharge duration in hours + const maxDischargeW = this.maxDischarge || 800; + const capacityKwh = (maxDischargeW / 800) * 2.688; + const dischargeHours = Math.ceil(capacityKwh / (maxDischargeW / 1000)); // e.g. 2.688/0.8 = 3.36 → 4 + + // Find top N most expensive prices in remaining window + const sortedDesc = [...remainingPrices].sort((a, b) => b - a); + const topN = sortedDesc.slice(0, Math.min(dischargeHours, sortedDesc.length)); + const peakThreshold = topN[topN.length - 1]; // Lowest price in the top-N + const bestPrice = sortedDesc[0]; + + // Only discharge now if price >= bestPrice × RTE + // This means: "don't discharge at €0.258 when you could earn €0.405 later" + // With 75% efficiency: threshold = €0.405 × 0.75 = €0.304 + const rteThreshold = bestPrice * this.BATTERY_EFFICIENCY; + + if (price < rteThreshold) { + const savings = bestPrice - price; + scores.discharge = Math.max(0, scores.discharge - 120); + scores.preserve += 80; + this.log(`PeakTiming: €${price.toFixed(3)} < best €${bestPrice.toFixed(3)} × ${this.BATTERY_EFFICIENCY} = €${rteThreshold.toFixed(3)} → suppress discharge, save €${savings.toFixed(3)}/kWh`); + } else { + this.log(`PeakTiming: €${price.toFixed(3)} >= RTE threshold €${rteThreshold.toFixed(3)} (best €${bestPrice.toFixed(3)} × ${this.BATTERY_EFFICIENCY}) → discharge OK`); + } + } + + /** + * Apply a 60-point bias toward the action recommended by the OptimizationEngine. + * The bias is strong enough to guide but does NOT override real-time rules + * (PV surplus scores ≥300, delay-charge zeroes out charge after this call, etc.). + */ + _applyOptimizerBias(scores, inputs) { + const optimizer = inputs.optimizer; + if (!optimizer) return; + + const action = optimizer.getSlot(new Date()); + if (!action) return; + + const BIAS = 60; + const soc = inputs.battery?.stateOfCharge ?? 50; + const zeroModeThreshold = this.settings.min_soc ?? 0; + + if (action === 'charge') { + scores.charge += BIAS; + this.log(`Optimizer: 24h-DP suggests charge → charge +${BIAS}`); + } else if (action === 'discharge') { + if (soc <= zeroModeThreshold) { + this.log(`Optimizer: 24h-DP suggests discharge but SoC ${soc}% <= ${zeroModeThreshold}% (ZERO MODE) → skipped`); + return; + } + scores.discharge += BIAS; + this.log(`Optimizer: 24h-DP suggests discharge → discharge +${BIAS}`); + } else { + scores.preserve += BIAS; + this.log(`Optimizer: 24h-DP suggests preserve → preserve +${BIAS}`); + } + } + + _selectMode(scores, ctx) { + let policyMode = 'preserve'; + let winner = scores.preserve; + + if (scores.charge > winner) { policyMode = 'charge'; winner = scores.charge; } + if (scores.discharge > winner) { policyMode = 'discharge'; winner = scores.discharge; } + + const total = scores.charge + scores.discharge + scores.preserve; + const confidence = Math.round((winner / (total || 1)) * 100); + const hwMode = this._mapPolicyToHwMode(policyMode, ctx); + + return { policyMode, hwMode, confidence: Math.min(confidence, 100) }; + } + + updateSettings(newSettings) { + this.settings = { ...this.settings, ...newSettings }; + this.BATTERY_EFFICIENCY = newSettings.battery_efficiency || this.BATTERY_EFFICIENCY; + this.MIN_PROFIT_MARGIN = newSettings.min_profit_margin ?? this.MIN_PROFIT_MARGIN; + } +} + +module.exports = PolicyEngine; \ No newline at end of file diff --git a/lib/sun-multisource.js b/lib/sun-multisource.js new file mode 100644 index 00000000..06312565 --- /dev/null +++ b/lib/sun-multisource.js @@ -0,0 +1,142 @@ +'use strict'; + +const fetchWithTimeout = require('../includes/utils/fetchWithTimeout'); + +// Cache duration: 30 minutes (forecast data changes slowly) +const CACHE_TTL_MS = 30 * 60 * 1000; + +class SunMultiSource { + constructor(homey) { + this.homey = homey; + // Per-URL cache to avoid redundant HTTP requests + this._cache = new Map(); + } + + // ------------------------------------------------------- + // GFS (Open-Meteo) + // ------------------------------------------------------- + async fetchGFS(lat, lon) { + const url = + `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}` + + `&hourly=sunshine_duration,shortwave_radiation`; + + return this._cachedFetch(url, "GFS"); + } + + // ------------------------------------------------------- + // ICON-D2 (DWD high-resolution model) + // ------------------------------------------------------- + async fetchHarmonie(lat, lon) { + const url = + `https://api.open-meteo.com/v1/dwd-icon?latitude=${lat}&longitude=${lon}` + + `&hourly=sunshine_duration,shortwave_radiation`; + + return this._cachedFetch(url, "ICON-D2"); + } + + // ------------------------------------------------------- + // CACHED FETCH — reuse results for 30 minutes + // ------------------------------------------------------- + async _cachedFetch(url, label) { + const cached = this._cache.get(url); + if (cached && Date.now() - cached.ts < CACHE_TTL_MS) { + return cached.data; + } + const data = await this._safeFetch(url, label); + if (!data.error) { + this._cache.set(url, { data, ts: Date.now() }); + } + // Prune stale entries to avoid unbounded growth + if (this._cache.size > 10) { + for (const [k, v] of this._cache) { + if (Date.now() - v.ts > CACHE_TTL_MS) this._cache.delete(k); + } + } + return data; + } + + // ------------------------------------------------------- + // SAFE FETCH WRAPPER + // ------------------------------------------------------- + async _safeFetch(url, label) { + try { + const res = await fetchWithTimeout(url, {}, 10000); + const text = await res.text(); + + let data; + try { + data = JSON.parse(text); + } catch (e) { + return { error: true, reason: `${label} returned non-JSON` }; + } + + if (!data.hourly || !Array.isArray(data.hourly.sunshine_duration)) { + return { error: true, reason: `${label} missing sunshine_duration` }; + } + + return data; + + } catch (err) { + return { error: true, reason: err.message }; + } + } + + // ------------------------------------------------------- + // SUN SCORE (0–100) + // Based on sunshine duration in next 4 hours. + // + // Open-Meteo returns sunshine_duration in SECONDS per hour (0–3600). + // Max possible over 4 hours = 4 × 3600 = 14400 seconds. + // Score = total sunshine seconds / 14400 × 100 + // + // Previously used 240 as baseline (minutes), causing scores to be + // inflated by 60× — any minimal sunshine would score 100. + // ------------------------------------------------------- + calculateSunScore(sunshineArray, hourlyTimes) { + if (!sunshineArray || !hourlyTimes) return 0; + + const now = new Date(); + + // Next full UTC hour + const nextHour = new Date(Date.UTC( + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate(), + now.getUTCHours() + 1, + 0, 0, 0 + )); + + // Find that hour in the forecast array + const idx = hourlyTimes.findIndex(t => { + const ts = new Date(t); + return ( + ts.getUTCFullYear() === nextHour.getUTCFullYear() && + ts.getUTCMonth() === nextHour.getUTCMonth() && + ts.getUTCDate() === nextHour.getUTCDate() && + ts.getUTCHours() === nextHour.getUTCHours() + ); + }); + + const start = idx !== -1 ? idx : 0; + + // Sum sunshine_duration (seconds) over next 4 hours + const next4h = sunshineArray.slice(start, start + 4); + const totalSeconds = next4h.reduce((a, b) => a + (b || 0), 0); + + // Max = 4 hours × 3600 seconds = 14400 seconds + const MAX_SECONDS = 4 * 3600; + const score = Math.min(100, Math.round((totalSeconds / MAX_SECONDS) * 100)); + + return score; + } + + // ------------------------------------------------------- + // COMPARE SCORES + // ------------------------------------------------------- + compareScores(gfs, harmonie) { + const diff = Math.abs(gfs - harmonie); + return { consistent: diff < 10, diff }; + } +} + +module.exports = SunMultiSource; \ No newline at end of file diff --git a/lib/tariff-manager.js b/lib/tariff-manager.js new file mode 100644 index 00000000..c888861b --- /dev/null +++ b/lib/tariff-manager.js @@ -0,0 +1,500 @@ +'use strict'; + +const MergedPriceProvider = require('./merged-price-provider'); + +/** + * TariffManager with dynamic pricing support + * + * ✅ ADDED: 15-minute price support via getAll15MinPrices() + * + * Dynamic provider is now MergedPriceProvider, which internally fetches from + * both Xadi and KwhPrice concurrently and produces the most complete hourly + * price table possible (up to 48h after ~13:15 CET). + * + * TariffManager no longer needs to orchestrate provider fallback itself — + * MergedPriceProvider handles that internally. _selectBestProvider() is kept + * for the coverage check and the "promote back to merged after failure" case. + */ +class TariffManager { + constructor(homey, settings) { + this.homey = homey; + this.settings = settings; + this.log = homey.log.bind(homey); + + this.dynamicProvider = null; + this._initializeDynamicProvider(); + } + + _initializeDynamicProvider() { + if (!this.settings.enable_dynamic_pricing) { + return; + } + + const markup = this.settings.dynamic_price_markup || 0.11; + + // MergedPriceProvider owns both Xadi and KwhPrice internally. + // Expose .xadiProvider and .kwhpriceProvider as pass-throughs so that + // device.js _schedulePriceRefresh() can still reference them directly. + this.mergedProvider = new MergedPriceProvider(this.homey, { markup }); + this.xadiProvider = this.mergedProvider.xadi; + this.kwhpriceProvider = this.mergedProvider.kwhprice; + + this.dynamicProvider = this.mergedProvider; + this.activeProvider = 'merged'; + + this.log('Dynamic pricing enabled with MergedPriceProvider (Xadi + KwhPrice)'); + + this._selectBestProvider(); + } + + async _selectBestProvider() { + this.log('🔍 Fetching merged prices...'); + + try { + await this.mergedProvider.fetchPrices(); + const count = this.mergedProvider.cache?.length || 0; + + if (count > 0) { + this.dynamicProvider = this.mergedProvider; + this.activeProvider = 'merged'; + const days = count > 24 ? 'today + tomorrow' : 'today only'; + this.log(`✅ Merged provider ready: ${count}h (${days})`); + + // Save 15-min prices immediately after full fetch (both providers done). + // This avoids the race where getCurrentTariff() saves prices before Xadi + // finishes its /day/tomorrow fetch, causing missing slots in the planning. + // Guard: only save if sub-providers have native 15-min cache — otherwise + // getAll15MinPrices() returns _expandHourlyTo15Min() (4 identical prices/hour) + // which would overwrite any valid 15-min data from the previous session. + const hasNative15Min = !!(this.kwhpriceProvider?.cache15min?.length > 0 || + this.xadiProvider?.cache15min?.length > 0); + if (hasNative15Min) { + const all15min = this.getAll15MinPrices(); + if (all15min.length > 0) { + this._lastPriceSettingsSave = Date.now(); // reset throttle + await this.homey.settings.set('policy_all_prices_15min', all15min); + this.log(`💾 Initial save: ${all15min.length} 15-min prices after full fetch`); + } + } else { + this.log('⚡ 15-min native cache not yet available — keeping existing policy_all_prices_15min'); + } + return; + } + + this.log('⚠️ Merged provider returned no prices'); + } catch (err) { + this.log('❌ Merged provider fetch failed:', err.message); + } + + this.log('❌ No price data available'); + this.dynamicProvider = null; + this.activeProvider = null; + } + + getCurrentTariff(gridPower = 0) { + const now = new Date(); + + if (this.settings.enable_dynamic_pricing && this.dynamicProvider) { + try { + return this._getDynamicTariff(gridPower, now); + } catch (error) { + this.log('Dynamic tariff fetch failed, falling back to manual:', error.message); + return this._getManualTariff(gridPower, now); + } + } + + return this._getManualTariff(gridPower, now); + } + + _getDynamicTariff(gridPower, now) { + const currentRate = this.dynamicProvider.getCurrentRate(); + const nextChange = this.dynamicProvider.getNextRateChange(); + const stats = this.dynamicProvider.getPriceStatistics(); + const top3Lowest = this.dynamicProvider.getTop3Cheapest(); + const top3Highest = this.dynamicProvider.getTop3MostExpensive(); + const allPrices = this.dynamicProvider.getAllHourlyPrices(); + + // Use 15-min price as currentPrice when available (more accurate for policy decisions) + const allPrices15min = this.getAll15MinPrices(); + const currentPrice = this.getCurrent15MinPrice() ?? this.dynamicProvider.getCurrentPrice(); + + // Build effectivePrices: 15-min slots with slot-based index (slots from now). + // slotHours = 0.25 for native 15-min, 1.0 fallback to hourly. + // Policy engine uses effectivePrices + slotHours for all future-price comparisons. + const slotMs = 15 * 60 * 1000; + const nowMs = now.getTime(); + let effectivePrices, slotHours; + if (allPrices15min?.length > 0) { + effectivePrices = allPrices15min + .map(p => { + const ts = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + return { ...p, index: Math.round((ts.getTime() - nowMs) / slotMs) }; + }) + .sort((a, b) => a.index - b.index); + slotHours = 0.25; + } else { + effectivePrices = null; // falls back to next24Hours in policy engine + slotHours = 1.0; + } + + const next24Hours = allPrices + .filter(p => p.index >= 0 && p.index < 24) + .sort((a, b) => a.index - b.index) + .map(p => ({ + hour: p.hour, + index: p.index, + price: p.price, + timestamp: p.timestamp, + source: p.source + })); + + // Throttle settings writes to at most once per 5 minutes to avoid + // hammering Homey's settings store (getCurrentTariff is called every 15s). + const _now = Date.now(); + if (!this._lastPriceSettingsSave || _now - this._lastPriceSettingsSave > 300_000) { + this._lastPriceSettingsSave = _now; + (async () => { + try { + await this.homey.settings.set('policy_all_prices', allPrices); + // Save 15-min prices for planning view — only when native data is available. + // If sub-providers have no cache15min, getAll15MinPrices() returns expanded + // hourly (4 identical prices/hour); saving that would overwrite valid data. + const hasNative = !!(this.kwhpriceProvider?.cache15min?.length > 0 || + this.xadiProvider?.cache15min?.length > 0); + if (hasNative && allPrices15min && allPrices15min.length > 0) { + await this.homey.settings.set('policy_all_prices_15min', allPrices15min); + this.log(`💾 Saved ${allPrices15min.length} 15-min prices to settings`); + } + } catch (err) { + this.log('Failed to save policy prices to settings:', err.message); + } + })(); + } + + return { + currentRate, + nextRateChange: nextChange, + gridPower, + isImporting: gridPower > 0, + isExporting: gridPower < 0, + currentPrice, + priceWithoutMarkup: currentPrice, + statistics: stats, + top3Lowest, + top3Highest, + allPrices, + allPrices15min, + effectivePrices, // 15-min slots with slot index, or null (falls back to next24Hours) + slotHours, // 0.25 for 15-min, 1.0 for hourly + next24Hours, + timestamp: now, + source: this.activeProvider || 'unknown' + }; + } + + _getManualTariff(gridPower, now) { + const currentRate = this._getCurrentRate(now); + const nextChange = this._getNextRateChange(now); + + return { + currentRate, + nextRateChange: nextChange, + gridPower, + isImporting: gridPower > 0, + isExporting: gridPower < 0, + currentPrice: null, + timestamp: now, + source: 'manual' + }; + } + + // ═══════════════════════════════════════════════════════════════════ + // ✅ NEW: 15-MINUTE PRICE METHODS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Get all 15-minute price intervals + * Returns array with 96 intervals (today) or 192 (today + tomorrow) + * + * Falls back to expanding hourly prices if provider doesn't support 15-min + */ + getAll15MinPrices() { + if (!this.dynamicProvider) return []; + + try { + const all15min = []; + let foundNative = false; + + // Try KwhPrice provider first + if (this.kwhpriceProvider && typeof this.kwhpriceProvider.getAll15MinPrices === 'function') { + const kwhPrices = this.kwhpriceProvider.getAll15MinPrices(); + if (kwhPrices && kwhPrices.length > 0) { + kwhPrices.forEach(p => all15min.push({ ...p, source: 'kwhprice' })); + foundNative = true; + // Logging moved to provider's fetch method + } + } + + // Try Xadi provider + if (this.xadiProvider && typeof this.xadiProvider.getAll15MinPrices === 'function') { + const xadiPrices = this.xadiProvider.getAll15MinPrices(); + if (xadiPrices && xadiPrices.length > 0) { + xadiPrices.forEach(p => all15min.push({ ...p, source: 'xadi' })); + foundNative = true; + // Logging moved to provider's fetch method + } + } + + // If we got native 15-min data from either source, deduplicate and return + if (foundNative && all15min.length > 0) { + // Deduplicate by timestamp (prefer KwhPrice over Xadi) + const seen = new Map(); + all15min.forEach(p => { + const key = p.timestamp instanceof Date ? p.timestamp.getTime() : new Date(p.timestamp).getTime(); + if (!seen.has(key) || p.source === 'kwhprice') { + seen.set(key, p); + } + }); + const deduplicated = Array.from(seen.values()).sort((a, b) => { + const aTime = a.timestamp instanceof Date ? a.timestamp : new Date(a.timestamp); + const bTime = b.timestamp instanceof Date ? b.timestamp : new Date(b.timestamp); + return aTime - bTime; + }); + // Only log once per hour to avoid spam + if (!this._last15MinLog || Date.now() - this._last15MinLog > 3600000) { + this.log(`📊 Serving ${deduplicated.length} cached 15-min intervals from providers`); + this._last15MinLog = Date.now(); + } + return deduplicated; + } + + // Fallback: Expand hourly to 15-min intervals + if (!this._lastExpandLog || Date.now() - this._lastExpandLog > 3600000) { + this.log('📊 Expanding hourly prices to 15-min intervals (fallback)'); + this._lastExpandLog = Date.now(); + } + return this._expandHourlyTo15Min(); + + } catch (error) { + this.log('Failed to get 15-min prices:', error.message); + return []; + } + } + + /** + * Get current 15-minute price (more accurate than hourly) + */ + getCurrent15MinPrice() { + if (!this.dynamicProvider) return null; + + try { + // Try kwhprice first (consistent with getAll15MinPrices priority) + if (this.kwhpriceProvider && typeof this.kwhpriceProvider.getCurrent15MinPrice === 'function') { + const p = this.kwhpriceProvider.getCurrent15MinPrice(); + if (p !== null && p !== undefined) return p; + } + + // Try xadi (native 15-min intervals) + if (this.xadiProvider && typeof this.xadiProvider.getCurrent15MinPrice === 'function') { + const p = this.xadiProvider.getCurrent15MinPrice(); + if (p !== null && p !== undefined) return p; + } + + // Fallback to hourly + return this.dynamicProvider.getCurrentPrice(); + + } catch (error) { + this.log('Failed to get current 15-min price:', error.message); + return null; + } + } + + /** + * Expand hourly prices to 15-minute intervals + * Each hour gets 4 slots with the same price (00, 15, 30, 45) + * + * This is a fallback when the provider doesn't have native 15-min data + */ + _expandHourlyTo15Min() { + const hourlyPrices = this.dynamicProvider.getAllHourlyPrices(); + const intervals = []; + + hourlyPrices.forEach(hourPrice => { + for (let m = 0; m < 60; m += 15) { + const ts = new Date(hourPrice.timestamp); + ts.setMinutes(m); + + intervals.push({ + hour: hourPrice.hour, + minute: m, + index: intervals.length, + price: hourPrice.price, + timestamp: ts, + hoursFromNow: Math.floor((ts - new Date()) / (1000 * 60 * 60)), + source: hourPrice.source || 'merged' + // Note: No 'isExpanded' flag - this is just converted data + }); + } + }); + + return intervals; + } + + // ═══════════════════════════════════════════════════════════════════ + // EXISTING METHODS (unchanged) + // ═══════════════════════════════════════════════════════════════════ + + _getCurrentRate(now) { + const tariffType = this.settings.tariff_type || 'fixed'; + if (tariffType === 'fixed') return 'standard'; + if (tariffType === 'time_of_use') return this._getTimeOfUseRate(now); + return 'standard'; + } + + _getTimeOfUseRate(now) { + const hour = now.getHours(); + const minute = now.getMinutes(); + const currentMinutes = hour * 60 + minute; + + const peakPeriods = this._parseTimePeriods(this.settings.peak_hours || '17:00-21:00'); + const offPeakPeriods = this._parseTimePeriods(this.settings.off_peak_hours || '23:00-07:00'); + const superOffPeakPeriods = this._parseTimePeriods(this.settings.super_off_peak_hours || ''); + + if (this._isInPeriod(currentMinutes, superOffPeakPeriods)) return 'super-off-peak'; + if (this._isInPeriod(currentMinutes, offPeakPeriods)) return 'off-peak'; + if (this._isInPeriod(currentMinutes, peakPeriods)) return 'peak'; + return 'standard'; + } + + _parseTimePeriods(periodsString) { + if (!periodsString) return []; + return periodsString.split(',') + .map(part => { + const [start, end] = part.trim().split('-'); + if (start && end) { + return { + start: this._timeToMinutes(start.trim()), + end: this._timeToMinutes(end.trim()) + }; + } + return null; + }) + .filter(Boolean); + } + + _timeToMinutes(timeString) { + const [hours, minutes] = timeString.split(':').map(Number); + return hours * 60 + minutes; + } + + _isInPeriod(currentMinutes, periods) { + return periods.some(period => { + if (period.end < period.start) { + return currentMinutes >= period.start || currentMinutes <= period.end; + } + return currentMinutes >= period.start && currentMinutes <= period.end; + }); + } + + _getNextRateChange(now) { + const tariffType = this.settings.tariff_type || 'fixed'; + if (tariffType === 'fixed') return null; + + const currentMinutes = now.getHours() * 60 + now.getMinutes(); + const boundaries = this._getAllBoundaries(); + + for (const boundary of boundaries) { + const minutesUntil = boundary.minutes > currentMinutes + ? boundary.minutes - currentMinutes + : (1440 - currentMinutes) + boundary.minutes; + + if (minutesUntil > 0) { + return { rate: boundary.rate, startsIn: minutesUntil }; + } + } + + return null; + } + + _getAllBoundaries() { + const boundaries = []; + + const addBoundaries = (periodsString, rate) => { + this._parseTimePeriods(periodsString).forEach(period => { + boundaries.push({ minutes: period.start, rate }); + boundaries.push({ minutes: period.end, rate: 'standard' }); + }); + }; + + addBoundaries(this.settings.peak_hours || '', 'peak'); + addBoundaries(this.settings.off_peak_hours || '', 'off-peak'); + addBoundaries(this.settings.super_off_peak_hours || '', 'super-off-peak'); + + return boundaries.sort((a, b) => a.minutes - b.minutes); + } + + getTariffMultiplier(rate) { + return { 'super-off-peak': 0.5, 'off-peak': 0.7, 'standard': 1.0, 'peak': 1.5 }[rate] || 1.0; + } + + updateSettings(newSettings) { + const dynamicChanged = newSettings.enable_dynamic_pricing !== this.settings.enable_dynamic_pricing; + this.settings = { ...this.settings, ...newSettings }; + + if (dynamicChanged) { + if (newSettings.enable_dynamic_pricing) { + this._initializeDynamicProvider(); + } else { + this.dynamicProvider = null; + this.log('Dynamic pricing disabled'); + } + } + + this.log('Tariff settings updated'); + } + + async updateDynamicPrices() { + if (!this.settings.enable_dynamic_pricing) return false; + + try { + await this._selectBestProvider(); + this.log('Dynamic prices updated successfully'); + return true; + } catch (error) { + this.log('Failed to update dynamic prices:', error.message); + return false; + } + } + + getCheapestHours(count = 3, lookAhead = 24) { + if (!this.dynamicProvider) return []; + try { + return this.dynamicProvider.getCheapestHours(count, lookAhead); + } catch (error) { + this.log('Failed to get cheapest hours:', error.message); + return []; + } + } + + getMostExpensiveHours(count = 3, lookAhead = 24) { + if (!this.dynamicProvider) return []; + try { + return this.dynamicProvider.getMostExpensiveHours(count, lookAhead); + } catch (error) { + this.log('Failed to get most expensive hours:', error.message); + return []; + } + } + + getPriceStatistics() { + if (!this.dynamicProvider) return null; + try { + return this.dynamicProvider.getPriceStatistics(); + } catch (error) { + this.log('Failed to get price statistics:', error.message); + return null; + } + } +} + +module.exports = TariffManager; \ No newline at end of file diff --git a/lib/weather-forecaster.js b/lib/weather-forecaster.js new file mode 100644 index 00000000..7be69058 --- /dev/null +++ b/lib/weather-forecaster.js @@ -0,0 +1,690 @@ +'use strict'; + +const fetchWithTimeout = require('../includes/utils/fetchWithTimeout'); + +/** + * WeatherForecaster + * Fetches and processes weather forecast data from Open-Meteo API + */ +class WeatherForecaster { + constructor(homey, learningEngine = null) { + this.homey = homey; + this.learningEngine = learningEngine; + this.cache = null; + this.cacheExpiry = null; + this.log = homey.log.bind(homey); + this.error = homey.error.bind(homey); + } + + /** + * Fetch weather forecast with caching + * @param {number} [latitude] Optional override + * @param {number} [longitude] Optional override + * @returns {Promise} Processed forecast data + */ + async fetchForecast(latitude, longitude, tilt = null, azimuth = null) { + // Check in-memory cache first (3 hours) + if (this.cache && this.cacheExpiry && this.cacheExpiry > Date.now()) { + this.log('Using cached weather forecast'); + return this.cache; + } + + // On restart, try to restore from persistent settings cache before hitting the API + if (!this.cache) { + const restored = this._loadCache(); + if (restored) { + this.cache = restored.cache; + this.cacheExpiry = restored.expiry; + this.log(`Restored weather forecast from settings (expires in ${Math.round((restored.expiry - Date.now()) / 60000)}min)`); + return this.cache; + } + } + + try { + let loc; + if (typeof latitude === 'number' && typeof longitude === 'number') { + loc = { latitude, longitude }; + } else { + loc = await this.getLocation(); + } + + const { latitude: lat, longitude: lon } = loc; + this.log(`Fetching weather for lat: ${lat}, lon: ${lon}`); + + const useTilted = typeof tilt === 'number' && typeof azimuth === 'number'; + + // Run all API calls in parallel + const [ensembleResult, standardResult, tiltedResult] = await Promise.allSettled([ + this._fetchEnsembleRadiation(lat, lon), + this._fetchStandardHourly(lat, lon), + useTilted ? this._fetchTiltedRadiation(lat, lon, tilt, azimuth) : Promise.resolve(null) + ]); + + // Standard hourly + daily is required + if (standardResult.status === 'rejected') { + throw standardResult.reason; + } + + const ensembleData = ensembleResult.status === 'fulfilled' ? ensembleResult.value : null; + const standardData = standardResult.value; + const tiltedData = tiltedResult.status === 'fulfilled' ? tiltedResult.value : null; + + if (!ensembleData) { + this.error('Ensemble radiation fetch failed, falling back to standard shortwave_radiation:', ensembleResult.reason); + } + + const rawData = this._mergeApiResponses(ensembleData, standardData, tiltedData, lat, lon); + + await this._learnFromYesterday(rawData); + + const newForecast = this._processForecast(rawData); + this.cache = this._blendForecast(this.cache, newForecast); + this.cacheExpiry = Date.now() + (60 * 60 * 1000); // 1 hour + + this._saveCache(); + this.log('Weather forecast fetched and cached successfully'); + return this.cache; + } catch (error) { + this.error('Failed to fetch weather forecast:', error); + + // Return cache if available, even if expired + if (this.cache) { + this.log('Returning stale cache due to fetch error'); + return this.cache; + } + + // Return pessimistic default if no cache + return this._getDefaultForecast(); + } + } + + /** + * Simple city lookup via Open-Meteo geocoding API + * @param {string} name + * @returns {Promise<{latitude:number, longitude:number, name:string} | null>} + */ + async lookupCity(name) { + try { + const params = new URLSearchParams({ + name, + count: '1', + language: 'en', + format: 'json' + }); + + const url = `https://geocoding-api.open-meteo.com/v1/search?${params.toString()}`; + this.log(`Geocoding city via Open-Meteo: ${url}`); + + const res = await fetchWithTimeout(url, {}, 10000); + if (!res.ok) { + throw new Error(`Geocoding error: ${res.status} ${res.statusText}`); + } + + const data = await res.json(); + if (!data.results || !data.results.length) { + this.log(`No geocoding results for "${name}"`); + return null; + } + + const best = data.results[0]; + return { + latitude: best.latitude, + longitude: best.longitude, + name: best.name + }; + } catch (err) { + this.error('Failed to lookup city:', err); + return null; + } + } + + /** + * Get Homey's geolocation + * @returns {Promise<{latitude: number, longitude: number}>} + */ + async getLocation() { + try { + const latitude = await this.homey.geolocation.getLatitude(); + const longitude = await this.homey.geolocation.getLongitude(); + + return { latitude, longitude }; + } catch (error) { + this.error('Failed to get Homey location:', error); + // Default to Amsterdam if geolocation fails + return { latitude: 52.3676, longitude: 4.9041 }; + } + } + + /** + * Fetch shortwave_radiation from 3-model ensemble (ECMWF, GFS, ICON) in parallel. + * Models= param is isolated here because it conflicts with daily= (no sunrise/sunset) + * and causes all variables to return with model-specific suffixes. + * @private + */ + async _fetchEnsembleRadiation(lat, lon) { + const params = new URLSearchParams({ + latitude: lat.toString(), + longitude: lon.toString(), + hourly: 'shortwave_radiation', + models: 'ecmwf_ifs04,gfs_seamless,icon_seamless', + past_days: '1', + forecast_days: '3', + timezone: 'UTC' + }); + const url = `https://api.open-meteo.com/v1/forecast?${params.toString()}`; + this.log(`Fetching ensemble radiation: ${url}`); + const res = await fetchWithTimeout(url, {}, 10000); + if (!res.ok) throw new Error(`Ensemble API error: ${res.status} ${res.statusText}`); + return res.json(); + } + + /** + * Fetch all non-radiation hourly variables + daily sunrise/sunset. + * Also includes shortwave_radiation as fallback if ensemble fetch fails. + * @private + */ + async _fetchStandardHourly(lat, lon) { + const params = new URLSearchParams({ + latitude: lat.toString(), + longitude: lon.toString(), + hourly: 'shortwave_radiation,sunshine_duration,temperature_2m,cloud_cover,precipitation_probability,weather_code', + daily: 'sunrise,sunset', + past_days: '1', + forecast_days: '3', + timezone: 'UTC' + }); + const url = `https://api.open-meteo.com/v1/forecast?${params.toString()}`; + this.log(`Fetching standard hourly: ${url}`); + const res = await fetchWithTimeout(url, {}, 10000); + if (!res.ok) throw new Error(`Standard API error: ${res.status} ${res.statusText}`); + return res.json(); + } + + /** + * Fetch panel-angle-adjusted irradiance (only when tilt/azimuth are configured). + * Uses best_match model — not all models support global_tilted_irradiance. + * @private + */ + async _fetchTiltedRadiation(lat, lon, tilt, azimuth) { + const params = new URLSearchParams({ + latitude: lat.toString(), + longitude: lon.toString(), + hourly: 'global_tilted_irradiance', + tilt: tilt.toString(), + azimuth: azimuth.toString(), + past_days: '1', + forecast_days: '3', + timezone: 'UTC' + }); + const url = `https://api.open-meteo.com/v1/forecast?${params.toString()}`; + this.log(`Fetching tilted radiation (tilt=${tilt}°, az=${azimuth}°): ${url}`); + const res = await fetchWithTimeout(url, {}, 10000); + if (!res.ok) throw new Error(`Tilted API error: ${res.status} ${res.statusText}`); + return res.json(); + } + + /** + * Merge ensemble, standard, and optional tilted API responses into a single + * rawData object compatible with _processForecast and _learnFromYesterday. + * @private + */ + _mergeApiResponses(ensembleData, standardData, tiltedData, lat, lon) { + const ENSEMBLE_MODELS = ['ecmwf_ifs04', 'gfs_seamless', 'icon_seamless']; + const times = standardData.hourly.time; + + let shortwave_radiation; + if (ensembleData) { + // Average shortwave_radiation across all models that returned data + shortwave_radiation = times.map((_, i) => { + let sum = 0; + let count = 0; + for (const model of ENSEMBLE_MODELS) { + const val = ensembleData.hourly[`shortwave_radiation_${model}`]?.[i]; + if (typeof val === 'number') { sum += val; count++; } + } + return count > 0 ? Math.round(sum / count) : 0; + }); + const sampleAvg = shortwave_radiation.slice(0, 24).filter(v => v > 0); + if (sampleAvg.length > 0) { + this.log(`Ensemble radiation averaged from ${ENSEMBLE_MODELS.length} models (sample avg: ${Math.round(sampleAvg.reduce((a, b) => a + b, 0) / sampleAvg.length)} W/m²)`); + } + } else { + // Fallback: use standard single-model shortwave_radiation + shortwave_radiation = standardData.hourly.shortwave_radiation; + } + + return { + latitude: lat, + longitude: lon, + timezone: standardData.timezone || 'UTC', + hourly: { + time: times, + shortwave_radiation, + sunshine_duration: standardData.hourly.sunshine_duration, + temperature_2m: standardData.hourly.temperature_2m, + cloud_cover: standardData.hourly.cloud_cover, + precipitation_probability: standardData.hourly.precipitation_probability, + weather_code: standardData.hourly.weather_code, + ...(tiltedData ? { global_tilted_irradiance: tiltedData.hourly.global_tilted_irradiance } : {}) + }, + daily: standardData.daily + }; + } + + /** + * Process raw API response into usable format + * @private + */ + _blendForecast(oldCache, newForecast) { + if (!oldCache) return newForecast; + + const α = 0.6; + const now = Date.now(); + + const oldProfileMap = new Map(oldCache.dailyProfiles.map(p => [p.time.getTime(), p])); + const oldHourlyMap = new Map(oldCache.hourlyForecast.map(p => [p.time.getTime(), p])); + + const dailyProfiles = newForecast.dailyProfiles.map(slot => { + if (slot.time.getTime() <= now) return slot; // past slots: actual data, keep as-is + const old = oldProfileMap.get(slot.time.getTime()); + if (!old) return slot; + return { ...slot, + radiationWm2: Math.round(α * slot.radiationWm2 + (1 - α) * old.radiationWm2), + sunshine: α * slot.sunshine + (1 - α) * old.sunshine, + }; + }); + + const hourlyForecast = newForecast.hourlyForecast.map(slot => { + const old = oldHourlyMap.get(slot.time.getTime()); + if (!old) return slot; + return { ...slot, + radiationWm2: Math.round(α * slot.radiationWm2 + (1 - α) * old.radiationWm2), + sunshine: α * slot.sunshine + (1 - α) * old.sunshine, + }; + }); + + const blendedNow = new Date(now); + return { + ...newForecast, + dailyProfiles, + hourlyForecast, + sunshineNext4Hours: this._sumSunshine(hourlyForecast, 0, 4), + sunshineNext8Hours: this._sumSunshine(hourlyForecast, 0, 8), + sunshineTodayRemaining: this._sumSunshineToday(hourlyForecast, blendedNow), + sunshineTomorrow: this._sumSunshineTomorrow(hourlyForecast, blendedNow), + }; + } + + _processForecast(rawData) { + const now = new Date(); + const hourly = rawData.hourly; + + // Find current hour index (times are UTC, append Z for correct parsing) + const currentIndex = hourly.time.findIndex(t => + new Date(`${t}Z`) > now + ); + + if (currentIndex === -1) { + this.error('Could not find current hour in forecast data'); + return this._getDefaultForecast(); + } + + const biasFactor = this.learningEngine?.getRadiationBiasFactor() ?? 1.0; + + // Extract sunrise/sunset for today and tomorrow (needed for boundary correction below). + // Open-Meteo returns daily values as "YYYY-MM-DDTHH:MM" without timezone suffix when + // timezone=UTC is requested — must append Z to parse as UTC, not local time. + const daily = rawData.daily || {}; + const parseDailyTime = v => v ? new Date(`${v}Z`) : null; + const todaySunrise = parseDailyTime(daily.sunrise?.[0]); + const todaySunset = parseDailyTime(daily.sunset?.[0]); + const tomorrowSunrise = parseDailyTime(daily.sunrise?.[1]); + const tomorrowSunset = parseDailyTime(daily.sunset?.[1]); + + // All available sunrise times for sunrise boundary correction (see below). + const allSunrises = (daily.sunrise || []).map(v => parseDailyTime(v)).filter(Boolean); + + // Process next 36 hourly slots. + const hourlyForecast = []; + const maxHours = Math.min(36, hourly.time.length - currentIndex); + + for (let i = 0; i < maxHours; i++) { + const idx = currentIndex + i; + + const cloudCover = hourly.cloud_cover?.[idx] ?? 100; + const rawRadiation = (hourly.global_tilted_irradiance?.[idx] ?? hourly.shortwave_radiation?.[idx]) ?? 0; + const rawSunshineSec = hourly.sunshine_duration?.[idx] ?? 0; + const weatherCode = hourly.weather_code?.[idx] ?? 0; + + // WMO codes 45 (fog) and 48 (rime fog) — Open-Meteo cloud_cover misses ground-level + // fog because it's not a cloud layer. Fog cuts solar irradiance by ~85–90%. + const isFog = weatherCode === 45 || weatherCode === 48; + const fogFactor = isFog ? 0.12 : 1.0; + + // Cloud factor for sunshine_duration only — shortwave_radiation already + // includes cloud attenuation (Open-Meteo GHI), so applying cloudFactor to + // radiation would double-penalise clouds and create artificial dips. + const cloudFactor = cloudCover <= 40 ? 1.0 + : cloudCover >= 90 ? 0.0 + : (90 - cloudCover) / 50; + + hourlyForecast.push({ + time: new Date(`${hourly.time[idx]}Z`), // Append Z — Open-Meteo returns UTC ISO without tz offset + sunshine: (rawSunshineSec * cloudFactor * fogFactor) / 3600, // seconds → hours (max 1h per slot) + cloudCover, + temp: hourly.temperature_2m?.[idx] ?? 0, + precipProb: hourly.precipitation_probability?.[idx] ?? 0, + weatherCode, + radiationWm2: Math.round(rawRadiation * biasFactor * fogFactor), + }); + } + + // Sunrise boundary correction: Open-Meteo averages radiation over the full 60-min slot, + // so the slot containing sunrise (e.g., 05:00–06:00 with sunrise at 05:27) only has + // 33 min of sun diluted over 60 min → ~55% of peak value. Scale up to the expected + // peak irradiance so PV forecast and optimizer aren't penalised for partial sunrise slots. + // sunshine_duration already reflects actual sun minutes, so no correction needed there. + for (const slot of hourlyForecast) { + const slotStartMs = slot.time.getTime(); + const slotEndMs = slotStartMs + 3_600_000; + for (const sunrise of allSunrises) { + const sunriseMs = sunrise.getTime(); + if (sunriseMs > slotStartMs && sunriseMs < slotEndMs) { + const sunMinutes = (slotEndMs - sunriseMs) / 60_000; + if (sunMinutes >= 2 && sunMinutes <= 58) { + slot.radiationWm2 = Math.round(slot.radiationWm2 * 60 / sunMinutes); + } + } + } + } + + // Build full-day radiation profiles for today + tomorrow (all 24h, including past hours + // from past_days=1 data). Used for PV visualization — hourlyForecast only has future hours. + const todayUtcDate = now.toISOString().slice(0, 10); + const tomorrowDate = new Date(now); + tomorrowDate.setUTCDate(tomorrowDate.getUTCDate() + 1); + const tomorrowUtcDate = tomorrowDate.toISOString().slice(0, 10); + + const dailyProfiles = []; + for (let i = 0; i < hourly.time.length; i++) { + const t = hourly.time[i]; + const dateStr = t.slice(0, 10); + if (dateStr !== todayUtcDate && dateStr !== tomorrowUtcDate) continue; + + const rawRadiation = (hourly.global_tilted_irradiance?.[i] ?? hourly.shortwave_radiation?.[i]) ?? 0; + const rawSunshineSec = hourly.sunshine_duration?.[i] ?? 0; + const cloudCover = hourly.cloud_cover?.[i] ?? 100; + const wCode = hourly.weather_code?.[i] ?? 0; + const fogF = (wCode === 45 || wCode === 48) ? 0.12 : 1.0; + const cloudF = cloudCover <= 40 ? 1.0 : cloudCover >= 90 ? 0.0 : (90 - cloudCover) / 50; + + dailyProfiles.push({ + time: new Date(`${t}Z`), + cloudCover, + sunshine: (rawSunshineSec * cloudF * fogF) / 3600, + radiationWm2: Math.round(rawRadiation * biasFactor * fogF), + weatherCode: wCode + }); + } + + // Apply the same sunrise boundary correction to dailyProfiles (used for past-hours + // PV chart via learned yield factors in device.js pvForecastByDay). + for (const slot of dailyProfiles) { + const slotStartMs = slot.time.getTime(); + const slotEndMs = slotStartMs + 3_600_000; + for (const sunrise of allSunrises) { + const sunriseMs = sunrise.getTime(); + if (sunriseMs > slotStartMs && sunriseMs < slotEndMs) { + const sunMinutes = (slotEndMs - sunriseMs) / 60_000; + if (sunMinutes >= 2 && sunMinutes <= 58) { + slot.radiationWm2 = Math.round(slot.radiationWm2 * 60 / sunMinutes); + } + } + } + } + + return { + sunshineNext4Hours: this._sumSunshine(hourlyForecast, 0, 4), + sunshineNext8Hours: this._sumSunshine(hourlyForecast, 0, 8), + sunshineTodayRemaining: this._sumSunshineToday(hourlyForecast, now), + sunshineTomorrow: this._sumSunshineTomorrow(hourlyForecast, now), + todaySunrise, + todaySunset, + tomorrowSunrise, + tomorrowSunset, + hourlyForecast, + dailyProfiles, + fetchedAt: now, + location: { + latitude: rawData.latitude, + longitude: rawData.longitude, + timezone: rawData.timezone + } + }; + } + + /** + * Compare yesterday's actual radiation (from past_days=1) against what was + * forecasted for those hours, then feed the ratio into the learning engine. + * @private + */ + async _learnFromYesterday(rawData) { + if (!this.learningEngine) return; + + try { + const hourly = rawData.hourly; + const now = new Date(); + + // currentIndex = first hour > now; everything before it is "past" (includes yesterday) + const currentIndex = hourly.time.findIndex(t => new Date(`${t}Z`) > now); + if (currentIndex <= 0) return; + + // Identify yesterday's UTC date string + const yesterday = new Date(now); + yesterday.setUTCDate(yesterday.getUTCDate() - 1); + const yDate = yesterday.toISOString().slice(0, 10); // 'YYYY-MM-DD' + + // Use GTI when available (tilt/azimuth configured) — same source as _processForecast uses. + // Bias factor must be trained on the same radiation quantity we predict with. + const useGti = Array.isArray(hourly.global_tilted_irradiance); + const radField = h => useGti + ? (hourly.global_tilted_irradiance?.[h] ?? hourly.shortwave_radiation?.[h]) + : hourly.shortwave_radiation?.[h]; + + // Extract yesterday's actual radiation for daylight hours (radiation > 10 W/m²) + let actualSum = 0, actualCount = 0; + + for (let i = 0; i < currentIndex; i++) { + const t = hourly.time[i]; + if (!t.startsWith(yDate)) continue; + const rad = radField(i); + if (typeof rad === 'number' && rad > 10) { + actualSum += rad; + actualCount++; + } + } + + if (actualCount === 0) return; // no daylight data for yesterday + + const actualAvg = actualSum / actualCount; + + // Load yesterday's forecasted radiation snapshot from device store (survives app redeploys) + const snapshot = this.learningEngine.getForecastSnapshot(yDate); + if (!snapshot || typeof snapshot.forecastAvgWm2 !== 'number') { + this.log(`No forecast snapshot for ${yDate} — skipping bias learning`); + } else { + await this.learningEngine.recordRadiationAccuracy(snapshot.forecastAvgWm2, actualAvg); + this.log(`Radiation bias for ${yDate} (${useGti ? 'GTI' : 'GHI'}): forecast=${snapshot.forecastAvgWm2.toFixed(0)} actual=${actualAvg.toFixed(0)} W/m²`); + } + + // Save today's forecasted radiation as snapshot for tomorrow's comparison + const todayDate = now.toISOString().slice(0, 10); + let todaySum = 0, todayCount = 0; + + for (let i = currentIndex; i < Math.min(currentIndex + 24, hourly.time.length); i++) { + const t = hourly.time[i]; + if (!t.startsWith(todayDate)) break; + const rad = radField(i); + if (typeof rad === 'number' && rad > 10) { + todaySum += rad; + todayCount++; + } + } + + if (todayCount > 0) { + await this.learningEngine.saveForecastSnapshot(todayDate, todaySum / todayCount); + } + + } catch (err) { + this.error('_learnFromYesterday error:', err.message); + } + } + + /** + * Sum sunshine hours for a range + * @private + */ + _sumSunshine(forecast, startHour, hours) { + let total = 0; + for (let i = startHour; i < startHour + hours && i < forecast.length; i++) { + total += forecast[i].sunshine; + } + return total; + } + + /** + * Sum remaining sunshine for today + * @private + */ + _sumSunshineToday(forecast, now) { + let total = 0; + const endOfDay = new Date(now); + endOfDay.setHours(23, 59, 59, 999); + + for (const hour of forecast) { + if (hour.time <= endOfDay) { + total += hour.sunshine; + } else { + break; + } + } + + return total; + } + + /** + * Sum sunshine for tomorrow + * @private + */ + _sumSunshineTomorrow(forecast, now) { + let total = 0; + const startOfTomorrow = new Date(now); + startOfTomorrow.setDate(startOfTomorrow.getDate() + 1); + startOfTomorrow.setHours(0, 0, 0, 0); + + const endOfTomorrow = new Date(startOfTomorrow); + endOfTomorrow.setHours(23, 59, 59, 999); + + for (const hour of forecast) { + if (hour.time >= startOfTomorrow && hour.time <= endOfTomorrow) { + total += hour.sunshine; + } + } + + return total; + } + + /** + * Calculate sunshine score (0-100) + * Higher score means better sunshine availability + */ + calculateSunScore(weather) { + const next4h = weather.sunshineNext4Hours; + const today = weather.sunshineTodayRemaining; + const tomorrow = weather.sunshineTomorrow; + + let score = 0; + + // Immediate sun (next 4h) = 50 points max + // Full 4 hours of sun = 50 points + score += Math.min(50, (next4h / 4) * 50); + + // Rest of today = 25 points max + // 8 hours of sun = 25 points + score += Math.min(25, (today / 8) * 25); + + // Tomorrow = 25 points max + // 10 hours of sun = 25 points + score += Math.min(25, (tomorrow / 10) * 25); + + return Math.round(score); + } + + /** + * Get default forecast when API fails + * @private + */ + _getDefaultForecast() { + const now = new Date(); + return { + sunshineNext4Hours: 0, + sunshineNext8Hours: 0, + sunshineTodayRemaining: 0, + sunshineTomorrow: 0, + hourlyForecast: [], + fetchedAt: now, + location: null + }; + } + + /** + * Invalidate cache (useful for testing or forced refresh) + */ + invalidateCache() { + this.cache = null; + this.cacheExpiry = null; + this.homey.settings.unset('weather_forecast_cache'); + this.log('Weather cache invalidated'); + } + + _saveCache() { + try { + this.homey.settings.set('weather_forecast_cache', { + expiry: this.cacheExpiry, + cache: this.cache + }); + } catch (e) { + this.error('Failed to persist weather cache:', e.message); + } + } + + _loadCache() { + try { + const stored = this.homey.settings.get('weather_forecast_cache'); + if (!stored || !stored.expiry || stored.expiry <= Date.now()) return null; + + // Revive Date objects (JSON serialization turns them into ISO strings) + const c = stored.cache; + const revive = v => v ? new Date(v) : null; + + if (Array.isArray(c.hourlyForecast)) { + c.hourlyForecast.forEach(h => { h.time = revive(h.time); }); + } + if (Array.isArray(c.dailyProfiles)) { + c.dailyProfiles.forEach(h => { h.time = revive(h.time); }); + } + c.todaySunrise = revive(c.todaySunrise); + c.todaySunset = revive(c.todaySunset); + c.tomorrowSunrise = revive(c.tomorrowSunrise); + c.tomorrowSunset = revive(c.tomorrowSunset); + if (c.fetchedAt) c.fetchedAt = revive(c.fetchedAt); + + return { cache: c, expiry: stored.expiry }; + } catch (e) { + this.error('Failed to load weather cache from settings:', e.message); + return null; + } + } +} + +module.exports = WeatherForecaster; diff --git a/lib/xadi-provider.js b/lib/xadi-provider.js new file mode 100644 index 00000000..f4fda167 --- /dev/null +++ b/lib/xadi-provider.js @@ -0,0 +1,452 @@ +'use strict'; + +const fetchWithTimeout = require('../includes/utils/fetchWithTimeout'); + +/** + * Xadi Day-Ahead Prices Provider + * + * ✅ UPDATED: Now preserves 15-minute intervals from Xadi API + * + * Fetches both /today and /next24h to ensure complete coverage. + * Markup is applied CLIENT-SIDE (not via the Xadi API parameters) so the + * formula is identical to KwhPriceProvider: (spot + markup) × 1.21 + */ +class XadiProvider { + constructor(homey, options = {}) { + this.homey = homey; + this.markup = options.markup !== undefined ? options.markup : 0.11; + this.cache = null; // Hourly averages (backward compatible) + this.cache15min = null; // ✅ NEW: 15-minute intervals + this.cacheExpiry = null; + this.log = homey.log.bind(homey); + this.error = homey.error.bind(homey); + + this._loadCache(); + } + + async _loadCache() { + try { + const cached = await this.homey.settings.get('xadi_cache'); + if (cached && cached.expiry > Date.now()) { + this.cache = cached.prices.map(p => ({ + ...p, + timestamp: new Date(p.timestamp) + })); + // ✅ NEW: Load 15-min cache + this.cache15min = cached.prices15min?.map(p => ({ + ...p, + timestamp: new Date(p.timestamp) + })) || null; + + this.cacheExpiry = cached.expiry; + this.log(`Loaded ${this.cache.length} hourly + ${this.cache15min?.length || 0} 15-min prices from storage (expires in ${Math.round((this.cacheExpiry - Date.now()) / 60000)}min)`); + } + } catch (error) { + this.log('Failed to load cached prices:', error.message); + } + } + + async _saveCache() { + // Cache is saved centrally by MergedPriceProvider — skip individual save + // to reduce settings store pressure and memory churn + } + + /** + * Map a single Xadi API item to a price object. + * Markup applied client-side: (spot + markup) × 1.21 + * + * ✅ NEW: Extracts minute from hour field (e.g., "10:15" → hour:10, minute:15) + * @private + */ + _mapItem(item) { + // Server applies markup + VAT — use price directly + const price = item.price; + const spot = item.markup?.originalPrice ?? item.price; + + // Extract hour and minute from "10:15" format + const [hourStr, minuteStr] = (item.hour || '0:00').split(':'); + const hour = parseInt(hourStr, 10); + const minute = parseInt(minuteStr, 10); + + return { + timestamp: new Date(item.time), + price, + priceMwh: item.priceMwh, + hour, + minute, + originalPrice: spot + }; + } + + /** + * ✅ NEW: Separate 15-minute intervals from hourly averages + * @private + */ + _separateIntervals(allPrices) { + const intervals15min = []; + const hourlyBuckets = {}; + + allPrices.forEach(p => { + // Add to 15-min array + intervals15min.push(p); + + // Also bucket by hour for hourly averages + const hourKey = `${p.timestamp.getFullYear()}-${p.timestamp.getMonth()}-${p.timestamp.getDate()}-${p.hour}`; + if (!hourlyBuckets[hourKey]) { + hourlyBuckets[hourKey] = { prices: [], timestamp: null }; + } + hourlyBuckets[hourKey].prices.push(p.price); + if (p.minute === 0 || !hourlyBuckets[hourKey].timestamp) { + hourlyBuckets[hourKey].timestamp = p.timestamp; + } + }); + + // Create hourly averages + const hourlyPrices = Object.entries(hourlyBuckets).map(([key, bucket]) => { + const avgPrice = bucket.prices.reduce((a, b) => a + b, 0) / bucket.prices.length; + const ts = bucket.timestamp || new Date(key.split('-').slice(0, 3).join('-')); + const hour = parseInt(key.split('-')[3], 10); + + return { + timestamp: ts, + price: avgPrice, + hour, + originalPrice: (avgPrice / 1.21) - this.markup // Reverse: (spot + markup) × 1.21 + }; + }).sort((a, b) => a.timestamp - b.timestamp); + + return { + intervals15min: intervals15min.sort((a, b) => a.timestamp - b.timestamp), + hourlyPrices + }; + } + + /** + * Fetch prices from /today, /next24h and /day/tomorrow. + * API called with markup=0&vat=0 to receive raw spot prices. + * + * ✅ UPDATED: Now stores both 15-min and hourly data + * @param {boolean} force - Force refresh even if cache is valid + */ + async fetchPrices(force = false) { + const now = Date.now(); + const hour = new Date().getHours(); + + const shouldForceRefresh = force && (hour >= 15 && hour <= 16) && + this.cache && (now - (this.cacheExpiry - 60 * 60 * 1000)) > 30 * 60 * 1000; + + if (this.cache && this.cacheExpiry > now && !shouldForceRefresh) { + this.log('Using cached Xadi prices'); + return this.cache; + } + + if (shouldForceRefresh) { + this.log('Forcing price refresh during release window (15:00-16:00)'); + } + + // Server applies both markup and VAT — use item.price directly, no client-side calculation + const baseParams = `markup=${this.markup}&vat=0.21`; + const allPrices = []; + const seenTimestamps = new Set(); + + try { + // 1. /today + const todayUrl = `https://dap.xadi.eu/api/nl/today?${baseParams}`; + this.log(`Fetching Xadi /today (CET): ${todayUrl}`); + try { + const res = await fetchWithTimeout(todayUrl, {}, 10000); + if (res.ok) { + const data = await res.json(); + if (data.status === 'success' && data.data && data.data.length > 0) { + const prices = data.data.map(item => this._mapItem(item)); + prices.forEach(p => { + const ts = p.timestamp.getTime(); + if (!seenTimestamps.has(ts)) { allPrices.push(p); seenTimestamps.add(ts); } + }); + this.log(`✅ Fetched ${prices.length} intervals from /today`); + } + } + } catch (err) { this.log(`Failed to fetch /today: ${err.message}`); } + + // 2. /next24h + const next24hUrl = `https://dap.xadi.eu/api/nl/next24h?${baseParams}`; + this.log(`Fetching Xadi /next24h (CET): ${next24hUrl}`); + try { + const res = await fetchWithTimeout(next24hUrl, {}, 10000); + if (res.ok) { + const data = await res.json(); + if (data.status === 'success' && data.data && data.data.length > 0) { + const prices = data.data.map(item => this._mapItem(item)); + let newCount = 0; + prices.forEach(p => { + const ts = p.timestamp.getTime(); + if (!seenTimestamps.has(ts)) { allPrices.push(p); seenTimestamps.add(ts); newCount++; } + }); + this.log(`✅ Fetched ${prices.length} intervals from /next24h (${newCount} new)`); + } + } + } catch (err) { this.log(`Failed to fetch /next24h: ${err.message}`); } + + // 3. /day/tomorrow + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrowStr = tomorrow.toISOString().split('T')[0]; + const dayAheadUrl = `https://dap.xadi.eu/api/nl/day/${tomorrowStr}?${baseParams}`; + this.log(`Attempting to fetch tomorrow's prices: ${dayAheadUrl}`); + try { + const res = await fetchWithTimeout(dayAheadUrl, {}, 10000); + if (res.ok) { + const data = await res.json(); + if (data.status === 'success' && data.data && data.data.length > 0) { + const prices = data.data.map(item => this._mapItem(item)); + let newCount = 0; + prices.forEach(p => { + const ts = p.timestamp.getTime(); + if (!seenTimestamps.has(ts)) { allPrices.push(p); seenTimestamps.add(ts); newCount++; } + }); + this.log(`✅ Fetched ${prices.length} intervals for tomorrow (${newCount} new)`); + } + } + } catch (err) { this.log(`Tomorrow's prices not yet available: ${err.message}`); } + + if (allPrices.length === 0) throw new Error('No price data available from any endpoint'); + + // ✅ NEW: Separate 15-min and hourly data + const { intervals15min, hourlyPrices } = this._separateIntervals(allPrices); + + const firstTime = intervals15min[0].timestamp; + const lastTime = intervals15min[intervals15min.length - 1].timestamp; + this.log(`📊 Total ${intervals15min.length} 15-min intervals (${hourlyPrices.length} hours) from ${firstTime.toISOString()} to ${lastTime.toISOString()}`); + + // Log sample + const sample = intervals15min.find(p => p.originalPrice > 0) || intervals15min[0]; + this.log(`Price calc sample: (€${sample.originalPrice.toFixed(5)} spot + €${this.markup} markup) × 1.21 = €${sample.price.toFixed(5)}`); + + this.cache15min = intervals15min; + this.cache = hourlyPrices; + this.cacheExpiry = Date.now() + 60 * 60 * 1000; + await this._saveCache(); + + return this.cache; + + } catch (error) { + this.error('Failed to fetch Xadi prices:', error); + if (this.cache) { + this.log('Returning stale cache due to fetch error'); + return this.cache; + } + throw error; + } + } + + // ✅ NEW: Get all 15-minute prices + getAll15MinPrices() { + if (!this.cache15min || this.cache15min.length === 0) return []; + const now = new Date(); + return this.cache15min.map((p, idx) => ({ + hour: p.hour, + minute: p.minute, + index: idx, + price: p.price, + timestamp: p.timestamp, + hoursFromNow: Math.floor((p.timestamp - now) / (1000 * 60 * 60)) + })); + } + + // ✅ NEW: Get current 15-minute price + getCurrent15MinPrice() { + if (!this.cache15min || this.cache15min.length === 0) return null; + const now = new Date(); + const current = this.cache15min.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + const end = new Date(start.getTime() + 15 * 60 * 1000); + return now >= start && now < end; + }); + return current ? current.price : null; + } + + // ════════════════════════════════════════════════════════════ + // EXISTING METHODS (mostly unchanged, work with hourly cache) + // ════════════════════════════════════════════════════════════ + + getCurrentRate() { + if (!this.cache || this.cache.length === 0) return 'standard'; + const now = new Date(); + let current = this.cache.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + return now >= start && now < new Date(start.getTime() + 3600 * 1000); + }) || this.cache.find(p => p.hour === now.getHours()) || this.cache[0]; + + const prices = this.cache.map(p => p.price); + const avg = prices.reduce((a, b) => a + b, 0) / prices.length; + const min = Math.min(...prices); + const max = Math.max(...prices); + const std = this._calculateStdDev(prices, avg); + const price = current.price; + + this.log(`Current price: €${price.toFixed(4)}/kWh | Avg: €${avg.toFixed(4)} | StdDev: ${std.toFixed(4)}`); + + if (price < avg - std || price < min + (max - min) * 0.15) return 'super-off-peak'; + if (price < avg - std * 0.3) return 'off-peak'; + if (price > avg + std * 0.5 || price > max - (max - min) * 0.20) return 'peak'; + return 'standard'; + } + + getCurrentPrice() { + if (!this.cache || this.cache.length === 0) return null; + const now = new Date(); + return ( + this.cache.find(p => { + const start = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + return now >= start && now < new Date(start.getTime() + 3600 * 1000); + }) || + this.cache.find(p => p.hour === now.getHours()) || + this.cache[0] + ).price; + } + + getNextRateChange() { + if (!this.cache || this.cache.length === 0) return null; + const now = new Date(); + const currentRate = this.getCurrentRate(); + for (let i = 1; i < 24; i++) { + const futureTime = new Date(now.getTime() + i * 3600 * 1000); + const futurePrice = this.cache.find(p => p.hour === futureTime.getHours()); + if (!futurePrice) continue; + const futureRate = this._categorizeSinglePrice(futurePrice.price); + if (futureRate !== currentRate) { + return { rate: futureRate, startsIn: i * 60, price: futurePrice.price, hour: futurePrice.hour }; + } + } + return null; + } + + _categorizeSinglePrice(price) { + const prices = this.cache.map(p => p.price); + const avg = prices.reduce((a, b) => a + b, 0) / prices.length; + const min = Math.min(...prices); + const max = Math.max(...prices); + const std = this._calculateStdDev(prices, avg); + if (price < avg - std || price < min + (max - min) * 0.15) return 'super-off-peak'; + if (price < avg - std * 0.3) return 'off-peak'; + if (price > avg + std * 0.5 || price > max - (max - min) * 0.20) return 'peak'; + return 'standard'; + } + + getPriceStatistics() { + if (!this.cache || this.cache.length === 0) return null; + const prices = this.cache.map(p => p.price); + const avg = prices.reduce((a, b) => a + b, 0) / prices.length; + const min = Math.min(...prices); + const max = Math.max(...prices); + return { average: avg, min, max, stdDev: this._calculateStdDev(prices, avg), range: max - min, current: this.getCurrentPrice(), totalHours: prices.length }; + } + + getCheapestHours(count = 3, lookAhead = 24) { + if (!this.cache || this.cache.length === 0) return []; + const now = new Date(); + const future = []; + for (let i = 0; i < lookAhead; i++) { + const ft = new Date(now.getTime() + i * 3600 * 1000); + const p = this.cache.find(q => { + const start = q.timestamp instanceof Date ? q.timestamp : new Date(q.timestamp); + return ft >= start && ft < new Date(start.getTime() + 3600 * 1000); + }) || this.cache.find(q => q.hour === ft.getHours()); + if (p) future.push({ ...p, hoursFromNow: i }); + } + return future.sort((a, b) => a.price - b.price).slice(0, count); + } + + getMostExpensiveHours(count = 3, lookAhead = 24) { + if (!this.cache || this.cache.length === 0) return []; + const now = new Date(); + const future = []; + for (let i = 0; i < lookAhead; i++) { + const ft = new Date(now.getTime() + i * 3600 * 1000); + const p = this.cache.find(q => { + const start = q.timestamp instanceof Date ? q.timestamp : new Date(q.timestamp); + return ft >= start && ft < new Date(start.getTime() + 3600 * 1000); + }) || this.cache.find(q => q.hour === ft.getHours()); + if (p) future.push({ ...p, hoursFromNow: i }); + } + return future.sort((a, b) => b.price - a.price).slice(0, count); + } + + _calculateStdDev(values, mean) { + const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length; + return Math.sqrt(variance); + } + + invalidateCache() { + this.cache = null; + this.cache15min = null; + this.cacheExpiry = null; + this.log('Xadi price cache invalidated'); + } + + getTop3Cheapest() { + if (!this.cache || this.cache.length === 0) return []; + const now = new Date(); + const future = this._getHourlyCache().filter(p => { + const ts = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + return ts >= new Date(now.getTime() - 30 * 60 * 1000); + }); + return [...future].sort((a, b) => a.price - b.price).slice(0, 3) + .map(p => ({ hour: p.hour, price: p.price, timestamp: p.timestamp })); + } + + getTop3MostExpensive() { + if (!this.cache || this.cache.length === 0) return []; + const now = new Date(); + const future = this._getHourlyCache().filter(p => { + const ts = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + return ts >= new Date(now.getTime() - 30 * 60 * 1000); + }); + return [...future].sort((a, b) => b.price - a.price).slice(0, 3) + .map(p => ({ hour: p.hour, price: p.price, timestamp: p.timestamp })); + } + + _getHourlyCache() { + const seen = new Set(); + return this.cache.filter(p => { + const ts = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + const key = `${ts.getFullYear()}-${ts.getMonth()}-${ts.getDate()}-${ts.getHours()}`; + const isOnHour = ts.getMinutes() === 0; + if (isOnHour) { seen.add(key); return true; } + if (!seen.has(key)) { seen.add(key); return true; } + return false; + }); + } + + hasPrices() { + return this.cache && this.cache.length > 0; + } + + getAllHourlyPrices() { + if (!this.cache || this.cache.length === 0) return []; + const now = new Date(); + return this.cache.map(p => { + const ts = p.timestamp instanceof Date ? p.timestamp : new Date(p.timestamp); + return { + hour: p.hour, + index: Math.floor((ts - now) / (1000 * 60 * 60)), + price: p.price, + timestamp: p.timestamp + }; + }); + } + + getCoverageInfo() { + if (!this.cache || this.cache.length === 0) { + return { hasPrices: false, totalHours: 0, firstHour: null, lastHour: null, hoursFromNow: { min: 0, max: 0 } }; + } + const now = new Date(); + const timestamps = this.cache.map(p => p.timestamp); + const first = new Date(Math.min(...timestamps)); + const last = new Date(Math.max(...timestamps)); + const hoursFromNow = this.cache.map(p => Math.floor((p.timestamp - now) / (1000 * 60 * 60))); + return { hasPrices: true, totalHours: this.cache.length, firstHour: first.toISOString(), lastHour: last.toISOString(), hoursFromNow: { min: Math.min(...hoursFromNow), max: Math.max(...hoursFromNow) } }; + } +} + +module.exports = XadiProvider; \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index cabe4f3e..85ecabd1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,13 +1,88 @@ { - "settings": { - "title": "HomeWizard settings", - "intro": "To connect to your HomeWizard we need the settings below.", - "heatlink_intro": "To connect to your Heatlink we need to know to wich HomeWizard it belongs.", - "energylink_intro": "To connect to your Energylink we need to know to wich HomeWizard it belongs.", - "wattcher_intro": "To connect to your Wattcher we need to know to wich HomeWizard it belongs.", - "password": "Password", - "use_ledring": "Use ledring", - "use_ledring_explain": "Ledring lights up green when preset switched successful, red when not successful", - "save": "Save" - } -} \ No newline at end of file + "settings": { + "title": "HomeWizard settings", + "intro": "To connect to your HomeWizard we need the settings below.", + "heatlink_intro": "To connect to your Heatlink we need to know to which HomeWizard it belongs.", + "energylink_intro": "To connect to your Energylink we need to know to which HomeWizard it belongs.", + "wattcher_intro": "To connect to your Wattcher we need to know to which HomeWizard it belongs.", + "thermometer_intro": "To connect to your thermometer we need to know to which HomeWizard it belongs.", + "password": "Password", + "use_ledring": "Use ledring", + "use_ledring_explain": "Ledring lights up green when preset switched successful, red when not successful", + "save": "Save", + "no_ip": "Please enter the HomeWizard's IP address", + "select": "Select HomeWizard", + "select_sensor": "Select Sensor", + "select_thermometer": "Select Thermometer", + "error": "Error: ", + "selection_error": "Invalid selection", + "notfound_error": "Sensor not found", + "thermometer_notfound_error": "Thermometer not found", + "fetch_error": "Could not read sensors", + "changelog": { + "label": "📜 View Changelog" + }, + "support_link": "https://community.homey.app/t/app-pro-homewizard/19267", + "support_label": "🛠️ Support Page", + "tab_baseload": "Baseload", + "tab_planning": "Battery Planning", + "planning_title": "Battery Planning - 48 Hours", + "planning_subtitle": "Expected prices, battery modes and SoC projection for today and tomorrow" + }, + "pair": { + "no_devices_found": "No devices found. Make sure:\n• Local API is enabled in HomeWizard app\n• Device and Homey are on same network\n• mDNS/Bonjour is enabled on your router\n• No VLAN isolation between networks", + "authorize_title": "Authorize", + "authorize_instruction": "Press the button on the device to complete the pairing", + "button_label_waiting": "Waiting for button press...", + "button_label_retry": "Try again", + "authorize_failed_no_button_pressed": "Pairing failed, the button was not pressed. Make sure the button is pressed within 60 seconds.", + "something_went_wrong": "Something went wrong, please try again: ", + "energy_v2": { + "authorize_instruction": "Press the button on the P1 Meter to complete the pairing" + }, + "plugin_battery": { + "authorize_instruction": "Press the button on the Plugin Battery to complete the pairing" + }, + "SDM230_v2": { + "authorize_instruction": "Press the button on the KWH Meter to complete the pairing" + }, + "SDM630_v2": { + "authorize_instruction": "Press the button on the KWH Meter to complete the pairing" + } + }, + "repair": { + "title": "Manual IP Override", + "description": "Use this if mDNS discovery is not working on your network (e.g., VLAN isolation, mesh Wi-Fi issues).", + "warning": "⚠️ Warning: Manual IP will not auto-update if the device gets a new IP address via DHCP. Use static IP reservation in your router.", + "label_current_mode": "Current mode:", + "label_discovery_ip": "Discovery IP:", + "label_manual_ip": "Manual IP address:", + "mode_manual": "Manual IP", + "mode_discovery": "mDNS Discovery (automatic)", + "unknown": "Unknown", + "btn_save": "Save & Test Connection", + "btn_clear": "Return to mDNS Discovery", + "testing": "Testing...", + "invalid_ip": "Invalid IP address format", + "connection_failed": "Cannot connect to device", + "wrong_device": "Wrong device - serial number mismatch", + "ip_required": "Please enter an IP address", + "success": "✓ Manual IP saved successfully", + "cleared": "✓ Returned to mDNS discovery", + "save_failed": "Failed to save manual IP", + "clear_failed": "Failed to clear manual IP", + "load_failed": "Failed to load current settings", + "confirm_clear": "Return to automatic mDNS discovery? Your manual IP will be removed." + }, + "camera": { + "planning_title": "Battery Planning" + }, + "device": { + "init": "Initializing device...", + "unreachable": "Device unreachable" + }, + "errors": { + "auth_failed": "Could not authenticate with your HomeWizard account", + "invalid_credentials": "Invalid credentials" + } +} diff --git a/locales/nl.json b/locales/nl.json index 2a441ce0..674b8cfd 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -1,13 +1,78 @@ { - "settings": { - "title": "HomeWizard instellingen", - "intro": "Om verbinding te kunnen maken met uw HomeWizard zijn onderstaande gegevens benodigd.", - "heatlink_intro": "Om verbinding te kunnen maken met uw Heatlink dient u aan te geven op welke HomeWizard deze is aangesloten.", - "energylink_intro": "Om verbinding te kunnen maken met uw Energylink dient u aan te geven op welke HomeWizard deze is aangesloten.", - "wattcher_intro": "Om verbinding te kunnen maken met uw Wattcher dient u aan te geven op welke HomeWizard deze is aangesloten.", - "password": "Wachtwoord", - "use_ledring": "Gebruik ledring", - "use_ledring_explain": "Ledring licht groen op indien de preset aangepast is, rood indien niet succesvol", - "save": "Opslaan" + "settings": { + "title": "HomeWizard instellingen", + "intro": "Om verbinding te kunnen maken met uw HomeWizard zijn onderstaande gegevens benodigd.", + "heatlink_intro": "Om verbinding te kunnen maken met uw Heatlink dient u aan te geven op welke HomeWizard deze is aangesloten.", + "energylink_intro": "Om verbinding te kunnen maken met uw Energylink dient u aan te geven op welke HomeWizard deze is aangesloten.", + "wattcher_intro": "Om verbinding te kunnen maken met uw Wattcher dient u aan te geven op welke HomeWizard deze is aangesloten.", + "thermometer_intro": "Om verbinding te kunnen maken met uw thermometer dient u aan te geven op welke HomeWizard deze is aangesloten.", + "password": "Wachtwoord", + "use_ledring": "Gebruik ledring", + "use_ledring_explain": "Ledring licht groen op indien de preset aangepast is, rood indien niet succesvol", + "save": "Opslaan", + "no_ip": "Voer het IP-adres van de HomeWizard in", + "select": "Selecteer HomeWizard", + "select_sensor": "Selecteer Sensor", + "select_thermometer": "Selecteer Thermometer", + "error": "Fout: ", + "selection_error": "Ongeldige selectie", + "notfound_error": "Sensor niet gevonden", + "thermometer_notfound_error": "Thermometer niet gevonden", + "fetch_error": "Sensoren uitlezen mislukt", + "changelog": { + "label": "📜 Bekijk Changelog" + }, + "support_label": "🛠️ Support pagina", + "support_link": "https://community.homey.app/t/app-pro-nl-homewizard/71211", + "tab_baseload": "Sluipverbruik", + "tab_planning": "Batterij Planning", + "planning_title": "Batterij Planning - 48 Uur", + "planning_subtitle": "Verwachte prijzen, batterij modi en SoC projectie voor vandaag en morgen" + }, + "pair": { + "no_devices_found": "Geen apparaten gevonden. Controleer:\n• Lokale API is ingeschakeld in HomeWizard app\n• Apparaat en Homey zijn op hetzelfde netwerk\n• mDNS/Bonjour is ingeschakeld op je router\n• Geen VLAN-isolatie tussen netwerken", + "authorize_title": "Authorizeren", + "authorize_instruction": "Druk op de knop van het apparaat om de koppeling te voltooien", + "button_label_waiting": "Wachten op indrukken knop...", + "button_label_retry": "Opnieuw proberen", + "authorize_failed_no_button_pressed": "Koppeling mislukt, de knop is niet ingedrukt. Zorg er voor dat de knop binnen 60 seconden wordt ingedrukt.", + "something_went_wrong": "Er is iets misgegaan, probeer het opnieuw: ", + "energy_v2": { + "authorize_instruction": "Druk op de knop van de P1 Meter om de koppeling te voltooien" + }, + "plugin_battery": { + "authorize_instruction": "Druk op de knop van de plugin batterij om de koppeling te voltooien" } -} \ No newline at end of file + }, + "repair": { + "title": "Handmatig IP-adres instellen", + "description": "Gebruik dit als mDNS-discovery niet werkt op je netwerk (bijv. VLAN-isolatie, mesh Wi-Fi problemen).", + "warning": "⚠️ Waarschuwing: Handmatig IP wordt niet automatisch bijgewerkt als het apparaat een nieuw IP krijgt via DHCP. Gebruik een statische IP-reservering in je router.", + "label_current_mode": "Huidige modus:", + "label_discovery_ip": "Discovery IP:", + "label_manual_ip": "Handmatig IP-adres:", + "mode_manual": "Handmatig IP", + "mode_discovery": "mDNS Discovery (automatisch)", + "unknown": "Onbekend", + "btn_save": "Opslaan & Verbinding testen", + "btn_clear": "Terug naar mDNS Discovery", + "testing": "Testen...", + "invalid_ip": "Ongeldig IP-adres formaat", + "connection_failed": "Kan geen verbinding maken met apparaat", + "wrong_device": "Verkeerd apparaat - serienummer komt niet overeen", + "ip_required": "Voer een IP-adres in", + "success": "✓ Handmatig IP succesvol opgeslagen", + "cleared": "✓ Teruggekeerd naar mDNS discovery", + "save_failed": "Opslaan handmatig IP mislukt", + "clear_failed": "Wissen handmatig IP mislukt", + "load_failed": "Laden huidige instellingen mislukt", + "confirm_clear": "Terugkeren naar automatische mDNS discovery? Je handmatige IP wordt verwijderd." + }, + "camera": { + "planning_title": "Batterij Planning" + }, + "device": { + "init": "Apparaat initialiseren...", + "unreachable": "Apparaat onbereikbaar" + } +} diff --git a/node_modules/request/.eslintrc b/node_modules/request/.eslintrc deleted file mode 100644 index 5a594815..00000000 --- a/node_modules/request/.eslintrc +++ /dev/null @@ -1,45 +0,0 @@ -{ - "env": { - "node": true - }, - "rules": { - // 2-space indentation - "indent": [2, 2, {"SwitchCase": 1}], - // Disallow semi-colons, unless needed to disambiguate statement - "semi": [2, "never"], - // Require strings to use single quotes - "quotes": [2, "single"], - // Require curly braces for all control statements - "curly": 2, - // Disallow using variables and functions before they've been defined - "no-use-before-define": 2, - // Allow any case for variable naming - "camelcase": 0, - // Disallow unused variables, except as function arguments - "no-unused-vars": [2, {"args":"none"}], - // Allow leading underscores for method names - // REASON: we use underscores to denote private methods - "no-underscore-dangle": 0, - // Allow multi spaces around operators since they are - // used for alignment. This is not consistent in the - // code. - "no-multi-spaces": 0, - // Style rule is: most objects use { beforeColon: false, afterColon: true }, unless aligning which uses: - // - // { - // beforeColon : true, - // afterColon : true - // } - // - // eslint can't handle this, so the check is disabled. - "key-spacing": 0, - // Allow shadowing vars in outer scope (needs discussion) - "no-shadow": 0, - // Use if () { } - // ^ space - "keyword-spacing": [2, {"after": true}], - // Use if () { } - // ^ space - "space-before-blocks": [2, "always"] - } -} diff --git a/node_modules/request/.npmignore b/node_modules/request/.npmignore deleted file mode 100644 index 67fe11cc..00000000 --- a/node_modules/request/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -coverage -tests -node_modules -examples -release.sh -disabled.appveyor.yml diff --git a/node_modules/request/.travis.yml b/node_modules/request/.travis.yml deleted file mode 100644 index e5d9bde2..00000000 --- a/node_modules/request/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: node_js - -node_js: - - node - - 0.12 - - 0.10 - -after_script: - - npm run test-cov - - cat ./coverage/lcov.info | codecov - - cat ./coverage/lcov.info | coveralls - -webhooks: - urls: https://webhooks.gitter.im/e/237280ed4796c19cc626 - on_success: change # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: false # default: false - -sudo: false diff --git a/node_modules/request/CHANGELOG.md b/node_modules/request/CHANGELOG.md deleted file mode 100644 index ce6826f2..00000000 --- a/node_modules/request/CHANGELOG.md +++ /dev/null @@ -1,623 +0,0 @@ -## Change Log - -### v2.72.0 (2016/04/17) -- [#2176](https://github.com/request/request/pull/2176) Do not try to pipe Gzip responses with no body (@simov) -- [#2175](https://github.com/request/request/pull/2175) Add 'delete' alias for the 'del' API method (@simov, @MuhanZou) -- [#2172](https://github.com/request/request/pull/2172) Add support for deflate content encoding (@czardoz) -- [#2169](https://github.com/request/request/pull/2169) Add callback option (@simov) -- [#2165](https://github.com/request/request/pull/2165) Check for self.req existence inside the write method (@simov) -- [#2167](https://github.com/request/request/pull/2167) Fix TravisCI badge reference master branch (@a0viedo) - -### v2.71.0 (2016/04/12) -- [#2164](https://github.com/request/request/pull/2164) Catch errors from the underlying http module (@simov) - -### v2.70.0 (2016/04/05) -- [#2147](https://github.com/request/request/pull/2147) Update eslint to version 2.5.3 🚀 (@simov, @greenkeeperio-bot) -- [#2009](https://github.com/request/request/pull/2009) Support JSON stringify replacer argument. (@elyobo) -- [#2142](https://github.com/request/request/pull/2142) Update eslint to version 2.5.1 🚀 (@greenkeeperio-bot) -- [#2128](https://github.com/request/request/pull/2128) Update browserify-istanbul to version 2.0.0 🚀 (@greenkeeperio-bot) -- [#2115](https://github.com/request/request/pull/2115) Update eslint to version 2.3.0 🚀 (@simov, @greenkeeperio-bot) -- [#2089](https://github.com/request/request/pull/2089) Fix badges (@simov) -- [#2092](https://github.com/request/request/pull/2092) Update browserify-istanbul to version 1.0.0 🚀 (@greenkeeperio-bot) -- [#2079](https://github.com/request/request/pull/2079) Accept read stream as body option (@simov) -- [#2070](https://github.com/request/request/pull/2070) Update bl to version 1.1.2 🚀 (@greenkeeperio-bot) -- [#2063](https://github.com/request/request/pull/2063) Up bluebird and oauth-sign (@simov) -- [#2058](https://github.com/request/request/pull/2058) Karma fixes for latest versions (@eiriksm) -- [#2057](https://github.com/request/request/pull/2057) Update contributing guidelines (@simov) -- [#2054](https://github.com/request/request/pull/2054) Update qs to version 6.1.0 🚀 (@greenkeeperio-bot) - -### v2.69.0 (2016/01/27) -- [#2041](https://github.com/request/request/pull/2041) restore aws4 as regular dependency (@rmg) - -### v2.68.0 (2016/01/27) -- [#2036](https://github.com/request/request/pull/2036) Add AWS Signature Version 4 (@simov, @mirkods) -- [#2022](https://github.com/request/request/pull/2022) Convert numeric multipart bodies to string (@simov, @feross) -- [#2024](https://github.com/request/request/pull/2024) Update har-validator dependency for nsp advisory #76 (@TylerDixon) -- [#2016](https://github.com/request/request/pull/2016) Update qs to version 6.0.2 🚀 (@greenkeeperio-bot) -- [#2007](https://github.com/request/request/pull/2007) Use the `extend` module instead of util._extend (@simov) -- [#2003](https://github.com/request/request/pull/2003) Update browserify to version 13.0.0 🚀 (@greenkeeperio-bot) -- [#1989](https://github.com/request/request/pull/1989) Update buffer-equal to version 1.0.0 🚀 (@greenkeeperio-bot) -- [#1956](https://github.com/request/request/pull/1956) Check form-data content-length value before setting up the header (@jongyoonlee) -- [#1958](https://github.com/request/request/pull/1958) Use IncomingMessage.destroy method (@simov) -- [#1952](https://github.com/request/request/pull/1952) Adds example for Tor proxy (@prometheansacrifice) -- [#1943](https://github.com/request/request/pull/1943) Update eslint to version 1.10.3 🚀 (@simov, @greenkeeperio-bot) -- [#1924](https://github.com/request/request/pull/1924) Update eslint to version 1.10.1 🚀 (@greenkeeperio-bot) -- [#1915](https://github.com/request/request/pull/1915) Remove content-length and transfer-encoding headers from defaultProxyHeaderWhiteList (@yaxia) - -### v2.67.0 (2015/11/19) -- [#1913](https://github.com/request/request/pull/1913) Update http-signature to version 1.1.0 🚀 (@greenkeeperio-bot) - -### v2.66.0 (2015/11/18) -- [#1906](https://github.com/request/request/pull/1906) Update README URLs based on HTTP redirects (@ReadmeCritic) -- [#1905](https://github.com/request/request/pull/1905) Convert typed arrays into regular buffers (@simov) -- [#1902](https://github.com/request/request/pull/1902) node-uuid@1.4.7 breaks build 🚨 (@greenkeeperio-bot) -- [#1894](https://github.com/request/request/pull/1894) Fix tunneling after redirection from https (Original: #1881) (@simov, @falms) -- [#1893](https://github.com/request/request/pull/1893) Update eslint to version 1.9.0 🚀 (@greenkeeperio-bot) -- [#1852](https://github.com/request/request/pull/1852) Update eslint to version 1.7.3 🚀 (@simov, @greenkeeperio-bot, @paulomcnally, @michelsalib, @arbaaz, @vladimirich, @LoicMahieu, @JoshWillik, @jzaefferer, @ryanwholey, @djchie, @thisconnect, @mgenereu, @acroca, @Sebmaster, @Bloutiouf) -- [#1876](https://github.com/request/request/pull/1876) Implement loose matching for har mime types (@simov) -- [#1875](https://github.com/request/request/pull/1875) Update bluebird to version 3.0.2 🚀 (@simov, @greenkeeperio-bot) -- [#1871](https://github.com/request/request/pull/1871) Update browserify to version 12.0.1 🚀 (@greenkeeperio-bot) -- [#1866](https://github.com/request/request/pull/1866) Add missing quotes on x-token property in README (@miguelmota) -- [#1874](https://github.com/request/request/pull/1874) Fix typo in README.md (@gswalden) -- [#1860](https://github.com/request/request/pull/1860) Improve referer header tests and docs (@simov) -- [#1861](https://github.com/request/request/pull/1861) Remove redundant call to Stream constructor (@watson) -- [#1857](https://github.com/request/request/pull/1857) Fix Referer header to point to the original host name (@simov) -- [#1850](https://github.com/request/request/pull/1850) Update karma-coverage to version 0.5.3 🚀 (@greenkeeperio-bot) -- [#1847](https://github.com/request/request/pull/1847) Use node's latest version when building (@simov) -- [#1836](https://github.com/request/request/pull/1836) Tunnel: fix wrong property name (@Bloutiouf) -- [#1820](https://github.com/request/request/pull/1820) Set href as request.js uses it (@mgenereu) -- [#1840](https://github.com/request/request/pull/1840) Update http-signature to version 1.0.2 🚀 (@greenkeeperio-bot) -- [#1845](https://github.com/request/request/pull/1845) Update istanbul to version 0.4.0 🚀 (@greenkeeperio-bot) - -### v2.65.0 (2015/10/11) -- [#1833](https://github.com/request/request/pull/1833) Update aws-sign2 to version 0.6.0 🚀 (@greenkeeperio-bot) -- [#1811](https://github.com/request/request/pull/1811) Enable loose cookie parsing in tough-cookie (@Sebmaster) -- [#1830](https://github.com/request/request/pull/1830) Bring back tilde ranges for all dependencies (@simov) -- [#1821](https://github.com/request/request/pull/1821) Implement support for RFC 2617 MD5-sess algorithm. (@BigDSK) -- [#1828](https://github.com/request/request/pull/1828) Updated qs dependency to 5.2.0 (@acroca) -- [#1818](https://github.com/request/request/pull/1818) Extract `readResponseBody` method out of `onRequestResponse` (@pvoisin) -- [#1819](https://github.com/request/request/pull/1819) Run stringify once (@mgenereu) -- [#1814](https://github.com/request/request/pull/1814) Updated har-validator to version 2.0.2 (@greenkeeperio-bot) -- [#1807](https://github.com/request/request/pull/1807) Updated tough-cookie to version 2.1.0 (@greenkeeperio-bot) -- [#1800](https://github.com/request/request/pull/1800) Add caret ranges for devDependencies, except eslint (@simov) -- [#1799](https://github.com/request/request/pull/1799) Updated karma-browserify to version 4.4.0 (@greenkeeperio-bot) -- [#1797](https://github.com/request/request/pull/1797) Updated tape to version 4.2.0 (@greenkeeperio-bot) -- [#1788](https://github.com/request/request/pull/1788) Pinned all dependencies (@greenkeeperio-bot) - -### v2.64.0 (2015/09/25) -- [#1787](https://github.com/request/request/pull/1787) npm ignore examples, release.sh and disabled.appveyor.yml (@thisconnect) -- [#1775](https://github.com/request/request/pull/1775) Fix typo in README.md (@djchie) -- [#1776](https://github.com/request/request/pull/1776) Changed word 'conjuction' to read 'conjunction' in README.md (@ryanwholey) -- [#1785](https://github.com/request/request/pull/1785) Revert: Set default application/json content-type when using json option #1772 (@simov) - -### v2.63.0 (2015/09/21) -- [#1772](https://github.com/request/request/pull/1772) Set default application/json content-type when using json option (@jzaefferer) - -### v2.62.0 (2015/09/15) -- [#1768](https://github.com/request/request/pull/1768) Add node 4.0 to the list of build targets (@simov) -- [#1767](https://github.com/request/request/pull/1767) Query strings now cooperate with unix sockets (@JoshWillik) -- [#1750](https://github.com/request/request/pull/1750) Revert doc about installation of tough-cookie added in #884 (@LoicMahieu) -- [#1746](https://github.com/request/request/pull/1746) Missed comma in Readme (@vladimirich) -- [#1743](https://github.com/request/request/pull/1743) Fix options not being initialized in defaults method (@simov) - -### v2.61.0 (2015/08/19) -- [#1721](https://github.com/request/request/pull/1721) Minor fix in README.md (@arbaaz) -- [#1733](https://github.com/request/request/pull/1733) Avoid useless Buffer transformation (@michelsalib) -- [#1726](https://github.com/request/request/pull/1726) Update README.md (@paulomcnally) -- [#1715](https://github.com/request/request/pull/1715) Fix forever option in node > 0.10 #1709 (@calibr) -- [#1716](https://github.com/request/request/pull/1716) Do not create Buffer from Object in setContentLength(iojs v3.0 issue) (@calibr) -- [#1711](https://github.com/request/request/pull/1711) Add ability to detect connect timeouts (@kevinburke) -- [#1712](https://github.com/request/request/pull/1712) Set certificate expiration to August 2, 2018 (@kevinburke) -- [#1700](https://github.com/request/request/pull/1700) debug() when JSON.parse() on a response body fails (@phillipj) - -### v2.60.0 (2015/07/21) -- [#1687](https://github.com/request/request/pull/1687) Fix caseless bug - content-type not being set for multipart/form-data (@simov, @garymathews) - -### v2.59.0 (2015/07/20) -- [#1671](https://github.com/request/request/pull/1671) Add tests and docs for using the agent, agentClass, agentOptions and forever options. - Forever option defaults to using http(s).Agent in node 0.12+ (@simov) -- [#1679](https://github.com/request/request/pull/1679) Fix - do not remove OAuth param when using OAuth realm (@simov, @jhalickman) -- [#1668](https://github.com/request/request/pull/1668) updated dependencies (@deamme) -- [#1656](https://github.com/request/request/pull/1656) Fix form method (@simov) -- [#1651](https://github.com/request/request/pull/1651) Preserve HEAD method when using followAllRedirects (@simov) -- [#1652](https://github.com/request/request/pull/1652) Update `encoding` option documentation in README.md (@daniel347x) -- [#1650](https://github.com/request/request/pull/1650) Allow content-type overriding when using the `form` option (@simov) -- [#1646](https://github.com/request/request/pull/1646) Clarify the nature of setting `ca` in `agentOptions` (@jeffcharles) - -### v2.58.0 (2015/06/16) -- [#1638](https://github.com/request/request/pull/1638) Use the `extend` module to deep extend in the defaults method (@simov) -- [#1631](https://github.com/request/request/pull/1631) Move tunnel logic into separate module (@simov) -- [#1634](https://github.com/request/request/pull/1634) Fix OAuth query transport_method (@simov) -- [#1603](https://github.com/request/request/pull/1603) Add codecov (@simov) - -### v2.57.0 (2015/05/31) -- [#1615](https://github.com/request/request/pull/1615) Replace '.client' with '.socket' as the former was deprecated in 2.2.0. (@ChALkeR) - -### v2.56.0 (2015/05/28) -- [#1610](https://github.com/request/request/pull/1610) Bump module dependencies (@simov) -- [#1600](https://github.com/request/request/pull/1600) Extract the querystring logic into separate module (@simov) -- [#1607](https://github.com/request/request/pull/1607) Re-generate certificates (@simov) -- [#1599](https://github.com/request/request/pull/1599) Move getProxyFromURI logic below the check for Invaild URI (#1595) (@simov) -- [#1598](https://github.com/request/request/pull/1598) Fix the way http verbs are defined in order to please intellisense IDEs (@simov, @flannelJesus) -- [#1591](https://github.com/request/request/pull/1591) A few minor fixes: (@simov) -- [#1584](https://github.com/request/request/pull/1584) Refactor test-default tests (according to comments in #1430) (@simov) -- [#1585](https://github.com/request/request/pull/1585) Fixing documentation regarding TLS options (#1583) (@mainakae) -- [#1574](https://github.com/request/request/pull/1574) Refresh the oauth_nonce on redirect (#1573) (@simov) -- [#1570](https://github.com/request/request/pull/1570) Discovered tests that weren't properly running (@seanstrom) -- [#1569](https://github.com/request/request/pull/1569) Fix pause before response arrives (@kevinoid) -- [#1558](https://github.com/request/request/pull/1558) Emit error instead of throw (@simov) -- [#1568](https://github.com/request/request/pull/1568) Fix stall when piping gzipped response (@kevinoid) -- [#1560](https://github.com/request/request/pull/1560) Update combined-stream (@apechimp) -- [#1543](https://github.com/request/request/pull/1543) Initial support for oauth_body_hash on json payloads (@simov, @aesopwolf) -- [#1541](https://github.com/request/request/pull/1541) Fix coveralls (@simov) -- [#1540](https://github.com/request/request/pull/1540) Fix recursive defaults for convenience methods (@simov) -- [#1536](https://github.com/request/request/pull/1536) More eslint style rules (@froatsnook) -- [#1533](https://github.com/request/request/pull/1533) Adding dependency status bar to README.md (@YasharF) -- [#1539](https://github.com/request/request/pull/1539) ensure the latest version of har-validator is included (@ahmadnassri) -- [#1516](https://github.com/request/request/pull/1516) forever+pool test (@devTristan) - -### v2.55.0 (2015/04/05) -- [#1520](https://github.com/request/request/pull/1520) Refactor defaults (@simov) -- [#1525](https://github.com/request/request/pull/1525) Delete request headers with undefined value. (@froatsnook) -- [#1521](https://github.com/request/request/pull/1521) Add promise tests (@simov) -- [#1518](https://github.com/request/request/pull/1518) Fix defaults (@simov) -- [#1515](https://github.com/request/request/pull/1515) Allow static invoking of convenience methods (@simov) -- [#1505](https://github.com/request/request/pull/1505) Fix multipart boundary extraction regexp (@simov) -- [#1510](https://github.com/request/request/pull/1510) Fix basic auth form data (@simov) - -### v2.54.0 (2015/03/24) -- [#1501](https://github.com/request/request/pull/1501) HTTP Archive 1.2 support (@ahmadnassri) -- [#1486](https://github.com/request/request/pull/1486) Add a test for the forever agent (@akshayp) -- [#1500](https://github.com/request/request/pull/1500) Adding handling for no auth method and null bearer (@philberg) -- [#1498](https://github.com/request/request/pull/1498) Add table of contents in readme (@simov) -- [#1477](https://github.com/request/request/pull/1477) Add support for qs options via qsOptions key (@simov) -- [#1496](https://github.com/request/request/pull/1496) Parameters encoded to base 64 should be decoded as UTF-8, not ASCII. (@albanm) -- [#1494](https://github.com/request/request/pull/1494) Update eslint (@froatsnook) -- [#1474](https://github.com/request/request/pull/1474) Require Colon in Basic Auth (@erykwalder) -- [#1481](https://github.com/request/request/pull/1481) Fix baseUrl and redirections. (@burningtree) -- [#1469](https://github.com/request/request/pull/1469) Feature/base url (@froatsnook) -- [#1459](https://github.com/request/request/pull/1459) Add option to time request/response cycle (including rollup of redirects) (@aaron-em) -- [#1468](https://github.com/request/request/pull/1468) Re-enable io.js/node 0.12 build (@simov, @mikeal, @BBB) -- [#1442](https://github.com/request/request/pull/1442) Fixed the issue with strictSSL tests on 0.12 & io.js by explicitly setting a cipher that matches the cert. (@BBB, @nicolasmccurdy, @demohi, @simov, @0x4139) -- [#1460](https://github.com/request/request/pull/1460) localAddress or proxy config is lost when redirecting (@simov, @0x4139) -- [#1453](https://github.com/request/request/pull/1453) Test on Node.js 0.12 and io.js with allowed failures (@nicolasmccurdy, @demohi) -- [#1426](https://github.com/request/request/pull/1426) Fixing tests to pass on io.js and node 0.12 (only test-https.js stiff failing) (@mikeal) -- [#1446](https://github.com/request/request/pull/1446) Missing HTTP referer header with redirects Fixes #1038 (@simov, @guimonz) -- [#1428](https://github.com/request/request/pull/1428) Deprecate Node v0.8.x (@nylen) -- [#1436](https://github.com/request/request/pull/1436) Add ability to set a requester without setting default options (@tikotzky) -- [#1435](https://github.com/request/request/pull/1435) dry up verb methods (@sethpollack) -- [#1423](https://github.com/request/request/pull/1423) Allow fully qualified multipart content-type header (@simov) -- [#1430](https://github.com/request/request/pull/1430) Fix recursive requester (@tikotzky) -- [#1429](https://github.com/request/request/pull/1429) Throw error when making HEAD request with a body (@tikotzky) -- [#1419](https://github.com/request/request/pull/1419) Add note that the project is broken in 0.12.x (@nylen) -- [#1413](https://github.com/request/request/pull/1413) Fix basic auth (@simov) -- [#1397](https://github.com/request/request/pull/1397) Improve pipe-from-file tests (@nylen) - -### v2.53.0 (2015/02/02) -- [#1396](https://github.com/request/request/pull/1396) Do not rfc3986 escape JSON bodies (@nylen, @simov) -- [#1392](https://github.com/request/request/pull/1392) Improve `timeout` option description (@watson) - -### v2.52.0 (2015/02/02) -- [#1383](https://github.com/request/request/pull/1383) Add missing HTTPS options that were not being passed to tunnel (@brichard19) (@nylen) -- [#1388](https://github.com/request/request/pull/1388) Upgrade mime-types package version (@roderickhsiao) -- [#1389](https://github.com/request/request/pull/1389) Revise Setup Tunnel Function (@seanstrom) -- [#1374](https://github.com/request/request/pull/1374) Allow explicitly disabling tunneling for proxied https destinations (@nylen) -- [#1376](https://github.com/request/request/pull/1376) Use karma-browserify for tests. Add browser test coverage reporter. (@eiriksm) -- [#1366](https://github.com/request/request/pull/1366) Refactor OAuth into separate module (@simov) -- [#1373](https://github.com/request/request/pull/1373) Rewrite tunnel test to be pure Node.js (@nylen) -- [#1371](https://github.com/request/request/pull/1371) Upgrade test reporter (@nylen) -- [#1360](https://github.com/request/request/pull/1360) Refactor basic, bearer, digest auth logic into separate class (@simov) -- [#1354](https://github.com/request/request/pull/1354) Remove circular dependency from debugging code (@nylen) -- [#1351](https://github.com/request/request/pull/1351) Move digest auth into private prototype method (@simov) -- [#1352](https://github.com/request/request/pull/1352) Update hawk dependency to ~2.3.0 (@mridgway) -- [#1353](https://github.com/request/request/pull/1353) Correct travis-ci badge (@dogancelik) -- [#1349](https://github.com/request/request/pull/1349) Make sure we return on errored browser requests. (@eiriksm) -- [#1346](https://github.com/request/request/pull/1346) getProxyFromURI Extraction Refactor (@seanstrom) -- [#1337](https://github.com/request/request/pull/1337) Standardize test ports on 6767 (@nylen) -- [#1341](https://github.com/request/request/pull/1341) Emit FormData error events as Request error events (@nylen, @rwky) -- [#1343](https://github.com/request/request/pull/1343) Clean up readme badges, and add Travis and Coveralls badges (@nylen) -- [#1345](https://github.com/request/request/pull/1345) Update README.md (@Aaron-Hartwig) -- [#1338](https://github.com/request/request/pull/1338) Always wait for server.close() callback in tests (@nylen) -- [#1342](https://github.com/request/request/pull/1342) Add mock https server and redo start of browser tests for this purpose. (@eiriksm) -- [#1339](https://github.com/request/request/pull/1339) Improve auth docs (@nylen) -- [#1335](https://github.com/request/request/pull/1335) Add support for OAuth plaintext signature method (@simov) -- [#1332](https://github.com/request/request/pull/1332) Add clean script to remove test-browser.js after the tests run (@seanstrom) -- [#1327](https://github.com/request/request/pull/1327) Fix errors generating coverage reports. (@nylen) -- [#1330](https://github.com/request/request/pull/1330) Return empty buffer upon empty response body and encoding is set to null (@seanstrom) -- [#1326](https://github.com/request/request/pull/1326) Use faster container-based infrastructure on Travis (@nylen) -- [#1315](https://github.com/request/request/pull/1315) Implement rfc3986 option (@simov, @nylen, @apoco, @DullReferenceException, @mmalecki, @oliamb, @cliffcrosland, @LewisJEllis, @eiriksm, @poislagarde) -- [#1314](https://github.com/request/request/pull/1314) Detect urlencoded form data header via regex (@simov) -- [#1317](https://github.com/request/request/pull/1317) Improve OAuth1.0 server side flow example (@simov) - -### v2.51.0 (2014/12/10) -- [#1310](https://github.com/request/request/pull/1310) Revert changes introduced in https://github.com/request/request/pull/1282 (@simov) - -### v2.50.0 (2014/12/09) -- [#1308](https://github.com/request/request/pull/1308) Add browser test to keep track of browserify compability. (@eiriksm) -- [#1299](https://github.com/request/request/pull/1299) Add optional support for jsonReviver (@poislagarde) -- [#1277](https://github.com/request/request/pull/1277) Add Coveralls configuration (@simov) -- [#1307](https://github.com/request/request/pull/1307) Upgrade form-data, add back browserify compability. Fixes #455. (@eiriksm) -- [#1305](https://github.com/request/request/pull/1305) Fix typo in README.md (@LewisJEllis) -- [#1288](https://github.com/request/request/pull/1288) Update README.md to explain custom file use case (@cliffcrosland) - -### v2.49.0 (2014/11/28) -- [#1295](https://github.com/request/request/pull/1295) fix(proxy): no-proxy false positive (@oliamb) -- [#1292](https://github.com/request/request/pull/1292) Upgrade `caseless` to 0.8.1 (@mmalecki) -- [#1276](https://github.com/request/request/pull/1276) Set transfer encoding for multipart/related to chunked by default (@simov) -- [#1275](https://github.com/request/request/pull/1275) Fix multipart content-type headers detection (@simov) -- [#1269](https://github.com/request/request/pull/1269) adds streams example for review (@tbuchok) -- [#1238](https://github.com/request/request/pull/1238) Add examples README.md (@simov) - -### v2.48.0 (2014/11/12) -- [#1263](https://github.com/request/request/pull/1263) Fixed a syntax error / typo in README.md (@xna2) -- [#1253](https://github.com/request/request/pull/1253) Add multipart chunked flag (@simov, @nylen) -- [#1251](https://github.com/request/request/pull/1251) Clarify that defaults() does not modify global defaults (@nylen) -- [#1250](https://github.com/request/request/pull/1250) Improve documentation for pool and maxSockets options (@nylen) -- [#1237](https://github.com/request/request/pull/1237) Documenting error handling when using streams (@vmattos) -- [#1244](https://github.com/request/request/pull/1244) Finalize changelog command (@nylen) -- [#1241](https://github.com/request/request/pull/1241) Fix typo (@alexanderGugel) -- [#1223](https://github.com/request/request/pull/1223) Show latest version number instead of "upcoming" in changelog (@nylen) -- [#1236](https://github.com/request/request/pull/1236) Document how to use custom CA in README (#1229) (@hypesystem) -- [#1228](https://github.com/request/request/pull/1228) Support for oauth with RSA-SHA1 signing (@nylen) -- [#1216](https://github.com/request/request/pull/1216) Made json and multipart options coexist (@nylen, @simov) -- [#1225](https://github.com/request/request/pull/1225) Allow header white/exclusive lists in any case. (@RReverser) - -### v2.47.0 (2014/10/26) -- [#1222](https://github.com/request/request/pull/1222) Move from mikeal/request to request/request (@nylen) -- [#1220](https://github.com/request/request/pull/1220) update qs dependency to 2.3.1 (@FredKSchott) -- [#1212](https://github.com/request/request/pull/1212) Improve tests/test-timeout.js (@nylen) -- [#1219](https://github.com/request/request/pull/1219) remove old globalAgent workaround for node 0.4 (@request) -- [#1214](https://github.com/request/request/pull/1214) Remove cruft left over from optional dependencies (@nylen) -- [#1215](https://github.com/request/request/pull/1215) Add proxyHeaderExclusiveList option for proxy-only headers. (@RReverser) -- [#1211](https://github.com/request/request/pull/1211) Allow 'Host' header instead of 'host' and remember case across redirects (@nylen) -- [#1208](https://github.com/request/request/pull/1208) Improve release script (@nylen) -- [#1213](https://github.com/request/request/pull/1213) Support for custom cookie store (@nylen, @mitsuru) -- [#1197](https://github.com/request/request/pull/1197) Clean up some code around setting the agent (@FredKSchott) -- [#1209](https://github.com/request/request/pull/1209) Improve multipart form append test (@simov) -- [#1207](https://github.com/request/request/pull/1207) Update changelog (@nylen) -- [#1185](https://github.com/request/request/pull/1185) Stream multipart/related bodies (@simov) - -### v2.46.0 (2014/10/23) -- [#1198](https://github.com/request/request/pull/1198) doc for TLS/SSL protocol options (@shawnzhu) -- [#1200](https://github.com/request/request/pull/1200) Add a Gitter chat badge to README.md (@gitter-badger) -- [#1196](https://github.com/request/request/pull/1196) Upgrade taper test reporter to v0.3.0 (@nylen) -- [#1199](https://github.com/request/request/pull/1199) Fix lint error: undeclared var i (@nylen) -- [#1191](https://github.com/request/request/pull/1191) Move self.proxy decision logic out of init and into a helper (@FredKSchott) -- [#1190](https://github.com/request/request/pull/1190) Move _buildRequest() logic back into init (@FredKSchott) -- [#1186](https://github.com/request/request/pull/1186) Support Smarter Unix URL Scheme (@FredKSchott) -- [#1178](https://github.com/request/request/pull/1178) update form documentation for new usage (@FredKSchott) -- [#1180](https://github.com/request/request/pull/1180) Enable no-mixed-requires linting rule (@nylen) -- [#1184](https://github.com/request/request/pull/1184) Don't forward authorization header across redirects to different hosts (@nylen) -- [#1183](https://github.com/request/request/pull/1183) Correct README about pre and postamble CRLF using multipart and not mult... (@netpoetica) -- [#1179](https://github.com/request/request/pull/1179) Lint tests directory (@nylen) -- [#1169](https://github.com/request/request/pull/1169) add metadata for form-data file field (@dotcypress) -- [#1173](https://github.com/request/request/pull/1173) remove optional dependencies (@seanstrom) -- [#1165](https://github.com/request/request/pull/1165) Cleanup event listeners and remove function creation from init (@FredKSchott) -- [#1174](https://github.com/request/request/pull/1174) update the request.cookie docs to have a valid cookie example (@seanstrom) -- [#1168](https://github.com/request/request/pull/1168) create a detach helper and use detach helper in replace of nextTick (@seanstrom) -- [#1171](https://github.com/request/request/pull/1171) in post can send form data and use callback (@MiroRadenovic) -- [#1159](https://github.com/request/request/pull/1159) accept charset for x-www-form-urlencoded content-type (@seanstrom) -- [#1157](https://github.com/request/request/pull/1157) Update README.md: body with json=true (@Rob--W) -- [#1164](https://github.com/request/request/pull/1164) Disable tests/test-timeout.js on Travis (@nylen) -- [#1153](https://github.com/request/request/pull/1153) Document how to run a single test (@nylen) -- [#1144](https://github.com/request/request/pull/1144) adds documentation for the "response" event within the streaming section (@tbuchok) -- [#1162](https://github.com/request/request/pull/1162) Update eslintrc file to no longer allow past errors (@FredKSchott) -- [#1155](https://github.com/request/request/pull/1155) Support/use self everywhere (@seanstrom) -- [#1161](https://github.com/request/request/pull/1161) fix no-use-before-define lint warnings (@emkay) -- [#1156](https://github.com/request/request/pull/1156) adding curly brackets to get rid of lint errors (@emkay) -- [#1151](https://github.com/request/request/pull/1151) Fix localAddress test on OS X (@nylen) -- [#1145](https://github.com/request/request/pull/1145) documentation: fix outdated reference to setCookieSync old name in README (@FredKSchott) -- [#1131](https://github.com/request/request/pull/1131) Update pool documentation (@FredKSchott) -- [#1143](https://github.com/request/request/pull/1143) Rewrite all tests to use tape (@nylen) -- [#1137](https://github.com/request/request/pull/1137) Add ability to specifiy querystring lib in options. (@jgrund) -- [#1138](https://github.com/request/request/pull/1138) allow hostname and port in place of host on uri (@cappslock) -- [#1134](https://github.com/request/request/pull/1134) Fix multiple redirects and `self.followRedirect` (@blakeembrey) -- [#1130](https://github.com/request/request/pull/1130) documentation fix: add note about npm test for contributing (@FredKSchott) -- [#1120](https://github.com/request/request/pull/1120) Support/refactor request setup tunnel (@seanstrom) -- [#1129](https://github.com/request/request/pull/1129) linting fix: convert double quote strings to use single quotes (@FredKSchott) -- [#1124](https://github.com/request/request/pull/1124) linting fix: remove unneccesary semi-colons (@FredKSchott) - -### v2.45.0 (2014/10/06) -- [#1128](https://github.com/request/request/pull/1128) Add test for setCookie regression (@nylen) -- [#1127](https://github.com/request/request/pull/1127) added tests around using objects as values in a query string (@bcoe) -- [#1103](https://github.com/request/request/pull/1103) Support/refactor request constructor (@nylen, @seanstrom) -- [#1119](https://github.com/request/request/pull/1119) add basic linting to request library (@FredKSchott) -- [#1121](https://github.com/request/request/pull/1121) Revert "Explicitly use sync versions of cookie functions" (@nylen) -- [#1118](https://github.com/request/request/pull/1118) linting fix: Restructure bad empty if statement (@FredKSchott) -- [#1117](https://github.com/request/request/pull/1117) Fix a bad check for valid URIs (@FredKSchott) -- [#1113](https://github.com/request/request/pull/1113) linting fix: space out operators (@FredKSchott) -- [#1116](https://github.com/request/request/pull/1116) Fix typo in `noProxyHost` definition (@FredKSchott) -- [#1114](https://github.com/request/request/pull/1114) linting fix: Added a `new` operator that was missing when creating and throwing a new error (@FredKSchott) -- [#1096](https://github.com/request/request/pull/1096) No_proxy support (@samcday) -- [#1107](https://github.com/request/request/pull/1107) linting-fix: remove unused variables (@FredKSchott) -- [#1112](https://github.com/request/request/pull/1112) linting fix: Make return values consistent and more straitforward (@FredKSchott) -- [#1111](https://github.com/request/request/pull/1111) linting fix: authPieces was getting redeclared (@FredKSchott) -- [#1105](https://github.com/request/request/pull/1105) Use strict mode in request (@FredKSchott) -- [#1110](https://github.com/request/request/pull/1110) linting fix: replace lazy '==' with more strict '===' (@FredKSchott) -- [#1109](https://github.com/request/request/pull/1109) linting fix: remove function call from if-else conditional statement (@FredKSchott) -- [#1102](https://github.com/request/request/pull/1102) Fix to allow setting a `requester` on recursive calls to `request.defaults` (@tikotzky) -- [#1095](https://github.com/request/request/pull/1095) Tweaking engines in package.json (@pdehaan) -- [#1082](https://github.com/request/request/pull/1082) Forward the socket event from the httpModule request (@seanstrom) -- [#972](https://github.com/request/request/pull/972) Clarify gzip handling in the README (@kevinoid) -- [#1089](https://github.com/request/request/pull/1089) Mention that encoding defaults to utf8, not Buffer (@stuartpb) -- [#1088](https://github.com/request/request/pull/1088) Fix cookie example in README.md and make it more clear (@pipi32167) -- [#1027](https://github.com/request/request/pull/1027) Add support for multipart form data in request options. (@crocket) -- [#1076](https://github.com/request/request/pull/1076) use Request.abort() to abort the request when the request has timed-out (@seanstrom) -- [#1068](https://github.com/request/request/pull/1068) add optional postamble required by .NET multipart requests (@netpoetica) - -### v2.43.0 (2014/09/18) -- [#1057](https://github.com/request/request/pull/1057) Defaults should not overwrite defined options (@davidwood) -- [#1046](https://github.com/request/request/pull/1046) Propagate datastream errors, useful in case gzip fails. (@ZJONSSON, @Janpot) -- [#1063](https://github.com/request/request/pull/1063) copy the input headers object #1060 (@finnp) -- [#1031](https://github.com/request/request/pull/1031) Explicitly use sync versions of cookie functions (@ZJONSSON) -- [#1056](https://github.com/request/request/pull/1056) Fix redirects when passing url.parse(x) as URL to convenience method (@nylen) - -### v2.42.0 (2014/09/04) -- [#1053](https://github.com/request/request/pull/1053) Fix #1051 Parse auth properly when using non-tunneling proxy (@isaacs) - -### v2.41.0 (2014/09/04) -- [#1050](https://github.com/request/request/pull/1050) Pass whitelisted headers to tunneling proxy. Organize all tunneling logic. (@isaacs, @Feldhacker) -- [#1035](https://github.com/request/request/pull/1035) souped up nodei.co badge (@rvagg) -- [#1048](https://github.com/request/request/pull/1048) Aws is now possible over a proxy (@steven-aerts) -- [#1039](https://github.com/request/request/pull/1039) extract out helper functions to a helper file (@seanstrom) -- [#1021](https://github.com/request/request/pull/1021) Support/refactor indexjs (@seanstrom) -- [#1033](https://github.com/request/request/pull/1033) Improve and document debug options (@nylen) -- [#1034](https://github.com/request/request/pull/1034) Fix readme headings (@nylen) -- [#1030](https://github.com/request/request/pull/1030) Allow recursive request.defaults (@tikotzky) -- [#1029](https://github.com/request/request/pull/1029) Fix a couple of typos (@nylen) -- [#675](https://github.com/request/request/pull/675) Checking for SSL fault on connection before reading SSL properties (@VRMink) -- [#989](https://github.com/request/request/pull/989) Added allowRedirect function. Should return true if redirect is allowed or false otherwise (@doronin) -- [#1025](https://github.com/request/request/pull/1025) [fixes #1023] Set self._ended to true once response has ended (@mridgway) -- [#1020](https://github.com/request/request/pull/1020) Add back removed debug metadata (@FredKSchott) -- [#1008](https://github.com/request/request/pull/1008) Moving to module instead of cutomer buffer concatenation. (@mikeal) -- [#770](https://github.com/request/request/pull/770) Added dependency badge for README file; (@timgluz, @mafintosh, @lalitkapoor, @stash, @bobyrizov) -- [#1016](https://github.com/request/request/pull/1016) toJSON no longer results in an infinite loop, returns simple objects (@FredKSchott) -- [#1018](https://github.com/request/request/pull/1018) Remove pre-0.4.4 HTTPS fix (@mmalecki) -- [#1006](https://github.com/request/request/pull/1006) Migrate to caseless, fixes #1001 (@mikeal) -- [#995](https://github.com/request/request/pull/995) Fix parsing array of objects (@sjonnet19) -- [#999](https://github.com/request/request/pull/999) Fix fallback for browserify for optional modules. (@eiriksm) -- [#996](https://github.com/request/request/pull/996) Wrong oauth signature when multiple same param keys exist [updated] (@bengl, @hyjin) - -### v2.40.0 (2014/08/06) -- [#992](https://github.com/request/request/pull/992) Fix security vulnerability. Update qs (@poeticninja) -- [#988](https://github.com/request/request/pull/988) “--” -> “—” (@upisfree) -- [#987](https://github.com/request/request/pull/987) Show optional modules as being loaded by the module that reqeusted them (@iarna) - -### v2.39.0 (2014/07/24) -- [#976](https://github.com/request/request/pull/976) Update README.md (@pvoznenko) - -### v2.38.0 (2014/07/22) -- [#952](https://github.com/request/request/pull/952) Adding support to client certificate with proxy use case (@ofirshaked) -- [#884](https://github.com/request/request/pull/884) Documented tough-cookie installation. (@wbyoung) -- [#935](https://github.com/request/request/pull/935) Correct repository url (@fritx) -- [#963](https://github.com/request/request/pull/963) Update changelog (@nylen) -- [#960](https://github.com/request/request/pull/960) Support gzip with encoding on node pre-v0.9.4 (@kevinoid) -- [#953](https://github.com/request/request/pull/953) Add async Content-Length computation when using form-data (@LoicMahieu) -- [#844](https://github.com/request/request/pull/844) Add support for HTTP[S]_PROXY environment variables. Fixes #595. (@jvmccarthy) -- [#946](https://github.com/request/request/pull/946) defaults: merge headers (@aj0strow) - -### v2.37.0 (2014/07/07) -- [#957](https://github.com/request/request/pull/957) Silence EventEmitter memory leak warning #311 (@watson) -- [#955](https://github.com/request/request/pull/955) check for content-length header before setting it in nextTick (@camilleanne) -- [#951](https://github.com/request/request/pull/951) Add support for gzip content decoding (@kevinoid) -- [#949](https://github.com/request/request/pull/949) Manually enter querystring in form option (@charlespwd) -- [#944](https://github.com/request/request/pull/944) Make request work with browserify (@eiriksm) -- [#943](https://github.com/request/request/pull/943) New mime module (@eiriksm) -- [#927](https://github.com/request/request/pull/927) Bump version of hawk dep. (@samccone) -- [#907](https://github.com/request/request/pull/907) append secureOptions to poolKey (@medovob) - -### v2.35.0 (2014/05/17) -- [#901](https://github.com/request/request/pull/901) Fixes #555 (@pigulla) -- [#897](https://github.com/request/request/pull/897) merge with default options (@vohof) -- [#891](https://github.com/request/request/pull/891) fixes 857 - options object is mutated by calling request (@lalitkapoor) -- [#869](https://github.com/request/request/pull/869) Pipefilter test (@tgohn) -- [#866](https://github.com/request/request/pull/866) Fix typo (@dandv) -- [#861](https://github.com/request/request/pull/861) Add support for RFC 6750 Bearer Tokens (@phedny) -- [#809](https://github.com/request/request/pull/809) upgrade tunnel-proxy to 0.4.0 (@ksato9700) -- [#850](https://github.com/request/request/pull/850) Fix word consistency in readme (@0xNobody) -- [#810](https://github.com/request/request/pull/810) add some exposition to mpu example in README.md (@mikermcneil) -- [#840](https://github.com/request/request/pull/840) improve error reporting for invalid protocols (@FND) -- [#821](https://github.com/request/request/pull/821) added secureOptions back (@nw) -- [#815](https://github.com/request/request/pull/815) Create changelog based on pull requests (@lalitkapoor) - -### v2.34.0 (2014/02/18) -- [#516](https://github.com/request/request/pull/516) UNIX Socket URL Support (@lyuzashi) -- [#801](https://github.com/request/request/pull/801) 794 ignore cookie parsing and domain errors (@lalitkapoor) -- [#802](https://github.com/request/request/pull/802) Added the Apache license to the package.json. (@keskival) -- [#793](https://github.com/request/request/pull/793) Adds content-length calculation when submitting forms using form-data li... (@Juul) -- [#785](https://github.com/request/request/pull/785) Provide ability to override content-type when `json` option used (@vvo) -- [#781](https://github.com/request/request/pull/781) simpler isReadStream function (@joaojeronimo) - -### v2.32.0 (2014/01/16) -- [#767](https://github.com/request/request/pull/767) Use tough-cookie CookieJar sync API (@stash) -- [#764](https://github.com/request/request/pull/764) Case-insensitive authentication scheme (@bobyrizov) -- [#763](https://github.com/request/request/pull/763) Upgrade tough-cookie to 0.10.0 (@stash) -- [#744](https://github.com/request/request/pull/744) Use Cookie.parse (@lalitkapoor) -- [#757](https://github.com/request/request/pull/757) require aws-sign2 (@mafintosh) - -### v2.31.0 (2014/01/08) -- [#645](https://github.com/request/request/pull/645) update twitter api url to v1.1 (@mick) -- [#746](https://github.com/request/request/pull/746) README: Markdown code highlight (@weakish) -- [#745](https://github.com/request/request/pull/745) updating setCookie example to make it clear that the callback is required (@emkay) -- [#742](https://github.com/request/request/pull/742) Add note about JSON output body type (@iansltx) -- [#741](https://github.com/request/request/pull/741) README example is using old cookie jar api (@emkay) -- [#736](https://github.com/request/request/pull/736) Fix callback arguments documentation (@mmalecki) - -### v2.30.0 (2013/12/13) -- [#732](https://github.com/request/request/pull/732) JSHINT: Creating global 'for' variable. Should be 'for (var ...'. (@Fritz-Lium) -- [#730](https://github.com/request/request/pull/730) better HTTP DIGEST support (@dai-shi) -- [#728](https://github.com/request/request/pull/728) Fix TypeError when calling request.cookie (@scarletmeow) - -### v2.29.0 (2013/12/06) -- [#727](https://github.com/request/request/pull/727) fix requester bug (@jchris) - -### v2.28.0 (2013/12/04) -- [#724](https://github.com/request/request/pull/724) README.md: add custom HTTP Headers example. (@tcort) -- [#719](https://github.com/request/request/pull/719) Made a comment gender neutral. (@unsetbit) -- [#715](https://github.com/request/request/pull/715) Request.multipart no longer crashes when header 'Content-type' present (@pastaclub) -- [#710](https://github.com/request/request/pull/710) Fixing listing in callback part of docs. (@lukasz-zak) -- [#696](https://github.com/request/request/pull/696) Edited README.md for formatting and clarity of phrasing (@Zearin) -- [#694](https://github.com/request/request/pull/694) Typo in README (@VRMink) -- [#690](https://github.com/request/request/pull/690) Handle blank password in basic auth. (@diversario) -- [#682](https://github.com/request/request/pull/682) Optional dependencies (@Turbo87) -- [#683](https://github.com/request/request/pull/683) Travis CI support (@Turbo87) -- [#674](https://github.com/request/request/pull/674) change cookie module,to tough-cookie.please check it . (@sxyizhiren) -- [#666](https://github.com/request/request/pull/666) make `ciphers` and `secureProtocol` to work in https request (@richarddong) -- [#656](https://github.com/request/request/pull/656) Test case for #304. (@diversario) -- [#662](https://github.com/request/request/pull/662) option.tunnel to explicitly disable tunneling (@seanmonstar) -- [#659](https://github.com/request/request/pull/659) fix failure when running with NODE_DEBUG=request, and a test for that (@jrgm) -- [#630](https://github.com/request/request/pull/630) Send random cnonce for HTTP Digest requests (@wprl) - -### v2.27.0 (2013/08/15) -- [#619](https://github.com/request/request/pull/619) decouple things a bit (@joaojeronimo) - -### v2.26.0 (2013/08/07) -- [#613](https://github.com/request/request/pull/613) Fixes #583, moved initialization of self.uri.pathname (@lexander) -- [#605](https://github.com/request/request/pull/605) Only include ":" + pass in Basic Auth if it's defined (fixes #602) (@bendrucker) - -### v2.24.0 (2013/07/23) -- [#596](https://github.com/request/request/pull/596) Global agent is being used when pool is specified (@Cauldrath) -- [#594](https://github.com/request/request/pull/594) Emit complete event when there is no callback (@RomainLK) -- [#601](https://github.com/request/request/pull/601) Fixed a small typo (@michalstanko) - -### v2.23.0 (2013/07/23) -- [#589](https://github.com/request/request/pull/589) Prevent setting headers after they are sent (@geek) -- [#587](https://github.com/request/request/pull/587) Global cookie jar disabled by default (@threepointone) - -### v2.22.0 (2013/07/05) -- [#544](https://github.com/request/request/pull/544) Update http-signature version. (@davidlehn) -- [#581](https://github.com/request/request/pull/581) Fix spelling of "ignoring." (@bigeasy) -- [#568](https://github.com/request/request/pull/568) use agentOptions to create agent when specified in request (@SamPlacette) -- [#564](https://github.com/request/request/pull/564) Fix redirections (@criloz) -- [#541](https://github.com/request/request/pull/541) The exported request function doesn't have an auth method (@tschaub) -- [#542](https://github.com/request/request/pull/542) Expose Request class (@regality) - -### v2.21.0 (2013/04/30) -- [#536](https://github.com/request/request/pull/536) Allow explicitly empty user field for basic authentication. (@mikeando) -- [#532](https://github.com/request/request/pull/532) fix typo (@fredericosilva) -- [#497](https://github.com/request/request/pull/497) Added redirect event (@Cauldrath) -- [#503](https://github.com/request/request/pull/503) Fix basic auth for passwords that contain colons (@tonistiigi) -- [#521](https://github.com/request/request/pull/521) Improving test-localAddress.js (@noway421) -- [#529](https://github.com/request/request/pull/529) dependencies versions bump (@jodaka) - -### v2.20.0 (2013/04/22) -- [#523](https://github.com/request/request/pull/523) Updating dependencies (@noway421) -- [#520](https://github.com/request/request/pull/520) Fixing test-tunnel.js (@noway421) -- [#519](https://github.com/request/request/pull/519) Update internal path state on post-creation QS changes (@jblebrun) -- [#510](https://github.com/request/request/pull/510) Add HTTP Signature support. (@davidlehn) -- [#502](https://github.com/request/request/pull/502) Fix POST (and probably other) requests that are retried after 401 Unauthorized (@nylen) -- [#508](https://github.com/request/request/pull/508) Honor the .strictSSL option when using proxies (tunnel-agent) (@jhs) -- [#512](https://github.com/request/request/pull/512) Make password optional to support the format: http://username@hostname/ (@pajato1) -- [#513](https://github.com/request/request/pull/513) add 'localAddress' support (@yyfrankyy) -- [#498](https://github.com/request/request/pull/498) Moving response emit above setHeaders on destination streams (@kenperkins) -- [#490](https://github.com/request/request/pull/490) Empty response body (3-rd argument) must be passed to callback as an empty string (@Olegas) -- [#479](https://github.com/request/request/pull/479) Changing so if Accept header is explicitly set, sending json does not ov... (@RoryH) -- [#475](https://github.com/request/request/pull/475) Use `unescape` from `querystring` (@shimaore) -- [#473](https://github.com/request/request/pull/473) V0.10 compat (@isaacs) -- [#471](https://github.com/request/request/pull/471) Using querystring library from visionmedia (@kbackowski) -- [#461](https://github.com/request/request/pull/461) Strip the UTF8 BOM from a UTF encoded response (@kppullin) -- [#460](https://github.com/request/request/pull/460) hawk 0.10.0 (@hueniverse) -- [#462](https://github.com/request/request/pull/462) if query params are empty, then request path shouldn't end with a '?' (merges cleanly now) (@jaipandya) -- [#456](https://github.com/request/request/pull/456) hawk 0.9.0 (@hueniverse) -- [#429](https://github.com/request/request/pull/429) Copy options before adding callback. (@nrn, @nfriedly, @youurayy, @jplock, @kapetan, @landeiro, @othiym23, @mmalecki) -- [#454](https://github.com/request/request/pull/454) Destroy the response if present when destroying the request (clean merge) (@mafintosh) -- [#310](https://github.com/request/request/pull/310) Twitter Oauth Stuff Out of Date; Now Updated (@joemccann, @isaacs, @mscdex) -- [#413](https://github.com/request/request/pull/413) rename googledoodle.png to .jpg (@nfriedly, @youurayy, @jplock, @kapetan, @landeiro, @othiym23, @mmalecki) -- [#448](https://github.com/request/request/pull/448) Convenience method for PATCH (@mloar) -- [#444](https://github.com/request/request/pull/444) protect against double callbacks on error path (@spollack) -- [#433](https://github.com/request/request/pull/433) Added support for HTTPS cert & key (@mmalecki) -- [#430](https://github.com/request/request/pull/430) Respect specified {Host,host} headers, not just {host} (@andrewschaaf) -- [#415](https://github.com/request/request/pull/415) Fixed a typo. (@jerem) -- [#338](https://github.com/request/request/pull/338) Add more auth options, including digest support (@nylen) -- [#403](https://github.com/request/request/pull/403) Optimize environment lookup to happen once only (@mmalecki) -- [#398](https://github.com/request/request/pull/398) Add more reporting to tests (@mmalecki) -- [#388](https://github.com/request/request/pull/388) Ensure "safe" toJSON doesn't break EventEmitters (@othiym23) -- [#381](https://github.com/request/request/pull/381) Resolving "Invalid signature. Expected signature base string: " (@landeiro) -- [#380](https://github.com/request/request/pull/380) Fixes missing host header on retried request when using forever agent (@mac-) -- [#376](https://github.com/request/request/pull/376) Headers lost on redirect (@kapetan) -- [#375](https://github.com/request/request/pull/375) Fix for missing oauth_timestamp parameter (@jplock) -- [#374](https://github.com/request/request/pull/374) Correct Host header for proxy tunnel CONNECT (@youurayy) -- [#370](https://github.com/request/request/pull/370) Twitter reverse auth uses x_auth_mode not x_auth_type (@drudge) -- [#369](https://github.com/request/request/pull/369) Don't remove x_auth_mode for Twitter reverse auth (@drudge) -- [#344](https://github.com/request/request/pull/344) Make AWS auth signing find headers correctly (@nlf) -- [#363](https://github.com/request/request/pull/363) rfc3986 on base_uri, now passes tests (@jeffmarshall) -- [#362](https://github.com/request/request/pull/362) Running `rfc3986` on `base_uri` in `oauth.hmacsign` instead of just `encodeURIComponent` (@jeffmarshall) -- [#361](https://github.com/request/request/pull/361) Don't create a Content-Length header if we already have it set (@danjenkins) -- [#360](https://github.com/request/request/pull/360) Delete self._form along with everything else on redirect (@jgautier) -- [#355](https://github.com/request/request/pull/355) stop sending erroneous headers on redirected requests (@azylman) -- [#332](https://github.com/request/request/pull/332) Fix #296 - Only set Content-Type if body exists (@Marsup) -- [#343](https://github.com/request/request/pull/343) Allow AWS to work in more situations, added a note in the README on its usage (@nlf) -- [#320](https://github.com/request/request/pull/320) request.defaults() doesn't need to wrap jar() (@StuartHarris) -- [#322](https://github.com/request/request/pull/322) Fix + test for piped into request bumped into redirect. #321 (@alexindigo) -- [#326](https://github.com/request/request/pull/326) Do not try to remove listener from an undefined connection (@strk) -- [#318](https://github.com/request/request/pull/318) Pass servername to tunneling secure socket creation (@isaacs) -- [#317](https://github.com/request/request/pull/317) Workaround for #313 (@isaacs) -- [#293](https://github.com/request/request/pull/293) Allow parser errors to bubble up to request (@mscdex) -- [#290](https://github.com/request/request/pull/290) A test for #289 (@isaacs) -- [#280](https://github.com/request/request/pull/280) Like in node.js print options if NODE_DEBUG contains the word request (@Filirom1) -- [#207](https://github.com/request/request/pull/207) Fix #206 Change HTTP/HTTPS agent when redirecting between protocols (@isaacs) -- [#214](https://github.com/request/request/pull/214) documenting additional behavior of json option (@jphaas) -- [#272](https://github.com/request/request/pull/272) Boundary begins with CRLF? (@elspoono, @timshadel, @naholyr, @nanodocumet, @TehShrike) -- [#284](https://github.com/request/request/pull/284) Remove stray `console.log()` call in multipart generator. (@bcherry) -- [#241](https://github.com/request/request/pull/241) Composability updates suggested by issue #239 (@polotek) -- [#282](https://github.com/request/request/pull/282) OAuth Authorization header contains non-"oauth_" parameters (@jplock) -- [#279](https://github.com/request/request/pull/279) fix tests with boundary by injecting boundry from header (@benatkin) -- [#273](https://github.com/request/request/pull/273) Pipe back pressure issue (@mafintosh) -- [#268](https://github.com/request/request/pull/268) I'm not OCD seriously (@TehShrike) -- [#263](https://github.com/request/request/pull/263) Bug in OAuth key generation for sha1 (@nanodocumet) -- [#265](https://github.com/request/request/pull/265) uncaughtException when redirected to invalid URI (@naholyr) -- [#262](https://github.com/request/request/pull/262) JSON test should check for equality (@timshadel) -- [#261](https://github.com/request/request/pull/261) Setting 'pool' to 'false' does NOT disable Agent pooling (@timshadel) -- [#249](https://github.com/request/request/pull/249) Fix for the fix of your (closed) issue #89 where self.headers[content-length] is set to 0 for all methods (@sethbridges, @polotek, @zephrax, @jeromegn) -- [#255](https://github.com/request/request/pull/255) multipart allow body === '' ( the empty string ) (@Filirom1) -- [#260](https://github.com/request/request/pull/260) fixed just another leak of 'i' (@sreuter) -- [#246](https://github.com/request/request/pull/246) Fixing the set-cookie header (@jeromegn) -- [#243](https://github.com/request/request/pull/243) Dynamic boundary (@zephrax) -- [#240](https://github.com/request/request/pull/240) don't error when null is passed for options (@polotek) -- [#211](https://github.com/request/request/pull/211) Replace all occurrences of special chars in RFC3986 (@chriso) -- [#224](https://github.com/request/request/pull/224) Multipart content-type change (@janjongboom) -- [#217](https://github.com/request/request/pull/217) need to use Authorization (titlecase) header with Tumblr OAuth (@visnup) -- [#203](https://github.com/request/request/pull/203) Fix cookie and redirect bugs and add auth support for HTTPS tunnel (@milewise) -- [#199](https://github.com/request/request/pull/199) Tunnel (@isaacs) -- [#198](https://github.com/request/request/pull/198) Bugfix on forever usage of util.inherits (@isaacs) -- [#197](https://github.com/request/request/pull/197) Make ForeverAgent work with HTTPS (@isaacs) -- [#193](https://github.com/request/request/pull/193) Fixes GH-119 (@goatslacker) -- [#188](https://github.com/request/request/pull/188) Add abort support to the returned request (@itay) -- [#176](https://github.com/request/request/pull/176) Querystring option (@csainty) -- [#182](https://github.com/request/request/pull/182) Fix request.defaults to support (uri, options, callback) api (@twilson63) -- [#180](https://github.com/request/request/pull/180) Modified the post, put, head and del shortcuts to support uri optional param (@twilson63) -- [#179](https://github.com/request/request/pull/179) fix to add opts in .pipe(stream, opts) (@substack) -- [#177](https://github.com/request/request/pull/177) Issue #173 Support uri as first and optional config as second argument (@twilson63) -- [#170](https://github.com/request/request/pull/170) can't create a cookie in a wrapped request (defaults) (@fabianonunes) -- [#168](https://github.com/request/request/pull/168) Picking off an EasyFix by adding some missing mimetypes. (@serby) -- [#161](https://github.com/request/request/pull/161) Fix cookie jar/headers.cookie collision (#125) (@papandreou) -- [#162](https://github.com/request/request/pull/162) Fix issue #159 (@dpetukhov) -- [#90](https://github.com/request/request/pull/90) add option followAllRedirects to follow post/put redirects (@jroes) -- [#148](https://github.com/request/request/pull/148) Retry Agent (@thejh) -- [#146](https://github.com/request/request/pull/146) Multipart should respect content-type if previously set (@apeace) -- [#144](https://github.com/request/request/pull/144) added "form" option to readme (@petejkim) -- [#133](https://github.com/request/request/pull/133) Fixed cookies parsing (@afanasy) -- [#135](https://github.com/request/request/pull/135) host vs hostname (@iangreenleaf) -- [#132](https://github.com/request/request/pull/132) return the body as a Buffer when encoding is set to null (@jahewson) -- [#112](https://github.com/request/request/pull/112) Support using a custom http-like module (@jhs) -- [#104](https://github.com/request/request/pull/104) Cookie handling contains bugs (@janjongboom) -- [#121](https://github.com/request/request/pull/121) Another patch for cookie handling regression (@jhurliman) -- [#117](https://github.com/request/request/pull/117) Remove the global `i` (@3rd-Eden) -- [#110](https://github.com/request/request/pull/110) Update to Iris Couch URL (@jhs) -- [#86](https://github.com/request/request/pull/86) Can't post binary to multipart requests (@kkaefer) -- [#105](https://github.com/request/request/pull/105) added test for proxy option. (@dominictarr) -- [#102](https://github.com/request/request/pull/102) Implemented cookies - closes issue 82: https://github.com/mikeal/request/issues/82 (@alessioalex) -- [#97](https://github.com/request/request/pull/97) Typo in previous pull causes TypeError in non-0.5.11 versions (@isaacs) -- [#96](https://github.com/request/request/pull/96) Authless parsed url host support (@isaacs) -- [#81](https://github.com/request/request/pull/81) Enhance redirect handling (@danmactough) -- [#78](https://github.com/request/request/pull/78) Don't try to do strictSSL for non-ssl connections (@isaacs) -- [#76](https://github.com/request/request/pull/76) Bug when a request fails and a timeout is set (@Marsup) -- [#70](https://github.com/request/request/pull/70) add test script to package.json (@isaacs, @aheckmann) -- [#73](https://github.com/request/request/pull/73) Fix #71 Respect the strictSSL flag (@isaacs) -- [#69](https://github.com/request/request/pull/69) Flatten chunked requests properly (@isaacs) -- [#67](https://github.com/request/request/pull/67) fixed global variable leaks (@aheckmann) -- [#66](https://github.com/request/request/pull/66) Do not overwrite established content-type headers for read stream deliver (@voodootikigod) -- [#53](https://github.com/request/request/pull/53) Parse json: Issue #51 (@benatkin) -- [#45](https://github.com/request/request/pull/45) Added timeout option (@mbrevoort) -- [#35](https://github.com/request/request/pull/35) The "end" event isn't emitted for some responses (@voxpelli) -- [#31](https://github.com/request/request/pull/31) Error on piping a request to a destination (@tobowers) \ No newline at end of file diff --git a/node_modules/request/CONTRIBUTING.md b/node_modules/request/CONTRIBUTING.md deleted file mode 100644 index 8aa6999a..00000000 --- a/node_modules/request/CONTRIBUTING.md +++ /dev/null @@ -1,81 +0,0 @@ - -# Contributing to Request - -:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: - -The following is a set of guidelines for contributing to Request and its packages, which are hosted in the [Request Organization](https://github.com/request) on GitHub. -These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. - - -## Submitting an Issue - -1. Provide a small self **sufficient** code example to **reproduce** the issue. -2. Run your test code using [request-debug](https://github.com/request/request-debug) and copy/paste the results inside the issue. -3. You should **always** use fenced code blocks when submitting code examples or any other formatted output: -
-  ```js
-  put your javascript code here
-  ```
-
-  ```
-  put any other formatted output here,
-  like for example the one returned from using request-debug
-  ```
-  
- -If the problem cannot be reliably reproduced, the issue will be marked as `Not enough info (see CONTRIBUTING.md)`. - -If the problem is not related to request the issue will be marked as `Help (please use Stackoverflow)`. - - -## Submitting a Pull Request - -1. In almost all of the cases your PR **needs tests**. Make sure you have any. -2. Run `npm test` locally. Fix any errors before pushing to GitHub. -3. After submitting the PR a build will be triggered on TravisCI. Wait for it to ends and make sure all jobs are passing. - - ------------------------------------------ - - -## Becoming a Contributor - -Individuals making significant and valuable contributions are given -commit-access to the project to contribute as they see fit. This project is -more like an open wiki than a standard guarded open source project. - - -## Rules - -There are a few basic ground-rules for contributors: - -1. **No `--force` pushes** or modifying the Git history in any way. -1. **Non-master branches** ought to be used for ongoing work. -1. **Any** change should be added through Pull Request. -1. **External API changes and significant modifications** ought to be subject - to an **internal pull-request** to solicit feedback from other contributors. -1. Internal pull-requests to solicit feedback are *encouraged* for any other - non-trivial contribution but left to the discretion of the contributor. -1. For significant changes wait a full 24 hours before merging so that active - contributors who are distributed throughout the world have a chance to weigh - in. -1. Contributors should attempt to adhere to the prevailing code-style. -1. Run `npm test` locally before submitting your PR, to catch any easy to miss - style & testing issues. To diagnose test failures, there are two ways to - run a single test file: - - `node_modules/.bin/taper tests/test-file.js` - run using the default - [`taper`](https://github.com/nylen/taper) test reporter. - - `node tests/test-file.js` - view the raw - [tap](https://testanything.org/) output. - - -## Releases - -Declaring formal releases remains the prerogative of the project maintainer. - - -## Changes to this arrangement - -This is an experiment and feedback is welcome! This document may also be -subject to pull-requests or changes by contributors where you believe you have -something valuable to add or change. diff --git a/node_modules/request/LICENSE b/node_modules/request/LICENSE deleted file mode 100644 index a4a9aee0..00000000 --- a/node_modules/request/LICENSE +++ /dev/null @@ -1,55 +0,0 @@ -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and - -You must cause any modified files to carry prominent notices stating that You changed the files; and - -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/node_modules/request/README.md b/node_modules/request/README.md deleted file mode 100644 index cf9072a2..00000000 --- a/node_modules/request/README.md +++ /dev/null @@ -1,1098 +0,0 @@ - -# Request - Simplified HTTP client - -[![npm package](https://nodei.co/npm/request.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/request/) - -[![Build status](https://img.shields.io/travis/request/request/master.svg?style=flat-square)](https://travis-ci.org/request/request) -[![Coverage](https://img.shields.io/codecov/c/github/request/request.svg?style=flat-square)](https://codecov.io/github/request/request?branch=master) -[![Coverage](https://img.shields.io/coveralls/request/request.svg?style=flat-square)](https://coveralls.io/r/request/request) -[![Dependency Status](https://img.shields.io/david/request/request.svg?style=flat-square)](https://david-dm.org/request/request) -[![Known Vulnerabilities](https://snyk.io/test/npm/request/badge.svg?style=flat-square)](https://snyk.io/test/npm/request) -[![Gitter](https://img.shields.io/badge/gitter-join_chat-blue.svg?style=flat-square)](https://gitter.im/request/request?utm_source=badge) - - -## Super simple to use - -Request is designed to be the simplest way possible to make http calls. It supports HTTPS and follows redirects by default. - -```js -var request = require('request'); -request('http://www.google.com', function (error, response, body) { - if (!error && response.statusCode == 200) { - console.log(body) // Show the HTML for the Google homepage. - } -}) -``` - - -## Table of contents - -- [Streaming](#streaming) -- [Forms](#forms) -- [HTTP Authentication](#http-authentication) -- [Custom HTTP Headers](#custom-http-headers) -- [OAuth Signing](#oauth-signing) -- [Proxies](#proxies) -- [Unix Domain Sockets](#unix-domain-sockets) -- [TLS/SSL Protocol](#tlsssl-protocol) -- [Support for HAR 1.2](#support-for-har-12) -- [**All Available Options**](#requestoptions-callback) - -Request also offers [convenience methods](#convenience-methods) like -`request.defaults` and `request.post`, and there are -lots of [usage examples](#examples) and several -[debugging techniques](#debugging). - - ---- - - -## Streaming - -You can stream any response to a file stream. - -```js -request('http://google.com/doodle.png').pipe(fs.createWriteStream('doodle.png')) -``` - -You can also stream a file to a PUT or POST request. This method will also check the file extension against a mapping of file extensions to content-types (in this case `application/json`) and use the proper `content-type` in the PUT request (if the headers don’t already provide one). - -```js -fs.createReadStream('file.json').pipe(request.put('http://mysite.com/obj.json')) -``` - -Request can also `pipe` to itself. When doing so, `content-type` and `content-length` are preserved in the PUT headers. - -```js -request.get('http://google.com/img.png').pipe(request.put('http://mysite.com/img.png')) -``` - -Request emits a "response" event when a response is received. The `response` argument will be an instance of [http.IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage). - -```js -request - .get('http://google.com/img.png') - .on('response', function(response) { - console.log(response.statusCode) // 200 - console.log(response.headers['content-type']) // 'image/png' - }) - .pipe(request.put('http://mysite.com/img.png')) -``` - -To easily handle errors when streaming requests, listen to the `error` event before piping: - -```js -request - .get('http://mysite.com/doodle.png') - .on('error', function(err) { - console.log(err) - }) - .pipe(fs.createWriteStream('doodle.png')) -``` - -Now let’s get fancy. - -```js -http.createServer(function (req, resp) { - if (req.url === '/doodle.png') { - if (req.method === 'PUT') { - req.pipe(request.put('http://mysite.com/doodle.png')) - } else if (req.method === 'GET' || req.method === 'HEAD') { - request.get('http://mysite.com/doodle.png').pipe(resp) - } - } -}) -``` - -You can also `pipe()` from `http.ServerRequest` instances, as well as to `http.ServerResponse` instances. The HTTP method, headers, and entity-body data will be sent. Which means that, if you don't really care about security, you can do: - -```js -http.createServer(function (req, resp) { - if (req.url === '/doodle.png') { - var x = request('http://mysite.com/doodle.png') - req.pipe(x) - x.pipe(resp) - } -}) -``` - -And since `pipe()` returns the destination stream in ≥ Node 0.5.x you can do one line proxying. :) - -```js -req.pipe(request('http://mysite.com/doodle.png')).pipe(resp) -``` - -Also, none of this new functionality conflicts with requests previous features, it just expands them. - -```js -var r = request.defaults({'proxy':'http://localproxy.com'}) - -http.createServer(function (req, resp) { - if (req.url === '/doodle.png') { - r.get('http://google.com/doodle.png').pipe(resp) - } -}) -``` - -You can still use intermediate proxies, the requests will still follow HTTP forwards, etc. - -[back to top](#table-of-contents) - - ---- - - -## Forms - -`request` supports `application/x-www-form-urlencoded` and `multipart/form-data` form uploads. For `multipart/related` refer to the `multipart` API. - - -#### application/x-www-form-urlencoded (URL-Encoded Forms) - -URL-encoded forms are simple. - -```js -request.post('http://service.com/upload', {form:{key:'value'}}) -// or -request.post('http://service.com/upload').form({key:'value'}) -// or -request.post({url:'http://service.com/upload', form: {key:'value'}}, function(err,httpResponse,body){ /* ... */ }) -``` - - -#### multipart/form-data (Multipart Form Uploads) - -For `multipart/form-data` we use the [form-data](https://github.com/form-data/form-data) library by [@felixge](https://github.com/felixge). For the most cases, you can pass your upload form data via the `formData` option. - - -```js -var formData = { - // Pass a simple key-value pair - my_field: 'my_value', - // Pass data via Buffers - my_buffer: new Buffer([1, 2, 3]), - // Pass data via Streams - my_file: fs.createReadStream(__dirname + '/unicycle.jpg'), - // Pass multiple values /w an Array - attachments: [ - fs.createReadStream(__dirname + '/attachment1.jpg'), - fs.createReadStream(__dirname + '/attachment2.jpg') - ], - // Pass optional meta-data with an 'options' object with style: {value: DATA, options: OPTIONS} - // Use case: for some types of streams, you'll need to provide "file"-related information manually. - // See the `form-data` README for more information about options: https://github.com/form-data/form-data - custom_file: { - value: fs.createReadStream('/dev/urandom'), - options: { - filename: 'topsecret.jpg', - contentType: 'image/jpg' - } - } -}; -request.post({url:'http://service.com/upload', formData: formData}, function optionalCallback(err, httpResponse, body) { - if (err) { - return console.error('upload failed:', err); - } - console.log('Upload successful! Server responded with:', body); -}); -``` - -For advanced cases, you can access the form-data object itself via `r.form()`. This can be modified until the request is fired on the next cycle of the event-loop. (Note that this calling `form()` will clear the currently set form data for that request.) - -```js -// NOTE: Advanced use-case, for normal use see 'formData' usage above -var r = request.post('http://service.com/upload', function optionalCallback(err, httpResponse, body) {...}) -var form = r.form(); -form.append('my_field', 'my_value'); -form.append('my_buffer', new Buffer([1, 2, 3])); -form.append('custom_file', fs.createReadStream(__dirname + '/unicycle.jpg'), {filename: 'unicycle.jpg'}); -``` -See the [form-data README](https://github.com/form-data/form-data) for more information & examples. - - -#### multipart/related - -Some variations in different HTTP implementations require a newline/CRLF before, after, or both before and after the boundary of a `multipart/related` request (using the multipart option). This has been observed in the .NET WebAPI version 4.0. You can turn on a boundary preambleCRLF or postamble by passing them as `true` to your request options. - -```js - request({ - method: 'PUT', - preambleCRLF: true, - postambleCRLF: true, - uri: 'http://service.com/upload', - multipart: [ - { - 'content-type': 'application/json', - body: JSON.stringify({foo: 'bar', _attachments: {'message.txt': {follows: true, length: 18, 'content_type': 'text/plain' }}}) - }, - { body: 'I am an attachment' }, - { body: fs.createReadStream('image.png') } - ], - // alternatively pass an object containing additional options - multipart: { - chunked: false, - data: [ - { - 'content-type': 'application/json', - body: JSON.stringify({foo: 'bar', _attachments: {'message.txt': {follows: true, length: 18, 'content_type': 'text/plain' }}}) - }, - { body: 'I am an attachment' } - ] - } - }, - function (error, response, body) { - if (error) { - return console.error('upload failed:', error); - } - console.log('Upload successful! Server responded with:', body); - }) -``` - -[back to top](#table-of-contents) - - ---- - - -## HTTP Authentication - -```js -request.get('http://some.server.com/').auth('username', 'password', false); -// or -request.get('http://some.server.com/', { - 'auth': { - 'user': 'username', - 'pass': 'password', - 'sendImmediately': false - } -}); -// or -request.get('http://some.server.com/').auth(null, null, true, 'bearerToken'); -// or -request.get('http://some.server.com/', { - 'auth': { - 'bearer': 'bearerToken' - } -}); -``` - -If passed as an option, `auth` should be a hash containing values: - -- `user` || `username` -- `pass` || `password` -- `sendImmediately` (optional) -- `bearer` (optional) - -The method form takes parameters -`auth(username, password, sendImmediately, bearer)`. - -`sendImmediately` defaults to `true`, which causes a basic or bearer -authentication header to be sent. If `sendImmediately` is `false`, then -`request` will retry with a proper authentication header after receiving a -`401` response from the server (which must contain a `WWW-Authenticate` header -indicating the required authentication method). - -Note that you can also specify basic authentication using the URL itself, as -detailed in [RFC 1738](http://www.ietf.org/rfc/rfc1738.txt). Simply pass the -`user:password` before the host with an `@` sign: - -```js -var username = 'username', - password = 'password', - url = 'http://' + username + ':' + password + '@some.server.com'; - -request({url: url}, function (error, response, body) { - // Do more stuff with 'body' here -}); -``` - -Digest authentication is supported, but it only works with `sendImmediately` -set to `false`; otherwise `request` will send basic authentication on the -initial request, which will probably cause the request to fail. - -Bearer authentication is supported, and is activated when the `bearer` value is -available. The value may be either a `String` or a `Function` returning a -`String`. Using a function to supply the bearer token is particularly useful if -used in conjunction with `defaults` to allow a single function to supply the -last known token at the time of sending a request, or to compute one on the fly. - -[back to top](#table-of-contents) - - ---- - - -## Custom HTTP Headers - -HTTP Headers, such as `User-Agent`, can be set in the `options` object. -In the example below, we call the github API to find out the number -of stars and forks for the request repository. This requires a -custom `User-Agent` header as well as https. - -```js -var request = require('request'); - -var options = { - url: 'https://api.github.com/repos/request/request', - headers: { - 'User-Agent': 'request' - } -}; - -function callback(error, response, body) { - if (!error && response.statusCode == 200) { - var info = JSON.parse(body); - console.log(info.stargazers_count + " Stars"); - console.log(info.forks_count + " Forks"); - } -} - -request(options, callback); -``` - -[back to top](#table-of-contents) - - ---- - - -## OAuth Signing - -[OAuth version 1.0](https://tools.ietf.org/html/rfc5849) is supported. The -default signing algorithm is -[HMAC-SHA1](https://tools.ietf.org/html/rfc5849#section-3.4.2): - -```js -// OAuth1.0 - 3-legged server side flow (Twitter example) -// step 1 -var qs = require('querystring') - , oauth = - { callback: 'http://mysite.com/callback/' - , consumer_key: CONSUMER_KEY - , consumer_secret: CONSUMER_SECRET - } - , url = 'https://api.twitter.com/oauth/request_token' - ; -request.post({url:url, oauth:oauth}, function (e, r, body) { - // Ideally, you would take the body in the response - // and construct a URL that a user clicks on (like a sign in button). - // The verifier is only available in the response after a user has - // verified with twitter that they are authorizing your app. - - // step 2 - var req_data = qs.parse(body) - var uri = 'https://api.twitter.com/oauth/authenticate' - + '?' + qs.stringify({oauth_token: req_data.oauth_token}) - // redirect the user to the authorize uri - - // step 3 - // after the user is redirected back to your server - var auth_data = qs.parse(body) - , oauth = - { consumer_key: CONSUMER_KEY - , consumer_secret: CONSUMER_SECRET - , token: auth_data.oauth_token - , token_secret: req_data.oauth_token_secret - , verifier: auth_data.oauth_verifier - } - , url = 'https://api.twitter.com/oauth/access_token' - ; - request.post({url:url, oauth:oauth}, function (e, r, body) { - // ready to make signed requests on behalf of the user - var perm_data = qs.parse(body) - , oauth = - { consumer_key: CONSUMER_KEY - , consumer_secret: CONSUMER_SECRET - , token: perm_data.oauth_token - , token_secret: perm_data.oauth_token_secret - } - , url = 'https://api.twitter.com/1.1/users/show.json' - , qs = - { screen_name: perm_data.screen_name - , user_id: perm_data.user_id - } - ; - request.get({url:url, oauth:oauth, qs:qs, json:true}, function (e, r, user) { - console.log(user) - }) - }) -}) -``` - -For [RSA-SHA1 signing](https://tools.ietf.org/html/rfc5849#section-3.4.3), make -the following changes to the OAuth options object: -* Pass `signature_method : 'RSA-SHA1'` -* Instead of `consumer_secret`, specify a `private_key` string in - [PEM format](http://how2ssl.com/articles/working_with_pem_files/) - -For [PLAINTEXT signing](http://oauth.net/core/1.0/#anchor22), make -the following changes to the OAuth options object: -* Pass `signature_method : 'PLAINTEXT'` - -To send OAuth parameters via query params or in a post body as described in The -[Consumer Request Parameters](http://oauth.net/core/1.0/#consumer_req_param) -section of the oauth1 spec: -* Pass `transport_method : 'query'` or `transport_method : 'body'` in the OAuth - options object. -* `transport_method` defaults to `'header'` - -To use [Request Body Hash](https://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html) you can either -* Manually generate the body hash and pass it as a string `body_hash: '...'` -* Automatically generate the body hash by passing `body_hash: true` - -[back to top](#table-of-contents) - - ---- - - -## Proxies - -If you specify a `proxy` option, then the request (and any subsequent -redirects) will be sent via a connection to the proxy server. - -If your endpoint is an `https` url, and you are using a proxy, then -request will send a `CONNECT` request to the proxy server *first*, and -then use the supplied connection to connect to the endpoint. - -That is, first it will make a request like: - -``` -HTTP/1.1 CONNECT endpoint-server.com:80 -Host: proxy-server.com -User-Agent: whatever user agent you specify -``` - -and then the proxy server make a TCP connection to `endpoint-server` -on port `80`, and return a response that looks like: - -``` -HTTP/1.1 200 OK -``` - -At this point, the connection is left open, and the client is -communicating directly with the `endpoint-server.com` machine. - -See [the wikipedia page on HTTP Tunneling](https://en.wikipedia.org/wiki/HTTP_tunnel) -for more information. - -By default, when proxying `http` traffic, request will simply make a -standard proxied `http` request. This is done by making the `url` -section of the initial line of the request a fully qualified url to -the endpoint. - -For example, it will make a single request that looks like: - -``` -HTTP/1.1 GET http://endpoint-server.com/some-url -Host: proxy-server.com -Other-Headers: all go here - -request body or whatever -``` - -Because a pure "http over http" tunnel offers no additional security -or other features, it is generally simpler to go with a -straightforward HTTP proxy in this case. However, if you would like -to force a tunneling proxy, you may set the `tunnel` option to `true`. - -You can also make a standard proxied `http` request by explicitly setting -`tunnel : false`, but **note that this will allow the proxy to see the traffic -to/from the destination server**. - -If you are using a tunneling proxy, you may set the -`proxyHeaderWhiteList` to share certain headers with the proxy. - -You can also set the `proxyHeaderExclusiveList` to share certain -headers only with the proxy and not with destination host. - -By default, this set is: - -``` -accept -accept-charset -accept-encoding -accept-language -accept-ranges -cache-control -content-encoding -content-language -content-length -content-location -content-md5 -content-range -content-type -connection -date -expect -max-forwards -pragma -proxy-authorization -referer -te -transfer-encoding -user-agent -via -``` - -Note that, when using a tunneling proxy, the `proxy-authorization` -header and any headers from custom `proxyHeaderExclusiveList` are -*never* sent to the endpoint server, but only to the proxy server. - - -### Controlling proxy behaviour using environment variables - -The following environment variables are respected by `request`: - - * `HTTP_PROXY` / `http_proxy` - * `HTTPS_PROXY` / `https_proxy` - * `NO_PROXY` / `no_proxy` - -When `HTTP_PROXY` / `http_proxy` are set, they will be used to proxy non-SSL requests that do not have an explicit `proxy` configuration option present. Similarly, `HTTPS_PROXY` / `https_proxy` will be respected for SSL requests that do not have an explicit `proxy` configuration option. It is valid to define a proxy in one of the environment variables, but then override it for a specific request, using the `proxy` configuration option. Furthermore, the `proxy` configuration option can be explicitly set to false / null to opt out of proxying altogether for that request. - -`request` is also aware of the `NO_PROXY`/`no_proxy` environment variables. These variables provide a granular way to opt out of proxying, on a per-host basis. It should contain a comma separated list of hosts to opt out of proxying. It is also possible to opt of proxying when a particular destination port is used. Finally, the variable may be set to `*` to opt out of the implicit proxy configuration of the other environment variables. - -Here's some examples of valid `no_proxy` values: - - * `google.com` - don't proxy HTTP/HTTPS requests to Google. - * `google.com:443` - don't proxy HTTPS requests to Google, but *do* proxy HTTP requests to Google. - * `google.com:443, yahoo.com:80` - don't proxy HTTPS requests to Google, and don't proxy HTTP requests to Yahoo! - * `*` - ignore `https_proxy`/`http_proxy` environment variables altogether. - -[back to top](#table-of-contents) - - ---- - - -## UNIX Domain Sockets - -`request` supports making requests to [UNIX Domain Sockets](https://en.wikipedia.org/wiki/Unix_domain_socket). To make one, use the following URL scheme: - -```js -/* Pattern */ 'http://unix:SOCKET:PATH' -/* Example */ request.get('http://unix:/absolute/path/to/unix.socket:/request/path') -``` - -Note: The `SOCKET` path is assumed to be absolute to the root of the host file system. - -[back to top](#table-of-contents) - - ---- - - -## TLS/SSL Protocol - -TLS/SSL Protocol options, such as `cert`, `key` and `passphrase`, can be -set directly in `options` object, in the `agentOptions` property of the `options` object, or even in `https.globalAgent.options`. Keep in mind that, although `agentOptions` allows for a slightly wider range of configurations, the recommended way is via `options` object directly, as using `agentOptions` or `https.globalAgent.options` would not be applied in the same way in proxied environments (as data travels through a TLS connection instead of an http/https agent). - -```js -var fs = require('fs') - , path = require('path') - , certFile = path.resolve(__dirname, 'ssl/client.crt') - , keyFile = path.resolve(__dirname, 'ssl/client.key') - , caFile = path.resolve(__dirname, 'ssl/ca.cert.pem') - , request = require('request'); - -var options = { - url: 'https://api.some-server.com/', - cert: fs.readFileSync(certFile), - key: fs.readFileSync(keyFile), - passphrase: 'password', - ca: fs.readFileSync(caFile) - } -}; - -request.get(options); -``` - -### Using `options.agentOptions` - -In the example below, we call an API requires client side SSL certificate -(in PEM format) with passphrase protected private key (in PEM format) and disable the SSLv3 protocol: - -```js -var fs = require('fs') - , path = require('path') - , certFile = path.resolve(__dirname, 'ssl/client.crt') - , keyFile = path.resolve(__dirname, 'ssl/client.key') - , request = require('request'); - -var options = { - url: 'https://api.some-server.com/', - agentOptions: { - cert: fs.readFileSync(certFile), - key: fs.readFileSync(keyFile), - // Or use `pfx` property replacing `cert` and `key` when using private key, certificate and CA certs in PFX or PKCS12 format: - // pfx: fs.readFileSync(pfxFilePath), - passphrase: 'password', - securityOptions: 'SSL_OP_NO_SSLv3' - } -}; - -request.get(options); -``` - -It is able to force using SSLv3 only by specifying `secureProtocol`: - -```js -request.get({ - url: 'https://api.some-server.com/', - agentOptions: { - secureProtocol: 'SSLv3_method' - } -}); -``` - -It is possible to accept other certificates than those signed by generally allowed Certificate Authorities (CAs). -This can be useful, for example, when using self-signed certificates. -To require a different root certificate, you can specify the signing CA by adding the contents of the CA's certificate file to the `agentOptions`. -The certificate the domain presents must be signed by the root certificate specified: - -```js -request.get({ - url: 'https://api.some-server.com/', - agentOptions: { - ca: fs.readFileSync('ca.cert.pem') - } -}); -``` - -[back to top](#table-of-contents) - - ---- - -## Support for HAR 1.2 - -The `options.har` property will override the values: `url`, `method`, `qs`, `headers`, `form`, `formData`, `body`, `json`, as well as construct multipart data and read files from disk when `request.postData.params[].fileName` is present without a matching `value`. - -a validation step will check if the HAR Request format matches the latest spec (v1.2) and will skip parsing if not matching. - -```js - var request = require('request') - request({ - // will be ignored - method: 'GET', - uri: 'http://www.google.com', - - // HTTP Archive Request Object - har: { - url: 'http://www.mockbin.com/har', - method: 'POST', - headers: [ - { - name: 'content-type', - value: 'application/x-www-form-urlencoded' - } - ], - postData: { - mimeType: 'application/x-www-form-urlencoded', - params: [ - { - name: 'foo', - value: 'bar' - }, - { - name: 'hello', - value: 'world' - } - ] - } - } - }) - - // a POST request will be sent to http://www.mockbin.com - // with body an application/x-www-form-urlencoded body: - // foo=bar&hello=world -``` - -[back to top](#table-of-contents) - - ---- - -## request(options, callback) - -The first argument can be either a `url` or an `options` object. The only required option is `uri`; all others are optional. - -- `uri` || `url` - fully qualified uri or a parsed url object from `url.parse()` -- `baseUrl` - fully qualified uri string used as the base url. Most useful with `request.defaults`, for example when you want to do many requests to the same domain. If `baseUrl` is `https://example.com/api/`, then requesting `/end/point?test=true` will fetch `https://example.com/api/end/point?test=true`. When `baseUrl` is given, `uri` must also be a string. -- `method` - http method (default: `"GET"`) -- `headers` - http headers (default: `{}`) - ---- - -- `qs` - object containing querystring values to be appended to the `uri` -- `qsParseOptions` - object containing options to pass to the [qs.parse](https://github.com/hapijs/qs#parsing-objects) method. Alternatively pass options to the [querystring.parse](https://nodejs.org/docs/v0.12.0/api/querystring.html#querystring_querystring_parse_str_sep_eq_options) method using this format `{sep:';', eq:':', options:{}}` -- `qsStringifyOptions` - object containing options to pass to the [qs.stringify](https://github.com/hapijs/qs#stringifying) method. Alternatively pass options to the [querystring.stringify](https://nodejs.org/docs/v0.12.0/api/querystring.html#querystring_querystring_stringify_obj_sep_eq_options) method using this format `{sep:';', eq:':', options:{}}`. For example, to change the way arrays are converted to query strings using the `qs` module pass the `arrayFormat` option with one of `indices|brackets|repeat` -- `useQuerystring` - If true, use `querystring` to stringify and parse - querystrings, otherwise use `qs` (default: `false`). Set this option to - `true` if you need arrays to be serialized as `foo=bar&foo=baz` instead of the - default `foo[0]=bar&foo[1]=baz`. - ---- - -- `body` - entity body for PATCH, POST and PUT requests. Must be a `Buffer`, `String` or `ReadStream`. If `json` is `true`, then `body` must be a JSON-serializable object. -- `form` - when passed an object or a querystring, this sets `body` to a querystring representation of value, and adds `Content-type: application/x-www-form-urlencoded` header. When passed no options, a `FormData` instance is returned (and is piped to request). See "Forms" section above. -- `formData` - Data to pass for a `multipart/form-data` request. See - [Forms](#forms) section above. -- `multipart` - array of objects which contain their own headers and `body` - attributes. Sends a `multipart/related` request. See [Forms](#forms) section - above. - - Alternatively you can pass in an object `{chunked: false, data: []}` where - `chunked` is used to specify whether the request is sent in - [chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) - In non-chunked requests, data items with body streams are not allowed. -- `preambleCRLF` - append a newline/CRLF before the boundary of your `multipart/form-data` request. -- `postambleCRLF` - append a newline/CRLF at the end of the boundary of your `multipart/form-data` request. -- `json` - sets `body` to JSON representation of value and adds `Content-type: application/json` header. Additionally, parses the response body as JSON. -- `jsonReviver` - a [reviver function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) that will be passed to `JSON.parse()` when parsing a JSON response body. -- `jsonReplacer` - a [replacer function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) that will be passed to `JSON.stringify()` when stringifying a JSON request body. - ---- - -- `auth` - A hash containing values `user` || `username`, `pass` || `password`, and `sendImmediately` (optional). See documentation above. -- `oauth` - Options for OAuth HMAC-SHA1 signing. See documentation above. -- `hawk` - Options for [Hawk signing](https://github.com/hueniverse/hawk). The `credentials` key must contain the necessary signing info, [see hawk docs for details](https://github.com/hueniverse/hawk#usage-example). -- `aws` - `object` containing AWS signing information. Should have the properties `key`, `secret`. Also requires the property `bucket`, unless you’re specifying your `bucket` as part of the path, or the request doesn’t use a bucket (i.e. GET Services). If you want to use AWS sign version 4 use the parameter `sign_version` with value `4` otherwise the default is version 2. **Note:** you need to `npm install aws4` first. -- `httpSignature` - Options for the [HTTP Signature Scheme](https://github.com/joyent/node-http-signature/blob/master/http_signing.md) using [Joyent's library](https://github.com/joyent/node-http-signature). The `keyId` and `key` properties must be specified. See the docs for other options. - ---- - -- `followRedirect` - follow HTTP 3xx responses as redirects (default: `true`). This property can also be implemented as function which gets `response` object as a single argument and should return `true` if redirects should continue or `false` otherwise. -- `followAllRedirects` - follow non-GET HTTP 3xx responses as redirects (default: `false`) -- `maxRedirects` - the maximum number of redirects to follow (default: `10`) -- `removeRefererHeader` - removes the referer header when a redirect happens (default: `false`). **Note:** if true, referer header set in the initial request is preserved during redirect chain. - ---- - -- `encoding` - Encoding to be used on `setEncoding` of response data. If `null`, the `body` is returned as a `Buffer`. Anything else **(including the default value of `undefined`)** will be passed as the [encoding](http://nodejs.org/api/buffer.html#buffer_buffer) parameter to `toString()` (meaning this is effectively `utf8` by default). (**Note:** if you expect binary data, you should set `encoding: null`.) -- `gzip` - If `true`, add an `Accept-Encoding` header to request compressed content encodings from the server (if not already present) and decode supported content encodings in the response. **Note:** Automatic decoding of the response content is performed on the body data returned through `request` (both through the `request` stream and passed to the callback function) but is not performed on the `response` stream (available from the `response` event) which is the unmodified `http.IncomingMessage` object which may contain compressed data. See example below. -- `jar` - If `true`, remember cookies for future use (or define your custom cookie jar; see examples section) - ---- - -- `agent` - `http(s).Agent` instance to use -- `agentClass` - alternatively specify your agent's class name -- `agentOptions` - and pass its options. **Note:** for HTTPS see [tls API doc for TLS/SSL options](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback) and the [documentation above](#using-optionsagentoptions). -- `forever` - set to `true` to use the [forever-agent](https://github.com/request/forever-agent) **Note:** Defaults to `http(s).Agent({keepAlive:true})` in node 0.12+ -- `pool` - An object describing which agents to use for the request. If this option is omitted the request will use the global agent (as long as your options allow for it). Otherwise, request will search the pool for your custom agent. If no custom agent is found, a new agent will be created and added to the pool. **Note:** `pool` is used only when the `agent` option is not specified. - - A `maxSockets` property can also be provided on the `pool` object to set the max number of sockets for all agents created (ex: `pool: {maxSockets: Infinity}`). - - Note that if you are sending multiple requests in a loop and creating - multiple new `pool` objects, `maxSockets` will not work as intended. To - work around this, either use [`request.defaults`](#requestdefaultsoptions) - with your pool options or create the pool object with the `maxSockets` - property outside of the loop. -- `timeout` - Integer containing the number of milliseconds to wait for a -server to send response headers (and start the response body) before aborting -the request. Note that if the underlying TCP connection cannot be established, -the OS-wide TCP connection timeout will overrule the `timeout` option ([the -default in Linux can be anywhere from 20-120 seconds][linux-timeout]). - -[linux-timeout]: http://www.sekuda.com/overriding_the_default_linux_kernel_20_second_tcp_socket_connect_timeout - ---- - -- `localAddress` - Local interface to bind for network connections. -- `proxy` - An HTTP proxy to be used. Supports proxy Auth with Basic Auth, identical to support for the `url` parameter (by embedding the auth info in the `uri`) -- `strictSSL` - If `true`, requires SSL certificates be valid. **Note:** to use your own certificate authority, you need to specify an agent that was created with that CA as an option. -- `tunnel` - controls the behavior of - [HTTP `CONNECT` tunneling](https://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_tunneling) - as follows: - - `undefined` (default) - `true` if the destination is `https`, `false` otherwise - - `true` - always tunnel to the destination by making a `CONNECT` request to - the proxy - - `false` - request the destination as a `GET` request. -- `proxyHeaderWhiteList` - A whitelist of headers to send to a - tunneling proxy. -- `proxyHeaderExclusiveList` - A whitelist of headers to send - exclusively to a tunneling proxy and not to destination. - ---- - -- `time` - If `true`, the request-response cycle (including all redirects) is timed at millisecond resolution, and the result provided on the response's `elapsedTime` property. -- `har` - A [HAR 1.2 Request Object](http://www.softwareishard.com/blog/har-12-spec/#request), will be processed from HAR format into options overwriting matching values *(see the [HAR 1.2 section](#support-for-har-1.2) for details)* -- `callback` - alternatively pass the request's callback in the options object - -The callback argument gets 3 arguments: - -1. An `error` when applicable (usually from [`http.ClientRequest`](http://nodejs.org/api/http.html#http_class_http_clientrequest) object) -2. An [`http.IncomingMessage`](http://nodejs.org/api/http.html#http_http_incomingmessage) object -3. The third is the `response` body (`String` or `Buffer`, or JSON object if the `json` option is supplied) - -[back to top](#table-of-contents) - - ---- - -## Convenience methods - -There are also shorthand methods for different HTTP METHODs and some other conveniences. - - -### request.defaults(options) - -This method **returns a wrapper** around the normal request API that defaults -to whatever options you pass to it. - -**Note:** `request.defaults()` **does not** modify the global request API; -instead, it **returns a wrapper** that has your default settings applied to it. - -**Note:** You can call `.defaults()` on the wrapper that is returned from -`request.defaults` to add/override defaults that were previously defaulted. - -For example: -```js -//requests using baseRequest() will set the 'x-token' header -var baseRequest = request.defaults({ - headers: {'x-token': 'my-token'} -}) - -//requests using specialRequest() will include the 'x-token' header set in -//baseRequest and will also include the 'special' header -var specialRequest = baseRequest.defaults({ - headers: {special: 'special value'} -}) -``` - -### request.put - -Same as `request()`, but defaults to `method: "PUT"`. - -```js -request.put(url) -``` - -### request.patch - -Same as `request()`, but defaults to `method: "PATCH"`. - -```js -request.patch(url) -``` - -### request.post - -Same as `request()`, but defaults to `method: "POST"`. - -```js -request.post(url) -``` - -### request.head - -Same as `request()`, but defaults to `method: "HEAD"`. - -```js -request.head(url) -``` - -### request.del / request.delete - -Same as `request()`, but defaults to `method: "DELETE"`. - -```js -request.del(url) -request.delete(url) -``` - -### request.get - -Same as `request()` (for uniformity). - -```js -request.get(url) -``` -### request.cookie - -Function that creates a new cookie. - -```js -request.cookie('key1=value1') -``` -### request.jar() - -Function that creates a new cookie jar. - -```js -request.jar() -``` - -[back to top](#table-of-contents) - - ---- - - -## Debugging - -There are at least three ways to debug the operation of `request`: - -1. Launch the node process like `NODE_DEBUG=request node script.js` - (`lib,request,otherlib` works too). - -2. Set `require('request').debug = true` at any time (this does the same thing - as #1). - -3. Use the [request-debug module](https://github.com/request/request-debug) to - view request and response headers and bodies. - -[back to top](#table-of-contents) - - ---- - -## Timeouts - -Most requests to external servers should have a timeout attached, in case the -server is not responding in a timely manner. Without a timeout, your code may -have a socket open/consume resources for minutes or more. - -There are two main types of timeouts: **connection timeouts** and **read -timeouts**. A connect timeout occurs if the timeout is hit while your client is -attempting to establish a connection to a remote machine (corresponding to the -[connect() call][connect] on the socket). A read timeout occurs any time the -server is too slow to send back a part of the response. - -These two situations have widely different implications for what went wrong -with the request, so it's useful to be able to distinguish them. You can detect -timeout errors by checking `err.code` for an 'ETIMEDOUT' value. Further, you -can detect whether the timeout was a connection timeout by checking if the -`err.connect` property is set to `true`. - -```js -request.get('http://10.255.255.1', {timeout: 1500}, function(err) { - console.log(err.code === 'ETIMEDOUT'); - // Set to `true` if the timeout was a connection timeout, `false` or - // `undefined` otherwise. - console.log(err.connect === true); - process.exit(0); -}); -``` - -[connect]: http://linux.die.net/man/2/connect - -## Examples: - -```js - var request = require('request') - , rand = Math.floor(Math.random()*100000000).toString() - ; - request( - { method: 'PUT' - , uri: 'http://mikeal.iriscouch.com/testjs/' + rand - , multipart: - [ { 'content-type': 'application/json' - , body: JSON.stringify({foo: 'bar', _attachments: {'message.txt': {follows: true, length: 18, 'content_type': 'text/plain' }}}) - } - , { body: 'I am an attachment' } - ] - } - , function (error, response, body) { - if(response.statusCode == 201){ - console.log('document saved as: http://mikeal.iriscouch.com/testjs/'+ rand) - } else { - console.log('error: '+ response.statusCode) - console.log(body) - } - } - ) -``` - -For backwards-compatibility, response compression is not supported by default. -To accept gzip-compressed responses, set the `gzip` option to `true`. Note -that the body data passed through `request` is automatically decompressed -while the response object is unmodified and will contain compressed data if -the server sent a compressed response. - -```js - var request = require('request') - request( - { method: 'GET' - , uri: 'http://www.google.com' - , gzip: true - } - , function (error, response, body) { - // body is the decompressed response body - console.log('server encoded the data as: ' + (response.headers['content-encoding'] || 'identity')) - console.log('the decoded data is: ' + body) - } - ).on('data', function(data) { - // decompressed data as it is received - console.log('decoded chunk: ' + data) - }) - .on('response', function(response) { - // unmodified http.IncomingMessage object - response.on('data', function(data) { - // compressed data as it is received - console.log('received ' + data.length + ' bytes of compressed data') - }) - }) -``` - -Cookies are disabled by default (else, they would be used in subsequent requests). To enable cookies, set `jar` to `true` (either in `defaults` or `options`). - -```js -var request = request.defaults({jar: true}) -request('http://www.google.com', function () { - request('http://images.google.com') -}) -``` - -To use a custom cookie jar (instead of `request`’s global cookie jar), set `jar` to an instance of `request.jar()` (either in `defaults` or `options`) - -```js -var j = request.jar() -var request = request.defaults({jar:j}) -request('http://www.google.com', function () { - request('http://images.google.com') -}) -``` - -OR - -```js -var j = request.jar(); -var cookie = request.cookie('key1=value1'); -var url = 'http://www.google.com'; -j.setCookie(cookie, url); -request({url: url, jar: j}, function () { - request('http://images.google.com') -}) -``` - -To use a custom cookie store (such as a -[`FileCookieStore`](https://github.com/mitsuru/tough-cookie-filestore) -which supports saving to and restoring from JSON files), pass it as a parameter -to `request.jar()`: - -```js -var FileCookieStore = require('tough-cookie-filestore'); -// NOTE - currently the 'cookies.json' file must already exist! -var j = request.jar(new FileCookieStore('cookies.json')); -request = request.defaults({ jar : j }) -request('http://www.google.com', function() { - request('http://images.google.com') -}) -``` - -The cookie store must be a -[`tough-cookie`](https://github.com/SalesforceEng/tough-cookie) -store and it must support synchronous operations; see the -[`CookieStore` API docs](https://github.com/SalesforceEng/tough-cookie#cookiestore-api) -for details. - -To inspect your cookie jar after a request: - -```js -var j = request.jar() -request({url: 'http://www.google.com', jar: j}, function () { - var cookie_string = j.getCookieString(url); // "key1=value1; key2=value2; ..." - var cookies = j.getCookies(url); - // [{key: 'key1', value: 'value1', domain: "www.google.com", ...}, ...] -}) -``` - -[back to top](#table-of-contents) diff --git a/node_modules/request/index.js b/node_modules/request/index.js deleted file mode 100755 index 911a90db..00000000 --- a/node_modules/request/index.js +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2010-2012 Mikeal Rogers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -'use strict' - -var extend = require('extend') - , cookies = require('./lib/cookies') - , helpers = require('./lib/helpers') - -var isFunction = helpers.isFunction - , paramsHaveRequestBody = helpers.paramsHaveRequestBody - - -// organize params for patch, post, put, head, del -function initParams(uri, options, callback) { - if (typeof options === 'function') { - callback = options - } - - var params = {} - if (typeof options === 'object') { - extend(params, options, {uri: uri}) - } else if (typeof uri === 'string') { - extend(params, {uri: uri}) - } else { - extend(params, uri) - } - - params.callback = callback || params.callback - return params -} - -function request (uri, options, callback) { - if (typeof uri === 'undefined') { - throw new Error('undefined is not a valid uri or options object.') - } - - var params = initParams(uri, options, callback) - - if (params.method === 'HEAD' && paramsHaveRequestBody(params)) { - throw new Error('HTTP HEAD requests MUST NOT include a request body.') - } - - return new request.Request(params) -} - -function verbFunc (verb) { - var method = verb.toUpperCase() - return function (uri, options, callback) { - var params = initParams(uri, options, callback) - params.method = method - return request(params, params.callback) - } -} - -// define like this to please codeintel/intellisense IDEs -request.get = verbFunc('get') -request.head = verbFunc('head') -request.post = verbFunc('post') -request.put = verbFunc('put') -request.patch = verbFunc('patch') -request.del = verbFunc('delete') -request['delete'] = verbFunc('delete') - -request.jar = function (store) { - return cookies.jar(store) -} - -request.cookie = function (str) { - return cookies.parse(str) -} - -function wrapRequestMethod (method, options, requester, verb) { - - return function (uri, opts, callback) { - var params = initParams(uri, opts, callback) - - var target = {} - extend(true, target, options, params) - - target.pool = params.pool || options.pool - - if (verb) { - target.method = verb.toUpperCase() - } - - if (isFunction(requester)) { - method = requester - } - - return method(target, target.callback) - } -} - -request.defaults = function (options, requester) { - var self = this - - options = options || {} - - if (typeof options === 'function') { - requester = options - options = {} - } - - var defaults = wrapRequestMethod(self, options, requester) - - var verbs = ['get', 'head', 'post', 'put', 'patch', 'del', 'delete'] - verbs.forEach(function(verb) { - defaults[verb] = wrapRequestMethod(self[verb], options, requester, verb) - }) - - defaults.cookie = wrapRequestMethod(self.cookie, options, requester) - defaults.jar = self.jar - defaults.defaults = self.defaults - return defaults -} - -request.forever = function (agentOptions, optionsArg) { - var options = {} - if (optionsArg) { - extend(options, optionsArg) - } - if (agentOptions) { - options.agentOptions = agentOptions - } - - options.forever = true - return request.defaults(options) -} - -// Exports - -module.exports = request -request.Request = require('./request') -request.initParams = initParams - -// Backwards compatibility for request.debug -Object.defineProperty(request, 'debug', { - enumerable : true, - get : function() { - return request.Request.debug - }, - set : function(debug) { - request.Request.debug = debug - } -}) diff --git a/node_modules/request/lib/auth.js b/node_modules/request/lib/auth.js deleted file mode 100644 index 1cb69521..00000000 --- a/node_modules/request/lib/auth.js +++ /dev/null @@ -1,168 +0,0 @@ -'use strict' - -var caseless = require('caseless') - , uuid = require('node-uuid') - , helpers = require('./helpers') - -var md5 = helpers.md5 - , toBase64 = helpers.toBase64 - - -function Auth (request) { - // define all public properties here - this.request = request - this.hasAuth = false - this.sentAuth = false - this.bearerToken = null - this.user = null - this.pass = null -} - -Auth.prototype.basic = function (user, pass, sendImmediately) { - var self = this - if (typeof user !== 'string' || (pass !== undefined && typeof pass !== 'string')) { - self.request.emit('error', new Error('auth() received invalid user or password')) - } - self.user = user - self.pass = pass - self.hasAuth = true - var header = user + ':' + (pass || '') - if (sendImmediately || typeof sendImmediately === 'undefined') { - var authHeader = 'Basic ' + toBase64(header) - self.sentAuth = true - return authHeader - } -} - -Auth.prototype.bearer = function (bearer, sendImmediately) { - var self = this - self.bearerToken = bearer - self.hasAuth = true - if (sendImmediately || typeof sendImmediately === 'undefined') { - if (typeof bearer === 'function') { - bearer = bearer() - } - var authHeader = 'Bearer ' + (bearer || '') - self.sentAuth = true - return authHeader - } -} - -Auth.prototype.digest = function (method, path, authHeader) { - // TODO: More complete implementation of RFC 2617. - // - handle challenge.domain - // - support qop="auth-int" only - // - handle Authentication-Info (not necessarily?) - // - check challenge.stale (not necessarily?) - // - increase nc (not necessarily?) - // For reference: - // http://tools.ietf.org/html/rfc2617#section-3 - // https://github.com/bagder/curl/blob/master/lib/http_digest.c - - var self = this - - var challenge = {} - var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi - for (;;) { - var match = re.exec(authHeader) - if (!match) { - break - } - challenge[match[1]] = match[2] || match[3] - } - - /** - * RFC 2617: handle both MD5 and MD5-sess algorithms. - * - * If the algorithm directive's value is "MD5" or unspecified, then HA1 is - * HA1=MD5(username:realm:password) - * If the algorithm directive's value is "MD5-sess", then HA1 is - * HA1=MD5(MD5(username:realm:password):nonce:cnonce) - */ - var ha1Compute = function (algorithm, user, realm, pass, nonce, cnonce) { - var ha1 = md5(user + ':' + realm + ':' + pass) - if (algorithm && algorithm.toLowerCase() === 'md5-sess') { - return md5(ha1 + ':' + nonce + ':' + cnonce) - } else { - return ha1 - } - } - - var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth' - var nc = qop && '00000001' - var cnonce = qop && uuid().replace(/-/g, '') - var ha1 = ha1Compute(challenge.algorithm, self.user, challenge.realm, self.pass, challenge.nonce, cnonce) - var ha2 = md5(method + ':' + path) - var digestResponse = qop - ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) - : md5(ha1 + ':' + challenge.nonce + ':' + ha2) - var authValues = { - username: self.user, - realm: challenge.realm, - nonce: challenge.nonce, - uri: path, - qop: qop, - response: digestResponse, - nc: nc, - cnonce: cnonce, - algorithm: challenge.algorithm, - opaque: challenge.opaque - } - - authHeader = [] - for (var k in authValues) { - if (authValues[k]) { - if (k === 'qop' || k === 'nc' || k === 'algorithm') { - authHeader.push(k + '=' + authValues[k]) - } else { - authHeader.push(k + '="' + authValues[k] + '"') - } - } - } - authHeader = 'Digest ' + authHeader.join(', ') - self.sentAuth = true - return authHeader -} - -Auth.prototype.onRequest = function (user, pass, sendImmediately, bearer) { - var self = this - , request = self.request - - var authHeader - if (bearer === undefined && user === undefined) { - self.request.emit('error', new Error('no auth mechanism defined')) - } else if (bearer !== undefined) { - authHeader = self.bearer(bearer, sendImmediately) - } else { - authHeader = self.basic(user, pass, sendImmediately) - } - if (authHeader) { - request.setHeader('authorization', authHeader) - } -} - -Auth.prototype.onResponse = function (response) { - var self = this - , request = self.request - - if (!self.hasAuth || self.sentAuth) { return null } - - var c = caseless(response.headers) - - var authHeader = c.get('www-authenticate') - var authVerb = authHeader && authHeader.split(' ')[0].toLowerCase() - request.debug('reauth', authVerb) - - switch (authVerb) { - case 'basic': - return self.basic(self.user, self.pass, true) - - case 'bearer': - return self.bearer(self.bearerToken, true) - - case 'digest': - return self.digest(request.method, request.path, authHeader) - } -} - -exports.Auth = Auth diff --git a/node_modules/request/lib/cookies.js b/node_modules/request/lib/cookies.js deleted file mode 100644 index 412c07d6..00000000 --- a/node_modules/request/lib/cookies.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict' - -var tough = require('tough-cookie') - -var Cookie = tough.Cookie - , CookieJar = tough.CookieJar - - -exports.parse = function(str) { - if (str && str.uri) { - str = str.uri - } - if (typeof str !== 'string') { - throw new Error('The cookie function only accepts STRING as param') - } - return Cookie.parse(str, {loose: true}) -} - -// Adapt the sometimes-Async api of tough.CookieJar to our requirements -function RequestJar(store) { - var self = this - self._jar = new CookieJar(store, {looseMode: true}) -} -RequestJar.prototype.setCookie = function(cookieOrStr, uri, options) { - var self = this - return self._jar.setCookieSync(cookieOrStr, uri, options || {}) -} -RequestJar.prototype.getCookieString = function(uri) { - var self = this - return self._jar.getCookieStringSync(uri) -} -RequestJar.prototype.getCookies = function(uri) { - var self = this - return self._jar.getCookiesSync(uri) -} - -exports.jar = function(store) { - return new RequestJar(store) -} diff --git a/node_modules/request/lib/getProxyFromURI.js b/node_modules/request/lib/getProxyFromURI.js deleted file mode 100644 index c2013a6e..00000000 --- a/node_modules/request/lib/getProxyFromURI.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict' - -function formatHostname(hostname) { - // canonicalize the hostname, so that 'oogle.com' won't match 'google.com' - return hostname.replace(/^\.*/, '.').toLowerCase() -} - -function parseNoProxyZone(zone) { - zone = zone.trim().toLowerCase() - - var zoneParts = zone.split(':', 2) - , zoneHost = formatHostname(zoneParts[0]) - , zonePort = zoneParts[1] - , hasPort = zone.indexOf(':') > -1 - - return {hostname: zoneHost, port: zonePort, hasPort: hasPort} -} - -function uriInNoProxy(uri, noProxy) { - var port = uri.port || (uri.protocol === 'https:' ? '443' : '80') - , hostname = formatHostname(uri.hostname) - , noProxyList = noProxy.split(',') - - // iterate through the noProxyList until it finds a match. - return noProxyList.map(parseNoProxyZone).some(function(noProxyZone) { - var isMatchedAt = hostname.indexOf(noProxyZone.hostname) - , hostnameMatched = ( - isMatchedAt > -1 && - (isMatchedAt === hostname.length - noProxyZone.hostname.length) - ) - - if (noProxyZone.hasPort) { - return (port === noProxyZone.port) && hostnameMatched - } - - return hostnameMatched - }) -} - -function getProxyFromURI(uri) { - // Decide the proper request proxy to use based on the request URI object and the - // environmental variables (NO_PROXY, HTTP_PROXY, etc.) - // respect NO_PROXY environment variables (see: http://lynx.isc.org/current/breakout/lynx_help/keystrokes/environments.html) - - var noProxy = process.env.NO_PROXY || process.env.no_proxy || '' - - // if the noProxy is a wildcard then return null - - if (noProxy === '*') { - return null - } - - // if the noProxy is not empty and the uri is found return null - - if (noProxy !== '' && uriInNoProxy(uri, noProxy)) { - return null - } - - // Check for HTTP or HTTPS Proxy in environment Else default to null - - if (uri.protocol === 'http:') { - return process.env.HTTP_PROXY || - process.env.http_proxy || null - } - - if (uri.protocol === 'https:') { - return process.env.HTTPS_PROXY || - process.env.https_proxy || - process.env.HTTP_PROXY || - process.env.http_proxy || null - } - - // if none of that works, return null - // (What uri protocol are you using then?) - - return null -} - -module.exports = getProxyFromURI diff --git a/node_modules/request/lib/har.js b/node_modules/request/lib/har.js deleted file mode 100644 index 30595748..00000000 --- a/node_modules/request/lib/har.js +++ /dev/null @@ -1,215 +0,0 @@ -'use strict' - -var fs = require('fs') -var qs = require('querystring') -var validate = require('har-validator') -var extend = require('extend') - -function Har (request) { - this.request = request -} - -Har.prototype.reducer = function (obj, pair) { - // new property ? - if (obj[pair.name] === undefined) { - obj[pair.name] = pair.value - return obj - } - - // existing? convert to array - var arr = [ - obj[pair.name], - pair.value - ] - - obj[pair.name] = arr - - return obj -} - -Har.prototype.prep = function (data) { - // construct utility properties - data.queryObj = {} - data.headersObj = {} - data.postData.jsonObj = false - data.postData.paramsObj = false - - // construct query objects - if (data.queryString && data.queryString.length) { - data.queryObj = data.queryString.reduce(this.reducer, {}) - } - - // construct headers objects - if (data.headers && data.headers.length) { - // loweCase header keys - data.headersObj = data.headers.reduceRight(function (headers, header) { - headers[header.name] = header.value - return headers - }, {}) - } - - // construct Cookie header - if (data.cookies && data.cookies.length) { - var cookies = data.cookies.map(function (cookie) { - return cookie.name + '=' + cookie.value - }) - - if (cookies.length) { - data.headersObj.cookie = cookies.join('; ') - } - } - - // prep body - function some (arr) { - return arr.some(function (type) { - return data.postData.mimeType.indexOf(type) === 0 - }) - } - - if (some([ - 'multipart/mixed', - 'multipart/related', - 'multipart/form-data', - 'multipart/alternative'])) { - - // reset values - data.postData.mimeType = 'multipart/form-data' - } - - else if (some([ - 'application/x-www-form-urlencoded'])) { - - if (!data.postData.params) { - data.postData.text = '' - } else { - data.postData.paramsObj = data.postData.params.reduce(this.reducer, {}) - - // always overwrite - data.postData.text = qs.stringify(data.postData.paramsObj) - } - } - - else if (some([ - 'text/json', - 'text/x-json', - 'application/json', - 'application/x-json'])) { - - data.postData.mimeType = 'application/json' - - if (data.postData.text) { - try { - data.postData.jsonObj = JSON.parse(data.postData.text) - } catch (e) { - this.request.debug(e) - - // force back to text/plain - data.postData.mimeType = 'text/plain' - } - } - } - - return data -} - -Har.prototype.options = function (options) { - // skip if no har property defined - if (!options.har) { - return options - } - - var har = {} - extend(har, options.har) - - // only process the first entry - if (har.log && har.log.entries) { - har = har.log.entries[0] - } - - // add optional properties to make validation successful - har.url = har.url || options.url || options.uri || options.baseUrl || '/' - har.httpVersion = har.httpVersion || 'HTTP/1.1' - har.queryString = har.queryString || [] - har.headers = har.headers || [] - har.cookies = har.cookies || [] - har.postData = har.postData || {} - har.postData.mimeType = har.postData.mimeType || 'application/octet-stream' - - har.bodySize = 0 - har.headersSize = 0 - har.postData.size = 0 - - if (!validate.request(har)) { - return options - } - - // clean up and get some utility properties - var req = this.prep(har) - - // construct new options - if (req.url) { - options.url = req.url - } - - if (req.method) { - options.method = req.method - } - - if (Object.keys(req.queryObj).length) { - options.qs = req.queryObj - } - - if (Object.keys(req.headersObj).length) { - options.headers = req.headersObj - } - - function test (type) { - return req.postData.mimeType.indexOf(type) === 0 - } - if (test('application/x-www-form-urlencoded')) { - options.form = req.postData.paramsObj - } - else if (test('application/json')) { - if (req.postData.jsonObj) { - options.body = req.postData.jsonObj - options.json = true - } - } - else if (test('multipart/form-data')) { - options.formData = {} - - req.postData.params.forEach(function (param) { - var attachment = {} - - if (!param.fileName && !param.fileName && !param.contentType) { - options.formData[param.name] = param.value - return - } - - // attempt to read from disk! - if (param.fileName && !param.value) { - attachment.value = fs.createReadStream(param.fileName) - } else if (param.value) { - attachment.value = param.value - } - - if (param.fileName) { - attachment.options = { - filename: param.fileName, - contentType: param.contentType ? param.contentType : null - } - } - - options.formData[param.name] = attachment - }) - } - else { - if (req.postData.text) { - options.body = req.postData.text - } - } - - return options -} - -exports.Har = Har diff --git a/node_modules/request/lib/helpers.js b/node_modules/request/lib/helpers.js deleted file mode 100644 index 356ff748..00000000 --- a/node_modules/request/lib/helpers.js +++ /dev/null @@ -1,74 +0,0 @@ -'use strict' - -var jsonSafeStringify = require('json-stringify-safe') - , crypto = require('crypto') - -function deferMethod() { - if (typeof setImmediate === 'undefined') { - return process.nextTick - } - - return setImmediate -} - -function isFunction(value) { - return typeof value === 'function' -} - -function paramsHaveRequestBody(params) { - return ( - params.body || - params.requestBodyStream || - (params.json && typeof params.json !== 'boolean') || - params.multipart - ) -} - -function safeStringify (obj, replacer) { - var ret - try { - ret = JSON.stringify(obj, replacer) - } catch (e) { - ret = jsonSafeStringify(obj, replacer) - } - return ret -} - -function md5 (str) { - return crypto.createHash('md5').update(str).digest('hex') -} - -function isReadStream (rs) { - return rs.readable && rs.path && rs.mode -} - -function toBase64 (str) { - return (new Buffer(str || '', 'utf8')).toString('base64') -} - -function copy (obj) { - var o = {} - Object.keys(obj).forEach(function (i) { - o[i] = obj[i] - }) - return o -} - -function version () { - var numbers = process.version.replace('v', '').split('.') - return { - major: parseInt(numbers[0], 10), - minor: parseInt(numbers[1], 10), - patch: parseInt(numbers[2], 10) - } -} - -exports.isFunction = isFunction -exports.paramsHaveRequestBody = paramsHaveRequestBody -exports.safeStringify = safeStringify -exports.md5 = md5 -exports.isReadStream = isReadStream -exports.toBase64 = toBase64 -exports.copy = copy -exports.version = version -exports.defer = deferMethod() diff --git a/node_modules/request/lib/multipart.js b/node_modules/request/lib/multipart.js deleted file mode 100644 index c1281726..00000000 --- a/node_modules/request/lib/multipart.js +++ /dev/null @@ -1,112 +0,0 @@ -'use strict' - -var uuid = require('node-uuid') - , CombinedStream = require('combined-stream') - , isstream = require('isstream') - - -function Multipart (request) { - this.request = request - this.boundary = uuid() - this.chunked = false - this.body = null -} - -Multipart.prototype.isChunked = function (options) { - var self = this - , chunked = false - , parts = options.data || options - - if (!parts.forEach) { - self.request.emit('error', new Error('Argument error, options.multipart.')) - } - - if (options.chunked !== undefined) { - chunked = options.chunked - } - - if (self.request.getHeader('transfer-encoding') === 'chunked') { - chunked = true - } - - if (!chunked) { - parts.forEach(function (part) { - if (typeof part.body === 'undefined') { - self.request.emit('error', new Error('Body attribute missing in multipart.')) - } - if (isstream(part.body)) { - chunked = true - } - }) - } - - return chunked -} - -Multipart.prototype.setHeaders = function (chunked) { - var self = this - - if (chunked && !self.request.hasHeader('transfer-encoding')) { - self.request.setHeader('transfer-encoding', 'chunked') - } - - var header = self.request.getHeader('content-type') - - if (!header || header.indexOf('multipart') === -1) { - self.request.setHeader('content-type', 'multipart/related; boundary=' + self.boundary) - } else { - if (header.indexOf('boundary') !== -1) { - self.boundary = header.replace(/.*boundary=([^\s;]+).*/, '$1') - } else { - self.request.setHeader('content-type', header + '; boundary=' + self.boundary) - } - } -} - -Multipart.prototype.build = function (parts, chunked) { - var self = this - var body = chunked ? new CombinedStream() : [] - - function add (part) { - if (typeof part === 'number') { - part = part.toString() - } - return chunked ? body.append(part) : body.push(new Buffer(part)) - } - - if (self.request.preambleCRLF) { - add('\r\n') - } - - parts.forEach(function (part) { - var preamble = '--' + self.boundary + '\r\n' - Object.keys(part).forEach(function (key) { - if (key === 'body') { return } - preamble += key + ': ' + part[key] + '\r\n' - }) - preamble += '\r\n' - add(preamble) - add(part.body) - add('\r\n') - }) - add('--' + self.boundary + '--') - - if (self.request.postambleCRLF) { - add('\r\n') - } - - return body -} - -Multipart.prototype.onRequest = function (options) { - var self = this - - var chunked = self.isChunked(options) - , parts = options.data || options - - self.setHeaders(chunked) - self.chunked = chunked - self.body = self.build(parts, chunked) -} - -exports.Multipart = Multipart diff --git a/node_modules/request/lib/oauth.js b/node_modules/request/lib/oauth.js deleted file mode 100644 index c24209b8..00000000 --- a/node_modules/request/lib/oauth.js +++ /dev/null @@ -1,147 +0,0 @@ -'use strict' - -var url = require('url') - , qs = require('qs') - , caseless = require('caseless') - , uuid = require('node-uuid') - , oauth = require('oauth-sign') - , crypto = require('crypto') - - -function OAuth (request) { - this.request = request - this.params = null -} - -OAuth.prototype.buildParams = function (_oauth, uri, method, query, form, qsLib) { - var oa = {} - for (var i in _oauth) { - oa['oauth_' + i] = _oauth[i] - } - if (!oa.oauth_version) { - oa.oauth_version = '1.0' - } - if (!oa.oauth_timestamp) { - oa.oauth_timestamp = Math.floor( Date.now() / 1000 ).toString() - } - if (!oa.oauth_nonce) { - oa.oauth_nonce = uuid().replace(/-/g, '') - } - if (!oa.oauth_signature_method) { - oa.oauth_signature_method = 'HMAC-SHA1' - } - - var consumer_secret_or_private_key = oa.oauth_consumer_secret || oa.oauth_private_key - delete oa.oauth_consumer_secret - delete oa.oauth_private_key - - var token_secret = oa.oauth_token_secret - delete oa.oauth_token_secret - - var realm = oa.oauth_realm - delete oa.oauth_realm - delete oa.oauth_transport_method - - var baseurl = uri.protocol + '//' + uri.host + uri.pathname - var params = qsLib.parse([].concat(query, form, qsLib.stringify(oa)).join('&')) - - oa.oauth_signature = oauth.sign( - oa.oauth_signature_method, - method, - baseurl, - params, - consumer_secret_or_private_key, - token_secret) - - if (realm) { - oa.realm = realm - } - - return oa -} - -OAuth.prototype.buildBodyHash = function(_oauth, body) { - if (['HMAC-SHA1', 'RSA-SHA1'].indexOf(_oauth.signature_method || 'HMAC-SHA1') < 0) { - this.request.emit('error', new Error('oauth: ' + _oauth.signature_method + - ' signature_method not supported with body_hash signing.')) - } - - var shasum = crypto.createHash('sha1') - shasum.update(body || '') - var sha1 = shasum.digest('hex') - - return new Buffer(sha1).toString('base64') -} - -OAuth.prototype.concatParams = function (oa, sep, wrap) { - wrap = wrap || '' - - var params = Object.keys(oa).filter(function (i) { - return i !== 'realm' && i !== 'oauth_signature' - }).sort() - - if (oa.realm) { - params.splice(0, 0, 'realm') - } - params.push('oauth_signature') - - return params.map(function (i) { - return i + '=' + wrap + oauth.rfc3986(oa[i]) + wrap - }).join(sep) -} - -OAuth.prototype.onRequest = function (_oauth) { - var self = this - self.params = _oauth - - var uri = self.request.uri || {} - , method = self.request.method || '' - , headers = caseless(self.request.headers) - , body = self.request.body || '' - , qsLib = self.request.qsLib || qs - - var form - , query - , contentType = headers.get('content-type') || '' - , formContentType = 'application/x-www-form-urlencoded' - , transport = _oauth.transport_method || 'header' - - if (contentType.slice(0, formContentType.length) === formContentType) { - contentType = formContentType - form = body - } - if (uri.query) { - query = uri.query - } - if (transport === 'body' && (method !== 'POST' || contentType !== formContentType)) { - self.request.emit('error', new Error('oauth: transport_method of body requires POST ' + - 'and content-type ' + formContentType)) - } - - if (!form && typeof _oauth.body_hash === 'boolean') { - _oauth.body_hash = self.buildBodyHash(_oauth, self.request.body.toString()) - } - - var oa = self.buildParams(_oauth, uri, method, query, form, qsLib) - - switch (transport) { - case 'header': - self.request.setHeader('Authorization', 'OAuth ' + self.concatParams(oa, ',', '"')) - break - - case 'query': - var href = self.request.uri.href += (query ? '&' : '?') + self.concatParams(oa, '&') - self.request.uri = url.parse(href) - self.request.path = self.request.uri.path - break - - case 'body': - self.request.body = (form ? form + '&' : '') + self.concatParams(oa, '&') - break - - default: - self.request.emit('error', new Error('oauth: transport_method invalid')) - } -} - -exports.OAuth = OAuth diff --git a/node_modules/request/lib/querystring.js b/node_modules/request/lib/querystring.js deleted file mode 100644 index baf5e802..00000000 --- a/node_modules/request/lib/querystring.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict' - -var qs = require('qs') - , querystring = require('querystring') - - -function Querystring (request) { - this.request = request - this.lib = null - this.useQuerystring = null - this.parseOptions = null - this.stringifyOptions = null -} - -Querystring.prototype.init = function (options) { - if (this.lib) {return} - - this.useQuerystring = options.useQuerystring - this.lib = (this.useQuerystring ? querystring : qs) - - this.parseOptions = options.qsParseOptions || {} - this.stringifyOptions = options.qsStringifyOptions || {} -} - -Querystring.prototype.stringify = function (obj) { - return (this.useQuerystring) - ? this.rfc3986(this.lib.stringify(obj, - this.stringifyOptions.sep || null, - this.stringifyOptions.eq || null, - this.stringifyOptions)) - : this.lib.stringify(obj, this.stringifyOptions) -} - -Querystring.prototype.parse = function (str) { - return (this.useQuerystring) - ? this.lib.parse(str, - this.parseOptions.sep || null, - this.parseOptions.eq || null, - this.parseOptions) - : this.lib.parse(str, this.parseOptions) -} - -Querystring.prototype.rfc3986 = function (str) { - return str.replace(/[!'()*]/g, function (c) { - return '%' + c.charCodeAt(0).toString(16).toUpperCase() - }) -} - -Querystring.prototype.unescape = querystring.unescape - -exports.Querystring = Querystring diff --git a/node_modules/request/lib/redirect.js b/node_modules/request/lib/redirect.js deleted file mode 100644 index 040dfe0e..00000000 --- a/node_modules/request/lib/redirect.js +++ /dev/null @@ -1,153 +0,0 @@ -'use strict' - -var url = require('url') -var isUrl = /^https?:/ - -function Redirect (request) { - this.request = request - this.followRedirect = true - this.followRedirects = true - this.followAllRedirects = false - this.allowRedirect = function () {return true} - this.maxRedirects = 10 - this.redirects = [] - this.redirectsFollowed = 0 - this.removeRefererHeader = false -} - -Redirect.prototype.onRequest = function (options) { - var self = this - - if (options.maxRedirects !== undefined) { - self.maxRedirects = options.maxRedirects - } - if (typeof options.followRedirect === 'function') { - self.allowRedirect = options.followRedirect - } - if (options.followRedirect !== undefined) { - self.followRedirects = !!options.followRedirect - } - if (options.followAllRedirects !== undefined) { - self.followAllRedirects = options.followAllRedirects - } - if (self.followRedirects || self.followAllRedirects) { - self.redirects = self.redirects || [] - } - if (options.removeRefererHeader !== undefined) { - self.removeRefererHeader = options.removeRefererHeader - } -} - -Redirect.prototype.redirectTo = function (response) { - var self = this - , request = self.request - - var redirectTo = null - if (response.statusCode >= 300 && response.statusCode < 400 && response.caseless.has('location')) { - var location = response.caseless.get('location') - request.debug('redirect', location) - - if (self.followAllRedirects) { - redirectTo = location - } else if (self.followRedirects) { - switch (request.method) { - case 'PATCH': - case 'PUT': - case 'POST': - case 'DELETE': - // Do not follow redirects - break - default: - redirectTo = location - break - } - } - } else if (response.statusCode === 401) { - var authHeader = request._auth.onResponse(response) - if (authHeader) { - request.setHeader('authorization', authHeader) - redirectTo = request.uri - } - } - return redirectTo -} - -Redirect.prototype.onResponse = function (response) { - var self = this - , request = self.request - - var redirectTo = self.redirectTo(response) - if (!redirectTo || !self.allowRedirect.call(request, response)) { - return false - } - - request.debug('redirect to', redirectTo) - - // ignore any potential response body. it cannot possibly be useful - // to us at this point. - // response.resume should be defined, but check anyway before calling. Workaround for browserify. - if (response.resume) { - response.resume() - } - - if (self.redirectsFollowed >= self.maxRedirects) { - request.emit('error', new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + request.uri.href)) - return false - } - self.redirectsFollowed += 1 - - if (!isUrl.test(redirectTo)) { - redirectTo = url.resolve(request.uri.href, redirectTo) - } - - var uriPrev = request.uri - request.uri = url.parse(redirectTo) - - // handle the case where we change protocol from https to http or vice versa - if (request.uri.protocol !== uriPrev.protocol) { - delete request.agent - } - - self.redirects.push( - { statusCode : response.statusCode - , redirectUri: redirectTo - } - ) - if (self.followAllRedirects && request.method !== 'HEAD' - && response.statusCode !== 401 && response.statusCode !== 307) { - request.method = 'GET' - } - // request.method = 'GET' // Force all redirects to use GET || commented out fixes #215 - delete request.src - delete request.req - delete request._started - if (response.statusCode !== 401 && response.statusCode !== 307) { - // Remove parameters from the previous response, unless this is the second request - // for a server that requires digest authentication. - delete request.body - delete request._form - if (request.headers) { - request.removeHeader('host') - request.removeHeader('content-type') - request.removeHeader('content-length') - if (request.uri.hostname !== request.originalHost.split(':')[0]) { - // Remove authorization if changing hostnames (but not if just - // changing ports or protocols). This matches the behavior of curl: - // https://github.com/bagder/curl/blob/6beb0eee/lib/http.c#L710 - request.removeHeader('authorization') - } - } - } - - if (!self.removeRefererHeader) { - request.setHeader('referer', uriPrev.href) - } - - request.emit('redirect') - - request.init() - - return true -} - -exports.Redirect = Redirect diff --git a/node_modules/request/lib/tunnel.js b/node_modules/request/lib/tunnel.js deleted file mode 100644 index bf96a8fe..00000000 --- a/node_modules/request/lib/tunnel.js +++ /dev/null @@ -1,176 +0,0 @@ -'use strict' - -var url = require('url') - , tunnel = require('tunnel-agent') - -var defaultProxyHeaderWhiteList = [ - 'accept', - 'accept-charset', - 'accept-encoding', - 'accept-language', - 'accept-ranges', - 'cache-control', - 'content-encoding', - 'content-language', - 'content-location', - 'content-md5', - 'content-range', - 'content-type', - 'connection', - 'date', - 'expect', - 'max-forwards', - 'pragma', - 'referer', - 'te', - 'user-agent', - 'via' -] - -var defaultProxyHeaderExclusiveList = [ - 'proxy-authorization' -] - -function constructProxyHost(uriObject) { - var port = uriObject.port - , protocol = uriObject.protocol - , proxyHost = uriObject.hostname + ':' - - if (port) { - proxyHost += port - } else if (protocol === 'https:') { - proxyHost += '443' - } else { - proxyHost += '80' - } - - return proxyHost -} - -function constructProxyHeaderWhiteList(headers, proxyHeaderWhiteList) { - var whiteList = proxyHeaderWhiteList - .reduce(function (set, header) { - set[header.toLowerCase()] = true - return set - }, {}) - - return Object.keys(headers) - .filter(function (header) { - return whiteList[header.toLowerCase()] - }) - .reduce(function (set, header) { - set[header] = headers[header] - return set - }, {}) -} - -function constructTunnelOptions (request, proxyHeaders) { - var proxy = request.proxy - - var tunnelOptions = { - proxy : { - host : proxy.hostname, - port : +proxy.port, - proxyAuth : proxy.auth, - headers : proxyHeaders - }, - headers : request.headers, - ca : request.ca, - cert : request.cert, - key : request.key, - passphrase : request.passphrase, - pfx : request.pfx, - ciphers : request.ciphers, - rejectUnauthorized : request.rejectUnauthorized, - secureOptions : request.secureOptions, - secureProtocol : request.secureProtocol - } - - return tunnelOptions -} - -function constructTunnelFnName(uri, proxy) { - var uriProtocol = (uri.protocol === 'https:' ? 'https' : 'http') - var proxyProtocol = (proxy.protocol === 'https:' ? 'Https' : 'Http') - return [uriProtocol, proxyProtocol].join('Over') -} - -function getTunnelFn(request) { - var uri = request.uri - var proxy = request.proxy - var tunnelFnName = constructTunnelFnName(uri, proxy) - return tunnel[tunnelFnName] -} - - -function Tunnel (request) { - this.request = request - this.proxyHeaderWhiteList = defaultProxyHeaderWhiteList - this.proxyHeaderExclusiveList = [] - if (typeof request.tunnel !== 'undefined') { - this.tunnelOverride = request.tunnel - } -} - -Tunnel.prototype.isEnabled = function () { - var self = this - , request = self.request - // Tunnel HTTPS by default. Allow the user to override this setting. - - // If self.tunnelOverride is set (the user specified a value), use it. - if (typeof self.tunnelOverride !== 'undefined') { - return self.tunnelOverride - } - - // If the destination is HTTPS, tunnel. - if (request.uri.protocol === 'https:') { - return true - } - - // Otherwise, do not use tunnel. - return false -} - -Tunnel.prototype.setup = function (options) { - var self = this - , request = self.request - - options = options || {} - - if (typeof request.proxy === 'string') { - request.proxy = url.parse(request.proxy) - } - - if (!request.proxy || !request.tunnel) { - return false - } - - // Setup Proxy Header Exclusive List and White List - if (options.proxyHeaderWhiteList) { - self.proxyHeaderWhiteList = options.proxyHeaderWhiteList - } - if (options.proxyHeaderExclusiveList) { - self.proxyHeaderExclusiveList = options.proxyHeaderExclusiveList - } - - var proxyHeaderExclusiveList = self.proxyHeaderExclusiveList.concat(defaultProxyHeaderExclusiveList) - var proxyHeaderWhiteList = self.proxyHeaderWhiteList.concat(proxyHeaderExclusiveList) - - // Setup Proxy Headers and Proxy Headers Host - // Only send the Proxy White Listed Header names - var proxyHeaders = constructProxyHeaderWhiteList(request.headers, proxyHeaderWhiteList) - proxyHeaders.host = constructProxyHost(request.uri) - - proxyHeaderExclusiveList.forEach(request.removeHeader, request) - - // Set Agent from Tunnel Data - var tunnelFn = getTunnelFn(request) - var tunnelOptions = constructTunnelOptions(request, proxyHeaders) - request.agent = tunnelFn(tunnelOptions) - - return true -} - -Tunnel.defaultProxyHeaderWhiteList = defaultProxyHeaderWhiteList -Tunnel.defaultProxyHeaderExclusiveList = defaultProxyHeaderExclusiveList -exports.Tunnel = Tunnel diff --git a/node_modules/request/node_modules/.bin/har-validator b/node_modules/request/node_modules/.bin/har-validator deleted file mode 120000 index c6ec1634..00000000 --- a/node_modules/request/node_modules/.bin/har-validator +++ /dev/null @@ -1 +0,0 @@ -../har-validator/bin/har-validator \ No newline at end of file diff --git a/node_modules/request/node_modules/.bin/uuid b/node_modules/request/node_modules/.bin/uuid deleted file mode 120000 index 80eb14aa..00000000 --- a/node_modules/request/node_modules/.bin/uuid +++ /dev/null @@ -1 +0,0 @@ -../node-uuid/bin/uuid \ No newline at end of file diff --git a/node_modules/request/node_modules/aws-sign2/LICENSE b/node_modules/request/node_modules/aws-sign2/LICENSE deleted file mode 100644 index a4a9aee0..00000000 --- a/node_modules/request/node_modules/aws-sign2/LICENSE +++ /dev/null @@ -1,55 +0,0 @@ -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and - -You must cause any modified files to carry prominent notices stating that You changed the files; and - -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/node_modules/request/node_modules/aws-sign2/README.md b/node_modules/request/node_modules/aws-sign2/README.md deleted file mode 100644 index 763564e0..00000000 --- a/node_modules/request/node_modules/aws-sign2/README.md +++ /dev/null @@ -1,4 +0,0 @@ -aws-sign -======== - -AWS signing. Originally pulled from LearnBoost/knox, maintained as vendor in request, now a standalone module. diff --git a/node_modules/request/node_modules/aws-sign2/index.js b/node_modules/request/node_modules/aws-sign2/index.js deleted file mode 100644 index ac720930..00000000 --- a/node_modules/request/node_modules/aws-sign2/index.js +++ /dev/null @@ -1,212 +0,0 @@ - -/*! - * Copyright 2010 LearnBoost - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Module dependencies. - */ - -var crypto = require('crypto') - , parse = require('url').parse - ; - -/** - * Valid keys. - */ - -var keys = - [ 'acl' - , 'location' - , 'logging' - , 'notification' - , 'partNumber' - , 'policy' - , 'requestPayment' - , 'torrent' - , 'uploadId' - , 'uploads' - , 'versionId' - , 'versioning' - , 'versions' - , 'website' - ] - -/** - * Return an "Authorization" header value with the given `options` - * in the form of "AWS :" - * - * @param {Object} options - * @return {String} - * @api private - */ - -function authorization (options) { - return 'AWS ' + options.key + ':' + sign(options) -} - -module.exports = authorization -module.exports.authorization = authorization - -/** - * Simple HMAC-SHA1 Wrapper - * - * @param {Object} options - * @return {String} - * @api private - */ - -function hmacSha1 (options) { - return crypto.createHmac('sha1', options.secret).update(options.message).digest('base64') -} - -module.exports.hmacSha1 = hmacSha1 - -/** - * Create a base64 sha1 HMAC for `options`. - * - * @param {Object} options - * @return {String} - * @api private - */ - -function sign (options) { - options.message = stringToSign(options) - return hmacSha1(options) -} -module.exports.sign = sign - -/** - * Create a base64 sha1 HMAC for `options`. - * - * Specifically to be used with S3 presigned URLs - * - * @param {Object} options - * @return {String} - * @api private - */ - -function signQuery (options) { - options.message = queryStringToSign(options) - return hmacSha1(options) -} -module.exports.signQuery= signQuery - -/** - * Return a string for sign() with the given `options`. - * - * Spec: - * - * \n - * \n - * \n - * \n - * [headers\n] - * - * - * @param {Object} options - * @return {String} - * @api private - */ - -function stringToSign (options) { - var headers = options.amazonHeaders || '' - if (headers) headers += '\n' - var r = - [ options.verb - , options.md5 - , options.contentType - , options.date ? options.date.toUTCString() : '' - , headers + options.resource - ] - return r.join('\n') -} -module.exports.queryStringToSign = stringToSign - -/** - * Return a string for sign() with the given `options`, but is meant exclusively - * for S3 presigned URLs - * - * Spec: - * - * \n - * - * - * @param {Object} options - * @return {String} - * @api private - */ - -function queryStringToSign (options){ - return 'GET\n\n\n' + options.date + '\n' + options.resource -} -module.exports.queryStringToSign = queryStringToSign - -/** - * Perform the following: - * - * - ignore non-amazon headers - * - lowercase fields - * - sort lexicographically - * - trim whitespace between ":" - * - join with newline - * - * @param {Object} headers - * @return {String} - * @api private - */ - -function canonicalizeHeaders (headers) { - var buf = [] - , fields = Object.keys(headers) - ; - for (var i = 0, len = fields.length; i < len; ++i) { - var field = fields[i] - , val = headers[field] - , field = field.toLowerCase() - ; - if (0 !== field.indexOf('x-amz')) continue - buf.push(field + ':' + val) - } - return buf.sort().join('\n') -} -module.exports.canonicalizeHeaders = canonicalizeHeaders - -/** - * Perform the following: - * - * - ignore non sub-resources - * - sort lexicographically - * - * @param {String} resource - * @return {String} - * @api private - */ - -function canonicalizeResource (resource) { - var url = parse(resource, true) - , path = url.pathname - , buf = [] - ; - - Object.keys(url.query).forEach(function(key){ - if (!~keys.indexOf(key)) return - var val = '' == url.query[key] ? '' : '=' + encodeURIComponent(url.query[key]) - buf.push(key + val) - }) - - return path + (buf.length ? '?' + buf.sort().join('&') : '') -} -module.exports.canonicalizeResource = canonicalizeResource diff --git a/node_modules/request/node_modules/aws-sign2/package.json b/node_modules/request/node_modules/aws-sign2/package.json deleted file mode 100644 index 68572c7b..00000000 --- a/node_modules/request/node_modules/aws-sign2/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "author": { - "name": "Mikeal Rogers", - "email": "mikeal.rogers@gmail.com", - "url": "http://www.futurealoof.com" - }, - "name": "aws-sign2", - "description": "AWS signing. Originally pulled from LearnBoost/knox, maintained as vendor in request, now a standalone module.", - "version": "0.6.0", - "repository": { - "url": "git+https://github.com/mikeal/aws-sign.git" - }, - "license": "Apache-2.0", - "main": "index.js", - "dependencies": {}, - "devDependencies": {}, - "optionalDependencies": {}, - "engines": { - "node": "*" - }, - "gitHead": "8554bdb41268fa295eb1ee300f4adaa9f7f07fec", - "bugs": { - "url": "https://github.com/mikeal/aws-sign/issues" - }, - "homepage": "https://github.com/mikeal/aws-sign#readme", - "_id": "aws-sign2@0.6.0", - "scripts": {}, - "_shasum": "14342dd38dbcc94d0e5b87d763cd63612c0e794f", - "_from": "aws-sign2@>=0.6.0 <0.7.0", - "_npmVersion": "2.14.4", - "_nodeVersion": "4.1.2", - "_npmUser": { - "name": "mikeal", - "email": "mikeal.rogers@gmail.com" - }, - "maintainers": [ - { - "name": "mikeal", - "email": "mikeal.rogers@gmail.com" - } - ], - "dist": { - "shasum": "14342dd38dbcc94d0e5b87d763cd63612c0e794f", - "tarball": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/aws4/.npmignore b/node_modules/request/node_modules/aws4/.npmignore deleted file mode 100644 index 6c6ade6f..00000000 --- a/node_modules/request/node_modules/aws4/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -test -examples -example.js -browser diff --git a/node_modules/request/node_modules/aws4/.tern-port b/node_modules/request/node_modules/aws4/.tern-port deleted file mode 100644 index 7fd1b522..00000000 --- a/node_modules/request/node_modules/aws4/.tern-port +++ /dev/null @@ -1 +0,0 @@ -62638 \ No newline at end of file diff --git a/node_modules/request/node_modules/aws4/.travis.yml b/node_modules/request/node_modules/aws4/.travis.yml deleted file mode 100644 index 61d06340..00000000 --- a/node_modules/request/node_modules/aws4/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - "0.10" - - "0.12" - - "4.2" diff --git a/node_modules/request/node_modules/aws4/LICENSE b/node_modules/request/node_modules/aws4/LICENSE deleted file mode 100644 index 4f321e59..00000000 --- a/node_modules/request/node_modules/aws4/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright 2013 Michael Hart (michael.hart.au@gmail.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/node_modules/request/node_modules/aws4/README.md b/node_modules/request/node_modules/aws4/README.md deleted file mode 100644 index 6c55da80..00000000 --- a/node_modules/request/node_modules/aws4/README.md +++ /dev/null @@ -1,514 +0,0 @@ -aws4 ----- - -[![Build Status](https://secure.travis-ci.org/mhart/aws4.png?branch=master)](http://travis-ci.org/mhart/aws4) - -A small utility to sign vanilla node.js http(s) request options using Amazon's -[AWS Signature Version 4](http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html). - -Can also be used [in the browser](./browser). - -This signature is supported by nearly all Amazon services, including -[S3](http://docs.aws.amazon.com/AmazonS3/latest/API/), -[EC2](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/), -[DynamoDB](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/API.html), -[Kinesis](http://docs.aws.amazon.com/kinesis/latest/APIReference/), -[Lambda](http://docs.aws.amazon.com/lambda/latest/dg/API_Reference.html), -[SQS](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/), -[SNS](http://docs.aws.amazon.com/sns/latest/api/), -[IAM](http://docs.aws.amazon.com/IAM/latest/APIReference/), -[STS](http://docs.aws.amazon.com/STS/latest/APIReference/), -[RDS](http://docs.aws.amazon.com/AmazonRDS/latest/APIReference/), -[CloudWatch](http://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/), -[CloudWatch Logs](http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/), -[CodeDeploy](http://docs.aws.amazon.com/codedeploy/latest/APIReference/), -[CloudFront](http://docs.aws.amazon.com/AmazonCloudFront/latest/APIReference/), -[CloudTrail](http://docs.aws.amazon.com/awscloudtrail/latest/APIReference/), -[ElastiCache](http://docs.aws.amazon.com/AmazonElastiCache/latest/APIReference/), -[EMR](http://docs.aws.amazon.com/ElasticMapReduce/latest/API/), -[Glacier](http://docs.aws.amazon.com/amazonglacier/latest/dev/amazon-glacier-api.html), -[CloudSearch](http://docs.aws.amazon.com/cloudsearch/latest/developerguide/APIReq.html), -[Elastic Load Balancing](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/APIReference/), -[Elastic Transcoder](http://docs.aws.amazon.com/elastictranscoder/latest/developerguide/api-reference.html), -[CloudFormation](http://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/), -[Elastic Beanstalk](http://docs.aws.amazon.com/elasticbeanstalk/latest/api/), -[Storage Gateway](http://docs.aws.amazon.com/storagegateway/latest/userguide/AWSStorageGatewayAPI.html), -[Data Pipeline](http://docs.aws.amazon.com/datapipeline/latest/APIReference/), -[Direct Connect](http://docs.aws.amazon.com/directconnect/latest/APIReference/), -[Redshift](http://docs.aws.amazon.com/redshift/latest/APIReference/), -[OpsWorks](http://docs.aws.amazon.com/opsworks/latest/APIReference/), -[SES](http://docs.aws.amazon.com/ses/latest/APIReference/), -[SWF](http://docs.aws.amazon.com/amazonswf/latest/apireference/), -[AutoScaling](http://docs.aws.amazon.com/AutoScaling/latest/APIReference/), -[Mobile Analytics](http://docs.aws.amazon.com/mobileanalytics/latest/ug/server-reference.html), -[Cognito Identity](http://docs.aws.amazon.com/cognitoidentity/latest/APIReference/), -[Cognito Sync](http://docs.aws.amazon.com/cognitosync/latest/APIReference/), -[Container Service](http://docs.aws.amazon.com/AmazonECS/latest/APIReference/), -[AppStream](http://docs.aws.amazon.com/appstream/latest/developerguide/appstream-api-rest.html), -[Key Management Service](http://docs.aws.amazon.com/kms/latest/APIReference/), -[Config](http://docs.aws.amazon.com/config/latest/APIReference/), -[CloudHSM](http://docs.aws.amazon.com/cloudhsm/latest/dg/api-ref.html), -[Route53](http://docs.aws.amazon.com/Route53/latest/APIReference/requests-rest.html) and -[Route53 Domains](http://docs.aws.amazon.com/Route53/latest/APIReference/requests-rpc.html). - -Indeed, the only AWS services that *don't* support v4 as of 2014-12-30 are -[Import/Export](http://docs.aws.amazon.com/AWSImportExport/latest/DG/api-reference.html) and -[SimpleDB](http://docs.aws.amazon.com/AmazonSimpleDB/latest/DeveloperGuide/SDB_API.html) -(they only support [AWS Signature Version 2](https://github.com/mhart/aws2)). - -It also provides defaults for a number of core AWS headers and -request parameters, making it very easy to query AWS services, or -build out a fully-featured AWS library. - -Example -------- - -```javascript -var http = require('http'), - https = require('https'), - aws4 = require('aws4') - -// given an options object you could pass to http.request -var opts = {host: 'sqs.us-east-1.amazonaws.com', path: '/?Action=ListQueues'} - -// alternatively (as aws4 can infer the host): -opts = {service: 'sqs', region: 'us-east-1', path: '/?Action=ListQueues'} - -// alternatively (as us-east-1 is default): -opts = {service: 'sqs', path: '/?Action=ListQueues'} - -aws4.sign(opts) // assumes AWS credentials are available in process.env - -console.log(opts) -/* -{ - host: 'sqs.us-east-1.amazonaws.com', - path: '/?Action=ListQueues', - headers: { - Host: 'sqs.us-east-1.amazonaws.com', - 'X-Amz-Date': '20121226T061030Z', - Authorization: 'AWS4-HMAC-SHA256 Credential=ABCDEF/20121226/us-east-1/sqs/aws4_request, ...' - } -} -*/ - -// we can now use this to query AWS using the standard node.js http API -http.request(opts, function(res) { res.pipe(process.stdout) }).end() -/* - - -... -*/ -``` - -More options ------------- - -```javascript -// you can also pass AWS credentials in explicitly (otherwise taken from process.env) -aws4.sign(opts, {accessKeyId: '', secretAccessKey: ''}) - -// can also add the signature to query strings -aws4.sign({service: 's3', path: '/my-bucket?X-Amz-Expires=12345', signQuery: true}) - -// create a utility function to pipe to stdout (with https this time) -function request(o) { https.request(o, function(res) { res.pipe(process.stdout) }).end(o.body || '') } - -// aws4 can infer the HTTP method if a body is passed in -// method will be POST and Content-Type: 'application/x-www-form-urlencoded; charset=utf-8' -request(aws4.sign({service: 'iam', body: 'Action=ListGroups&Version=2010-05-08'})) -/* - -... -*/ - -// can specify any custom option or header as per usual -request(aws4.sign({ - service: 'dynamodb', - region: 'ap-southeast-2', - method: 'POST', - path: '/', - headers: { - 'Content-Type': 'application/x-amz-json-1.0', - 'X-Amz-Target': 'DynamoDB_20120810.ListTables' - }, - body: '{}' -})) -/* -{"TableNames":[]} -... -*/ - -// works with all other services that support Signature Version 4 - -request(aws4.sign({service: 's3', path: '/', signQuery: true})) -/* - -... -*/ - -request(aws4.sign({service: 'ec2', path: '/?Action=DescribeRegions&Version=2014-06-15'})) -/* - -... -*/ - -request(aws4.sign({service: 'sns', path: '/?Action=ListTopics&Version=2010-03-31'})) -/* - -... -*/ - -request(aws4.sign({service: 'sts', path: '/?Action=GetSessionToken&Version=2011-06-15'})) -/* - -... -*/ - -request(aws4.sign({service: 'cloudsearch', path: '/?Action=ListDomainNames&Version=2013-01-01'})) -/* - -... -*/ - -request(aws4.sign({service: 'ses', path: '/?Action=ListIdentities&Version=2010-12-01'})) -/* - -... -*/ - -request(aws4.sign({service: 'autoscaling', path: '/?Action=DescribeAutoScalingInstances&Version=2011-01-01'})) -/* - -... -*/ - -request(aws4.sign({service: 'elasticloadbalancing', path: '/?Action=DescribeLoadBalancers&Version=2012-06-01'})) -/* - -... -*/ - -request(aws4.sign({service: 'cloudformation', path: '/?Action=ListStacks&Version=2010-05-15'})) -/* - -... -*/ - -request(aws4.sign({service: 'elasticbeanstalk', path: '/?Action=ListAvailableSolutionStacks&Version=2010-12-01'})) -/* - -... -*/ - -request(aws4.sign({service: 'rds', path: '/?Action=DescribeDBInstances&Version=2012-09-17'})) -/* - -... -*/ - -request(aws4.sign({service: 'monitoring', path: '/?Action=ListMetrics&Version=2010-08-01'})) -/* - -... -*/ - -request(aws4.sign({service: 'redshift', path: '/?Action=DescribeClusters&Version=2012-12-01'})) -/* - -... -*/ - -request(aws4.sign({service: 'cloudfront', path: '/2014-05-31/distribution'})) -/* - -... -*/ - -request(aws4.sign({service: 'elasticache', path: '/?Action=DescribeCacheClusters&Version=2014-07-15'})) -/* - -... -*/ - -request(aws4.sign({service: 'elasticmapreduce', path: '/?Action=DescribeJobFlows&Version=2009-03-31'})) -/* - -... -*/ - -request(aws4.sign({service: 'route53', path: '/2013-04-01/hostedzone'})) -/* - -... -*/ - -request(aws4.sign({service: 'appstream', path: '/applications'})) -/* -{"_links":{"curie":[{"href":"http://docs.aws.amazon.com/appstream/latest/... -... -*/ - -request(aws4.sign({service: 'cognito-sync', path: '/identitypools'})) -/* -{"Count":0,"IdentityPoolUsages":[],"MaxResults":16,"NextToken":null} -... -*/ - -request(aws4.sign({service: 'elastictranscoder', path: '/2012-09-25/pipelines'})) -/* -{"NextPageToken":null,"Pipelines":[]} -... -*/ - -request(aws4.sign({service: 'lambda', path: '/2014-11-13/functions/'})) -/* -{"Functions":[],"NextMarker":null} -... -*/ - -request(aws4.sign({service: 'ecs', path: '/?Action=ListClusters&Version=2014-11-13'})) -/* - -... -*/ - -request(aws4.sign({service: 'glacier', path: '/-/vaults', headers: {'X-Amz-Glacier-Version': '2012-06-01'}})) -/* -{"Marker":null,"VaultList":[]} -... -*/ - -request(aws4.sign({service: 'storagegateway', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'StorageGateway_20120630.ListGateways' -}})) -/* -{"Gateways":[]} -... -*/ - -request(aws4.sign({service: 'datapipeline', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'DataPipeline.ListPipelines' -}})) -/* -{"hasMoreResults":false,"pipelineIdList":[]} -... -*/ - -request(aws4.sign({service: 'opsworks', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'OpsWorks_20130218.DescribeStacks' -}})) -/* -{"Stacks":[]} -... -*/ - -request(aws4.sign({service: 'route53domains', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'Route53Domains_v20140515.ListDomains' -}})) -/* -{"Domains":[]} -... -*/ - -request(aws4.sign({service: 'kinesis', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'Kinesis_20131202.ListStreams' -}})) -/* -{"HasMoreStreams":false,"StreamNames":[]} -... -*/ - -request(aws4.sign({service: 'cloudtrail', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'CloudTrail_20131101.DescribeTrails' -}})) -/* -{"trailList":[]} -... -*/ - -request(aws4.sign({service: 'logs', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'Logs_20140328.DescribeLogGroups' -}})) -/* -{"logGroups":[]} -... -*/ - -request(aws4.sign({service: 'codedeploy', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'CodeDeploy_20141006.ListApplications' -}})) -/* -{"applications":[]} -... -*/ - -request(aws4.sign({service: 'directconnect', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'OvertureService.DescribeConnections' -}})) -/* -{"connections":[]} -... -*/ - -request(aws4.sign({service: 'kms', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'TrentService.ListKeys' -}})) -/* -{"Keys":[],"Truncated":false} -... -*/ - -request(aws4.sign({service: 'config', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'StarlingDoveService.DescribeDeliveryChannels' -}})) -/* -{"DeliveryChannels":[]} -... -*/ - -request(aws4.sign({service: 'cloudhsm', body: '{}', headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'CloudHsmFrontendService.ListAvailableZones' -}})) -/* -{"AZList":["us-east-1a","us-east-1b","us-east-1c"]} -... -*/ - -request(aws4.sign({ - service: 'swf', - body: '{"registrationStatus":"REGISTERED"}', - headers: { - 'Content-Type': 'application/x-amz-json-1.0', - 'X-Amz-Target': 'SimpleWorkflowService.ListDomains' - } -})) -/* -{"domainInfos":[]} -... -*/ - -request(aws4.sign({ - service: 'cognito-identity', - body: '{"MaxResults": 1}', - headers: { - 'Content-Type': 'application/x-amz-json-1.1', - 'X-Amz-Target': 'AWSCognitoIdentityService.ListIdentityPools' - } -})) -/* -{"IdentityPools":[]} -... -*/ - -request(aws4.sign({ - service: 'mobileanalytics', - path: '/2014-06-05/events', - body: JSON.stringify({events:[{ - eventType: 'a', - timestamp: new Date().toISOString(), - session: {}, - }]}), - headers: { - 'Content-Type': 'application/json', - 'X-Amz-Client-Context': JSON.stringify({ - client: {client_id: 'a', app_title: 'a'}, - custom: {}, - env: {platform: 'a'}, - services: {}, - }), - } -})) -/* -(HTTP 202, empty response) -*/ -``` - -API ---- - -### aws4.sign(requestOptions, [credentials]) - -This calculates and populates the `Authorization` header of -`requestOptions`, and any other necessary AWS headers and/or request -options. Returns `requestOptions` as a convenience for chaining. - -`requestOptions` is an object holding the same options that the node.js -[http.request](http://nodejs.org/docs/latest/api/http.html#http_http_request_options_callback) -function takes. - -The following properties of `requestOptions` are used in the signing or -populated if they don't already exist: - -- `hostname` or `host` (will be determined from `service` and `region` if not given) -- `method` (will use `'GET'` if not given or `'POST'` if there is a `body`) -- `path` (will use `'/'` if not given) -- `body` (will use `''` if not given) -- `service` (will be calculated from `hostname` or `host` if not given) -- `region` (will be calculated from `hostname` or `host` or use `'us-east-1'` if not given) -- `headers['Host']` (will use `hostname` or `host` or be calculated if not given) -- `headers['Content-Type']` (will use `'application/x-www-form-urlencoded; charset=utf-8'` - if not given and there is a `body`) -- `headers['Date']` (used to calculate the signature date if given, otherwise `new Date` is used) - -Your AWS credentials (which can be found in your -[AWS console](https://portal.aws.amazon.com/gp/aws/securityCredentials)) -can be specified in one of two ways: - -- As the second argument, like this: - -```javascript -aws4.sign(requestOptions, { - secretAccessKey: "", - accessKeyId: "", - sessionToken: "" -}) -``` - -- From `process.env`, such as this: - -``` -export AWS_SECRET_ACCESS_KEY="" -export AWS_ACCESS_KEY_ID="" -export AWS_SESSION_TOKEN="" -``` - -(will also use `AWS_ACCESS_KEY` and `AWS_SECRET_KEY` if available) - -The `sessionToken` property and `AWS_SESSION_TOKEN` environment variable are optional for signing -with [IAM STS temporary credentials](http://docs.aws.amazon.com/STS/latest/UsingSTS/using-temp-creds.html). - -Installation ------------- - -With [npm](http://npmjs.org/) do: - -``` -npm install aws4 -``` - -Can also be used [in the browser](./browser). - -Thanks ------- - -Thanks to [@jed](https://github.com/jed) for his -[dynamo-client](https://github.com/jed/dynamo-client) lib where I first -committed and subsequently extracted this code. - -Also thanks to the -[official node.js AWS SDK](https://github.com/aws/aws-sdk-js) for giving -me a start on implementing the v4 signature. - diff --git a/node_modules/request/node_modules/aws4/aws4.js b/node_modules/request/node_modules/aws4/aws4.js deleted file mode 100644 index cbe5dc90..00000000 --- a/node_modules/request/node_modules/aws4/aws4.js +++ /dev/null @@ -1,318 +0,0 @@ -var aws4 = exports, - url = require('url'), - querystring = require('querystring'), - crypto = require('crypto'), - lru = require('./lru'), - credentialsCache = lru(1000) - -// http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html - -function hmac(key, string, encoding) { - return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding) -} - -function hash(string, encoding) { - return crypto.createHash('sha256').update(string, 'utf8').digest(encoding) -} - -// This function assumes the string has already been percent encoded -function encodeRfc3986(urlEncodedString) { - return urlEncodedString.replace(/[!'()*]/g, function(c) { - return '%' + c.charCodeAt(0).toString(16).toUpperCase() - }) -} - -// request: { path | body, [host], [method], [headers], [service], [region] } -// credentials: { accessKeyId, secretAccessKey, [sessionToken] } -function RequestSigner(request, credentials) { - - if (typeof request === 'string') request = url.parse(request) - - var headers = request.headers = (request.headers || {}), - hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers.host) - - this.request = request - this.credentials = credentials || this.defaultCredentials() - - this.service = request.service || hostParts[0] || '' - this.region = request.region || hostParts[1] || 'us-east-1' - - // SES uses a different domain from the service name - if (this.service === 'email') this.service = 'ses' - - if (!request.method && request.body) - request.method = 'POST' - - if (!headers.Host && !headers.host) { - headers.Host = request.hostname || request.host || this.createHost() - - // If a port is specified explicitly, use it as is - if (request.port) - headers.Host += ':' + request.port - } - if (!request.hostname && !request.host) - request.hostname = headers.Host || headers.host -} - -RequestSigner.prototype.matchHost = function(host) { - var match = (host || '').match(/([^\.]+)\.(?:([^\.]*)\.)?amazonaws\.com$/) - var hostParts = (match || []).slice(1, 3) - - // ES's hostParts are sometimes the other way round, if the value that is expected - // to be region equals ‘es’ switch them back - // e.g. search-cluster-name-aaaa00aaaa0aaa0aaaaaaa0aaa.us-east-1.es.amazonaws.com - if (hostParts[1] === 'es') - hostParts = hostParts.reverse() - - return hostParts -} - -// http://docs.aws.amazon.com/general/latest/gr/rande.html -RequestSigner.prototype.isSingleRegion = function() { - // Special case for S3 and SimpleDB in us-east-1 - if (['s3', 'sdb'].indexOf(this.service) >= 0 && this.region === 'us-east-1') return true - - return ['cloudfront', 'ls', 'route53', 'iam', 'importexport', 'sts'] - .indexOf(this.service) >= 0 -} - -RequestSigner.prototype.createHost = function() { - var region = this.isSingleRegion() ? '' : - (this.service === 's3' && this.region !== 'us-east-1' ? '-' : '.') + this.region, - service = this.service === 'ses' ? 'email' : this.service - return service + region + '.amazonaws.com' -} - -RequestSigner.prototype.prepareRequest = function() { - this.parsePath() - - var request = this.request, headers = request.headers, query - - if (request.signQuery) { - - this.parsedPath.query = query = this.parsedPath.query || {} - - if (this.credentials.sessionToken) - query['X-Amz-Security-Token'] = this.credentials.sessionToken - - if (this.service === 's3' && !query['X-Amz-Expires']) - query['X-Amz-Expires'] = 86400 - - if (query['X-Amz-Date']) - this.datetime = query['X-Amz-Date'] - else - query['X-Amz-Date'] = this.getDateTime() - - query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256' - query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString() - query['X-Amz-SignedHeaders'] = this.signedHeaders() - - } else { - - if (!request.doNotModifyHeaders) { - if (request.body && !headers['Content-Type'] && !headers['content-type']) - headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8' - - if (request.body && !headers['Content-Length'] && !headers['content-length']) - headers['Content-Length'] = Buffer.byteLength(request.body) - - if (this.credentials.sessionToken) - headers['X-Amz-Security-Token'] = this.credentials.sessionToken - - if (this.service === 's3') - headers['X-Amz-Content-Sha256'] = hash(this.request.body || '', 'hex') - - if (headers['X-Amz-Date']) - this.datetime = headers['X-Amz-Date'] - else - headers['X-Amz-Date'] = this.getDateTime() - } - - delete headers.Authorization - delete headers.authorization - } -} - -RequestSigner.prototype.sign = function() { - if (!this.parsedPath) this.prepareRequest() - - if (this.request.signQuery) { - this.parsedPath.query['X-Amz-Signature'] = this.signature() - } else { - this.request.headers.Authorization = this.authHeader() - } - - this.request.path = this.formatPath() - - return this.request -} - -RequestSigner.prototype.getDateTime = function() { - if (!this.datetime) { - var headers = this.request.headers, - date = new Date(headers.Date || headers.date || new Date) - - this.datetime = date.toISOString().replace(/[:\-]|\.\d{3}/g, '') - } - return this.datetime -} - -RequestSigner.prototype.getDate = function() { - return this.getDateTime().substr(0, 8) -} - -RequestSigner.prototype.authHeader = function() { - return [ - 'AWS4-HMAC-SHA256 Credential=' + this.credentials.accessKeyId + '/' + this.credentialString(), - 'SignedHeaders=' + this.signedHeaders(), - 'Signature=' + this.signature(), - ].join(', ') -} - -RequestSigner.prototype.signature = function() { - var date = this.getDate(), - cacheKey = [this.credentials.secretAccessKey, date, this.region, this.service].join(), - kDate, kRegion, kService, kCredentials = credentialsCache.get(cacheKey) - if (!kCredentials) { - kDate = hmac('AWS4' + this.credentials.secretAccessKey, date) - kRegion = hmac(kDate, this.region) - kService = hmac(kRegion, this.service) - kCredentials = hmac(kService, 'aws4_request') - credentialsCache.set(cacheKey, kCredentials) - } - return hmac(kCredentials, this.stringToSign(), 'hex') -} - -RequestSigner.prototype.stringToSign = function() { - return [ - 'AWS4-HMAC-SHA256', - this.getDateTime(), - this.credentialString(), - hash(this.canonicalString(), 'hex'), - ].join('\n') -} - -RequestSigner.prototype.canonicalString = function() { - if (!this.parsedPath) this.prepareRequest() - - var pathStr = this.parsedPath.path, - query = this.parsedPath.query, - queryStr = '', - normalizePath = this.service !== 's3', - decodePath = this.service === 's3' || this.request.doNotEncodePath, - decodeSlashesInPath = this.service === 's3', - firstValOnly = this.service === 's3', - bodyHash = this.service === 's3' && this.request.signQuery ? - 'UNSIGNED-PAYLOAD' : hash(this.request.body || '', 'hex') - - if (query) { - queryStr = encodeRfc3986(querystring.stringify(Object.keys(query).sort().reduce(function(obj, key) { - if (!key) return obj - obj[key] = !Array.isArray(query[key]) ? query[key] : - (firstValOnly ? query[key][0] : query[key].slice().sort()) - return obj - }, {}))) - } - if (pathStr !== '/') { - if (normalizePath) pathStr = pathStr.replace(/\/{2,}/g, '/') - pathStr = pathStr.split('/').reduce(function(path, piece) { - if (normalizePath && piece === '..') { - path.pop() - } else if (!normalizePath || piece !== '.') { - if (decodePath) piece = querystring.unescape(piece) - path.push(encodeRfc3986(querystring.escape(piece))) - } - return path - }, []).join('/') - if (pathStr[0] !== '/') pathStr = '/' + pathStr - if (decodeSlashesInPath) pathStr = pathStr.replace(/%2F/g, '/') - } - - return [ - this.request.method || 'GET', - pathStr, - queryStr, - this.canonicalHeaders() + '\n', - this.signedHeaders(), - bodyHash, - ].join('\n') -} - -RequestSigner.prototype.canonicalHeaders = function() { - var headers = this.request.headers - function trimAll(header) { - return header.toString().trim().replace(/\s+/g, ' ') - } - return Object.keys(headers) - .sort(function(a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1 }) - .map(function(key) { return key.toLowerCase() + ':' + trimAll(headers[key]) }) - .join('\n') -} - -RequestSigner.prototype.signedHeaders = function() { - return Object.keys(this.request.headers) - .map(function(key) { return key.toLowerCase() }) - .sort() - .join(';') -} - -RequestSigner.prototype.credentialString = function() { - return [ - this.getDate(), - this.region, - this.service, - 'aws4_request', - ].join('/') -} - -RequestSigner.prototype.defaultCredentials = function() { - var env = process.env - return { - accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY, - secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY, - sessionToken: env.AWS_SESSION_TOKEN, - } -} - -RequestSigner.prototype.parsePath = function() { - var path = this.request.path || '/', - queryIx = path.indexOf('?'), - query = null - - if (queryIx >= 0) { - query = querystring.parse(path.slice(queryIx + 1)) - path = path.slice(0, queryIx) - } - - // S3 doesn't always encode characters > 127 correctly and - // all services don't encode characters > 255 correctly - // So if there are non-reserved chars (and it's not already all % encoded), just encode them all - if (/[^0-9A-Za-z!'()*\-._~%/]/.test(path)) { - path = path.split('/').map(function(piece) { - return querystring.escape(querystring.unescape(piece)) - }).join('/') - } - - this.parsedPath = { - path: path, - query: query, - } -} - -RequestSigner.prototype.formatPath = function() { - var path = this.parsedPath.path, - query = this.parsedPath.query - - if (!query) return path - - // Services don't support empty query string keys - if (query[''] != null) delete query[''] - - return path + '?' + encodeRfc3986(querystring.stringify(query)) -} - -aws4.RequestSigner = RequestSigner - -aws4.sign = function(request, credentials) { - return new RequestSigner(request, credentials).sign() -} diff --git a/node_modules/request/node_modules/aws4/lru.js b/node_modules/request/node_modules/aws4/lru.js deleted file mode 100644 index 333f66a4..00000000 --- a/node_modules/request/node_modules/aws4/lru.js +++ /dev/null @@ -1,96 +0,0 @@ -module.exports = function(size) { - return new LruCache(size) -} - -function LruCache(size) { - this.capacity = size | 0 - this.map = Object.create(null) - this.list = new DoublyLinkedList() -} - -LruCache.prototype.get = function(key) { - var node = this.map[key] - if (node == null) return undefined - this.used(node) - return node.val -} - -LruCache.prototype.set = function(key, val) { - var node = this.map[key] - if (node != null) { - node.val = val - } else { - if (!this.capacity) this.prune() - if (!this.capacity) return false - node = new DoublyLinkedNode(key, val) - this.map[key] = node - this.capacity-- - } - this.used(node) - return true -} - -LruCache.prototype.used = function(node) { - this.list.moveToFront(node) -} - -LruCache.prototype.prune = function() { - var node = this.list.pop() - if (node != null) { - delete this.map[node.key] - this.capacity++ - } -} - - -function DoublyLinkedList() { - this.firstNode = null - this.lastNode = null -} - -DoublyLinkedList.prototype.moveToFront = function(node) { - if (this.firstNode == node) return - - this.remove(node) - - if (this.firstNode == null) { - this.firstNode = node - this.lastNode = node - node.prev = null - node.next = null - } else { - node.prev = null - node.next = this.firstNode - node.next.prev = node - this.firstNode = node - } -} - -DoublyLinkedList.prototype.pop = function() { - var lastNode = this.lastNode - if (lastNode != null) { - this.remove(lastNode) - } - return lastNode -} - -DoublyLinkedList.prototype.remove = function(node) { - if (this.firstNode == node) { - this.firstNode = node.next - } else if (node.prev != null) { - node.prev.next = node.next - } - if (this.lastNode == node) { - this.lastNode = node.prev - } else if (node.next != null) { - node.next.prev = node.prev - } -} - - -function DoublyLinkedNode(key, val) { - this.key = key - this.val = val - this.prev = null - this.next = null -} diff --git a/node_modules/request/node_modules/aws4/package.json b/node_modules/request/node_modules/aws4/package.json deleted file mode 100644 index d643c933..00000000 --- a/node_modules/request/node_modules/aws4/package.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "name": "aws4", - "version": "1.4.1", - "description": "Signs and prepares requests using AWS Signature Version 4", - "author": { - "name": "Michael Hart", - "email": "michael.hart.au@gmail.com", - "url": "http://github.com/mhart" - }, - "main": "aws4.js", - "keywords": [ - "amazon", - "aws", - "signature", - "s3", - "ec2", - "autoscaling", - "cloudformation", - "elasticloadbalancing", - "elb", - "elasticbeanstalk", - "cloudsearch", - "dynamodb", - "kinesis", - "lambda", - "glacier", - "sqs", - "sns", - "iam", - "sts", - "ses", - "swf", - "storagegateway", - "datapipeline", - "directconnect", - "redshift", - "opsworks", - "rds", - "monitoring", - "cloudtrail", - "cloudfront", - "codedeploy", - "elasticache", - "elasticmapreduce", - "elastictranscoder", - "emr", - "cloudwatch", - "mobileanalytics", - "cognitoidentity", - "cognitosync", - "cognito", - "containerservice", - "ecs", - "appstream", - "keymanagementservice", - "kms", - "config", - "cloudhsm", - "route53", - "route53domains", - "logs" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/mhart/aws4.git" - }, - "license": "MIT", - "devDependencies": { - "mocha": "^2.4.5", - "should": "^8.2.2" - }, - "scripts": { - "test": "mocha ./test/fast.js ./test/slow.js -b -t 100s -R list" - }, - "gitHead": "f126d3ff80be1ddde0fc6b50bb51a7f199547e81", - "bugs": { - "url": "https://github.com/mhart/aws4/issues" - }, - "homepage": "https://github.com/mhart/aws4#readme", - "_id": "aws4@1.4.1", - "_shasum": "fde7d5292466d230e5ee0f4e038d9dfaab08fc61", - "_from": "aws4@>=1.2.1 <2.0.0", - "_npmVersion": "2.15.4", - "_nodeVersion": "4.4.3", - "_npmUser": { - "name": "hichaelmart", - "email": "michael.hart.au@gmail.com" - }, - "maintainers": [ - { - "name": "hichaelmart", - "email": "michael.hart.au@gmail.com" - } - ], - "dist": { - "shasum": "fde7d5292466d230e5ee0f4e038d9dfaab08fc61", - "tarball": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz" - }, - "_npmOperationalInternal": { - "host": "packages-12-west.internal.npmjs.com", - "tmp": "tmp/aws4-1.4.1.tgz_1462643218465_0.6527479749638587" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/bl/.jshintrc b/node_modules/request/node_modules/bl/.jshintrc deleted file mode 100644 index c8ef3ca4..00000000 --- a/node_modules/request/node_modules/bl/.jshintrc +++ /dev/null @@ -1,59 +0,0 @@ -{ - "predef": [ ] - , "bitwise": false - , "camelcase": false - , "curly": false - , "eqeqeq": false - , "forin": false - , "immed": false - , "latedef": false - , "noarg": true - , "noempty": true - , "nonew": true - , "plusplus": false - , "quotmark": true - , "regexp": false - , "undef": true - , "unused": true - , "strict": false - , "trailing": true - , "maxlen": 120 - , "asi": true - , "boss": true - , "debug": true - , "eqnull": true - , "esnext": true - , "evil": true - , "expr": true - , "funcscope": false - , "globalstrict": false - , "iterator": false - , "lastsemic": true - , "laxbreak": true - , "laxcomma": true - , "loopfunc": true - , "multistr": false - , "onecase": false - , "proto": false - , "regexdash": false - , "scripturl": true - , "smarttabs": false - , "shadow": false - , "sub": true - , "supernew": false - , "validthis": true - , "browser": true - , "couch": false - , "devel": false - , "dojo": false - , "mootools": false - , "node": true - , "nonstandard": true - , "prototypejs": false - , "rhino": false - , "worker": true - , "wsh": false - , "nomen": false - , "onevar": false - , "passfail": false -} \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/.npmignore b/node_modules/request/node_modules/bl/.npmignore deleted file mode 100644 index 40b878db..00000000 --- a/node_modules/request/node_modules/bl/.npmignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/.travis.yml b/node_modules/request/node_modules/bl/.travis.yml deleted file mode 100644 index 5cb0480b..00000000 --- a/node_modules/request/node_modules/bl/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -sudo: false -language: node_js -node_js: - - '0.10' - - '0.12' - - '4' - - '5' -branches: - only: - - master -notifications: - email: - - rod@vagg.org diff --git a/node_modules/request/node_modules/bl/LICENSE.md b/node_modules/request/node_modules/bl/LICENSE.md deleted file mode 100644 index ccb24797..00000000 --- a/node_modules/request/node_modules/bl/LICENSE.md +++ /dev/null @@ -1,13 +0,0 @@ -The MIT License (MIT) -===================== - -Copyright (c) 2014 bl contributors ----------------------------------- - -*bl contributors listed at * - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/bl/README.md b/node_modules/request/node_modules/bl/README.md deleted file mode 100644 index f7044db2..00000000 --- a/node_modules/request/node_modules/bl/README.md +++ /dev/null @@ -1,200 +0,0 @@ -# bl *(BufferList)* - -[![Build Status](https://travis-ci.org/rvagg/bl.svg?branch=master)](https://travis-ci.org/rvagg/bl) - -**A Node.js Buffer list collector, reader and streamer thingy.** - -[![NPM](https://nodei.co/npm/bl.png?downloads=true&downloadRank=true)](https://nodei.co/npm/bl/) -[![NPM](https://nodei.co/npm-dl/bl.png?months=6&height=3)](https://nodei.co/npm/bl/) - -**bl** is a storage object for collections of Node Buffers, exposing them with the main Buffer readable API. Also works as a duplex stream so you can collect buffers from a stream that emits them and emit buffers to a stream that consumes them! - -The original buffers are kept intact and copies are only done as necessary. Any reads that require the use of a single original buffer will return a slice of that buffer only (which references the same memory as the original buffer). Reads that span buffers perform concatenation as required and return the results transparently. - -```js -const BufferList = require('bl') - -var bl = new BufferList() -bl.append(new Buffer('abcd')) -bl.append(new Buffer('efg')) -bl.append('hi') // bl will also accept & convert Strings -bl.append(new Buffer('j')) -bl.append(new Buffer([ 0x3, 0x4 ])) - -console.log(bl.length) // 12 - -console.log(bl.slice(0, 10).toString('ascii')) // 'abcdefghij' -console.log(bl.slice(3, 10).toString('ascii')) // 'defghij' -console.log(bl.slice(3, 6).toString('ascii')) // 'def' -console.log(bl.slice(3, 8).toString('ascii')) // 'defgh' -console.log(bl.slice(5, 10).toString('ascii')) // 'fghij' - -// or just use toString! -console.log(bl.toString()) // 'abcdefghij\u0003\u0004' -console.log(bl.toString('ascii', 3, 8)) // 'defgh' -console.log(bl.toString('ascii', 5, 10)) // 'fghij' - -// other standard Buffer readables -console.log(bl.readUInt16BE(10)) // 0x0304 -console.log(bl.readUInt16LE(10)) // 0x0403 -``` - -Give it a callback in the constructor and use it just like **[concat-stream](https://github.com/maxogden/node-concat-stream)**: - -```js -const bl = require('bl') - , fs = require('fs') - -fs.createReadStream('README.md') - .pipe(bl(function (err, data) { // note 'new' isn't strictly required - // `data` is a complete Buffer object containing the full data - console.log(data.toString()) - })) -``` - -Note that when you use the *callback* method like this, the resulting `data` parameter is a concatenation of all `Buffer` objects in the list. If you want to avoid the overhead of this concatenation (in cases of extreme performance consciousness), then avoid the *callback* method and just listen to `'end'` instead, like a standard Stream. - -Or to fetch a URL using [hyperquest](https://github.com/substack/hyperquest) (should work with [request](http://github.com/mikeal/request) and even plain Node http too!): -```js -const hyperquest = require('hyperquest') - , bl = require('bl') - , url = 'https://raw.github.com/rvagg/bl/master/README.md' - -hyperquest(url).pipe(bl(function (err, data) { - console.log(data.toString()) -})) -``` - -Or, use it as a readable stream to recompose a list of Buffers to an output source: - -```js -const BufferList = require('bl') - , fs = require('fs') - -var bl = new BufferList() -bl.append(new Buffer('abcd')) -bl.append(new Buffer('efg')) -bl.append(new Buffer('hi')) -bl.append(new Buffer('j')) - -bl.pipe(fs.createWriteStream('gibberish.txt')) -``` - -## API - - * new BufferList([ callback ]) - * bl.length - * bl.append(buffer) - * bl.get(index) - * bl.slice([ start[, end ] ]) - * bl.copy(dest, [ destStart, [ srcStart [, srcEnd ] ] ]) - * bl.duplicate() - * bl.consume(bytes) - * bl.toString([encoding, [ start, [ end ]]]) - * bl.readDoubleBE(), bl.readDoubleLE(), bl.readFloatBE(), bl.readFloatLE(), bl.readInt32BE(), bl.readInt32LE(), bl.readUInt32BE(), bl.readUInt32LE(), bl.readInt16BE(), bl.readInt16LE(), bl.readUInt16BE(), bl.readUInt16LE(), bl.readInt8(), bl.readUInt8() - * Streams - --------------------------------------------------------- - -### new BufferList([ callback | Buffer | Buffer array | BufferList | BufferList array | String ]) -The constructor takes an optional callback, if supplied, the callback will be called with an error argument followed by a reference to the **bl** instance, when `bl.end()` is called (i.e. from a piped stream). This is a convenient method of collecting the entire contents of a stream, particularly when the stream is *chunky*, such as a network stream. - -Normally, no arguments are required for the constructor, but you can initialise the list by passing in a single `Buffer` object or an array of `Buffer` object. - -`new` is not strictly required, if you don't instantiate a new object, it will be done automatically for you so you can create a new instance simply with: - -```js -var bl = require('bl') -var myinstance = bl() - -// equivilant to: - -var BufferList = require('bl') -var myinstance = new BufferList() -``` - --------------------------------------------------------- - -### bl.length -Get the length of the list in bytes. This is the sum of the lengths of all of the buffers contained in the list, minus any initial offset for a semi-consumed buffer at the beginning. Should accurately represent the total number of bytes that can be read from the list. - --------------------------------------------------------- - -### bl.append(Buffer | Buffer array | BufferList | BufferList array | String) -`append(buffer)` adds an additional buffer or BufferList to the internal list. `this` is returned so it can be chained. - --------------------------------------------------------- - -### bl.get(index) -`get()` will return the byte at the specified index. - --------------------------------------------------------- - -### bl.slice([ start, [ end ] ]) -`slice()` returns a new `Buffer` object containing the bytes within the range specified. Both `start` and `end` are optional and will default to the beginning and end of the list respectively. - -If the requested range spans a single internal buffer then a slice of that buffer will be returned which shares the original memory range of that Buffer. If the range spans multiple buffers then copy operations will likely occur to give you a uniform Buffer. - --------------------------------------------------------- - -### bl.copy(dest, [ destStart, [ srcStart [, srcEnd ] ] ]) -`copy()` copies the content of the list in the `dest` buffer, starting from `destStart` and containing the bytes within the range specified with `srcStart` to `srcEnd`. `destStart`, `start` and `end` are optional and will default to the beginning of the `dest` buffer, and the beginning and end of the list respectively. - --------------------------------------------------------- - -### bl.duplicate() -`duplicate()` performs a **shallow-copy** of the list. The internal Buffers remains the same, so if you change the underlying Buffers, the change will be reflected in both the original and the duplicate. This method is needed if you want to call `consume()` or `pipe()` and still keep the original list.Example: - -```js -var bl = new BufferList() - -bl.append('hello') -bl.append(' world') -bl.append('\n') - -bl.duplicate().pipe(process.stdout, { end: false }) - -console.log(bl.toString()) -``` - --------------------------------------------------------- - -### bl.consume(bytes) -`consume()` will shift bytes *off the start of the list*. The number of bytes consumed don't need to line up with the sizes of the internal Buffers—initial offsets will be calculated accordingly in order to give you a consistent view of the data. - --------------------------------------------------------- - -### bl.toString([encoding, [ start, [ end ]]]) -`toString()` will return a string representation of the buffer. The optional `start` and `end` arguments are passed on to `slice()`, while the `encoding` is passed on to `toString()` of the resulting Buffer. See the [Buffer#toString()](http://nodejs.org/docs/latest/api/buffer.html#buffer_buf_tostring_encoding_start_end) documentation for more information. - --------------------------------------------------------- - -### bl.readDoubleBE(), bl.readDoubleLE(), bl.readFloatBE(), bl.readFloatLE(), bl.readInt32BE(), bl.readInt32LE(), bl.readUInt32BE(), bl.readUInt32LE(), bl.readInt16BE(), bl.readInt16LE(), bl.readUInt16BE(), bl.readUInt16LE(), bl.readInt8(), bl.readUInt8() - -All of the standard byte-reading methods of the `Buffer` interface are implemented and will operate across internal Buffer boundaries transparently. - -See the [Buffer](http://nodejs.org/docs/latest/api/buffer.html) documentation for how these work. - --------------------------------------------------------- - -### Streams -**bl** is a Node **[Duplex Stream](http://nodejs.org/docs/latest/api/stream.html#stream_class_stream_duplex)**, so it can be read from and written to like a standard Node stream. You can also `pipe()` to and from a **bl** instance. - --------------------------------------------------------- - -## Contributors - -**bl** is brought to you by the following hackers: - - * [Rod Vagg](https://github.com/rvagg) - * [Matteo Collina](https://github.com/mcollina) - * [Jarett Cruger](https://github.com/jcrugzz) - -======= - - -## License & copyright - -Copyright (c) 2013-2014 bl contributors (listed above). - -bl is licensed under the MIT license. All rights not explicitly granted in the MIT license are reserved. See the included LICENSE.md file for more details. diff --git a/node_modules/request/node_modules/bl/bl.js b/node_modules/request/node_modules/bl/bl.js deleted file mode 100644 index f585df17..00000000 --- a/node_modules/request/node_modules/bl/bl.js +++ /dev/null @@ -1,243 +0,0 @@ -var DuplexStream = require('readable-stream/duplex') - , util = require('util') - - -function BufferList (callback) { - if (!(this instanceof BufferList)) - return new BufferList(callback) - - this._bufs = [] - this.length = 0 - - if (typeof callback == 'function') { - this._callback = callback - - var piper = function piper (err) { - if (this._callback) { - this._callback(err) - this._callback = null - } - }.bind(this) - - this.on('pipe', function onPipe (src) { - src.on('error', piper) - }) - this.on('unpipe', function onUnpipe (src) { - src.removeListener('error', piper) - }) - } else { - this.append(callback) - } - - DuplexStream.call(this) -} - - -util.inherits(BufferList, DuplexStream) - - -BufferList.prototype._offset = function _offset (offset) { - var tot = 0, i = 0, _t - for (; i < this._bufs.length; i++) { - _t = tot + this._bufs[i].length - if (offset < _t) - return [ i, offset - tot ] - tot = _t - } -} - - -BufferList.prototype.append = function append (buf) { - var i = 0 - , newBuf - - if (Array.isArray(buf)) { - for (; i < buf.length; i++) - this.append(buf[i]) - } else if (buf instanceof BufferList) { - // unwrap argument into individual BufferLists - for (; i < buf._bufs.length; i++) - this.append(buf._bufs[i]) - } else if (buf != null) { - // coerce number arguments to strings, since Buffer(number) does - // uninitialized memory allocation - if (typeof buf == 'number') - buf = buf.toString() - - newBuf = Buffer.isBuffer(buf) ? buf : new Buffer(buf) - this._bufs.push(newBuf) - this.length += newBuf.length - } - - return this -} - - -BufferList.prototype._write = function _write (buf, encoding, callback) { - this.append(buf) - - if (typeof callback == 'function') - callback() -} - - -BufferList.prototype._read = function _read (size) { - if (!this.length) - return this.push(null) - - size = Math.min(size, this.length) - this.push(this.slice(0, size)) - this.consume(size) -} - - -BufferList.prototype.end = function end (chunk) { - DuplexStream.prototype.end.call(this, chunk) - - if (this._callback) { - this._callback(null, this.slice()) - this._callback = null - } -} - - -BufferList.prototype.get = function get (index) { - return this.slice(index, index + 1)[0] -} - - -BufferList.prototype.slice = function slice (start, end) { - return this.copy(null, 0, start, end) -} - - -BufferList.prototype.copy = function copy (dst, dstStart, srcStart, srcEnd) { - if (typeof srcStart != 'number' || srcStart < 0) - srcStart = 0 - if (typeof srcEnd != 'number' || srcEnd > this.length) - srcEnd = this.length - if (srcStart >= this.length) - return dst || new Buffer(0) - if (srcEnd <= 0) - return dst || new Buffer(0) - - var copy = !!dst - , off = this._offset(srcStart) - , len = srcEnd - srcStart - , bytes = len - , bufoff = (copy && dstStart) || 0 - , start = off[1] - , l - , i - - // copy/slice everything - if (srcStart === 0 && srcEnd == this.length) { - if (!copy) // slice, just return a full concat - return Buffer.concat(this._bufs) - - // copy, need to copy individual buffers - for (i = 0; i < this._bufs.length; i++) { - this._bufs[i].copy(dst, bufoff) - bufoff += this._bufs[i].length - } - - return dst - } - - // easy, cheap case where it's a subset of one of the buffers - if (bytes <= this._bufs[off[0]].length - start) { - return copy - ? this._bufs[off[0]].copy(dst, dstStart, start, start + bytes) - : this._bufs[off[0]].slice(start, start + bytes) - } - - if (!copy) // a slice, we need something to copy in to - dst = new Buffer(len) - - for (i = off[0]; i < this._bufs.length; i++) { - l = this._bufs[i].length - start - - if (bytes > l) { - this._bufs[i].copy(dst, bufoff, start) - } else { - this._bufs[i].copy(dst, bufoff, start, start + bytes) - break - } - - bufoff += l - bytes -= l - - if (start) - start = 0 - } - - return dst -} - -BufferList.prototype.toString = function toString (encoding, start, end) { - return this.slice(start, end).toString(encoding) -} - -BufferList.prototype.consume = function consume (bytes) { - while (this._bufs.length) { - if (bytes >= this._bufs[0].length) { - bytes -= this._bufs[0].length - this.length -= this._bufs[0].length - this._bufs.shift() - } else { - this._bufs[0] = this._bufs[0].slice(bytes) - this.length -= bytes - break - } - } - return this -} - - -BufferList.prototype.duplicate = function duplicate () { - var i = 0 - , copy = new BufferList() - - for (; i < this._bufs.length; i++) - copy.append(this._bufs[i]) - - return copy -} - - -BufferList.prototype.destroy = function destroy () { - this._bufs.length = 0 - this.length = 0 - this.push(null) -} - - -;(function () { - var methods = { - 'readDoubleBE' : 8 - , 'readDoubleLE' : 8 - , 'readFloatBE' : 4 - , 'readFloatLE' : 4 - , 'readInt32BE' : 4 - , 'readInt32LE' : 4 - , 'readUInt32BE' : 4 - , 'readUInt32LE' : 4 - , 'readInt16BE' : 2 - , 'readInt16LE' : 2 - , 'readUInt16BE' : 2 - , 'readUInt16LE' : 2 - , 'readInt8' : 1 - , 'readUInt8' : 1 - } - - for (var m in methods) { - (function (m) { - BufferList.prototype[m] = function (offset) { - return this.slice(offset, offset + methods[m])[m](0) - } - }(m)) - } -}()) - - -module.exports = BufferList diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/.npmignore b/node_modules/request/node_modules/bl/node_modules/readable-stream/.npmignore deleted file mode 100644 index 38344f87..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -build/ -test/ -examples/ -fs.js -zlib.js \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/.travis.yml b/node_modules/request/node_modules/bl/node_modules/readable-stream/.travis.yml deleted file mode 100644 index 1b821184..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/.travis.yml +++ /dev/null @@ -1,52 +0,0 @@ -sudo: false -language: node_js -before_install: - - npm install -g npm@2 - - npm install -g npm -notifications: - email: false -matrix: - fast_finish: true - allow_failures: - - env: TASK=browser BROWSER_NAME=ipad BROWSER_VERSION="6.0..latest" - - env: TASK=browser BROWSER_NAME=iphone BROWSER_VERSION="6.0..latest" - include: - - node_js: '0.8' - env: TASK=test - - node_js: '0.10' - env: TASK=test - - node_js: '0.11' - env: TASK=test - - node_js: '0.12' - env: TASK=test - - node_js: 1 - env: TASK=test - - node_js: 2 - env: TASK=test - - node_js: 3 - env: TASK=test - - node_js: 4 - env: TASK=test - - node_js: 5 - env: TASK=test - - node_js: 5 - env: TASK=browser BROWSER_NAME=android BROWSER_VERSION="4.0..latest" - - node_js: 5 - env: TASK=browser BROWSER_NAME=ie BROWSER_VERSION="9..latest" - - node_js: 5 - env: TASK=browser BROWSER_NAME=opera BROWSER_VERSION="11..latest" - - node_js: 5 - env: TASK=browser BROWSER_NAME=chrome BROWSER_VERSION="-3..latest" - - node_js: 5 - env: TASK=browser BROWSER_NAME=firefox BROWSER_VERSION="-3..latest" - - node_js: 5 - env: TASK=browser BROWSER_NAME=ipad BROWSER_VERSION="6.0..latest" - - node_js: 5 - env: TASK=browser BROWSER_NAME=iphone BROWSER_VERSION="6.0..latest" - - node_js: 5 - env: TASK=browser BROWSER_NAME=safari BROWSER_VERSION="5..latest" -script: "npm run $TASK" -env: - global: - - secure: rE2Vvo7vnjabYNULNyLFxOyt98BoJexDqsiOnfiD6kLYYsiQGfr/sbZkPMOFm9qfQG7pjqx+zZWZjGSswhTt+626C0t/njXqug7Yps4c3dFblzGfreQHp7wNX5TFsvrxd6dAowVasMp61sJcRnB2w8cUzoe3RAYUDHyiHktwqMc= - - secure: g9YINaKAdMatsJ28G9jCGbSaguXCyxSTy+pBO6Ch0Cf57ZLOTka3HqDj8p3nV28LUIHZ3ut5WO43CeYKwt4AUtLpBS3a0dndHdY6D83uY6b2qh5hXlrcbeQTq2cvw2y95F7hm4D1kwrgZ7ViqaKggRcEupAL69YbJnxeUDKWEdI= diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/.zuul.yml b/node_modules/request/node_modules/bl/node_modules/readable-stream/.zuul.yml deleted file mode 100644 index 96d9cfbd..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/.zuul.yml +++ /dev/null @@ -1 +0,0 @@ -ui: tape diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/LICENSE b/node_modules/request/node_modules/bl/node_modules/readable-stream/LICENSE deleted file mode 100644 index e3d4e695..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/LICENSE +++ /dev/null @@ -1,18 +0,0 @@ -Copyright Joyent, Inc. and other Node contributors. All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/README.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/README.md deleted file mode 100644 index 86b95a3b..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# readable-stream - -***Node-core v5.8.0 streams for userland*** [![Build Status](https://travis-ci.org/nodejs/readable-stream.svg?branch=master)](https://travis-ci.org/nodejs/readable-stream) - - -[![NPM](https://nodei.co/npm/readable-stream.png?downloads=true&downloadRank=true)](https://nodei.co/npm/readable-stream/) -[![NPM](https://nodei.co/npm-dl/readable-stream.png?&months=6&height=3)](https://nodei.co/npm/readable-stream/) - - -[![Sauce Test Status](https://saucelabs.com/browser-matrix/readable-stream.svg)](https://saucelabs.com/u/readable-stream) - -```bash -npm install --save readable-stream -``` - -***Node-core streams for userland*** - -This package is a mirror of the Streams2 and Streams3 implementations in -Node-core, including [documentation](doc/stream.markdown). - -If you want to guarantee a stable streams base, regardless of what version of -Node you, or the users of your libraries are using, use **readable-stream** *only* and avoid the *"stream"* module in Node-core, for background see [this blogpost](http://r.va.gg/2014/06/why-i-dont-use-nodes-core-stream-module.html). - -As of version 2.0.0 **readable-stream** uses semantic versioning. - -# Streams WG Team Members - -* **Chris Dickinson** ([@chrisdickinson](https://github.com/chrisdickinson)) <christopher.s.dickinson@gmail.com> - - Release GPG key: 9554F04D7259F04124DE6B476D5A82AC7E37093B -* **Calvin Metcalf** ([@calvinmetcalf](https://github.com/calvinmetcalf)) <calvin.metcalf@gmail.com> - - Release GPG key: F3EF5F62A87FC27A22E643F714CE4FF5015AA242 -* **Rod Vagg** ([@rvagg](https://github.com/rvagg)) <rod@vagg.org> - - Release GPG key: DD8F2338BAE7501E3DD5AC78C273792F7D83545D -* **Sam Newman** ([@sonewman](https://github.com/sonewman)) <newmansam@outlook.com> -* **Mathias Buus** ([@mafintosh](https://github.com/mafintosh)) <mathiasbuus@gmail.com> -* **Domenic Denicola** ([@domenic](https://github.com/domenic)) <d@domenic.me> diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/doc/stream.markdown b/node_modules/request/node_modules/bl/node_modules/readable-stream/doc/stream.markdown deleted file mode 100644 index 0bc3819e..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/doc/stream.markdown +++ /dev/null @@ -1,1760 +0,0 @@ -# Stream - - Stability: 2 - Stable - -A stream is an abstract interface implemented by various objects in -Node.js. For example a [request to an HTTP server][http-incoming-message] is a -stream, as is [`process.stdout`][]. Streams are readable, writable, or both. All -streams are instances of [`EventEmitter`][]. - -You can load the Stream base classes by doing `require('stream')`. -There are base classes provided for [Readable][] streams, [Writable][] -streams, [Duplex][] streams, and [Transform][] streams. - -This document is split up into 3 sections: - -1. The first section explains the parts of the API that you need to be - aware of to use streams in your programs. -2. The second section explains the parts of the API that you need to - use if you implement your own custom streams yourself. The API is designed to - make this easy for you to do. -3. The third section goes into more depth about how streams work, - including some of the internal mechanisms and functions that you - should probably not modify unless you definitely know what you are - doing. - - -## API for Stream Consumers - - - -Streams can be either [Readable][], [Writable][], or both ([Duplex][]). - -All streams are EventEmitters, but they also have other custom methods -and properties depending on whether they are Readable, Writable, or -Duplex. - -If a stream is both Readable and Writable, then it implements all of -the methods and events. So, a [Duplex][] or [Transform][] stream is -fully described by this API, though their implementation may be -somewhat different. - -It is not necessary to implement Stream interfaces in order to consume -streams in your programs. If you **are** implementing streaming -interfaces in your own program, please also refer to -[API for Stream Implementors][]. - -Almost all Node.js programs, no matter how simple, use Streams in some -way. Here is an example of using Streams in an Node.js program: - -```js -const http = require('http'); - -var server = http.createServer( (req, res) => { - // req is an http.IncomingMessage, which is a Readable Stream - // res is an http.ServerResponse, which is a Writable Stream - - var body = ''; - // we want to get the data as utf8 strings - // If you don't set an encoding, then you'll get Buffer objects - req.setEncoding('utf8'); - - // Readable streams emit 'data' events once a listener is added - req.on('data', (chunk) => { - body += chunk; - }); - - // the end event tells you that you have entire body - req.on('end', () => { - try { - var data = JSON.parse(body); - } catch (er) { - // uh oh! bad json! - res.statusCode = 400; - return res.end(`error: ${er.message}`); - } - - // write back something interesting to the user: - res.write(typeof data); - res.end(); - }); -}); - -server.listen(1337); - -// $ curl localhost:1337 -d '{}' -// object -// $ curl localhost:1337 -d '"foo"' -// string -// $ curl localhost:1337 -d 'not json' -// error: Unexpected token o -``` - -### Class: stream.Duplex - -Duplex streams are streams that implement both the [Readable][] and -[Writable][] interfaces. - -Examples of Duplex streams include: - -* [TCP sockets][] -* [zlib streams][zlib] -* [crypto streams][crypto] - -### Class: stream.Readable - - - -The Readable stream interface is the abstraction for a *source* of -data that you are reading from. In other words, data comes *out* of a -Readable stream. - -A Readable stream will not start emitting data until you indicate that -you are ready to receive it. - -Readable streams have two "modes": a **flowing mode** and a **paused -mode**. When in flowing mode, data is read from the underlying system -and provided to your program as fast as possible. In paused mode, you -must explicitly call [`stream.read()`][stream-read] to get chunks of data out. -Streams start out in paused mode. - -**Note**: If no data event handlers are attached, and there are no -[`stream.pipe()`][] destinations, and the stream is switched into flowing -mode, then data will be lost. - -You can switch to flowing mode by doing any of the following: - -* Adding a [`'data'`][] event handler to listen for data. -* Calling the [`stream.resume()`][stream-resume] method to explicitly open the - flow. -* Calling the [`stream.pipe()`][] method to send the data to a [Writable][]. - -You can switch back to paused mode by doing either of the following: - -* If there are no pipe destinations, by calling the - [`stream.pause()`][stream-pause] method. -* If there are pipe destinations, by removing any [`'data'`][] event - handlers, and removing all pipe destinations by calling the - [`stream.unpipe()`][] method. - -Note that, for backwards compatibility reasons, removing [`'data'`][] -event handlers will **not** automatically pause the stream. Also, if -there are piped destinations, then calling [`stream.pause()`][stream-pause] will -not guarantee that the stream will *remain* paused once those -destinations drain and ask for more data. - -Examples of readable streams include: - -* [HTTP responses, on the client][http-incoming-message] -* [HTTP requests, on the server][http-incoming-message] -* [fs read streams][] -* [zlib streams][zlib] -* [crypto streams][crypto] -* [TCP sockets][] -* [child process stdout and stderr][] -* [`process.stdin`][] - -#### Event: 'close' - -Emitted when the stream and any of its underlying resources (a file -descriptor, for example) have been closed. The event indicates that -no more events will be emitted, and no further computation will occur. - -Not all streams will emit the `'close'` event. - -#### Event: 'data' - -* `chunk` {Buffer|String} The chunk of data. - -Attaching a `'data'` event listener to a stream that has not been -explicitly paused will switch the stream into flowing mode. Data will -then be passed as soon as it is available. - -If you just want to get all the data out of the stream as fast as -possible, this is the best way to do so. - -```js -var readable = getReadableStreamSomehow(); -readable.on('data', (chunk) => { - console.log('got %d bytes of data', chunk.length); -}); -``` - -#### Event: 'end' - -This event fires when there will be no more data to read. - -Note that the `'end'` event **will not fire** unless the data is -completely consumed. This can be done by switching into flowing mode, -or by calling [`stream.read()`][stream-read] repeatedly until you get to the -end. - -```js -var readable = getReadableStreamSomehow(); -readable.on('data', (chunk) => { - console.log('got %d bytes of data', chunk.length); -}); -readable.on('end', () => { - console.log('there will be no more data.'); -}); -``` - -#### Event: 'error' - -* {Error Object} - -Emitted if there was an error receiving data. - -#### Event: 'readable' - -When a chunk of data can be read from the stream, it will emit a -`'readable'` event. - -In some cases, listening for a `'readable'` event will cause some data -to be read into the internal buffer from the underlying system, if it -hadn't already. - -```javascript -var readable = getReadableStreamSomehow(); -readable.on('readable', () => { - // there is some data to read now -}); -``` - -Once the internal buffer is drained, a `'readable'` event will fire -again when more data is available. - -The `'readable'` event is not emitted in the "flowing" mode with the -sole exception of the last one, on end-of-stream. - -The `'readable'` event indicates that the stream has new information: -either new data is available or the end of the stream has been reached. -In the former case, [`stream.read()`][stream-read] will return that data. In the -latter case, [`stream.read()`][stream-read] will return null. For instance, in -the following example, `foo.txt` is an empty file: - -```js -const fs = require('fs'); -var rr = fs.createReadStream('foo.txt'); -rr.on('readable', () => { - console.log('readable:', rr.read()); -}); -rr.on('end', () => { - console.log('end'); -}); -``` - -The output of running this script is: - -``` -$ node test.js -readable: null -end -``` - -#### readable.isPaused() - -* Return: {Boolean} - -This method returns whether or not the `readable` has been **explicitly** -paused by client code (using [`stream.pause()`][stream-pause] without a -corresponding [`stream.resume()`][stream-resume]). - -```js -var readable = new stream.Readable - -readable.isPaused() // === false -readable.pause() -readable.isPaused() // === true -readable.resume() -readable.isPaused() // === false -``` - -#### readable.pause() - -* Return: `this` - -This method will cause a stream in flowing mode to stop emitting -[`'data'`][] events, switching out of flowing mode. Any data that becomes -available will remain in the internal buffer. - -```js -var readable = getReadableStreamSomehow(); -readable.on('data', (chunk) => { - console.log('got %d bytes of data', chunk.length); - readable.pause(); - console.log('there will be no more data for 1 second'); - setTimeout(() => { - console.log('now data will start flowing again'); - readable.resume(); - }, 1000); -}); -``` - -#### readable.pipe(destination[, options]) - -* `destination` {stream.Writable} The destination for writing data -* `options` {Object} Pipe options - * `end` {Boolean} End the writer when the reader ends. Default = `true` - -This method pulls all the data out of a readable stream, and writes it -to the supplied destination, automatically managing the flow so that -the destination is not overwhelmed by a fast readable stream. - -Multiple destinations can be piped to safely. - -```js -var readable = getReadableStreamSomehow(); -var writable = fs.createWriteStream('file.txt'); -// All the data from readable goes into 'file.txt' -readable.pipe(writable); -``` - -This function returns the destination stream, so you can set up pipe -chains like so: - -```js -var r = fs.createReadStream('file.txt'); -var z = zlib.createGzip(); -var w = fs.createWriteStream('file.txt.gz'); -r.pipe(z).pipe(w); -``` - -For example, emulating the Unix `cat` command: - -```js -process.stdin.pipe(process.stdout); -``` - -By default [`stream.end()`][stream-end] is called on the destination when the -source stream emits [`'end'`][], so that `destination` is no longer writable. -Pass `{ end: false }` as `options` to keep the destination stream open. - -This keeps `writer` open so that "Goodbye" can be written at the -end. - -```js -reader.pipe(writer, { end: false }); -reader.on('end', () => { - writer.end('Goodbye\n'); -}); -``` - -Note that [`process.stderr`][] and [`process.stdout`][] are never closed until -the process exits, regardless of the specified options. - -#### readable.read([size]) - -* `size` {Number} Optional argument to specify how much data to read. -* Return {String|Buffer|Null} - -The `read()` method pulls some data out of the internal buffer and -returns it. If there is no data available, then it will return -`null`. - -If you pass in a `size` argument, then it will return that many -bytes. If `size` bytes are not available, then it will return `null`, -unless we've ended, in which case it will return the data remaining -in the buffer. - -If you do not specify a `size` argument, then it will return all the -data in the internal buffer. - -This method should only be called in paused mode. In flowing mode, -this method is called automatically until the internal buffer is -drained. - -```js -var readable = getReadableStreamSomehow(); -readable.on('readable', () => { - var chunk; - while (null !== (chunk = readable.read())) { - console.log('got %d bytes of data', chunk.length); - } -}); -``` - -If this method returns a data chunk, then it will also trigger the -emission of a [`'data'`][] event. - -Note that calling [`stream.read([size])`][stream-read] after the [`'end'`][] -event has been triggered will return `null`. No runtime error will be raised. - -#### readable.resume() - -* Return: `this` - -This method will cause the readable stream to resume emitting [`'data'`][] -events. - -This method will switch the stream into flowing mode. If you do *not* -want to consume the data from a stream, but you *do* want to get to -its [`'end'`][] event, you can call [`stream.resume()`][stream-resume] to open -the flow of data. - -```js -var readable = getReadableStreamSomehow(); -readable.resume(); -readable.on('end', () => { - console.log('got to the end, but did not read anything'); -}); -``` - -#### readable.setEncoding(encoding) - -* `encoding` {String} The encoding to use. -* Return: `this` - -Call this function to cause the stream to return strings of the specified -encoding instead of Buffer objects. For example, if you do -`readable.setEncoding('utf8')`, then the output data will be interpreted as -UTF-8 data, and returned as strings. If you do `readable.setEncoding('hex')`, -then the data will be encoded in hexadecimal string format. - -This properly handles multi-byte characters that would otherwise be -potentially mangled if you simply pulled the Buffers directly and -called [`buf.toString(encoding)`][] on them. If you want to read the data -as strings, always use this method. - -Also you can disable any encoding at all with `readable.setEncoding(null)`. -This approach is very useful if you deal with binary data or with large -multi-byte strings spread out over multiple chunks. - -```js -var readable = getReadableStreamSomehow(); -readable.setEncoding('utf8'); -readable.on('data', (chunk) => { - assert.equal(typeof chunk, 'string'); - console.log('got %d characters of string data', chunk.length); -}); -``` - -#### readable.unpipe([destination]) - -* `destination` {stream.Writable} Optional specific stream to unpipe - -This method will remove the hooks set up for a previous [`stream.pipe()`][] -call. - -If the destination is not specified, then all pipes are removed. - -If the destination is specified, but no pipe is set up for it, then -this is a no-op. - -```js -var readable = getReadableStreamSomehow(); -var writable = fs.createWriteStream('file.txt'); -// All the data from readable goes into 'file.txt', -// but only for the first second -readable.pipe(writable); -setTimeout(() => { - console.log('stop writing to file.txt'); - readable.unpipe(writable); - console.log('manually close the file stream'); - writable.end(); -}, 1000); -``` - -#### readable.unshift(chunk) - -* `chunk` {Buffer|String} Chunk of data to unshift onto the read queue - -This is useful in certain cases where a stream is being consumed by a -parser, which needs to "un-consume" some data that it has -optimistically pulled out of the source, so that the stream can be -passed on to some other party. - -Note that `stream.unshift(chunk)` cannot be called after the [`'end'`][] event -has been triggered; a runtime error will be raised. - -If you find that you must often call `stream.unshift(chunk)` in your -programs, consider implementing a [Transform][] stream instead. (See [API -for Stream Implementors][].) - -```js -// Pull off a header delimited by \n\n -// use unshift() if we get too much -// Call the callback with (error, header, stream) -const StringDecoder = require('string_decoder').StringDecoder; -function parseHeader(stream, callback) { - stream.on('error', callback); - stream.on('readable', onReadable); - var decoder = new StringDecoder('utf8'); - var header = ''; - function onReadable() { - var chunk; - while (null !== (chunk = stream.read())) { - var str = decoder.write(chunk); - if (str.match(/\n\n/)) { - // found the header boundary - var split = str.split(/\n\n/); - header += split.shift(); - var remaining = split.join('\n\n'); - var buf = new Buffer(remaining, 'utf8'); - if (buf.length) - stream.unshift(buf); - stream.removeListener('error', callback); - stream.removeListener('readable', onReadable); - // now the body of the message can be read from the stream. - callback(null, header, stream); - } else { - // still reading the header. - header += str; - } - } - } -} -``` - -Note that, unlike [`stream.push(chunk)`][stream-push], `stream.unshift(chunk)` -will not end the reading process by resetting the internal reading state of the -stream. This can cause unexpected results if `unshift()` is called during a -read (i.e. from within a [`stream._read()`][stream-_read] implementation on a -custom stream). Following the call to `unshift()` with an immediate -[`stream.push('')`][stream-push] will reset the reading state appropriately, -however it is best to simply avoid calling `unshift()` while in the process of -performing a read. - -#### readable.wrap(stream) - -* `stream` {Stream} An "old style" readable stream - -Versions of Node.js prior to v0.10 had streams that did not implement the -entire Streams API as it is today. (See [Compatibility][] for -more information.) - -If you are using an older Node.js library that emits [`'data'`][] events and -has a [`stream.pause()`][stream-pause] method that is advisory only, then you -can use the `wrap()` method to create a [Readable][] stream that uses the old -stream as its data source. - -You will very rarely ever need to call this function, but it exists -as a convenience for interacting with old Node.js programs and libraries. - -For example: - -```js -const OldReader = require('./old-api-module.js').OldReader; -const Readable = require('stream').Readable; -const oreader = new OldReader; -const myReader = new Readable().wrap(oreader); - -myReader.on('readable', () => { - myReader.read(); // etc. -}); -``` - -### Class: stream.Transform - -Transform streams are [Duplex][] streams where the output is in some way -computed from the input. They implement both the [Readable][] and -[Writable][] interfaces. - -Examples of Transform streams include: - -* [zlib streams][zlib] -* [crypto streams][crypto] - -### Class: stream.Writable - - - -The Writable stream interface is an abstraction for a *destination* -that you are writing data *to*. - -Examples of writable streams include: - -* [HTTP requests, on the client][] -* [HTTP responses, on the server][] -* [fs write streams][] -* [zlib streams][zlib] -* [crypto streams][crypto] -* [TCP sockets][] -* [child process stdin][] -* [`process.stdout`][], [`process.stderr`][] - -#### Event: 'drain' - -If a [`stream.write(chunk)`][stream-write] call returns `false`, then the -`'drain'` event will indicate when it is appropriate to begin writing more data -to the stream. - -```js -// Write the data to the supplied writable stream one million times. -// Be attentive to back-pressure. -function writeOneMillionTimes(writer, data, encoding, callback) { - var i = 1000000; - write(); - function write() { - var ok = true; - do { - i -= 1; - if (i === 0) { - // last time! - writer.write(data, encoding, callback); - } else { - // see if we should continue, or wait - // don't pass the callback, because we're not done yet. - ok = writer.write(data, encoding); - } - } while (i > 0 && ok); - if (i > 0) { - // had to stop early! - // write some more once it drains - writer.once('drain', write); - } - } -} -``` - -#### Event: 'error' - -* {Error} - -Emitted if there was an error when writing or piping data. - -#### Event: 'finish' - -When the [`stream.end()`][stream-end] method has been called, and all data has -been flushed to the underlying system, this event is emitted. - -```javascript -var writer = getWritableStreamSomehow(); -for (var i = 0; i < 100; i ++) { - writer.write('hello, #${i}!\n'); -} -writer.end('this is the end\n'); -writer.on('finish', () => { - console.error('all writes are now complete.'); -}); -``` - -#### Event: 'pipe' - -* `src` {stream.Readable} source stream that is piping to this writable - -This is emitted whenever the [`stream.pipe()`][] method is called on a readable -stream, adding this writable to its set of destinations. - -```js -var writer = getWritableStreamSomehow(); -var reader = getReadableStreamSomehow(); -writer.on('pipe', (src) => { - console.error('something is piping into the writer'); - assert.equal(src, reader); -}); -reader.pipe(writer); -``` - -#### Event: 'unpipe' - -* `src` {[Readable][] Stream} The source stream that - [unpiped][`stream.unpipe()`] this writable - -This is emitted whenever the [`stream.unpipe()`][] method is called on a -readable stream, removing this writable from its set of destinations. - -```js -var writer = getWritableStreamSomehow(); -var reader = getReadableStreamSomehow(); -writer.on('unpipe', (src) => { - console.error('something has stopped piping into the writer'); - assert.equal(src, reader); -}); -reader.pipe(writer); -reader.unpipe(writer); -``` - -#### writable.cork() - -Forces buffering of all writes. - -Buffered data will be flushed either at [`stream.uncork()`][] or at -[`stream.end()`][stream-end] call. - -#### writable.end([chunk][, encoding][, callback]) - -* `chunk` {String|Buffer} Optional data to write -* `encoding` {String} The encoding, if `chunk` is a String -* `callback` {Function} Optional callback for when the stream is finished - -Call this method when no more data will be written to the stream. If supplied, -the callback is attached as a listener on the [`'finish'`][] event. - -Calling [`stream.write()`][stream-write] after calling -[`stream.end()`][stream-end] will raise an error. - -```js -// write 'hello, ' and then end with 'world!' -var file = fs.createWriteStream('example.txt'); -file.write('hello, '); -file.end('world!'); -// writing more now is not allowed! -``` - -#### writable.setDefaultEncoding(encoding) - -* `encoding` {String} The new default encoding - -Sets the default encoding for a writable stream. - -#### writable.uncork() - -Flush all data, buffered since [`stream.cork()`][] call. - -#### writable.write(chunk[, encoding][, callback]) - -* `chunk` {String|Buffer} The data to write -* `encoding` {String} The encoding, if `chunk` is a String -* `callback` {Function} Callback for when this chunk of data is flushed -* Returns: {Boolean} `true` if the data was handled completely. - -This method writes some data to the underlying system, and calls the -supplied callback once the data has been fully handled. - -The return value indicates if you should continue writing right now. -If the data had to be buffered internally, then it will return -`false`. Otherwise, it will return `true`. - -This return value is strictly advisory. You MAY continue to write, -even if it returns `false`. However, writes will be buffered in -memory, so it is best not to do this excessively. Instead, wait for -the [`'drain'`][] event before writing more data. - - -## API for Stream Implementors - - - -To implement any sort of stream, the pattern is the same: - -1. Extend the appropriate parent class in your own subclass. (The - [`util.inherits()`][] method is particularly helpful for this.) -2. Call the appropriate parent class constructor in your constructor, - to be sure that the internal mechanisms are set up properly. -3. Implement one or more specific methods, as detailed below. - -The class to extend and the method(s) to implement depend on the sort -of stream class you are writing: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Use-case

-
-

Class

-
-

Method(s) to implement

-
-

Reading only

-
-

[Readable](#stream_class_stream_readable_1)

-
-

[_read][stream-_read]

-
-

Writing only

-
-

[Writable](#stream_class_stream_writable_1)

-
-

[_write][stream-_write], [_writev][stream-_writev]

-
-

Reading and writing

-
-

[Duplex](#stream_class_stream_duplex_1)

-
-

[_read][stream-_read], [_write][stream-_write], [_writev][stream-_writev]

-
-

Operate on written data, then read the result

-
-

[Transform](#stream_class_stream_transform_1)

-
-

[_transform][stream-_transform], [_flush][stream-_flush]

-
- -In your implementation code, it is very important to never call the methods -described in [API for Stream Consumers][]. Otherwise, you can potentially cause -adverse side effects in programs that consume your streaming interfaces. - -### Class: stream.Duplex - - - -A "duplex" stream is one that is both Readable and Writable, such as a TCP -socket connection. - -Note that `stream.Duplex` is an abstract class designed to be extended -with an underlying implementation of the [`stream._read(size)`][stream-_read] -and [`stream._write(chunk, encoding, callback)`][stream-_write] methods as you -would with a Readable or Writable stream class. - -Since JavaScript doesn't have multiple prototypal inheritance, this class -prototypally inherits from Readable, and then parasitically from Writable. It is -thus up to the user to implement both the low-level -[`stream._read(n)`][stream-_read] method as well as the low-level -[`stream._write(chunk, encoding, callback)`][stream-_write] method on extension -duplex classes. - -#### new stream.Duplex(options) - -* `options` {Object} Passed to both Writable and Readable - constructors. Also has the following fields: - * `allowHalfOpen` {Boolean} Default = `true`. If set to `false`, then - the stream will automatically end the readable side when the - writable side ends and vice versa. - * `readableObjectMode` {Boolean} Default = `false`. Sets `objectMode` - for readable side of the stream. Has no effect if `objectMode` - is `true`. - * `writableObjectMode` {Boolean} Default = `false`. Sets `objectMode` - for writable side of the stream. Has no effect if `objectMode` - is `true`. - -In classes that extend the Duplex class, make sure to call the -constructor so that the buffering settings can be properly -initialized. - -### Class: stream.PassThrough - -This is a trivial implementation of a [Transform][] stream that simply -passes the input bytes across to the output. Its purpose is mainly -for examples and testing, but there are occasionally use cases where -it can come in handy as a building block for novel sorts of streams. - -### Class: stream.Readable - - - -`stream.Readable` is an abstract class designed to be extended with an -underlying implementation of the [`stream._read(size)`][stream-_read] method. - -Please see [API for Stream Consumers][] for how to consume -streams in your programs. What follows is an explanation of how to -implement Readable streams in your programs. - -#### new stream.Readable([options]) - -* `options` {Object} - * `highWaterMark` {Number} The maximum number of bytes to store in - the internal buffer before ceasing to read from the underlying - resource. Default = `16384` (16kb), or `16` for `objectMode` streams - * `encoding` {String} If specified, then buffers will be decoded to - strings using the specified encoding. Default = `null` - * `objectMode` {Boolean} Whether this stream should behave - as a stream of objects. Meaning that [`stream.read(n)`][stream-read] returns - a single value instead of a Buffer of size n. Default = `false` - * `read` {Function} Implementation for the [`stream._read()`][stream-_read] - method. - -In classes that extend the Readable class, make sure to call the -Readable constructor so that the buffering settings can be properly -initialized. - -#### readable.\_read(size) - -* `size` {Number} Number of bytes to read asynchronously - -Note: **Implement this method, but do NOT call it directly.** - -This method is prefixed with an underscore because it is internal to the -class that defines it and should only be called by the internal Readable -class methods. All Readable stream implementations must provide a \_read -method to fetch data from the underlying resource. - -When `_read()` is called, if data is available from the resource, the `_read()` -implementation should start pushing that data into the read queue by calling -[`this.push(dataChunk)`][stream-push]. `_read()` should continue reading from -the resource and pushing data until push returns `false`, at which point it -should stop reading from the resource. Only when `_read()` is called again after -it has stopped should it start reading more data from the resource and pushing -that data onto the queue. - -Note: once the `_read()` method is called, it will not be called again until -the [`stream.push()`][stream-push] method is called. - -The `size` argument is advisory. Implementations where a "read" is a -single call that returns data can use this to know how much data to -fetch. Implementations where that is not relevant, such as TCP or -TLS, may ignore this argument, and simply provide data whenever it -becomes available. There is no need, for example to "wait" until -`size` bytes are available before calling [`stream.push(chunk)`][stream-push]. - -#### readable.push(chunk[, encoding]) - - -* `chunk` {Buffer|Null|String} Chunk of data to push into the read queue -* `encoding` {String} Encoding of String chunks. Must be a valid - Buffer encoding, such as `'utf8'` or `'ascii'` -* return {Boolean} Whether or not more pushes should be performed - -Note: **This method should be called by Readable implementors, NOT -by consumers of Readable streams.** - -If a value other than null is passed, The `push()` method adds a chunk of data -into the queue for subsequent stream processors to consume. If `null` is -passed, it signals the end of the stream (EOF), after which no more data -can be written. - -The data added with `push()` can be pulled out by calling the -[`stream.read()`][stream-read] method when the [`'readable'`][] event fires. - -This API is designed to be as flexible as possible. For example, -you may be wrapping a lower-level source which has some sort of -pause/resume mechanism, and a data callback. In those cases, you -could wrap the low-level source object by doing something like this: - -```js -// source is an object with readStop() and readStart() methods, -// and an `ondata` member that gets called when it has data, and -// an `onend` member that gets called when the data is over. - -util.inherits(SourceWrapper, Readable); - -function SourceWrapper(options) { - Readable.call(this, options); - - this._source = getLowlevelSourceObject(); - - // Every time there's data, we push it into the internal buffer. - this._source.ondata = (chunk) => { - // if push() returns false, then we need to stop reading from source - if (!this.push(chunk)) - this._source.readStop(); - }; - - // When the source ends, we push the EOF-signaling `null` chunk - this._source.onend = () => { - this.push(null); - }; -} - -// _read will be called when the stream wants to pull more data in -// the advisory size argument is ignored in this case. -SourceWrapper.prototype._read = function(size) { - this._source.readStart(); -}; -``` - -#### Example: A Counting Stream - - - -This is a basic example of a Readable stream. It emits the numerals -from 1 to 1,000,000 in ascending order, and then ends. - -```js -const Readable = require('stream').Readable; -const util = require('util'); -util.inherits(Counter, Readable); - -function Counter(opt) { - Readable.call(this, opt); - this._max = 1000000; - this._index = 1; -} - -Counter.prototype._read = function() { - var i = this._index++; - if (i > this._max) - this.push(null); - else { - var str = '' + i; - var buf = new Buffer(str, 'ascii'); - this.push(buf); - } -}; -``` - -#### Example: SimpleProtocol v1 (Sub-optimal) - -This is similar to the `parseHeader` function described -[here](#stream_readable_unshift_chunk), but implemented as a custom stream. -Also, note that this implementation does not convert the incoming data to a -string. - -However, this would be better implemented as a [Transform][] stream. See -[SimpleProtocol v2][] for a better implementation. - -```js -// A parser for a simple data protocol. -// The "header" is a JSON object, followed by 2 \n characters, and -// then a message body. -// -// NOTE: This can be done more simply as a Transform stream! -// Using Readable directly for this is sub-optimal. See the -// alternative example below under the Transform section. - -const Readable = require('stream').Readable; -const util = require('util'); - -util.inherits(SimpleProtocol, Readable); - -function SimpleProtocol(source, options) { - if (!(this instanceof SimpleProtocol)) - return new SimpleProtocol(source, options); - - Readable.call(this, options); - this._inBody = false; - this._sawFirstCr = false; - - // source is a readable stream, such as a socket or file - this._source = source; - - var self = this; - source.on('end', () => { - self.push(null); - }); - - // give it a kick whenever the source is readable - // read(0) will not consume any bytes - source.on('readable', () => { - self.read(0); - }); - - this._rawHeader = []; - this.header = null; -} - -SimpleProtocol.prototype._read = function(n) { - if (!this._inBody) { - var chunk = this._source.read(); - - // if the source doesn't have data, we don't have data yet. - if (chunk === null) - return this.push(''); - - // check if the chunk has a \n\n - var split = -1; - for (var i = 0; i < chunk.length; i++) { - if (chunk[i] === 10) { // '\n' - if (this._sawFirstCr) { - split = i; - break; - } else { - this._sawFirstCr = true; - } - } else { - this._sawFirstCr = false; - } - } - - if (split === -1) { - // still waiting for the \n\n - // stash the chunk, and try again. - this._rawHeader.push(chunk); - this.push(''); - } else { - this._inBody = true; - var h = chunk.slice(0, split); - this._rawHeader.push(h); - var header = Buffer.concat(this._rawHeader).toString(); - try { - this.header = JSON.parse(header); - } catch (er) { - this.emit('error', new Error('invalid simple protocol data')); - return; - } - // now, because we got some extra data, unshift the rest - // back into the read queue so that our consumer will see it. - var b = chunk.slice(split); - this.unshift(b); - // calling unshift by itself does not reset the reading state - // of the stream; since we're inside _read, doing an additional - // push('') will reset the state appropriately. - this.push(''); - - // and let them know that we are done parsing the header. - this.emit('header', this.header); - } - } else { - // from there on, just provide the data to our consumer. - // careful not to push(null), since that would indicate EOF. - var chunk = this._source.read(); - if (chunk) this.push(chunk); - } -}; - -// Usage: -// var parser = new SimpleProtocol(source); -// Now parser is a readable stream that will emit 'header' -// with the parsed header data. -``` - -### Class: stream.Transform - -A "transform" stream is a duplex stream where the output is causally -connected in some way to the input, such as a [zlib][] stream or a -[crypto][] stream. - -There is no requirement that the output be the same size as the input, -the same number of chunks, or arrive at the same time. For example, a -Hash stream will only ever have a single chunk of output which is -provided when the input is ended. A zlib stream will produce output -that is either much smaller or much larger than its input. - -Rather than implement the [`stream._read()`][stream-_read] and -[`stream._write()`][stream-_write] methods, Transform classes must implement the -[`stream._transform()`][stream-_transform] method, and may optionally -also implement the [`stream._flush()`][stream-_flush] method. (See below.) - -#### new stream.Transform([options]) - -* `options` {Object} Passed to both Writable and Readable - constructors. Also has the following fields: - * `transform` {Function} Implementation for the - [`stream._transform()`][stream-_transform] method. - * `flush` {Function} Implementation for the [`stream._flush()`][stream-_flush] - method. - -In classes that extend the Transform class, make sure to call the -constructor so that the buffering settings can be properly -initialized. - -#### Events: 'finish' and 'end' - -The [`'finish'`][] and [`'end'`][] events are from the parent Writable -and Readable classes respectively. The `'finish'` event is fired after -[`stream.end()`][stream-end] is called and all chunks have been processed by -[`stream._transform()`][stream-_transform], `'end'` is fired after all data has -been output which is after the callback in [`stream._flush()`][stream-_flush] -has been called. - -#### transform.\_flush(callback) - -* `callback` {Function} Call this function (optionally with an error - argument) when you are done flushing any remaining data. - -Note: **This function MUST NOT be called directly.** It MAY be implemented -by child classes, and if so, will be called by the internal Transform -class methods only. - -In some cases, your transform operation may need to emit a bit more -data at the end of the stream. For example, a `Zlib` compression -stream will store up some internal state so that it can optimally -compress the output. At the end, however, it needs to do the best it -can with what is left, so that the data will be complete. - -In those cases, you can implement a `_flush()` method, which will be -called at the very end, after all the written data is consumed, but -before emitting [`'end'`][] to signal the end of the readable side. Just -like with [`stream._transform()`][stream-_transform], call -`transform.push(chunk)` zero or more times, as appropriate, and call `callback` -when the flush operation is complete. - -This method is prefixed with an underscore because it is internal to -the class that defines it, and should not be called directly by user -programs. However, you **are** expected to override this method in -your own extension classes. - -#### transform.\_transform(chunk, encoding, callback) - -* `chunk` {Buffer|String} The chunk to be transformed. Will **always** - be a buffer unless the `decodeStrings` option was set to `false`. -* `encoding` {String} If the chunk is a string, then this is the - encoding type. If chunk is a buffer, then this is the special - value - 'buffer', ignore it in this case. -* `callback` {Function} Call this function (optionally with an error - argument and data) when you are done processing the supplied chunk. - -Note: **This function MUST NOT be called directly.** It should be -implemented by child classes, and called by the internal Transform -class methods only. - -All Transform stream implementations must provide a `_transform()` -method to accept input and produce output. - -`_transform()` should do whatever has to be done in this specific -Transform class, to handle the bytes being written, and pass them off -to the readable portion of the interface. Do asynchronous I/O, -process things, and so on. - -Call `transform.push(outputChunk)` 0 or more times to generate output -from this input chunk, depending on how much data you want to output -as a result of this chunk. - -Call the callback function only when the current chunk is completely -consumed. Note that there may or may not be output as a result of any -particular input chunk. If you supply a second argument to the callback -it will be passed to the push method. In other words the following are -equivalent: - -```js -transform.prototype._transform = function (data, encoding, callback) { - this.push(data); - callback(); -}; - -transform.prototype._transform = function (data, encoding, callback) { - callback(null, data); -}; -``` - -This method is prefixed with an underscore because it is internal to -the class that defines it, and should not be called directly by user -programs. However, you **are** expected to override this method in -your own extension classes. - -#### Example: `SimpleProtocol` parser v2 - -The example [here](#stream_example_simpleprotocol_v1_sub_optimal) of a simple -protocol parser can be implemented simply by using the higher level -[Transform][] stream class, similar to the `parseHeader` and `SimpleProtocol -v1` examples. - -In this example, rather than providing the input as an argument, it -would be piped into the parser, which is a more idiomatic Node.js stream -approach. - -```javascript -const util = require('util'); -const Transform = require('stream').Transform; -util.inherits(SimpleProtocol, Transform); - -function SimpleProtocol(options) { - if (!(this instanceof SimpleProtocol)) - return new SimpleProtocol(options); - - Transform.call(this, options); - this._inBody = false; - this._sawFirstCr = false; - this._rawHeader = []; - this.header = null; -} - -SimpleProtocol.prototype._transform = function(chunk, encoding, done) { - if (!this._inBody) { - // check if the chunk has a \n\n - var split = -1; - for (var i = 0; i < chunk.length; i++) { - if (chunk[i] === 10) { // '\n' - if (this._sawFirstCr) { - split = i; - break; - } else { - this._sawFirstCr = true; - } - } else { - this._sawFirstCr = false; - } - } - - if (split === -1) { - // still waiting for the \n\n - // stash the chunk, and try again. - this._rawHeader.push(chunk); - } else { - this._inBody = true; - var h = chunk.slice(0, split); - this._rawHeader.push(h); - var header = Buffer.concat(this._rawHeader).toString(); - try { - this.header = JSON.parse(header); - } catch (er) { - this.emit('error', new Error('invalid simple protocol data')); - return; - } - // and let them know that we are done parsing the header. - this.emit('header', this.header); - - // now, because we got some extra data, emit this first. - this.push(chunk.slice(split)); - } - } else { - // from there on, just provide the data to our consumer as-is. - this.push(chunk); - } - done(); -}; - -// Usage: -// var parser = new SimpleProtocol(); -// source.pipe(parser) -// Now parser is a readable stream that will emit 'header' -// with the parsed header data. -``` - -### Class: stream.Writable - - - -`stream.Writable` is an abstract class designed to be extended with an -underlying implementation of the -[`stream._write(chunk, encoding, callback)`][stream-_write] method. - -Please see [API for Stream Consumers][] for how to consume -writable streams in your programs. What follows is an explanation of -how to implement Writable streams in your programs. - -#### new stream.Writable([options]) - -* `options` {Object} - * `highWaterMark` {Number} Buffer level when - [`stream.write()`][stream-write] starts returning `false`. Default = `16384` - (16kb), or `16` for `objectMode` streams. - * `decodeStrings` {Boolean} Whether or not to decode strings into - Buffers before passing them to [`stream._write()`][stream-_write]. - Default = `true` - * `objectMode` {Boolean} Whether or not the - [`stream.write(anyObj)`][stream-write] is a valid operation. If set you can - write arbitrary data instead of only `Buffer` / `String` data. - Default = `false` - * `write` {Function} Implementation for the - [`stream._write()`][stream-_write] method. - * `writev` {Function} Implementation for the - [`stream._writev()`][stream-_writev] method. - -In classes that extend the Writable class, make sure to call the -constructor so that the buffering settings can be properly -initialized. - -#### writable.\_write(chunk, encoding, callback) - -* `chunk` {Buffer|String} The chunk to be written. Will **always** - be a buffer unless the `decodeStrings` option was set to `false`. -* `encoding` {String} If the chunk is a string, then this is the - encoding type. If chunk is a buffer, then this is the special - value - 'buffer', ignore it in this case. -* `callback` {Function} Call this function (optionally with an error - argument) when you are done processing the supplied chunk. - -All Writable stream implementations must provide a -[`stream._write()`][stream-_write] method to send data to the underlying -resource. - -Note: **This function MUST NOT be called directly.** It should be -implemented by child classes, and called by the internal Writable -class methods only. - -Call the callback using the standard `callback(error)` pattern to -signal that the write completed successfully or with an error. - -If the `decodeStrings` flag is set in the constructor options, then -`chunk` may be a string rather than a Buffer, and `encoding` will -indicate the sort of string that it is. This is to support -implementations that have an optimized handling for certain string -data encodings. If you do not explicitly set the `decodeStrings` -option to `false`, then you can safely ignore the `encoding` argument, -and assume that `chunk` will always be a Buffer. - -This method is prefixed with an underscore because it is internal to -the class that defines it, and should not be called directly by user -programs. However, you **are** expected to override this method in -your own extension classes. - -#### writable.\_writev(chunks, callback) - -* `chunks` {Array} The chunks to be written. Each chunk has following - format: `{ chunk: ..., encoding: ... }`. -* `callback` {Function} Call this function (optionally with an error - argument) when you are done processing the supplied chunks. - -Note: **This function MUST NOT be called directly.** It may be -implemented by child classes, and called by the internal Writable -class methods only. - -This function is completely optional to implement. In most cases it is -unnecessary. If implemented, it will be called with all the chunks -that are buffered in the write queue. - - -## Simplified Constructor API - - - -In simple cases there is now the added benefit of being able to construct a -stream without inheritance. - -This can be done by passing the appropriate methods as constructor options: - -Examples: - -### Duplex - -```js -var duplex = new stream.Duplex({ - read: function(n) { - // sets this._read under the hood - - // push data onto the read queue, passing null - // will signal the end of the stream (EOF) - this.push(chunk); - }, - write: function(chunk, encoding, next) { - // sets this._write under the hood - - // An optional error can be passed as the first argument - next() - } -}); - -// or - -var duplex = new stream.Duplex({ - read: function(n) { - // sets this._read under the hood - - // push data onto the read queue, passing null - // will signal the end of the stream (EOF) - this.push(chunk); - }, - writev: function(chunks, next) { - // sets this._writev under the hood - - // An optional error can be passed as the first argument - next() - } -}); -``` - -### Readable - -```js -var readable = new stream.Readable({ - read: function(n) { - // sets this._read under the hood - - // push data onto the read queue, passing null - // will signal the end of the stream (EOF) - this.push(chunk); - } -}); -``` - -### Transform - -```js -var transform = new stream.Transform({ - transform: function(chunk, encoding, next) { - // sets this._transform under the hood - - // generate output as many times as needed - // this.push(chunk); - - // call when the current chunk is consumed - next(); - }, - flush: function(done) { - // sets this._flush under the hood - - // generate output as many times as needed - // this.push(chunk); - - done(); - } -}); -``` - -### Writable - -```js -var writable = new stream.Writable({ - write: function(chunk, encoding, next) { - // sets this._write under the hood - - // An optional error can be passed as the first argument - next() - } -}); - -// or - -var writable = new stream.Writable({ - writev: function(chunks, next) { - // sets this._writev under the hood - - // An optional error can be passed as the first argument - next() - } -}); -``` - -## Streams: Under the Hood - - - -### Buffering - - - -Both Writable and Readable streams will buffer data on an internal -object which can be retrieved from `_writableState.getBuffer()` or -`_readableState.buffer`, respectively. - -The amount of data that will potentially be buffered depends on the -`highWaterMark` option which is passed into the constructor. - -Buffering in Readable streams happens when the implementation calls -[`stream.push(chunk)`][stream-push]. If the consumer of the Stream does not -call [`stream.read()`][stream-read], then the data will sit in the internal -queue until it is consumed. - -Buffering in Writable streams happens when the user calls -[`stream.write(chunk)`][stream-write] repeatedly, even when it returns `false`. - -The purpose of streams, especially with the [`stream.pipe()`][] method, is to -limit the buffering of data to acceptable levels, so that sources and -destinations of varying speed will not overwhelm the available memory. - -### Compatibility with Older Node.js Versions - - - -In versions of Node.js prior to v0.10, the Readable stream interface was -simpler, but also less powerful and less useful. - -* Rather than waiting for you to call the [`stream.read()`][stream-read] method, - [`'data'`][] events would start emitting immediately. If you needed to do - some I/O to decide how to handle data, then you had to store the chunks - in some kind of buffer so that they would not be lost. -* The [`stream.pause()`][stream-pause] method was advisory, rather than - guaranteed. This meant that you still had to be prepared to receive - [`'data'`][] events even when the stream was in a paused state. - -In Node.js v0.10, the [Readable][] class was added. -For backwards compatibility with older Node.js programs, Readable streams -switch into "flowing mode" when a [`'data'`][] event handler is added, or -when the [`stream.resume()`][stream-resume] method is called. The effect is -that, even if you are not using the new [`stream.read()`][stream-read] method -and [`'readable'`][] event, you no longer have to worry about losing -[`'data'`][] chunks. - -Most programs will continue to function normally. However, this -introduces an edge case in the following conditions: - -* No [`'data'`][] event handler is added. -* The [`stream.resume()`][stream-resume] method is never called. -* The stream is not piped to any writable destination. - -For example, consider the following code: - -```js -// WARNING! BROKEN! -net.createServer((socket) => { - - // we add an 'end' method, but never consume the data - socket.on('end', () => { - // It will never get here. - socket.end('I got your message (but didnt read it)\n'); - }); - -}).listen(1337); -``` - -In versions of Node.js prior to v0.10, the incoming message data would be -simply discarded. However, in Node.js v0.10 and beyond, -the socket will remain paused forever. - -The workaround in this situation is to call the -[`stream.resume()`][stream-resume] method to start the flow of data: - -```js -// Workaround -net.createServer((socket) => { - - socket.on('end', () => { - socket.end('I got your message (but didnt read it)\n'); - }); - - // start the flow of data, discarding it. - socket.resume(); - -}).listen(1337); -``` - -In addition to new Readable streams switching into flowing mode, -pre-v0.10 style streams can be wrapped in a Readable class using the -[`stream.wrap()`][] method. - - -### Object Mode - - - -Normally, Streams operate on Strings and Buffers exclusively. - -Streams that are in **object mode** can emit generic JavaScript values -other than Buffers and Strings. - -A Readable stream in object mode will always return a single item from -a call to [`stream.read(size)`][stream-read], regardless of what the size -argument is. - -A Writable stream in object mode will always ignore the `encoding` -argument to [`stream.write(data, encoding)`][stream-write]. - -The special value `null` still retains its special value for object -mode streams. That is, for object mode readable streams, `null` as a -return value from [`stream.read()`][stream-read] indicates that there is no more -data, and [`stream.push(null)`][stream-push] will signal the end of stream data -(`EOF`). - -No streams in Node.js core are object mode streams. This pattern is only -used by userland streaming libraries. - -You should set `objectMode` in your stream child class constructor on -the options object. Setting `objectMode` mid-stream is not safe. - -For Duplex streams `objectMode` can be set exclusively for readable or -writable side with `readableObjectMode` and `writableObjectMode` -respectively. These options can be used to implement parsers and -serializers with Transform streams. - -```js -const util = require('util'); -const StringDecoder = require('string_decoder').StringDecoder; -const Transform = require('stream').Transform; -util.inherits(JSONParseStream, Transform); - -// Gets \n-delimited JSON string data, and emits the parsed objects -function JSONParseStream() { - if (!(this instanceof JSONParseStream)) - return new JSONParseStream(); - - Transform.call(this, { readableObjectMode : true }); - - this._buffer = ''; - this._decoder = new StringDecoder('utf8'); -} - -JSONParseStream.prototype._transform = function(chunk, encoding, cb) { - this._buffer += this._decoder.write(chunk); - // split on newlines - var lines = this._buffer.split(/\r?\n/); - // keep the last partial line buffered - this._buffer = lines.pop(); - for (var l = 0; l < lines.length; l++) { - var line = lines[l]; - try { - var obj = JSON.parse(line); - } catch (er) { - this.emit('error', er); - return; - } - // push the parsed object out to the readable consumer - this.push(obj); - } - cb(); -}; - -JSONParseStream.prototype._flush = function(cb) { - // Just handle any leftover - var rem = this._buffer.trim(); - if (rem) { - try { - var obj = JSON.parse(rem); - } catch (er) { - this.emit('error', er); - return; - } - // push the parsed object out to the readable consumer - this.push(obj); - } - cb(); -}; -``` - -### `stream.read(0)` - -There are some cases where you want to trigger a refresh of the -underlying readable stream mechanisms, without actually consuming any -data. In that case, you can call `stream.read(0)`, which will always -return null. - -If the internal read buffer is below the `highWaterMark`, and the -stream is not currently reading, then calling `stream.read(0)` will trigger -a low-level [`stream._read()`][stream-_read] call. - -There is almost never a need to do this. However, you will see some -cases in Node.js's internals where this is done, particularly in the -Readable stream class internals. - -### `stream.push('')` - -Pushing a zero-byte string or Buffer (when not in [Object mode][]) has an -interesting side effect. Because it *is* a call to -[`stream.push()`][stream-push], it will end the `reading` process. However, it -does *not* add any data to the readable buffer, so there's nothing for -a user to consume. - -Very rarely, there are cases where you have no data to provide now, -but the consumer of your stream (or, perhaps, another bit of your own -code) will know when to check again, by calling [`stream.read(0)`][stream-read]. -In those cases, you *may* call `stream.push('')`. - -So far, the only use case for this functionality is in the -[`tls.CryptoStream`][] class, which is deprecated in Node.js/io.js v1.0. If you -find that you have to use `stream.push('')`, please consider another -approach, because it almost certainly indicates that something is -horribly wrong. - -[`'data'`]: #stream_event_data -[`'drain'`]: #stream_event_drain -[`'end'`]: #stream_event_end -[`'finish'`]: #stream_event_finish -[`'readable'`]: #stream_event_readable -[`buf.toString(encoding)`]: https://nodejs.org/docs/v5.8.0/api/buffer.html#buffer_buf_tostring_encoding_start_end -[`EventEmitter`]: https://nodejs.org/docs/v5.8.0/api/events.html#events_class_eventemitter -[`process.stderr`]: https://nodejs.org/docs/v5.8.0/api/process.html#process_process_stderr -[`process.stdin`]: https://nodejs.org/docs/v5.8.0/api/process.html#process_process_stdin -[`process.stdout`]: https://nodejs.org/docs/v5.8.0/api/process.html#process_process_stdout -[`stream.cork()`]: #stream_writable_cork -[`stream.pipe()`]: #stream_readable_pipe_destination_options -[`stream.uncork()`]: #stream_writable_uncork -[`stream.unpipe()`]: #stream_readable_unpipe_destination -[`stream.wrap()`]: #stream_readable_wrap_stream -[`tls.CryptoStream`]: https://nodejs.org/docs/v5.8.0/api/tls.html#tls_class_cryptostream -[`util.inherits()`]: https://nodejs.org/docs/v5.8.0/api/util.html#util_util_inherits_constructor_superconstructor -[API for Stream Consumers]: #stream_api_for_stream_consumers -[API for Stream Implementors]: #stream_api_for_stream_implementors -[child process stdin]: https://nodejs.org/docs/v5.8.0/api/child_process.html#child_process_child_stdin -[child process stdout and stderr]: https://nodejs.org/docs/v5.8.0/api/child_process.html#child_process_child_stdout -[Compatibility]: #stream_compatibility_with_older_node_js_versions -[crypto]: crypto.html -[Duplex]: #stream_class_stream_duplex -[fs read streams]: https://nodejs.org/docs/v5.8.0/api/fs.html#fs_class_fs_readstream -[fs write streams]: https://nodejs.org/docs/v5.8.0/api/fs.html#fs_class_fs_writestream -[HTTP requests, on the client]: https://nodejs.org/docs/v5.8.0/api/http.html#http_class_http_clientrequest -[HTTP responses, on the server]: https://nodejs.org/docs/v5.8.0/api/http.html#http_class_http_serverresponse -[http-incoming-message]: https://nodejs.org/docs/v5.8.0/api/http.html#http_class_http_incomingmessage -[Object mode]: #stream_object_mode -[Readable]: #stream_class_stream_readable -[SimpleProtocol v2]: #stream_example_simpleprotocol_parser_v2 -[stream-_flush]: #stream_transform_flush_callback -[stream-_read]: #stream_readable_read_size_1 -[stream-_transform]: #stream_transform_transform_chunk_encoding_callback -[stream-_write]: #stream_writable_write_chunk_encoding_callback_1 -[stream-_writev]: #stream_writable_writev_chunks_callback -[stream-end]: #stream_writable_end_chunk_encoding_callback -[stream-pause]: #stream_readable_pause -[stream-push]: #stream_readable_push_chunk_encoding -[stream-read]: #stream_readable_read_size -[stream-resume]: #stream_readable_resume -[stream-write]: #stream_writable_write_chunk_encoding_callback -[TCP sockets]: https://nodejs.org/docs/v5.8.0/api/net.html#net_class_net_socket -[Transform]: #stream_class_stream_transform -[Writable]: #stream_class_stream_writable -[zlib]: zlib.html diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/doc/wg-meetings/2015-01-30.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/doc/wg-meetings/2015-01-30.md deleted file mode 100644 index 83275f19..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/doc/wg-meetings/2015-01-30.md +++ /dev/null @@ -1,60 +0,0 @@ -# streams WG Meeting 2015-01-30 - -## Links - -* **Google Hangouts Video**: http://www.youtube.com/watch?v=I9nDOSGfwZg -* **GitHub Issue**: https://github.com/iojs/readable-stream/issues/106 -* **Original Minutes Google Doc**: https://docs.google.com/document/d/17aTgLnjMXIrfjgNaTUnHQO7m3xgzHR2VXBTmi03Qii4/ - -## Agenda - -Extracted from https://github.com/iojs/readable-stream/labels/wg-agenda prior to meeting. - -* adopt a charter [#105](https://github.com/iojs/readable-stream/issues/105) -* release and versioning strategy [#101](https://github.com/iojs/readable-stream/issues/101) -* simpler stream creation [#102](https://github.com/iojs/readable-stream/issues/102) -* proposal: deprecate implicit flowing of streams [#99](https://github.com/iojs/readable-stream/issues/99) - -## Minutes - -### adopt a charter - -* group: +1's all around - -### What versioning scheme should be adopted? -* group: +1’s 3.0.0 -* domenic+group: pulling in patches from other sources where appropriate -* mikeal: version independently, suggesting versions for io.js -* mikeal+domenic: work with TC to notify in advance of changes -simpler stream creation - -### streamline creation of streams -* sam: streamline creation of streams -* domenic: nice simple solution posted - but, we lose the opportunity to change the model - may not be backwards incompatible (double check keys) - - **action item:** domenic will check - -### remove implicit flowing of streams on(‘data’) -* add isFlowing / isPaused -* mikeal: worrying that we’re documenting polyfill methods – confuses users -* domenic: more reflective API is probably good, with warning labels for users -* new section for mad scientists (reflective stream access) -* calvin: name the “third state” -* mikeal: maybe borrow the name from whatwg? -* domenic: we’re missing the “third state” -* consensus: kind of difficult to name the third state -* mikeal: figure out differences in states / compat -* mathias: always flow on data – eliminates third state - * explore what it breaks - -**action items:** -* ask isaac for ability to list packages by what public io.js APIs they use (esp. Stream) -* ask rod/build for infrastructure -* **chris**: explore the “flow on data” approach -* add isPaused/isFlowing -* add new docs section -* move isPaused to that section - - diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/duplex.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/duplex.js deleted file mode 100644 index ca807af8..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/duplex.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./lib/_stream_duplex.js") diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_duplex.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_duplex.js deleted file mode 100644 index 736693b8..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_duplex.js +++ /dev/null @@ -1,75 +0,0 @@ -// a duplex stream is just a stream that is both readable and writable. -// Since JS doesn't have multiple prototypal inheritance, this class -// prototypally inherits from Readable, and then parasitically from -// Writable. - -'use strict'; - -/**/ - -var objectKeys = Object.keys || function (obj) { - var keys = []; - for (var key in obj) { - keys.push(key); - }return keys; -}; -/**/ - -module.exports = Duplex; - -/**/ -var processNextTick = require('process-nextick-args'); -/**/ - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -var Readable = require('./_stream_readable'); -var Writable = require('./_stream_writable'); - -util.inherits(Duplex, Readable); - -var keys = objectKeys(Writable.prototype); -for (var v = 0; v < keys.length; v++) { - var method = keys[v]; - if (!Duplex.prototype[method]) Duplex.prototype[method] = Writable.prototype[method]; -} - -function Duplex(options) { - if (!(this instanceof Duplex)) return new Duplex(options); - - Readable.call(this, options); - Writable.call(this, options); - - if (options && options.readable === false) this.readable = false; - - if (options && options.writable === false) this.writable = false; - - this.allowHalfOpen = true; - if (options && options.allowHalfOpen === false) this.allowHalfOpen = false; - - this.once('end', onend); -} - -// the no-half-open enforcer -function onend() { - // if we allow half-open state, or if the writable side ended, - // then we're ok. - if (this.allowHalfOpen || this._writableState.ended) return; - - // no more data can be written. - // But allow more writes to happen in this tick. - processNextTick(onEndNT, this); -} - -function onEndNT(self) { - self.end(); -} - -function forEach(xs, f) { - for (var i = 0, l = xs.length; i < l; i++) { - f(xs[i], i); - } -} \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_passthrough.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_passthrough.js deleted file mode 100644 index d06f71f1..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_passthrough.js +++ /dev/null @@ -1,26 +0,0 @@ -// a passthrough stream. -// basically just the most minimal sort of Transform stream. -// Every written chunk gets output as-is. - -'use strict'; - -module.exports = PassThrough; - -var Transform = require('./_stream_transform'); - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -util.inherits(PassThrough, Transform); - -function PassThrough(options) { - if (!(this instanceof PassThrough)) return new PassThrough(options); - - Transform.call(this, options); -} - -PassThrough.prototype._transform = function (chunk, encoding, cb) { - cb(null, chunk); -}; \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_readable.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_readable.js deleted file mode 100644 index 54a9d5c5..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_readable.js +++ /dev/null @@ -1,880 +0,0 @@ -'use strict'; - -module.exports = Readable; - -/**/ -var processNextTick = require('process-nextick-args'); -/**/ - -/**/ -var isArray = require('isarray'); -/**/ - -/**/ -var Buffer = require('buffer').Buffer; -/**/ - -Readable.ReadableState = ReadableState; - -var EE = require('events'); - -/**/ -var EElistenerCount = function (emitter, type) { - return emitter.listeners(type).length; -}; -/**/ - -/**/ -var Stream; -(function () { - try { - Stream = require('st' + 'ream'); - } catch (_) {} finally { - if (!Stream) Stream = require('events').EventEmitter; - } -})(); -/**/ - -var Buffer = require('buffer').Buffer; - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -/**/ -var debugUtil = require('util'); -var debug = undefined; -if (debugUtil && debugUtil.debuglog) { - debug = debugUtil.debuglog('stream'); -} else { - debug = function () {}; -} -/**/ - -var StringDecoder; - -util.inherits(Readable, Stream); - -var Duplex; -function ReadableState(options, stream) { - Duplex = Duplex || require('./_stream_duplex'); - - options = options || {}; - - // object stream flag. Used to make read(n) ignore n and to - // make all the buffer merging and length checks go away - this.objectMode = !!options.objectMode; - - if (stream instanceof Duplex) this.objectMode = this.objectMode || !!options.readableObjectMode; - - // the point at which it stops calling _read() to fill the buffer - // Note: 0 is a valid value, means "don't call _read preemptively ever" - var hwm = options.highWaterMark; - var defaultHwm = this.objectMode ? 16 : 16 * 1024; - this.highWaterMark = hwm || hwm === 0 ? hwm : defaultHwm; - - // cast to ints. - this.highWaterMark = ~ ~this.highWaterMark; - - this.buffer = []; - this.length = 0; - this.pipes = null; - this.pipesCount = 0; - this.flowing = null; - this.ended = false; - this.endEmitted = false; - this.reading = false; - - // a flag to be able to tell if the onwrite cb is called immediately, - // or on a later tick. We set this to true at first, because any - // actions that shouldn't happen until "later" should generally also - // not happen before the first write call. - this.sync = true; - - // whenever we return null, then we set a flag to say - // that we're awaiting a 'readable' event emission. - this.needReadable = false; - this.emittedReadable = false; - this.readableListening = false; - this.resumeScheduled = false; - - // Crypto is kind of old and crusty. Historically, its default string - // encoding is 'binary' so we have to make this configurable. - // Everything else in the universe uses 'utf8', though. - this.defaultEncoding = options.defaultEncoding || 'utf8'; - - // when piping, we only care about 'readable' events that happen - // after read()ing all the bytes and not getting any pushback. - this.ranOut = false; - - // the number of writers that are awaiting a drain event in .pipe()s - this.awaitDrain = 0; - - // if true, a maybeReadMore has been scheduled - this.readingMore = false; - - this.decoder = null; - this.encoding = null; - if (options.encoding) { - if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder; - this.decoder = new StringDecoder(options.encoding); - this.encoding = options.encoding; - } -} - -var Duplex; -function Readable(options) { - Duplex = Duplex || require('./_stream_duplex'); - - if (!(this instanceof Readable)) return new Readable(options); - - this._readableState = new ReadableState(options, this); - - // legacy - this.readable = true; - - if (options && typeof options.read === 'function') this._read = options.read; - - Stream.call(this); -} - -// Manually shove something into the read() buffer. -// This returns true if the highWaterMark has not been hit yet, -// similar to how Writable.write() returns true if you should -// write() some more. -Readable.prototype.push = function (chunk, encoding) { - var state = this._readableState; - - if (!state.objectMode && typeof chunk === 'string') { - encoding = encoding || state.defaultEncoding; - if (encoding !== state.encoding) { - chunk = new Buffer(chunk, encoding); - encoding = ''; - } - } - - return readableAddChunk(this, state, chunk, encoding, false); -}; - -// Unshift should *always* be something directly out of read() -Readable.prototype.unshift = function (chunk) { - var state = this._readableState; - return readableAddChunk(this, state, chunk, '', true); -}; - -Readable.prototype.isPaused = function () { - return this._readableState.flowing === false; -}; - -function readableAddChunk(stream, state, chunk, encoding, addToFront) { - var er = chunkInvalid(state, chunk); - if (er) { - stream.emit('error', er); - } else if (chunk === null) { - state.reading = false; - onEofChunk(stream, state); - } else if (state.objectMode || chunk && chunk.length > 0) { - if (state.ended && !addToFront) { - var e = new Error('stream.push() after EOF'); - stream.emit('error', e); - } else if (state.endEmitted && addToFront) { - var e = new Error('stream.unshift() after end event'); - stream.emit('error', e); - } else { - var skipAdd; - if (state.decoder && !addToFront && !encoding) { - chunk = state.decoder.write(chunk); - skipAdd = !state.objectMode && chunk.length === 0; - } - - if (!addToFront) state.reading = false; - - // Don't add to the buffer if we've decoded to an empty string chunk and - // we're not in object mode - if (!skipAdd) { - // if we want the data now, just emit it. - if (state.flowing && state.length === 0 && !state.sync) { - stream.emit('data', chunk); - stream.read(0); - } else { - // update the buffer info. - state.length += state.objectMode ? 1 : chunk.length; - if (addToFront) state.buffer.unshift(chunk);else state.buffer.push(chunk); - - if (state.needReadable) emitReadable(stream); - } - } - - maybeReadMore(stream, state); - } - } else if (!addToFront) { - state.reading = false; - } - - return needMoreData(state); -} - -// if it's past the high water mark, we can push in some more. -// Also, if we have no data yet, we can stand some -// more bytes. This is to work around cases where hwm=0, -// such as the repl. Also, if the push() triggered a -// readable event, and the user called read(largeNumber) such that -// needReadable was set, then we ought to push more, so that another -// 'readable' event will be triggered. -function needMoreData(state) { - return !state.ended && (state.needReadable || state.length < state.highWaterMark || state.length === 0); -} - -// backwards compatibility. -Readable.prototype.setEncoding = function (enc) { - if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder; - this._readableState.decoder = new StringDecoder(enc); - this._readableState.encoding = enc; - return this; -}; - -// Don't raise the hwm > 8MB -var MAX_HWM = 0x800000; -function computeNewHighWaterMark(n) { - if (n >= MAX_HWM) { - n = MAX_HWM; - } else { - // Get the next highest power of 2 - n--; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - n++; - } - return n; -} - -function howMuchToRead(n, state) { - if (state.length === 0 && state.ended) return 0; - - if (state.objectMode) return n === 0 ? 0 : 1; - - if (n === null || isNaN(n)) { - // only flow one buffer at a time - if (state.flowing && state.buffer.length) return state.buffer[0].length;else return state.length; - } - - if (n <= 0) return 0; - - // If we're asking for more than the target buffer level, - // then raise the water mark. Bump up to the next highest - // power of 2, to prevent increasing it excessively in tiny - // amounts. - if (n > state.highWaterMark) state.highWaterMark = computeNewHighWaterMark(n); - - // don't have that much. return null, unless we've ended. - if (n > state.length) { - if (!state.ended) { - state.needReadable = true; - return 0; - } else { - return state.length; - } - } - - return n; -} - -// you can override either this method, or the async _read(n) below. -Readable.prototype.read = function (n) { - debug('read', n); - var state = this._readableState; - var nOrig = n; - - if (typeof n !== 'number' || n > 0) state.emittedReadable = false; - - // if we're doing read(0) to trigger a readable event, but we - // already have a bunch of data in the buffer, then just trigger - // the 'readable' event and move on. - if (n === 0 && state.needReadable && (state.length >= state.highWaterMark || state.ended)) { - debug('read: emitReadable', state.length, state.ended); - if (state.length === 0 && state.ended) endReadable(this);else emitReadable(this); - return null; - } - - n = howMuchToRead(n, state); - - // if we've ended, and we're now clear, then finish it up. - if (n === 0 && state.ended) { - if (state.length === 0) endReadable(this); - return null; - } - - // All the actual chunk generation logic needs to be - // *below* the call to _read. The reason is that in certain - // synthetic stream cases, such as passthrough streams, _read - // may be a completely synchronous operation which may change - // the state of the read buffer, providing enough data when - // before there was *not* enough. - // - // So, the steps are: - // 1. Figure out what the state of things will be after we do - // a read from the buffer. - // - // 2. If that resulting state will trigger a _read, then call _read. - // Note that this may be asynchronous, or synchronous. Yes, it is - // deeply ugly to write APIs this way, but that still doesn't mean - // that the Readable class should behave improperly, as streams are - // designed to be sync/async agnostic. - // Take note if the _read call is sync or async (ie, if the read call - // has returned yet), so that we know whether or not it's safe to emit - // 'readable' etc. - // - // 3. Actually pull the requested chunks out of the buffer and return. - - // if we need a readable event, then we need to do some reading. - var doRead = state.needReadable; - debug('need readable', doRead); - - // if we currently have less than the highWaterMark, then also read some - if (state.length === 0 || state.length - n < state.highWaterMark) { - doRead = true; - debug('length less than watermark', doRead); - } - - // however, if we've ended, then there's no point, and if we're already - // reading, then it's unnecessary. - if (state.ended || state.reading) { - doRead = false; - debug('reading or ended', doRead); - } - - if (doRead) { - debug('do read'); - state.reading = true; - state.sync = true; - // if the length is currently zero, then we *need* a readable event. - if (state.length === 0) state.needReadable = true; - // call internal read method - this._read(state.highWaterMark); - state.sync = false; - } - - // If _read pushed data synchronously, then `reading` will be false, - // and we need to re-evaluate how much data we can return to the user. - if (doRead && !state.reading) n = howMuchToRead(nOrig, state); - - var ret; - if (n > 0) ret = fromList(n, state);else ret = null; - - if (ret === null) { - state.needReadable = true; - n = 0; - } - - state.length -= n; - - // If we have nothing in the buffer, then we want to know - // as soon as we *do* get something into the buffer. - if (state.length === 0 && !state.ended) state.needReadable = true; - - // If we tried to read() past the EOF, then emit end on the next tick. - if (nOrig !== n && state.ended && state.length === 0) endReadable(this); - - if (ret !== null) this.emit('data', ret); - - return ret; -}; - -function chunkInvalid(state, chunk) { - var er = null; - if (!Buffer.isBuffer(chunk) && typeof chunk !== 'string' && chunk !== null && chunk !== undefined && !state.objectMode) { - er = new TypeError('Invalid non-string/buffer chunk'); - } - return er; -} - -function onEofChunk(stream, state) { - if (state.ended) return; - if (state.decoder) { - var chunk = state.decoder.end(); - if (chunk && chunk.length) { - state.buffer.push(chunk); - state.length += state.objectMode ? 1 : chunk.length; - } - } - state.ended = true; - - // emit 'readable' now to make sure it gets picked up. - emitReadable(stream); -} - -// Don't emit readable right away in sync mode, because this can trigger -// another read() call => stack overflow. This way, it might trigger -// a nextTick recursion warning, but that's not so bad. -function emitReadable(stream) { - var state = stream._readableState; - state.needReadable = false; - if (!state.emittedReadable) { - debug('emitReadable', state.flowing); - state.emittedReadable = true; - if (state.sync) processNextTick(emitReadable_, stream);else emitReadable_(stream); - } -} - -function emitReadable_(stream) { - debug('emit readable'); - stream.emit('readable'); - flow(stream); -} - -// at this point, the user has presumably seen the 'readable' event, -// and called read() to consume some data. that may have triggered -// in turn another _read(n) call, in which case reading = true if -// it's in progress. -// However, if we're not ended, or reading, and the length < hwm, -// then go ahead and try to read some more preemptively. -function maybeReadMore(stream, state) { - if (!state.readingMore) { - state.readingMore = true; - processNextTick(maybeReadMore_, stream, state); - } -} - -function maybeReadMore_(stream, state) { - var len = state.length; - while (!state.reading && !state.flowing && !state.ended && state.length < state.highWaterMark) { - debug('maybeReadMore read 0'); - stream.read(0); - if (len === state.length) - // didn't get any data, stop spinning. - break;else len = state.length; - } - state.readingMore = false; -} - -// abstract method. to be overridden in specific implementation classes. -// call cb(er, data) where data is <= n in length. -// for virtual (non-string, non-buffer) streams, "length" is somewhat -// arbitrary, and perhaps not very meaningful. -Readable.prototype._read = function (n) { - this.emit('error', new Error('not implemented')); -}; - -Readable.prototype.pipe = function (dest, pipeOpts) { - var src = this; - var state = this._readableState; - - switch (state.pipesCount) { - case 0: - state.pipes = dest; - break; - case 1: - state.pipes = [state.pipes, dest]; - break; - default: - state.pipes.push(dest); - break; - } - state.pipesCount += 1; - debug('pipe count=%d opts=%j', state.pipesCount, pipeOpts); - - var doEnd = (!pipeOpts || pipeOpts.end !== false) && dest !== process.stdout && dest !== process.stderr; - - var endFn = doEnd ? onend : cleanup; - if (state.endEmitted) processNextTick(endFn);else src.once('end', endFn); - - dest.on('unpipe', onunpipe); - function onunpipe(readable) { - debug('onunpipe'); - if (readable === src) { - cleanup(); - } - } - - function onend() { - debug('onend'); - dest.end(); - } - - // when the dest drains, it reduces the awaitDrain counter - // on the source. This would be more elegant with a .once() - // handler in flow(), but adding and removing repeatedly is - // too slow. - var ondrain = pipeOnDrain(src); - dest.on('drain', ondrain); - - var cleanedUp = false; - function cleanup() { - debug('cleanup'); - // cleanup event handlers once the pipe is broken - dest.removeListener('close', onclose); - dest.removeListener('finish', onfinish); - dest.removeListener('drain', ondrain); - dest.removeListener('error', onerror); - dest.removeListener('unpipe', onunpipe); - src.removeListener('end', onend); - src.removeListener('end', cleanup); - src.removeListener('data', ondata); - - cleanedUp = true; - - // if the reader is waiting for a drain event from this - // specific writer, then it would cause it to never start - // flowing again. - // So, if this is awaiting a drain, then we just call it now. - // If we don't know, then assume that we are waiting for one. - if (state.awaitDrain && (!dest._writableState || dest._writableState.needDrain)) ondrain(); - } - - src.on('data', ondata); - function ondata(chunk) { - debug('ondata'); - var ret = dest.write(chunk); - if (false === ret) { - // If the user unpiped during `dest.write()`, it is possible - // to get stuck in a permanently paused state if that write - // also returned false. - if (state.pipesCount === 1 && state.pipes[0] === dest && src.listenerCount('data') === 1 && !cleanedUp) { - debug('false write response, pause', src._readableState.awaitDrain); - src._readableState.awaitDrain++; - } - src.pause(); - } - } - - // if the dest has an error, then stop piping into it. - // however, don't suppress the throwing behavior for this. - function onerror(er) { - debug('onerror', er); - unpipe(); - dest.removeListener('error', onerror); - if (EElistenerCount(dest, 'error') === 0) dest.emit('error', er); - } - // This is a brutally ugly hack to make sure that our error handler - // is attached before any userland ones. NEVER DO THIS. - if (!dest._events || !dest._events.error) dest.on('error', onerror);else if (isArray(dest._events.error)) dest._events.error.unshift(onerror);else dest._events.error = [onerror, dest._events.error]; - - // Both close and finish should trigger unpipe, but only once. - function onclose() { - dest.removeListener('finish', onfinish); - unpipe(); - } - dest.once('close', onclose); - function onfinish() { - debug('onfinish'); - dest.removeListener('close', onclose); - unpipe(); - } - dest.once('finish', onfinish); - - function unpipe() { - debug('unpipe'); - src.unpipe(dest); - } - - // tell the dest that it's being piped to - dest.emit('pipe', src); - - // start the flow if it hasn't been started already. - if (!state.flowing) { - debug('pipe resume'); - src.resume(); - } - - return dest; -}; - -function pipeOnDrain(src) { - return function () { - var state = src._readableState; - debug('pipeOnDrain', state.awaitDrain); - if (state.awaitDrain) state.awaitDrain--; - if (state.awaitDrain === 0 && EElistenerCount(src, 'data')) { - state.flowing = true; - flow(src); - } - }; -} - -Readable.prototype.unpipe = function (dest) { - var state = this._readableState; - - // if we're not piping anywhere, then do nothing. - if (state.pipesCount === 0) return this; - - // just one destination. most common case. - if (state.pipesCount === 1) { - // passed in one, but it's not the right one. - if (dest && dest !== state.pipes) return this; - - if (!dest) dest = state.pipes; - - // got a match. - state.pipes = null; - state.pipesCount = 0; - state.flowing = false; - if (dest) dest.emit('unpipe', this); - return this; - } - - // slow case. multiple pipe destinations. - - if (!dest) { - // remove all. - var dests = state.pipes; - var len = state.pipesCount; - state.pipes = null; - state.pipesCount = 0; - state.flowing = false; - - for (var _i = 0; _i < len; _i++) { - dests[_i].emit('unpipe', this); - }return this; - } - - // try to find the right one. - var i = indexOf(state.pipes, dest); - if (i === -1) return this; - - state.pipes.splice(i, 1); - state.pipesCount -= 1; - if (state.pipesCount === 1) state.pipes = state.pipes[0]; - - dest.emit('unpipe', this); - - return this; -}; - -// set up data events if they are asked for -// Ensure readable listeners eventually get something -Readable.prototype.on = function (ev, fn) { - var res = Stream.prototype.on.call(this, ev, fn); - - // If listening to data, and it has not explicitly been paused, - // then call resume to start the flow of data on the next tick. - if (ev === 'data' && false !== this._readableState.flowing) { - this.resume(); - } - - if (ev === 'readable' && !this._readableState.endEmitted) { - var state = this._readableState; - if (!state.readableListening) { - state.readableListening = true; - state.emittedReadable = false; - state.needReadable = true; - if (!state.reading) { - processNextTick(nReadingNextTick, this); - } else if (state.length) { - emitReadable(this, state); - } - } - } - - return res; -}; -Readable.prototype.addListener = Readable.prototype.on; - -function nReadingNextTick(self) { - debug('readable nexttick read 0'); - self.read(0); -} - -// pause() and resume() are remnants of the legacy readable stream API -// If the user uses them, then switch into old mode. -Readable.prototype.resume = function () { - var state = this._readableState; - if (!state.flowing) { - debug('resume'); - state.flowing = true; - resume(this, state); - } - return this; -}; - -function resume(stream, state) { - if (!state.resumeScheduled) { - state.resumeScheduled = true; - processNextTick(resume_, stream, state); - } -} - -function resume_(stream, state) { - if (!state.reading) { - debug('resume read 0'); - stream.read(0); - } - - state.resumeScheduled = false; - stream.emit('resume'); - flow(stream); - if (state.flowing && !state.reading) stream.read(0); -} - -Readable.prototype.pause = function () { - debug('call pause flowing=%j', this._readableState.flowing); - if (false !== this._readableState.flowing) { - debug('pause'); - this._readableState.flowing = false; - this.emit('pause'); - } - return this; -}; - -function flow(stream) { - var state = stream._readableState; - debug('flow', state.flowing); - if (state.flowing) { - do { - var chunk = stream.read(); - } while (null !== chunk && state.flowing); - } -} - -// wrap an old-style stream as the async data source. -// This is *not* part of the readable stream interface. -// It is an ugly unfortunate mess of history. -Readable.prototype.wrap = function (stream) { - var state = this._readableState; - var paused = false; - - var self = this; - stream.on('end', function () { - debug('wrapped end'); - if (state.decoder && !state.ended) { - var chunk = state.decoder.end(); - if (chunk && chunk.length) self.push(chunk); - } - - self.push(null); - }); - - stream.on('data', function (chunk) { - debug('wrapped data'); - if (state.decoder) chunk = state.decoder.write(chunk); - - // don't skip over falsy values in objectMode - if (state.objectMode && (chunk === null || chunk === undefined)) return;else if (!state.objectMode && (!chunk || !chunk.length)) return; - - var ret = self.push(chunk); - if (!ret) { - paused = true; - stream.pause(); - } - }); - - // proxy all the other methods. - // important when wrapping filters and duplexes. - for (var i in stream) { - if (this[i] === undefined && typeof stream[i] === 'function') { - this[i] = function (method) { - return function () { - return stream[method].apply(stream, arguments); - }; - }(i); - } - } - - // proxy certain important events. - var events = ['error', 'close', 'destroy', 'pause', 'resume']; - forEach(events, function (ev) { - stream.on(ev, self.emit.bind(self, ev)); - }); - - // when we try to consume some more bytes, simply unpause the - // underlying stream. - self._read = function (n) { - debug('wrapped _read', n); - if (paused) { - paused = false; - stream.resume(); - } - }; - - return self; -}; - -// exposed for testing purposes only. -Readable._fromList = fromList; - -// Pluck off n bytes from an array of buffers. -// Length is the combined lengths of all the buffers in the list. -function fromList(n, state) { - var list = state.buffer; - var length = state.length; - var stringMode = !!state.decoder; - var objectMode = !!state.objectMode; - var ret; - - // nothing in the list, definitely empty. - if (list.length === 0) return null; - - if (length === 0) ret = null;else if (objectMode) ret = list.shift();else if (!n || n >= length) { - // read it all, truncate the array. - if (stringMode) ret = list.join('');else if (list.length === 1) ret = list[0];else ret = Buffer.concat(list, length); - list.length = 0; - } else { - // read just some of it. - if (n < list[0].length) { - // just take a part of the first list item. - // slice is the same for buffers and strings. - var buf = list[0]; - ret = buf.slice(0, n); - list[0] = buf.slice(n); - } else if (n === list[0].length) { - // first list is a perfect match - ret = list.shift(); - } else { - // complex case. - // we have enough to cover it, but it spans past the first buffer. - if (stringMode) ret = '';else ret = new Buffer(n); - - var c = 0; - for (var i = 0, l = list.length; i < l && c < n; i++) { - var buf = list[0]; - var cpy = Math.min(n - c, buf.length); - - if (stringMode) ret += buf.slice(0, cpy);else buf.copy(ret, c, 0, cpy); - - if (cpy < buf.length) list[0] = buf.slice(cpy);else list.shift(); - - c += cpy; - } - } - } - - return ret; -} - -function endReadable(stream) { - var state = stream._readableState; - - // If we get here before consuming all the bytes, then that is a - // bug in node. Should never happen. - if (state.length > 0) throw new Error('endReadable called on non-empty stream'); - - if (!state.endEmitted) { - state.ended = true; - processNextTick(endReadableNT, state, stream); - } -} - -function endReadableNT(state, stream) { - // Check that we didn't get one last unshift. - if (!state.endEmitted && state.length === 0) { - state.endEmitted = true; - stream.readable = false; - stream.emit('end'); - } -} - -function forEach(xs, f) { - for (var i = 0, l = xs.length; i < l; i++) { - f(xs[i], i); - } -} - -function indexOf(xs, x) { - for (var i = 0, l = xs.length; i < l; i++) { - if (xs[i] === x) return i; - } - return -1; -} \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_transform.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_transform.js deleted file mode 100644 index 625cdc17..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_transform.js +++ /dev/null @@ -1,180 +0,0 @@ -// a transform stream is a readable/writable stream where you do -// something with the data. Sometimes it's called a "filter", -// but that's not a great name for it, since that implies a thing where -// some bits pass through, and others are simply ignored. (That would -// be a valid example of a transform, of course.) -// -// While the output is causally related to the input, it's not a -// necessarily symmetric or synchronous transformation. For example, -// a zlib stream might take multiple plain-text writes(), and then -// emit a single compressed chunk some time in the future. -// -// Here's how this works: -// -// The Transform stream has all the aspects of the readable and writable -// stream classes. When you write(chunk), that calls _write(chunk,cb) -// internally, and returns false if there's a lot of pending writes -// buffered up. When you call read(), that calls _read(n) until -// there's enough pending readable data buffered up. -// -// In a transform stream, the written data is placed in a buffer. When -// _read(n) is called, it transforms the queued up data, calling the -// buffered _write cb's as it consumes chunks. If consuming a single -// written chunk would result in multiple output chunks, then the first -// outputted bit calls the readcb, and subsequent chunks just go into -// the read buffer, and will cause it to emit 'readable' if necessary. -// -// This way, back-pressure is actually determined by the reading side, -// since _read has to be called to start processing a new chunk. However, -// a pathological inflate type of transform can cause excessive buffering -// here. For example, imagine a stream where every byte of input is -// interpreted as an integer from 0-255, and then results in that many -// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in -// 1kb of data being output. In this case, you could write a very small -// amount of input, and end up with a very large amount of output. In -// such a pathological inflating mechanism, there'd be no way to tell -// the system to stop doing the transform. A single 4MB write could -// cause the system to run out of memory. -// -// However, even in such a pathological case, only a single written chunk -// would be consumed, and then the rest would wait (un-transformed) until -// the results of the previous transformed chunk were consumed. - -'use strict'; - -module.exports = Transform; - -var Duplex = require('./_stream_duplex'); - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -util.inherits(Transform, Duplex); - -function TransformState(stream) { - this.afterTransform = function (er, data) { - return afterTransform(stream, er, data); - }; - - this.needTransform = false; - this.transforming = false; - this.writecb = null; - this.writechunk = null; - this.writeencoding = null; -} - -function afterTransform(stream, er, data) { - var ts = stream._transformState; - ts.transforming = false; - - var cb = ts.writecb; - - if (!cb) return stream.emit('error', new Error('no writecb in Transform class')); - - ts.writechunk = null; - ts.writecb = null; - - if (data !== null && data !== undefined) stream.push(data); - - cb(er); - - var rs = stream._readableState; - rs.reading = false; - if (rs.needReadable || rs.length < rs.highWaterMark) { - stream._read(rs.highWaterMark); - } -} - -function Transform(options) { - if (!(this instanceof Transform)) return new Transform(options); - - Duplex.call(this, options); - - this._transformState = new TransformState(this); - - // when the writable side finishes, then flush out anything remaining. - var stream = this; - - // start out asking for a readable event once data is transformed. - this._readableState.needReadable = true; - - // we have implemented the _read method, and done the other things - // that Readable wants before the first _read call, so unset the - // sync guard flag. - this._readableState.sync = false; - - if (options) { - if (typeof options.transform === 'function') this._transform = options.transform; - - if (typeof options.flush === 'function') this._flush = options.flush; - } - - this.once('prefinish', function () { - if (typeof this._flush === 'function') this._flush(function (er) { - done(stream, er); - });else done(stream); - }); -} - -Transform.prototype.push = function (chunk, encoding) { - this._transformState.needTransform = false; - return Duplex.prototype.push.call(this, chunk, encoding); -}; - -// This is the part where you do stuff! -// override this function in implementation classes. -// 'chunk' is an input chunk. -// -// Call `push(newChunk)` to pass along transformed output -// to the readable side. You may call 'push' zero or more times. -// -// Call `cb(err)` when you are done with this chunk. If you pass -// an error, then that'll put the hurt on the whole operation. If you -// never call cb(), then you'll never get another chunk. -Transform.prototype._transform = function (chunk, encoding, cb) { - throw new Error('not implemented'); -}; - -Transform.prototype._write = function (chunk, encoding, cb) { - var ts = this._transformState; - ts.writecb = cb; - ts.writechunk = chunk; - ts.writeencoding = encoding; - if (!ts.transforming) { - var rs = this._readableState; - if (ts.needTransform || rs.needReadable || rs.length < rs.highWaterMark) this._read(rs.highWaterMark); - } -}; - -// Doesn't matter what the args are here. -// _transform does all the work. -// That we got here means that the readable side wants more data. -Transform.prototype._read = function (n) { - var ts = this._transformState; - - if (ts.writechunk !== null && ts.writecb && !ts.transforming) { - ts.transforming = true; - this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform); - } else { - // mark that we need a transform, so that any data that comes in - // will get processed, now that we've asked for it. - ts.needTransform = true; - } -}; - -function done(stream, er) { - if (er) return stream.emit('error', er); - - // if there's nothing in the write buffer, then that means - // that nothing more will ever be provided - var ws = stream._writableState; - var ts = stream._transformState; - - if (ws.length) throw new Error('calling transform done when ws.length != 0'); - - if (ts.transforming) throw new Error('calling transform done when still transforming'); - - return stream.push(null); -} \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_writable.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_writable.js deleted file mode 100644 index 95916c99..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_writable.js +++ /dev/null @@ -1,516 +0,0 @@ -// A bit simpler than readable streams. -// Implement an async ._write(chunk, encoding, cb), and it'll handle all -// the drain event emission and buffering. - -'use strict'; - -module.exports = Writable; - -/**/ -var processNextTick = require('process-nextick-args'); -/**/ - -/**/ -var asyncWrite = !process.browser && ['v0.10', 'v0.9.'].indexOf(process.version.slice(0, 5)) > -1 ? setImmediate : processNextTick; -/**/ - -/**/ -var Buffer = require('buffer').Buffer; -/**/ - -Writable.WritableState = WritableState; - -/**/ -var util = require('core-util-is'); -util.inherits = require('inherits'); -/**/ - -/**/ -var internalUtil = { - deprecate: require('util-deprecate') -}; -/**/ - -/**/ -var Stream; -(function () { - try { - Stream = require('st' + 'ream'); - } catch (_) {} finally { - if (!Stream) Stream = require('events').EventEmitter; - } -})(); -/**/ - -var Buffer = require('buffer').Buffer; - -util.inherits(Writable, Stream); - -function nop() {} - -function WriteReq(chunk, encoding, cb) { - this.chunk = chunk; - this.encoding = encoding; - this.callback = cb; - this.next = null; -} - -var Duplex; -function WritableState(options, stream) { - Duplex = Duplex || require('./_stream_duplex'); - - options = options || {}; - - // object stream flag to indicate whether or not this stream - // contains buffers or objects. - this.objectMode = !!options.objectMode; - - if (stream instanceof Duplex) this.objectMode = this.objectMode || !!options.writableObjectMode; - - // the point at which write() starts returning false - // Note: 0 is a valid value, means that we always return false if - // the entire buffer is not flushed immediately on write() - var hwm = options.highWaterMark; - var defaultHwm = this.objectMode ? 16 : 16 * 1024; - this.highWaterMark = hwm || hwm === 0 ? hwm : defaultHwm; - - // cast to ints. - this.highWaterMark = ~ ~this.highWaterMark; - - this.needDrain = false; - // at the start of calling end() - this.ending = false; - // when end() has been called, and returned - this.ended = false; - // when 'finish' is emitted - this.finished = false; - - // should we decode strings into buffers before passing to _write? - // this is here so that some node-core streams can optimize string - // handling at a lower level. - var noDecode = options.decodeStrings === false; - this.decodeStrings = !noDecode; - - // Crypto is kind of old and crusty. Historically, its default string - // encoding is 'binary' so we have to make this configurable. - // Everything else in the universe uses 'utf8', though. - this.defaultEncoding = options.defaultEncoding || 'utf8'; - - // not an actual buffer we keep track of, but a measurement - // of how much we're waiting to get pushed to some underlying - // socket or file. - this.length = 0; - - // a flag to see when we're in the middle of a write. - this.writing = false; - - // when true all writes will be buffered until .uncork() call - this.corked = 0; - - // a flag to be able to tell if the onwrite cb is called immediately, - // or on a later tick. We set this to true at first, because any - // actions that shouldn't happen until "later" should generally also - // not happen before the first write call. - this.sync = true; - - // a flag to know if we're processing previously buffered items, which - // may call the _write() callback in the same tick, so that we don't - // end up in an overlapped onwrite situation. - this.bufferProcessing = false; - - // the callback that's passed to _write(chunk,cb) - this.onwrite = function (er) { - onwrite(stream, er); - }; - - // the callback that the user supplies to write(chunk,encoding,cb) - this.writecb = null; - - // the amount that is being written when _write is called. - this.writelen = 0; - - this.bufferedRequest = null; - this.lastBufferedRequest = null; - - // number of pending user-supplied write callbacks - // this must be 0 before 'finish' can be emitted - this.pendingcb = 0; - - // emit prefinish if the only thing we're waiting for is _write cbs - // This is relevant for synchronous Transform streams - this.prefinished = false; - - // True if the error was already emitted and should not be thrown again - this.errorEmitted = false; - - // count buffered requests - this.bufferedRequestCount = 0; - - // create the two objects needed to store the corked requests - // they are not a linked list, as no new elements are inserted in there - this.corkedRequestsFree = new CorkedRequest(this); - this.corkedRequestsFree.next = new CorkedRequest(this); -} - -WritableState.prototype.getBuffer = function writableStateGetBuffer() { - var current = this.bufferedRequest; - var out = []; - while (current) { - out.push(current); - current = current.next; - } - return out; -}; - -(function () { - try { - Object.defineProperty(WritableState.prototype, 'buffer', { - get: internalUtil.deprecate(function () { - return this.getBuffer(); - }, '_writableState.buffer is deprecated. Use _writableState.getBuffer ' + 'instead.') - }); - } catch (_) {} -})(); - -var Duplex; -function Writable(options) { - Duplex = Duplex || require('./_stream_duplex'); - - // Writable ctor is applied to Duplexes, though they're not - // instanceof Writable, they're instanceof Readable. - if (!(this instanceof Writable) && !(this instanceof Duplex)) return new Writable(options); - - this._writableState = new WritableState(options, this); - - // legacy. - this.writable = true; - - if (options) { - if (typeof options.write === 'function') this._write = options.write; - - if (typeof options.writev === 'function') this._writev = options.writev; - } - - Stream.call(this); -} - -// Otherwise people can pipe Writable streams, which is just wrong. -Writable.prototype.pipe = function () { - this.emit('error', new Error('Cannot pipe. Not readable.')); -}; - -function writeAfterEnd(stream, cb) { - var er = new Error('write after end'); - // TODO: defer error events consistently everywhere, not just the cb - stream.emit('error', er); - processNextTick(cb, er); -} - -// If we get something that is not a buffer, string, null, or undefined, -// and we're not in objectMode, then that's an error. -// Otherwise stream chunks are all considered to be of length=1, and the -// watermarks determine how many objects to keep in the buffer, rather than -// how many bytes or characters. -function validChunk(stream, state, chunk, cb) { - var valid = true; - - if (!Buffer.isBuffer(chunk) && typeof chunk !== 'string' && chunk !== null && chunk !== undefined && !state.objectMode) { - var er = new TypeError('Invalid non-string/buffer chunk'); - stream.emit('error', er); - processNextTick(cb, er); - valid = false; - } - return valid; -} - -Writable.prototype.write = function (chunk, encoding, cb) { - var state = this._writableState; - var ret = false; - - if (typeof encoding === 'function') { - cb = encoding; - encoding = null; - } - - if (Buffer.isBuffer(chunk)) encoding = 'buffer';else if (!encoding) encoding = state.defaultEncoding; - - if (typeof cb !== 'function') cb = nop; - - if (state.ended) writeAfterEnd(this, cb);else if (validChunk(this, state, chunk, cb)) { - state.pendingcb++; - ret = writeOrBuffer(this, state, chunk, encoding, cb); - } - - return ret; -}; - -Writable.prototype.cork = function () { - var state = this._writableState; - - state.corked++; -}; - -Writable.prototype.uncork = function () { - var state = this._writableState; - - if (state.corked) { - state.corked--; - - if (!state.writing && !state.corked && !state.finished && !state.bufferProcessing && state.bufferedRequest) clearBuffer(this, state); - } -}; - -Writable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) { - // node::ParseEncoding() requires lower case. - if (typeof encoding === 'string') encoding = encoding.toLowerCase(); - if (!(['hex', 'utf8', 'utf-8', 'ascii', 'binary', 'base64', 'ucs2', 'ucs-2', 'utf16le', 'utf-16le', 'raw'].indexOf((encoding + '').toLowerCase()) > -1)) throw new TypeError('Unknown encoding: ' + encoding); - this._writableState.defaultEncoding = encoding; -}; - -function decodeChunk(state, chunk, encoding) { - if (!state.objectMode && state.decodeStrings !== false && typeof chunk === 'string') { - chunk = new Buffer(chunk, encoding); - } - return chunk; -} - -// if we're already writing something, then just put this -// in the queue, and wait our turn. Otherwise, call _write -// If we return false, then we need a drain event, so set that flag. -function writeOrBuffer(stream, state, chunk, encoding, cb) { - chunk = decodeChunk(state, chunk, encoding); - - if (Buffer.isBuffer(chunk)) encoding = 'buffer'; - var len = state.objectMode ? 1 : chunk.length; - - state.length += len; - - var ret = state.length < state.highWaterMark; - // we must ensure that previous needDrain will not be reset to false. - if (!ret) state.needDrain = true; - - if (state.writing || state.corked) { - var last = state.lastBufferedRequest; - state.lastBufferedRequest = new WriteReq(chunk, encoding, cb); - if (last) { - last.next = state.lastBufferedRequest; - } else { - state.bufferedRequest = state.lastBufferedRequest; - } - state.bufferedRequestCount += 1; - } else { - doWrite(stream, state, false, len, chunk, encoding, cb); - } - - return ret; -} - -function doWrite(stream, state, writev, len, chunk, encoding, cb) { - state.writelen = len; - state.writecb = cb; - state.writing = true; - state.sync = true; - if (writev) stream._writev(chunk, state.onwrite);else stream._write(chunk, encoding, state.onwrite); - state.sync = false; -} - -function onwriteError(stream, state, sync, er, cb) { - --state.pendingcb; - if (sync) processNextTick(cb, er);else cb(er); - - stream._writableState.errorEmitted = true; - stream.emit('error', er); -} - -function onwriteStateUpdate(state) { - state.writing = false; - state.writecb = null; - state.length -= state.writelen; - state.writelen = 0; -} - -function onwrite(stream, er) { - var state = stream._writableState; - var sync = state.sync; - var cb = state.writecb; - - onwriteStateUpdate(state); - - if (er) onwriteError(stream, state, sync, er, cb);else { - // Check if we're actually ready to finish, but don't emit yet - var finished = needFinish(state); - - if (!finished && !state.corked && !state.bufferProcessing && state.bufferedRequest) { - clearBuffer(stream, state); - } - - if (sync) { - /**/ - asyncWrite(afterWrite, stream, state, finished, cb); - /**/ - } else { - afterWrite(stream, state, finished, cb); - } - } -} - -function afterWrite(stream, state, finished, cb) { - if (!finished) onwriteDrain(stream, state); - state.pendingcb--; - cb(); - finishMaybe(stream, state); -} - -// Must force callback to be called on nextTick, so that we don't -// emit 'drain' before the write() consumer gets the 'false' return -// value, and has a chance to attach a 'drain' listener. -function onwriteDrain(stream, state) { - if (state.length === 0 && state.needDrain) { - state.needDrain = false; - stream.emit('drain'); - } -} - -// if there's something in the buffer waiting, then process it -function clearBuffer(stream, state) { - state.bufferProcessing = true; - var entry = state.bufferedRequest; - - if (stream._writev && entry && entry.next) { - // Fast case, write everything using _writev() - var l = state.bufferedRequestCount; - var buffer = new Array(l); - var holder = state.corkedRequestsFree; - holder.entry = entry; - - var count = 0; - while (entry) { - buffer[count] = entry; - entry = entry.next; - count += 1; - } - - doWrite(stream, state, true, state.length, buffer, '', holder.finish); - - // doWrite is always async, defer these to save a bit of time - // as the hot path ends with doWrite - state.pendingcb++; - state.lastBufferedRequest = null; - state.corkedRequestsFree = holder.next; - holder.next = null; - } else { - // Slow case, write chunks one-by-one - while (entry) { - var chunk = entry.chunk; - var encoding = entry.encoding; - var cb = entry.callback; - var len = state.objectMode ? 1 : chunk.length; - - doWrite(stream, state, false, len, chunk, encoding, cb); - entry = entry.next; - // if we didn't call the onwrite immediately, then - // it means that we need to wait until it does. - // also, that means that the chunk and cb are currently - // being processed, so move the buffer counter past them. - if (state.writing) { - break; - } - } - - if (entry === null) state.lastBufferedRequest = null; - } - - state.bufferedRequestCount = 0; - state.bufferedRequest = entry; - state.bufferProcessing = false; -} - -Writable.prototype._write = function (chunk, encoding, cb) { - cb(new Error('not implemented')); -}; - -Writable.prototype._writev = null; - -Writable.prototype.end = function (chunk, encoding, cb) { - var state = this._writableState; - - if (typeof chunk === 'function') { - cb = chunk; - chunk = null; - encoding = null; - } else if (typeof encoding === 'function') { - cb = encoding; - encoding = null; - } - - if (chunk !== null && chunk !== undefined) this.write(chunk, encoding); - - // .end() fully uncorks - if (state.corked) { - state.corked = 1; - this.uncork(); - } - - // ignore unnecessary end() calls. - if (!state.ending && !state.finished) endWritable(this, state, cb); -}; - -function needFinish(state) { - return state.ending && state.length === 0 && state.bufferedRequest === null && !state.finished && !state.writing; -} - -function prefinish(stream, state) { - if (!state.prefinished) { - state.prefinished = true; - stream.emit('prefinish'); - } -} - -function finishMaybe(stream, state) { - var need = needFinish(state); - if (need) { - if (state.pendingcb === 0) { - prefinish(stream, state); - state.finished = true; - stream.emit('finish'); - } else { - prefinish(stream, state); - } - } - return need; -} - -function endWritable(stream, state, cb) { - state.ending = true; - finishMaybe(stream, state); - if (cb) { - if (state.finished) processNextTick(cb);else stream.once('finish', cb); - } - state.ended = true; - stream.writable = false; -} - -// It seems a linked list but it is not -// there will be only 2 of these for each stream -function CorkedRequest(state) { - var _this = this; - - this.next = null; - this.entry = null; - - this.finish = function (err) { - var entry = _this.entry; - _this.entry = null; - while (entry) { - var cb = entry.callback; - state.pendingcb--; - cb(err); - entry = entry.next; - } - if (state.corkedRequestsFree) { - state.corkedRequestsFree.next = _this; - } else { - state.corkedRequestsFree = _this; - } - }; -} \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/LICENSE b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/LICENSE deleted file mode 100644 index d8d7f943..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright Node.js contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/README.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/README.md deleted file mode 100644 index 5a76b414..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# core-util-is - -The `util.is*` functions introduced in Node v0.12. diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/float.patch b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/float.patch deleted file mode 100644 index a06d5c05..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/float.patch +++ /dev/null @@ -1,604 +0,0 @@ -diff --git a/lib/util.js b/lib/util.js -index a03e874..9074e8e 100644 ---- a/lib/util.js -+++ b/lib/util.js -@@ -19,430 +19,6 @@ - // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE - // USE OR OTHER DEALINGS IN THE SOFTWARE. - --var formatRegExp = /%[sdj%]/g; --exports.format = function(f) { -- if (!isString(f)) { -- var objects = []; -- for (var i = 0; i < arguments.length; i++) { -- objects.push(inspect(arguments[i])); -- } -- return objects.join(' '); -- } -- -- var i = 1; -- var args = arguments; -- var len = args.length; -- var str = String(f).replace(formatRegExp, function(x) { -- if (x === '%%') return '%'; -- if (i >= len) return x; -- switch (x) { -- case '%s': return String(args[i++]); -- case '%d': return Number(args[i++]); -- case '%j': -- try { -- return JSON.stringify(args[i++]); -- } catch (_) { -- return '[Circular]'; -- } -- default: -- return x; -- } -- }); -- for (var x = args[i]; i < len; x = args[++i]) { -- if (isNull(x) || !isObject(x)) { -- str += ' ' + x; -- } else { -- str += ' ' + inspect(x); -- } -- } -- return str; --}; -- -- --// Mark that a method should not be used. --// Returns a modified function which warns once by default. --// If --no-deprecation is set, then it is a no-op. --exports.deprecate = function(fn, msg) { -- // Allow for deprecating things in the process of starting up. -- if (isUndefined(global.process)) { -- return function() { -- return exports.deprecate(fn, msg).apply(this, arguments); -- }; -- } -- -- if (process.noDeprecation === true) { -- return fn; -- } -- -- var warned = false; -- function deprecated() { -- if (!warned) { -- if (process.throwDeprecation) { -- throw new Error(msg); -- } else if (process.traceDeprecation) { -- console.trace(msg); -- } else { -- console.error(msg); -- } -- warned = true; -- } -- return fn.apply(this, arguments); -- } -- -- return deprecated; --}; -- -- --var debugs = {}; --var debugEnviron; --exports.debuglog = function(set) { -- if (isUndefined(debugEnviron)) -- debugEnviron = process.env.NODE_DEBUG || ''; -- set = set.toUpperCase(); -- if (!debugs[set]) { -- if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) { -- var pid = process.pid; -- debugs[set] = function() { -- var msg = exports.format.apply(exports, arguments); -- console.error('%s %d: %s', set, pid, msg); -- }; -- } else { -- debugs[set] = function() {}; -- } -- } -- return debugs[set]; --}; -- -- --/** -- * Echos the value of a value. Trys to print the value out -- * in the best way possible given the different types. -- * -- * @param {Object} obj The object to print out. -- * @param {Object} opts Optional options object that alters the output. -- */ --/* legacy: obj, showHidden, depth, colors*/ --function inspect(obj, opts) { -- // default options -- var ctx = { -- seen: [], -- stylize: stylizeNoColor -- }; -- // legacy... -- if (arguments.length >= 3) ctx.depth = arguments[2]; -- if (arguments.length >= 4) ctx.colors = arguments[3]; -- if (isBoolean(opts)) { -- // legacy... -- ctx.showHidden = opts; -- } else if (opts) { -- // got an "options" object -- exports._extend(ctx, opts); -- } -- // set default options -- if (isUndefined(ctx.showHidden)) ctx.showHidden = false; -- if (isUndefined(ctx.depth)) ctx.depth = 2; -- if (isUndefined(ctx.colors)) ctx.colors = false; -- if (isUndefined(ctx.customInspect)) ctx.customInspect = true; -- if (ctx.colors) ctx.stylize = stylizeWithColor; -- return formatValue(ctx, obj, ctx.depth); --} --exports.inspect = inspect; -- -- --// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics --inspect.colors = { -- 'bold' : [1, 22], -- 'italic' : [3, 23], -- 'underline' : [4, 24], -- 'inverse' : [7, 27], -- 'white' : [37, 39], -- 'grey' : [90, 39], -- 'black' : [30, 39], -- 'blue' : [34, 39], -- 'cyan' : [36, 39], -- 'green' : [32, 39], -- 'magenta' : [35, 39], -- 'red' : [31, 39], -- 'yellow' : [33, 39] --}; -- --// Don't use 'blue' not visible on cmd.exe --inspect.styles = { -- 'special': 'cyan', -- 'number': 'yellow', -- 'boolean': 'yellow', -- 'undefined': 'grey', -- 'null': 'bold', -- 'string': 'green', -- 'date': 'magenta', -- // "name": intentionally not styling -- 'regexp': 'red' --}; -- -- --function stylizeWithColor(str, styleType) { -- var style = inspect.styles[styleType]; -- -- if (style) { -- return '\u001b[' + inspect.colors[style][0] + 'm' + str + -- '\u001b[' + inspect.colors[style][1] + 'm'; -- } else { -- return str; -- } --} -- -- --function stylizeNoColor(str, styleType) { -- return str; --} -- -- --function arrayToHash(array) { -- var hash = {}; -- -- array.forEach(function(val, idx) { -- hash[val] = true; -- }); -- -- return hash; --} -- -- --function formatValue(ctx, value, recurseTimes) { -- // Provide a hook for user-specified inspect functions. -- // Check that value is an object with an inspect function on it -- if (ctx.customInspect && -- value && -- isFunction(value.inspect) && -- // Filter out the util module, it's inspect function is special -- value.inspect !== exports.inspect && -- // Also filter out any prototype objects using the circular check. -- !(value.constructor && value.constructor.prototype === value)) { -- var ret = value.inspect(recurseTimes, ctx); -- if (!isString(ret)) { -- ret = formatValue(ctx, ret, recurseTimes); -- } -- return ret; -- } -- -- // Primitive types cannot have properties -- var primitive = formatPrimitive(ctx, value); -- if (primitive) { -- return primitive; -- } -- -- // Look up the keys of the object. -- var keys = Object.keys(value); -- var visibleKeys = arrayToHash(keys); -- -- if (ctx.showHidden) { -- keys = Object.getOwnPropertyNames(value); -- } -- -- // Some type of object without properties can be shortcutted. -- if (keys.length === 0) { -- if (isFunction(value)) { -- var name = value.name ? ': ' + value.name : ''; -- return ctx.stylize('[Function' + name + ']', 'special'); -- } -- if (isRegExp(value)) { -- return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); -- } -- if (isDate(value)) { -- return ctx.stylize(Date.prototype.toString.call(value), 'date'); -- } -- if (isError(value)) { -- return formatError(value); -- } -- } -- -- var base = '', array = false, braces = ['{', '}']; -- -- // Make Array say that they are Array -- if (isArray(value)) { -- array = true; -- braces = ['[', ']']; -- } -- -- // Make functions say that they are functions -- if (isFunction(value)) { -- var n = value.name ? ': ' + value.name : ''; -- base = ' [Function' + n + ']'; -- } -- -- // Make RegExps say that they are RegExps -- if (isRegExp(value)) { -- base = ' ' + RegExp.prototype.toString.call(value); -- } -- -- // Make dates with properties first say the date -- if (isDate(value)) { -- base = ' ' + Date.prototype.toUTCString.call(value); -- } -- -- // Make error with message first say the error -- if (isError(value)) { -- base = ' ' + formatError(value); -- } -- -- if (keys.length === 0 && (!array || value.length == 0)) { -- return braces[0] + base + braces[1]; -- } -- -- if (recurseTimes < 0) { -- if (isRegExp(value)) { -- return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); -- } else { -- return ctx.stylize('[Object]', 'special'); -- } -- } -- -- ctx.seen.push(value); -- -- var output; -- if (array) { -- output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); -- } else { -- output = keys.map(function(key) { -- return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); -- }); -- } -- -- ctx.seen.pop(); -- -- return reduceToSingleString(output, base, braces); --} -- -- --function formatPrimitive(ctx, value) { -- if (isUndefined(value)) -- return ctx.stylize('undefined', 'undefined'); -- if (isString(value)) { -- var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') -- .replace(/'/g, "\\'") -- .replace(/\\"/g, '"') + '\''; -- return ctx.stylize(simple, 'string'); -- } -- if (isNumber(value)) { -- // Format -0 as '-0'. Strict equality won't distinguish 0 from -0, -- // so instead we use the fact that 1 / -0 < 0 whereas 1 / 0 > 0 . -- if (value === 0 && 1 / value < 0) -- return ctx.stylize('-0', 'number'); -- return ctx.stylize('' + value, 'number'); -- } -- if (isBoolean(value)) -- return ctx.stylize('' + value, 'boolean'); -- // For some reason typeof null is "object", so special case here. -- if (isNull(value)) -- return ctx.stylize('null', 'null'); --} -- -- --function formatError(value) { -- return '[' + Error.prototype.toString.call(value) + ']'; --} -- -- --function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { -- var output = []; -- for (var i = 0, l = value.length; i < l; ++i) { -- if (hasOwnProperty(value, String(i))) { -- output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, -- String(i), true)); -- } else { -- output.push(''); -- } -- } -- keys.forEach(function(key) { -- if (!key.match(/^\d+$/)) { -- output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, -- key, true)); -- } -- }); -- return output; --} -- -- --function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { -- var name, str, desc; -- desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; -- if (desc.get) { -- if (desc.set) { -- str = ctx.stylize('[Getter/Setter]', 'special'); -- } else { -- str = ctx.stylize('[Getter]', 'special'); -- } -- } else { -- if (desc.set) { -- str = ctx.stylize('[Setter]', 'special'); -- } -- } -- if (!hasOwnProperty(visibleKeys, key)) { -- name = '[' + key + ']'; -- } -- if (!str) { -- if (ctx.seen.indexOf(desc.value) < 0) { -- if (isNull(recurseTimes)) { -- str = formatValue(ctx, desc.value, null); -- } else { -- str = formatValue(ctx, desc.value, recurseTimes - 1); -- } -- if (str.indexOf('\n') > -1) { -- if (array) { -- str = str.split('\n').map(function(line) { -- return ' ' + line; -- }).join('\n').substr(2); -- } else { -- str = '\n' + str.split('\n').map(function(line) { -- return ' ' + line; -- }).join('\n'); -- } -- } -- } else { -- str = ctx.stylize('[Circular]', 'special'); -- } -- } -- if (isUndefined(name)) { -- if (array && key.match(/^\d+$/)) { -- return str; -- } -- name = JSON.stringify('' + key); -- if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { -- name = name.substr(1, name.length - 2); -- name = ctx.stylize(name, 'name'); -- } else { -- name = name.replace(/'/g, "\\'") -- .replace(/\\"/g, '"') -- .replace(/(^"|"$)/g, "'"); -- name = ctx.stylize(name, 'string'); -- } -- } -- -- return name + ': ' + str; --} -- -- --function reduceToSingleString(output, base, braces) { -- var numLinesEst = 0; -- var length = output.reduce(function(prev, cur) { -- numLinesEst++; -- if (cur.indexOf('\n') >= 0) numLinesEst++; -- return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; -- }, 0); -- -- if (length > 60) { -- return braces[0] + -- (base === '' ? '' : base + '\n ') + -- ' ' + -- output.join(',\n ') + -- ' ' + -- braces[1]; -- } -- -- return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; --} -- -- - // NOTE: These type checking functions intentionally don't use `instanceof` - // because it is fragile and can be easily faked with `Object.create()`. - function isArray(ar) { -@@ -522,166 +98,10 @@ function isPrimitive(arg) { - exports.isPrimitive = isPrimitive; - - function isBuffer(arg) { -- return arg instanceof Buffer; -+ return Buffer.isBuffer(arg); - } - exports.isBuffer = isBuffer; - - function objectToString(o) { - return Object.prototype.toString.call(o); --} -- -- --function pad(n) { -- return n < 10 ? '0' + n.toString(10) : n.toString(10); --} -- -- --var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', -- 'Oct', 'Nov', 'Dec']; -- --// 26 Feb 16:19:34 --function timestamp() { -- var d = new Date(); -- var time = [pad(d.getHours()), -- pad(d.getMinutes()), -- pad(d.getSeconds())].join(':'); -- return [d.getDate(), months[d.getMonth()], time].join(' '); --} -- -- --// log is just a thin wrapper to console.log that prepends a timestamp --exports.log = function() { -- console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); --}; -- -- --/** -- * Inherit the prototype methods from one constructor into another. -- * -- * The Function.prototype.inherits from lang.js rewritten as a standalone -- * function (not on Function.prototype). NOTE: If this file is to be loaded -- * during bootstrapping this function needs to be rewritten using some native -- * functions as prototype setup using normal JavaScript does not work as -- * expected during bootstrapping (see mirror.js in r114903). -- * -- * @param {function} ctor Constructor function which needs to inherit the -- * prototype. -- * @param {function} superCtor Constructor function to inherit prototype from. -- */ --exports.inherits = function(ctor, superCtor) { -- ctor.super_ = superCtor; -- ctor.prototype = Object.create(superCtor.prototype, { -- constructor: { -- value: ctor, -- enumerable: false, -- writable: true, -- configurable: true -- } -- }); --}; -- --exports._extend = function(origin, add) { -- // Don't do anything if add isn't an object -- if (!add || !isObject(add)) return origin; -- -- var keys = Object.keys(add); -- var i = keys.length; -- while (i--) { -- origin[keys[i]] = add[keys[i]]; -- } -- return origin; --}; -- --function hasOwnProperty(obj, prop) { -- return Object.prototype.hasOwnProperty.call(obj, prop); --} -- -- --// Deprecated old stuff. -- --exports.p = exports.deprecate(function() { -- for (var i = 0, len = arguments.length; i < len; ++i) { -- console.error(exports.inspect(arguments[i])); -- } --}, 'util.p: Use console.error() instead'); -- -- --exports.exec = exports.deprecate(function() { -- return require('child_process').exec.apply(this, arguments); --}, 'util.exec is now called `child_process.exec`.'); -- -- --exports.print = exports.deprecate(function() { -- for (var i = 0, len = arguments.length; i < len; ++i) { -- process.stdout.write(String(arguments[i])); -- } --}, 'util.print: Use console.log instead'); -- -- --exports.puts = exports.deprecate(function() { -- for (var i = 0, len = arguments.length; i < len; ++i) { -- process.stdout.write(arguments[i] + '\n'); -- } --}, 'util.puts: Use console.log instead'); -- -- --exports.debug = exports.deprecate(function(x) { -- process.stderr.write('DEBUG: ' + x + '\n'); --}, 'util.debug: Use console.error instead'); -- -- --exports.error = exports.deprecate(function(x) { -- for (var i = 0, len = arguments.length; i < len; ++i) { -- process.stderr.write(arguments[i] + '\n'); -- } --}, 'util.error: Use console.error instead'); -- -- --exports.pump = exports.deprecate(function(readStream, writeStream, callback) { -- var callbackCalled = false; -- -- function call(a, b, c) { -- if (callback && !callbackCalled) { -- callback(a, b, c); -- callbackCalled = true; -- } -- } -- -- readStream.addListener('data', function(chunk) { -- if (writeStream.write(chunk) === false) readStream.pause(); -- }); -- -- writeStream.addListener('drain', function() { -- readStream.resume(); -- }); -- -- readStream.addListener('end', function() { -- writeStream.end(); -- }); -- -- readStream.addListener('close', function() { -- call(); -- }); -- -- readStream.addListener('error', function(err) { -- writeStream.end(); -- call(err); -- }); -- -- writeStream.addListener('error', function(err) { -- readStream.destroy(); -- call(err); -- }); --}, 'util.pump(): Use readableStream.pipe() instead'); -- -- --var uv; --exports._errnoException = function(err, syscall) { -- if (isUndefined(uv)) uv = process.binding('uv'); -- var errname = uv.errname(err); -- var e = new Error(syscall + ' ' + errname); -- e.code = errname; -- e.errno = errname; -- e.syscall = syscall; -- return e; --}; -+} \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/lib/util.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/lib/util.js deleted file mode 100644 index ff4c851c..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/lib/util.js +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -// NOTE: These type checking functions intentionally don't use `instanceof` -// because it is fragile and can be easily faked with `Object.create()`. - -function isArray(arg) { - if (Array.isArray) { - return Array.isArray(arg); - } - return objectToString(arg) === '[object Array]'; -} -exports.isArray = isArray; - -function isBoolean(arg) { - return typeof arg === 'boolean'; -} -exports.isBoolean = isBoolean; - -function isNull(arg) { - return arg === null; -} -exports.isNull = isNull; - -function isNullOrUndefined(arg) { - return arg == null; -} -exports.isNullOrUndefined = isNullOrUndefined; - -function isNumber(arg) { - return typeof arg === 'number'; -} -exports.isNumber = isNumber; - -function isString(arg) { - return typeof arg === 'string'; -} -exports.isString = isString; - -function isSymbol(arg) { - return typeof arg === 'symbol'; -} -exports.isSymbol = isSymbol; - -function isUndefined(arg) { - return arg === void 0; -} -exports.isUndefined = isUndefined; - -function isRegExp(re) { - return objectToString(re) === '[object RegExp]'; -} -exports.isRegExp = isRegExp; - -function isObject(arg) { - return typeof arg === 'object' && arg !== null; -} -exports.isObject = isObject; - -function isDate(d) { - return objectToString(d) === '[object Date]'; -} -exports.isDate = isDate; - -function isError(e) { - return (objectToString(e) === '[object Error]' || e instanceof Error); -} -exports.isError = isError; - -function isFunction(arg) { - return typeof arg === 'function'; -} -exports.isFunction = isFunction; - -function isPrimitive(arg) { - return arg === null || - typeof arg === 'boolean' || - typeof arg === 'number' || - typeof arg === 'string' || - typeof arg === 'symbol' || // ES6 symbol - typeof arg === 'undefined'; -} -exports.isPrimitive = isPrimitive; - -exports.isBuffer = Buffer.isBuffer; - -function objectToString(o) { - return Object.prototype.toString.call(o); -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/package.json b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/package.json deleted file mode 100644 index 19fb8592..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "core-util-is", - "version": "1.0.2", - "description": "The `util.is*` functions introduced in Node v0.12.", - "main": "lib/util.js", - "repository": { - "type": "git", - "url": "git://github.com/isaacs/core-util-is.git" - }, - "keywords": [ - "util", - "isBuffer", - "isArray", - "isNumber", - "isString", - "isRegExp", - "isThis", - "isThat", - "polyfill" - ], - "author": { - "name": "Isaac Z. Schlueter", - "email": "i@izs.me", - "url": "http://blog.izs.me/" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/isaacs/core-util-is/issues" - }, - "scripts": { - "test": "tap test.js" - }, - "devDependencies": { - "tap": "^2.3.0" - }, - "gitHead": "a177da234df5638b363ddc15fa324619a38577c8", - "homepage": "https://github.com/isaacs/core-util-is#readme", - "_id": "core-util-is@1.0.2", - "_shasum": "b5fd54220aa2bc5ab57aab7140c940754503c1a7", - "_from": "core-util-is@>=1.0.0 <1.1.0", - "_npmVersion": "3.3.2", - "_nodeVersion": "4.0.0", - "_npmUser": { - "name": "isaacs", - "email": "i@izs.me" - }, - "dist": { - "shasum": "b5fd54220aa2bc5ab57aab7140c940754503c1a7", - "tarball": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - }, - "maintainers": [ - { - "name": "isaacs", - "email": "i@izs.me" - } - ], - "directories": {}, - "_resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/test.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/test.js deleted file mode 100644 index 1a490c65..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/test.js +++ /dev/null @@ -1,68 +0,0 @@ -var assert = require('tap'); - -var t = require('./lib/util'); - -assert.equal(t.isArray([]), true); -assert.equal(t.isArray({}), false); - -assert.equal(t.isBoolean(null), false); -assert.equal(t.isBoolean(true), true); -assert.equal(t.isBoolean(false), true); - -assert.equal(t.isNull(null), true); -assert.equal(t.isNull(undefined), false); -assert.equal(t.isNull(false), false); -assert.equal(t.isNull(), false); - -assert.equal(t.isNullOrUndefined(null), true); -assert.equal(t.isNullOrUndefined(undefined), true); -assert.equal(t.isNullOrUndefined(false), false); -assert.equal(t.isNullOrUndefined(), true); - -assert.equal(t.isNumber(null), false); -assert.equal(t.isNumber('1'), false); -assert.equal(t.isNumber(1), true); - -assert.equal(t.isString(null), false); -assert.equal(t.isString('1'), true); -assert.equal(t.isString(1), false); - -assert.equal(t.isSymbol(null), false); -assert.equal(t.isSymbol('1'), false); -assert.equal(t.isSymbol(1), false); -assert.equal(t.isSymbol(Symbol()), true); - -assert.equal(t.isUndefined(null), false); -assert.equal(t.isUndefined(undefined), true); -assert.equal(t.isUndefined(false), false); -assert.equal(t.isUndefined(), true); - -assert.equal(t.isRegExp(null), false); -assert.equal(t.isRegExp('1'), false); -assert.equal(t.isRegExp(new RegExp()), true); - -assert.equal(t.isObject({}), true); -assert.equal(t.isObject([]), true); -assert.equal(t.isObject(new RegExp()), true); -assert.equal(t.isObject(new Date()), true); - -assert.equal(t.isDate(null), false); -assert.equal(t.isDate('1'), false); -assert.equal(t.isDate(new Date()), true); - -assert.equal(t.isError(null), false); -assert.equal(t.isError({ err: true }), false); -assert.equal(t.isError(new Error()), true); - -assert.equal(t.isFunction(null), false); -assert.equal(t.isFunction({ }), false); -assert.equal(t.isFunction(function() {}), true); - -assert.equal(t.isPrimitive(null), true); -assert.equal(t.isPrimitive(''), true); -assert.equal(t.isPrimitive(0), true); -assert.equal(t.isPrimitive(new Date()), false); - -assert.equal(t.isBuffer(null), false); -assert.equal(t.isBuffer({}), false); -assert.equal(t.isBuffer(new Buffer(0)), true); diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/LICENSE b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/LICENSE deleted file mode 100644 index dea3013d..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/LICENSE +++ /dev/null @@ -1,16 +0,0 @@ -The ISC License - -Copyright (c) Isaac Z. Schlueter - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. - diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/README.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/README.md deleted file mode 100644 index b1c56658..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/README.md +++ /dev/null @@ -1,42 +0,0 @@ -Browser-friendly inheritance fully compatible with standard node.js -[inherits](http://nodejs.org/api/util.html#util_util_inherits_constructor_superconstructor). - -This package exports standard `inherits` from node.js `util` module in -node environment, but also provides alternative browser-friendly -implementation through [browser -field](https://gist.github.com/shtylman/4339901). Alternative -implementation is a literal copy of standard one located in standalone -module to avoid requiring of `util`. It also has a shim for old -browsers with no `Object.create` support. - -While keeping you sure you are using standard `inherits` -implementation in node.js environment, it allows bundlers such as -[browserify](https://github.com/substack/node-browserify) to not -include full `util` package to your client code if all you need is -just `inherits` function. It worth, because browser shim for `util` -package is large and `inherits` is often the single function you need -from it. - -It's recommended to use this package instead of -`require('util').inherits` for any code that has chances to be used -not only in node.js but in browser too. - -## usage - -```js -var inherits = require('inherits'); -// then use exactly as the standard one -``` - -## note on version ~1.0 - -Version ~1.0 had completely different motivation and is not compatible -neither with 2.0 nor with standard node.js `inherits`. - -If you are using version ~1.0 and planning to switch to ~2.0, be -careful: - -* new version uses `super_` instead of `super` for referencing - superclass -* new version overwrites current prototype while old one preserves any - existing fields on it diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/inherits.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/inherits.js deleted file mode 100644 index 29f5e24f..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/inherits.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('util').inherits diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/inherits_browser.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/inherits_browser.js deleted file mode 100644 index c1e78a75..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/inherits_browser.js +++ /dev/null @@ -1,23 +0,0 @@ -if (typeof Object.create === 'function') { - // implementation from standard node.js 'util' module - module.exports = function inherits(ctor, superCtor) { - ctor.super_ = superCtor - ctor.prototype = Object.create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: false, - writable: true, - configurable: true - } - }); - }; -} else { - // old school shim for old browsers - module.exports = function inherits(ctor, superCtor) { - ctor.super_ = superCtor - var TempCtor = function () {} - TempCtor.prototype = superCtor.prototype - ctor.prototype = new TempCtor() - ctor.prototype.constructor = ctor - } -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/package.json b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/package.json deleted file mode 100644 index 02af46a5..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "inherits", - "description": "Browser-friendly inheritance fully compatible with standard node.js inherits()", - "version": "2.0.1", - "keywords": [ - "inheritance", - "class", - "klass", - "oop", - "object-oriented", - "inherits", - "browser", - "browserify" - ], - "main": "./inherits.js", - "browser": "./inherits_browser.js", - "repository": { - "type": "git", - "url": "git://github.com/isaacs/inherits.git" - }, - "license": "ISC", - "scripts": { - "test": "node test" - }, - "bugs": { - "url": "https://github.com/isaacs/inherits/issues" - }, - "_id": "inherits@2.0.1", - "dist": { - "shasum": "b17d08d326b4423e568eff719f91b0b1cbdf69f1", - "tarball": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" - }, - "_from": "inherits@>=2.0.1 <2.1.0", - "_npmVersion": "1.3.8", - "_npmUser": { - "name": "isaacs", - "email": "i@izs.me" - }, - "maintainers": [ - { - "name": "isaacs", - "email": "i@izs.me" - } - ], - "directories": {}, - "_shasum": "b17d08d326b4423e568eff719f91b0b1cbdf69f1", - "_resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "readme": "ERROR: No README data found!", - "homepage": "https://github.com/isaacs/inherits#readme" -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/test.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/test.js deleted file mode 100644 index fc53012d..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/test.js +++ /dev/null @@ -1,25 +0,0 @@ -var inherits = require('./inherits.js') -var assert = require('assert') - -function test(c) { - assert(c.constructor === Child) - assert(c.constructor.super_ === Parent) - assert(Object.getPrototypeOf(c) === Child.prototype) - assert(Object.getPrototypeOf(Object.getPrototypeOf(c)) === Parent.prototype) - assert(c instanceof Child) - assert(c instanceof Parent) -} - -function Child() { - Parent.call(this) - test(this) -} - -function Parent() {} - -inherits(Child, Parent) - -var c = new Child -test(c) - -console.log('ok') diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/.npmignore b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/.npmignore deleted file mode 100644 index 3c3629e6..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/.npmignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/.travis.yml b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/.travis.yml deleted file mode 100644 index cc4dba29..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -node_js: - - "0.8" - - "0.10" diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/Makefile b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/Makefile deleted file mode 100644 index 787d56e1..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/Makefile +++ /dev/null @@ -1,6 +0,0 @@ - -test: - @node_modules/.bin/tape test.js - -.PHONY: test - diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/README.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/README.md deleted file mode 100644 index 16d2c59c..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/README.md +++ /dev/null @@ -1,60 +0,0 @@ - -# isarray - -`Array#isArray` for older browsers. - -[![build status](https://secure.travis-ci.org/juliangruber/isarray.svg)](http://travis-ci.org/juliangruber/isarray) -[![downloads](https://img.shields.io/npm/dm/isarray.svg)](https://www.npmjs.org/package/isarray) - -[![browser support](https://ci.testling.com/juliangruber/isarray.png) -](https://ci.testling.com/juliangruber/isarray) - -## Usage - -```js -var isArray = require('isarray'); - -console.log(isArray([])); // => true -console.log(isArray({})); // => false -``` - -## Installation - -With [npm](http://npmjs.org) do - -```bash -$ npm install isarray -``` - -Then bundle for the browser with -[browserify](https://github.com/substack/browserify). - -With [component](http://component.io) do - -```bash -$ component install juliangruber/isarray -``` - -## License - -(MIT) - -Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/component.json b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/component.json deleted file mode 100644 index 9e31b683..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/component.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name" : "isarray", - "description" : "Array#isArray for older browsers", - "version" : "0.0.1", - "repository" : "juliangruber/isarray", - "homepage": "https://github.com/juliangruber/isarray", - "main" : "index.js", - "scripts" : [ - "index.js" - ], - "dependencies" : {}, - "keywords": ["browser","isarray","array"], - "author": { - "name": "Julian Gruber", - "email": "mail@juliangruber.com", - "url": "http://juliangruber.com" - }, - "license": "MIT" -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/index.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/index.js deleted file mode 100644 index a57f6349..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/index.js +++ /dev/null @@ -1,5 +0,0 @@ -var toString = {}.toString; - -module.exports = Array.isArray || function (arr) { - return toString.call(arr) == '[object Array]'; -}; diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/package.json b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/package.json deleted file mode 100644 index e86d232e..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/package.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "isarray", - "description": "Array#isArray for older browsers", - "version": "1.0.0", - "repository": { - "type": "git", - "url": "git://github.com/juliangruber/isarray.git" - }, - "homepage": "https://github.com/juliangruber/isarray", - "main": "index.js", - "dependencies": {}, - "devDependencies": { - "tape": "~2.13.4" - }, - "keywords": [ - "browser", - "isarray", - "array" - ], - "author": { - "name": "Julian Gruber", - "email": "mail@juliangruber.com", - "url": "http://juliangruber.com" - }, - "license": "MIT", - "testling": { - "files": "test.js", - "browsers": [ - "ie/8..latest", - "firefox/17..latest", - "firefox/nightly", - "chrome/22..latest", - "chrome/canary", - "opera/12..latest", - "opera/next", - "safari/5.1..latest", - "ipad/6.0..latest", - "iphone/6.0..latest", - "android-browser/4.2..latest" - ] - }, - "scripts": { - "test": "tape test.js" - }, - "gitHead": "2a23a281f369e9ae06394c0fb4d2381355a6ba33", - "bugs": { - "url": "https://github.com/juliangruber/isarray/issues" - }, - "_id": "isarray@1.0.0", - "_shasum": "bb935d48582cba168c06834957a54a3e07124f11", - "_from": "isarray@>=1.0.0 <1.1.0", - "_npmVersion": "3.3.12", - "_nodeVersion": "5.1.0", - "_npmUser": { - "name": "juliangruber", - "email": "julian@juliangruber.com" - }, - "dist": { - "shasum": "bb935d48582cba168c06834957a54a3e07124f11", - "tarball": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - }, - "maintainers": [ - { - "name": "juliangruber", - "email": "julian@juliangruber.com" - } - ], - "directories": {}, - "_resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/test.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/test.js deleted file mode 100644 index e0c3444d..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/test.js +++ /dev/null @@ -1,20 +0,0 @@ -var isArray = require('./'); -var test = require('tape'); - -test('is array', function(t){ - t.ok(isArray([])); - t.notOk(isArray({})); - t.notOk(isArray(null)); - t.notOk(isArray(false)); - - var obj = {}; - obj[0] = true; - t.notOk(isArray(obj)); - - var arr = []; - arr.foo = 'bar'; - t.ok(isArray(arr)); - - t.end(); -}); - diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/.travis.yml b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/.travis.yml deleted file mode 100644 index 36201b10..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: node_js -node_js: - - "0.8" - - "0.10" - - "0.11" - - "0.12" - - "1.7.1" - - 1 - - 2 - - 3 - - 4 - - 5 diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/index.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/index.js deleted file mode 100644 index a4f40f84..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/index.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -if (!process.version || - process.version.indexOf('v0.') === 0 || - process.version.indexOf('v1.') === 0 && process.version.indexOf('v1.8.') !== 0) { - module.exports = nextTick; -} else { - module.exports = process.nextTick; -} - -function nextTick(fn, arg1, arg2, arg3) { - if (typeof fn !== 'function') { - throw new TypeError('"callback" argument must be a function'); - } - var len = arguments.length; - var args, i; - switch (len) { - case 0: - case 1: - return process.nextTick(fn); - case 2: - return process.nextTick(function afterTickOne() { - fn.call(null, arg1); - }); - case 3: - return process.nextTick(function afterTickTwo() { - fn.call(null, arg1, arg2); - }); - case 4: - return process.nextTick(function afterTickThree() { - fn.call(null, arg1, arg2, arg3); - }); - default: - args = new Array(len - 1); - i = 0; - while (i < args.length) { - args[i++] = arguments[i]; - } - return process.nextTick(function afterTick() { - fn.apply(null, args); - }); - } -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/license.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/license.md deleted file mode 100644 index c67e3532..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/license.md +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2015 Calvin Metcalf - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.** diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/package.json b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/package.json deleted file mode 100644 index 211b098d..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "process-nextick-args", - "version": "1.0.7", - "description": "process.nextTick but always with args", - "main": "index.js", - "scripts": { - "test": "node test.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/calvinmetcalf/process-nextick-args.git" - }, - "author": "", - "license": "MIT", - "bugs": { - "url": "https://github.com/calvinmetcalf/process-nextick-args/issues" - }, - "homepage": "https://github.com/calvinmetcalf/process-nextick-args", - "devDependencies": { - "tap": "~0.2.6" - }, - "gitHead": "5c00899ab01dd32f93ad4b5743da33da91404f39", - "_id": "process-nextick-args@1.0.7", - "_shasum": "150e20b756590ad3f91093f25a4f2ad8bff30ba3", - "_from": "process-nextick-args@>=1.0.6 <1.1.0", - "_npmVersion": "3.8.6", - "_nodeVersion": "5.11.0", - "_npmUser": { - "name": "cwmma", - "email": "calvin.metcalf@gmail.com" - }, - "dist": { - "shasum": "150e20b756590ad3f91093f25a4f2ad8bff30ba3", - "tarball": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" - }, - "maintainers": [ - { - "name": "cwmma", - "email": "calvin.metcalf@gmail.com" - } - ], - "_npmOperationalInternal": { - "host": "packages-12-west.internal.npmjs.com", - "tmp": "tmp/process-nextick-args-1.0.7.tgz_1462394251778_0.36989671061746776" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/readme.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/readme.md deleted file mode 100644 index 78e7cfae..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/readme.md +++ /dev/null @@ -1,18 +0,0 @@ -process-nextick-args -===== - -[![Build Status](https://travis-ci.org/calvinmetcalf/process-nextick-args.svg?branch=master)](https://travis-ci.org/calvinmetcalf/process-nextick-args) - -```bash -npm install --save process-nextick-args -``` - -Always be able to pass arguments to process.nextTick, no matter the platform - -```js -var nextTick = require('process-nextick-args'); - -nextTick(function (a, b, c) { - console.log(a, b, c); -}, 'step', 3, 'profit'); -``` diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/test.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/test.js deleted file mode 100644 index ef157215..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/test.js +++ /dev/null @@ -1,24 +0,0 @@ -var test = require("tap").test; -var nextTick = require('./'); - -test('should work', function (t) { - t.plan(5); - nextTick(function (a) { - t.ok(a); - nextTick(function (thing) { - t.equals(thing, 7); - }, 7); - }, true); - nextTick(function (a, b, c) { - t.equals(a, 'step'); - t.equals(b, 3); - t.equals(c, 'profit'); - }, 'step', 3, 'profit'); -}); - -test('correct number of arguments', function (t) { - t.plan(1); - nextTick(function () { - t.equals(2, arguments.length, 'correct number'); - }, 1, 2); -}); diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/.npmignore b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/.npmignore deleted file mode 100644 index 206320cc..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -build -test diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/LICENSE b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/LICENSE deleted file mode 100644 index 6de584a4..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright Joyent, Inc. and other Node contributors. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the -following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/README.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/README.md deleted file mode 100644 index 4d2aa001..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/README.md +++ /dev/null @@ -1,7 +0,0 @@ -**string_decoder.js** (`require('string_decoder')`) from Node.js core - -Copyright Joyent, Inc. and other Node contributors. See LICENCE file for details. - -Version numbers match the versions found in Node core, e.g. 0.10.24 matches Node 0.10.24, likewise 0.11.10 matches Node 0.11.10. **Prefer the stable version over the unstable.** - -The *build/* directory contains a build script that will scrape the source from the [joyent/node](https://github.com/joyent/node) repo given a specific Node version. \ No newline at end of file diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/index.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/index.js deleted file mode 100644 index b00e54fb..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/index.js +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - -var Buffer = require('buffer').Buffer; - -var isBufferEncoding = Buffer.isEncoding - || function(encoding) { - switch (encoding && encoding.toLowerCase()) { - case 'hex': case 'utf8': case 'utf-8': case 'ascii': case 'binary': case 'base64': case 'ucs2': case 'ucs-2': case 'utf16le': case 'utf-16le': case 'raw': return true; - default: return false; - } - } - - -function assertEncoding(encoding) { - if (encoding && !isBufferEncoding(encoding)) { - throw new Error('Unknown encoding: ' + encoding); - } -} - -// StringDecoder provides an interface for efficiently splitting a series of -// buffers into a series of JS strings without breaking apart multi-byte -// characters. CESU-8 is handled as part of the UTF-8 encoding. -// -// @TODO Handling all encodings inside a single object makes it very difficult -// to reason about this code, so it should be split up in the future. -// @TODO There should be a utf8-strict encoding that rejects invalid UTF-8 code -// points as used by CESU-8. -var StringDecoder = exports.StringDecoder = function(encoding) { - this.encoding = (encoding || 'utf8').toLowerCase().replace(/[-_]/, ''); - assertEncoding(encoding); - switch (this.encoding) { - case 'utf8': - // CESU-8 represents each of Surrogate Pair by 3-bytes - this.surrogateSize = 3; - break; - case 'ucs2': - case 'utf16le': - // UTF-16 represents each of Surrogate Pair by 2-bytes - this.surrogateSize = 2; - this.detectIncompleteChar = utf16DetectIncompleteChar; - break; - case 'base64': - // Base-64 stores 3 bytes in 4 chars, and pads the remainder. - this.surrogateSize = 3; - this.detectIncompleteChar = base64DetectIncompleteChar; - break; - default: - this.write = passThroughWrite; - return; - } - - // Enough space to store all bytes of a single character. UTF-8 needs 4 - // bytes, but CESU-8 may require up to 6 (3 bytes per surrogate). - this.charBuffer = new Buffer(6); - // Number of bytes received for the current incomplete multi-byte character. - this.charReceived = 0; - // Number of bytes expected for the current incomplete multi-byte character. - this.charLength = 0; -}; - - -// write decodes the given buffer and returns it as JS string that is -// guaranteed to not contain any partial multi-byte characters. Any partial -// character found at the end of the buffer is buffered up, and will be -// returned when calling write again with the remaining bytes. -// -// Note: Converting a Buffer containing an orphan surrogate to a String -// currently works, but converting a String to a Buffer (via `new Buffer`, or -// Buffer#write) will replace incomplete surrogates with the unicode -// replacement character. See https://codereview.chromium.org/121173009/ . -StringDecoder.prototype.write = function(buffer) { - var charStr = ''; - // if our last write ended with an incomplete multibyte character - while (this.charLength) { - // determine how many remaining bytes this buffer has to offer for this char - var available = (buffer.length >= this.charLength - this.charReceived) ? - this.charLength - this.charReceived : - buffer.length; - - // add the new bytes to the char buffer - buffer.copy(this.charBuffer, this.charReceived, 0, available); - this.charReceived += available; - - if (this.charReceived < this.charLength) { - // still not enough chars in this buffer? wait for more ... - return ''; - } - - // remove bytes belonging to the current character from the buffer - buffer = buffer.slice(available, buffer.length); - - // get the character that was split - charStr = this.charBuffer.slice(0, this.charLength).toString(this.encoding); - - // CESU-8: lead surrogate (D800-DBFF) is also the incomplete character - var charCode = charStr.charCodeAt(charStr.length - 1); - if (charCode >= 0xD800 && charCode <= 0xDBFF) { - this.charLength += this.surrogateSize; - charStr = ''; - continue; - } - this.charReceived = this.charLength = 0; - - // if there are no more bytes in this buffer, just emit our char - if (buffer.length === 0) { - return charStr; - } - break; - } - - // determine and set charLength / charReceived - this.detectIncompleteChar(buffer); - - var end = buffer.length; - if (this.charLength) { - // buffer the incomplete character bytes we got - buffer.copy(this.charBuffer, 0, buffer.length - this.charReceived, end); - end -= this.charReceived; - } - - charStr += buffer.toString(this.encoding, 0, end); - - var end = charStr.length - 1; - var charCode = charStr.charCodeAt(end); - // CESU-8: lead surrogate (D800-DBFF) is also the incomplete character - if (charCode >= 0xD800 && charCode <= 0xDBFF) { - var size = this.surrogateSize; - this.charLength += size; - this.charReceived += size; - this.charBuffer.copy(this.charBuffer, size, 0, size); - buffer.copy(this.charBuffer, 0, 0, size); - return charStr.substring(0, end); - } - - // or just emit the charStr - return charStr; -}; - -// detectIncompleteChar determines if there is an incomplete UTF-8 character at -// the end of the given buffer. If so, it sets this.charLength to the byte -// length that character, and sets this.charReceived to the number of bytes -// that are available for this character. -StringDecoder.prototype.detectIncompleteChar = function(buffer) { - // determine how many bytes we have to check at the end of this buffer - var i = (buffer.length >= 3) ? 3 : buffer.length; - - // Figure out if one of the last i bytes of our buffer announces an - // incomplete char. - for (; i > 0; i--) { - var c = buffer[buffer.length - i]; - - // See http://en.wikipedia.org/wiki/UTF-8#Description - - // 110XXXXX - if (i == 1 && c >> 5 == 0x06) { - this.charLength = 2; - break; - } - - // 1110XXXX - if (i <= 2 && c >> 4 == 0x0E) { - this.charLength = 3; - break; - } - - // 11110XXX - if (i <= 3 && c >> 3 == 0x1E) { - this.charLength = 4; - break; - } - } - this.charReceived = i; -}; - -StringDecoder.prototype.end = function(buffer) { - var res = ''; - if (buffer && buffer.length) - res = this.write(buffer); - - if (this.charReceived) { - var cr = this.charReceived; - var buf = this.charBuffer; - var enc = this.encoding; - res += buf.slice(0, cr).toString(enc); - } - - return res; -}; - -function passThroughWrite(buffer) { - return buffer.toString(this.encoding); -} - -function utf16DetectIncompleteChar(buffer) { - this.charReceived = buffer.length % 2; - this.charLength = this.charReceived ? 2 : 0; -} - -function base64DetectIncompleteChar(buffer) { - this.charReceived = buffer.length % 3; - this.charLength = this.charReceived ? 3 : 0; -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/package.json b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/package.json deleted file mode 100644 index 8e8b77db..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/string_decoder/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "string_decoder", - "version": "0.10.31", - "description": "The string_decoder module from Node core", - "main": "index.js", - "dependencies": {}, - "devDependencies": { - "tap": "~0.4.8" - }, - "scripts": { - "test": "tap test/simple/*.js" - }, - "repository": { - "type": "git", - "url": "git://github.com/rvagg/string_decoder.git" - }, - "homepage": "https://github.com/rvagg/string_decoder", - "keywords": [ - "string", - "decoder", - "browser", - "browserify" - ], - "license": "MIT", - "gitHead": "d46d4fd87cf1d06e031c23f1ba170ca7d4ade9a0", - "bugs": { - "url": "https://github.com/rvagg/string_decoder/issues" - }, - "_id": "string_decoder@0.10.31", - "_shasum": "62e203bc41766c6c28c9fc84301dab1c5310fa94", - "_from": "string_decoder@>=0.10.0 <0.11.0", - "_npmVersion": "1.4.23", - "_npmUser": { - "name": "rvagg", - "email": "rod@vagg.org" - }, - "maintainers": [ - { - "name": "substack", - "email": "mail@substack.net" - }, - { - "name": "rvagg", - "email": "rod@vagg.org" - } - ], - "dist": { - "shasum": "62e203bc41766c6c28c9fc84301dab1c5310fa94", - "tarball": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/History.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/History.md deleted file mode 100644 index acc86753..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/History.md +++ /dev/null @@ -1,16 +0,0 @@ - -1.0.2 / 2015-10-07 -================== - - * use try/catch when checking `localStorage` (#3, @kumavis) - -1.0.1 / 2014-11-25 -================== - - * browser: use `console.warn()` for deprecation calls - * browser: more jsdocs - -1.0.0 / 2014-04-30 -================== - - * initial commit diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/LICENSE b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/LICENSE deleted file mode 100644 index 6a60e8c2..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -(The MIT License) - -Copyright (c) 2014 Nathan Rajlich - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/README.md b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/README.md deleted file mode 100644 index 75622fa7..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/README.md +++ /dev/null @@ -1,53 +0,0 @@ -util-deprecate -============== -### The Node.js `util.deprecate()` function with browser support - -In Node.js, this module simply re-exports the `util.deprecate()` function. - -In the web browser (i.e. via browserify), a browser-specific implementation -of the `util.deprecate()` function is used. - - -## API - -A `deprecate()` function is the only thing exposed by this module. - -``` javascript -// setup: -exports.foo = deprecate(foo, 'foo() is deprecated, use bar() instead'); - - -// users see: -foo(); -// foo() is deprecated, use bar() instead -foo(); -foo(); -``` - - -## License - -(The MIT License) - -Copyright (c) 2014 Nathan Rajlich - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/browser.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/browser.js deleted file mode 100644 index 549ae2f0..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/browser.js +++ /dev/null @@ -1,67 +0,0 @@ - -/** - * Module exports. - */ - -module.exports = deprecate; - -/** - * Mark that a method should not be used. - * Returns a modified function which warns once by default. - * - * If `localStorage.noDeprecation = true` is set, then it is a no-op. - * - * If `localStorage.throwDeprecation = true` is set, then deprecated functions - * will throw an Error when invoked. - * - * If `localStorage.traceDeprecation = true` is set, then deprecated functions - * will invoke `console.trace()` instead of `console.error()`. - * - * @param {Function} fn - the function to deprecate - * @param {String} msg - the string to print to the console when `fn` is invoked - * @returns {Function} a new "deprecated" version of `fn` - * @api public - */ - -function deprecate (fn, msg) { - if (config('noDeprecation')) { - return fn; - } - - var warned = false; - function deprecated() { - if (!warned) { - if (config('throwDeprecation')) { - throw new Error(msg); - } else if (config('traceDeprecation')) { - console.trace(msg); - } else { - console.warn(msg); - } - warned = true; - } - return fn.apply(this, arguments); - } - - return deprecated; -} - -/** - * Checks `localStorage` for boolean values for the given `name`. - * - * @param {String} name - * @returns {Boolean} - * @api private - */ - -function config (name) { - // accessing global.localStorage can trigger a DOMException in sandboxed iframes - try { - if (!global.localStorage) return false; - } catch (_) { - return false; - } - var val = global.localStorage[name]; - if (null == val) return false; - return String(val).toLowerCase() === 'true'; -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/node.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/node.js deleted file mode 100644 index 5e6fcff5..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/node.js +++ /dev/null @@ -1,6 +0,0 @@ - -/** - * For Node.js, simply re-export the core `util.deprecate` function. - */ - -module.exports = require('util').deprecate; diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/package.json b/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/package.json deleted file mode 100644 index a0181354..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "util-deprecate", - "version": "1.0.2", - "description": "The Node.js `util.deprecate()` function with browser support", - "main": "node.js", - "browser": "browser.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git://github.com/TooTallNate/util-deprecate.git" - }, - "keywords": [ - "util", - "deprecate", - "browserify", - "browser", - "node" - ], - "author": { - "name": "Nathan Rajlich", - "email": "nathan@tootallnate.net", - "url": "http://n8.io/" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/TooTallNate/util-deprecate/issues" - }, - "homepage": "https://github.com/TooTallNate/util-deprecate", - "gitHead": "475fb6857cd23fafff20c1be846c1350abf8e6d4", - "_id": "util-deprecate@1.0.2", - "_shasum": "450d4dc9fa70de732762fbd2d4a28981419a0ccf", - "_from": "util-deprecate@>=1.0.1 <1.1.0", - "_npmVersion": "2.14.4", - "_nodeVersion": "4.1.2", - "_npmUser": { - "name": "tootallnate", - "email": "nathan@tootallnate.net" - }, - "maintainers": [ - { - "name": "tootallnate", - "email": "nathan@tootallnate.net" - } - ], - "dist": { - "shasum": "450d4dc9fa70de732762fbd2d4a28981419a0ccf", - "tarball": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/package.json b/node_modules/request/node_modules/bl/node_modules/readable-stream/package.json deleted file mode 100644 index b9fc0dae..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/package.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "name": "readable-stream", - "version": "2.0.6", - "description": "Streams3, a user-land copy of the stream library from Node.js", - "main": "readable.js", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - }, - "devDependencies": { - "tap": "~0.2.6", - "tape": "~4.5.1", - "zuul": "~3.9.0" - }, - "scripts": { - "test": "tap test/parallel/*.js test/ours/*.js", - "browser": "npm run write-zuul && zuul -- test/browser.js", - "write-zuul": "printf \"ui: tape\nbrowsers:\n - name: $BROWSER_NAME\n version: $BROWSER_VERSION\n\">.zuul.yml" - }, - "repository": { - "type": "git", - "url": "git://github.com/nodejs/readable-stream.git" - }, - "keywords": [ - "readable", - "stream", - "pipe" - ], - "browser": { - "util": false - }, - "license": "MIT", - "gitHead": "01fb5608a970b42c900b96746cadc13d27dd9d7e", - "bugs": { - "url": "https://github.com/nodejs/readable-stream/issues" - }, - "homepage": "https://github.com/nodejs/readable-stream#readme", - "_id": "readable-stream@2.0.6", - "_shasum": "8f90341e68a53ccc928788dacfcd11b36eb9b78e", - "_from": "readable-stream@>=2.0.5 <2.1.0", - "_npmVersion": "3.6.0", - "_nodeVersion": "5.7.0", - "_npmUser": { - "name": "cwmma", - "email": "calvin.metcalf@gmail.com" - }, - "dist": { - "shasum": "8f90341e68a53ccc928788dacfcd11b36eb9b78e", - "tarball": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" - }, - "maintainers": [ - { - "name": "isaacs", - "email": "isaacs@npmjs.com" - }, - { - "name": "tootallnate", - "email": "nathan@tootallnate.net" - }, - { - "name": "rvagg", - "email": "rod@vagg.org" - }, - { - "name": "cwmma", - "email": "calvin.metcalf@gmail.com" - } - ], - "_npmOperationalInternal": { - "host": "packages-12-west.internal.npmjs.com", - "tmp": "tmp/readable-stream-2.0.6.tgz_1457893507709_0.369257491780445" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/passthrough.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/passthrough.js deleted file mode 100644 index 27e8d8a5..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/passthrough.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./lib/_stream_passthrough.js") diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/readable.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/readable.js deleted file mode 100644 index 6222a579..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/readable.js +++ /dev/null @@ -1,12 +0,0 @@ -var Stream = (function (){ - try { - return require('st' + 'ream'); // hack to fix a circular dependency issue when used with browserify - } catch(_){} -}()); -exports = module.exports = require('./lib/_stream_readable.js'); -exports.Stream = Stream || exports; -exports.Readable = exports; -exports.Writable = require('./lib/_stream_writable.js'); -exports.Duplex = require('./lib/_stream_duplex.js'); -exports.Transform = require('./lib/_stream_transform.js'); -exports.PassThrough = require('./lib/_stream_passthrough.js'); diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/transform.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/transform.js deleted file mode 100644 index 5d482f07..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/transform.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./lib/_stream_transform.js") diff --git a/node_modules/request/node_modules/bl/node_modules/readable-stream/writable.js b/node_modules/request/node_modules/bl/node_modules/readable-stream/writable.js deleted file mode 100644 index e1e9efdf..00000000 --- a/node_modules/request/node_modules/bl/node_modules/readable-stream/writable.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./lib/_stream_writable.js") diff --git a/node_modules/request/node_modules/bl/package.json b/node_modules/request/node_modules/bl/package.json deleted file mode 100644 index 23203168..00000000 --- a/node_modules/request/node_modules/bl/package.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "bl", - "version": "1.1.2", - "description": "Buffer List: collect buffers and access with a standard readable Buffer interface, streamable too!", - "main": "bl.js", - "scripts": { - "test": "node test/test.js | faucet" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rvagg/bl.git" - }, - "homepage": "https://github.com/rvagg/bl", - "authors": [ - "Rod Vagg (https://github.com/rvagg)", - "Matteo Collina (https://github.com/mcollina)", - "Jarett Cruger (https://github.com/jcrugzz)" - ], - "keywords": [ - "buffer", - "buffers", - "stream", - "awesomesauce" - ], - "license": "MIT", - "dependencies": { - "readable-stream": "~2.0.5" - }, - "devDependencies": { - "faucet": "0.0.1", - "hash_file": "~0.1.1", - "tape": "~4.4.0" - }, - "gitHead": "ea42021059dc65fc60d7f4b9217c73431f09d23d", - "bugs": { - "url": "https://github.com/rvagg/bl/issues" - }, - "_id": "bl@1.1.2", - "_shasum": "fdca871a99713aa00d19e3bbba41c44787a65398", - "_from": "bl@>=1.1.2 <1.2.0", - "_npmVersion": "3.3.12", - "_nodeVersion": "5.3.0", - "_npmUser": { - "name": "rvagg", - "email": "rod@vagg.org" - }, - "maintainers": [ - { - "name": "rvagg", - "email": "rod@vagg.org" - } - ], - "dist": { - "shasum": "fdca871a99713aa00d19e3bbba41c44787a65398", - "tarball": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz" - }, - "_npmOperationalInternal": { - "host": "packages-9-west.internal.npmjs.com", - "tmp": "tmp/bl-1.1.2.tgz_1455246621698_0.6300242957659066" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/bl/test/test.js b/node_modules/request/node_modules/bl/test/test.js deleted file mode 100644 index c95b1ba4..00000000 --- a/node_modules/request/node_modules/bl/test/test.js +++ /dev/null @@ -1,640 +0,0 @@ -var tape = require('tape') - , crypto = require('crypto') - , fs = require('fs') - , hash = require('hash_file') - , BufferList = require('../') - - , encodings = - ('hex utf8 utf-8 ascii binary base64' - + (process.browser ? '' : ' ucs2 ucs-2 utf16le utf-16le')).split(' ') - -tape('single bytes from single buffer', function (t) { - var bl = new BufferList() - bl.append(new Buffer('abcd')) - - t.equal(bl.length, 4) - - t.equal(bl.get(0), 97) - t.equal(bl.get(1), 98) - t.equal(bl.get(2), 99) - t.equal(bl.get(3), 100) - - t.end() -}) - -tape('single bytes from multiple buffers', function (t) { - var bl = new BufferList() - bl.append(new Buffer('abcd')) - bl.append(new Buffer('efg')) - bl.append(new Buffer('hi')) - bl.append(new Buffer('j')) - - t.equal(bl.length, 10) - - t.equal(bl.get(0), 97) - t.equal(bl.get(1), 98) - t.equal(bl.get(2), 99) - t.equal(bl.get(3), 100) - t.equal(bl.get(4), 101) - t.equal(bl.get(5), 102) - t.equal(bl.get(6), 103) - t.equal(bl.get(7), 104) - t.equal(bl.get(8), 105) - t.equal(bl.get(9), 106) - t.end() -}) - -tape('multi bytes from single buffer', function (t) { - var bl = new BufferList() - bl.append(new Buffer('abcd')) - - t.equal(bl.length, 4) - - t.equal(bl.slice(0, 4).toString('ascii'), 'abcd') - t.equal(bl.slice(0, 3).toString('ascii'), 'abc') - t.equal(bl.slice(1, 4).toString('ascii'), 'bcd') - - t.end() -}) - -tape('multiple bytes from multiple buffers', function (t) { - var bl = new BufferList() - - bl.append(new Buffer('abcd')) - bl.append(new Buffer('efg')) - bl.append(new Buffer('hi')) - bl.append(new Buffer('j')) - - t.equal(bl.length, 10) - - t.equal(bl.slice(0, 10).toString('ascii'), 'abcdefghij') - t.equal(bl.slice(3, 10).toString('ascii'), 'defghij') - t.equal(bl.slice(3, 6).toString('ascii'), 'def') - t.equal(bl.slice(3, 8).toString('ascii'), 'defgh') - t.equal(bl.slice(5, 10).toString('ascii'), 'fghij') - - t.end() -}) - -tape('multiple bytes from multiple buffer lists', function (t) { - var bl = new BufferList() - - bl.append(new BufferList([ new Buffer('abcd'), new Buffer('efg') ])) - bl.append(new BufferList([ new Buffer('hi'), new Buffer('j') ])) - - t.equal(bl.length, 10) - - t.equal(bl.slice(0, 10).toString('ascii'), 'abcdefghij') - - t.equal(bl.slice(3, 10).toString('ascii'), 'defghij') - t.equal(bl.slice(3, 6).toString('ascii'), 'def') - t.equal(bl.slice(3, 8).toString('ascii'), 'defgh') - t.equal(bl.slice(5, 10).toString('ascii'), 'fghij') - - t.end() -}) - -// same data as previous test, just using nested constructors -tape('multiple bytes from crazy nested buffer lists', function (t) { - var bl = new BufferList() - - bl.append(new BufferList([ - new BufferList([ - new BufferList(new Buffer('abc')) - , new Buffer('d') - , new BufferList(new Buffer('efg')) - ]) - , new BufferList([ new Buffer('hi') ]) - , new BufferList(new Buffer('j')) - ])) - - t.equal(bl.length, 10) - - t.equal(bl.slice(0, 10).toString('ascii'), 'abcdefghij') - - t.equal(bl.slice(3, 10).toString('ascii'), 'defghij') - t.equal(bl.slice(3, 6).toString('ascii'), 'def') - t.equal(bl.slice(3, 8).toString('ascii'), 'defgh') - t.equal(bl.slice(5, 10).toString('ascii'), 'fghij') - - t.end() -}) - -tape('append accepts arrays of Buffers', function (t) { - var bl = new BufferList() - bl.append(new Buffer('abc')) - bl.append([ new Buffer('def') ]) - bl.append([ new Buffer('ghi'), new Buffer('jkl') ]) - bl.append([ new Buffer('mnop'), new Buffer('qrstu'), new Buffer('vwxyz') ]) - t.equal(bl.length, 26) - t.equal(bl.slice().toString('ascii'), 'abcdefghijklmnopqrstuvwxyz') - t.end() -}) - -tape('append accepts arrays of BufferLists', function (t) { - var bl = new BufferList() - bl.append(new Buffer('abc')) - bl.append([ new BufferList('def') ]) - bl.append(new BufferList([ new Buffer('ghi'), new BufferList('jkl') ])) - bl.append([ new Buffer('mnop'), new BufferList([ new Buffer('qrstu'), new Buffer('vwxyz') ]) ]) - t.equal(bl.length, 26) - t.equal(bl.slice().toString('ascii'), 'abcdefghijklmnopqrstuvwxyz') - t.end() -}) - -tape('append chainable', function (t) { - var bl = new BufferList() - t.ok(bl.append(new Buffer('abcd')) === bl) - t.ok(bl.append([ new Buffer('abcd') ]) === bl) - t.ok(bl.append(new BufferList(new Buffer('abcd'))) === bl) - t.ok(bl.append([ new BufferList(new Buffer('abcd')) ]) === bl) - t.end() -}) - -tape('append chainable (test results)', function (t) { - var bl = new BufferList('abc') - .append([ new BufferList('def') ]) - .append(new BufferList([ new Buffer('ghi'), new BufferList('jkl') ])) - .append([ new Buffer('mnop'), new BufferList([ new Buffer('qrstu'), new Buffer('vwxyz') ]) ]) - - t.equal(bl.length, 26) - t.equal(bl.slice().toString('ascii'), 'abcdefghijklmnopqrstuvwxyz') - t.end() -}) - -tape('consuming from multiple buffers', function (t) { - var bl = new BufferList() - - bl.append(new Buffer('abcd')) - bl.append(new Buffer('efg')) - bl.append(new Buffer('hi')) - bl.append(new Buffer('j')) - - t.equal(bl.length, 10) - - t.equal(bl.slice(0, 10).toString('ascii'), 'abcdefghij') - - bl.consume(3) - t.equal(bl.length, 7) - t.equal(bl.slice(0, 7).toString('ascii'), 'defghij') - - bl.consume(2) - t.equal(bl.length, 5) - t.equal(bl.slice(0, 5).toString('ascii'), 'fghij') - - bl.consume(1) - t.equal(bl.length, 4) - t.equal(bl.slice(0, 4).toString('ascii'), 'ghij') - - bl.consume(1) - t.equal(bl.length, 3) - t.equal(bl.slice(0, 3).toString('ascii'), 'hij') - - bl.consume(2) - t.equal(bl.length, 1) - t.equal(bl.slice(0, 1).toString('ascii'), 'j') - - t.end() -}) - -tape('complete consumption', function (t) { - var bl = new BufferList() - - bl.append(new Buffer('a')) - bl.append(new Buffer('b')) - - bl.consume(2) - - t.equal(bl.length, 0) - t.equal(bl._bufs.length, 0) - - t.end() -}) - -tape('test readUInt8 / readInt8', function (t) { - var buf1 = new Buffer(1) - , buf2 = new Buffer(3) - , buf3 = new Buffer(3) - , bl = new BufferList() - - buf2[1] = 0x3 - buf2[2] = 0x4 - buf3[0] = 0x23 - buf3[1] = 0x42 - - bl.append(buf1) - bl.append(buf2) - bl.append(buf3) - - t.equal(bl.readUInt8(2), 0x3) - t.equal(bl.readInt8(2), 0x3) - t.equal(bl.readUInt8(3), 0x4) - t.equal(bl.readInt8(3), 0x4) - t.equal(bl.readUInt8(4), 0x23) - t.equal(bl.readInt8(4), 0x23) - t.equal(bl.readUInt8(5), 0x42) - t.equal(bl.readInt8(5), 0x42) - t.end() -}) - -tape('test readUInt16LE / readUInt16BE / readInt16LE / readInt16BE', function (t) { - var buf1 = new Buffer(1) - , buf2 = new Buffer(3) - , buf3 = new Buffer(3) - , bl = new BufferList() - - buf2[1] = 0x3 - buf2[2] = 0x4 - buf3[0] = 0x23 - buf3[1] = 0x42 - - bl.append(buf1) - bl.append(buf2) - bl.append(buf3) - - t.equal(bl.readUInt16BE(2), 0x0304) - t.equal(bl.readUInt16LE(2), 0x0403) - t.equal(bl.readInt16BE(2), 0x0304) - t.equal(bl.readInt16LE(2), 0x0403) - t.equal(bl.readUInt16BE(3), 0x0423) - t.equal(bl.readUInt16LE(3), 0x2304) - t.equal(bl.readInt16BE(3), 0x0423) - t.equal(bl.readInt16LE(3), 0x2304) - t.equal(bl.readUInt16BE(4), 0x2342) - t.equal(bl.readUInt16LE(4), 0x4223) - t.equal(bl.readInt16BE(4), 0x2342) - t.equal(bl.readInt16LE(4), 0x4223) - t.end() -}) - -tape('test readUInt32LE / readUInt32BE / readInt32LE / readInt32BE', function (t) { - var buf1 = new Buffer(1) - , buf2 = new Buffer(3) - , buf3 = new Buffer(3) - , bl = new BufferList() - - buf2[1] = 0x3 - buf2[2] = 0x4 - buf3[0] = 0x23 - buf3[1] = 0x42 - - bl.append(buf1) - bl.append(buf2) - bl.append(buf3) - - t.equal(bl.readUInt32BE(2), 0x03042342) - t.equal(bl.readUInt32LE(2), 0x42230403) - t.equal(bl.readInt32BE(2), 0x03042342) - t.equal(bl.readInt32LE(2), 0x42230403) - t.end() -}) - -tape('test readFloatLE / readFloatBE', function (t) { - var buf1 = new Buffer(1) - , buf2 = new Buffer(3) - , buf3 = new Buffer(3) - , bl = new BufferList() - - buf2[1] = 0x00 - buf2[2] = 0x00 - buf3[0] = 0x80 - buf3[1] = 0x3f - - bl.append(buf1) - bl.append(buf2) - bl.append(buf3) - - t.equal(bl.readFloatLE(2), 0x01) - t.end() -}) - -tape('test readDoubleLE / readDoubleBE', function (t) { - var buf1 = new Buffer(1) - , buf2 = new Buffer(3) - , buf3 = new Buffer(10) - , bl = new BufferList() - - buf2[1] = 0x55 - buf2[2] = 0x55 - buf3[0] = 0x55 - buf3[1] = 0x55 - buf3[2] = 0x55 - buf3[3] = 0x55 - buf3[4] = 0xd5 - buf3[5] = 0x3f - - bl.append(buf1) - bl.append(buf2) - bl.append(buf3) - - t.equal(bl.readDoubleLE(2), 0.3333333333333333) - t.end() -}) - -tape('test toString', function (t) { - var bl = new BufferList() - - bl.append(new Buffer('abcd')) - bl.append(new Buffer('efg')) - bl.append(new Buffer('hi')) - bl.append(new Buffer('j')) - - t.equal(bl.toString('ascii', 0, 10), 'abcdefghij') - t.equal(bl.toString('ascii', 3, 10), 'defghij') - t.equal(bl.toString('ascii', 3, 6), 'def') - t.equal(bl.toString('ascii', 3, 8), 'defgh') - t.equal(bl.toString('ascii', 5, 10), 'fghij') - - t.end() -}) - -tape('test toString encoding', function (t) { - var bl = new BufferList() - , b = new Buffer('abcdefghij\xff\x00') - - bl.append(new Buffer('abcd')) - bl.append(new Buffer('efg')) - bl.append(new Buffer('hi')) - bl.append(new Buffer('j')) - bl.append(new Buffer('\xff\x00')) - - encodings.forEach(function (enc) { - t.equal(bl.toString(enc), b.toString(enc), enc) - }) - - t.end() -}) - -!process.browser && tape('test stream', function (t) { - var random = crypto.randomBytes(65534) - , rndhash = hash(random, 'md5') - , md5sum = crypto.createHash('md5') - , bl = new BufferList(function (err, buf) { - t.ok(Buffer.isBuffer(buf)) - t.ok(err === null) - t.equal(rndhash, hash(bl.slice(), 'md5')) - t.equal(rndhash, hash(buf, 'md5')) - - bl.pipe(fs.createWriteStream('/tmp/bl_test_rnd_out.dat')) - .on('close', function () { - var s = fs.createReadStream('/tmp/bl_test_rnd_out.dat') - s.on('data', md5sum.update.bind(md5sum)) - s.on('end', function() { - t.equal(rndhash, md5sum.digest('hex'), 'woohoo! correct hash!') - t.end() - }) - }) - - }) - - fs.writeFileSync('/tmp/bl_test_rnd.dat', random) - fs.createReadStream('/tmp/bl_test_rnd.dat').pipe(bl) -}) - -tape('instantiation with Buffer', function (t) { - var buf = crypto.randomBytes(1024) - , buf2 = crypto.randomBytes(1024) - , b = BufferList(buf) - - t.equal(buf.toString('hex'), b.slice().toString('hex'), 'same buffer') - b = BufferList([ buf, buf2 ]) - t.equal(b.slice().toString('hex'), Buffer.concat([ buf, buf2 ]).toString('hex'), 'same buffer') - t.end() -}) - -tape('test String appendage', function (t) { - var bl = new BufferList() - , b = new Buffer('abcdefghij\xff\x00') - - bl.append('abcd') - bl.append('efg') - bl.append('hi') - bl.append('j') - bl.append('\xff\x00') - - encodings.forEach(function (enc) { - t.equal(bl.toString(enc), b.toString(enc)) - }) - - t.end() -}) - -tape('test Number appendage', function (t) { - var bl = new BufferList() - , b = new Buffer('1234567890') - - bl.append(1234) - bl.append(567) - bl.append(89) - bl.append(0) - - encodings.forEach(function (enc) { - t.equal(bl.toString(enc), b.toString(enc)) - }) - - t.end() -}) - -tape('write nothing, should get empty buffer', function (t) { - t.plan(3) - BufferList(function (err, data) { - t.notOk(err, 'no error') - t.ok(Buffer.isBuffer(data), 'got a buffer') - t.equal(0, data.length, 'got a zero-length buffer') - t.end() - }).end() -}) - -tape('unicode string', function (t) { - t.plan(2) - var inp1 = '\u2600' - , inp2 = '\u2603' - , exp = inp1 + ' and ' + inp2 - , bl = BufferList() - bl.write(inp1) - bl.write(' and ') - bl.write(inp2) - t.equal(exp, bl.toString()) - t.equal(new Buffer(exp).toString('hex'), bl.toString('hex')) -}) - -tape('should emit finish', function (t) { - var source = BufferList() - , dest = BufferList() - - source.write('hello') - source.pipe(dest) - - dest.on('finish', function () { - t.equal(dest.toString('utf8'), 'hello') - t.end() - }) -}) - -tape('basic copy', function (t) { - var buf = crypto.randomBytes(1024) - , buf2 = new Buffer(1024) - , b = BufferList(buf) - - b.copy(buf2) - t.equal(b.slice().toString('hex'), buf2.toString('hex'), 'same buffer') - t.end() -}) - -tape('copy after many appends', function (t) { - var buf = crypto.randomBytes(512) - , buf2 = new Buffer(1024) - , b = BufferList(buf) - - b.append(buf) - b.copy(buf2) - t.equal(b.slice().toString('hex'), buf2.toString('hex'), 'same buffer') - t.end() -}) - -tape('copy at a precise position', function (t) { - var buf = crypto.randomBytes(1004) - , buf2 = new Buffer(1024) - , b = BufferList(buf) - - b.copy(buf2, 20) - t.equal(b.slice().toString('hex'), buf2.slice(20).toString('hex'), 'same buffer') - t.end() -}) - -tape('copy starting from a precise location', function (t) { - var buf = crypto.randomBytes(10) - , buf2 = new Buffer(5) - , b = BufferList(buf) - - b.copy(buf2, 0, 5) - t.equal(b.slice(5).toString('hex'), buf2.toString('hex'), 'same buffer') - t.end() -}) - -tape('copy in an interval', function (t) { - var rnd = crypto.randomBytes(10) - , b = BufferList(rnd) // put the random bytes there - , actual = new Buffer(3) - , expected = new Buffer(3) - - rnd.copy(expected, 0, 5, 8) - b.copy(actual, 0, 5, 8) - - t.equal(actual.toString('hex'), expected.toString('hex'), 'same buffer') - t.end() -}) - -tape('copy an interval between two buffers', function (t) { - var buf = crypto.randomBytes(10) - , buf2 = new Buffer(10) - , b = BufferList(buf) - - b.append(buf) - b.copy(buf2, 0, 5, 15) - - t.equal(b.slice(5, 15).toString('hex'), buf2.toString('hex'), 'same buffer') - t.end() -}) - -tape('duplicate', function (t) { - t.plan(2) - - var bl = new BufferList('abcdefghij\xff\x00') - , dup = bl.duplicate() - - t.equal(bl.prototype, dup.prototype) - t.equal(bl.toString('hex'), dup.toString('hex')) -}) - -tape('destroy no pipe', function (t) { - t.plan(2) - - var bl = new BufferList('alsdkfja;lsdkfja;lsdk') - bl.destroy() - - t.equal(bl._bufs.length, 0) - t.equal(bl.length, 0) -}) - -!process.browser && tape('destroy with pipe before read end', function (t) { - t.plan(2) - - var bl = new BufferList() - fs.createReadStream(__dirname + '/test.js') - .pipe(bl) - - bl.destroy() - - t.equal(bl._bufs.length, 0) - t.equal(bl.length, 0) - -}) - -!process.browser && tape('destroy with pipe before read end with race', function (t) { - t.plan(2) - - var bl = new BufferList() - fs.createReadStream(__dirname + '/test.js') - .pipe(bl) - - setTimeout(function () { - bl.destroy() - setTimeout(function () { - t.equal(bl._bufs.length, 0) - t.equal(bl.length, 0) - }, 500) - }, 500) -}) - -!process.browser && tape('destroy with pipe after read end', function (t) { - t.plan(2) - - var bl = new BufferList() - fs.createReadStream(__dirname + '/test.js') - .on('end', onEnd) - .pipe(bl) - - function onEnd () { - bl.destroy() - - t.equal(bl._bufs.length, 0) - t.equal(bl.length, 0) - } -}) - -!process.browser && tape('destroy with pipe while writing to a destination', function (t) { - t.plan(4) - - var bl = new BufferList() - , ds = new BufferList() - - fs.createReadStream(__dirname + '/test.js') - .on('end', onEnd) - .pipe(bl) - - function onEnd () { - bl.pipe(ds) - - setTimeout(function () { - bl.destroy() - - t.equals(bl._bufs.length, 0) - t.equals(bl.length, 0) - - ds.destroy() - - t.equals(bl._bufs.length, 0) - t.equals(bl.length, 0) - - }, 100) - } -}) - -!process.browser && tape('handle error', function (t) { - t.plan(2) - fs.createReadStream('/does/not/exist').pipe(BufferList(function (err, data) { - t.ok(err instanceof Error, 'has error') - t.notOk(data, 'no data') - })) -}) diff --git a/node_modules/request/node_modules/caseless/LICENSE b/node_modules/request/node_modules/caseless/LICENSE deleted file mode 100644 index 61789f4a..00000000 --- a/node_modules/request/node_modules/caseless/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: -You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. -END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/node_modules/request/node_modules/caseless/README.md b/node_modules/request/node_modules/caseless/README.md deleted file mode 100644 index e5077a21..00000000 --- a/node_modules/request/node_modules/caseless/README.md +++ /dev/null @@ -1,45 +0,0 @@ -## Caseless -- wrap an object to set and get property with caseless semantics but also preserve caseing. - -This library is incredibly useful when working with HTTP headers. It allows you to get/set/check for headers in a caseless manner while also preserving the caseing of headers the first time they are set. - -## Usage - -```javascript -var headers = {} - , c = caseless(headers) - ; -c.set('a-Header', 'asdf') -c.get('a-header') === 'asdf' -``` - -## has(key) - -Has takes a name and if it finds a matching header will return that header name with the preserved caseing it was set with. - -```javascript -c.has('a-header') === 'a-Header' -``` - -## set(key, value[, clobber=true]) - -Set is fairly straight forward except that if the header exists and clobber is disabled it will add `','+value` to the existing header. - -```javascript -c.set('a-Header', 'fdas') -c.set('a-HEADER', 'more', false) -c.get('a-header') === 'fdsa,more' -``` - -## swap(key) - -Swaps the casing of a header with the new one that is passed in. - -```javascript -var headers = {} - , c = caseless(headers) - ; -c.set('a-Header', 'fdas') -c.swap('a-HEADER') -c.has('a-header') === 'a-HEADER' -headers === {'a-HEADER': 'fdas'} -``` diff --git a/node_modules/request/node_modules/caseless/index.js b/node_modules/request/node_modules/caseless/index.js deleted file mode 100644 index d86a70ec..00000000 --- a/node_modules/request/node_modules/caseless/index.js +++ /dev/null @@ -1,66 +0,0 @@ -function Caseless (dict) { - this.dict = dict || {} -} -Caseless.prototype.set = function (name, value, clobber) { - if (typeof name === 'object') { - for (var i in name) { - this.set(i, name[i], value) - } - } else { - if (typeof clobber === 'undefined') clobber = true - var has = this.has(name) - - if (!clobber && has) this.dict[has] = this.dict[has] + ',' + value - else this.dict[has || name] = value - return has - } -} -Caseless.prototype.has = function (name) { - var keys = Object.keys(this.dict) - , name = name.toLowerCase() - ; - for (var i=0;i=0.11.0 <0.12.0", - "_npmVersion": "2.8.3", - "_nodeVersion": "1.8.1", - "_npmUser": { - "name": "mikeal", - "email": "mikeal.rogers@gmail.com" - }, - "maintainers": [ - { - "name": "mikeal", - "email": "mikeal.rogers@gmail.com" - }, - { - "name": "nylen", - "email": "jnylen@gmail.com" - }, - { - "name": "simov", - "email": "simeonvelichkov@gmail.com" - } - ], - "dist": { - "shasum": "715b96ea9841593cc33067923f5ec60ebda4f7d7", - "tarball": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/caseless/test.js b/node_modules/request/node_modules/caseless/test.js deleted file mode 100644 index 084bbaf5..00000000 --- a/node_modules/request/node_modules/caseless/test.js +++ /dev/null @@ -1,40 +0,0 @@ -var tape = require('tape') - , caseless = require('./') - ; - -tape('set get has', function (t) { - var headers = {} - , c = caseless(headers) - ; - t.plan(17) - c.set('a-Header', 'asdf') - t.equal(c.get('a-header'), 'asdf') - t.equal(c.has('a-header'), 'a-Header') - t.ok(!c.has('nothing')) - // old bug where we used the wrong regex - t.ok(!c.has('a-hea')) - c.set('a-header', 'fdsa') - t.equal(c.get('a-header'), 'fdsa') - t.equal(c.get('a-Header'), 'fdsa') - c.set('a-HEADER', 'more', false) - t.equal(c.get('a-header'), 'fdsa,more') - - t.deepEqual(headers, {'a-Header': 'fdsa,more'}) - c.swap('a-HEADER') - t.deepEqual(headers, {'a-HEADER': 'fdsa,more'}) - - c.set('deleteme', 'foobar') - t.ok(c.has('deleteme')) - t.ok(c.del('deleteme')) - t.notOk(c.has('deleteme')) - t.notOk(c.has('idonotexist')) - t.ok(c.del('idonotexist')) - - c.set('tva', 'test1') - c.set('tva-header', 'test2') - t.equal(c.has('tva'), 'tva') - t.notOk(c.has('header')) - - t.equal(c.get('tva'), 'test1') - -}) diff --git a/node_modules/request/node_modules/combined-stream/License b/node_modules/request/node_modules/combined-stream/License deleted file mode 100644 index 4804b7ab..00000000 --- a/node_modules/request/node_modules/combined-stream/License +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2011 Debuggable Limited - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/node_modules/request/node_modules/combined-stream/Readme.md b/node_modules/request/node_modules/combined-stream/Readme.md deleted file mode 100644 index 3a9e025f..00000000 --- a/node_modules/request/node_modules/combined-stream/Readme.md +++ /dev/null @@ -1,138 +0,0 @@ -# combined-stream - -A stream that emits multiple other streams one after another. - -**NB** Currently `combined-stream` works with streams vesrion 1 only. There is ongoing effort to switch this library to streams version 2. Any help is welcome. :) Meanwhile you can explore other libraries that provide streams2 support with more or less compatability with `combined-stream`. - -- [combined-stream2](https://www.npmjs.com/package/combined-stream2): A drop-in streams2-compatible replacement for the combined-stream module. - -- [multistream](https://www.npmjs.com/package/multistream): A stream that emits multiple other streams one after another. - -## Installation - -``` bash -npm install combined-stream -``` - -## Usage - -Here is a simple example that shows how you can use combined-stream to combine -two files into one: - -``` javascript -var CombinedStream = require('combined-stream'); -var fs = require('fs'); - -var combinedStream = CombinedStream.create(); -combinedStream.append(fs.createReadStream('file1.txt')); -combinedStream.append(fs.createReadStream('file2.txt')); - -combinedStream.pipe(fs.createWriteStream('combined.txt')); -``` - -While the example above works great, it will pause all source streams until -they are needed. If you don't want that to happen, you can set `pauseStreams` -to `false`: - -``` javascript -var CombinedStream = require('combined-stream'); -var fs = require('fs'); - -var combinedStream = CombinedStream.create({pauseStreams: false}); -combinedStream.append(fs.createReadStream('file1.txt')); -combinedStream.append(fs.createReadStream('file2.txt')); - -combinedStream.pipe(fs.createWriteStream('combined.txt')); -``` - -However, what if you don't have all the source streams yet, or you don't want -to allocate the resources (file descriptors, memory, etc.) for them right away? -Well, in that case you can simply provide a callback that supplies the stream -by calling a `next()` function: - -``` javascript -var CombinedStream = require('combined-stream'); -var fs = require('fs'); - -var combinedStream = CombinedStream.create(); -combinedStream.append(function(next) { - next(fs.createReadStream('file1.txt')); -}); -combinedStream.append(function(next) { - next(fs.createReadStream('file2.txt')); -}); - -combinedStream.pipe(fs.createWriteStream('combined.txt')); -``` - -## API - -### CombinedStream.create([options]) - -Returns a new combined stream object. Available options are: - -* `maxDataSize` -* `pauseStreams` - -The effect of those options is described below. - -### combinedStream.pauseStreams = `true` - -Whether to apply back pressure to the underlaying streams. If set to `false`, -the underlaying streams will never be paused. If set to `true`, the -underlaying streams will be paused right after being appended, as well as when -`delayedStream.pipe()` wants to throttle. - -### combinedStream.maxDataSize = `2 * 1024 * 1024` - -The maximum amount of bytes (or characters) to buffer for all source streams. -If this value is exceeded, `combinedStream` emits an `'error'` event. - -### combinedStream.dataSize = `0` - -The amount of bytes (or characters) currently buffered by `combinedStream`. - -### combinedStream.append(stream) - -Appends the given `stream` to the combinedStream object. If `pauseStreams` is -set to `true, this stream will also be paused right away. - -`streams` can also be a function that takes one parameter called `next`. `next` -is a function that must be invoked in order to provide the `next` stream, see -example above. - -Regardless of how the `stream` is appended, combined-stream always attaches an -`'error'` listener to it, so you don't have to do that manually. - -Special case: `stream` can also be a String or Buffer. - -### combinedStream.write(data) - -You should not call this, `combinedStream` takes care of piping the appended -streams into itself for you. - -### combinedStream.resume() - -Causes `combinedStream` to start drain the streams it manages. The function is -idempotent, and also emits a `'resume'` event each time which usually goes to -the stream that is currently being drained. - -### combinedStream.pause(); - -If `combinedStream.pauseStreams` is set to `false`, this does nothing. -Otherwise a `'pause'` event is emitted, this goes to the stream that is -currently being drained, so you can use it to apply back pressure. - -### combinedStream.end(); - -Sets `combinedStream.writable` to false, emits an `'end'` event, and removes -all streams from the queue. - -### combinedStream.destroy(); - -Same as `combinedStream.end()`, except it emits a `'close'` event instead of -`'end'`. - -## License - -combined-stream is licensed under the MIT license. diff --git a/node_modules/request/node_modules/combined-stream/lib/combined_stream.js b/node_modules/request/node_modules/combined-stream/lib/combined_stream.js deleted file mode 100644 index 6b5c21b6..00000000 --- a/node_modules/request/node_modules/combined-stream/lib/combined_stream.js +++ /dev/null @@ -1,188 +0,0 @@ -var util = require('util'); -var Stream = require('stream').Stream; -var DelayedStream = require('delayed-stream'); - -module.exports = CombinedStream; -function CombinedStream() { - this.writable = false; - this.readable = true; - this.dataSize = 0; - this.maxDataSize = 2 * 1024 * 1024; - this.pauseStreams = true; - - this._released = false; - this._streams = []; - this._currentStream = null; -} -util.inherits(CombinedStream, Stream); - -CombinedStream.create = function(options) { - var combinedStream = new this(); - - options = options || {}; - for (var option in options) { - combinedStream[option] = options[option]; - } - - return combinedStream; -}; - -CombinedStream.isStreamLike = function(stream) { - return (typeof stream !== 'function') - && (typeof stream !== 'string') - && (typeof stream !== 'boolean') - && (typeof stream !== 'number') - && (!Buffer.isBuffer(stream)); -}; - -CombinedStream.prototype.append = function(stream) { - var isStreamLike = CombinedStream.isStreamLike(stream); - - if (isStreamLike) { - if (!(stream instanceof DelayedStream)) { - var newStream = DelayedStream.create(stream, { - maxDataSize: Infinity, - pauseStream: this.pauseStreams, - }); - stream.on('data', this._checkDataSize.bind(this)); - stream = newStream; - } - - this._handleErrors(stream); - - if (this.pauseStreams) { - stream.pause(); - } - } - - this._streams.push(stream); - return this; -}; - -CombinedStream.prototype.pipe = function(dest, options) { - Stream.prototype.pipe.call(this, dest, options); - this.resume(); - return dest; -}; - -CombinedStream.prototype._getNext = function() { - this._currentStream = null; - var stream = this._streams.shift(); - - - if (typeof stream == 'undefined') { - this.end(); - return; - } - - if (typeof stream !== 'function') { - this._pipeNext(stream); - return; - } - - var getStream = stream; - getStream(function(stream) { - var isStreamLike = CombinedStream.isStreamLike(stream); - if (isStreamLike) { - stream.on('data', this._checkDataSize.bind(this)); - this._handleErrors(stream); - } - - this._pipeNext(stream); - }.bind(this)); -}; - -CombinedStream.prototype._pipeNext = function(stream) { - this._currentStream = stream; - - var isStreamLike = CombinedStream.isStreamLike(stream); - if (isStreamLike) { - stream.on('end', this._getNext.bind(this)); - stream.pipe(this, {end: false}); - return; - } - - var value = stream; - this.write(value); - this._getNext(); -}; - -CombinedStream.prototype._handleErrors = function(stream) { - var self = this; - stream.on('error', function(err) { - self._emitError(err); - }); -}; - -CombinedStream.prototype.write = function(data) { - this.emit('data', data); -}; - -CombinedStream.prototype.pause = function() { - if (!this.pauseStreams) { - return; - } - - if(this.pauseStreams && this._currentStream && typeof(this._currentStream.pause) == 'function') this._currentStream.pause(); - this.emit('pause'); -}; - -CombinedStream.prototype.resume = function() { - if (!this._released) { - this._released = true; - this.writable = true; - this._getNext(); - } - - if(this.pauseStreams && this._currentStream && typeof(this._currentStream.resume) == 'function') this._currentStream.resume(); - this.emit('resume'); -}; - -CombinedStream.prototype.end = function() { - this._reset(); - this.emit('end'); -}; - -CombinedStream.prototype.destroy = function() { - this._reset(); - this.emit('close'); -}; - -CombinedStream.prototype._reset = function() { - this.writable = false; - this._streams = []; - this._currentStream = null; -}; - -CombinedStream.prototype._checkDataSize = function() { - this._updateDataSize(); - if (this.dataSize <= this.maxDataSize) { - return; - } - - var message = - 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.'; - this._emitError(new Error(message)); -}; - -CombinedStream.prototype._updateDataSize = function() { - this.dataSize = 0; - - var self = this; - this._streams.forEach(function(stream) { - if (!stream.dataSize) { - return; - } - - self.dataSize += stream.dataSize; - }); - - if (this._currentStream && this._currentStream.dataSize) { - this.dataSize += this._currentStream.dataSize; - } -}; - -CombinedStream.prototype._emitError = function(err) { - this._reset(); - this.emit('error', err); -}; diff --git a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/.npmignore b/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/.npmignore deleted file mode 100644 index 9daeafb9..00000000 --- a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/.npmignore +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/License b/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/License deleted file mode 100644 index 4804b7ab..00000000 --- a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/License +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2011 Debuggable Limited - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/Makefile b/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/Makefile deleted file mode 100644 index b4ff85a3..00000000 --- a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -SHELL := /bin/bash - -test: - @./test/run.js - -.PHONY: test - diff --git a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/Readme.md b/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/Readme.md deleted file mode 100644 index aca36f9f..00000000 --- a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/Readme.md +++ /dev/null @@ -1,141 +0,0 @@ -# delayed-stream - -Buffers events from a stream until you are ready to handle them. - -## Installation - -``` bash -npm install delayed-stream -``` - -## Usage - -The following example shows how to write a http echo server that delays its -response by 1000 ms. - -``` javascript -var DelayedStream = require('delayed-stream'); -var http = require('http'); - -http.createServer(function(req, res) { - var delayed = DelayedStream.create(req); - - setTimeout(function() { - res.writeHead(200); - delayed.pipe(res); - }, 1000); -}); -``` - -If you are not using `Stream#pipe`, you can also manually release the buffered -events by calling `delayedStream.resume()`: - -``` javascript -var delayed = DelayedStream.create(req); - -setTimeout(function() { - // Emit all buffered events and resume underlaying source - delayed.resume(); -}, 1000); -``` - -## Implementation - -In order to use this meta stream properly, here are a few things you should -know about the implementation. - -### Event Buffering / Proxying - -All events of the `source` stream are hijacked by overwriting the `source.emit` -method. Until node implements a catch-all event listener, this is the only way. - -However, delayed-stream still continues to emit all events it captures on the -`source`, regardless of whether you have released the delayed stream yet or -not. - -Upon creation, delayed-stream captures all `source` events and stores them in -an internal event buffer. Once `delayedStream.release()` is called, all -buffered events are emitted on the `delayedStream`, and the event buffer is -cleared. After that, delayed-stream merely acts as a proxy for the underlaying -source. - -### Error handling - -Error events on `source` are buffered / proxied just like any other events. -However, `delayedStream.create` attaches a no-op `'error'` listener to the -`source`. This way you only have to handle errors on the `delayedStream` -object, rather than in two places. - -### Buffer limits - -delayed-stream provides a `maxDataSize` property that can be used to limit -the amount of data being buffered. In order to protect you from bad `source` -streams that don't react to `source.pause()`, this feature is enabled by -default. - -## API - -### DelayedStream.create(source, [options]) - -Returns a new `delayedStream`. Available options are: - -* `pauseStream` -* `maxDataSize` - -The description for those properties can be found below. - -### delayedStream.source - -The `source` stream managed by this object. This is useful if you are -passing your `delayedStream` around, and you still want to access properties -on the `source` object. - -### delayedStream.pauseStream = true - -Whether to pause the underlaying `source` when calling -`DelayedStream.create()`. Modifying this property afterwards has no effect. - -### delayedStream.maxDataSize = 1024 * 1024 - -The amount of data to buffer before emitting an `error`. - -If the underlaying source is emitting `Buffer` objects, the `maxDataSize` -refers to bytes. - -If the underlaying source is emitting JavaScript strings, the size refers to -characters. - -If you know what you are doing, you can set this property to `Infinity` to -disable this feature. You can also modify this property during runtime. - -### delayedStream.dataSize = 0 - -The amount of data buffered so far. - -### delayedStream.readable - -An ECMA5 getter that returns the value of `source.readable`. - -### delayedStream.resume() - -If the `delayedStream` has not been released so far, `delayedStream.release()` -is called. - -In either case, `source.resume()` is called. - -### delayedStream.pause() - -Calls `source.pause()`. - -### delayedStream.pipe(dest) - -Calls `delayedStream.resume()` and then proxies the arguments to `source.pipe`. - -### delayedStream.release() - -Emits and clears all events that have been buffered up so far. This does not -resume the underlaying source, use `delayedStream.resume()` instead. - -## License - -delayed-stream is licensed under the MIT license. diff --git a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/lib/delayed_stream.js b/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/lib/delayed_stream.js deleted file mode 100644 index b38fc85f..00000000 --- a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/lib/delayed_stream.js +++ /dev/null @@ -1,107 +0,0 @@ -var Stream = require('stream').Stream; -var util = require('util'); - -module.exports = DelayedStream; -function DelayedStream() { - this.source = null; - this.dataSize = 0; - this.maxDataSize = 1024 * 1024; - this.pauseStream = true; - - this._maxDataSizeExceeded = false; - this._released = false; - this._bufferedEvents = []; -} -util.inherits(DelayedStream, Stream); - -DelayedStream.create = function(source, options) { - var delayedStream = new this(); - - options = options || {}; - for (var option in options) { - delayedStream[option] = options[option]; - } - - delayedStream.source = source; - - var realEmit = source.emit; - source.emit = function() { - delayedStream._handleEmit(arguments); - return realEmit.apply(source, arguments); - }; - - source.on('error', function() {}); - if (delayedStream.pauseStream) { - source.pause(); - } - - return delayedStream; -}; - -Object.defineProperty(DelayedStream.prototype, 'readable', { - configurable: true, - enumerable: true, - get: function() { - return this.source.readable; - } -}); - -DelayedStream.prototype.setEncoding = function() { - return this.source.setEncoding.apply(this.source, arguments); -}; - -DelayedStream.prototype.resume = function() { - if (!this._released) { - this.release(); - } - - this.source.resume(); -}; - -DelayedStream.prototype.pause = function() { - this.source.pause(); -}; - -DelayedStream.prototype.release = function() { - this._released = true; - - this._bufferedEvents.forEach(function(args) { - this.emit.apply(this, args); - }.bind(this)); - this._bufferedEvents = []; -}; - -DelayedStream.prototype.pipe = function() { - var r = Stream.prototype.pipe.apply(this, arguments); - this.resume(); - return r; -}; - -DelayedStream.prototype._handleEmit = function(args) { - if (this._released) { - this.emit.apply(this, args); - return; - } - - if (args[0] === 'data') { - this.dataSize += args[1].length; - this._checkIfMaxDataSizeExceeded(); - } - - this._bufferedEvents.push(args); -}; - -DelayedStream.prototype._checkIfMaxDataSizeExceeded = function() { - if (this._maxDataSizeExceeded) { - return; - } - - if (this.dataSize <= this.maxDataSize) { - return; - } - - this._maxDataSizeExceeded = true; - var message = - 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.' - this.emit('error', new Error(message)); -}; diff --git a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/package.json b/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/package.json deleted file mode 100644 index a4717288..00000000 --- a/node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/package.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "author": { - "name": "Felix Geisendörfer", - "email": "felix@debuggable.com", - "url": "http://debuggable.com/" - }, - "contributors": [ - { - "name": "Mike Atkins", - "email": "apeherder@gmail.com" - } - ], - "name": "delayed-stream", - "description": "Buffers events from a stream until you are ready to handle them.", - "license": "MIT", - "version": "1.0.0", - "homepage": "https://github.com/felixge/node-delayed-stream", - "repository": { - "type": "git", - "url": "git://github.com/felixge/node-delayed-stream.git" - }, - "main": "./lib/delayed_stream", - "engines": { - "node": ">=0.4.0" - }, - "scripts": { - "test": "make test" - }, - "dependencies": {}, - "devDependencies": { - "fake": "0.2.0", - "far": "0.0.1" - }, - "gitHead": "07a9dc99fb8f1a488160026b9ad77493f766fb84", - "bugs": { - "url": "https://github.com/felixge/node-delayed-stream/issues" - }, - "_id": "delayed-stream@1.0.0", - "_shasum": "df3ae199acadfb7d440aaae0b29e2272b24ec619", - "_from": "delayed-stream@>=1.0.0 <1.1.0", - "_npmVersion": "2.8.3", - "_nodeVersion": "1.6.4", - "_npmUser": { - "name": "apechimp", - "email": "apeherder@gmail.com" - }, - "dist": { - "shasum": "df3ae199acadfb7d440aaae0b29e2272b24ec619", - "tarball": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - }, - "maintainers": [ - { - "name": "felixge", - "email": "felix@debuggable.com" - }, - { - "name": "apechimp", - "email": "apeherder@gmail.com" - } - ], - "directories": {}, - "_resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/combined-stream/package.json b/node_modules/request/node_modules/combined-stream/package.json deleted file mode 100644 index 74d35f12..00000000 --- a/node_modules/request/node_modules/combined-stream/package.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "author": { - "name": "Felix Geisendörfer", - "email": "felix@debuggable.com", - "url": "http://debuggable.com/" - }, - "name": "combined-stream", - "description": "A stream that emits multiple other streams one after another.", - "version": "1.0.5", - "homepage": "https://github.com/felixge/node-combined-stream", - "repository": { - "type": "git", - "url": "git://github.com/felixge/node-combined-stream.git" - }, - "main": "./lib/combined_stream", - "scripts": { - "test": "node test/run.js" - }, - "engines": { - "node": ">= 0.8" - }, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "devDependencies": { - "far": "~0.0.7" - }, - "license": "MIT", - "gitHead": "cfc7b815d090a109bcedb5bb0f6713148d55a6b7", - "bugs": { - "url": "https://github.com/felixge/node-combined-stream/issues" - }, - "_id": "combined-stream@1.0.5", - "_shasum": "938370a57b4a51dea2c77c15d5c5fdf895164009", - "_from": "combined-stream@>=1.0.5 <1.1.0", - "_npmVersion": "2.10.1", - "_nodeVersion": "0.12.4", - "_npmUser": { - "name": "alexindigo", - "email": "iam@alexindigo.com" - }, - "dist": { - "shasum": "938370a57b4a51dea2c77c15d5c5fdf895164009", - "tarball": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" - }, - "maintainers": [ - { - "name": "felixge", - "email": "felix@debuggable.com" - }, - { - "name": "celer", - "email": "dtyree77@gmail.com" - }, - { - "name": "alexindigo", - "email": "iam@alexindigo.com" - }, - { - "name": "apechimp", - "email": "apeherder@gmail.com" - } - ], - "directories": {}, - "_resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/extend/.eslintrc b/node_modules/request/node_modules/extend/.eslintrc deleted file mode 100644 index d49f1735..00000000 --- a/node_modules/request/node_modules/extend/.eslintrc +++ /dev/null @@ -1,192 +0,0 @@ -{ - "env": { - "browser": false, - "node": true, - "amd": false, - "mocha": false, - "jasmine": false - }, - - "rules": { - "accessor-pairs": [2, { getWithoutSet: false, setWithoutGet: true }], - "array-bracket-spacing": [2, "never", { - "singleValue": false, - "objectsInArrays": false, - "arraysInArrays": false - }], - "block-scoped-var": [0], - "brace-style": [2, "1tbs", { "allowSingleLine": true }], - "camelcase": [2], - "comma-dangle": [2, "never"], - "comma-spacing": [2], - "comma-style": [2, "last"], - "complexity": [2, 15], - "computed-property-spacing": [2, "never"], - "consistent-return": [2], - "consistent-this": [0, "that"], - "constructor-super": [2], - "curly": [2, "all"], - "default-case": [2], - "dot-notation": [2, { "allowKeywords": true }], - "eol-last": [2], - "eqeqeq": [2], - "func-names": [0], - "func-style": [2, "expression"], - "generator-star-spacing": [2, { "before": false, "after": true }], - "global-strict": [0, "never"], - "guard-for-in": [0], - "handle-callback-err": [0], - "key-spacing": [2, { "beforeColon": false, "afterColon": true }], - "linebreak-style": [2, "unix"], - "lines-around-comment": [2, { - "beforeBlockComment": false, - "afterBlockComment": false, - "beforeLineComment": false, - "beforeLineComment": false, - "allowBlockStart": true, - "allowBlockEnd": true - }], - "quotes": [2, "single", "avoid-escape"], - "max-depth": [1, 4], - "max-len": [0, 80, 4], - "max-nested-callbacks": [2, 2], - "max-params": [2, 2], - "max-statements": [2, 21], - "new-parens": [2], - "new-cap": [2], - "newline-after-var": [0], - "no-alert": [2], - "no-array-constructor": [2], - "no-bitwise": [0], - "no-caller": [2], - "no-catch-shadow": [2], - "no-cond-assign": [2], - "no-console": [2], - "no-constant-condition": [2], - "no-continue": [2], - "no-control-regex": [2], - "no-debugger": [2], - "no-delete-var": [2], - "no-div-regex": [0], - "no-dupe-args": [2], - "no-dupe-keys": [2], - "no-duplicate-case": [2], - "no-else-return": [0], - "no-empty": [2], - "no-empty-character-class": [2], - "no-empty-label": [2], - "no-eq-null": [0], - "no-eval": [2], - "no-ex-assign": [2], - "no-extend-native": [2], - "no-extra-bind": [2], - "no-extra-boolean-cast": [2], - "no-extra-parens": [0], - "no-extra-semi": [2], - "no-fallthrough": [2], - "no-floating-decimal": [2], - "no-func-assign": [2], - "no-implied-eval": [2], - "no-inline-comments": [0], - "no-inner-declarations": [2, "functions"], - "no-invalid-regexp": [2], - "no-irregular-whitespace": [2], - "no-iterator": [2], - "no-label-var": [2], - "no-labels": [2], - "no-lone-blocks": [2], - "no-lonely-if": [2], - "no-loop-func": [2], - "no-mixed-requires": [0, false], - "no-mixed-spaces-and-tabs": [2, false], - "no-multi-spaces": [2], - "no-multi-str": [2], - "no-multiple-empty-lines": [2, {"max": 1}], - "no-native-reassign": [2], - "no-negated-in-lhs": [2], - "no-nested-ternary": [0], - "no-new": [2], - "no-new-func": [2], - "no-new-object": [2], - "no-new-require": [0], - "no-new-wrappers": [2], - "no-obj-calls": [2], - "no-octal": [2], - "no-octal-escape": [2], - "no-param-reassign": [2], - "no-path-concat": [0], - "no-plusplus": [0], - "no-process-env": [0], - "no-process-exit": [2], - "no-proto": [2], - "no-redeclare": [2], - "no-regex-spaces": [2], - "no-reserved-keys": [2], - "no-restricted-modules": [0], - "no-return-assign": [2, "always"], - "no-script-url": [2], - "no-self-compare": [0], - "no-sequences": [2], - "no-shadow": [2], - "no-shadow-restricted-names": [2], - "no-space-before-semi": [2], - "no-spaced-func": [2], - "no-sparse-arrays": [2], - "no-sync": [0], - "no-ternary": [0], - "no-this-before-super": [2], - "no-throw-literal": [2], - "no-trailing-spaces": [2, { "skipBlankLines": false }], - "no-undef": [2], - "no-undef-init": [2], - "no-undefined": [0], - "no-underscore-dangle": [2], - "no-unexpected-multiline": [2], - "no-unneeded-ternary": [2], - "no-unreachable": [2], - "no-unused-expressions": [2], - "no-unused-vars": [2, { "vars": "all", "args": "after-used" }], - "no-use-before-define": [2], - "no-void": [0], - "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], - "no-with": [2], - "no-wrap-func": [2], - "object-curly-spacing": [2, "always"], - "object-shorthand": [2, "never"], - "one-var": [0], - "operator-assignment": [0, "always"], - "operator-linebreak": [2, "none"], - "padded-blocks": [0], - "prefer-const": [0], - "quote-props": [0], - "radix": [0], - "semi": [2], - "semi-spacing": [2, { "before": false, "after": true }], - "sort-vars": [0], - "space-after-keywords": [2, "always"], - "space-before-function-paren": [2, { "anonymous": "always", "named": "never" }], - "space-before-blocks": [0, "always"], - "space-in-brackets": [0, "never", { - "singleValue": true, - "arraysInArrays": false, - "arraysInObjects": false, - "objectsInArrays": true, - "objectsInObjects": true, - "propertyName": false - }], - "space-in-parens": [2, "never"], - "space-infix-ops": [2], - "space-return-throw-case": [2], - "space-unary-ops": [2, { "words": true, "nonwords": false }], - "spaced-comment": [2, "always"], - "spaced-line-comment": [0, "always"], - "strict": [2, "global"], - "use-isnan": [2], - "valid-jsdoc": [0], - "valid-typeof": [2], - "vars-on-top": [0], - "wrap-iife": [2], - "wrap-regex": [2], - "yoda": [2, "never", { "exceptRange": true, "onlyEquality": false }] - } -} diff --git a/node_modules/request/node_modules/extend/.jscs.json b/node_modules/request/node_modules/extend/.jscs.json deleted file mode 100644 index 7e84b282..00000000 --- a/node_modules/request/node_modules/extend/.jscs.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "additionalRules": [], - - "requireSemicolons": true, - - "disallowMultipleSpaces": true, - - "disallowIdentifierNames": [], - - "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"], - - "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch", "function"], - - "disallowSpaceAfterKeywords": [], - - "requireSpacesInAnonymousFunctionExpression": { "beforeOpeningRoundBrace": true, "beforeOpeningCurlyBrace": true }, - "requireSpacesInNamedFunctionExpression": { "beforeOpeningCurlyBrace": true }, - "disallowSpacesInNamedFunctionExpression": { "beforeOpeningRoundBrace": true }, - "requireSpacesInFunctionDeclaration": { "beforeOpeningCurlyBrace": true }, - "disallowSpacesInFunctionDeclaration": { "beforeOpeningRoundBrace": true }, - - "requireSpaceBetweenArguments": true, - - "disallowSpacesInsideParentheses": true, - - "disallowSpacesInsideArrayBrackets": true, - - "disallowQuotedKeysInObjects": "allButReserved", - - "disallowSpaceAfterObjectKeys": true, - - "requireCommaBeforeLineBreak": true, - - "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], - "requireSpaceAfterPrefixUnaryOperators": [], - - "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], - "requireSpaceBeforePostfixUnaryOperators": [], - - "disallowSpaceBeforeBinaryOperators": [], - "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], - - "requireSpaceAfterBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], - "disallowSpaceAfterBinaryOperators": [], - - "disallowImplicitTypeConversion": ["binary", "string"], - - "disallowKeywords": ["with", "eval"], - - "requireKeywordsOnNewLine": [], - "disallowKeywordsOnNewLine": ["else"], - - "requireLineFeedAtFileEnd": true, - - "disallowTrailingWhitespace": true, - - "disallowTrailingComma": true, - - "excludeFiles": ["node_modules/**", "vendor/**"], - - "disallowMultipleLineStrings": true, - - "requireDotNotation": true, - - "requireParenthesesAroundIIFE": true, - - "validateLineBreaks": "LF", - - "validateQuoteMarks": { - "escape": true, - "mark": "'" - }, - - "disallowOperatorBeforeLineBreak": [], - - "requireSpaceBeforeKeywords": [ - "do", - "for", - "if", - "else", - "switch", - "case", - "try", - "catch", - "finally", - "while", - "with", - "return" - ], - - "validateAlignedFunctionParameters": { - "lineBreakAfterOpeningBraces": true, - "lineBreakBeforeClosingBraces": true - }, - - "requirePaddingNewLinesBeforeExport": true, - - "validateNewlineAfterArrayElements": { - "maximum": 6 - }, - - "requirePaddingNewLinesAfterUseStrict": true -} - diff --git a/node_modules/request/node_modules/extend/.npmignore b/node_modules/request/node_modules/extend/.npmignore deleted file mode 100644 index 30d74d25..00000000 --- a/node_modules/request/node_modules/extend/.npmignore +++ /dev/null @@ -1 +0,0 @@ -test \ No newline at end of file diff --git a/node_modules/request/node_modules/extend/.travis.yml b/node_modules/request/node_modules/extend/.travis.yml deleted file mode 100644 index ebef6449..00000000 --- a/node_modules/request/node_modules/extend/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -language: node_js -node_js: - - "iojs-v2.3" - - "iojs-v2.2" - - "iojs-v2.1" - - "iojs-v2.0" - - "iojs-v1.8" - - "iojs-v1.7" - - "iojs-v1.6" - - "iojs-v1.5" - - "iojs-v1.4" - - "iojs-v1.3" - - "iojs-v1.2" - - "iojs-v1.1" - - "iojs-v1.0" - - "0.12" - - "0.11" - - "0.10" - - "0.9" - - "0.8" - - "0.6" - - "0.4" -before_install: - - '[ "${TRAVIS_NODE_VERSION}" = "0.6" ] || npm install -g npm@1.4.28 && npm install -g npm' -sudo: false -matrix: - fast_finish: true - allow_failures: - - node_js: "iojs-v2.2" - - node_js: "iojs-v2.1" - - node_js: "iojs-v2.0" - - node_js: "iojs-v1.7" - - node_js: "iojs-v1.6" - - node_js: "iojs-v1.5" - - node_js: "iojs-v1.4" - - node_js: "iojs-v1.3" - - node_js: "iojs-v1.2" - - node_js: "iojs-v1.1" - - node_js: "iojs-v1.0" - - node_js: "0.11" - - node_js: "0.9" - - node_js: "0.8" - - node_js: "0.6" - - node_js: "0.4" diff --git a/node_modules/request/node_modules/extend/CHANGELOG.md b/node_modules/request/node_modules/extend/CHANGELOG.md deleted file mode 100644 index ee0cfd6a..00000000 --- a/node_modules/request/node_modules/extend/CHANGELOG.md +++ /dev/null @@ -1,69 +0,0 @@ -3.0.0 / 2015-07-01 -================== - * [Possible breaking change] Use global "strict" directive (#32) - * [Tests] `int` is an ES3 reserved word - * [Tests] Test up to `io.js` `v2.3` - * [Tests] Add `npm run eslint` - * [Dev Deps] Update `covert`, `jscs` - -2.0.1 / 2015-04-25 -================== - * Use an inline `isArray` check, for ES3 browsers. (#27) - * Some old browsers fail when an identifier is `toString` - * Test latest `node` and `io.js` versions on `travis-ci`; speed up builds - * Add license info to package.json (#25) - * Update `tape`, `jscs` - * Adding a CHANGELOG - -2.0.0 / 2014-10-01 -================== - * Increase code coverage to 100%; run code coverage as part of tests - * Add `npm run lint`; Run linter as part of tests - * Remove nodeType and setInterval checks in isPlainObject - * Updating `tape`, `jscs`, `covert` - * General style and README cleanup - -1.3.0 / 2014-06-20 -================== - * Add component.json for browser support (#18) - * Use SVG for badges in README (#16) - * Updating `tape`, `covert` - * Updating travis-ci to work with multiple node versions - * Fix `deep === false` bug (returning target as {}) (#14) - * Fixing constructor checks in isPlainObject - * Adding additional test coverage - * Adding `npm run coverage` - * Add LICENSE (#13) - * Adding a warning about `false`, per #11 - * General style and whitespace cleanup - -1.2.1 / 2013-09-14 -================== - * Fixing hasOwnProperty bugs that would only have shown up in specific browsers. Fixes #8 - * Updating `tape` - -1.2.0 / 2013-09-02 -================== - * Updating the README: add badges - * Adding a missing variable reference. - * Using `tape` instead of `buster` for tests; add more tests (#7) - * Adding node 0.10 to Travis CI (#6) - * Enabling "npm test" and cleaning up package.json (#5) - * Add Travis CI. - -1.1.3 / 2012-12-06 -================== - * Added unit tests. - * Ensure extend function is named. (Looks nicer in a stack trace.) - * README cleanup. - -1.1.1 / 2012-11-07 -================== - * README cleanup. - * Added installation instructions. - * Added a missing semicolon - -1.0.0 / 2012-04-08 -================== - * Initial commit - diff --git a/node_modules/request/node_modules/extend/LICENSE b/node_modules/request/node_modules/extend/LICENSE deleted file mode 100644 index e16d6a56..00000000 --- a/node_modules/request/node_modules/extend/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Stefan Thomas - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/node_modules/request/node_modules/extend/README.md b/node_modules/request/node_modules/extend/README.md deleted file mode 100644 index 632fb0f9..00000000 --- a/node_modules/request/node_modules/extend/README.md +++ /dev/null @@ -1,62 +0,0 @@ -[![Build Status][travis-svg]][travis-url] -[![dependency status][deps-svg]][deps-url] -[![dev dependency status][dev-deps-svg]][dev-deps-url] - -# extend() for Node.js [![Version Badge][npm-version-png]][npm-url] - -`node-extend` is a port of the classic extend() method from jQuery. It behaves as you expect. It is simple, tried and true. - -## Installation - -This package is available on [npm][npm-url] as: `extend` - -``` sh -npm install extend -``` - -## Usage - -**Syntax:** extend **(** [`deep`], `target`, `object1`, [`objectN`] **)** - -*Extend one object with one or more others, returning the modified object.* - -Keep in mind that the target object will be modified, and will be returned from extend(). - -If a boolean true is specified as the first argument, extend performs a deep copy, recursively copying any objects it finds. Otherwise, the copy will share structure with the original object(s). -Undefined properties are not copied. However, properties inherited from the object's prototype will be copied over. -Warning: passing `false` as the first argument is not supported. - -### Arguments - -* `deep` *Boolean* (optional) -If set, the merge becomes recursive (i.e. deep copy). -* `target` *Object* -The object to extend. -* `object1` *Object* -The object that will be merged into the first. -* `objectN` *Object* (Optional) -More objects to merge into the first. - -## License - -`node-extend` is licensed under the [MIT License][mit-license-url]. - -## Acknowledgements - -All credit to the jQuery authors for perfecting this amazing utility. - -Ported to Node.js by [Stefan Thomas][github-justmoon] with contributions by [Jonathan Buchanan][github-insin] and [Jordan Harband][github-ljharb]. - -[travis-svg]: https://travis-ci.org/justmoon/node-extend.svg -[travis-url]: https://travis-ci.org/justmoon/node-extend -[npm-url]: https://npmjs.org/package/extend -[mit-license-url]: http://opensource.org/licenses/MIT -[github-justmoon]: https://github.com/justmoon -[github-insin]: https://github.com/insin -[github-ljharb]: https://github.com/ljharb -[npm-version-png]: http://vb.teelaun.ch/justmoon/node-extend.svg -[deps-svg]: https://david-dm.org/justmoon/node-extend.svg -[deps-url]: https://david-dm.org/justmoon/node-extend -[dev-deps-svg]: https://david-dm.org/justmoon/node-extend/dev-status.svg -[dev-deps-url]: https://david-dm.org/justmoon/node-extend#info=devDependencies - diff --git a/node_modules/request/node_modules/extend/component.json b/node_modules/request/node_modules/extend/component.json deleted file mode 100644 index 1500a2f3..00000000 --- a/node_modules/request/node_modules/extend/component.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "extend", - "author": "Stefan Thomas (http://www.justmoon.net)", - "version": "3.0.0", - "description": "Port of jQuery.extend for node.js and the browser.", - "scripts": [ - "index.js" - ], - "contributors": [ - { - "name": "Jordan Harband", - "url": "https://github.com/ljharb" - } - ], - "keywords": [ - "extend", - "clone", - "merge" - ], - "repository" : { - "type": "git", - "url": "https://github.com/justmoon/node-extend.git" - }, - "dependencies": { - }, - "devDependencies": { - "tape" : "~3.0.0", - "covert": "~0.4.0", - "jscs": "~1.6.2" - } -} - diff --git a/node_modules/request/node_modules/extend/index.js b/node_modules/request/node_modules/extend/index.js deleted file mode 100644 index f5ec75d5..00000000 --- a/node_modules/request/node_modules/extend/index.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -var hasOwn = Object.prototype.hasOwnProperty; -var toStr = Object.prototype.toString; - -var isArray = function isArray(arr) { - if (typeof Array.isArray === 'function') { - return Array.isArray(arr); - } - - return toStr.call(arr) === '[object Array]'; -}; - -var isPlainObject = function isPlainObject(obj) { - if (!obj || toStr.call(obj) !== '[object Object]') { - return false; - } - - var hasOwnConstructor = hasOwn.call(obj, 'constructor'); - var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); - // Not own constructor property must be Object - if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { - return false; - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - var key; - for (key in obj) {/**/} - - return typeof key === 'undefined' || hasOwn.call(obj, key); -}; - -module.exports = function extend() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[0], - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if (typeof target === 'boolean') { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } else if ((typeof target !== 'object' && typeof target !== 'function') || target == null) { - target = {}; - } - - for (; i < length; ++i) { - options = arguments[i]; - // Only deal with non-null/undefined values - if (options != null) { - // Extend the base object - for (name in options) { - src = target[name]; - copy = options[name]; - - // Prevent never-ending loop - if (target !== copy) { - // Recurse if we're merging plain objects or arrays - if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { - if (copyIsArray) { - copyIsArray = false; - clone = src && isArray(src) ? src : []; - } else { - clone = src && isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[name] = extend(deep, clone, copy); - - // Don't bring in undefined values - } else if (typeof copy !== 'undefined') { - target[name] = copy; - } - } - } - } - } - - // Return the modified object - return target; -}; - diff --git a/node_modules/request/node_modules/extend/package.json b/node_modules/request/node_modules/extend/package.json deleted file mode 100644 index f8341433..00000000 --- a/node_modules/request/node_modules/extend/package.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "extend", - "author": { - "name": "Stefan Thomas", - "email": "justmoon@members.fsf.org", - "url": "http://www.justmoon.net" - }, - "version": "3.0.0", - "description": "Port of jQuery.extend for node.js and the browser", - "main": "index", - "scripts": { - "test": "npm run lint && node test/index.js && npm run coverage-quiet", - "coverage": "covert test/index.js", - "coverage-quiet": "covert test/index.js --quiet", - "lint": "npm run jscs && npm run eslint", - "jscs": "jscs *.js */*.js", - "eslint": "eslint *.js */*.js" - }, - "contributors": [ - { - "name": "Jordan Harband", - "url": "https://github.com/ljharb" - } - ], - "keywords": [ - "extend", - "clone", - "merge" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/justmoon/node-extend.git" - }, - "dependencies": {}, - "devDependencies": { - "tape": "^4.0.0", - "covert": "^1.1.0", - "jscs": "^1.13.1", - "eslint": "^0.24.0" - }, - "license": "MIT", - "gitHead": "148e7270cab2e9413af2cd0cab147070d755ed6d", - "bugs": { - "url": "https://github.com/justmoon/node-extend/issues" - }, - "homepage": "https://github.com/justmoon/node-extend#readme", - "_id": "extend@3.0.0", - "_shasum": "5a474353b9f3353ddd8176dfd37b91c83a46f1d4", - "_from": "extend@>=3.0.0 <3.1.0", - "_npmVersion": "2.11.3", - "_nodeVersion": "2.3.1", - "_npmUser": { - "name": "ljharb", - "email": "ljharb@gmail.com" - }, - "dist": { - "shasum": "5a474353b9f3353ddd8176dfd37b91c83a46f1d4", - "tarball": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" - }, - "maintainers": [ - { - "name": "justmoon", - "email": "justmoon@members.fsf.org" - }, - { - "name": "ljharb", - "email": "ljharb@gmail.com" - } - ], - "directories": {}, - "_resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/forever-agent/LICENSE b/node_modules/request/node_modules/forever-agent/LICENSE deleted file mode 100644 index a4a9aee0..00000000 --- a/node_modules/request/node_modules/forever-agent/LICENSE +++ /dev/null @@ -1,55 +0,0 @@ -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and - -You must cause any modified files to carry prominent notices stating that You changed the files; and - -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/node_modules/request/node_modules/forever-agent/README.md b/node_modules/request/node_modules/forever-agent/README.md deleted file mode 100644 index 9d5b6634..00000000 --- a/node_modules/request/node_modules/forever-agent/README.md +++ /dev/null @@ -1,4 +0,0 @@ -forever-agent -============= - -HTTP Agent that keeps socket connections alive between keep-alive requests. Formerly part of mikeal/request, now a standalone module. diff --git a/node_modules/request/node_modules/forever-agent/index.js b/node_modules/request/node_modules/forever-agent/index.js deleted file mode 100644 index 416c7abd..00000000 --- a/node_modules/request/node_modules/forever-agent/index.js +++ /dev/null @@ -1,138 +0,0 @@ -module.exports = ForeverAgent -ForeverAgent.SSL = ForeverAgentSSL - -var util = require('util') - , Agent = require('http').Agent - , net = require('net') - , tls = require('tls') - , AgentSSL = require('https').Agent - -function getConnectionName(host, port) { - var name = '' - if (typeof host === 'string') { - name = host + ':' + port - } else { - // For node.js v012.0 and iojs-v1.5.1, host is an object. And any existing localAddress is part of the connection name. - name = host.host + ':' + host.port + ':' + (host.localAddress ? (host.localAddress + ':') : ':') - } - return name -} - -function ForeverAgent(options) { - var self = this - self.options = options || {} - self.requests = {} - self.sockets = {} - self.freeSockets = {} - self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets - self.minSockets = self.options.minSockets || ForeverAgent.defaultMinSockets - self.on('free', function(socket, host, port) { - var name = getConnectionName(host, port) - - if (self.requests[name] && self.requests[name].length) { - self.requests[name].shift().onSocket(socket) - } else if (self.sockets[name].length < self.minSockets) { - if (!self.freeSockets[name]) self.freeSockets[name] = [] - self.freeSockets[name].push(socket) - - // if an error happens while we don't use the socket anyway, meh, throw the socket away - var onIdleError = function() { - socket.destroy() - } - socket._onIdleError = onIdleError - socket.on('error', onIdleError) - } else { - // If there are no pending requests just destroy the - // socket and it will get removed from the pool. This - // gets us out of timeout issues and allows us to - // default to Connection:keep-alive. - socket.destroy() - } - }) - -} -util.inherits(ForeverAgent, Agent) - -ForeverAgent.defaultMinSockets = 5 - - -ForeverAgent.prototype.createConnection = net.createConnection -ForeverAgent.prototype.addRequestNoreuse = Agent.prototype.addRequest -ForeverAgent.prototype.addRequest = function(req, host, port) { - var name = getConnectionName(host, port) - - if (typeof host !== 'string') { - var options = host - port = options.port - host = options.host - } - - if (this.freeSockets[name] && this.freeSockets[name].length > 0 && !req.useChunkedEncodingByDefault) { - var idleSocket = this.freeSockets[name].pop() - idleSocket.removeListener('error', idleSocket._onIdleError) - delete idleSocket._onIdleError - req._reusedSocket = true - req.onSocket(idleSocket) - } else { - this.addRequestNoreuse(req, host, port) - } -} - -ForeverAgent.prototype.removeSocket = function(s, name, host, port) { - if (this.sockets[name]) { - var index = this.sockets[name].indexOf(s) - if (index !== -1) { - this.sockets[name].splice(index, 1) - } - } else if (this.sockets[name] && this.sockets[name].length === 0) { - // don't leak - delete this.sockets[name] - delete this.requests[name] - } - - if (this.freeSockets[name]) { - var index = this.freeSockets[name].indexOf(s) - if (index !== -1) { - this.freeSockets[name].splice(index, 1) - if (this.freeSockets[name].length === 0) { - delete this.freeSockets[name] - } - } - } - - if (this.requests[name] && this.requests[name].length) { - // If we have pending requests and a socket gets closed a new one - // needs to be created to take over in the pool for the one that closed. - this.createSocket(name, host, port).emit('free') - } -} - -function ForeverAgentSSL (options) { - ForeverAgent.call(this, options) -} -util.inherits(ForeverAgentSSL, ForeverAgent) - -ForeverAgentSSL.prototype.createConnection = createConnectionSSL -ForeverAgentSSL.prototype.addRequestNoreuse = AgentSSL.prototype.addRequest - -function createConnectionSSL (port, host, options) { - if (typeof port === 'object') { - options = port; - } else if (typeof host === 'object') { - options = host; - } else if (typeof options === 'object') { - options = options; - } else { - options = {}; - } - - if (typeof port === 'number') { - options.port = port; - } - - if (typeof host === 'string') { - options.host = host; - } - - return tls.connect(options); -} diff --git a/node_modules/request/node_modules/forever-agent/package.json b/node_modules/request/node_modules/forever-agent/package.json deleted file mode 100644 index 36c4f594..00000000 --- a/node_modules/request/node_modules/forever-agent/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "author": { - "name": "Mikeal Rogers", - "email": "mikeal.rogers@gmail.com", - "url": "http://www.futurealoof.com" - }, - "name": "forever-agent", - "description": "HTTP Agent that keeps socket connections alive between keep-alive requests. Formerly part of mikeal/request, now a standalone module.", - "version": "0.6.1", - "license": "Apache-2.0", - "repository": { - "url": "git+https://github.com/mikeal/forever-agent.git" - }, - "main": "index.js", - "dependencies": {}, - "devDependencies": {}, - "optionalDependencies": {}, - "engines": { - "node": "*" - }, - "gitHead": "1b3b6163f2b3c2c4122bbfa288c1325c0df9871d", - "bugs": { - "url": "https://github.com/mikeal/forever-agent/issues" - }, - "homepage": "https://github.com/mikeal/forever-agent", - "_id": "forever-agent@0.6.1", - "scripts": {}, - "_shasum": "fbc71f0c41adeb37f96c577ad1ed42d8fdacca91", - "_from": "forever-agent@>=0.6.1 <0.7.0", - "_npmVersion": "1.4.28", - "_npmUser": { - "name": "simov", - "email": "simeonvelichkov@gmail.com" - }, - "maintainers": [ - { - "name": "mikeal", - "email": "mikeal.rogers@gmail.com" - }, - { - "name": "nylen", - "email": "jnylen@gmail.com" - }, - { - "name": "simov", - "email": "simeonvelichkov@gmail.com" - } - ], - "dist": { - "shasum": "fbc71f0c41adeb37f96c577ad1ed42d8fdacca91", - "tarball": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/form-data/.dockerignore b/node_modules/request/node_modules/form-data/.dockerignore deleted file mode 100644 index c67305cf..00000000 --- a/node_modules/request/node_modules/form-data/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -*.iml -*.sublime-* -*.un~ -.idea -sftp-config.json -node_modules/ -test/tmp/ diff --git a/node_modules/request/node_modules/form-data/.editorconfig b/node_modules/request/node_modules/form-data/.editorconfig deleted file mode 100644 index 0f099897..00000000 --- a/node_modules/request/node_modules/form-data/.editorconfig +++ /dev/null @@ -1,10 +0,0 @@ -# editorconfig.org -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true diff --git a/node_modules/request/node_modules/form-data/.eslintignore b/node_modules/request/node_modules/form-data/.eslintignore deleted file mode 100644 index 8d87b1d2..00000000 --- a/node_modules/request/node_modules/form-data/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/* diff --git a/node_modules/request/node_modules/form-data/.eslintrc b/node_modules/request/node_modules/form-data/.eslintrc deleted file mode 100644 index 129fcef0..00000000 --- a/node_modules/request/node_modules/form-data/.eslintrc +++ /dev/null @@ -1,64 +0,0 @@ -{ - "env": { - "node": true, - "browser": false - }, - "rules": { - // 2-space indentation - "indent": [2, 2, {"SwitchCase": 1}], - // Require strings to use single quotes - "quotes": [2, "single"], - // Allow only unix line-endings - "linebreak-style": [2, "unix"], - // Always require semicolons - "semi": [2, "always"], - // Require curly braces for multi-line control statements - "curly": [2, "multi-line"], - // Always handle callback error cases - "handle-callback-err": [2, "^err"], - // Require JSDoc for all function definitions - "valid-jsdoc": [2, { - "requireReturn": false, - "requireReturnDescription": false, - "prefer": { - "return": "returns" - } - }], - "require-jsdoc": [2, { - "require": { - "FunctionDeclaration": true - } - }], - "no-redeclare": [2, { "builtinGlobals": true }], - "no-shadow": [2, { "builtinGlobals": true, "hoist": "all" }], - // Disallow using variables before they've been defined - // functions are ok - "no-use-before-define": [2, "nofunc"], - "no-shadow-restricted-names": 2, - "no-extra-semi": 2, - // Disallow unused variables - "no-unused-vars": 2, - "no-undef": 2, - // Use if () { } - // ^ space - "keyword-spacing": 2, - // Use if () { } - // ^ space - "space-before-blocks": [2, "always"], - // eslint can't handle this, so the check is disabled. - "key-spacing": 0, - "strict": 0, - // Do not force dot-notation - "dot-notation": 0, - "eol-last": 0, - "no-new": 0, - "semi-spacing": 0, - // Allow multi spaces around operators since they are - // used for alignment. This is not consistent in the - // code. - "no-multi-spaces": 0, - "eqeqeq": 0, - "no-mixed-requires": 0, - "no-console": 0 - } -} diff --git a/node_modules/request/node_modules/form-data/License b/node_modules/request/node_modules/form-data/License deleted file mode 100644 index c7ff12a2..00000000 --- a/node_modules/request/node_modules/form-data/License +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. diff --git a/node_modules/request/node_modules/form-data/README.md b/node_modules/request/node_modules/form-data/README.md deleted file mode 100644 index e857db6e..00000000 --- a/node_modules/request/node_modules/form-data/README.md +++ /dev/null @@ -1,218 +0,0 @@ -# Form-Data [![NPM Module](https://img.shields.io/npm/v/form-data.svg)](https://www.npmjs.com/package/form-data) [![Join the chat at https://gitter.im/form-data/form-data](http://form-data.github.io/images/gitterbadge.svg)](https://gitter.im/form-data/form-data) - -A library to create readable ```"multipart/form-data"``` streams. Can be used to submit forms and file uploads to other web applications. - -The API of this library is inspired by the [XMLHttpRequest-2 FormData Interface][xhr2-fd]. - -[xhr2-fd]: http://dev.w3.org/2006/webapi/XMLHttpRequest-2/Overview.html#the-formdata-interface -[streams2-thing]: http://nodejs.org/api/stream.html#stream_compatibility_with_older_node_versions - -[![Linux Build](https://img.shields.io/travis/form-data/form-data/master.svg?label=linux:0.10-5.x)](https://travis-ci.org/form-data/form-data) -[![Windows Build](https://img.shields.io/appveyor/ci/alexindigo/form-data/master.svg?label=windows:0.10-5.x)](https://ci.appveyor.com/project/alexindigo/form-data) -[![Coverage Status](https://img.shields.io/coveralls/form-data/form-data/master.svg?label=code+coverage)](https://coveralls.io/github/form-data/form-data?branch=master) - -[![Dependency Status](https://img.shields.io/david/form-data/form-data.svg)](https://david-dm.org/form-data/form-data) -[![Codacy Badge](https://img.shields.io/codacy/43ece80331c246179695e41f81eeffe2.svg)](https://www.codacy.com/app/form-data/form-data) -[![bitHound Overall Score](https://www.bithound.io/github/form-data/form-data/badges/score.svg)](https://www.bithound.io/github/form-data/form-data) - -## Install - -``` -npm install form-data -``` - -## Usage - -In this example we are constructing a form with 3 fields that contain a string, -a buffer and a file stream. - -``` javascript -var FormData = require('form-data'); -var fs = require('fs'); - -var form = new FormData(); -form.append('my_field', 'my value'); -form.append('my_buffer', new Buffer(10)); -form.append('my_file', fs.createReadStream('/foo/bar.jpg')); -``` - -Also you can use http-response stream: - -``` javascript -var FormData = require('form-data'); -var http = require('http'); - -var form = new FormData(); - -http.request('http://nodejs.org/images/logo.png', function(response) { - form.append('my_field', 'my value'); - form.append('my_buffer', new Buffer(10)); - form.append('my_logo', response); -}); -``` - -Or @mikeal's [request](https://github.com/request/request) stream: - -``` javascript -var FormData = require('form-data'); -var request = require('request'); - -var form = new FormData(); - -form.append('my_field', 'my value'); -form.append('my_buffer', new Buffer(10)); -form.append('my_logo', request('http://nodejs.org/images/logo.png')); -``` - -In order to submit this form to a web application, call ```submit(url, [callback])``` method: - -``` javascript -form.submit('http://example.org/', function(err, res) { - // res – response object (http.IncomingMessage) // - res.resume(); -}); - -``` - -For more advanced request manipulations ```submit()``` method returns ```http.ClientRequest``` object, or you can choose from one of the alternative submission methods. - -### Alternative submission methods - -You can use node's http client interface: - -``` javascript -var http = require('http'); - -var request = http.request({ - method: 'post', - host: 'example.org', - path: '/upload', - headers: form.getHeaders() -}); - -form.pipe(request); - -request.on('response', function(res) { - console.log(res.statusCode); -}); -``` - -Or if you would prefer the `'Content-Length'` header to be set for you: - -``` javascript -form.submit('example.org/upload', function(err, res) { - console.log(res.statusCode); -}); -``` - -To use custom headers and pre-known length in parts: - -``` javascript -var CRLF = '\r\n'; -var form = new FormData(); - -var options = { - header: CRLF + '--' + form.getBoundary() + CRLF + 'X-Custom-Header: 123' + CRLF + CRLF, - knownLength: 1 -}; - -form.append('my_buffer', buffer, options); - -form.submit('http://example.com/', function(err, res) { - if (err) throw err; - console.log('Done'); -}); -``` - -Form-Data can recognize and fetch all the required information from common types of streams (```fs.readStream```, ```http.response``` and ```mikeal's request```), for some other types of streams you'd need to provide "file"-related information manually: - -``` javascript -someModule.stream(function(err, stdout, stderr) { - if (err) throw err; - - var form = new FormData(); - - form.append('file', stdout, { - filename: 'unicycle.jpg', - contentType: 'image/jpg', - knownLength: 19806 - }); - - form.submit('http://example.com/', function(err, res) { - if (err) throw err; - console.log('Done'); - }); -}); -``` - -For edge cases, like POST request to URL with query string or to pass HTTP auth credentials, object can be passed to `form.submit()` as first parameter: - -``` javascript -form.submit({ - host: 'example.com', - path: '/probably.php?extra=params', - auth: 'username:password' -}, function(err, res) { - console.log(res.statusCode); -}); -``` - -In case you need to also send custom HTTP headers with the POST request, you can use the `headers` key in first parameter of `form.submit()`: - -``` javascript -form.submit({ - host: 'example.com', - path: '/surelynot.php', - headers: {'x-test-header': 'test-header-value'} -}, function(err, res) { - console.log(res.statusCode); -}); -``` - -### Integration with other libraries - -#### Request - -Form submission using [request](https://github.com/request/request): - -```javascript -var formData = { - my_field: 'my_value', - my_file: fs.createReadStream(__dirname + '/unicycle.jpg'), -}; - -request.post({url:'http://service.com/upload', formData: formData}, function(err, httpResponse, body) { - if (err) { - return console.error('upload failed:', err); - } - console.log('Upload successful! Server responded with:', body); -}); -``` - -For more details see [request readme](https://github.com/request/request#multipartform-data-multipart-form-uploads). - -#### node-fetch - -You can also submit a form using [node-fetch](https://github.com/bitinn/node-fetch): - -```javascript -var form = new FormData(); - -form.append('a', 1); - -fetch('http://example.com', { method: 'POST', body: form }) - .then(function(res) { - return res.json(); - }).then(function(json) { - console.log(json); - }); -``` - -## Notes - -- ```getLengthSync()``` method DOESN'T calculate length for streams, use ```knownLength``` options as workaround. -- If it feels like FormData hangs after submit and you're on ```node-0.10```, please check [Compatibility with Older Node Versions][streams2-thing] - -## License - -Form-Data is licensed under the MIT license. diff --git a/node_modules/request/node_modules/form-data/lib/browser.js b/node_modules/request/node_modules/form-data/lib/browser.js deleted file mode 100644 index 8141d658..00000000 --- a/node_modules/request/node_modules/form-data/lib/browser.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-env browser */ -module.exports = FormData; diff --git a/node_modules/request/node_modules/form-data/lib/form_data.js b/node_modules/request/node_modules/form-data/lib/form_data.js deleted file mode 100644 index 55328b46..00000000 --- a/node_modules/request/node_modules/form-data/lib/form_data.js +++ /dev/null @@ -1,411 +0,0 @@ -var CombinedStream = require('combined-stream'); -var util = require('util'); -var path = require('path'); -var http = require('http'); -var https = require('https'); -var parseUrl = require('url').parse; -var fs = require('fs'); -var mime = require('mime-types'); -var async = require('async'); -var populate = require('./populate.js'); - -// Public API -module.exports = FormData; - -// make it a Stream -util.inherits(FormData, CombinedStream); - -/** - * Create readable "multipart/form-data" streams. - * Can be used to submit forms - * and file uploads to other web applications. - * - * @constructor - */ -function FormData() { - if (!(this instanceof FormData)) { - throw new TypeError('Failed to construct FormData: Please use the _new_ operator, this object constructor cannot be called as a function.'); - } - - this._overheadLength = 0; - this._valueLength = 0; - this._lengthRetrievers = []; - - CombinedStream.call(this); -} - -FormData.LINE_BREAK = '\r\n'; -FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream'; - -FormData.prototype.append = function(field, value, options) { - - options = options || {}; - - // allow filename as single option - if (typeof options == 'string') { - options = {filename: options}; - } - - var append = CombinedStream.prototype.append.bind(this); - - // all that streamy business can't handle numbers - if (typeof value == 'number') { - value = '' + value; - } - - // https://github.com/felixge/node-form-data/issues/38 - if (util.isArray(value)) { - // Please convert your array into string - // the way web server expects it - this._error(new Error('Arrays are not supported.')); - return; - } - - var header = this._multiPartHeader(field, value, options); - var footer = this._multiPartFooter(); - - append(header); - append(value); - append(footer); - - // pass along options.knownLength - this._trackLength(header, value, options); -}; - -FormData.prototype._trackLength = function(header, value, options) { - var valueLength = 0; - - // used w/ getLengthSync(), when length is known. - // e.g. for streaming directly from a remote server, - // w/ a known file a size, and not wanting to wait for - // incoming file to finish to get its size. - if (options.knownLength != null) { - valueLength += +options.knownLength; - } else if (Buffer.isBuffer(value)) { - valueLength = value.length; - } else if (typeof value === 'string') { - valueLength = Buffer.byteLength(value); - } - - this._valueLength += valueLength; - - // @check why add CRLF? does this account for custom/multiple CRLFs? - this._overheadLength += - Buffer.byteLength(header) + - FormData.LINE_BREAK.length; - - // empty or either doesn't have path or not an http response - if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) { - return; - } - - // no need to bother with the length - if (!options.knownLength) { - this._lengthRetrievers.push(function(next) { - - if (value.hasOwnProperty('fd')) { - - // take read range into a account - // `end` = Infinity –> read file till the end - // - // TODO: Looks like there is bug in Node fs.createReadStream - // it doesn't respect `end` options without `start` options - // Fix it when node fixes it. - // https://github.com/joyent/node/issues/7819 - if (value.end != undefined && value.end != Infinity && value.start != undefined) { - - // when end specified - // no need to calculate range - // inclusive, starts with 0 - next(null, value.end + 1 - (value.start ? value.start : 0)); - - // not that fast snoopy - } else { - // still need to fetch file size from fs - fs.stat(value.path, function(err, stat) { - - var fileSize; - - if (err) { - next(err); - return; - } - - // update final size based on the range options - fileSize = stat.size - (value.start ? value.start : 0); - next(null, fileSize); - }); - } - - // or http response - } else if (value.hasOwnProperty('httpVersion')) { - next(null, +value.headers['content-length']); - - // or request stream http://github.com/mikeal/request - } else if (value.hasOwnProperty('httpModule')) { - // wait till response come back - value.on('response', function(response) { - value.pause(); - next(null, +response.headers['content-length']); - }); - value.resume(); - - // something else - } else { - next('Unknown stream'); - } - }); - } -}; - -FormData.prototype._multiPartHeader = function(field, value, options) { - // custom header specified (as string)? - // it becomes responsible for boundary - // (e.g. to handle extra CRLFs on .NET servers) - if (options.header) { - return options.header; - } - - var contentDisposition = this._getContentDisposition(value, options); - var contentType = this._getContentType(value, options); - - var contents = ''; - var headers = { - // add custom disposition as third element or keep it two elements if not - 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []), - // if no content type. allow it to be empty array - 'Content-Type': [].concat(contentType || []) - }; - - for (var prop in headers) { - if (headers[prop].length) { - contents += prop + ': ' + headers[prop].join('; ') + FormData.LINE_BREAK; - } - } - - return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK; -}; - -FormData.prototype._getContentDisposition = function(value, options) { - - var contentDisposition; - - // custom filename takes precedence - // fs- and request- streams have path property - var filename = options.filename || value.path; - - // or try http response - if (!filename && value.readable && value.hasOwnProperty('httpVersion')) { - filename = value.client._httpMessage.path; - } - - if (filename) { - contentDisposition = 'filename="' + path.basename(filename) + '"'; - } - - return contentDisposition; -}; - -FormData.prototype._getContentType = function(value, options) { - - // use custom content-type above all - var contentType = options.contentType; - - // or try `path` from fs-, request- streams - if (!contentType && value.path) { - contentType = mime.lookup(value.path); - } - - // or if it's http-reponse - if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) { - contentType = value.headers['content-type']; - } - - // or guess it from the filename - if (!contentType && options.filename) { - contentType = mime.lookup(options.filename); - } - - // fallback to the default content type if `value` is not simple value - if (!contentType && typeof value == 'object') { - contentType = FormData.DEFAULT_CONTENT_TYPE; - } - - return contentType; -}; - -FormData.prototype._multiPartFooter = function() { - return function(next) { - var footer = FormData.LINE_BREAK; - - var lastPart = (this._streams.length === 0); - if (lastPart) { - footer += this._lastBoundary(); - } - - next(footer); - }.bind(this); -}; - -FormData.prototype._lastBoundary = function() { - return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK; -}; - -FormData.prototype.getHeaders = function(userHeaders) { - var header; - var formHeaders = { - 'content-type': 'multipart/form-data; boundary=' + this.getBoundary() - }; - - for (header in userHeaders) { - if (userHeaders.hasOwnProperty(header)) { - formHeaders[header.toLowerCase()] = userHeaders[header]; - } - } - - return formHeaders; -}; - -FormData.prototype.getCustomHeaders = function(contentType) { - contentType = contentType ? contentType : 'multipart/form-data'; - - var formHeaders = { - 'content-type': contentType + '; boundary=' + this.getBoundary(), - 'content-length': this.getLengthSync() - }; - - return formHeaders; -}; - -FormData.prototype.getBoundary = function() { - if (!this._boundary) { - this._generateBoundary(); - } - - return this._boundary; -}; - -FormData.prototype._generateBoundary = function() { - // This generates a 50 character boundary similar to those used by Firefox. - // They are optimized for boyer-moore parsing. - var boundary = '--------------------------'; - for (var i = 0; i < 24; i++) { - boundary += Math.floor(Math.random() * 10).toString(16); - } - - this._boundary = boundary; -}; - -// Note: getLengthSync DOESN'T calculate streams length -// As workaround one can calculate file size manually -// and add it as knownLength option -FormData.prototype.getLengthSync = function() { - var knownLength = this._overheadLength + this._valueLength; - - // Don't get confused, there are 3 "internal" streams for each keyval pair - // so it basically checks if there is any value added to the form - if (this._streams.length) { - knownLength += this._lastBoundary().length; - } - - // https://github.com/form-data/form-data/issues/40 - if (this._lengthRetrievers.length) { - // Some async length retrievers are present - // therefore synchronous length calculation is false. - // Please use getLength(callback) to get proper length - this._error(new Error('Cannot calculate proper length in synchronous way.')); - } - - return knownLength; -}; - -FormData.prototype.getLength = function(cb) { - var knownLength = this._overheadLength + this._valueLength; - - if (this._streams.length) { - knownLength += this._lastBoundary().length; - } - - if (!this._lengthRetrievers.length) { - process.nextTick(cb.bind(this, null, knownLength)); - return; - } - - async.parallel(this._lengthRetrievers, function(err, values) { - if (err) { - cb(err); - return; - } - - values.forEach(function(length) { - knownLength += length; - }); - - cb(null, knownLength); - }); -}; - -FormData.prototype.submit = function(params, cb) { - var request - , options - , defaults = {method: 'post'} - ; - - // parse provided url if it's string - // or treat it as options object - if (typeof params == 'string') { - - params = parseUrl(params); - options = populate({ - port: params.port, - path: params.pathname, - host: params.hostname - }, defaults); - - // use custom params - } else { - - options = populate(params, defaults); - // if no port provided use default one - if (!options.port) { - options.port = options.protocol == 'https:' ? 443 : 80; - } - } - - // put that good code in getHeaders to some use - options.headers = this.getHeaders(params.headers); - - // https if specified, fallback to http in any other case - if (options.protocol == 'https:') { - request = https.request(options); - } else { - request = http.request(options); - } - - // get content length and fire away - this.getLength(function(err, length) { - if (err) { - this._error(err); - return; - } - - // add content length - request.setHeader('Content-Length', length); - - this.pipe(request); - if (cb) { - request.on('error', cb); - request.on('response', cb.bind(this, null)); - } - }.bind(this)); - - return request; -}; - -FormData.prototype._error = function(err) { - if (!this.error) { - this.error = err; - this.pause(); - this.emit('error', err); - } -}; diff --git a/node_modules/request/node_modules/form-data/lib/populate.js b/node_modules/request/node_modules/form-data/lib/populate.js deleted file mode 100644 index 6f64a6d3..00000000 --- a/node_modules/request/node_modules/form-data/lib/populate.js +++ /dev/null @@ -1,9 +0,0 @@ -// populates missing values -module.exports = function(dst, src) { - for (var prop in src) { - if (src.hasOwnProperty(prop) && !dst[prop]) { - dst[prop] = src[prop]; - } - } - return dst; -}; diff --git a/node_modules/request/node_modules/form-data/node_modules/async/CHANGELOG.md b/node_modules/request/node_modules/form-data/node_modules/async/CHANGELOG.md deleted file mode 100644 index f15e0812..00000000 --- a/node_modules/request/node_modules/form-data/node_modules/async/CHANGELOG.md +++ /dev/null @@ -1,125 +0,0 @@ -# v1.5.2 -- Allow using `"consructor"` as an argument in `memoize` (#998) -- Give a better error messsage when `auto` dependency checking fails (#994) -- Various doc updates (#936, #956, #979, #1002) - -# v1.5.1 -- Fix issue with `pause` in `queue` with concurrency enabled (#946) -- `while` and `until` now pass the final result to callback (#963) -- `auto` will properly handle concurrency when there is no callback (#966) -- `auto` will now properly stop execution when an error occurs (#988, #993) -- Various doc fixes (#971, #980) - -# v1.5.0 - -- Added `transform`, analogous to [`_.transform`](http://lodash.com/docs#transform) (#892) -- `map` now returns an object when an object is passed in, rather than array with non-numeric keys. `map` will begin always returning an array with numeric indexes in the next major release. (#873) -- `auto` now accepts an optional `concurrency` argument to limit the number of running tasks (#637) -- Added `queue#workersList()`, to retrieve the list of currently running tasks. (#891) -- Various code simplifications (#896, #904) -- Various doc fixes :scroll: (#890, #894, #903, #905, #912) - -# v1.4.2 - -- Ensure coverage files don't get published on npm (#879) - -# v1.4.1 - -- Add in overlooked `detectLimit` method (#866) -- Removed unnecessary files from npm releases (#861) -- Removed usage of a reserved word to prevent :boom: in older environments (#870) - -# v1.4.0 - -- `asyncify` now supports promises (#840) -- Added `Limit` versions of `filter` and `reject` (#836) -- Add `Limit` versions of `detect`, `some` and `every` (#828, #829) -- `some`, `every` and `detect` now short circuit early (#828, #829) -- Improve detection of the global object (#804), enabling use in WebWorkers -- `whilst` now called with arguments from iterator (#823) -- `during` now gets called with arguments from iterator (#824) -- Code simplifications and optimizations aplenty ([diff](https://github.com/caolan/async/compare/v1.3.0...v1.4.0)) - - -# v1.3.0 - -New Features: -- Added `constant` -- Added `asyncify`/`wrapSync` for making sync functions work with callbacks. (#671, #806) -- Added `during` and `doDuring`, which are like `whilst` with an async truth test. (#800) -- `retry` now accepts an `interval` parameter to specify a delay between retries. (#793) -- `async` should work better in Web Workers due to better `root` detection (#804) -- Callbacks are now optional in `whilst`, `doWhilst`, `until`, and `doUntil` (#642) -- Various internal updates (#786, #801, #802, #803) -- Various doc fixes (#790, #794) - -Bug Fixes: -- `cargo` now exposes the `payload` size, and `cargo.payload` can be changed on the fly after the `cargo` is created. (#740, #744, #783) - - -# v1.2.1 - -Bug Fix: - -- Small regression with synchronous iterator behavior in `eachSeries` with a 1-element array. Before 1.1.0, `eachSeries`'s callback was called on the same tick, which this patch restores. In 2.0.0, it will be called on the next tick. (#782) - - -# v1.2.0 - -New Features: - -- Added `timesLimit` (#743) -- `concurrency` can be changed after initialization in `queue` by setting `q.concurrency`. The new concurrency will be reflected the next time a task is processed. (#747, #772) - -Bug Fixes: - -- Fixed a regression in `each` and family with empty arrays that have additional properties. (#775, #777) - - -# v1.1.1 - -Bug Fix: - -- Small regression with synchronous iterator behavior in `eachSeries` with a 1-element array. Before 1.1.0, `eachSeries`'s callback was called on the same tick, which this patch restores. In 2.0.0, it will be called on the next tick. (#782) - - -# v1.1.0 - -New Features: - -- `cargo` now supports all of the same methods and event callbacks as `queue`. -- Added `ensureAsync` - A wrapper that ensures an async function calls its callback on a later tick. (#769) -- Optimized `map`, `eachOf`, and `waterfall` families of functions -- Passing a `null` or `undefined` array to `map`, `each`, `parallel` and families will be treated as an empty array (#667). -- The callback is now optional for the composed results of `compose` and `seq`. (#618) -- Reduced file size by 4kb, (minified version by 1kb) -- Added code coverage through `nyc` and `coveralls` (#768) - -Bug Fixes: - -- `forever` will no longer stack overflow with a synchronous iterator (#622) -- `eachLimit` and other limit functions will stop iterating once an error occurs (#754) -- Always pass `null` in callbacks when there is no error (#439) -- Ensure proper conditions when calling `drain()` after pushing an empty data set to a queue (#668) -- `each` and family will properly handle an empty array (#578) -- `eachSeries` and family will finish if the underlying array is modified during execution (#557) -- `queue` will throw if a non-function is passed to `q.push()` (#593) -- Doc fixes (#629, #766) - - -# v1.0.0 - -No known breaking changes, we are simply complying with semver from here on out. - -Changes: - -- Start using a changelog! -- Add `forEachOf` for iterating over Objects (or to iterate Arrays with indexes available) (#168 #704 #321) -- Detect deadlocks in `auto` (#663) -- Better support for require.js (#527) -- Throw if queue created with concurrency `0` (#714) -- Fix unneeded iteration in `queue.resume()` (#758) -- Guard against timer mocking overriding `setImmediate` (#609 #611) -- Miscellaneous doc fixes (#542 #596 #615 #628 #631 #690 #729) -- Use single noop function internally (#546) -- Optimize internal `_each`, `_map` and `_keys` functions. diff --git a/node_modules/request/node_modules/form-data/node_modules/async/LICENSE b/node_modules/request/node_modules/form-data/node_modules/async/LICENSE deleted file mode 100644 index 8f296985..00000000 --- a/node_modules/request/node_modules/form-data/node_modules/async/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2010-2014 Caolan McMahon - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/node_modules/request/node_modules/form-data/node_modules/async/README.md b/node_modules/request/node_modules/form-data/node_modules/async/README.md deleted file mode 100644 index 316c4050..00000000 --- a/node_modules/request/node_modules/form-data/node_modules/async/README.md +++ /dev/null @@ -1,1877 +0,0 @@ -# Async.js - -[![Build Status via Travis CI](https://travis-ci.org/caolan/async.svg?branch=master)](https://travis-ci.org/caolan/async) -[![NPM version](http://img.shields.io/npm/v/async.svg)](https://www.npmjs.org/package/async) -[![Coverage Status](https://coveralls.io/repos/caolan/async/badge.svg?branch=master)](https://coveralls.io/r/caolan/async?branch=master) -[![Join the chat at https://gitter.im/caolan/async](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/caolan/async?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - - -Async is a utility module which provides straight-forward, powerful functions -for working with asynchronous JavaScript. Although originally designed for -use with [Node.js](http://nodejs.org) and installable via `npm install async`, -it can also be used directly in the browser. - -Async is also installable via: - -- [bower](http://bower.io/): `bower install async` -- [component](https://github.com/component/component): `component install - caolan/async` -- [jam](http://jamjs.org/): `jam install async` -- [spm](http://spmjs.io/): `spm install async` - -Async provides around 20 functions that include the usual 'functional' -suspects (`map`, `reduce`, `filter`, `each`…) as well as some common patterns -for asynchronous control flow (`parallel`, `series`, `waterfall`…). All these -functions assume you follow the Node.js convention of providing a single -callback as the last argument of your `async` function. - - -## Quick Examples - -```javascript -async.map(['file1','file2','file3'], fs.stat, function(err, results){ - // results is now an array of stats for each file -}); - -async.filter(['file1','file2','file3'], fs.exists, function(results){ - // results now equals an array of the existing files -}); - -async.parallel([ - function(){ ... }, - function(){ ... } -], callback); - -async.series([ - function(){ ... }, - function(){ ... } -]); -``` - -There are many more functions available so take a look at the docs below for a -full list. This module aims to be comprehensive, so if you feel anything is -missing please create a GitHub issue for it. - -## Common Pitfalls [(StackOverflow)](http://stackoverflow.com/questions/tagged/async.js) -### Synchronous iteration functions - -If you get an error like `RangeError: Maximum call stack size exceeded.` or other stack overflow issues when using async, you are likely using a synchronous iterator. By *synchronous* we mean a function that calls its callback on the same tick in the javascript event loop, without doing any I/O or using any timers. Calling many callbacks iteratively will quickly overflow the stack. If you run into this issue, just defer your callback with `async.setImmediate` to start a new call stack on the next tick of the event loop. - -This can also arise by accident if you callback early in certain cases: - -```js -async.eachSeries(hugeArray, function iterator(item, callback) { - if (inCache(item)) { - callback(null, cache[item]); // if many items are cached, you'll overflow - } else { - doSomeIO(item, callback); - } -}, function done() { - //... -}); -``` - -Just change it to: - -```js -async.eachSeries(hugeArray, function iterator(item, callback) { - if (inCache(item)) { - async.setImmediate(function () { - callback(null, cache[item]); - }); - } else { - doSomeIO(item, callback); - //... -``` - -Async guards against synchronous functions in some, but not all, cases. If you are still running into stack overflows, you can defer as suggested above, or wrap functions with [`async.ensureAsync`](#ensureAsync) Functions that are asynchronous by their nature do not have this problem and don't need the extra callback deferral. - -If JavaScript's event loop is still a bit nebulous, check out [this article](http://blog.carbonfive.com/2013/10/27/the-javascript-event-loop-explained/) or [this talk](http://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html) for more detailed information about how it works. - - -### Multiple callbacks - -Make sure to always `return` when calling a callback early, otherwise you will cause multiple callbacks and unpredictable behavior in many cases. - -```js -async.waterfall([ - function (callback) { - getSomething(options, function (err, result) { - if (err) { - callback(new Error("failed getting something:" + err.message)); - // we should return here - } - // since we did not return, this callback still will be called and - // `processData` will be called twice - callback(null, result); - }); - }, - processData -], done) -``` - -It is always good practice to `return callback(err, result)` whenever a callback call is not the last statement of a function. - - -### Binding a context to an iterator - -This section is really about `bind`, not about `async`. If you are wondering how to -make `async` execute your iterators in a given context, or are confused as to why -a method of another library isn't working as an iterator, study this example: - -```js -// Here is a simple object with an (unnecessarily roundabout) squaring method -var AsyncSquaringLibrary = { - squareExponent: 2, - square: function(number, callback){ - var result = Math.pow(number, this.squareExponent); - setTimeout(function(){ - callback(null, result); - }, 200); - } -}; - -async.map([1, 2, 3], AsyncSquaringLibrary.square, function(err, result){ - // result is [NaN, NaN, NaN] - // This fails because the `this.squareExponent` expression in the square - // function is not evaluated in the context of AsyncSquaringLibrary, and is - // therefore undefined. -}); - -async.map([1, 2, 3], AsyncSquaringLibrary.square.bind(AsyncSquaringLibrary), function(err, result){ - // result is [1, 4, 9] - // With the help of bind we can attach a context to the iterator before - // passing it to async. Now the square function will be executed in its - // 'home' AsyncSquaringLibrary context and the value of `this.squareExponent` - // will be as expected. -}); -``` - -## Download - -The source is available for download from -[GitHub](https://github.com/caolan/async/blob/master/lib/async.js). -Alternatively, you can install using Node Package Manager (`npm`): - - npm install async - -As well as using Bower: - - bower install async - -__Development:__ [async.js](https://github.com/caolan/async/raw/master/lib/async.js) - 29.6kb Uncompressed - -## In the Browser - -So far it's been tested in IE6, IE7, IE8, FF3.6 and Chrome 5. - -Usage: - -```html - - -``` - -## Documentation - -Some functions are also available in the following forms: -* `Series` - the same as `` but runs only a single async operation at a time -* `Limit` - the same as `` but runs a maximum of `limit` async operations at a time - -### Collections - -* [`each`](#each), `eachSeries`, `eachLimit` -* [`forEachOf`](#forEachOf), `forEachOfSeries`, `forEachOfLimit` -* [`map`](#map), `mapSeries`, `mapLimit` -* [`filter`](#filter), `filterSeries`, `filterLimit` -* [`reject`](#reject), `rejectSeries`, `rejectLimit` -* [`reduce`](#reduce), [`reduceRight`](#reduceRight) -* [`detect`](#detect), `detectSeries`, `detectLimit` -* [`sortBy`](#sortBy) -* [`some`](#some), `someLimit` -* [`every`](#every), `everyLimit` -* [`concat`](#concat), `concatSeries` - -### Control Flow - -* [`series`](#seriestasks-callback) -* [`parallel`](#parallel), `parallelLimit` -* [`whilst`](#whilst), [`doWhilst`](#doWhilst) -* [`until`](#until), [`doUntil`](#doUntil) -* [`during`](#during), [`doDuring`](#doDuring) -* [`forever`](#forever) -* [`waterfall`](#waterfall) -* [`compose`](#compose) -* [`seq`](#seq) -* [`applyEach`](#applyEach), `applyEachSeries` -* [`queue`](#queue), [`priorityQueue`](#priorityQueue) -* [`cargo`](#cargo) -* [`auto`](#auto) -* [`retry`](#retry) -* [`iterator`](#iterator) -* [`times`](#times), `timesSeries`, `timesLimit` - -### Utils - -* [`apply`](#apply) -* [`nextTick`](#nextTick) -* [`memoize`](#memoize) -* [`unmemoize`](#unmemoize) -* [`ensureAsync`](#ensureAsync) -* [`constant`](#constant) -* [`asyncify`](#asyncify) -* [`wrapSync`](#wrapSync) -* [`log`](#log) -* [`dir`](#dir) -* [`noConflict`](#noConflict) - -## Collections - - - -### each(arr, iterator, [callback]) - -Applies the function `iterator` to each item in `arr`, in parallel. -The `iterator` is called with an item from the list, and a callback for when it -has finished. If the `iterator` passes an error to its `callback`, the main -`callback` (for the `each` function) is immediately called with the error. - -Note, that since this function applies `iterator` to each item in parallel, -there is no guarantee that the iterator functions will complete in order. - -__Arguments__ - -* `arr` - An array to iterate over. -* `iterator(item, callback)` - A function to apply to each item in `arr`. - The iterator is passed a `callback(err)` which must be called once it has - completed. If no error has occurred, the `callback` should be run without - arguments or with an explicit `null` argument. The array index is not passed - to the iterator. If you need the index, use [`forEachOf`](#forEachOf). -* `callback(err)` - *Optional* A callback which is called when all `iterator` functions - have finished, or an error occurs. - -__Examples__ - - -```js -// assuming openFiles is an array of file names and saveFile is a function -// to save the modified contents of that file: - -async.each(openFiles, saveFile, function(err){ - // if any of the saves produced an error, err would equal that error -}); -``` - -```js -// assuming openFiles is an array of file names - -async.each(openFiles, function(file, callback) { - - // Perform operation on file here. - console.log('Processing file ' + file); - - if( file.length > 32 ) { - console.log('This file name is too long'); - callback('File name too long'); - } else { - // Do work to process file here - console.log('File processed'); - callback(); - } -}, function(err){ - // if any of the file processing produced an error, err would equal that error - if( err ) { - // One of the iterations produced an error. - // All processing will now stop. - console.log('A file failed to process'); - } else { - console.log('All files have been processed successfully'); - } -}); -``` - -__Related__ - -* eachSeries(arr, iterator, [callback]) -* eachLimit(arr, limit, iterator, [callback]) - ---------------------------------------- - - - - -### forEachOf(obj, iterator, [callback]) - -Like `each`, except that it iterates over objects, and passes the key as the second argument to the iterator. - -__Arguments__ - -* `obj` - An object or array to iterate over. -* `iterator(item, key, callback)` - A function to apply to each item in `obj`. -The `key` is the item's key, or index in the case of an array. The iterator is -passed a `callback(err)` which must be called once it has completed. If no -error has occurred, the callback should be run without arguments or with an -explicit `null` argument. -* `callback(err)` - *Optional* A callback which is called when all `iterator` functions have finished, or an error occurs. - -__Example__ - -```js -var obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"}; -var configs = {}; - -async.forEachOf(obj, function (value, key, callback) { - fs.readFile(__dirname + value, "utf8", function (err, data) { - if (err) return callback(err); - try { - configs[key] = JSON.parse(data); - } catch (e) { - return callback(e); - } - callback(); - }) -}, function (err) { - if (err) console.error(err.message); - // configs is now a map of JSON data - doSomethingWith(configs); -}) -``` - -__Related__ - -* forEachOfSeries(obj, iterator, [callback]) -* forEachOfLimit(obj, limit, iterator, [callback]) - ---------------------------------------- - - -### map(arr, iterator, [callback]) - -Produces a new array of values by mapping each value in `arr` through -the `iterator` function. The `iterator` is called with an item from `arr` and a -callback for when it has finished processing. Each of these callback takes 2 arguments: -an `error`, and the transformed item from `arr`. If `iterator` passes an error to its -callback, the main `callback` (for the `map` function) is immediately called with the error. - -Note, that since this function applies the `iterator` to each item in parallel, -there is no guarantee that the `iterator` functions will complete in order. -However, the results array will be in the same order as the original `arr`. - -__Arguments__ - -* `arr` - An array to iterate over. -* `iterator(item, callback)` - A function to apply to each item in `arr`. - The iterator is passed a `callback(err, transformed)` which must be called once - it has completed with an error (which can be `null`) and a transformed item. -* `callback(err, results)` - *Optional* A callback which is called when all `iterator` - functions have finished, or an error occurs. Results is an array of the - transformed items from the `arr`. - -__Example__ - -```js -async.map(['file1','file2','file3'], fs.stat, function(err, results){ - // results is now an array of stats for each file -}); -``` - -__Related__ -* mapSeries(arr, iterator, [callback]) -* mapLimit(arr, limit, iterator, [callback]) - ---------------------------------------- - - - -### filter(arr, iterator, [callback]) - -__Alias:__ `select` - -Returns a new array of all the values in `arr` which pass an async truth test. -_The callback for each `iterator` call only accepts a single argument of `true` or -`false`; it does not accept an error argument first!_ This is in-line with the -way node libraries work with truth tests like `fs.exists`. This operation is -performed in parallel, but the results array will be in the same order as the -original. - -__Arguments__ - -* `arr` - An array to iterate over. -* `iterator(item, callback)` - A truth test to apply to each item in `arr`. - The `iterator` is passed a `callback(truthValue)`, which must be called with a - boolean argument once it has completed. -* `callback(results)` - *Optional* A callback which is called after all the `iterator` - functions have finished. - -__Example__ - -```js -async.filter(['file1','file2','file3'], fs.exists, function(results){ - // results now equals an array of the existing files -}); -``` - -__Related__ - -* filterSeries(arr, iterator, [callback]) -* filterLimit(arr, limit, iterator, [callback]) - ---------------------------------------- - - -### reject(arr, iterator, [callback]) - -The opposite of [`filter`](#filter). Removes values that pass an `async` truth test. - -__Related__ - -* rejectSeries(arr, iterator, [callback]) -* rejectLimit(arr, limit, iterator, [callback]) - ---------------------------------------- - - -### reduce(arr, memo, iterator, [callback]) - -__Aliases:__ `inject`, `foldl` - -Reduces `arr` into a single value using an async `iterator` to return -each successive step. `memo` is the initial state of the reduction. -This function only operates in series. - -For performance reasons, it may make sense to split a call to this function into -a parallel map, and then use the normal `Array.prototype.reduce` on the results. -This function is for situations where each step in the reduction needs to be async; -if you can get the data before reducing it, then it's probably a good idea to do so. - -__Arguments__ - -* `arr` - An array to iterate over. -* `memo` - The initial state of the reduction. -* `iterator(memo, item, callback)` - A function applied to each item in the - array to produce the next step in the reduction. The `iterator` is passed a - `callback(err, reduction)` which accepts an optional error as its first - argument, and the state of the reduction as the second. If an error is - passed to the callback, the reduction is stopped and the main `callback` is - immediately called with the error. -* `callback(err, result)` - *Optional* A callback which is called after all the `iterator` - functions have finished. Result is the reduced value. - -__Example__ - -```js -async.reduce([1,2,3], 0, function(memo, item, callback){ - // pointless async: - process.nextTick(function(){ - callback(null, memo + item) - }); -}, function(err, result){ - // result is now equal to the last value of memo, which is 6 -}); -``` - ---------------------------------------- - - -### reduceRight(arr, memo, iterator, [callback]) - -__Alias:__ `foldr` - -Same as [`reduce`](#reduce), only operates on `arr` in reverse order. - - ---------------------------------------- - - -### detect(arr, iterator, [callback]) - -Returns the first value in `arr` that passes an async truth test. The -`iterator` is applied in parallel, meaning the first iterator to return `true` will -fire the detect `callback` with that result. That means the result might not be -the first item in the original `arr` (in terms of order) that passes the test. - -If order within the original `arr` is important, then look at [`detectSeries`](#detectSeries). - -__Arguments__ - -* `arr` - An array to iterate over. -* `iterator(item, callback)` - A truth test to apply to each item in `arr`. - The iterator is passed a `callback(truthValue)` which must be called with a - boolean argument once it has completed. **Note: this callback does not take an error as its first argument.** -* `callback(result)` - *Optional* A callback which is called as soon as any iterator returns - `true`, or after all the `iterator` functions have finished. Result will be - the first item in the array that passes the truth test (iterator) or the - value `undefined` if none passed. **Note: this callback does not take an error as its first argument.** - -__Example__ - -```js -async.detect(['file1','file2','file3'], fs.exists, function(result){ - // result now equals the first file in the list that exists -}); -``` - -__Related__ - -* detectSeries(arr, iterator, [callback]) -* detectLimit(arr, limit, iterator, [callback]) - ---------------------------------------- - - -### sortBy(arr, iterator, [callback]) - -Sorts a list by the results of running each `arr` value through an async `iterator`. - -__Arguments__ - -* `arr` - An array to iterate over. -* `iterator(item, callback)` - A function to apply to each item in `arr`. - The iterator is passed a `callback(err, sortValue)` which must be called once it - has completed with an error (which can be `null`) and a value to use as the sort - criteria. -* `callback(err, results)` - *Optional* A callback which is called after all the `iterator` - functions have finished, or an error occurs. Results is the items from - the original `arr` sorted by the values returned by the `iterator` calls. - -__Example__ - -```js -async.sortBy(['file1','file2','file3'], function(file, callback){ - fs.stat(file, function(err, stats){ - callback(err, stats.mtime); - }); -}, function(err, results){ - // results is now the original array of files sorted by - // modified date -}); -``` - -__Sort Order__ - -By modifying the callback parameter the sorting order can be influenced: - -```js -//ascending order -async.sortBy([1,9,3,5], function(x, callback){ - callback(null, x); -}, function(err,result){ - //result callback -} ); - -//descending order -async.sortBy([1,9,3,5], function(x, callback){ - callback(null, x*-1); //<- x*-1 instead of x, turns the order around -}, function(err,result){ - //result callback -} ); -``` - ---------------------------------------- - - -### some(arr, iterator, [callback]) - -__Alias:__ `any` - -Returns `true` if at least one element in the `arr` satisfies an async test. -_The callback for each iterator call only accepts a single argument of `true` or -`false`; it does not accept an error argument first!_ This is in-line with the -way node libraries work with truth tests like `fs.exists`. Once any iterator -call returns `true`, the main `callback` is immediately called. - -__Arguments__ - -* `arr` - An array to iterate over. -* `iterator(item, callback)` - A truth test to apply to each item in the array - in parallel. The iterator is passed a `callback(truthValue)`` which must be - called with a boolean argument once it has completed. -* `callback(result)` - *Optional* A callback which is called as soon as any iterator returns - `true`, or after all the iterator functions have finished. Result will be - either `true` or `false` depending on the values of the async tests. - - **Note: the callbacks do not take an error as their first argument.** -__Example__ - -```js -async.some(['file1','file2','file3'], fs.exists, function(result){ - // if result is true then at least one of the files exists -}); -``` - -__Related__ - -* someLimit(arr, limit, iterator, callback) - ---------------------------------------- - - -### every(arr, iterator, [callback]) - -__Alias:__ `all` - -Returns `true` if every element in `arr` satisfies an async test. -_The callback for each `iterator` call only accepts a single argument of `true` or -`false`; it does not accept an error argument first!_ This is in-line with the -way node libraries work with truth tests like `fs.exists`. - -__Arguments__ - -* `arr` - An array to iterate over. -* `iterator(item, callback)` - A truth test to apply to each item in the array - in parallel. The iterator is passed a `callback(truthValue)` which must be - called with a boolean argument once it has completed. -* `callback(result)` - *Optional* A callback which is called as soon as any iterator returns - `false`, or after all the iterator functions have finished. Result will be - either `true` or `false` depending on the values of the async tests. - - **Note: the callbacks do not take an error as their first argument.** - -__Example__ - -```js -async.every(['file1','file2','file3'], fs.exists, function(result){ - // if result is true then every file exists -}); -``` - -__Related__ - -* everyLimit(arr, limit, iterator, callback) - ---------------------------------------- - - -### concat(arr, iterator, [callback]) - -Applies `iterator` to each item in `arr`, concatenating the results. Returns the -concatenated list. The `iterator`s are called in parallel, and the results are -concatenated as they return. There is no guarantee that the results array will -be returned in the original order of `arr` passed to the `iterator` function. - -__Arguments__ - -* `arr` - An array to iterate over. -* `iterator(item, callback)` - A function to apply to each item in `arr`. - The iterator is passed a `callback(err, results)` which must be called once it - has completed with an error (which can be `null`) and an array of results. -* `callback(err, results)` - *Optional* A callback which is called after all the `iterator` - functions have finished, or an error occurs. Results is an array containing - the concatenated results of the `iterator` function. - -__Example__ - -```js -async.concat(['dir1','dir2','dir3'], fs.readdir, function(err, files){ - // files is now a list of filenames that exist in the 3 directories -}); -``` - -__Related__ - -* concatSeries(arr, iterator, [callback]) - - -## Control Flow - - -### series(tasks, [callback]) - -Run the functions in the `tasks` array in series, each one running once the previous -function has completed. If any functions in the series pass an error to its -callback, no more functions are run, and `callback` is immediately called with the value of the error. -Otherwise, `callback` receives an array of results when `tasks` have completed. - -It is also possible to use an object instead of an array. Each property will be -run as a function, and the results will be passed to the final `callback` as an object -instead of an array. This can be a more readable way of handling results from -[`series`](#series). - -**Note** that while many implementations preserve the order of object properties, the -[ECMAScript Language Specification](http://www.ecma-international.org/ecma-262/5.1/#sec-8.6) -explicitly states that - -> The mechanics and order of enumerating the properties is not specified. - -So if you rely on the order in which your series of functions are executed, and want -this to work on all platforms, consider using an array. - -__Arguments__ - -* `tasks` - An array or object containing functions to run, each function is passed - a `callback(err, result)` it must call on completion with an error `err` (which can - be `null`) and an optional `result` value. -* `callback(err, results)` - An optional callback to run once all the functions - have completed. This function gets a results array (or object) containing all - the result arguments passed to the `task` callbacks. - -__Example__ - -```js -async.series([ - function(callback){ - // do some stuff ... - callback(null, 'one'); - }, - function(callback){ - // do some more stuff ... - callback(null, 'two'); - } -], -// optional callback -function(err, results){ - // results is now equal to ['one', 'two'] -}); - - -// an example using an object instead of an array -async.series({ - one: function(callback){ - setTimeout(function(){ - callback(null, 1); - }, 200); - }, - two: function(callback){ - setTimeout(function(){ - callback(null, 2); - }, 100); - } -}, -function(err, results) { - // results is now equal to: {one: 1, two: 2} -}); -``` - ---------------------------------------- - - -### parallel(tasks, [callback]) - -Run the `tasks` array of functions in parallel, without waiting until the previous -function has completed. If any of the functions pass an error to its -callback, the main `callback` is immediately called with the value of the error. -Once the `tasks` have completed, the results are passed to the final `callback` as an -array. - -**Note:** `parallel` is about kicking-off I/O tasks in parallel, not about parallel execution of code. If your tasks do not use any timers or perform any I/O, they will actually be executed in series. Any synchronous setup sections for each task will happen one after the other. JavaScript remains single-threaded. - -It is also possible to use an object instead of an array. Each property will be -run as a function and the results will be passed to the final `callback` as an object -instead of an array. This can be a more readable way of handling results from -[`parallel`](#parallel). - - -__Arguments__ - -* `tasks` - An array or object containing functions to run. Each function is passed - a `callback(err, result)` which it must call on completion with an error `err` - (which can be `null`) and an optional `result` value. -* `callback(err, results)` - An optional callback to run once all the functions - have completed successfully. This function gets a results array (or object) containing all - the result arguments passed to the task callbacks. - -__Example__ - -```js -async.parallel([ - function(callback){ - setTimeout(function(){ - callback(null, 'one'); - }, 200); - }, - function(callback){ - setTimeout(function(){ - callback(null, 'two'); - }, 100); - } -], -// optional callback -function(err, results){ - // the results array will equal ['one','two'] even though - // the second function had a shorter timeout. -}); - - -// an example using an object instead of an array -async.parallel({ - one: function(callback){ - setTimeout(function(){ - callback(null, 1); - }, 200); - }, - two: function(callback){ - setTimeout(function(){ - callback(null, 2); - }, 100); - } -}, -function(err, results) { - // results is now equals to: {one: 1, two: 2} -}); -``` - -__Related__ - -* parallelLimit(tasks, limit, [callback]) - ---------------------------------------- - - -### whilst(test, fn, callback) - -Repeatedly call `fn`, while `test` returns `true`. Calls `callback` when stopped, -or an error occurs. - -__Arguments__ - -* `test()` - synchronous truth test to perform before each execution of `fn`. -* `fn(callback)` - A function which is called each time `test` passes. The function is - passed a `callback(err)`, which must be called once it has completed with an - optional `err` argument. -* `callback(err, [results])` - A callback which is called after the test - function has failed and repeated execution of `fn` has stopped. `callback` - will be passed an error and any arguments passed to the final `fn`'s callback. - -__Example__ - -```js -var count = 0; - -async.whilst( - function () { return count < 5; }, - function (callback) { - count++; - setTimeout(function () { - callback(null, count); - }, 1000); - }, - function (err, n) { - // 5 seconds have passed, n = 5 - } -); -``` - ---------------------------------------- - - -### doWhilst(fn, test, callback) - -The post-check version of [`whilst`](#whilst). To reflect the difference in -the order of operations, the arguments `test` and `fn` are switched. - -`doWhilst` is to `whilst` as `do while` is to `while` in plain JavaScript. - ---------------------------------------- - - -### until(test, fn, callback) - -Repeatedly call `fn` until `test` returns `true`. Calls `callback` when stopped, -or an error occurs. `callback` will be passed an error and any arguments passed -to the final `fn`'s callback. - -The inverse of [`whilst`](#whilst). - ---------------------------------------- - - -### doUntil(fn, test, callback) - -Like [`doWhilst`](#doWhilst), except the `test` is inverted. Note the argument ordering differs from `until`. - ---------------------------------------- - - -### during(test, fn, callback) - -Like [`whilst`](#whilst), except the `test` is an asynchronous function that is passed a callback in the form of `function (err, truth)`. If error is passed to `test` or `fn`, the main callback is immediately called with the value of the error. - -__Example__ - -```js -var count = 0; - -async.during( - function (callback) { - return callback(null, count < 5); - }, - function (callback) { - count++; - setTimeout(callback, 1000); - }, - function (err) { - // 5 seconds have passed - } -); -``` - ---------------------------------------- - - -### doDuring(fn, test, callback) - -The post-check version of [`during`](#during). To reflect the difference in -the order of operations, the arguments `test` and `fn` are switched. - -Also a version of [`doWhilst`](#doWhilst) with asynchronous `test` function. - ---------------------------------------- - - -### forever(fn, [errback]) - -Calls the asynchronous function `fn` with a callback parameter that allows it to -call itself again, in series, indefinitely. - -If an error is passed to the callback then `errback` is called with the -error, and execution stops, otherwise it will never be called. - -```js -async.forever( - function(next) { - // next is suitable for passing to things that need a callback(err [, whatever]); - // it will result in this function being called again. - }, - function(err) { - // if next is called with a value in its first parameter, it will appear - // in here as 'err', and execution will stop. - } -); -``` - ---------------------------------------- - - -### waterfall(tasks, [callback]) - -Runs the `tasks` array of functions in series, each passing their results to the next in -the array. However, if any of the `tasks` pass an error to their own callback, the -next function is not executed, and the main `callback` is immediately called with -the error. - -__Arguments__ - -* `tasks` - An array of functions to run, each function is passed a - `callback(err, result1, result2, ...)` it must call on completion. The first - argument is an error (which can be `null`) and any further arguments will be - passed as arguments in order to the next task. -* `callback(err, [results])` - An optional callback to run once all the functions - have completed. This will be passed the results of the last task's callback. - - - -__Example__ - -```js -async.waterfall([ - function(callback) { - callback(null, 'one', 'two'); - }, - function(arg1, arg2, callback) { - // arg1 now equals 'one' and arg2 now equals 'two' - callback(null, 'three'); - }, - function(arg1, callback) { - // arg1 now equals 'three' - callback(null, 'done'); - } -], function (err, result) { - // result now equals 'done' -}); -``` -Or, with named functions: - -```js -async.waterfall([ - myFirstFunction, - mySecondFunction, - myLastFunction, -], function (err, result) { - // result now equals 'done' -}); -function myFirstFunction(callback) { - callback(null, 'one', 'two'); -} -function mySecondFunction(arg1, arg2, callback) { - // arg1 now equals 'one' and arg2 now equals 'two' - callback(null, 'three'); -} -function myLastFunction(arg1, callback) { - // arg1 now equals 'three' - callback(null, 'done'); -} -``` - -Or, if you need to pass any argument to the first function: - -```js -async.waterfall([ - async.apply(myFirstFunction, 'zero'), - mySecondFunction, - myLastFunction, -], function (err, result) { - // result now equals 'done' -}); -function myFirstFunction(arg1, callback) { - // arg1 now equals 'zero' - callback(null, 'one', 'two'); -} -function mySecondFunction(arg1, arg2, callback) { - // arg1 now equals 'one' and arg2 now equals 'two' - callback(null, 'three'); -} -function myLastFunction(arg1, callback) { - // arg1 now equals 'three' - callback(null, 'done'); -} -``` - ---------------------------------------- - -### compose(fn1, fn2...) - -Creates a function which is a composition of the passed asynchronous -functions. Each function consumes the return value of the function that -follows. Composing functions `f()`, `g()`, and `h()` would produce the result of -`f(g(h()))`, only this version uses callbacks to obtain the return values. - -Each function is executed with the `this` binding of the composed function. - -__Arguments__ - -* `functions...` - the asynchronous functions to compose - - -__Example__ - -```js -function add1(n, callback) { - setTimeout(function () { - callback(null, n + 1); - }, 10); -} - -function mul3(n, callback) { - setTimeout(function () { - callback(null, n * 3); - }, 10); -} - -var add1mul3 = async.compose(mul3, add1); - -add1mul3(4, function (err, result) { - // result now equals 15 -}); -``` - ---------------------------------------- - -### seq(fn1, fn2...) - -Version of the compose function that is more natural to read. -Each function consumes the return value of the previous function. -It is the equivalent of [`compose`](#compose) with the arguments reversed. - -Each function is executed with the `this` binding of the composed function. - -__Arguments__ - -* `functions...` - the asynchronous functions to compose - - -__Example__ - -```js -// Requires lodash (or underscore), express3 and dresende's orm2. -// Part of an app, that fetches cats of the logged user. -// This example uses `seq` function to avoid overnesting and error -// handling clutter. -app.get('/cats', function(request, response) { - var User = request.models.User; - async.seq( - _.bind(User.get, User), // 'User.get' has signature (id, callback(err, data)) - function(user, fn) { - user.getCats(fn); // 'getCats' has signature (callback(err, data)) - } - )(req.session.user_id, function (err, cats) { - if (err) { - console.error(err); - response.json({ status: 'error', message: err.message }); - } else { - response.json({ status: 'ok', message: 'Cats found', data: cats }); - } - }); -}); -``` - ---------------------------------------- - -### applyEach(fns, args..., callback) - -Applies the provided arguments to each function in the array, calling -`callback` after all functions have completed. If you only provide the first -argument, then it will return a function which lets you pass in the -arguments as if it were a single function call. - -__Arguments__ - -* `fns` - the asynchronous functions to all call with the same arguments -* `args...` - any number of separate arguments to pass to the function -* `callback` - the final argument should be the callback, called when all - functions have completed processing - - -__Example__ - -```js -async.applyEach([enableSearch, updateSchema], 'bucket', callback); - -// partial application example: -async.each( - buckets, - async.applyEach([enableSearch, updateSchema]), - callback -); -``` - -__Related__ - -* applyEachSeries(tasks, args..., [callback]) - ---------------------------------------- - - -### queue(worker, [concurrency]) - -Creates a `queue` object with the specified `concurrency`. Tasks added to the -`queue` are processed in parallel (up to the `concurrency` limit). If all -`worker`s are in progress, the task is queued until one becomes available. -Once a `worker` completes a `task`, that `task`'s callback is called. - -__Arguments__ - -* `worker(task, callback)` - An asynchronous function for processing a queued - task, which must call its `callback(err)` argument when finished, with an - optional `error` as an argument. If you want to handle errors from an individual task, pass a callback to `q.push()`. -* `concurrency` - An `integer` for determining how many `worker` functions should be - run in parallel. If omitted, the concurrency defaults to `1`. If the concurrency is `0`, an error is thrown. - -__Queue objects__ - -The `queue` object returned by this function has the following properties and -methods: - -* `length()` - a function returning the number of items waiting to be processed. -* `started` - a function returning whether or not any items have been pushed and processed by the queue -* `running()` - a function returning the number of items currently being processed. -* `workersList()` - a function returning the array of items currently being processed. -* `idle()` - a function returning false if there are items waiting or being processed, or true if not. -* `concurrency` - an integer for determining how many `worker` functions should be - run in parallel. This property can be changed after a `queue` is created to - alter the concurrency on-the-fly. -* `push(task, [callback])` - add a new task to the `queue`. Calls `callback` once - the `worker` has finished processing the task. Instead of a single task, a `tasks` array - can be submitted. The respective callback is used for every task in the list. -* `unshift(task, [callback])` - add a new task to the front of the `queue`. -* `saturated` - a callback that is called when the `queue` length hits the `concurrency` limit, - and further tasks will be queued. -* `empty` - a callback that is called when the last item from the `queue` is given to a `worker`. -* `drain` - a callback that is called when the last item from the `queue` has returned from the `worker`. -* `paused` - a boolean for determining whether the queue is in a paused state -* `pause()` - a function that pauses the processing of tasks until `resume()` is called. -* `resume()` - a function that resumes the processing of queued tasks when the queue is paused. -* `kill()` - a function that removes the `drain` callback and empties remaining tasks from the queue forcing it to go idle. - -__Example__ - -```js -// create a queue object with concurrency 2 - -var q = async.queue(function (task, callback) { - console.log('hello ' + task.name); - callback(); -}, 2); - - -// assign a callback -q.drain = function() { - console.log('all items have been processed'); -} - -// add some items to the queue - -q.push({name: 'foo'}, function (err) { - console.log('finished processing foo'); -}); -q.push({name: 'bar'}, function (err) { - console.log('finished processing bar'); -}); - -// add some items to the queue (batch-wise) - -q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], function (err) { - console.log('finished processing item'); -}); - -// add some items to the front of the queue - -q.unshift({name: 'bar'}, function (err) { - console.log('finished processing bar'); -}); -``` - - ---------------------------------------- - - -### priorityQueue(worker, concurrency) - -The same as [`queue`](#queue) only tasks are assigned a priority and completed in ascending priority order. There are two differences between `queue` and `priorityQueue` objects: - -* `push(task, priority, [callback])` - `priority` should be a number. If an array of - `tasks` is given, all tasks will be assigned the same priority. -* The `unshift` method was removed. - ---------------------------------------- - - -### cargo(worker, [payload]) - -Creates a `cargo` object with the specified payload. Tasks added to the -cargo will be processed altogether (up to the `payload` limit). If the -`worker` is in progress, the task is queued until it becomes available. Once -the `worker` has completed some tasks, each callback of those tasks is called. -Check out [these](https://camo.githubusercontent.com/6bbd36f4cf5b35a0f11a96dcd2e97711ffc2fb37/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130382f62626330636662302d356632392d313165322d393734662d3333393763363464633835382e676966) [animations](https://camo.githubusercontent.com/f4810e00e1c5f5f8addbe3e9f49064fd5d102699/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f313637363837312f36383130312f38346339323036362d356632392d313165322d383134662d3964336430323431336266642e676966) for how `cargo` and `queue` work. - -While [queue](#queue) passes only one task to one of a group of workers -at a time, cargo passes an array of tasks to a single worker, repeating -when the worker is finished. - -__Arguments__ - -* `worker(tasks, callback)` - An asynchronous function for processing an array of - queued tasks, which must call its `callback(err)` argument when finished, with - an optional `err` argument. -* `payload` - An optional `integer` for determining how many tasks should be - processed per round; if omitted, the default is unlimited. - -__Cargo objects__ - -The `cargo` object returned by this function has the following properties and -methods: - -* `length()` - A function returning the number of items waiting to be processed. -* `payload` - An `integer` for determining how many tasks should be - process per round. This property can be changed after a `cargo` is created to - alter the payload on-the-fly. -* `push(task, [callback])` - Adds `task` to the `queue`. The callback is called - once the `worker` has finished processing the task. Instead of a single task, an array of `tasks` - can be submitted. The respective callback is used for every task in the list. -* `saturated` - A callback that is called when the `queue.length()` hits the concurrency and further tasks will be queued. -* `empty` - A callback that is called when the last item from the `queue` is given to a `worker`. -* `drain` - A callback that is called when the last item from the `queue` has returned from the `worker`. -* `idle()`, `pause()`, `resume()`, `kill()` - cargo inherits all of the same methods and event calbacks as [`queue`](#queue) - -__Example__ - -```js -// create a cargo object with payload 2 - -var cargo = async.cargo(function (tasks, callback) { - for(var i=0; i -### auto(tasks, [concurrency], [callback]) - -Determines the best order for running the functions in `tasks`, based on their requirements. Each function can optionally depend on other functions being completed first, and each function is run as soon as its requirements are satisfied. - -If any of the functions pass an error to their callback, the `auto` sequence will stop. Further tasks will not execute (so any other functions depending on it will not run), and the main `callback` is immediately called with the error. Functions also receive an object containing the results of functions which have completed so far. - -Note, all functions are called with a `results` object as a second argument, -so it is unsafe to pass functions in the `tasks` object which cannot handle the -extra argument. - -For example, this snippet of code: - -```js -async.auto({ - readData: async.apply(fs.readFile, 'data.txt', 'utf-8') -}, callback); -``` - -will have the effect of calling `readFile` with the results object as the last -argument, which will fail: - -```js -fs.readFile('data.txt', 'utf-8', cb, {}); -``` - -Instead, wrap the call to `readFile` in a function which does not forward the -`results` object: - -```js -async.auto({ - readData: function(cb, results){ - fs.readFile('data.txt', 'utf-8', cb); - } -}, callback); -``` - -__Arguments__ - -* `tasks` - An object. Each of its properties is either a function or an array of - requirements, with the function itself the last item in the array. The object's key - of a property serves as the name of the task defined by that property, - i.e. can be used when specifying requirements for other tasks. - The function receives two arguments: (1) a `callback(err, result)` which must be - called when finished, passing an `error` (which can be `null`) and the result of - the function's execution, and (2) a `results` object, containing the results of - the previously executed functions. -* `concurrency` - An optional `integer` for determining the maximum number of tasks that can be run in parallel. By default, as many as possible. -* `callback(err, results)` - An optional callback which is called when all the - tasks have been completed. It receives the `err` argument if any `tasks` - pass an error to their callback. Results are always returned; however, if - an error occurs, no further `tasks` will be performed, and the results - object will only contain partial results. - - -__Example__ - -```js -async.auto({ - get_data: function(callback){ - console.log('in get_data'); - // async code to get some data - callback(null, 'data', 'converted to array'); - }, - make_folder: function(callback){ - console.log('in make_folder'); - // async code to create a directory to store a file in - // this is run at the same time as getting the data - callback(null, 'folder'); - }, - write_file: ['get_data', 'make_folder', function(callback, results){ - console.log('in write_file', JSON.stringify(results)); - // once there is some data and the directory exists, - // write the data to a file in the directory - callback(null, 'filename'); - }], - email_link: ['write_file', function(callback, results){ - console.log('in email_link', JSON.stringify(results)); - // once the file is written let's email a link to it... - // results.write_file contains the filename returned by write_file. - callback(null, {'file':results.write_file, 'email':'user@example.com'}); - }] -}, function(err, results) { - console.log('err = ', err); - console.log('results = ', results); -}); -``` - -This is a fairly trivial example, but to do this using the basic parallel and -series functions would look like this: - -```js -async.parallel([ - function(callback){ - console.log('in get_data'); - // async code to get some data - callback(null, 'data', 'converted to array'); - }, - function(callback){ - console.log('in make_folder'); - // async code to create a directory to store a file in - // this is run at the same time as getting the data - callback(null, 'folder'); - } -], -function(err, results){ - async.series([ - function(callback){ - console.log('in write_file', JSON.stringify(results)); - // once there is some data and the directory exists, - // write the data to a file in the directory - results.push('filename'); - callback(null); - }, - function(callback){ - console.log('in email_link', JSON.stringify(results)); - // once the file is written let's email a link to it... - callback(null, {'file':results.pop(), 'email':'user@example.com'}); - } - ]); -}); -``` - -For a complicated series of `async` tasks, using the [`auto`](#auto) function makes adding -new tasks much easier (and the code more readable). - - ---------------------------------------- - - -### retry([opts = {times: 5, interval: 0}| 5], task, [callback]) - -Attempts to get a successful response from `task` no more than `times` times before -returning an error. If the task is successful, the `callback` will be passed the result -of the successful task. If all attempts fail, the callback will be passed the error and -result (if any) of the final attempt. - -__Arguments__ - -* `opts` - Can be either an object with `times` and `interval` or a number. - * `times` - The number of attempts to make before giving up. The default is `5`. - * `interval` - The time to wait between retries, in milliseconds. The default is `0`. - * If `opts` is a number, the number specifies the number of times to retry, with the default interval of `0`. -* `task(callback, results)` - A function which receives two arguments: (1) a `callback(err, result)` - which must be called when finished, passing `err` (which can be `null`) and the `result` of - the function's execution, and (2) a `results` object, containing the results of - the previously executed functions (if nested inside another control flow). -* `callback(err, results)` - An optional callback which is called when the - task has succeeded, or after the final failed attempt. It receives the `err` and `result` arguments of the last attempt at completing the `task`. - -The [`retry`](#retry) function can be used as a stand-alone control flow by passing a callback, as shown below: - -```js -// try calling apiMethod 3 times -async.retry(3, apiMethod, function(err, result) { - // do something with the result -}); -``` - -```js -// try calling apiMethod 3 times, waiting 200 ms between each retry -async.retry({times: 3, interval: 200}, apiMethod, function(err, result) { - // do something with the result -}); -``` - -```js -// try calling apiMethod the default 5 times no delay between each retry -async.retry(apiMethod, function(err, result) { - // do something with the result -}); -``` - -It can also be embedded within other control flow functions to retry individual methods -that are not as reliable, like this: - -```js -async.auto({ - users: api.getUsers.bind(api), - payments: async.retry(3, api.getPayments.bind(api)) -}, function(err, results) { - // do something with the results -}); -``` - - ---------------------------------------- - - -### iterator(tasks) - -Creates an iterator function which calls the next function in the `tasks` array, -returning a continuation to call the next one after that. It's also possible to -“peek” at the next iterator with `iterator.next()`. - -This function is used internally by the `async` module, but can be useful when -you want to manually control the flow of functions in series. - -__Arguments__ - -* `tasks` - An array of functions to run. - -__Example__ - -```js -var iterator = async.iterator([ - function(){ sys.p('one'); }, - function(){ sys.p('two'); }, - function(){ sys.p('three'); } -]); - -node> var iterator2 = iterator(); -'one' -node> var iterator3 = iterator2(); -'two' -node> iterator3(); -'three' -node> var nextfn = iterator2.next(); -node> nextfn(); -'three' -``` - ---------------------------------------- - - -### apply(function, arguments..) - -Creates a continuation function with some arguments already applied. - -Useful as a shorthand when combined with other control flow functions. Any arguments -passed to the returned function are added to the arguments originally passed -to apply. - -__Arguments__ - -* `function` - The function you want to eventually apply all arguments to. -* `arguments...` - Any number of arguments to automatically apply when the - continuation is called. - -__Example__ - -```js -// using apply - -async.parallel([ - async.apply(fs.writeFile, 'testfile1', 'test1'), - async.apply(fs.writeFile, 'testfile2', 'test2'), -]); - - -// the same process without using apply - -async.parallel([ - function(callback){ - fs.writeFile('testfile1', 'test1', callback); - }, - function(callback){ - fs.writeFile('testfile2', 'test2', callback); - } -]); -``` - -It's possible to pass any number of additional arguments when calling the -continuation: - -```js -node> var fn = async.apply(sys.puts, 'one'); -node> fn('two', 'three'); -one -two -three -``` - ---------------------------------------- - - -### nextTick(callback), setImmediate(callback) - -Calls `callback` on a later loop around the event loop. In Node.js this just -calls `process.nextTick`; in the browser it falls back to `setImmediate(callback)` -if available, otherwise `setTimeout(callback, 0)`, which means other higher priority -events may precede the execution of `callback`. - -This is used internally for browser-compatibility purposes. - -__Arguments__ - -* `callback` - The function to call on a later loop around the event loop. - -__Example__ - -```js -var call_order = []; -async.nextTick(function(){ - call_order.push('two'); - // call_order now equals ['one','two'] -}); -call_order.push('one') -``` - - -### times(n, iterator, [callback]) - -Calls the `iterator` function `n` times, and accumulates results in the same manner -you would use with [`map`](#map). - -__Arguments__ - -* `n` - The number of times to run the function. -* `iterator` - The function to call `n` times. -* `callback` - see [`map`](#map) - -__Example__ - -```js -// Pretend this is some complicated async factory -var createUser = function(id, callback) { - callback(null, { - id: 'user' + id - }) -} -// generate 5 users -async.times(5, function(n, next){ - createUser(n, function(err, user) { - next(err, user) - }) -}, function(err, users) { - // we should now have 5 users -}); -``` - -__Related__ - -* timesSeries(n, iterator, [callback]) -* timesLimit(n, limit, iterator, [callback]) - - -## Utils - - -### memoize(fn, [hasher]) - -Caches the results of an `async` function. When creating a hash to store function -results against, the callback is omitted from the hash and an optional hash -function can be used. - -If no hash function is specified, the first argument is used as a hash key, which may work reasonably if it is a string or a data type that converts to a distinct string. Note that objects and arrays will not behave reasonably. Neither will cases where the other arguments are significant. In such cases, specify your own hash function. - -The cache of results is exposed as the `memo` property of the function returned -by `memoize`. - -__Arguments__ - -* `fn` - The function to proxy and cache results from. -* `hasher` - An optional function for generating a custom hash for storing - results. It has all the arguments applied to it apart from the callback, and - must be synchronous. - -__Example__ - -```js -var slow_fn = function (name, callback) { - // do something - callback(null, result); -}; -var fn = async.memoize(slow_fn); - -// fn can now be used as if it were slow_fn -fn('some name', function () { - // callback -}); -``` - - -### unmemoize(fn) - -Undoes a [`memoize`](#memoize)d function, reverting it to the original, unmemoized -form. Handy for testing. - -__Arguments__ - -* `fn` - the memoized function - ---------------------------------------- - - -### ensureAsync(fn) - -Wrap an async function and ensure it calls its callback on a later tick of the event loop. If the function already calls its callback on a next tick, no extra deferral is added. This is useful for preventing stack overflows (`RangeError: Maximum call stack size exceeded`) and generally keeping [Zalgo](http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony) contained. - -__Arguments__ - -* `fn` - an async function, one that expects a node-style callback as its last argument - -Returns a wrapped function with the exact same call signature as the function passed in. - -__Example__ - -```js -function sometimesAsync(arg, callback) { - if (cache[arg]) { - return callback(null, cache[arg]); // this would be synchronous!! - } else { - doSomeIO(arg, callback); // this IO would be asynchronous - } -} - -// this has a risk of stack overflows if many results are cached in a row -async.mapSeries(args, sometimesAsync, done); - -// this will defer sometimesAsync's callback if necessary, -// preventing stack overflows -async.mapSeries(args, async.ensureAsync(sometimesAsync), done); - -``` - ---------------------------------------- - - -### constant(values...) - -Returns a function that when called, calls-back with the values provided. Useful as the first function in a `waterfall`, or for plugging values in to `auto`. - -__Example__ - -```js -async.waterfall([ - async.constant(42), - function (value, next) { - // value === 42 - }, - //... -], callback); - -async.waterfall([ - async.constant(filename, "utf8"), - fs.readFile, - function (fileData, next) { - //... - } - //... -], callback); - -async.auto({ - hostname: async.constant("https://server.net/"), - port: findFreePort, - launchServer: ["hostname", "port", function (cb, options) { - startServer(options, cb); - }], - //... -}, callback); - -``` - ---------------------------------------- - - - -### asyncify(func) - -__Alias:__ `wrapSync` - -Take a sync function and make it async, passing its return value to a callback. This is useful for plugging sync functions into a waterfall, series, or other async functions. Any arguments passed to the generated function will be passed to the wrapped function (except for the final callback argument). Errors thrown will be passed to the callback. - -__Example__ - -```js -async.waterfall([ - async.apply(fs.readFile, filename, "utf8"), - async.asyncify(JSON.parse), - function (data, next) { - // data is the result of parsing the text. - // If there was a parsing error, it would have been caught. - } -], callback) -``` - -If the function passed to `asyncify` returns a Promise, that promises's resolved/rejected state will be used to call the callback, rather than simply the synchronous return value. Example: - -```js -async.waterfall([ - async.apply(fs.readFile, filename, "utf8"), - async.asyncify(function (contents) { - return db.model.create(contents); - }), - function (model, next) { - // `model` is the instantiated model object. - // If there was an error, this function would be skipped. - } -], callback) -``` - -This also means you can asyncify ES2016 `async` functions. - -```js -var q = async.queue(async.asyncify(async function (file) { - var intermediateStep = await processFile(file); - return await somePromise(intermediateStep) -})); - -q.push(files); -``` - ---------------------------------------- - - -### log(function, arguments) - -Logs the result of an `async` function to the `console`. Only works in Node.js or -in browsers that support `console.log` and `console.error` (such as FF and Chrome). -If multiple arguments are returned from the async function, `console.log` is -called on each argument in order. - -__Arguments__ - -* `function` - The function you want to eventually apply all arguments to. -* `arguments...` - Any number of arguments to apply to the function. - -__Example__ - -```js -var hello = function(name, callback){ - setTimeout(function(){ - callback(null, 'hello ' + name); - }, 1000); -}; -``` -```js -node> async.log(hello, 'world'); -'hello world' -``` - ---------------------------------------- - - -### dir(function, arguments) - -Logs the result of an `async` function to the `console` using `console.dir` to -display the properties of the resulting object. Only works in Node.js or -in browsers that support `console.dir` and `console.error` (such as FF and Chrome). -If multiple arguments are returned from the async function, `console.dir` is -called on each argument in order. - -__Arguments__ - -* `function` - The function you want to eventually apply all arguments to. -* `arguments...` - Any number of arguments to apply to the function. - -__Example__ - -```js -var hello = function(name, callback){ - setTimeout(function(){ - callback(null, {hello: name}); - }, 1000); -}; -``` -```js -node> async.dir(hello, 'world'); -{hello: 'world'} -``` - ---------------------------------------- - - -### noConflict() - -Changes the value of `async` back to its original value, returning a reference to the -`async` object. diff --git a/node_modules/request/node_modules/form-data/node_modules/async/dist/async.js b/node_modules/request/node_modules/form-data/node_modules/async/dist/async.js deleted file mode 100644 index 31e7620f..00000000 --- a/node_modules/request/node_modules/form-data/node_modules/async/dist/async.js +++ /dev/null @@ -1,1265 +0,0 @@ -/*! - * async - * https://github.com/caolan/async - * - * Copyright 2010-2014 Caolan McMahon - * Released under the MIT license - */ -(function () { - - var async = {}; - function noop() {} - function identity(v) { - return v; - } - function toBool(v) { - return !!v; - } - function notId(v) { - return !v; - } - - // global on the server, window in the browser - var previous_async; - - // Establish the root object, `window` (`self`) in the browser, `global` - // on the server, or `this` in some virtual machines. We use `self` - // instead of `window` for `WebWorker` support. - var root = typeof self === 'object' && self.self === self && self || - typeof global === 'object' && global.global === global && global || - this; - - if (root != null) { - previous_async = root.async; - } - - async.noConflict = function () { - root.async = previous_async; - return async; - }; - - function only_once(fn) { - return function() { - if (fn === null) throw new Error("Callback was already called."); - fn.apply(this, arguments); - fn = null; - }; - } - - function _once(fn) { - return function() { - if (fn === null) return; - fn.apply(this, arguments); - fn = null; - }; - } - - //// cross-browser compatiblity functions //// - - var _toString = Object.prototype.toString; - - var _isArray = Array.isArray || function (obj) { - return _toString.call(obj) === '[object Array]'; - }; - - // Ported from underscore.js isObject - var _isObject = function(obj) { - var type = typeof obj; - return type === 'function' || type === 'object' && !!obj; - }; - - function _isArrayLike(arr) { - return _isArray(arr) || ( - // has a positive integer length property - typeof arr.length === "number" && - arr.length >= 0 && - arr.length % 1 === 0 - ); - } - - function _arrayEach(arr, iterator) { - var index = -1, - length = arr.length; - - while (++index < length) { - iterator(arr[index], index, arr); - } - } - - function _map(arr, iterator) { - var index = -1, - length = arr.length, - result = Array(length); - - while (++index < length) { - result[index] = iterator(arr[index], index, arr); - } - return result; - } - - function _range(count) { - return _map(Array(count), function (v, i) { return i; }); - } - - function _reduce(arr, iterator, memo) { - _arrayEach(arr, function (x, i, a) { - memo = iterator(memo, x, i, a); - }); - return memo; - } - - function _forEachOf(object, iterator) { - _arrayEach(_keys(object), function (key) { - iterator(object[key], key); - }); - } - - function _indexOf(arr, item) { - for (var i = 0; i < arr.length; i++) { - if (arr[i] === item) return i; - } - return -1; - } - - var _keys = Object.keys || function (obj) { - var keys = []; - for (var k in obj) { - if (obj.hasOwnProperty(k)) { - keys.push(k); - } - } - return keys; - }; - - function _keyIterator(coll) { - var i = -1; - var len; - var keys; - if (_isArrayLike(coll)) { - len = coll.length; - return function next() { - i++; - return i < len ? i : null; - }; - } else { - keys = _keys(coll); - len = keys.length; - return function next() { - i++; - return i < len ? keys[i] : null; - }; - } - } - - // Similar to ES6's rest param (http://ariya.ofilabs.com/2013/03/es6-and-rest-parameter.html) - // This accumulates the arguments passed into an array, after a given index. - // From underscore.js (https://github.com/jashkenas/underscore/pull/2140). - function _restParam(func, startIndex) { - startIndex = startIndex == null ? func.length - 1 : +startIndex; - return function() { - var length = Math.max(arguments.length - startIndex, 0); - var rest = Array(length); - for (var index = 0; index < length; index++) { - rest[index] = arguments[index + startIndex]; - } - switch (startIndex) { - case 0: return func.call(this, rest); - case 1: return func.call(this, arguments[0], rest); - } - // Currently unused but handle cases outside of the switch statement: - // var args = Array(startIndex + 1); - // for (index = 0; index < startIndex; index++) { - // args[index] = arguments[index]; - // } - // args[startIndex] = rest; - // return func.apply(this, args); - }; - } - - function _withoutIndex(iterator) { - return function (value, index, callback) { - return iterator(value, callback); - }; - } - - //// exported async module functions //// - - //// nextTick implementation with browser-compatible fallback //// - - // capture the global reference to guard against fakeTimer mocks - var _setImmediate = typeof setImmediate === 'function' && setImmediate; - - var _delay = _setImmediate ? function(fn) { - // not a direct alias for IE10 compatibility - _setImmediate(fn); - } : function(fn) { - setTimeout(fn, 0); - }; - - if (typeof process === 'object' && typeof process.nextTick === 'function') { - async.nextTick = process.nextTick; - } else { - async.nextTick = _delay; - } - async.setImmediate = _setImmediate ? _delay : async.nextTick; - - - async.forEach = - async.each = function (arr, iterator, callback) { - return async.eachOf(arr, _withoutIndex(iterator), callback); - }; - - async.forEachSeries = - async.eachSeries = function (arr, iterator, callback) { - return async.eachOfSeries(arr, _withoutIndex(iterator), callback); - }; - - - async.forEachLimit = - async.eachLimit = function (arr, limit, iterator, callback) { - return _eachOfLimit(limit)(arr, _withoutIndex(iterator), callback); - }; - - async.forEachOf = - async.eachOf = function (object, iterator, callback) { - callback = _once(callback || noop); - object = object || []; - - var iter = _keyIterator(object); - var key, completed = 0; - - while ((key = iter()) != null) { - completed += 1; - iterator(object[key], key, only_once(done)); - } - - if (completed === 0) callback(null); - - function done(err) { - completed--; - if (err) { - callback(err); - } - // Check key is null in case iterator isn't exhausted - // and done resolved synchronously. - else if (key === null && completed <= 0) { - callback(null); - } - } - }; - - async.forEachOfSeries = - async.eachOfSeries = function (obj, iterator, callback) { - callback = _once(callback || noop); - obj = obj || []; - var nextKey = _keyIterator(obj); - var key = nextKey(); - function iterate() { - var sync = true; - if (key === null) { - return callback(null); - } - iterator(obj[key], key, only_once(function (err) { - if (err) { - callback(err); - } - else { - key = nextKey(); - if (key === null) { - return callback(null); - } else { - if (sync) { - async.setImmediate(iterate); - } else { - iterate(); - } - } - } - })); - sync = false; - } - iterate(); - }; - - - - async.forEachOfLimit = - async.eachOfLimit = function (obj, limit, iterator, callback) { - _eachOfLimit(limit)(obj, iterator, callback); - }; - - function _eachOfLimit(limit) { - - return function (obj, iterator, callback) { - callback = _once(callback || noop); - obj = obj || []; - var nextKey = _keyIterator(obj); - if (limit <= 0) { - return callback(null); - } - var done = false; - var running = 0; - var errored = false; - - (function replenish () { - if (done && running <= 0) { - return callback(null); - } - - while (running < limit && !errored) { - var key = nextKey(); - if (key === null) { - done = true; - if (running <= 0) { - callback(null); - } - return; - } - running += 1; - iterator(obj[key], key, only_once(function (err) { - running -= 1; - if (err) { - callback(err); - errored = true; - } - else { - replenish(); - } - })); - } - })(); - }; - } - - - function doParallel(fn) { - return function (obj, iterator, callback) { - return fn(async.eachOf, obj, iterator, callback); - }; - } - function doParallelLimit(fn) { - return function (obj, limit, iterator, callback) { - return fn(_eachOfLimit(limit), obj, iterator, callback); - }; - } - function doSeries(fn) { - return function (obj, iterator, callback) { - return fn(async.eachOfSeries, obj, iterator, callback); - }; - } - - function _asyncMap(eachfn, arr, iterator, callback) { - callback = _once(callback || noop); - arr = arr || []; - var results = _isArrayLike(arr) ? [] : {}; - eachfn(arr, function (value, index, callback) { - iterator(value, function (err, v) { - results[index] = v; - callback(err); - }); - }, function (err) { - callback(err, results); - }); - } - - async.map = doParallel(_asyncMap); - async.mapSeries = doSeries(_asyncMap); - async.mapLimit = doParallelLimit(_asyncMap); - - // reduce only has a series version, as doing reduce in parallel won't - // work in many situations. - async.inject = - async.foldl = - async.reduce = function (arr, memo, iterator, callback) { - async.eachOfSeries(arr, function (x, i, callback) { - iterator(memo, x, function (err, v) { - memo = v; - callback(err); - }); - }, function (err) { - callback(err, memo); - }); - }; - - async.foldr = - async.reduceRight = function (arr, memo, iterator, callback) { - var reversed = _map(arr, identity).reverse(); - async.reduce(reversed, memo, iterator, callback); - }; - - async.transform = function (arr, memo, iterator, callback) { - if (arguments.length === 3) { - callback = iterator; - iterator = memo; - memo = _isArray(arr) ? [] : {}; - } - - async.eachOf(arr, function(v, k, cb) { - iterator(memo, v, k, cb); - }, function(err) { - callback(err, memo); - }); - }; - - function _filter(eachfn, arr, iterator, callback) { - var results = []; - eachfn(arr, function (x, index, callback) { - iterator(x, function (v) { - if (v) { - results.push({index: index, value: x}); - } - callback(); - }); - }, function () { - callback(_map(results.sort(function (a, b) { - return a.index - b.index; - }), function (x) { - return x.value; - })); - }); - } - - async.select = - async.filter = doParallel(_filter); - - async.selectLimit = - async.filterLimit = doParallelLimit(_filter); - - async.selectSeries = - async.filterSeries = doSeries(_filter); - - function _reject(eachfn, arr, iterator, callback) { - _filter(eachfn, arr, function(value, cb) { - iterator(value, function(v) { - cb(!v); - }); - }, callback); - } - async.reject = doParallel(_reject); - async.rejectLimit = doParallelLimit(_reject); - async.rejectSeries = doSeries(_reject); - - function _createTester(eachfn, check, getResult) { - return function(arr, limit, iterator, cb) { - function done() { - if (cb) cb(getResult(false, void 0)); - } - function iteratee(x, _, callback) { - if (!cb) return callback(); - iterator(x, function (v) { - if (cb && check(v)) { - cb(getResult(true, x)); - cb = iterator = false; - } - callback(); - }); - } - if (arguments.length > 3) { - eachfn(arr, limit, iteratee, done); - } else { - cb = iterator; - iterator = limit; - eachfn(arr, iteratee, done); - } - }; - } - - async.any = - async.some = _createTester(async.eachOf, toBool, identity); - - async.someLimit = _createTester(async.eachOfLimit, toBool, identity); - - async.all = - async.every = _createTester(async.eachOf, notId, notId); - - async.everyLimit = _createTester(async.eachOfLimit, notId, notId); - - function _findGetResult(v, x) { - return x; - } - async.detect = _createTester(async.eachOf, identity, _findGetResult); - async.detectSeries = _createTester(async.eachOfSeries, identity, _findGetResult); - async.detectLimit = _createTester(async.eachOfLimit, identity, _findGetResult); - - async.sortBy = function (arr, iterator, callback) { - async.map(arr, function (x, callback) { - iterator(x, function (err, criteria) { - if (err) { - callback(err); - } - else { - callback(null, {value: x, criteria: criteria}); - } - }); - }, function (err, results) { - if (err) { - return callback(err); - } - else { - callback(null, _map(results.sort(comparator), function (x) { - return x.value; - })); - } - - }); - - function comparator(left, right) { - var a = left.criteria, b = right.criteria; - return a < b ? -1 : a > b ? 1 : 0; - } - }; - - async.auto = function (tasks, concurrency, callback) { - if (typeof arguments[1] === 'function') { - // concurrency is optional, shift the args. - callback = concurrency; - concurrency = null; - } - callback = _once(callback || noop); - var keys = _keys(tasks); - var remainingTasks = keys.length; - if (!remainingTasks) { - return callback(null); - } - if (!concurrency) { - concurrency = remainingTasks; - } - - var results = {}; - var runningTasks = 0; - - var hasError = false; - - var listeners = []; - function addListener(fn) { - listeners.unshift(fn); - } - function removeListener(fn) { - var idx = _indexOf(listeners, fn); - if (idx >= 0) listeners.splice(idx, 1); - } - function taskComplete() { - remainingTasks--; - _arrayEach(listeners.slice(0), function (fn) { - fn(); - }); - } - - addListener(function () { - if (!remainingTasks) { - callback(null, results); - } - }); - - _arrayEach(keys, function (k) { - if (hasError) return; - var task = _isArray(tasks[k]) ? tasks[k]: [tasks[k]]; - var taskCallback = _restParam(function(err, args) { - runningTasks--; - if (args.length <= 1) { - args = args[0]; - } - if (err) { - var safeResults = {}; - _forEachOf(results, function(val, rkey) { - safeResults[rkey] = val; - }); - safeResults[k] = args; - hasError = true; - - callback(err, safeResults); - } - else { - results[k] = args; - async.setImmediate(taskComplete); - } - }); - var requires = task.slice(0, task.length - 1); - // prevent dead-locks - var len = requires.length; - var dep; - while (len--) { - if (!(dep = tasks[requires[len]])) { - throw new Error('Has nonexistent dependency in ' + requires.join(', ')); - } - if (_isArray(dep) && _indexOf(dep, k) >= 0) { - throw new Error('Has cyclic dependencies'); - } - } - function ready() { - return runningTasks < concurrency && _reduce(requires, function (a, x) { - return (a && results.hasOwnProperty(x)); - }, true) && !results.hasOwnProperty(k); - } - if (ready()) { - runningTasks++; - task[task.length - 1](taskCallback, results); - } - else { - addListener(listener); - } - function listener() { - if (ready()) { - runningTasks++; - removeListener(listener); - task[task.length - 1](taskCallback, results); - } - } - }); - }; - - - - async.retry = function(times, task, callback) { - var DEFAULT_TIMES = 5; - var DEFAULT_INTERVAL = 0; - - var attempts = []; - - var opts = { - times: DEFAULT_TIMES, - interval: DEFAULT_INTERVAL - }; - - function parseTimes(acc, t){ - if(typeof t === 'number'){ - acc.times = parseInt(t, 10) || DEFAULT_TIMES; - } else if(typeof t === 'object'){ - acc.times = parseInt(t.times, 10) || DEFAULT_TIMES; - acc.interval = parseInt(t.interval, 10) || DEFAULT_INTERVAL; - } else { - throw new Error('Unsupported argument type for \'times\': ' + typeof t); - } - } - - var length = arguments.length; - if (length < 1 || length > 3) { - throw new Error('Invalid arguments - must be either (task), (task, callback), (times, task) or (times, task, callback)'); - } else if (length <= 2 && typeof times === 'function') { - callback = task; - task = times; - } - if (typeof times !== 'function') { - parseTimes(opts, times); - } - opts.callback = callback; - opts.task = task; - - function wrappedTask(wrappedCallback, wrappedResults) { - function retryAttempt(task, finalAttempt) { - return function(seriesCallback) { - task(function(err, result){ - seriesCallback(!err || finalAttempt, {err: err, result: result}); - }, wrappedResults); - }; - } - - function retryInterval(interval){ - return function(seriesCallback){ - setTimeout(function(){ - seriesCallback(null); - }, interval); - }; - } - - while (opts.times) { - - var finalAttempt = !(opts.times-=1); - attempts.push(retryAttempt(opts.task, finalAttempt)); - if(!finalAttempt && opts.interval > 0){ - attempts.push(retryInterval(opts.interval)); - } - } - - async.series(attempts, function(done, data){ - data = data[data.length - 1]; - (wrappedCallback || opts.callback)(data.err, data.result); - }); - } - - // If a callback is passed, run this as a controll flow - return opts.callback ? wrappedTask() : wrappedTask; - }; - - async.waterfall = function (tasks, callback) { - callback = _once(callback || noop); - if (!_isArray(tasks)) { - var err = new Error('First argument to waterfall must be an array of functions'); - return callback(err); - } - if (!tasks.length) { - return callback(); - } - function wrapIterator(iterator) { - return _restParam(function (err, args) { - if (err) { - callback.apply(null, [err].concat(args)); - } - else { - var next = iterator.next(); - if (next) { - args.push(wrapIterator(next)); - } - else { - args.push(callback); - } - ensureAsync(iterator).apply(null, args); - } - }); - } - wrapIterator(async.iterator(tasks))(); - }; - - function _parallel(eachfn, tasks, callback) { - callback = callback || noop; - var results = _isArrayLike(tasks) ? [] : {}; - - eachfn(tasks, function (task, key, callback) { - task(_restParam(function (err, args) { - if (args.length <= 1) { - args = args[0]; - } - results[key] = args; - callback(err); - })); - }, function (err) { - callback(err, results); - }); - } - - async.parallel = function (tasks, callback) { - _parallel(async.eachOf, tasks, callback); - }; - - async.parallelLimit = function(tasks, limit, callback) { - _parallel(_eachOfLimit(limit), tasks, callback); - }; - - async.series = function(tasks, callback) { - _parallel(async.eachOfSeries, tasks, callback); - }; - - async.iterator = function (tasks) { - function makeCallback(index) { - function fn() { - if (tasks.length) { - tasks[index].apply(null, arguments); - } - return fn.next(); - } - fn.next = function () { - return (index < tasks.length - 1) ? makeCallback(index + 1): null; - }; - return fn; - } - return makeCallback(0); - }; - - async.apply = _restParam(function (fn, args) { - return _restParam(function (callArgs) { - return fn.apply( - null, args.concat(callArgs) - ); - }); - }); - - function _concat(eachfn, arr, fn, callback) { - var result = []; - eachfn(arr, function (x, index, cb) { - fn(x, function (err, y) { - result = result.concat(y || []); - cb(err); - }); - }, function (err) { - callback(err, result); - }); - } - async.concat = doParallel(_concat); - async.concatSeries = doSeries(_concat); - - async.whilst = function (test, iterator, callback) { - callback = callback || noop; - if (test()) { - var next = _restParam(function(err, args) { - if (err) { - callback(err); - } else if (test.apply(this, args)) { - iterator(next); - } else { - callback.apply(null, [null].concat(args)); - } - }); - iterator(next); - } else { - callback(null); - } - }; - - async.doWhilst = function (iterator, test, callback) { - var calls = 0; - return async.whilst(function() { - return ++calls <= 1 || test.apply(this, arguments); - }, iterator, callback); - }; - - async.until = function (test, iterator, callback) { - return async.whilst(function() { - return !test.apply(this, arguments); - }, iterator, callback); - }; - - async.doUntil = function (iterator, test, callback) { - return async.doWhilst(iterator, function() { - return !test.apply(this, arguments); - }, callback); - }; - - async.during = function (test, iterator, callback) { - callback = callback || noop; - - var next = _restParam(function(err, args) { - if (err) { - callback(err); - } else { - args.push(check); - test.apply(this, args); - } - }); - - var check = function(err, truth) { - if (err) { - callback(err); - } else if (truth) { - iterator(next); - } else { - callback(null); - } - }; - - test(check); - }; - - async.doDuring = function (iterator, test, callback) { - var calls = 0; - async.during(function(next) { - if (calls++ < 1) { - next(null, true); - } else { - test.apply(this, arguments); - } - }, iterator, callback); - }; - - function _queue(worker, concurrency, payload) { - if (concurrency == null) { - concurrency = 1; - } - else if(concurrency === 0) { - throw new Error('Concurrency must not be zero'); - } - function _insert(q, data, pos, callback) { - if (callback != null && typeof callback !== "function") { - throw new Error("task callback must be a function"); - } - q.started = true; - if (!_isArray(data)) { - data = [data]; - } - if(data.length === 0 && q.idle()) { - // call drain immediately if there are no tasks - return async.setImmediate(function() { - q.drain(); - }); - } - _arrayEach(data, function(task) { - var item = { - data: task, - callback: callback || noop - }; - - if (pos) { - q.tasks.unshift(item); - } else { - q.tasks.push(item); - } - - if (q.tasks.length === q.concurrency) { - q.saturated(); - } - }); - async.setImmediate(q.process); - } - function _next(q, tasks) { - return function(){ - workers -= 1; - - var removed = false; - var args = arguments; - _arrayEach(tasks, function (task) { - _arrayEach(workersList, function (worker, index) { - if (worker === task && !removed) { - workersList.splice(index, 1); - removed = true; - } - }); - - task.callback.apply(task, args); - }); - if (q.tasks.length + workers === 0) { - q.drain(); - } - q.process(); - }; - } - - var workers = 0; - var workersList = []; - var q = { - tasks: [], - concurrency: concurrency, - payload: payload, - saturated: noop, - empty: noop, - drain: noop, - started: false, - paused: false, - push: function (data, callback) { - _insert(q, data, false, callback); - }, - kill: function () { - q.drain = noop; - q.tasks = []; - }, - unshift: function (data, callback) { - _insert(q, data, true, callback); - }, - process: function () { - while(!q.paused && workers < q.concurrency && q.tasks.length){ - - var tasks = q.payload ? - q.tasks.splice(0, q.payload) : - q.tasks.splice(0, q.tasks.length); - - var data = _map(tasks, function (task) { - return task.data; - }); - - if (q.tasks.length === 0) { - q.empty(); - } - workers += 1; - workersList.push(tasks[0]); - var cb = only_once(_next(q, tasks)); - worker(data, cb); - } - }, - length: function () { - return q.tasks.length; - }, - running: function () { - return workers; - }, - workersList: function () { - return workersList; - }, - idle: function() { - return q.tasks.length + workers === 0; - }, - pause: function () { - q.paused = true; - }, - resume: function () { - if (q.paused === false) { return; } - q.paused = false; - var resumeCount = Math.min(q.concurrency, q.tasks.length); - // Need to call q.process once per concurrent - // worker to preserve full concurrency after pause - for (var w = 1; w <= resumeCount; w++) { - async.setImmediate(q.process); - } - } - }; - return q; - } - - async.queue = function (worker, concurrency) { - var q = _queue(function (items, cb) { - worker(items[0], cb); - }, concurrency, 1); - - return q; - }; - - async.priorityQueue = function (worker, concurrency) { - - function _compareTasks(a, b){ - return a.priority - b.priority; - } - - function _binarySearch(sequence, item, compare) { - var beg = -1, - end = sequence.length - 1; - while (beg < end) { - var mid = beg + ((end - beg + 1) >>> 1); - if (compare(item, sequence[mid]) >= 0) { - beg = mid; - } else { - end = mid - 1; - } - } - return beg; - } - - function _insert(q, data, priority, callback) { - if (callback != null && typeof callback !== "function") { - throw new Error("task callback must be a function"); - } - q.started = true; - if (!_isArray(data)) { - data = [data]; - } - if(data.length === 0) { - // call drain immediately if there are no tasks - return async.setImmediate(function() { - q.drain(); - }); - } - _arrayEach(data, function(task) { - var item = { - data: task, - priority: priority, - callback: typeof callback === 'function' ? callback : noop - }; - - q.tasks.splice(_binarySearch(q.tasks, item, _compareTasks) + 1, 0, item); - - if (q.tasks.length === q.concurrency) { - q.saturated(); - } - async.setImmediate(q.process); - }); - } - - // Start with a normal queue - var q = async.queue(worker, concurrency); - - // Override push to accept second parameter representing priority - q.push = function (data, priority, callback) { - _insert(q, data, priority, callback); - }; - - // Remove unshift function - delete q.unshift; - - return q; - }; - - async.cargo = function (worker, payload) { - return _queue(worker, 1, payload); - }; - - function _console_fn(name) { - return _restParam(function (fn, args) { - fn.apply(null, args.concat([_restParam(function (err, args) { - if (typeof console === 'object') { - if (err) { - if (console.error) { - console.error(err); - } - } - else if (console[name]) { - _arrayEach(args, function (x) { - console[name](x); - }); - } - } - })])); - }); - } - async.log = _console_fn('log'); - async.dir = _console_fn('dir'); - /*async.info = _console_fn('info'); - async.warn = _console_fn('warn'); - async.error = _console_fn('error');*/ - - async.memoize = function (fn, hasher) { - var memo = {}; - var queues = {}; - var has = Object.prototype.hasOwnProperty; - hasher = hasher || identity; - var memoized = _restParam(function memoized(args) { - var callback = args.pop(); - var key = hasher.apply(null, args); - if (has.call(memo, key)) { - async.setImmediate(function () { - callback.apply(null, memo[key]); - }); - } - else if (has.call(queues, key)) { - queues[key].push(callback); - } - else { - queues[key] = [callback]; - fn.apply(null, args.concat([_restParam(function (args) { - memo[key] = args; - var q = queues[key]; - delete queues[key]; - for (var i = 0, l = q.length; i < l; i++) { - q[i].apply(null, args); - } - })])); - } - }); - memoized.memo = memo; - memoized.unmemoized = fn; - return memoized; - }; - - async.unmemoize = function (fn) { - return function () { - return (fn.unmemoized || fn).apply(null, arguments); - }; - }; - - function _times(mapper) { - return function (count, iterator, callback) { - mapper(_range(count), iterator, callback); - }; - } - - async.times = _times(async.map); - async.timesSeries = _times(async.mapSeries); - async.timesLimit = function (count, limit, iterator, callback) { - return async.mapLimit(_range(count), limit, iterator, callback); - }; - - async.seq = function (/* functions... */) { - var fns = arguments; - return _restParam(function (args) { - var that = this; - - var callback = args[args.length - 1]; - if (typeof callback == 'function') { - args.pop(); - } else { - callback = noop; - } - - async.reduce(fns, args, function (newargs, fn, cb) { - fn.apply(that, newargs.concat([_restParam(function (err, nextargs) { - cb(err, nextargs); - })])); - }, - function (err, results) { - callback.apply(that, [err].concat(results)); - }); - }); - }; - - async.compose = function (/* functions... */) { - return async.seq.apply(null, Array.prototype.reverse.call(arguments)); - }; - - - function _applyEach(eachfn) { - return _restParam(function(fns, args) { - var go = _restParam(function(args) { - var that = this; - var callback = args.pop(); - return eachfn(fns, function (fn, _, cb) { - fn.apply(that, args.concat([cb])); - }, - callback); - }); - if (args.length) { - return go.apply(this, args); - } - else { - return go; - } - }); - } - - async.applyEach = _applyEach(async.eachOf); - async.applyEachSeries = _applyEach(async.eachOfSeries); - - - async.forever = function (fn, callback) { - var done = only_once(callback || noop); - var task = ensureAsync(fn); - function next(err) { - if (err) { - return done(err); - } - task(next); - } - next(); - }; - - function ensureAsync(fn) { - return _restParam(function (args) { - var callback = args.pop(); - args.push(function () { - var innerArgs = arguments; - if (sync) { - async.setImmediate(function () { - callback.apply(null, innerArgs); - }); - } else { - callback.apply(null, innerArgs); - } - }); - var sync = true; - fn.apply(this, args); - sync = false; - }); - } - - async.ensureAsync = ensureAsync; - - async.constant = _restParam(function(values) { - var args = [null].concat(values); - return function (callback) { - return callback.apply(this, args); - }; - }); - - async.wrapSync = - async.asyncify = function asyncify(func) { - return _restParam(function (args) { - var callback = args.pop(); - var result; - try { - result = func.apply(this, args); - } catch (e) { - return callback(e); - } - // if result is Promise object - if (_isObject(result) && typeof result.then === "function") { - result.then(function(value) { - callback(null, value); - })["catch"](function(err) { - callback(err.message ? err : new Error(err)); - }); - } else { - callback(null, result); - } - }); - }; - - // Node.js - if (typeof module === 'object' && module.exports) { - module.exports = async; - } - // AMD / RequireJS - else if (typeof define === 'function' && define.amd) { - define([], function () { - return async; - }); - } - // included directly via '); - expect(encoded).to.equal('\\x3cscript\\x3ealert\\x281\\x29\\x3c\\x2fscript\\x3e'); - done(); - }); - - it('encodes \' characters', function (done) { - - var encoded = Hoek.escapeJavaScript('something(\'param\')'); - expect(encoded).to.equal('something\\x28\\x27param\\x27\\x29'); - done(); - }); - - it('encodes large unicode characters with the correct padding', function (done) { - - var encoded = Hoek.escapeJavaScript(String.fromCharCode(500) + String.fromCharCode(1000)); - expect(encoded).to.equal('\\u0500\\u1000'); - done(); - }); - - it('doesn\'t throw an exception when passed null', function (done) { - - var encoded = Hoek.escapeJavaScript(null); - expect(encoded).to.equal(''); - done(); - }); -}); - -describe('escapeHtml()', function () { - - it('encodes / characters', function (done) { - - var encoded = Hoek.escapeHtml(''); - expect(encoded).to.equal('<script>alert(1)</script>'); - done(); - }); - - it('encodes < and > as named characters', function (done) { - - var encoded = Hoek.escapeHtml(' - - \ No newline at end of file diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/jsbn/example.js b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/jsbn/example.js deleted file mode 100644 index 664c1b45..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/jsbn/example.js +++ /dev/null @@ -1,3 +0,0 @@ -var BigInteger = require('./'); -var a = new BigInteger('91823918239182398123'); -console.log(a.bitLength()); \ No newline at end of file diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/jsbn/index.js b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/jsbn/index.js deleted file mode 100644 index e32fe13d..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/jsbn/index.js +++ /dev/null @@ -1,1358 +0,0 @@ -(function(){ - - // Copyright (c) 2005 Tom Wu - // All Rights Reserved. - // See "LICENSE" for details. - - // Basic JavaScript BN library - subset useful for RSA encryption. - - // Bits per digit - var dbits; - - // JavaScript engine analysis - var canary = 0xdeadbeefcafe; - var j_lm = ((canary&0xffffff)==0xefcafe); - - // (public) Constructor - function BigInteger(a,b,c) { - if(a != null) - if("number" == typeof a) this.fromNumber(a,b,c); - else if(b == null && "string" != typeof a) this.fromString(a,256); - else this.fromString(a,b); - } - - // return new, unset BigInteger - function nbi() { return new BigInteger(null); } - - // am: Compute w_j += (x*this_i), propagate carries, - // c is initial carry, returns final carry. - // c < 3*dvalue, x < 2*dvalue, this_i < dvalue - // We need to select the fastest one that works in this environment. - - // am1: use a single mult and divide to get the high bits, - // max digit bits should be 26 because - // max internal value = 2*dvalue^2-2*dvalue (< 2^53) - function am1(i,x,w,j,c,n) { - while(--n >= 0) { - var v = x*this[i++]+w[j]+c; - c = Math.floor(v/0x4000000); - w[j++] = v&0x3ffffff; - } - return c; - } - // am2 avoids a big mult-and-extract completely. - // Max digit bits should be <= 30 because we do bitwise ops - // on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) - function am2(i,x,w,j,c,n) { - var xl = x&0x7fff, xh = x>>15; - while(--n >= 0) { - var l = this[i]&0x7fff; - var h = this[i++]>>15; - var m = xh*l+h*xl; - l = xl*l+((m&0x7fff)<<15)+w[j]+(c&0x3fffffff); - c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); - w[j++] = l&0x3fffffff; - } - return c; - } - // Alternately, set max digit bits to 28 since some - // browsers slow down when dealing with 32-bit numbers. - function am3(i,x,w,j,c,n) { - var xl = x&0x3fff, xh = x>>14; - while(--n >= 0) { - var l = this[i]&0x3fff; - var h = this[i++]>>14; - var m = xh*l+h*xl; - l = xl*l+((m&0x3fff)<<14)+w[j]+c; - c = (l>>28)+(m>>14)+xh*h; - w[j++] = l&0xfffffff; - } - return c; - } - var inBrowser = typeof navigator !== "undefined"; - if(inBrowser && j_lm && (navigator.appName == "Microsoft Internet Explorer")) { - BigInteger.prototype.am = am2; - dbits = 30; - } - else if(inBrowser && j_lm && (navigator.appName != "Netscape")) { - BigInteger.prototype.am = am1; - dbits = 26; - } - else { // Mozilla/Netscape seems to prefer am3 - BigInteger.prototype.am = am3; - dbits = 28; - } - - BigInteger.prototype.DB = dbits; - BigInteger.prototype.DM = ((1<= 0; --i) r[i] = this[i]; - r.t = this.t; - r.s = this.s; - } - - // (protected) set from integer value x, -DV <= x < DV - function bnpFromInt(x) { - this.t = 1; - this.s = (x<0)?-1:0; - if(x > 0) this[0] = x; - else if(x < -1) this[0] = x+this.DV; - else this.t = 0; - } - - // return bigint initialized to value - function nbv(i) { var r = nbi(); r.fromInt(i); return r; } - - // (protected) set from string and radix - function bnpFromString(s,b) { - var k; - if(b == 16) k = 4; - else if(b == 8) k = 3; - else if(b == 256) k = 8; // byte array - else if(b == 2) k = 1; - else if(b == 32) k = 5; - else if(b == 4) k = 2; - else { this.fromRadix(s,b); return; } - this.t = 0; - this.s = 0; - var i = s.length, mi = false, sh = 0; - while(--i >= 0) { - var x = (k==8)?s[i]&0xff:intAt(s,i); - if(x < 0) { - if(s.charAt(i) == "-") mi = true; - continue; - } - mi = false; - if(sh == 0) - this[this.t++] = x; - else if(sh+k > this.DB) { - this[this.t-1] |= (x&((1<<(this.DB-sh))-1))<>(this.DB-sh)); - } - else - this[this.t-1] |= x<= this.DB) sh -= this.DB; - } - if(k == 8 && (s[0]&0x80) != 0) { - this.s = -1; - if(sh > 0) this[this.t-1] |= ((1<<(this.DB-sh))-1)< 0 && this[this.t-1] == c) --this.t; - } - - // (public) return string representation in given radix - function bnToString(b) { - if(this.s < 0) return "-"+this.negate().toString(b); - var k; - if(b == 16) k = 4; - else if(b == 8) k = 3; - else if(b == 2) k = 1; - else if(b == 32) k = 5; - else if(b == 4) k = 2; - else return this.toRadix(b); - var km = (1< 0) { - if(p < this.DB && (d = this[i]>>p) > 0) { m = true; r = int2char(d); } - while(i >= 0) { - if(p < k) { - d = (this[i]&((1<>(p+=this.DB-k); - } - else { - d = (this[i]>>(p-=k))&km; - if(p <= 0) { p += this.DB; --i; } - } - if(d > 0) m = true; - if(m) r += int2char(d); - } - } - return m?r:"0"; - } - - // (public) -this - function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; } - - // (public) |this| - function bnAbs() { return (this.s<0)?this.negate():this; } - - // (public) return + if this > a, - if this < a, 0 if equal - function bnCompareTo(a) { - var r = this.s-a.s; - if(r != 0) return r; - var i = this.t; - r = i-a.t; - if(r != 0) return (this.s<0)?-r:r; - while(--i >= 0) if((r=this[i]-a[i]) != 0) return r; - return 0; - } - - // returns bit length of the integer x - function nbits(x) { - var r = 1, t; - if((t=x>>>16) != 0) { x = t; r += 16; } - if((t=x>>8) != 0) { x = t; r += 8; } - if((t=x>>4) != 0) { x = t; r += 4; } - if((t=x>>2) != 0) { x = t; r += 2; } - if((t=x>>1) != 0) { x = t; r += 1; } - return r; - } - - // (public) return the number of bits in "this" - function bnBitLength() { - if(this.t <= 0) return 0; - return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM)); - } - - // (protected) r = this << n*DB - function bnpDLShiftTo(n,r) { - var i; - for(i = this.t-1; i >= 0; --i) r[i+n] = this[i]; - for(i = n-1; i >= 0; --i) r[i] = 0; - r.t = this.t+n; - r.s = this.s; - } - - // (protected) r = this >> n*DB - function bnpDRShiftTo(n,r) { - for(var i = n; i < this.t; ++i) r[i-n] = this[i]; - r.t = Math.max(this.t-n,0); - r.s = this.s; - } - - // (protected) r = this << n - function bnpLShiftTo(n,r) { - var bs = n%this.DB; - var cbs = this.DB-bs; - var bm = (1<= 0; --i) { - r[i+ds+1] = (this[i]>>cbs)|c; - c = (this[i]&bm)<= 0; --i) r[i] = 0; - r[ds] = c; - r.t = this.t+ds+1; - r.s = this.s; - r.clamp(); - } - - // (protected) r = this >> n - function bnpRShiftTo(n,r) { - r.s = this.s; - var ds = Math.floor(n/this.DB); - if(ds >= this.t) { r.t = 0; return; } - var bs = n%this.DB; - var cbs = this.DB-bs; - var bm = (1<>bs; - for(var i = ds+1; i < this.t; ++i) { - r[i-ds-1] |= (this[i]&bm)<>bs; - } - if(bs > 0) r[this.t-ds-1] |= (this.s&bm)<>= this.DB; - } - if(a.t < this.t) { - c -= a.s; - while(i < this.t) { - c += this[i]; - r[i++] = c&this.DM; - c >>= this.DB; - } - c += this.s; - } - else { - c += this.s; - while(i < a.t) { - c -= a[i]; - r[i++] = c&this.DM; - c >>= this.DB; - } - c -= a.s; - } - r.s = (c<0)?-1:0; - if(c < -1) r[i++] = this.DV+c; - else if(c > 0) r[i++] = c; - r.t = i; - r.clamp(); - } - - // (protected) r = this * a, r != this,a (HAC 14.12) - // "this" should be the larger one if appropriate. - function bnpMultiplyTo(a,r) { - var x = this.abs(), y = a.abs(); - var i = x.t; - r.t = i+y.t; - while(--i >= 0) r[i] = 0; - for(i = 0; i < y.t; ++i) r[i+x.t] = x.am(0,y[i],r,i,0,x.t); - r.s = 0; - r.clamp(); - if(this.s != a.s) BigInteger.ZERO.subTo(r,r); - } - - // (protected) r = this^2, r != this (HAC 14.16) - function bnpSquareTo(r) { - var x = this.abs(); - var i = r.t = 2*x.t; - while(--i >= 0) r[i] = 0; - for(i = 0; i < x.t-1; ++i) { - var c = x.am(i,x[i],r,2*i,0,1); - if((r[i+x.t]+=x.am(i+1,2*x[i],r,2*i+1,c,x.t-i-1)) >= x.DV) { - r[i+x.t] -= x.DV; - r[i+x.t+1] = 1; - } - } - if(r.t > 0) r[r.t-1] += x.am(i,x[i],r,2*i,0,1); - r.s = 0; - r.clamp(); - } - - // (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) - // r != q, this != m. q or r may be null. - function bnpDivRemTo(m,q,r) { - var pm = m.abs(); - if(pm.t <= 0) return; - var pt = this.abs(); - if(pt.t < pm.t) { - if(q != null) q.fromInt(0); - if(r != null) this.copyTo(r); - return; - } - if(r == null) r = nbi(); - var y = nbi(), ts = this.s, ms = m.s; - var nsh = this.DB-nbits(pm[pm.t-1]); // normalize modulus - if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); } - else { pm.copyTo(y); pt.copyTo(r); } - var ys = y.t; - var y0 = y[ys-1]; - if(y0 == 0) return; - var yt = y0*(1<1)?y[ys-2]>>this.F2:0); - var d1 = this.FV/yt, d2 = (1<= 0) { - r[r.t++] = 1; - r.subTo(t,r); - } - BigInteger.ONE.dlShiftTo(ys,t); - t.subTo(y,y); // "negative" y so we can replace sub with am later - while(y.t < ys) y[y.t++] = 0; - while(--j >= 0) { - // Estimate quotient digit - var qd = (r[--i]==y0)?this.DM:Math.floor(r[i]*d1+(r[i-1]+e)*d2); - if((r[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out - y.dlShiftTo(j,t); - r.subTo(t,r); - while(r[i] < --qd) r.subTo(t,r); - } - } - if(q != null) { - r.drShiftTo(ys,q); - if(ts != ms) BigInteger.ZERO.subTo(q,q); - } - r.t = ys; - r.clamp(); - if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder - if(ts < 0) BigInteger.ZERO.subTo(r,r); - } - - // (public) this mod a - function bnMod(a) { - var r = nbi(); - this.abs().divRemTo(a,null,r); - if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r); - return r; - } - - // Modular reduction using "classic" algorithm - function Classic(m) { this.m = m; } - function cConvert(x) { - if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); - else return x; - } - function cRevert(x) { return x; } - function cReduce(x) { x.divRemTo(this.m,null,x); } - function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } - function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); } - - Classic.prototype.convert = cConvert; - Classic.prototype.revert = cRevert; - Classic.prototype.reduce = cReduce; - Classic.prototype.mulTo = cMulTo; - Classic.prototype.sqrTo = cSqrTo; - - // (protected) return "-1/this % 2^DB"; useful for Mont. reduction - // justification: - // xy == 1 (mod m) - // xy = 1+km - // xy(2-xy) = (1+km)(1-km) - // x[y(2-xy)] = 1-k^2m^2 - // x[y(2-xy)] == 1 (mod m^2) - // if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 - // should reduce x and y(2-xy) by m^2 at each step to keep size bounded. - // JS multiply "overflows" differently from C/C++, so care is needed here. - function bnpInvDigit() { - if(this.t < 1) return 0; - var x = this[0]; - if((x&1) == 0) return 0; - var y = x&3; // y == 1/x mod 2^2 - y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4 - y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8 - y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16 - // last step - calculate inverse mod DV directly; - // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints - y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits - // we really want the negative inverse, and -DV < y < DV - return (y>0)?this.DV-y:-y; - } - - // Montgomery reduction - function Montgomery(m) { - this.m = m; - this.mp = m.invDigit(); - this.mpl = this.mp&0x7fff; - this.mph = this.mp>>15; - this.um = (1<<(m.DB-15))-1; - this.mt2 = 2*m.t; - } - - // xR mod m - function montConvert(x) { - var r = nbi(); - x.abs().dlShiftTo(this.m.t,r); - r.divRemTo(this.m,null,r); - if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r); - return r; - } - - // x/R mod m - function montRevert(x) { - var r = nbi(); - x.copyTo(r); - this.reduce(r); - return r; - } - - // x = x/R mod m (HAC 14.32) - function montReduce(x) { - while(x.t <= this.mt2) // pad x so am has enough room later - x[x.t++] = 0; - for(var i = 0; i < this.m.t; ++i) { - // faster way of calculating u0 = x[i]*mp mod DV - var j = x[i]&0x7fff; - var u0 = (j*this.mpl+(((j*this.mph+(x[i]>>15)*this.mpl)&this.um)<<15))&x.DM; - // use am to combine the multiply-shift-add into one call - j = i+this.m.t; - x[j] += this.m.am(0,u0,x,i,0,this.m.t); - // propagate carry - while(x[j] >= x.DV) { x[j] -= x.DV; x[++j]++; } - } - x.clamp(); - x.drShiftTo(this.m.t,x); - if(x.compareTo(this.m) >= 0) x.subTo(this.m,x); - } - - // r = "x^2/R mod m"; x != r - function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); } - - // r = "xy/R mod m"; x,y != r - function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } - - Montgomery.prototype.convert = montConvert; - Montgomery.prototype.revert = montRevert; - Montgomery.prototype.reduce = montReduce; - Montgomery.prototype.mulTo = montMulTo; - Montgomery.prototype.sqrTo = montSqrTo; - - // (protected) true iff this is even - function bnpIsEven() { return ((this.t>0)?(this[0]&1):this.s) == 0; } - - // (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) - function bnpExp(e,z) { - if(e > 0xffffffff || e < 1) return BigInteger.ONE; - var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1; - g.copyTo(r); - while(--i >= 0) { - z.sqrTo(r,r2); - if((e&(1< 0) z.mulTo(r2,g,r); - else { var t = r; r = r2; r2 = t; } - } - return z.revert(r); - } - - // (public) this^e % m, 0 <= e < 2^32 - function bnModPowInt(e,m) { - var z; - if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); - return this.exp(e,z); - } - - // protected - BigInteger.prototype.copyTo = bnpCopyTo; - BigInteger.prototype.fromInt = bnpFromInt; - BigInteger.prototype.fromString = bnpFromString; - BigInteger.prototype.clamp = bnpClamp; - BigInteger.prototype.dlShiftTo = bnpDLShiftTo; - BigInteger.prototype.drShiftTo = bnpDRShiftTo; - BigInteger.prototype.lShiftTo = bnpLShiftTo; - BigInteger.prototype.rShiftTo = bnpRShiftTo; - BigInteger.prototype.subTo = bnpSubTo; - BigInteger.prototype.multiplyTo = bnpMultiplyTo; - BigInteger.prototype.squareTo = bnpSquareTo; - BigInteger.prototype.divRemTo = bnpDivRemTo; - BigInteger.prototype.invDigit = bnpInvDigit; - BigInteger.prototype.isEven = bnpIsEven; - BigInteger.prototype.exp = bnpExp; - - // public - BigInteger.prototype.toString = bnToString; - BigInteger.prototype.negate = bnNegate; - BigInteger.prototype.abs = bnAbs; - BigInteger.prototype.compareTo = bnCompareTo; - BigInteger.prototype.bitLength = bnBitLength; - BigInteger.prototype.mod = bnMod; - BigInteger.prototype.modPowInt = bnModPowInt; - - // "constants" - BigInteger.ZERO = nbv(0); - BigInteger.ONE = nbv(1); - - // Copyright (c) 2005-2009 Tom Wu - // All Rights Reserved. - // See "LICENSE" for details. - - // Extended JavaScript BN functions, required for RSA private ops. - - // Version 1.1: new BigInteger("0", 10) returns "proper" zero - // Version 1.2: square() API, isProbablePrime fix - - // (public) - function bnClone() { var r = nbi(); this.copyTo(r); return r; } - - // (public) return value as integer - function bnIntValue() { - if(this.s < 0) { - if(this.t == 1) return this[0]-this.DV; - else if(this.t == 0) return -1; - } - else if(this.t == 1) return this[0]; - else if(this.t == 0) return 0; - // assumes 16 < DB < 32 - return ((this[1]&((1<<(32-this.DB))-1))<>24; } - - // (public) return value as short (assumes DB>=16) - function bnShortValue() { return (this.t==0)?this.s:(this[0]<<16)>>16; } - - // (protected) return x s.t. r^x < DV - function bnpChunkSize(r) { return Math.floor(Math.LN2*this.DB/Math.log(r)); } - - // (public) 0 if this == 0, 1 if this > 0 - function bnSigNum() { - if(this.s < 0) return -1; - else if(this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; - else return 1; - } - - // (protected) convert to radix string - function bnpToRadix(b) { - if(b == null) b = 10; - if(this.signum() == 0 || b < 2 || b > 36) return "0"; - var cs = this.chunkSize(b); - var a = Math.pow(b,cs); - var d = nbv(a), y = nbi(), z = nbi(), r = ""; - this.divRemTo(d,y,z); - while(y.signum() > 0) { - r = (a+z.intValue()).toString(b).substr(1) + r; - y.divRemTo(d,y,z); - } - return z.intValue().toString(b) + r; - } - - // (protected) convert from radix string - function bnpFromRadix(s,b) { - this.fromInt(0); - if(b == null) b = 10; - var cs = this.chunkSize(b); - var d = Math.pow(b,cs), mi = false, j = 0, w = 0; - for(var i = 0; i < s.length; ++i) { - var x = intAt(s,i); - if(x < 0) { - if(s.charAt(i) == "-" && this.signum() == 0) mi = true; - continue; - } - w = b*w+x; - if(++j >= cs) { - this.dMultiply(d); - this.dAddOffset(w,0); - j = 0; - w = 0; - } - } - if(j > 0) { - this.dMultiply(Math.pow(b,j)); - this.dAddOffset(w,0); - } - if(mi) BigInteger.ZERO.subTo(this,this); - } - - // (protected) alternate constructor - function bnpFromNumber(a,b,c) { - if("number" == typeof b) { - // new BigInteger(int,int,RNG) - if(a < 2) this.fromInt(1); - else { - this.fromNumber(a,c); - if(!this.testBit(a-1)) // force MSB set - this.bitwiseTo(BigInteger.ONE.shiftLeft(a-1),op_or,this); - if(this.isEven()) this.dAddOffset(1,0); // force odd - while(!this.isProbablePrime(b)) { - this.dAddOffset(2,0); - if(this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a-1),this); - } - } - } - else { - // new BigInteger(int,RNG) - var x = new Array(), t = a&7; - x.length = (a>>3)+1; - b.nextBytes(x); - if(t > 0) x[0] &= ((1< 0) { - if(p < this.DB && (d = this[i]>>p) != (this.s&this.DM)>>p) - r[k++] = d|(this.s<<(this.DB-p)); - while(i >= 0) { - if(p < 8) { - d = (this[i]&((1<>(p+=this.DB-8); - } - else { - d = (this[i]>>(p-=8))&0xff; - if(p <= 0) { p += this.DB; --i; } - } - if((d&0x80) != 0) d |= -256; - if(k == 0 && (this.s&0x80) != (d&0x80)) ++k; - if(k > 0 || d != this.s) r[k++] = d; - } - } - return r; - } - - function bnEquals(a) { return(this.compareTo(a)==0); } - function bnMin(a) { return(this.compareTo(a)<0)?this:a; } - function bnMax(a) { return(this.compareTo(a)>0)?this:a; } - - // (protected) r = this op a (bitwise) - function bnpBitwiseTo(a,op,r) { - var i, f, m = Math.min(a.t,this.t); - for(i = 0; i < m; ++i) r[i] = op(this[i],a[i]); - if(a.t < this.t) { - f = a.s&this.DM; - for(i = m; i < this.t; ++i) r[i] = op(this[i],f); - r.t = this.t; - } - else { - f = this.s&this.DM; - for(i = m; i < a.t; ++i) r[i] = op(f,a[i]); - r.t = a.t; - } - r.s = op(this.s,a.s); - r.clamp(); - } - - // (public) this & a - function op_and(x,y) { return x&y; } - function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; } - - // (public) this | a - function op_or(x,y) { return x|y; } - function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; } - - // (public) this ^ a - function op_xor(x,y) { return x^y; } - function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; } - - // (public) this & ~a - function op_andnot(x,y) { return x&~y; } - function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; } - - // (public) ~this - function bnNot() { - var r = nbi(); - for(var i = 0; i < this.t; ++i) r[i] = this.DM&~this[i]; - r.t = this.t; - r.s = ~this.s; - return r; - } - - // (public) this << n - function bnShiftLeft(n) { - var r = nbi(); - if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r); - return r; - } - - // (public) this >> n - function bnShiftRight(n) { - var r = nbi(); - if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r); - return r; - } - - // return index of lowest 1-bit in x, x < 2^31 - function lbit(x) { - if(x == 0) return -1; - var r = 0; - if((x&0xffff) == 0) { x >>= 16; r += 16; } - if((x&0xff) == 0) { x >>= 8; r += 8; } - if((x&0xf) == 0) { x >>= 4; r += 4; } - if((x&3) == 0) { x >>= 2; r += 2; } - if((x&1) == 0) ++r; - return r; - } - - // (public) returns index of lowest 1-bit (or -1 if none) - function bnGetLowestSetBit() { - for(var i = 0; i < this.t; ++i) - if(this[i] != 0) return i*this.DB+lbit(this[i]); - if(this.s < 0) return this.t*this.DB; - return -1; - } - - // return number of 1 bits in x - function cbit(x) { - var r = 0; - while(x != 0) { x &= x-1; ++r; } - return r; - } - - // (public) return number of set bits - function bnBitCount() { - var r = 0, x = this.s&this.DM; - for(var i = 0; i < this.t; ++i) r += cbit(this[i]^x); - return r; - } - - // (public) true iff nth bit is set - function bnTestBit(n) { - var j = Math.floor(n/this.DB); - if(j >= this.t) return(this.s!=0); - return((this[j]&(1<<(n%this.DB)))!=0); - } - - // (protected) this op (1<>= this.DB; - } - if(a.t < this.t) { - c += a.s; - while(i < this.t) { - c += this[i]; - r[i++] = c&this.DM; - c >>= this.DB; - } - c += this.s; - } - else { - c += this.s; - while(i < a.t) { - c += a[i]; - r[i++] = c&this.DM; - c >>= this.DB; - } - c += a.s; - } - r.s = (c<0)?-1:0; - if(c > 0) r[i++] = c; - else if(c < -1) r[i++] = this.DV+c; - r.t = i; - r.clamp(); - } - - // (public) this + a - function bnAdd(a) { var r = nbi(); this.addTo(a,r); return r; } - - // (public) this - a - function bnSubtract(a) { var r = nbi(); this.subTo(a,r); return r; } - - // (public) this * a - function bnMultiply(a) { var r = nbi(); this.multiplyTo(a,r); return r; } - - // (public) this^2 - function bnSquare() { var r = nbi(); this.squareTo(r); return r; } - - // (public) this / a - function bnDivide(a) { var r = nbi(); this.divRemTo(a,r,null); return r; } - - // (public) this % a - function bnRemainder(a) { var r = nbi(); this.divRemTo(a,null,r); return r; } - - // (public) [this/a,this%a] - function bnDivideAndRemainder(a) { - var q = nbi(), r = nbi(); - this.divRemTo(a,q,r); - return new Array(q,r); - } - - // (protected) this *= n, this >= 0, 1 < n < DV - function bnpDMultiply(n) { - this[this.t] = this.am(0,n-1,this,0,0,this.t); - ++this.t; - this.clamp(); - } - - // (protected) this += n << w words, this >= 0 - function bnpDAddOffset(n,w) { - if(n == 0) return; - while(this.t <= w) this[this.t++] = 0; - this[w] += n; - while(this[w] >= this.DV) { - this[w] -= this.DV; - if(++w >= this.t) this[this.t++] = 0; - ++this[w]; - } - } - - // A "null" reducer - function NullExp() {} - function nNop(x) { return x; } - function nMulTo(x,y,r) { x.multiplyTo(y,r); } - function nSqrTo(x,r) { x.squareTo(r); } - - NullExp.prototype.convert = nNop; - NullExp.prototype.revert = nNop; - NullExp.prototype.mulTo = nMulTo; - NullExp.prototype.sqrTo = nSqrTo; - - // (public) this^e - function bnPow(e) { return this.exp(e,new NullExp()); } - - // (protected) r = lower n words of "this * a", a.t <= n - // "this" should be the larger one if appropriate. - function bnpMultiplyLowerTo(a,n,r) { - var i = Math.min(this.t+a.t,n); - r.s = 0; // assumes a,this >= 0 - r.t = i; - while(i > 0) r[--i] = 0; - var j; - for(j = r.t-this.t; i < j; ++i) r[i+this.t] = this.am(0,a[i],r,i,0,this.t); - for(j = Math.min(a.t,n); i < j; ++i) this.am(0,a[i],r,i,0,n-i); - r.clamp(); - } - - // (protected) r = "this * a" without lower n words, n > 0 - // "this" should be the larger one if appropriate. - function bnpMultiplyUpperTo(a,n,r) { - --n; - var i = r.t = this.t+a.t-n; - r.s = 0; // assumes a,this >= 0 - while(--i >= 0) r[i] = 0; - for(i = Math.max(n-this.t,0); i < a.t; ++i) - r[this.t+i-n] = this.am(n-i,a[i],r,0,0,this.t+i-n); - r.clamp(); - r.drShiftTo(1,r); - } - - // Barrett modular reduction - function Barrett(m) { - // setup Barrett - this.r2 = nbi(); - this.q3 = nbi(); - BigInteger.ONE.dlShiftTo(2*m.t,this.r2); - this.mu = this.r2.divide(m); - this.m = m; - } - - function barrettConvert(x) { - if(x.s < 0 || x.t > 2*this.m.t) return x.mod(this.m); - else if(x.compareTo(this.m) < 0) return x; - else { var r = nbi(); x.copyTo(r); this.reduce(r); return r; } - } - - function barrettRevert(x) { return x; } - - // x = x mod m (HAC 14.42) - function barrettReduce(x) { - x.drShiftTo(this.m.t-1,this.r2); - if(x.t > this.m.t+1) { x.t = this.m.t+1; x.clamp(); } - this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3); - this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2); - while(x.compareTo(this.r2) < 0) x.dAddOffset(1,this.m.t+1); - x.subTo(this.r2,x); - while(x.compareTo(this.m) >= 0) x.subTo(this.m,x); - } - - // r = x^2 mod m; x != r - function barrettSqrTo(x,r) { x.squareTo(r); this.reduce(r); } - - // r = x*y mod m; x,y != r - function barrettMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } - - Barrett.prototype.convert = barrettConvert; - Barrett.prototype.revert = barrettRevert; - Barrett.prototype.reduce = barrettReduce; - Barrett.prototype.mulTo = barrettMulTo; - Barrett.prototype.sqrTo = barrettSqrTo; - - // (public) this^e % m (HAC 14.85) - function bnModPow(e,m) { - var i = e.bitLength(), k, r = nbv(1), z; - if(i <= 0) return r; - else if(i < 18) k = 1; - else if(i < 48) k = 3; - else if(i < 144) k = 4; - else if(i < 768) k = 5; - else k = 6; - if(i < 8) - z = new Classic(m); - else if(m.isEven()) - z = new Barrett(m); - else - z = new Montgomery(m); - - // precomputation - var g = new Array(), n = 3, k1 = k-1, km = (1< 1) { - var g2 = nbi(); - z.sqrTo(g[1],g2); - while(n <= km) { - g[n] = nbi(); - z.mulTo(g2,g[n-2],g[n]); - n += 2; - } - } - - var j = e.t-1, w, is1 = true, r2 = nbi(), t; - i = nbits(e[j])-1; - while(j >= 0) { - if(i >= k1) w = (e[j]>>(i-k1))&km; - else { - w = (e[j]&((1<<(i+1))-1))<<(k1-i); - if(j > 0) w |= e[j-1]>>(this.DB+i-k1); - } - - n = k; - while((w&1) == 0) { w >>= 1; --n; } - if((i -= n) < 0) { i += this.DB; --j; } - if(is1) { // ret == 1, don't bother squaring or multiplying it - g[w].copyTo(r); - is1 = false; - } - else { - while(n > 1) { z.sqrTo(r,r2); z.sqrTo(r2,r); n -= 2; } - if(n > 0) z.sqrTo(r,r2); else { t = r; r = r2; r2 = t; } - z.mulTo(r2,g[w],r); - } - - while(j >= 0 && (e[j]&(1< 0) { - x.rShiftTo(g,x); - y.rShiftTo(g,y); - } - while(x.signum() > 0) { - if((i = x.getLowestSetBit()) > 0) x.rShiftTo(i,x); - if((i = y.getLowestSetBit()) > 0) y.rShiftTo(i,y); - if(x.compareTo(y) >= 0) { - x.subTo(y,x); - x.rShiftTo(1,x); - } - else { - y.subTo(x,y); - y.rShiftTo(1,y); - } - } - if(g > 0) y.lShiftTo(g,y); - return y; - } - - // (protected) this % n, n < 2^26 - function bnpModInt(n) { - if(n <= 0) return 0; - var d = this.DV%n, r = (this.s<0)?n-1:0; - if(this.t > 0) - if(d == 0) r = this[0]%n; - else for(var i = this.t-1; i >= 0; --i) r = (d*r+this[i])%n; - return r; - } - - // (public) 1/this % m (HAC 14.61) - function bnModInverse(m) { - var ac = m.isEven(); - if((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; - var u = m.clone(), v = this.clone(); - var a = nbv(1), b = nbv(0), c = nbv(0), d = nbv(1); - while(u.signum() != 0) { - while(u.isEven()) { - u.rShiftTo(1,u); - if(ac) { - if(!a.isEven() || !b.isEven()) { a.addTo(this,a); b.subTo(m,b); } - a.rShiftTo(1,a); - } - else if(!b.isEven()) b.subTo(m,b); - b.rShiftTo(1,b); - } - while(v.isEven()) { - v.rShiftTo(1,v); - if(ac) { - if(!c.isEven() || !d.isEven()) { c.addTo(this,c); d.subTo(m,d); } - c.rShiftTo(1,c); - } - else if(!d.isEven()) d.subTo(m,d); - d.rShiftTo(1,d); - } - if(u.compareTo(v) >= 0) { - u.subTo(v,u); - if(ac) a.subTo(c,a); - b.subTo(d,b); - } - else { - v.subTo(u,v); - if(ac) c.subTo(a,c); - d.subTo(b,d); - } - } - if(v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; - if(d.compareTo(m) >= 0) return d.subtract(m); - if(d.signum() < 0) d.addTo(m,d); else return d; - if(d.signum() < 0) return d.add(m); else return d; - } - - var lowprimes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997]; - var lplim = (1<<26)/lowprimes[lowprimes.length-1]; - - // (public) test primality with certainty >= 1-.5^t - function bnIsProbablePrime(t) { - var i, x = this.abs(); - if(x.t == 1 && x[0] <= lowprimes[lowprimes.length-1]) { - for(i = 0; i < lowprimes.length; ++i) - if(x[0] == lowprimes[i]) return true; - return false; - } - if(x.isEven()) return false; - i = 1; - while(i < lowprimes.length) { - var m = lowprimes[i], j = i+1; - while(j < lowprimes.length && m < lplim) m *= lowprimes[j++]; - m = x.modInt(m); - while(i < j) if(m%lowprimes[i++] == 0) return false; - } - return x.millerRabin(t); - } - - // (protected) true if probably prime (HAC 4.24, Miller-Rabin) - function bnpMillerRabin(t) { - var n1 = this.subtract(BigInteger.ONE); - var k = n1.getLowestSetBit(); - if(k <= 0) return false; - var r = n1.shiftRight(k); - t = (t+1)>>1; - if(t > lowprimes.length) t = lowprimes.length; - var a = nbi(); - for(var i = 0; i < t; ++i) { - //Pick bases at random, instead of starting at 2 - a.fromInt(lowprimes[Math.floor(Math.random()*lowprimes.length)]); - var y = a.modPow(r,this); - if(y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { - var j = 1; - while(j++ < k && y.compareTo(n1) != 0) { - y = y.modPowInt(2,this); - if(y.compareTo(BigInteger.ONE) == 0) return false; - } - if(y.compareTo(n1) != 0) return false; - } - } - return true; - } - - // protected - BigInteger.prototype.chunkSize = bnpChunkSize; - BigInteger.prototype.toRadix = bnpToRadix; - BigInteger.prototype.fromRadix = bnpFromRadix; - BigInteger.prototype.fromNumber = bnpFromNumber; - BigInteger.prototype.bitwiseTo = bnpBitwiseTo; - BigInteger.prototype.changeBit = bnpChangeBit; - BigInteger.prototype.addTo = bnpAddTo; - BigInteger.prototype.dMultiply = bnpDMultiply; - BigInteger.prototype.dAddOffset = bnpDAddOffset; - BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; - BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; - BigInteger.prototype.modInt = bnpModInt; - BigInteger.prototype.millerRabin = bnpMillerRabin; - - // public - BigInteger.prototype.clone = bnClone; - BigInteger.prototype.intValue = bnIntValue; - BigInteger.prototype.byteValue = bnByteValue; - BigInteger.prototype.shortValue = bnShortValue; - BigInteger.prototype.signum = bnSigNum; - BigInteger.prototype.toByteArray = bnToByteArray; - BigInteger.prototype.equals = bnEquals; - BigInteger.prototype.min = bnMin; - BigInteger.prototype.max = bnMax; - BigInteger.prototype.and = bnAnd; - BigInteger.prototype.or = bnOr; - BigInteger.prototype.xor = bnXor; - BigInteger.prototype.andNot = bnAndNot; - BigInteger.prototype.not = bnNot; - BigInteger.prototype.shiftLeft = bnShiftLeft; - BigInteger.prototype.shiftRight = bnShiftRight; - BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; - BigInteger.prototype.bitCount = bnBitCount; - BigInteger.prototype.testBit = bnTestBit; - BigInteger.prototype.setBit = bnSetBit; - BigInteger.prototype.clearBit = bnClearBit; - BigInteger.prototype.flipBit = bnFlipBit; - BigInteger.prototype.add = bnAdd; - BigInteger.prototype.subtract = bnSubtract; - BigInteger.prototype.multiply = bnMultiply; - BigInteger.prototype.divide = bnDivide; - BigInteger.prototype.remainder = bnRemainder; - BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; - BigInteger.prototype.modPow = bnModPow; - BigInteger.prototype.modInverse = bnModInverse; - BigInteger.prototype.pow = bnPow; - BigInteger.prototype.gcd = bnGCD; - BigInteger.prototype.isProbablePrime = bnIsProbablePrime; - - // JSBN-specific extension - BigInteger.prototype.square = bnSquare; - - // Expose the Barrett function - BigInteger.prototype.Barrett = Barrett - - // BigInteger interfaces not implemented in jsbn: - - // BigInteger(int signum, byte[] magnitude) - // double doubleValue() - // float floatValue() - // int hashCode() - // long longValue() - // static BigInteger valueOf(long val) - - // Random number generator - requires a PRNG backend, e.g. prng4.js - - // For best results, put code like - // - // in your main HTML document. - - var rng_state; - var rng_pool; - var rng_pptr; - - // Mix in a 32-bit integer into the pool - function rng_seed_int(x) { - rng_pool[rng_pptr++] ^= x & 255; - rng_pool[rng_pptr++] ^= (x >> 8) & 255; - rng_pool[rng_pptr++] ^= (x >> 16) & 255; - rng_pool[rng_pptr++] ^= (x >> 24) & 255; - if(rng_pptr >= rng_psize) rng_pptr -= rng_psize; - } - - // Mix in the current time (w/milliseconds) into the pool - function rng_seed_time() { - rng_seed_int(new Date().getTime()); - } - - // Initialize the pool with junk if needed. - if(rng_pool == null) { - rng_pool = new Array(); - rng_pptr = 0; - var t; - if(typeof window !== "undefined" && window.crypto) { - if (window.crypto.getRandomValues) { - // Use webcrypto if available - var ua = new Uint8Array(32); - window.crypto.getRandomValues(ua); - for(t = 0; t < 32; ++t) - rng_pool[rng_pptr++] = ua[t]; - } - else if(navigator.appName == "Netscape" && navigator.appVersion < "5") { - // Extract entropy (256 bits) from NS4 RNG if available - var z = window.crypto.random(32); - for(t = 0; t < z.length; ++t) - rng_pool[rng_pptr++] = z.charCodeAt(t) & 255; - } - } - while(rng_pptr < rng_psize) { // extract some randomness from Math.random() - t = Math.floor(65536 * Math.random()); - rng_pool[rng_pptr++] = t >>> 8; - rng_pool[rng_pptr++] = t & 255; - } - rng_pptr = 0; - rng_seed_time(); - //rng_seed_int(window.screenX); - //rng_seed_int(window.screenY); - } - - function rng_get_byte() { - if(rng_state == null) { - rng_seed_time(); - rng_state = prng_newstate(); - rng_state.init(rng_pool); - for(rng_pptr = 0; rng_pptr < rng_pool.length; ++rng_pptr) - rng_pool[rng_pptr] = 0; - rng_pptr = 0; - //rng_pool = null; - } - // TODO: allow reseeding after first request - return rng_state.next(); - } - - function rng_get_bytes(ba) { - var i; - for(i = 0; i < ba.length; ++i) ba[i] = rng_get_byte(); - } - - function SecureRandom() {} - - SecureRandom.prototype.nextBytes = rng_get_bytes; - - // prng4.js - uses Arcfour as a PRNG - - function Arcfour() { - this.i = 0; - this.j = 0; - this.S = new Array(); - } - - // Initialize arcfour context from key, an array of ints, each from [0..255] - function ARC4init(key) { - var i, j, t; - for(i = 0; i < 256; ++i) - this.S[i] = i; - j = 0; - for(i = 0; i < 256; ++i) { - j = (j + this.S[i] + key[i % key.length]) & 255; - t = this.S[i]; - this.S[i] = this.S[j]; - this.S[j] = t; - } - this.i = 0; - this.j = 0; - } - - function ARC4next() { - var t; - this.i = (this.i + 1) & 255; - this.j = (this.j + this.S[this.i]) & 255; - t = this.S[this.i]; - this.S[this.i] = this.S[this.j]; - this.S[this.j] = t; - return this.S[(t + this.S[this.i]) & 255]; - } - - Arcfour.prototype.init = ARC4init; - Arcfour.prototype.next = ARC4next; - - // Plug in your RNG constructor here - function prng_newstate() { - return new Arcfour(); - } - - // Pool size must be a multiple of 4 and greater than 32. - // An array of bytes the size of the pool will be passed to init() - var rng_psize = 256; - - if (typeof exports !== 'undefined') { - exports = module.exports = { - BigInteger: BigInteger, - SecureRandom: SecureRandom, - }; - } else { - this.BigInteger = BigInteger; - this.SecureRandom = SecureRandom; - } - -}).call(this); diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/jsbn/package.json b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/jsbn/package.json deleted file mode 100644 index 0113b456..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/jsbn/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "jsbn", - "version": "0.1.0", - "description": "The jsbn library is a fast, portable implementation of large-number math in pure JavaScript, enabling public-key crypto and other applications on desktop and mobile browsers.", - "main": "index.js", - "scripts": { - "test": "mocha test.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/andyperlitch/jsbn.git" - }, - "keywords": [ - "biginteger", - "bignumber", - "big", - "integer" - ], - "author": { - "name": "Tom Wu" - }, - "license": "BSD", - "gitHead": "148a967b112806e63ddeeed78ee7938eef74c84a", - "bugs": { - "url": "https://github.com/andyperlitch/jsbn/issues" - }, - "homepage": "https://github.com/andyperlitch/jsbn", - "_id": "jsbn@0.1.0", - "_shasum": "650987da0dd74f4ebf5a11377a2aa2d273e97dfd", - "_from": "jsbn@>=0.1.0 <0.2.0", - "_npmVersion": "2.7.4", - "_nodeVersion": "0.12.2", - "_npmUser": { - "name": "andyperlitch", - "email": "andyperlitch@gmail.com" - }, - "dist": { - "shasum": "650987da0dd74f4ebf5a11377a2aa2d273e97dfd", - "tarball": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" - }, - "maintainers": [ - { - "name": "andyperlitch", - "email": "andyperlitch@gmail.com" - } - ], - "directories": {}, - "_resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/.npmignore b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/.npmignore deleted file mode 100644 index 7d98dcbd..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -.eslintrc -.travis.yml -bower.json -test diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/CHANGELOG.md b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/CHANGELOG.md deleted file mode 100644 index 77c69bd5..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/CHANGELOG.md +++ /dev/null @@ -1,128 +0,0 @@ -TweetNaCl.js Changelog -====================== - - -v0.13.2 -------- - -* Fixed undefined variable bug in fast version of Poly1305. No worries, this - bug was *never* triggered. - -* Specified CC0 public domain dedication. - -* Updated development dependencies. - - -v0.13.1 -------- - -* Exclude `crypto` and `buffer` modules from browserify builds. - - -v0.13.0 -------- - -* Made `nacl-fast` the default version in NPM package. Now - `require("tweetnacl")` will use fast version; to get the original version, - use `require("tweetnacl/nacl.js")`. - -* Cleanup temporary array after generating random bytes. - - -v0.12.2 -------- - -* Improved performance of curve operations, making `nacl.scalarMult`, `nacl.box`, - `nacl.sign` and related functions up to 3x faster in `nacl-fast` version. - - -v0.12.1 -------- - -* Significantly improved performance of Salsa20 (~1.5x faster) and - Poly1305 (~3.5x faster) in `nacl-fast` version. - - -v0.12.0 -------- - -* Instead of using the given secret key directly, TweetNaCl.js now copies it to - a new array in `nacl.box.keyPair.fromSecretKey` and - `nacl.sign.keyPair.fromSecretKey`. - - -v0.11.2 -------- - -* Added new constant: `nacl.sign.seedLength`. - - -v0.11.1 -------- - -* Even faster hash for both short and long inputs (in `nacl-fast`). - - -v0.11.0 -------- - -* Implement `nacl.sign.keyPair.fromSeed` to enable creation of sign key pairs - deterministically from a 32-byte seed. (It behaves like - [libsodium's](http://doc.libsodium.org/public-key_cryptography/public-key_signatures.html) - `crypto_sign_seed_keypair`: the seed becomes a secret part of the secret key.) - -* Fast version now has an improved hash implementation that is 2x-5x faster. - -* Fixed benchmarks, which may have produced incorrect measurements. - - -v0.10.1 -------- - -* Exported undocumented `nacl.lowlevel.crypto_core_hsalsa20`. - - -v0.10.0 -------- - -* **Signature API breaking change!** `nacl.sign` and `nacl.sign.open` now deal - with signed messages, and new `nacl.sign.detached` and - `nacl.sign.detached.verify` are available. - - Previously, `nacl.sign` returned a signature, and `nacl.sign.open` accepted a - message and "detached" signature. This was unlike NaCl's API, which dealt with - signed messages (concatenation of signature and message). - - The new API is: - - nacl.sign(message, secretKey) -> signedMessage - nacl.sign.open(signedMessage, publicKey) -> message | null - - Since detached signatures are common, two new API functions were introduced: - - nacl.sign.detached(message, secretKey) -> signature - nacl.sign.detached.verify(message, signature, publicKey) -> true | false - - (Note that it's `verify`, not `open`, and it returns a boolean value, unlike - `open`, which returns an "unsigned" message.) - -* NPM package now comes without `test` directory to keep it small. - - -v0.9.2 ------- - -* Improved documentation. -* Fast version: increased theoretical message size limit from 2^32-1 to 2^52 - bytes in Poly1305 (and thus, secretbox and box). However this has no impact - in practice since JavaScript arrays or ArrayBuffers are limited to 32-bit - indexes, and most implementations won't allocate more than a gigabyte or so. - (Obviously, there are no tests for the correctness of implementation.) Also, - it's not recommended to use messages that large without splitting them into - smaller packets anyway. - - -v0.9.1 ------- - -* Initial release diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/README.md b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/README.md deleted file mode 100644 index 11bd3472..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/README.md +++ /dev/null @@ -1,463 +0,0 @@ -TweetNaCl.js -============ - -Port of [TweetNaCl](http://tweetnacl.cr.yp.to) / [NaCl](http://nacl.cr.yp.to/) -to JavaScript for modern browsers and Node.js. Public domain. - -[![Build Status](https://travis-ci.org/dchest/tweetnacl-js.svg?branch=master) -](https://travis-ci.org/dchest/tweetnacl-js) - -[Demo](https://dchest.github.io/tweetnacl-js/) - -**:warning: Beta version. The library is stable and API is frozen, however -it has not been independently reviewed. If you can help reviewing it, please -[contact me](mailto:dmitry@codingrobots.com).** - -Documentation -============= - -* [Overview](#overview) -* [Installation](#installation) -* [Usage](#usage) - * [Public-key authenticated encryption (box)](#public-key-authenticated-encryption-box) - * [Secret-key authenticated encryption (secretbox)](#secret-key-authenticated-encryption-secretbox) - * [Scalar multiplication](#scalar-multiplication) - * [Signatures](#signatures) - * [Hashing](#hashing) - * [Random bytes generation](#random-bytes-generation) - * [Constant-time comparison](#constant-time-comparison) - * [Utilities](#utilities) -* [Examples](#examples) -* [System requirements](#system-requirements) -* [Development and testing](#development-and-testing) -* [Contributors](#contributors) -* [Who uses it](#who-uses-it) - - -Overview --------- - -The primary goal of this project is to produce a translation of TweetNaCl to -JavaScript which is as close as possible to the original C implementation, plus -a thin layer of idiomatic high-level API on top of it. - -There are two versions, you can use either of them: - -* `nacl.js` is the port of TweetNaCl with minimum differences from the - original + high-level API. - -* `nacl-fast.js` is like `nacl.js`, but with some functions replaced with - faster versions. - - -Installation ------------- - -You can install TweetNaCl.js via a package manager: - -[Bower](http://bower.io): - - $ bower install tweetnacl - -[NPM](https://www.npmjs.org/): - - $ npm install tweetnacl - -or [download source code](https://github.com/dchest/tweetnacl-js/releases). - - -Usage ------- - -All API functions accept and return bytes as `Uint8Array`s. If you need to -encode or decode strings, use functions from `nacl.util` namespace. - -### Public-key authenticated encryption (box) - -Implements *curve25519-xsalsa20-poly1305*. - -#### nacl.box.keyPair() - -Generates a new random key pair for box and returns it as an object with -`publicKey` and `secretKey` members: - - { - publicKey: ..., // Uint8Array with 32-byte public key - secretKey: ... // Uint8Array with 32-byte secret key - } - - -#### nacl.box.keyPair.fromSecretKey(secretKey) - -Returns a key pair for box with public key corresponding to the given secret -key. - -#### nacl.box(message, nonce, theirPublicKey, mySecretKey) - -Encrypt and authenticates message using peer's public key, our secret key, and -the given nonce, which must be unique for each distinct message for a key pair. - -Returns an encrypted and authenticated message, which is -`nacl.box.overheadLength` longer than the original message. - -#### nacl.box.open(box, nonce, theirPublicKey, mySecretKey) - -Authenticates and decrypts the given box with peer's public key, our secret -key, and the given nonce. - -Returns the original message, or `false` if authentication fails. - -#### nacl.box.before(theirPublicKey, mySecretKey) - -Returns a precomputed shared key which can be used in `nacl.box.after` and -`nacl.box.open.after`. - -#### nacl.box.after(message, nonce, sharedKey) - -Same as `nacl.box`, but uses a shared key precomputed with `nacl.box.before`. - -#### nacl.box.open.after(box, nonce, sharedKey) - -Same as `nacl.box.open`, but uses a shared key precomputed with `nacl.box.before`. - -#### nacl.box.publicKeyLength = 32 - -Length of public key in bytes. - -#### nacl.box.secretKeyLength = 32 - -Length of secret key in bytes. - -#### nacl.box.sharedKeyLength = 32 - -Length of precomputed shared key in bytes. - -#### nacl.box.nonceLength = 24 - -Length of nonce in bytes. - -#### nacl.box.overheadLength = 16 - -Length of overhead added to box compared to original message. - - -### Secret-key authenticated encryption (secretbox) - -Implements *xsalsa20-poly1305*. - -#### nacl.secretbox(message, nonce, key) - -Encrypt and authenticates message using the key and the nonce. The nonce must -be unique for each distinct message for this key. - -Returns an encrypted and authenticated message, which is -`nacl.secretbox.overheadLength` longer than the original message. - -#### nacl.secretbox.open(box, nonce, key) - -Authenticates and decrypts the given secret box using the key and the nonce. - -Returns the original message, or `false` if authentication fails. - -#### nacl.secretbox.keyLength = 32 - -Length of key in bytes. - -#### nacl.secretbox.nonceLength = 24 - -Length of nonce in bytes. - -#### nacl.secretbox.overheadLength = 16 - -Length of overhead added to secret box compared to original message. - - -### Scalar multiplication - -Implements *curve25519*. - -#### nacl.scalarMult(n, p) - -Multiplies an integer `n` by a group element `p` and returns the resulting -group element. - -#### nacl.scalarMult.base(n) - -Multiplies an integer `n` by a standard group element and returns the resulting -group element. - -#### nacl.scalarMult.scalarLength = 32 - -Length of scalar in bytes. - -#### nacl.scalarMult.groupElementLength = 32 - -Length of group element in bytes. - - -### Signatures - -Implements [ed25519](http://ed25519.cr.yp.to). - -#### nacl.sign.keyPair() - -Generates new random key pair for signing and returns it as an object with -`publicKey` and `secretKey` members: - - { - publicKey: ..., // Uint8Array with 32-byte public key - secretKey: ... // Uint8Array with 64-byte secret key - } - -#### nacl.sign.keyPair.fromSecretKey(secretKey) - -Returns a signing key pair with public key corresponding to the given -64-byte secret key. The secret key must have been generated by -`nacl.sign.keyPair` or `nacl.sign.keyPair.fromSeed`. - -#### nacl.sign.keyPair.fromSeed(seed) - -Returns a new signing key pair generated deterministically from a 32-byte seed. -The seed must contain enough entropy to be secure. This method is not -recommended for general use: instead, use `nacl.sign.keyPair` to generate a new -key pair from a random seed. - -#### nacl.sign(message, secretKey) - -Signs the message using the secret key and returns a signed message. - -#### nacl.sign.open(signedMessage, publicKey) - -Verifies the signed message and returns the message without signature. - -Returns `null` if verification failed. - -#### nacl.sign.detached(message, secretKey) - -Signs the message using the secret key and returns a signature. - -#### nacl.sign.detached.verify(message, signature, publicKey) - -Verifies the signature for the message and returns `true` if verification -succeeded or `false` if it failed. - -#### nacl.sign.publicKeyLength = 32 - -Length of signing public key in bytes. - -#### nacl.sign.secretKeyLength = 64 - -Length of signing secret key in bytes. - -#### nacl.sign.seedLength = 32 - -Length of seed for `nacl.sign.keyPair.fromSeed` in bytes. - -#### nacl.sign.signatureLength = 64 - -Length of signature in bytes. - - -### Hashing - -Implements *SHA-512*. - -#### nacl.hash(message) - -Returns SHA-512 hash of the message. - -#### nacl.hash.hashLength = 64 - -Length of hash in bytes. - - -### Random bytes generation - -#### nacl.randomBytes(length) - -Returns a `Uint8Array` of the given length containing random bytes of -cryptographic quality. - -**Implementation note** - -TweetNaCl.js uses the following methods to generate random bytes, -depending on the platform it runs on: - -* `window.crypto.getRandomValues` (WebCrypto standard) -* `window.msCrypto.getRandomValues` (Internet Explorer 11) -* `crypto.randomBytes` (Node.js) - -Note that browsers are required to throw `QuotaExceededError` exception if -requested `length` is more than 65536, so do not ask for more than 65536 bytes -in *one call* (multiple calls to get as many bytes as you like are okay: -browsers can generate infinite amount of random bytes without any bad -consequences). - -If the platform doesn't provide a suitable PRNG, the following functions, -which require random numbers, will throw exception: - -* `nacl.randomBytes` -* `nacl.box.keyPair` -* `nacl.sign.keyPair` - -Other functions are deterministic and will continue working. - -If a platform you are targeting doesn't implement secure random number -generator, but you somehow have a cryptographically-strong source of entropy -(not `Math.random`!), and you know what you are doing, you can plug it into -TweetNaCl.js like this: - - nacl.setPRNG(function(x, n) { - // ... copy n random bytes into x ... - }); - -Note that `nacl.setPRNG` *completely replaces* internal random byte generator -with the one provided. - - -### Constant-time comparison - -#### nacl.verify(x, y) - -Compares `x` and `y` in constant time and returns `true` if their lengths are -non-zero and equal, and their contents are equal. - -Returns `false` if either of the arguments has zero length, or arguments have -different lengths, or their contents differ. - - -### Utilities - -Encoding/decoding functions are provided for convenience. They are correct, -however their performance and wide compatibility with uncommon runtimes is not -something that is considered important compared to the simplicity and size of -implementation. You can use third-party libraries if you need to. - -#### nacl.util.decodeUTF8(string) - -Decodes string and returns `Uint8Array` of bytes. - -#### nacl.util.encodeUTF8(array) - -Encodes `Uint8Array` or `Array` of bytes into string. - -#### nacl.util.decodeBase64(string) - -Decodes Base-64 encoded string and returns `Uint8Array` of bytes. - -#### nacl.util.encodeBase64(array) - -Encodes `Uint8Array` or `Array` of bytes into string using Base-64 encoding. - - -System requirements -------------------- - -TweetNaCl.js supports modern browsers that have a cryptographically secure -pseudorandom number generator and typed arrays, including the latest versions -of: - -* Chrome -* Firefox -* Safari (Mac, iOS) -* Internet Explorer 11 - -Other systems: - -* Node.js (we test on 0.10 and later) - - -Development and testing ------------------------- - -Install NPM modules needed for development: - - $ npm install - -To build minified versions: - - $ npm run build - -Tests use minified version, so make sure to rebuild it every time you change -`nacl.js` or `nacl-fast.js`. - -### Testing - -To run tests in Node.js: - - $ npm test - -By default all tests described here work on `nacl.min.js`. To test other -versions, set environment variable `NACL_SRC` to the file name you want to test. -For example, the following command will test fast minified version: - - $ NACL_SRC=nacl-fast.min.js npm test - -To run full suite of tests in Node.js, including comparing outputs of -JavaScript port to outputs of the original C version: - - $ npm run testall - -To prepare tests for browsers: - - $ npm run browser - -and then open `test/browser/test.html` (or `test/browser/test-fast.html`) to -run them. - -To run headless browser tests with `testling`: - - $ npm run testling - -(If you get `Error: spawn ENOENT`, install *xvfb*: `sudo apt-get install xvfb`.) - -### Benchmarking - -To run benchmarks in Node.js: - - $ npm run bench - $ NACL_SRC=nacl-fast.min.js npm run bench - -To run benchmarks in a browser, open `test/benchmark/bench.html` (or -`test/benchmark/bench-fast.html`). - - -Contributors ------------- - -JavaScript port: - - * [Dmitry Chestnykh](http://github.com/dchest) (ported xsalsa20, poly1305, curve25519) - * [Devi Mandiri](https://github.com/devi) (ported curve25519, ed25519, sha512) - -Original authors of [NaCl](http://nacl.cr.yp.to), [TweetNaCl](http://tweetnacl.cr.yp.to) -and [Poly1305-donna](https://github.com/floodyberry/poly1305-donna) -(who are *not* responsible for any errors in this implementation): - - * [Daniel J. Bernstein](http://cr.yp.to/djb.html) - * Wesley Janssen - * [Tanja Lange](http://hyperelliptic.org/tanja) - * [Peter Schwabe](http://www.cryptojedi.org/users/peter/) - * [Matthew Dempsky](https://github.com/mdempsky) - * [Andrew Moon](https://github.com/floodyberry) - -Contributors have dedicated their work to the public domain. - -This software is distributed without any warranty. - - -Third-party libraries based on TweetNaCl.js -------------------------------------------- - -* [forward-secrecy](https://github.com/alax/forward-secrecy) — Axolotl ratchet implementation -* [nacl-stream](https://github.com/dchest/nacl-stream-js) - streaming encryption -* [tweetnacl-auth-js](https://github.com/dchest/tweetnacl-auth-js) — implementation of [`crypto_auth`](http://nacl.cr.yp.to/auth.html) - - -Who uses it ------------ - -Some notable users of TweetNaCl.js: - -* [miniLock](http://minilock.io/) -* [Stellar](https://www.stellar.org/) diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl-fast.js b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl-fast.js deleted file mode 100644 index 6c499584..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl-fast.js +++ /dev/null @@ -1,2418 +0,0 @@ -(function(nacl) { -'use strict'; - -// Ported in 2014 by Dmitry Chestnykh and Devi Mandiri. -// Public domain. -// -// Implementation derived from TweetNaCl version 20140427. -// See for details: http://tweetnacl.cr.yp.to/ - -var gf = function(init) { - var i, r = new Float64Array(16); - if (init) for (i = 0; i < init.length; i++) r[i] = init[i]; - return r; -}; - -// Pluggable, initialized in high-level API below. -var randombytes = function(/* x, n */) { throw new Error('no PRNG'); }; - -var _0 = new Uint8Array(16); -var _9 = new Uint8Array(32); _9[0] = 9; - -var gf0 = gf(), - gf1 = gf([1]), - _121665 = gf([0xdb41, 1]), - D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]), - D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]), - X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]), - Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]), - I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]); - -function ts64(x, i, h, l) { - x[i] = (h >> 24) & 0xff; - x[i+1] = (h >> 16) & 0xff; - x[i+2] = (h >> 8) & 0xff; - x[i+3] = h & 0xff; - x[i+4] = (l >> 24) & 0xff; - x[i+5] = (l >> 16) & 0xff; - x[i+6] = (l >> 8) & 0xff; - x[i+7] = l & 0xff; -} - -function vn(x, xi, y, yi, n) { - var i,d = 0; - for (i = 0; i < n; i++) d |= x[xi+i]^y[yi+i]; - return (1 & ((d - 1) >>> 8)) - 1; -} - -function crypto_verify_16(x, xi, y, yi) { - return vn(x,xi,y,yi,16); -} - -function crypto_verify_32(x, xi, y, yi) { - return vn(x,xi,y,yi,32); -} - -function core_salsa20(o, p, k, c) { - var j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, - j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, - j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, - j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, - j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, - j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, - j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, - j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, - j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, - j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, - j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, - j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, - j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, - j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, - j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, - j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24; - - var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, - x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, - x15 = j15, u; - - for (var i = 0; i < 20; i += 2) { - u = x0 + x12 | 0; - x4 ^= u<<7 | u>>>(32-7); - u = x4 + x0 | 0; - x8 ^= u<<9 | u>>>(32-9); - u = x8 + x4 | 0; - x12 ^= u<<13 | u>>>(32-13); - u = x12 + x8 | 0; - x0 ^= u<<18 | u>>>(32-18); - - u = x5 + x1 | 0; - x9 ^= u<<7 | u>>>(32-7); - u = x9 + x5 | 0; - x13 ^= u<<9 | u>>>(32-9); - u = x13 + x9 | 0; - x1 ^= u<<13 | u>>>(32-13); - u = x1 + x13 | 0; - x5 ^= u<<18 | u>>>(32-18); - - u = x10 + x6 | 0; - x14 ^= u<<7 | u>>>(32-7); - u = x14 + x10 | 0; - x2 ^= u<<9 | u>>>(32-9); - u = x2 + x14 | 0; - x6 ^= u<<13 | u>>>(32-13); - u = x6 + x2 | 0; - x10 ^= u<<18 | u>>>(32-18); - - u = x15 + x11 | 0; - x3 ^= u<<7 | u>>>(32-7); - u = x3 + x15 | 0; - x7 ^= u<<9 | u>>>(32-9); - u = x7 + x3 | 0; - x11 ^= u<<13 | u>>>(32-13); - u = x11 + x7 | 0; - x15 ^= u<<18 | u>>>(32-18); - - u = x0 + x3 | 0; - x1 ^= u<<7 | u>>>(32-7); - u = x1 + x0 | 0; - x2 ^= u<<9 | u>>>(32-9); - u = x2 + x1 | 0; - x3 ^= u<<13 | u>>>(32-13); - u = x3 + x2 | 0; - x0 ^= u<<18 | u>>>(32-18); - - u = x5 + x4 | 0; - x6 ^= u<<7 | u>>>(32-7); - u = x6 + x5 | 0; - x7 ^= u<<9 | u>>>(32-9); - u = x7 + x6 | 0; - x4 ^= u<<13 | u>>>(32-13); - u = x4 + x7 | 0; - x5 ^= u<<18 | u>>>(32-18); - - u = x10 + x9 | 0; - x11 ^= u<<7 | u>>>(32-7); - u = x11 + x10 | 0; - x8 ^= u<<9 | u>>>(32-9); - u = x8 + x11 | 0; - x9 ^= u<<13 | u>>>(32-13); - u = x9 + x8 | 0; - x10 ^= u<<18 | u>>>(32-18); - - u = x15 + x14 | 0; - x12 ^= u<<7 | u>>>(32-7); - u = x12 + x15 | 0; - x13 ^= u<<9 | u>>>(32-9); - u = x13 + x12 | 0; - x14 ^= u<<13 | u>>>(32-13); - u = x14 + x13 | 0; - x15 ^= u<<18 | u>>>(32-18); - } - x0 = x0 + j0 | 0; - x1 = x1 + j1 | 0; - x2 = x2 + j2 | 0; - x3 = x3 + j3 | 0; - x4 = x4 + j4 | 0; - x5 = x5 + j5 | 0; - x6 = x6 + j6 | 0; - x7 = x7 + j7 | 0; - x8 = x8 + j8 | 0; - x9 = x9 + j9 | 0; - x10 = x10 + j10 | 0; - x11 = x11 + j11 | 0; - x12 = x12 + j12 | 0; - x13 = x13 + j13 | 0; - x14 = x14 + j14 | 0; - x15 = x15 + j15 | 0; - - o[ 0] = x0 >>> 0 & 0xff; - o[ 1] = x0 >>> 8 & 0xff; - o[ 2] = x0 >>> 16 & 0xff; - o[ 3] = x0 >>> 24 & 0xff; - - o[ 4] = x1 >>> 0 & 0xff; - o[ 5] = x1 >>> 8 & 0xff; - o[ 6] = x1 >>> 16 & 0xff; - o[ 7] = x1 >>> 24 & 0xff; - - o[ 8] = x2 >>> 0 & 0xff; - o[ 9] = x2 >>> 8 & 0xff; - o[10] = x2 >>> 16 & 0xff; - o[11] = x2 >>> 24 & 0xff; - - o[12] = x3 >>> 0 & 0xff; - o[13] = x3 >>> 8 & 0xff; - o[14] = x3 >>> 16 & 0xff; - o[15] = x3 >>> 24 & 0xff; - - o[16] = x4 >>> 0 & 0xff; - o[17] = x4 >>> 8 & 0xff; - o[18] = x4 >>> 16 & 0xff; - o[19] = x4 >>> 24 & 0xff; - - o[20] = x5 >>> 0 & 0xff; - o[21] = x5 >>> 8 & 0xff; - o[22] = x5 >>> 16 & 0xff; - o[23] = x5 >>> 24 & 0xff; - - o[24] = x6 >>> 0 & 0xff; - o[25] = x6 >>> 8 & 0xff; - o[26] = x6 >>> 16 & 0xff; - o[27] = x6 >>> 24 & 0xff; - - o[28] = x7 >>> 0 & 0xff; - o[29] = x7 >>> 8 & 0xff; - o[30] = x7 >>> 16 & 0xff; - o[31] = x7 >>> 24 & 0xff; - - o[32] = x8 >>> 0 & 0xff; - o[33] = x8 >>> 8 & 0xff; - o[34] = x8 >>> 16 & 0xff; - o[35] = x8 >>> 24 & 0xff; - - o[36] = x9 >>> 0 & 0xff; - o[37] = x9 >>> 8 & 0xff; - o[38] = x9 >>> 16 & 0xff; - o[39] = x9 >>> 24 & 0xff; - - o[40] = x10 >>> 0 & 0xff; - o[41] = x10 >>> 8 & 0xff; - o[42] = x10 >>> 16 & 0xff; - o[43] = x10 >>> 24 & 0xff; - - o[44] = x11 >>> 0 & 0xff; - o[45] = x11 >>> 8 & 0xff; - o[46] = x11 >>> 16 & 0xff; - o[47] = x11 >>> 24 & 0xff; - - o[48] = x12 >>> 0 & 0xff; - o[49] = x12 >>> 8 & 0xff; - o[50] = x12 >>> 16 & 0xff; - o[51] = x12 >>> 24 & 0xff; - - o[52] = x13 >>> 0 & 0xff; - o[53] = x13 >>> 8 & 0xff; - o[54] = x13 >>> 16 & 0xff; - o[55] = x13 >>> 24 & 0xff; - - o[56] = x14 >>> 0 & 0xff; - o[57] = x14 >>> 8 & 0xff; - o[58] = x14 >>> 16 & 0xff; - o[59] = x14 >>> 24 & 0xff; - - o[60] = x15 >>> 0 & 0xff; - o[61] = x15 >>> 8 & 0xff; - o[62] = x15 >>> 16 & 0xff; - o[63] = x15 >>> 24 & 0xff; -} - -function core_hsalsa20(o,p,k,c) { - var j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, - j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, - j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, - j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, - j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, - j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, - j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, - j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, - j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, - j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, - j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, - j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, - j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, - j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, - j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, - j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24; - - var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, - x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, - x15 = j15, u; - - for (var i = 0; i < 20; i += 2) { - u = x0 + x12 | 0; - x4 ^= u<<7 | u>>>(32-7); - u = x4 + x0 | 0; - x8 ^= u<<9 | u>>>(32-9); - u = x8 + x4 | 0; - x12 ^= u<<13 | u>>>(32-13); - u = x12 + x8 | 0; - x0 ^= u<<18 | u>>>(32-18); - - u = x5 + x1 | 0; - x9 ^= u<<7 | u>>>(32-7); - u = x9 + x5 | 0; - x13 ^= u<<9 | u>>>(32-9); - u = x13 + x9 | 0; - x1 ^= u<<13 | u>>>(32-13); - u = x1 + x13 | 0; - x5 ^= u<<18 | u>>>(32-18); - - u = x10 + x6 | 0; - x14 ^= u<<7 | u>>>(32-7); - u = x14 + x10 | 0; - x2 ^= u<<9 | u>>>(32-9); - u = x2 + x14 | 0; - x6 ^= u<<13 | u>>>(32-13); - u = x6 + x2 | 0; - x10 ^= u<<18 | u>>>(32-18); - - u = x15 + x11 | 0; - x3 ^= u<<7 | u>>>(32-7); - u = x3 + x15 | 0; - x7 ^= u<<9 | u>>>(32-9); - u = x7 + x3 | 0; - x11 ^= u<<13 | u>>>(32-13); - u = x11 + x7 | 0; - x15 ^= u<<18 | u>>>(32-18); - - u = x0 + x3 | 0; - x1 ^= u<<7 | u>>>(32-7); - u = x1 + x0 | 0; - x2 ^= u<<9 | u>>>(32-9); - u = x2 + x1 | 0; - x3 ^= u<<13 | u>>>(32-13); - u = x3 + x2 | 0; - x0 ^= u<<18 | u>>>(32-18); - - u = x5 + x4 | 0; - x6 ^= u<<7 | u>>>(32-7); - u = x6 + x5 | 0; - x7 ^= u<<9 | u>>>(32-9); - u = x7 + x6 | 0; - x4 ^= u<<13 | u>>>(32-13); - u = x4 + x7 | 0; - x5 ^= u<<18 | u>>>(32-18); - - u = x10 + x9 | 0; - x11 ^= u<<7 | u>>>(32-7); - u = x11 + x10 | 0; - x8 ^= u<<9 | u>>>(32-9); - u = x8 + x11 | 0; - x9 ^= u<<13 | u>>>(32-13); - u = x9 + x8 | 0; - x10 ^= u<<18 | u>>>(32-18); - - u = x15 + x14 | 0; - x12 ^= u<<7 | u>>>(32-7); - u = x12 + x15 | 0; - x13 ^= u<<9 | u>>>(32-9); - u = x13 + x12 | 0; - x14 ^= u<<13 | u>>>(32-13); - u = x14 + x13 | 0; - x15 ^= u<<18 | u>>>(32-18); - } - - o[ 0] = x0 >>> 0 & 0xff; - o[ 1] = x0 >>> 8 & 0xff; - o[ 2] = x0 >>> 16 & 0xff; - o[ 3] = x0 >>> 24 & 0xff; - - o[ 4] = x5 >>> 0 & 0xff; - o[ 5] = x5 >>> 8 & 0xff; - o[ 6] = x5 >>> 16 & 0xff; - o[ 7] = x5 >>> 24 & 0xff; - - o[ 8] = x10 >>> 0 & 0xff; - o[ 9] = x10 >>> 8 & 0xff; - o[10] = x10 >>> 16 & 0xff; - o[11] = x10 >>> 24 & 0xff; - - o[12] = x15 >>> 0 & 0xff; - o[13] = x15 >>> 8 & 0xff; - o[14] = x15 >>> 16 & 0xff; - o[15] = x15 >>> 24 & 0xff; - - o[16] = x6 >>> 0 & 0xff; - o[17] = x6 >>> 8 & 0xff; - o[18] = x6 >>> 16 & 0xff; - o[19] = x6 >>> 24 & 0xff; - - o[20] = x7 >>> 0 & 0xff; - o[21] = x7 >>> 8 & 0xff; - o[22] = x7 >>> 16 & 0xff; - o[23] = x7 >>> 24 & 0xff; - - o[24] = x8 >>> 0 & 0xff; - o[25] = x8 >>> 8 & 0xff; - o[26] = x8 >>> 16 & 0xff; - o[27] = x8 >>> 24 & 0xff; - - o[28] = x9 >>> 0 & 0xff; - o[29] = x9 >>> 8 & 0xff; - o[30] = x9 >>> 16 & 0xff; - o[31] = x9 >>> 24 & 0xff; -} - -function crypto_core_salsa20(out,inp,k,c) { - core_salsa20(out,inp,k,c); -} - -function crypto_core_hsalsa20(out,inp,k,c) { - core_hsalsa20(out,inp,k,c); -} - -var sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]); - // "expand 32-byte k" - -function crypto_stream_salsa20_xor(c,cpos,m,mpos,b,n,k) { - var z = new Uint8Array(16), x = new Uint8Array(64); - var u, i; - for (i = 0; i < 16; i++) z[i] = 0; - for (i = 0; i < 8; i++) z[i] = n[i]; - while (b >= 64) { - crypto_core_salsa20(x,z,k,sigma); - for (i = 0; i < 64; i++) c[cpos+i] = m[mpos+i] ^ x[i]; - u = 1; - for (i = 8; i < 16; i++) { - u = u + (z[i] & 0xff) | 0; - z[i] = u & 0xff; - u >>>= 8; - } - b -= 64; - cpos += 64; - mpos += 64; - } - if (b > 0) { - crypto_core_salsa20(x,z,k,sigma); - for (i = 0; i < b; i++) c[cpos+i] = m[mpos+i] ^ x[i]; - } - return 0; -} - -function crypto_stream_salsa20(c,cpos,b,n,k) { - var z = new Uint8Array(16), x = new Uint8Array(64); - var u, i; - for (i = 0; i < 16; i++) z[i] = 0; - for (i = 0; i < 8; i++) z[i] = n[i]; - while (b >= 64) { - crypto_core_salsa20(x,z,k,sigma); - for (i = 0; i < 64; i++) c[cpos+i] = x[i]; - u = 1; - for (i = 8; i < 16; i++) { - u = u + (z[i] & 0xff) | 0; - z[i] = u & 0xff; - u >>>= 8; - } - b -= 64; - cpos += 64; - } - if (b > 0) { - crypto_core_salsa20(x,z,k,sigma); - for (i = 0; i < b; i++) c[cpos+i] = x[i]; - } - return 0; -} - -function crypto_stream(c,cpos,d,n,k) { - var s = new Uint8Array(32); - crypto_core_hsalsa20(s,n,k,sigma); - var sn = new Uint8Array(8); - for (var i = 0; i < 8; i++) sn[i] = n[i+16]; - return crypto_stream_salsa20(c,cpos,d,sn,s); -} - -function crypto_stream_xor(c,cpos,m,mpos,d,n,k) { - var s = new Uint8Array(32); - crypto_core_hsalsa20(s,n,k,sigma); - var sn = new Uint8Array(8); - for (var i = 0; i < 8; i++) sn[i] = n[i+16]; - return crypto_stream_salsa20_xor(c,cpos,m,mpos,d,sn,s); -} - -/* -* Port of Andrew Moon's Poly1305-donna-16. Public domain. -* https://github.com/floodyberry/poly1305-donna -*/ - -var poly1305 = function(key) { - this.buffer = new Uint8Array(16); - this.r = new Uint16Array(10); - this.h = new Uint16Array(10); - this.pad = new Uint16Array(8); - this.leftover = 0; - this.fin = 0; - - var t0, t1, t2, t3, t4, t5, t6, t7; - - t0 = key[ 0] & 0xff | (key[ 1] & 0xff) << 8; this.r[0] = ( t0 ) & 0x1fff; - t1 = key[ 2] & 0xff | (key[ 3] & 0xff) << 8; this.r[1] = ((t0 >>> 13) | (t1 << 3)) & 0x1fff; - t2 = key[ 4] & 0xff | (key[ 5] & 0xff) << 8; this.r[2] = ((t1 >>> 10) | (t2 << 6)) & 0x1f03; - t3 = key[ 6] & 0xff | (key[ 7] & 0xff) << 8; this.r[3] = ((t2 >>> 7) | (t3 << 9)) & 0x1fff; - t4 = key[ 8] & 0xff | (key[ 9] & 0xff) << 8; this.r[4] = ((t3 >>> 4) | (t4 << 12)) & 0x00ff; - this.r[5] = ((t4 >>> 1)) & 0x1ffe; - t5 = key[10] & 0xff | (key[11] & 0xff) << 8; this.r[6] = ((t4 >>> 14) | (t5 << 2)) & 0x1fff; - t6 = key[12] & 0xff | (key[13] & 0xff) << 8; this.r[7] = ((t5 >>> 11) | (t6 << 5)) & 0x1f81; - t7 = key[14] & 0xff | (key[15] & 0xff) << 8; this.r[8] = ((t6 >>> 8) | (t7 << 8)) & 0x1fff; - this.r[9] = ((t7 >>> 5)) & 0x007f; - - this.pad[0] = key[16] & 0xff | (key[17] & 0xff) << 8; - this.pad[1] = key[18] & 0xff | (key[19] & 0xff) << 8; - this.pad[2] = key[20] & 0xff | (key[21] & 0xff) << 8; - this.pad[3] = key[22] & 0xff | (key[23] & 0xff) << 8; - this.pad[4] = key[24] & 0xff | (key[25] & 0xff) << 8; - this.pad[5] = key[26] & 0xff | (key[27] & 0xff) << 8; - this.pad[6] = key[28] & 0xff | (key[29] & 0xff) << 8; - this.pad[7] = key[30] & 0xff | (key[31] & 0xff) << 8; -}; - -poly1305.prototype.blocks = function(m, mpos, bytes) { - var hibit = this.fin ? 0 : (1 << 11); - var t0, t1, t2, t3, t4, t5, t6, t7, c; - var d0, d1, d2, d3, d4, d5, d6, d7, d8, d9; - - var h0 = this.h[0], - h1 = this.h[1], - h2 = this.h[2], - h3 = this.h[3], - h4 = this.h[4], - h5 = this.h[5], - h6 = this.h[6], - h7 = this.h[7], - h8 = this.h[8], - h9 = this.h[9]; - - var r0 = this.r[0], - r1 = this.r[1], - r2 = this.r[2], - r3 = this.r[3], - r4 = this.r[4], - r5 = this.r[5], - r6 = this.r[6], - r7 = this.r[7], - r8 = this.r[8], - r9 = this.r[9]; - - while (bytes >= 16) { - t0 = m[mpos+ 0] & 0xff | (m[mpos+ 1] & 0xff) << 8; h0 += ( t0 ) & 0x1fff; - t1 = m[mpos+ 2] & 0xff | (m[mpos+ 3] & 0xff) << 8; h1 += ((t0 >>> 13) | (t1 << 3)) & 0x1fff; - t2 = m[mpos+ 4] & 0xff | (m[mpos+ 5] & 0xff) << 8; h2 += ((t1 >>> 10) | (t2 << 6)) & 0x1fff; - t3 = m[mpos+ 6] & 0xff | (m[mpos+ 7] & 0xff) << 8; h3 += ((t2 >>> 7) | (t3 << 9)) & 0x1fff; - t4 = m[mpos+ 8] & 0xff | (m[mpos+ 9] & 0xff) << 8; h4 += ((t3 >>> 4) | (t4 << 12)) & 0x1fff; - h5 += ((t4 >>> 1)) & 0x1fff; - t5 = m[mpos+10] & 0xff | (m[mpos+11] & 0xff) << 8; h6 += ((t4 >>> 14) | (t5 << 2)) & 0x1fff; - t6 = m[mpos+12] & 0xff | (m[mpos+13] & 0xff) << 8; h7 += ((t5 >>> 11) | (t6 << 5)) & 0x1fff; - t7 = m[mpos+14] & 0xff | (m[mpos+15] & 0xff) << 8; h8 += ((t6 >>> 8) | (t7 << 8)) & 0x1fff; - h9 += ((t7 >>> 5)) | hibit; - - c = 0; - - d0 = c; - d0 += h0 * r0; - d0 += h1 * (5 * r9); - d0 += h2 * (5 * r8); - d0 += h3 * (5 * r7); - d0 += h4 * (5 * r6); - c = (d0 >>> 13); d0 &= 0x1fff; - d0 += h5 * (5 * r5); - d0 += h6 * (5 * r4); - d0 += h7 * (5 * r3); - d0 += h8 * (5 * r2); - d0 += h9 * (5 * r1); - c += (d0 >>> 13); d0 &= 0x1fff; - - d1 = c; - d1 += h0 * r1; - d1 += h1 * r0; - d1 += h2 * (5 * r9); - d1 += h3 * (5 * r8); - d1 += h4 * (5 * r7); - c = (d1 >>> 13); d1 &= 0x1fff; - d1 += h5 * (5 * r6); - d1 += h6 * (5 * r5); - d1 += h7 * (5 * r4); - d1 += h8 * (5 * r3); - d1 += h9 * (5 * r2); - c += (d1 >>> 13); d1 &= 0x1fff; - - d2 = c; - d2 += h0 * r2; - d2 += h1 * r1; - d2 += h2 * r0; - d2 += h3 * (5 * r9); - d2 += h4 * (5 * r8); - c = (d2 >>> 13); d2 &= 0x1fff; - d2 += h5 * (5 * r7); - d2 += h6 * (5 * r6); - d2 += h7 * (5 * r5); - d2 += h8 * (5 * r4); - d2 += h9 * (5 * r3); - c += (d2 >>> 13); d2 &= 0x1fff; - - d3 = c; - d3 += h0 * r3; - d3 += h1 * r2; - d3 += h2 * r1; - d3 += h3 * r0; - d3 += h4 * (5 * r9); - c = (d3 >>> 13); d3 &= 0x1fff; - d3 += h5 * (5 * r8); - d3 += h6 * (5 * r7); - d3 += h7 * (5 * r6); - d3 += h8 * (5 * r5); - d3 += h9 * (5 * r4); - c += (d3 >>> 13); d3 &= 0x1fff; - - d4 = c; - d4 += h0 * r4; - d4 += h1 * r3; - d4 += h2 * r2; - d4 += h3 * r1; - d4 += h4 * r0; - c = (d4 >>> 13); d4 &= 0x1fff; - d4 += h5 * (5 * r9); - d4 += h6 * (5 * r8); - d4 += h7 * (5 * r7); - d4 += h8 * (5 * r6); - d4 += h9 * (5 * r5); - c += (d4 >>> 13); d4 &= 0x1fff; - - d5 = c; - d5 += h0 * r5; - d5 += h1 * r4; - d5 += h2 * r3; - d5 += h3 * r2; - d5 += h4 * r1; - c = (d5 >>> 13); d5 &= 0x1fff; - d5 += h5 * r0; - d5 += h6 * (5 * r9); - d5 += h7 * (5 * r8); - d5 += h8 * (5 * r7); - d5 += h9 * (5 * r6); - c += (d5 >>> 13); d5 &= 0x1fff; - - d6 = c; - d6 += h0 * r6; - d6 += h1 * r5; - d6 += h2 * r4; - d6 += h3 * r3; - d6 += h4 * r2; - c = (d6 >>> 13); d6 &= 0x1fff; - d6 += h5 * r1; - d6 += h6 * r0; - d6 += h7 * (5 * r9); - d6 += h8 * (5 * r8); - d6 += h9 * (5 * r7); - c += (d6 >>> 13); d6 &= 0x1fff; - - d7 = c; - d7 += h0 * r7; - d7 += h1 * r6; - d7 += h2 * r5; - d7 += h3 * r4; - d7 += h4 * r3; - c = (d7 >>> 13); d7 &= 0x1fff; - d7 += h5 * r2; - d7 += h6 * r1; - d7 += h7 * r0; - d7 += h8 * (5 * r9); - d7 += h9 * (5 * r8); - c += (d7 >>> 13); d7 &= 0x1fff; - - d8 = c; - d8 += h0 * r8; - d8 += h1 * r7; - d8 += h2 * r6; - d8 += h3 * r5; - d8 += h4 * r4; - c = (d8 >>> 13); d8 &= 0x1fff; - d8 += h5 * r3; - d8 += h6 * r2; - d8 += h7 * r1; - d8 += h8 * r0; - d8 += h9 * (5 * r9); - c += (d8 >>> 13); d8 &= 0x1fff; - - d9 = c; - d9 += h0 * r9; - d9 += h1 * r8; - d9 += h2 * r7; - d9 += h3 * r6; - d9 += h4 * r5; - c = (d9 >>> 13); d9 &= 0x1fff; - d9 += h5 * r4; - d9 += h6 * r3; - d9 += h7 * r2; - d9 += h8 * r1; - d9 += h9 * r0; - c += (d9 >>> 13); d9 &= 0x1fff; - - c = (((c << 2) + c)) | 0; - c = (c + d0) | 0; - d0 = c & 0x1fff; - c = (c >>> 13); - d1 += c; - - h0 = d0; - h1 = d1; - h2 = d2; - h3 = d3; - h4 = d4; - h5 = d5; - h6 = d6; - h7 = d7; - h8 = d8; - h9 = d9; - - mpos += 16; - bytes -= 16; - } - this.h[0] = h0; - this.h[1] = h1; - this.h[2] = h2; - this.h[3] = h3; - this.h[4] = h4; - this.h[5] = h5; - this.h[6] = h6; - this.h[7] = h7; - this.h[8] = h8; - this.h[9] = h9; -}; - -poly1305.prototype.finish = function(mac, macpos) { - var g = new Uint16Array(10); - var c, mask, f, i; - - if (this.leftover) { - i = this.leftover; - this.buffer[i++] = 1; - for (; i < 16; i++) this.buffer[i] = 0; - this.fin = 1; - this.blocks(this.buffer, 0, 16); - } - - c = this.h[1] >>> 13; - this.h[1] &= 0x1fff; - for (i = 2; i < 10; i++) { - this.h[i] += c; - c = this.h[i] >>> 13; - this.h[i] &= 0x1fff; - } - this.h[0] += (c * 5); - c = this.h[0] >>> 13; - this.h[0] &= 0x1fff; - this.h[1] += c; - c = this.h[1] >>> 13; - this.h[1] &= 0x1fff; - this.h[2] += c; - - g[0] = this.h[0] + 5; - c = g[0] >>> 13; - g[0] &= 0x1fff; - for (i = 1; i < 10; i++) { - g[i] = this.h[i] + c; - c = g[i] >>> 13; - g[i] &= 0x1fff; - } - g[9] -= (1 << 13); - - mask = (g[9] >>> ((2 * 8) - 1)) - 1; - for (i = 0; i < 10; i++) g[i] &= mask; - mask = ~mask; - for (i = 0; i < 10; i++) this.h[i] = (this.h[i] & mask) | g[i]; - - this.h[0] = ((this.h[0] ) | (this.h[1] << 13) ) & 0xffff; - this.h[1] = ((this.h[1] >>> 3) | (this.h[2] << 10) ) & 0xffff; - this.h[2] = ((this.h[2] >>> 6) | (this.h[3] << 7) ) & 0xffff; - this.h[3] = ((this.h[3] >>> 9) | (this.h[4] << 4) ) & 0xffff; - this.h[4] = ((this.h[4] >>> 12) | (this.h[5] << 1) | (this.h[6] << 14)) & 0xffff; - this.h[5] = ((this.h[6] >>> 2) | (this.h[7] << 11) ) & 0xffff; - this.h[6] = ((this.h[7] >>> 5) | (this.h[8] << 8) ) & 0xffff; - this.h[7] = ((this.h[8] >>> 8) | (this.h[9] << 5) ) & 0xffff; - - f = this.h[0] + this.pad[0]; - this.h[0] = f & 0xffff; - for (i = 1; i < 8; i++) { - f = (((this.h[i] + this.pad[i]) | 0) + (f >>> 16)) | 0; - this.h[i] = f & 0xffff; - } - - mac[macpos+ 0] = (this.h[0] >>> 0) & 0xff; - mac[macpos+ 1] = (this.h[0] >>> 8) & 0xff; - mac[macpos+ 2] = (this.h[1] >>> 0) & 0xff; - mac[macpos+ 3] = (this.h[1] >>> 8) & 0xff; - mac[macpos+ 4] = (this.h[2] >>> 0) & 0xff; - mac[macpos+ 5] = (this.h[2] >>> 8) & 0xff; - mac[macpos+ 6] = (this.h[3] >>> 0) & 0xff; - mac[macpos+ 7] = (this.h[3] >>> 8) & 0xff; - mac[macpos+ 8] = (this.h[4] >>> 0) & 0xff; - mac[macpos+ 9] = (this.h[4] >>> 8) & 0xff; - mac[macpos+10] = (this.h[5] >>> 0) & 0xff; - mac[macpos+11] = (this.h[5] >>> 8) & 0xff; - mac[macpos+12] = (this.h[6] >>> 0) & 0xff; - mac[macpos+13] = (this.h[6] >>> 8) & 0xff; - mac[macpos+14] = (this.h[7] >>> 0) & 0xff; - mac[macpos+15] = (this.h[7] >>> 8) & 0xff; -}; - -poly1305.prototype.update = function(m, mpos, bytes) { - var i, want; - - if (this.leftover) { - want = (16 - this.leftover); - if (want > bytes) - want = bytes; - for (i = 0; i < want; i++) - this.buffer[this.leftover + i] = m[mpos+i]; - bytes -= want; - mpos += want; - this.leftover += want; - if (this.leftover < 16) - return; - this.blocks(this.buffer, 0, 16); - this.leftover = 0; - } - - if (bytes >= 16) { - want = bytes - (bytes % 16); - this.blocks(m, mpos, want); - mpos += want; - bytes -= want; - } - - if (bytes) { - for (i = 0; i < bytes; i++) - this.buffer[this.leftover + i] = m[mpos+i]; - this.leftover += bytes; - } -}; - -function crypto_onetimeauth(out, outpos, m, mpos, n, k) { - var s = new poly1305(k); - s.update(m, mpos, n); - s.finish(out, outpos); - return 0; -} - -function crypto_onetimeauth_verify(h, hpos, m, mpos, n, k) { - var x = new Uint8Array(16); - crypto_onetimeauth(x,0,m,mpos,n,k); - return crypto_verify_16(h,hpos,x,0); -} - -function crypto_secretbox(c,m,d,n,k) { - var i; - if (d < 32) return -1; - crypto_stream_xor(c,0,m,0,d,n,k); - crypto_onetimeauth(c, 16, c, 32, d - 32, c); - for (i = 0; i < 16; i++) c[i] = 0; - return 0; -} - -function crypto_secretbox_open(m,c,d,n,k) { - var i; - var x = new Uint8Array(32); - if (d < 32) return -1; - crypto_stream(x,0,32,n,k); - if (crypto_onetimeauth_verify(c, 16,c, 32,d - 32,x) !== 0) return -1; - crypto_stream_xor(m,0,c,0,d,n,k); - for (i = 0; i < 32; i++) m[i] = 0; - return 0; -} - -function set25519(r, a) { - var i; - for (i = 0; i < 16; i++) r[i] = a[i]|0; -} - -function car25519(o) { - var i, v, c = 1; - for (i = 0; i < 16; i++) { - v = o[i] + c + 65535; - c = Math.floor(v / 65536); - o[i] = v - c * 65536; - } - o[0] += c-1 + 37 * (c-1); -} - -function sel25519(p, q, b) { - var t, c = ~(b-1); - for (var i = 0; i < 16; i++) { - t = c & (p[i] ^ q[i]); - p[i] ^= t; - q[i] ^= t; - } -} - -function pack25519(o, n) { - var i, j, b; - var m = gf(), t = gf(); - for (i = 0; i < 16; i++) t[i] = n[i]; - car25519(t); - car25519(t); - car25519(t); - for (j = 0; j < 2; j++) { - m[0] = t[0] - 0xffed; - for (i = 1; i < 15; i++) { - m[i] = t[i] - 0xffff - ((m[i-1]>>16) & 1); - m[i-1] &= 0xffff; - } - m[15] = t[15] - 0x7fff - ((m[14]>>16) & 1); - b = (m[15]>>16) & 1; - m[14] &= 0xffff; - sel25519(t, m, 1-b); - } - for (i = 0; i < 16; i++) { - o[2*i] = t[i] & 0xff; - o[2*i+1] = t[i]>>8; - } -} - -function neq25519(a, b) { - var c = new Uint8Array(32), d = new Uint8Array(32); - pack25519(c, a); - pack25519(d, b); - return crypto_verify_32(c, 0, d, 0); -} - -function par25519(a) { - var d = new Uint8Array(32); - pack25519(d, a); - return d[0] & 1; -} - -function unpack25519(o, n) { - var i; - for (i = 0; i < 16; i++) o[i] = n[2*i] + (n[2*i+1] << 8); - o[15] &= 0x7fff; -} - -function A(o, a, b) { - for (var i = 0; i < 16; i++) o[i] = a[i] + b[i]; -} - -function Z(o, a, b) { - for (var i = 0; i < 16; i++) o[i] = a[i] - b[i]; -} - -function M(o, a, b) { - var v, c, - t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0, - t8 = 0, t9 = 0, t10 = 0, t11 = 0, t12 = 0, t13 = 0, t14 = 0, t15 = 0, - t16 = 0, t17 = 0, t18 = 0, t19 = 0, t20 = 0, t21 = 0, t22 = 0, t23 = 0, - t24 = 0, t25 = 0, t26 = 0, t27 = 0, t28 = 0, t29 = 0, t30 = 0, - b0 = b[0], - b1 = b[1], - b2 = b[2], - b3 = b[3], - b4 = b[4], - b5 = b[5], - b6 = b[6], - b7 = b[7], - b8 = b[8], - b9 = b[9], - b10 = b[10], - b11 = b[11], - b12 = b[12], - b13 = b[13], - b14 = b[14], - b15 = b[15]; - - v = a[0]; - t0 += v * b0; - t1 += v * b1; - t2 += v * b2; - t3 += v * b3; - t4 += v * b4; - t5 += v * b5; - t6 += v * b6; - t7 += v * b7; - t8 += v * b8; - t9 += v * b9; - t10 += v * b10; - t11 += v * b11; - t12 += v * b12; - t13 += v * b13; - t14 += v * b14; - t15 += v * b15; - v = a[1]; - t1 += v * b0; - t2 += v * b1; - t3 += v * b2; - t4 += v * b3; - t5 += v * b4; - t6 += v * b5; - t7 += v * b6; - t8 += v * b7; - t9 += v * b8; - t10 += v * b9; - t11 += v * b10; - t12 += v * b11; - t13 += v * b12; - t14 += v * b13; - t15 += v * b14; - t16 += v * b15; - v = a[2]; - t2 += v * b0; - t3 += v * b1; - t4 += v * b2; - t5 += v * b3; - t6 += v * b4; - t7 += v * b5; - t8 += v * b6; - t9 += v * b7; - t10 += v * b8; - t11 += v * b9; - t12 += v * b10; - t13 += v * b11; - t14 += v * b12; - t15 += v * b13; - t16 += v * b14; - t17 += v * b15; - v = a[3]; - t3 += v * b0; - t4 += v * b1; - t5 += v * b2; - t6 += v * b3; - t7 += v * b4; - t8 += v * b5; - t9 += v * b6; - t10 += v * b7; - t11 += v * b8; - t12 += v * b9; - t13 += v * b10; - t14 += v * b11; - t15 += v * b12; - t16 += v * b13; - t17 += v * b14; - t18 += v * b15; - v = a[4]; - t4 += v * b0; - t5 += v * b1; - t6 += v * b2; - t7 += v * b3; - t8 += v * b4; - t9 += v * b5; - t10 += v * b6; - t11 += v * b7; - t12 += v * b8; - t13 += v * b9; - t14 += v * b10; - t15 += v * b11; - t16 += v * b12; - t17 += v * b13; - t18 += v * b14; - t19 += v * b15; - v = a[5]; - t5 += v * b0; - t6 += v * b1; - t7 += v * b2; - t8 += v * b3; - t9 += v * b4; - t10 += v * b5; - t11 += v * b6; - t12 += v * b7; - t13 += v * b8; - t14 += v * b9; - t15 += v * b10; - t16 += v * b11; - t17 += v * b12; - t18 += v * b13; - t19 += v * b14; - t20 += v * b15; - v = a[6]; - t6 += v * b0; - t7 += v * b1; - t8 += v * b2; - t9 += v * b3; - t10 += v * b4; - t11 += v * b5; - t12 += v * b6; - t13 += v * b7; - t14 += v * b8; - t15 += v * b9; - t16 += v * b10; - t17 += v * b11; - t18 += v * b12; - t19 += v * b13; - t20 += v * b14; - t21 += v * b15; - v = a[7]; - t7 += v * b0; - t8 += v * b1; - t9 += v * b2; - t10 += v * b3; - t11 += v * b4; - t12 += v * b5; - t13 += v * b6; - t14 += v * b7; - t15 += v * b8; - t16 += v * b9; - t17 += v * b10; - t18 += v * b11; - t19 += v * b12; - t20 += v * b13; - t21 += v * b14; - t22 += v * b15; - v = a[8]; - t8 += v * b0; - t9 += v * b1; - t10 += v * b2; - t11 += v * b3; - t12 += v * b4; - t13 += v * b5; - t14 += v * b6; - t15 += v * b7; - t16 += v * b8; - t17 += v * b9; - t18 += v * b10; - t19 += v * b11; - t20 += v * b12; - t21 += v * b13; - t22 += v * b14; - t23 += v * b15; - v = a[9]; - t9 += v * b0; - t10 += v * b1; - t11 += v * b2; - t12 += v * b3; - t13 += v * b4; - t14 += v * b5; - t15 += v * b6; - t16 += v * b7; - t17 += v * b8; - t18 += v * b9; - t19 += v * b10; - t20 += v * b11; - t21 += v * b12; - t22 += v * b13; - t23 += v * b14; - t24 += v * b15; - v = a[10]; - t10 += v * b0; - t11 += v * b1; - t12 += v * b2; - t13 += v * b3; - t14 += v * b4; - t15 += v * b5; - t16 += v * b6; - t17 += v * b7; - t18 += v * b8; - t19 += v * b9; - t20 += v * b10; - t21 += v * b11; - t22 += v * b12; - t23 += v * b13; - t24 += v * b14; - t25 += v * b15; - v = a[11]; - t11 += v * b0; - t12 += v * b1; - t13 += v * b2; - t14 += v * b3; - t15 += v * b4; - t16 += v * b5; - t17 += v * b6; - t18 += v * b7; - t19 += v * b8; - t20 += v * b9; - t21 += v * b10; - t22 += v * b11; - t23 += v * b12; - t24 += v * b13; - t25 += v * b14; - t26 += v * b15; - v = a[12]; - t12 += v * b0; - t13 += v * b1; - t14 += v * b2; - t15 += v * b3; - t16 += v * b4; - t17 += v * b5; - t18 += v * b6; - t19 += v * b7; - t20 += v * b8; - t21 += v * b9; - t22 += v * b10; - t23 += v * b11; - t24 += v * b12; - t25 += v * b13; - t26 += v * b14; - t27 += v * b15; - v = a[13]; - t13 += v * b0; - t14 += v * b1; - t15 += v * b2; - t16 += v * b3; - t17 += v * b4; - t18 += v * b5; - t19 += v * b6; - t20 += v * b7; - t21 += v * b8; - t22 += v * b9; - t23 += v * b10; - t24 += v * b11; - t25 += v * b12; - t26 += v * b13; - t27 += v * b14; - t28 += v * b15; - v = a[14]; - t14 += v * b0; - t15 += v * b1; - t16 += v * b2; - t17 += v * b3; - t18 += v * b4; - t19 += v * b5; - t20 += v * b6; - t21 += v * b7; - t22 += v * b8; - t23 += v * b9; - t24 += v * b10; - t25 += v * b11; - t26 += v * b12; - t27 += v * b13; - t28 += v * b14; - t29 += v * b15; - v = a[15]; - t15 += v * b0; - t16 += v * b1; - t17 += v * b2; - t18 += v * b3; - t19 += v * b4; - t20 += v * b5; - t21 += v * b6; - t22 += v * b7; - t23 += v * b8; - t24 += v * b9; - t25 += v * b10; - t26 += v * b11; - t27 += v * b12; - t28 += v * b13; - t29 += v * b14; - t30 += v * b15; - - t0 += 38 * t16; - t1 += 38 * t17; - t2 += 38 * t18; - t3 += 38 * t19; - t4 += 38 * t20; - t5 += 38 * t21; - t6 += 38 * t22; - t7 += 38 * t23; - t8 += 38 * t24; - t9 += 38 * t25; - t10 += 38 * t26; - t11 += 38 * t27; - t12 += 38 * t28; - t13 += 38 * t29; - t14 += 38 * t30; - // t15 left as is - - // first car - c = 1; - v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; - v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; - v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; - v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; - v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; - v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; - v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; - v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; - v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; - v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; - v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; - v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; - v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; - v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; - v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; - v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; - t0 += c-1 + 37 * (c-1); - - // second car - c = 1; - v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; - v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; - v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; - v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; - v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; - v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; - v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; - v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; - v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; - v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; - v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; - v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; - v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; - v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; - v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; - v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; - t0 += c-1 + 37 * (c-1); - - o[ 0] = t0; - o[ 1] = t1; - o[ 2] = t2; - o[ 3] = t3; - o[ 4] = t4; - o[ 5] = t5; - o[ 6] = t6; - o[ 7] = t7; - o[ 8] = t8; - o[ 9] = t9; - o[10] = t10; - o[11] = t11; - o[12] = t12; - o[13] = t13; - o[14] = t14; - o[15] = t15; -} - -function S(o, a) { - M(o, a, a); -} - -function inv25519(o, i) { - var c = gf(); - var a; - for (a = 0; a < 16; a++) c[a] = i[a]; - for (a = 253; a >= 0; a--) { - S(c, c); - if(a !== 2 && a !== 4) M(c, c, i); - } - for (a = 0; a < 16; a++) o[a] = c[a]; -} - -function pow2523(o, i) { - var c = gf(); - var a; - for (a = 0; a < 16; a++) c[a] = i[a]; - for (a = 250; a >= 0; a--) { - S(c, c); - if(a !== 1) M(c, c, i); - } - for (a = 0; a < 16; a++) o[a] = c[a]; -} - -function crypto_scalarmult(q, n, p) { - var z = new Uint8Array(32); - var x = new Float64Array(80), r, i; - var a = gf(), b = gf(), c = gf(), - d = gf(), e = gf(), f = gf(); - for (i = 0; i < 31; i++) z[i] = n[i]; - z[31]=(n[31]&127)|64; - z[0]&=248; - unpack25519(x,p); - for (i = 0; i < 16; i++) { - b[i]=x[i]; - d[i]=a[i]=c[i]=0; - } - a[0]=d[0]=1; - for (i=254; i>=0; --i) { - r=(z[i>>>3]>>>(i&7))&1; - sel25519(a,b,r); - sel25519(c,d,r); - A(e,a,c); - Z(a,a,c); - A(c,b,d); - Z(b,b,d); - S(d,e); - S(f,a); - M(a,c,a); - M(c,b,e); - A(e,a,c); - Z(a,a,c); - S(b,a); - Z(c,d,f); - M(a,c,_121665); - A(a,a,d); - M(c,c,a); - M(a,d,f); - M(d,b,x); - S(b,e); - sel25519(a,b,r); - sel25519(c,d,r); - } - for (i = 0; i < 16; i++) { - x[i+16]=a[i]; - x[i+32]=c[i]; - x[i+48]=b[i]; - x[i+64]=d[i]; - } - var x32 = x.subarray(32); - var x16 = x.subarray(16); - inv25519(x32,x32); - M(x16,x16,x32); - pack25519(q,x16); - return 0; -} - -function crypto_scalarmult_base(q, n) { - return crypto_scalarmult(q, n, _9); -} - -function crypto_box_keypair(y, x) { - randombytes(x, 32); - return crypto_scalarmult_base(y, x); -} - -function crypto_box_beforenm(k, y, x) { - var s = new Uint8Array(32); - crypto_scalarmult(s, x, y); - return crypto_core_hsalsa20(k, _0, s, sigma); -} - -var crypto_box_afternm = crypto_secretbox; -var crypto_box_open_afternm = crypto_secretbox_open; - -function crypto_box(c, m, d, n, y, x) { - var k = new Uint8Array(32); - crypto_box_beforenm(k, y, x); - return crypto_box_afternm(c, m, d, n, k); -} - -function crypto_box_open(m, c, d, n, y, x) { - var k = new Uint8Array(32); - crypto_box_beforenm(k, y, x); - return crypto_box_open_afternm(m, c, d, n, k); -} - -var K = [ - 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, - 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc, - 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019, - 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118, - 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe, - 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2, - 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, - 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694, - 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3, - 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65, - 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483, - 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5, - 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, - 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4, - 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725, - 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70, - 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926, - 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df, - 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, - 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b, - 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001, - 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30, - 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910, - 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8, - 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, - 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8, - 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb, - 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3, - 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60, - 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec, - 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, - 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b, - 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207, - 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178, - 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6, - 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b, - 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, - 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c, - 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a, - 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817 -]; - -function crypto_hashblocks_hl(hh, hl, m, n) { - var wh = new Int32Array(16), wl = new Int32Array(16), - bh0, bh1, bh2, bh3, bh4, bh5, bh6, bh7, - bl0, bl1, bl2, bl3, bl4, bl5, bl6, bl7, - th, tl, i, j, h, l, a, b, c, d; - - var ah0 = hh[0], - ah1 = hh[1], - ah2 = hh[2], - ah3 = hh[3], - ah4 = hh[4], - ah5 = hh[5], - ah6 = hh[6], - ah7 = hh[7], - - al0 = hl[0], - al1 = hl[1], - al2 = hl[2], - al3 = hl[3], - al4 = hl[4], - al5 = hl[5], - al6 = hl[6], - al7 = hl[7]; - - var pos = 0; - while (n >= 128) { - for (i = 0; i < 16; i++) { - j = 8 * i + pos; - wh[i] = (m[j+0] << 24) | (m[j+1] << 16) | (m[j+2] << 8) | m[j+3]; - wl[i] = (m[j+4] << 24) | (m[j+5] << 16) | (m[j+6] << 8) | m[j+7]; - } - for (i = 0; i < 80; i++) { - bh0 = ah0; - bh1 = ah1; - bh2 = ah2; - bh3 = ah3; - bh4 = ah4; - bh5 = ah5; - bh6 = ah6; - bh7 = ah7; - - bl0 = al0; - bl1 = al1; - bl2 = al2; - bl3 = al3; - bl4 = al4; - bl5 = al5; - bl6 = al6; - bl7 = al7; - - // add - h = ah7; - l = al7; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - // Sigma1 - h = ((ah4 >>> 14) | (al4 << (32-14))) ^ ((ah4 >>> 18) | (al4 << (32-18))) ^ ((al4 >>> (41-32)) | (ah4 << (32-(41-32)))); - l = ((al4 >>> 14) | (ah4 << (32-14))) ^ ((al4 >>> 18) | (ah4 << (32-18))) ^ ((ah4 >>> (41-32)) | (al4 << (32-(41-32)))); - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - // Ch - h = (ah4 & ah5) ^ (~ah4 & ah6); - l = (al4 & al5) ^ (~al4 & al6); - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - // K - h = K[i*2]; - l = K[i*2+1]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - // w - h = wh[i%16]; - l = wl[i%16]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - th = c & 0xffff | d << 16; - tl = a & 0xffff | b << 16; - - // add - h = th; - l = tl; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - // Sigma0 - h = ((ah0 >>> 28) | (al0 << (32-28))) ^ ((al0 >>> (34-32)) | (ah0 << (32-(34-32)))) ^ ((al0 >>> (39-32)) | (ah0 << (32-(39-32)))); - l = ((al0 >>> 28) | (ah0 << (32-28))) ^ ((ah0 >>> (34-32)) | (al0 << (32-(34-32)))) ^ ((ah0 >>> (39-32)) | (al0 << (32-(39-32)))); - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - // Maj - h = (ah0 & ah1) ^ (ah0 & ah2) ^ (ah1 & ah2); - l = (al0 & al1) ^ (al0 & al2) ^ (al1 & al2); - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - bh7 = (c & 0xffff) | (d << 16); - bl7 = (a & 0xffff) | (b << 16); - - // add - h = bh3; - l = bl3; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = th; - l = tl; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - bh3 = (c & 0xffff) | (d << 16); - bl3 = (a & 0xffff) | (b << 16); - - ah1 = bh0; - ah2 = bh1; - ah3 = bh2; - ah4 = bh3; - ah5 = bh4; - ah6 = bh5; - ah7 = bh6; - ah0 = bh7; - - al1 = bl0; - al2 = bl1; - al3 = bl2; - al4 = bl3; - al5 = bl4; - al6 = bl5; - al7 = bl6; - al0 = bl7; - - if (i%16 === 15) { - for (j = 0; j < 16; j++) { - // add - h = wh[j]; - l = wl[j]; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = wh[(j+9)%16]; - l = wl[(j+9)%16]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - // sigma0 - th = wh[(j+1)%16]; - tl = wl[(j+1)%16]; - h = ((th >>> 1) | (tl << (32-1))) ^ ((th >>> 8) | (tl << (32-8))) ^ (th >>> 7); - l = ((tl >>> 1) | (th << (32-1))) ^ ((tl >>> 8) | (th << (32-8))) ^ ((tl >>> 7) | (th << (32-7))); - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - // sigma1 - th = wh[(j+14)%16]; - tl = wl[(j+14)%16]; - h = ((th >>> 19) | (tl << (32-19))) ^ ((tl >>> (61-32)) | (th << (32-(61-32)))) ^ (th >>> 6); - l = ((tl >>> 19) | (th << (32-19))) ^ ((th >>> (61-32)) | (tl << (32-(61-32)))) ^ ((tl >>> 6) | (th << (32-6))); - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - wh[j] = (c & 0xffff) | (d << 16); - wl[j] = (a & 0xffff) | (b << 16); - } - } - } - - // add - h = ah0; - l = al0; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = hh[0]; - l = hl[0]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - hh[0] = ah0 = (c & 0xffff) | (d << 16); - hl[0] = al0 = (a & 0xffff) | (b << 16); - - h = ah1; - l = al1; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = hh[1]; - l = hl[1]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - hh[1] = ah1 = (c & 0xffff) | (d << 16); - hl[1] = al1 = (a & 0xffff) | (b << 16); - - h = ah2; - l = al2; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = hh[2]; - l = hl[2]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - hh[2] = ah2 = (c & 0xffff) | (d << 16); - hl[2] = al2 = (a & 0xffff) | (b << 16); - - h = ah3; - l = al3; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = hh[3]; - l = hl[3]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - hh[3] = ah3 = (c & 0xffff) | (d << 16); - hl[3] = al3 = (a & 0xffff) | (b << 16); - - h = ah4; - l = al4; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = hh[4]; - l = hl[4]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - hh[4] = ah4 = (c & 0xffff) | (d << 16); - hl[4] = al4 = (a & 0xffff) | (b << 16); - - h = ah5; - l = al5; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = hh[5]; - l = hl[5]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - hh[5] = ah5 = (c & 0xffff) | (d << 16); - hl[5] = al5 = (a & 0xffff) | (b << 16); - - h = ah6; - l = al6; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = hh[6]; - l = hl[6]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - hh[6] = ah6 = (c & 0xffff) | (d << 16); - hl[6] = al6 = (a & 0xffff) | (b << 16); - - h = ah7; - l = al7; - - a = l & 0xffff; b = l >>> 16; - c = h & 0xffff; d = h >>> 16; - - h = hh[7]; - l = hl[7]; - - a += l & 0xffff; b += l >>> 16; - c += h & 0xffff; d += h >>> 16; - - b += a >>> 16; - c += b >>> 16; - d += c >>> 16; - - hh[7] = ah7 = (c & 0xffff) | (d << 16); - hl[7] = al7 = (a & 0xffff) | (b << 16); - - pos += 128; - n -= 128; - } - - return n; -} - -function crypto_hash(out, m, n) { - var hh = new Int32Array(8), - hl = new Int32Array(8), - x = new Uint8Array(256), - i, b = n; - - hh[0] = 0x6a09e667; - hh[1] = 0xbb67ae85; - hh[2] = 0x3c6ef372; - hh[3] = 0xa54ff53a; - hh[4] = 0x510e527f; - hh[5] = 0x9b05688c; - hh[6] = 0x1f83d9ab; - hh[7] = 0x5be0cd19; - - hl[0] = 0xf3bcc908; - hl[1] = 0x84caa73b; - hl[2] = 0xfe94f82b; - hl[3] = 0x5f1d36f1; - hl[4] = 0xade682d1; - hl[5] = 0x2b3e6c1f; - hl[6] = 0xfb41bd6b; - hl[7] = 0x137e2179; - - crypto_hashblocks_hl(hh, hl, m, n); - n %= 128; - - for (i = 0; i < n; i++) x[i] = m[b-n+i]; - x[n] = 128; - - n = 256-128*(n<112?1:0); - x[n-9] = 0; - ts64(x, n-8, (b / 0x20000000) | 0, b << 3); - crypto_hashblocks_hl(hh, hl, x, n); - - for (i = 0; i < 8; i++) ts64(out, 8*i, hh[i], hl[i]); - - return 0; -} - -function add(p, q) { - var a = gf(), b = gf(), c = gf(), - d = gf(), e = gf(), f = gf(), - g = gf(), h = gf(), t = gf(); - - Z(a, p[1], p[0]); - Z(t, q[1], q[0]); - M(a, a, t); - A(b, p[0], p[1]); - A(t, q[0], q[1]); - M(b, b, t); - M(c, p[3], q[3]); - M(c, c, D2); - M(d, p[2], q[2]); - A(d, d, d); - Z(e, b, a); - Z(f, d, c); - A(g, d, c); - A(h, b, a); - - M(p[0], e, f); - M(p[1], h, g); - M(p[2], g, f); - M(p[3], e, h); -} - -function cswap(p, q, b) { - var i; - for (i = 0; i < 4; i++) { - sel25519(p[i], q[i], b); - } -} - -function pack(r, p) { - var tx = gf(), ty = gf(), zi = gf(); - inv25519(zi, p[2]); - M(tx, p[0], zi); - M(ty, p[1], zi); - pack25519(r, ty); - r[31] ^= par25519(tx) << 7; -} - -function scalarmult(p, q, s) { - var b, i; - set25519(p[0], gf0); - set25519(p[1], gf1); - set25519(p[2], gf1); - set25519(p[3], gf0); - for (i = 255; i >= 0; --i) { - b = (s[(i/8)|0] >> (i&7)) & 1; - cswap(p, q, b); - add(q, p); - add(p, p); - cswap(p, q, b); - } -} - -function scalarbase(p, s) { - var q = [gf(), gf(), gf(), gf()]; - set25519(q[0], X); - set25519(q[1], Y); - set25519(q[2], gf1); - M(q[3], X, Y); - scalarmult(p, q, s); -} - -function crypto_sign_keypair(pk, sk, seeded) { - var d = new Uint8Array(64); - var p = [gf(), gf(), gf(), gf()]; - var i; - - if (!seeded) randombytes(sk, 32); - crypto_hash(d, sk, 32); - d[0] &= 248; - d[31] &= 127; - d[31] |= 64; - - scalarbase(p, d); - pack(pk, p); - - for (i = 0; i < 32; i++) sk[i+32] = pk[i]; - return 0; -} - -var L = new Float64Array([0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10]); - -function modL(r, x) { - var carry, i, j, k; - for (i = 63; i >= 32; --i) { - carry = 0; - for (j = i - 32, k = i - 12; j < k; ++j) { - x[j] += carry - 16 * x[i] * L[j - (i - 32)]; - carry = (x[j] + 128) >> 8; - x[j] -= carry * 256; - } - x[j] += carry; - x[i] = 0; - } - carry = 0; - for (j = 0; j < 32; j++) { - x[j] += carry - (x[31] >> 4) * L[j]; - carry = x[j] >> 8; - x[j] &= 255; - } - for (j = 0; j < 32; j++) x[j] -= carry * L[j]; - for (i = 0; i < 32; i++) { - x[i+1] += x[i] >> 8; - r[i] = x[i] & 255; - } -} - -function reduce(r) { - var x = new Float64Array(64), i; - for (i = 0; i < 64; i++) x[i] = r[i]; - for (i = 0; i < 64; i++) r[i] = 0; - modL(r, x); -} - -// Note: difference from C - smlen returned, not passed as argument. -function crypto_sign(sm, m, n, sk) { - var d = new Uint8Array(64), h = new Uint8Array(64), r = new Uint8Array(64); - var i, j, x = new Float64Array(64); - var p = [gf(), gf(), gf(), gf()]; - - crypto_hash(d, sk, 32); - d[0] &= 248; - d[31] &= 127; - d[31] |= 64; - - var smlen = n + 64; - for (i = 0; i < n; i++) sm[64 + i] = m[i]; - for (i = 0; i < 32; i++) sm[32 + i] = d[32 + i]; - - crypto_hash(r, sm.subarray(32), n+32); - reduce(r); - scalarbase(p, r); - pack(sm, p); - - for (i = 32; i < 64; i++) sm[i] = sk[i]; - crypto_hash(h, sm, n + 64); - reduce(h); - - for (i = 0; i < 64; i++) x[i] = 0; - for (i = 0; i < 32; i++) x[i] = r[i]; - for (i = 0; i < 32; i++) { - for (j = 0; j < 32; j++) { - x[i+j] += h[i] * d[j]; - } - } - - modL(sm.subarray(32), x); - return smlen; -} - -function unpackneg(r, p) { - var t = gf(), chk = gf(), num = gf(), - den = gf(), den2 = gf(), den4 = gf(), - den6 = gf(); - - set25519(r[2], gf1); - unpack25519(r[1], p); - S(num, r[1]); - M(den, num, D); - Z(num, num, r[2]); - A(den, r[2], den); - - S(den2, den); - S(den4, den2); - M(den6, den4, den2); - M(t, den6, num); - M(t, t, den); - - pow2523(t, t); - M(t, t, num); - M(t, t, den); - M(t, t, den); - M(r[0], t, den); - - S(chk, r[0]); - M(chk, chk, den); - if (neq25519(chk, num)) M(r[0], r[0], I); - - S(chk, r[0]); - M(chk, chk, den); - if (neq25519(chk, num)) return -1; - - if (par25519(r[0]) === (p[31]>>7)) Z(r[0], gf0, r[0]); - - M(r[3], r[0], r[1]); - return 0; -} - -function crypto_sign_open(m, sm, n, pk) { - var i, mlen; - var t = new Uint8Array(32), h = new Uint8Array(64); - var p = [gf(), gf(), gf(), gf()], - q = [gf(), gf(), gf(), gf()]; - - mlen = -1; - if (n < 64) return -1; - - if (unpackneg(q, pk)) return -1; - - for (i = 0; i < n; i++) m[i] = sm[i]; - for (i = 0; i < 32; i++) m[i+32] = pk[i]; - crypto_hash(h, m, n); - reduce(h); - scalarmult(p, q, h); - - scalarbase(q, sm.subarray(32)); - add(p, q); - pack(t, p); - - n -= 64; - if (crypto_verify_32(sm, 0, t, 0)) { - for (i = 0; i < n; i++) m[i] = 0; - return -1; - } - - for (i = 0; i < n; i++) m[i] = sm[i + 64]; - mlen = n; - return mlen; -} - -var crypto_secretbox_KEYBYTES = 32, - crypto_secretbox_NONCEBYTES = 24, - crypto_secretbox_ZEROBYTES = 32, - crypto_secretbox_BOXZEROBYTES = 16, - crypto_scalarmult_BYTES = 32, - crypto_scalarmult_SCALARBYTES = 32, - crypto_box_PUBLICKEYBYTES = 32, - crypto_box_SECRETKEYBYTES = 32, - crypto_box_BEFORENMBYTES = 32, - crypto_box_NONCEBYTES = crypto_secretbox_NONCEBYTES, - crypto_box_ZEROBYTES = crypto_secretbox_ZEROBYTES, - crypto_box_BOXZEROBYTES = crypto_secretbox_BOXZEROBYTES, - crypto_sign_BYTES = 64, - crypto_sign_PUBLICKEYBYTES = 32, - crypto_sign_SECRETKEYBYTES = 64, - crypto_sign_SEEDBYTES = 32, - crypto_hash_BYTES = 64; - -nacl.lowlevel = { - crypto_core_hsalsa20: crypto_core_hsalsa20, - crypto_stream_xor: crypto_stream_xor, - crypto_stream: crypto_stream, - crypto_stream_salsa20_xor: crypto_stream_salsa20_xor, - crypto_stream_salsa20: crypto_stream_salsa20, - crypto_onetimeauth: crypto_onetimeauth, - crypto_onetimeauth_verify: crypto_onetimeauth_verify, - crypto_verify_16: crypto_verify_16, - crypto_verify_32: crypto_verify_32, - crypto_secretbox: crypto_secretbox, - crypto_secretbox_open: crypto_secretbox_open, - crypto_scalarmult: crypto_scalarmult, - crypto_scalarmult_base: crypto_scalarmult_base, - crypto_box_beforenm: crypto_box_beforenm, - crypto_box_afternm: crypto_box_afternm, - crypto_box: crypto_box, - crypto_box_open: crypto_box_open, - crypto_box_keypair: crypto_box_keypair, - crypto_hash: crypto_hash, - crypto_sign: crypto_sign, - crypto_sign_keypair: crypto_sign_keypair, - crypto_sign_open: crypto_sign_open, - - crypto_secretbox_KEYBYTES: crypto_secretbox_KEYBYTES, - crypto_secretbox_NONCEBYTES: crypto_secretbox_NONCEBYTES, - crypto_secretbox_ZEROBYTES: crypto_secretbox_ZEROBYTES, - crypto_secretbox_BOXZEROBYTES: crypto_secretbox_BOXZEROBYTES, - crypto_scalarmult_BYTES: crypto_scalarmult_BYTES, - crypto_scalarmult_SCALARBYTES: crypto_scalarmult_SCALARBYTES, - crypto_box_PUBLICKEYBYTES: crypto_box_PUBLICKEYBYTES, - crypto_box_SECRETKEYBYTES: crypto_box_SECRETKEYBYTES, - crypto_box_BEFORENMBYTES: crypto_box_BEFORENMBYTES, - crypto_box_NONCEBYTES: crypto_box_NONCEBYTES, - crypto_box_ZEROBYTES: crypto_box_ZEROBYTES, - crypto_box_BOXZEROBYTES: crypto_box_BOXZEROBYTES, - crypto_sign_BYTES: crypto_sign_BYTES, - crypto_sign_PUBLICKEYBYTES: crypto_sign_PUBLICKEYBYTES, - crypto_sign_SECRETKEYBYTES: crypto_sign_SECRETKEYBYTES, - crypto_sign_SEEDBYTES: crypto_sign_SEEDBYTES, - crypto_hash_BYTES: crypto_hash_BYTES -}; - -/* High-level API */ - -function checkLengths(k, n) { - if (k.length !== crypto_secretbox_KEYBYTES) throw new Error('bad key size'); - if (n.length !== crypto_secretbox_NONCEBYTES) throw new Error('bad nonce size'); -} - -function checkBoxLengths(pk, sk) { - if (pk.length !== crypto_box_PUBLICKEYBYTES) throw new Error('bad public key size'); - if (sk.length !== crypto_box_SECRETKEYBYTES) throw new Error('bad secret key size'); -} - -function checkArrayTypes() { - var t, i; - for (i = 0; i < arguments.length; i++) { - if ((t = Object.prototype.toString.call(arguments[i])) !== '[object Uint8Array]') - throw new TypeError('unexpected type ' + t + ', use Uint8Array'); - } -} - -function cleanup(arr) { - for (var i = 0; i < arr.length; i++) arr[i] = 0; -} - -nacl.util = {}; - -nacl.util.decodeUTF8 = function(s) { - var i, d = unescape(encodeURIComponent(s)), b = new Uint8Array(d.length); - for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); - return b; -}; - -nacl.util.encodeUTF8 = function(arr) { - var i, s = []; - for (i = 0; i < arr.length; i++) s.push(String.fromCharCode(arr[i])); - return decodeURIComponent(escape(s.join(''))); -}; - -nacl.util.encodeBase64 = function(arr) { - if (typeof btoa === 'undefined') { - return (new Buffer(arr)).toString('base64'); - } else { - var i, s = [], len = arr.length; - for (i = 0; i < len; i++) s.push(String.fromCharCode(arr[i])); - return btoa(s.join('')); - } -}; - -nacl.util.decodeBase64 = function(s) { - if (typeof atob === 'undefined') { - return new Uint8Array(Array.prototype.slice.call(new Buffer(s, 'base64'), 0)); - } else { - var i, d = atob(s), b = new Uint8Array(d.length); - for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); - return b; - } -}; - -nacl.randomBytes = function(n) { - var b = new Uint8Array(n); - randombytes(b, n); - return b; -}; - -nacl.secretbox = function(msg, nonce, key) { - checkArrayTypes(msg, nonce, key); - checkLengths(key, nonce); - var m = new Uint8Array(crypto_secretbox_ZEROBYTES + msg.length); - var c = new Uint8Array(m.length); - for (var i = 0; i < msg.length; i++) m[i+crypto_secretbox_ZEROBYTES] = msg[i]; - crypto_secretbox(c, m, m.length, nonce, key); - return c.subarray(crypto_secretbox_BOXZEROBYTES); -}; - -nacl.secretbox.open = function(box, nonce, key) { - checkArrayTypes(box, nonce, key); - checkLengths(key, nonce); - var c = new Uint8Array(crypto_secretbox_BOXZEROBYTES + box.length); - var m = new Uint8Array(c.length); - for (var i = 0; i < box.length; i++) c[i+crypto_secretbox_BOXZEROBYTES] = box[i]; - if (c.length < 32) return false; - if (crypto_secretbox_open(m, c, c.length, nonce, key) !== 0) return false; - return m.subarray(crypto_secretbox_ZEROBYTES); -}; - -nacl.secretbox.keyLength = crypto_secretbox_KEYBYTES; -nacl.secretbox.nonceLength = crypto_secretbox_NONCEBYTES; -nacl.secretbox.overheadLength = crypto_secretbox_BOXZEROBYTES; - -nacl.scalarMult = function(n, p) { - checkArrayTypes(n, p); - if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); - if (p.length !== crypto_scalarmult_BYTES) throw new Error('bad p size'); - var q = new Uint8Array(crypto_scalarmult_BYTES); - crypto_scalarmult(q, n, p); - return q; -}; - -nacl.scalarMult.base = function(n) { - checkArrayTypes(n); - if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); - var q = new Uint8Array(crypto_scalarmult_BYTES); - crypto_scalarmult_base(q, n); - return q; -}; - -nacl.scalarMult.scalarLength = crypto_scalarmult_SCALARBYTES; -nacl.scalarMult.groupElementLength = crypto_scalarmult_BYTES; - -nacl.box = function(msg, nonce, publicKey, secretKey) { - var k = nacl.box.before(publicKey, secretKey); - return nacl.secretbox(msg, nonce, k); -}; - -nacl.box.before = function(publicKey, secretKey) { - checkArrayTypes(publicKey, secretKey); - checkBoxLengths(publicKey, secretKey); - var k = new Uint8Array(crypto_box_BEFORENMBYTES); - crypto_box_beforenm(k, publicKey, secretKey); - return k; -}; - -nacl.box.after = nacl.secretbox; - -nacl.box.open = function(msg, nonce, publicKey, secretKey) { - var k = nacl.box.before(publicKey, secretKey); - return nacl.secretbox.open(msg, nonce, k); -}; - -nacl.box.open.after = nacl.secretbox.open; - -nacl.box.keyPair = function() { - var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); - var sk = new Uint8Array(crypto_box_SECRETKEYBYTES); - crypto_box_keypair(pk, sk); - return {publicKey: pk, secretKey: sk}; -}; - -nacl.box.keyPair.fromSecretKey = function(secretKey) { - checkArrayTypes(secretKey); - if (secretKey.length !== crypto_box_SECRETKEYBYTES) - throw new Error('bad secret key size'); - var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); - crypto_scalarmult_base(pk, secretKey); - return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; -}; - -nacl.box.publicKeyLength = crypto_box_PUBLICKEYBYTES; -nacl.box.secretKeyLength = crypto_box_SECRETKEYBYTES; -nacl.box.sharedKeyLength = crypto_box_BEFORENMBYTES; -nacl.box.nonceLength = crypto_box_NONCEBYTES; -nacl.box.overheadLength = nacl.secretbox.overheadLength; - -nacl.sign = function(msg, secretKey) { - checkArrayTypes(msg, secretKey); - if (secretKey.length !== crypto_sign_SECRETKEYBYTES) - throw new Error('bad secret key size'); - var signedMsg = new Uint8Array(crypto_sign_BYTES+msg.length); - crypto_sign(signedMsg, msg, msg.length, secretKey); - return signedMsg; -}; - -nacl.sign.open = function(signedMsg, publicKey) { - if (arguments.length !== 2) - throw new Error('nacl.sign.open accepts 2 arguments; did you mean to use nacl.sign.detached.verify?'); - checkArrayTypes(signedMsg, publicKey); - if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) - throw new Error('bad public key size'); - var tmp = new Uint8Array(signedMsg.length); - var mlen = crypto_sign_open(tmp, signedMsg, signedMsg.length, publicKey); - if (mlen < 0) return null; - var m = new Uint8Array(mlen); - for (var i = 0; i < m.length; i++) m[i] = tmp[i]; - return m; -}; - -nacl.sign.detached = function(msg, secretKey) { - var signedMsg = nacl.sign(msg, secretKey); - var sig = new Uint8Array(crypto_sign_BYTES); - for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; - return sig; -}; - -nacl.sign.detached.verify = function(msg, sig, publicKey) { - checkArrayTypes(msg, sig, publicKey); - if (sig.length !== crypto_sign_BYTES) - throw new Error('bad signature size'); - if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) - throw new Error('bad public key size'); - var sm = new Uint8Array(crypto_sign_BYTES + msg.length); - var m = new Uint8Array(crypto_sign_BYTES + msg.length); - var i; - for (i = 0; i < crypto_sign_BYTES; i++) sm[i] = sig[i]; - for (i = 0; i < msg.length; i++) sm[i+crypto_sign_BYTES] = msg[i]; - return (crypto_sign_open(m, sm, sm.length, publicKey) >= 0); -}; - -nacl.sign.keyPair = function() { - var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); - var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); - crypto_sign_keypair(pk, sk); - return {publicKey: pk, secretKey: sk}; -}; - -nacl.sign.keyPair.fromSecretKey = function(secretKey) { - checkArrayTypes(secretKey); - if (secretKey.length !== crypto_sign_SECRETKEYBYTES) - throw new Error('bad secret key size'); - var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); - for (var i = 0; i < pk.length; i++) pk[i] = secretKey[32+i]; - return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; -}; - -nacl.sign.keyPair.fromSeed = function(seed) { - checkArrayTypes(seed); - if (seed.length !== crypto_sign_SEEDBYTES) - throw new Error('bad seed size'); - var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); - var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); - for (var i = 0; i < 32; i++) sk[i] = seed[i]; - crypto_sign_keypair(pk, sk, true); - return {publicKey: pk, secretKey: sk}; -}; - -nacl.sign.publicKeyLength = crypto_sign_PUBLICKEYBYTES; -nacl.sign.secretKeyLength = crypto_sign_SECRETKEYBYTES; -nacl.sign.seedLength = crypto_sign_SEEDBYTES; -nacl.sign.signatureLength = crypto_sign_BYTES; - -nacl.hash = function(msg) { - checkArrayTypes(msg); - var h = new Uint8Array(crypto_hash_BYTES); - crypto_hash(h, msg, msg.length); - return h; -}; - -nacl.hash.hashLength = crypto_hash_BYTES; - -nacl.verify = function(x, y) { - checkArrayTypes(x, y); - // Zero length arguments are considered not equal. - if (x.length === 0 || y.length === 0) return false; - if (x.length !== y.length) return false; - return (vn(x, 0, y, 0, x.length) === 0) ? true : false; -}; - -nacl.setPRNG = function(fn) { - randombytes = fn; -}; - -(function() { - // Initialize PRNG if environment provides CSPRNG. - // If not, methods calling randombytes will throw. - var crypto; - if (typeof window !== 'undefined') { - // Browser. - if (window.crypto && window.crypto.getRandomValues) { - crypto = window.crypto; // Standard - } else if (window.msCrypto && window.msCrypto.getRandomValues) { - crypto = window.msCrypto; // Internet Explorer 11+ - } - if (crypto) { - nacl.setPRNG(function(x, n) { - var i, v = new Uint8Array(n); - crypto.getRandomValues(v); - for (i = 0; i < n; i++) x[i] = v[i]; - cleanup(v); - }); - } - } else if (typeof require !== 'undefined') { - // Node.js. - crypto = require('crypto'); - if (crypto) { - nacl.setPRNG(function(x, n) { - var i, v = crypto.randomBytes(n); - for (i = 0; i < n; i++) x[i] = v[i]; - cleanup(v); - }); - } - } -})(); - -})(typeof module !== 'undefined' && module.exports ? module.exports : (window.nacl = window.nacl || {})); diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl-fast.min.js b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl-fast.min.js deleted file mode 100644 index 7072c2af..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl-fast.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(r){"use strict";function t(r,t,n,e){r[t]=n>>24&255,r[t+1]=n>>16&255,r[t+2]=n>>8&255,r[t+3]=255&n,r[t+4]=e>>24&255,r[t+5]=e>>16&255,r[t+6]=e>>8&255,r[t+7]=255&e}function n(r,t,n,e,o){var i,h=0;for(i=0;o>i;i++)h|=r[t+i]^n[e+i];return(1&h-1>>>8)-1}function e(r,t,e,o){return n(r,t,e,o,16)}function o(r,t,e,o){return n(r,t,e,o,32)}function i(r,t,n,e){for(var o,i=255&e[0]|(255&e[1])<<8|(255&e[2])<<16|(255&e[3])<<24,h=255&n[0]|(255&n[1])<<8|(255&n[2])<<16|(255&n[3])<<24,a=255&n[4]|(255&n[5])<<8|(255&n[6])<<16|(255&n[7])<<24,f=255&n[8]|(255&n[9])<<8|(255&n[10])<<16|(255&n[11])<<24,s=255&n[12]|(255&n[13])<<8|(255&n[14])<<16|(255&n[15])<<24,u=255&e[4]|(255&e[5])<<8|(255&e[6])<<16|(255&e[7])<<24,c=255&t[0]|(255&t[1])<<8|(255&t[2])<<16|(255&t[3])<<24,y=255&t[4]|(255&t[5])<<8|(255&t[6])<<16|(255&t[7])<<24,l=255&t[8]|(255&t[9])<<8|(255&t[10])<<16|(255&t[11])<<24,w=255&t[12]|(255&t[13])<<8|(255&t[14])<<16|(255&t[15])<<24,p=255&e[8]|(255&e[9])<<8|(255&e[10])<<16|(255&e[11])<<24,g=255&n[16]|(255&n[17])<<8|(255&n[18])<<16|(255&n[19])<<24,v=255&n[20]|(255&n[21])<<8|(255&n[22])<<16|(255&n[23])<<24,b=255&n[24]|(255&n[25])<<8|(255&n[26])<<16|(255&n[27])<<24,d=255&n[28]|(255&n[29])<<8|(255&n[30])<<16|(255&n[31])<<24,A=255&e[12]|(255&e[13])<<8|(255&e[14])<<16|(255&e[15])<<24,_=i,U=h,E=a,x=f,M=s,m=u,B=c,S=y,K=l,T=w,Y=p,k=g,L=v,C=b,R=d,z=A,P=0;20>P;P+=2)o=_+L|0,M^=o<<7|o>>>25,o=M+_|0,K^=o<<9|o>>>23,o=K+M|0,L^=o<<13|o>>>19,o=L+K|0,_^=o<<18|o>>>14,o=m+U|0,T^=o<<7|o>>>25,o=T+m|0,C^=o<<9|o>>>23,o=C+T|0,U^=o<<13|o>>>19,o=U+C|0,m^=o<<18|o>>>14,o=Y+B|0,R^=o<<7|o>>>25,o=R+Y|0,E^=o<<9|o>>>23,o=E+R|0,B^=o<<13|o>>>19,o=B+E|0,Y^=o<<18|o>>>14,o=z+k|0,x^=o<<7|o>>>25,o=x+z|0,S^=o<<9|o>>>23,o=S+x|0,k^=o<<13|o>>>19,o=k+S|0,z^=o<<18|o>>>14,o=_+x|0,U^=o<<7|o>>>25,o=U+_|0,E^=o<<9|o>>>23,o=E+U|0,x^=o<<13|o>>>19,o=x+E|0,_^=o<<18|o>>>14,o=m+M|0,B^=o<<7|o>>>25,o=B+m|0,S^=o<<9|o>>>23,o=S+B|0,M^=o<<13|o>>>19,o=M+S|0,m^=o<<18|o>>>14,o=Y+T|0,k^=o<<7|o>>>25,o=k+Y|0,K^=o<<9|o>>>23,o=K+k|0,T^=o<<13|o>>>19,o=T+K|0,Y^=o<<18|o>>>14,o=z+R|0,L^=o<<7|o>>>25,o=L+z|0,C^=o<<9|o>>>23,o=C+L|0,R^=o<<13|o>>>19,o=R+C|0,z^=o<<18|o>>>14;_=_+i|0,U=U+h|0,E=E+a|0,x=x+f|0,M=M+s|0,m=m+u|0,B=B+c|0,S=S+y|0,K=K+l|0,T=T+w|0,Y=Y+p|0,k=k+g|0,L=L+v|0,C=C+b|0,R=R+d|0,z=z+A|0,r[0]=_>>>0&255,r[1]=_>>>8&255,r[2]=_>>>16&255,r[3]=_>>>24&255,r[4]=U>>>0&255,r[5]=U>>>8&255,r[6]=U>>>16&255,r[7]=U>>>24&255,r[8]=E>>>0&255,r[9]=E>>>8&255,r[10]=E>>>16&255,r[11]=E>>>24&255,r[12]=x>>>0&255,r[13]=x>>>8&255,r[14]=x>>>16&255,r[15]=x>>>24&255,r[16]=M>>>0&255,r[17]=M>>>8&255,r[18]=M>>>16&255,r[19]=M>>>24&255,r[20]=m>>>0&255,r[21]=m>>>8&255,r[22]=m>>>16&255,r[23]=m>>>24&255,r[24]=B>>>0&255,r[25]=B>>>8&255,r[26]=B>>>16&255,r[27]=B>>>24&255,r[28]=S>>>0&255,r[29]=S>>>8&255,r[30]=S>>>16&255,r[31]=S>>>24&255,r[32]=K>>>0&255,r[33]=K>>>8&255,r[34]=K>>>16&255,r[35]=K>>>24&255,r[36]=T>>>0&255,r[37]=T>>>8&255,r[38]=T>>>16&255,r[39]=T>>>24&255,r[40]=Y>>>0&255,r[41]=Y>>>8&255,r[42]=Y>>>16&255,r[43]=Y>>>24&255,r[44]=k>>>0&255,r[45]=k>>>8&255,r[46]=k>>>16&255,r[47]=k>>>24&255,r[48]=L>>>0&255,r[49]=L>>>8&255,r[50]=L>>>16&255,r[51]=L>>>24&255,r[52]=C>>>0&255,r[53]=C>>>8&255,r[54]=C>>>16&255,r[55]=C>>>24&255,r[56]=R>>>0&255,r[57]=R>>>8&255,r[58]=R>>>16&255,r[59]=R>>>24&255,r[60]=z>>>0&255,r[61]=z>>>8&255,r[62]=z>>>16&255,r[63]=z>>>24&255}function h(r,t,n,e){for(var o,i=255&e[0]|(255&e[1])<<8|(255&e[2])<<16|(255&e[3])<<24,h=255&n[0]|(255&n[1])<<8|(255&n[2])<<16|(255&n[3])<<24,a=255&n[4]|(255&n[5])<<8|(255&n[6])<<16|(255&n[7])<<24,f=255&n[8]|(255&n[9])<<8|(255&n[10])<<16|(255&n[11])<<24,s=255&n[12]|(255&n[13])<<8|(255&n[14])<<16|(255&n[15])<<24,u=255&e[4]|(255&e[5])<<8|(255&e[6])<<16|(255&e[7])<<24,c=255&t[0]|(255&t[1])<<8|(255&t[2])<<16|(255&t[3])<<24,y=255&t[4]|(255&t[5])<<8|(255&t[6])<<16|(255&t[7])<<24,l=255&t[8]|(255&t[9])<<8|(255&t[10])<<16|(255&t[11])<<24,w=255&t[12]|(255&t[13])<<8|(255&t[14])<<16|(255&t[15])<<24,p=255&e[8]|(255&e[9])<<8|(255&e[10])<<16|(255&e[11])<<24,g=255&n[16]|(255&n[17])<<8|(255&n[18])<<16|(255&n[19])<<24,v=255&n[20]|(255&n[21])<<8|(255&n[22])<<16|(255&n[23])<<24,b=255&n[24]|(255&n[25])<<8|(255&n[26])<<16|(255&n[27])<<24,d=255&n[28]|(255&n[29])<<8|(255&n[30])<<16|(255&n[31])<<24,A=255&e[12]|(255&e[13])<<8|(255&e[14])<<16|(255&e[15])<<24,_=i,U=h,E=a,x=f,M=s,m=u,B=c,S=y,K=l,T=w,Y=p,k=g,L=v,C=b,R=d,z=A,P=0;20>P;P+=2)o=_+L|0,M^=o<<7|o>>>25,o=M+_|0,K^=o<<9|o>>>23,o=K+M|0,L^=o<<13|o>>>19,o=L+K|0,_^=o<<18|o>>>14,o=m+U|0,T^=o<<7|o>>>25,o=T+m|0,C^=o<<9|o>>>23,o=C+T|0,U^=o<<13|o>>>19,o=U+C|0,m^=o<<18|o>>>14,o=Y+B|0,R^=o<<7|o>>>25,o=R+Y|0,E^=o<<9|o>>>23,o=E+R|0,B^=o<<13|o>>>19,o=B+E|0,Y^=o<<18|o>>>14,o=z+k|0,x^=o<<7|o>>>25,o=x+z|0,S^=o<<9|o>>>23,o=S+x|0,k^=o<<13|o>>>19,o=k+S|0,z^=o<<18|o>>>14,o=_+x|0,U^=o<<7|o>>>25,o=U+_|0,E^=o<<9|o>>>23,o=E+U|0,x^=o<<13|o>>>19,o=x+E|0,_^=o<<18|o>>>14,o=m+M|0,B^=o<<7|o>>>25,o=B+m|0,S^=o<<9|o>>>23,o=S+B|0,M^=o<<13|o>>>19,o=M+S|0,m^=o<<18|o>>>14,o=Y+T|0,k^=o<<7|o>>>25,o=k+Y|0,K^=o<<9|o>>>23,o=K+k|0,T^=o<<13|o>>>19,o=T+K|0,Y^=o<<18|o>>>14,o=z+R|0,L^=o<<7|o>>>25,o=L+z|0,C^=o<<9|o>>>23,o=C+L|0,R^=o<<13|o>>>19,o=R+C|0,z^=o<<18|o>>>14;r[0]=_>>>0&255,r[1]=_>>>8&255,r[2]=_>>>16&255,r[3]=_>>>24&255,r[4]=m>>>0&255,r[5]=m>>>8&255,r[6]=m>>>16&255,r[7]=m>>>24&255,r[8]=Y>>>0&255,r[9]=Y>>>8&255,r[10]=Y>>>16&255,r[11]=Y>>>24&255,r[12]=z>>>0&255,r[13]=z>>>8&255,r[14]=z>>>16&255,r[15]=z>>>24&255,r[16]=B>>>0&255,r[17]=B>>>8&255,r[18]=B>>>16&255,r[19]=B>>>24&255,r[20]=S>>>0&255,r[21]=S>>>8&255,r[22]=S>>>16&255,r[23]=S>>>24&255,r[24]=K>>>0&255,r[25]=K>>>8&255,r[26]=K>>>16&255,r[27]=K>>>24&255,r[28]=T>>>0&255,r[29]=T>>>8&255,r[30]=T>>>16&255,r[31]=T>>>24&255}function a(r,t,n,e){i(r,t,n,e)}function f(r,t,n,e){h(r,t,n,e)}function s(r,t,n,e,o,i,h){var f,s,u=new Uint8Array(16),c=new Uint8Array(64);for(s=0;16>s;s++)u[s]=0;for(s=0;8>s;s++)u[s]=i[s];for(;o>=64;){for(a(c,u,h,cr),s=0;64>s;s++)r[t+s]=n[e+s]^c[s];for(f=1,s=8;16>s;s++)f=f+(255&u[s])|0,u[s]=255&f,f>>>=8;o-=64,t+=64,e+=64}if(o>0)for(a(c,u,h,cr),s=0;o>s;s++)r[t+s]=n[e+s]^c[s];return 0}function u(r,t,n,e,o){var i,h,f=new Uint8Array(16),s=new Uint8Array(64);for(h=0;16>h;h++)f[h]=0;for(h=0;8>h;h++)f[h]=e[h];for(;n>=64;){for(a(s,f,o,cr),h=0;64>h;h++)r[t+h]=s[h];for(i=1,h=8;16>h;h++)i=i+(255&f[h])|0,f[h]=255&i,i>>>=8;n-=64,t+=64}if(n>0)for(a(s,f,o,cr),h=0;n>h;h++)r[t+h]=s[h];return 0}function c(r,t,n,e,o){var i=new Uint8Array(32);f(i,e,o,cr);for(var h=new Uint8Array(8),a=0;8>a;a++)h[a]=e[a+16];return u(r,t,n,h,i)}function y(r,t,n,e,o,i,h){var a=new Uint8Array(32);f(a,i,h,cr);for(var u=new Uint8Array(8),c=0;8>c;c++)u[c]=i[c+16];return s(r,t,n,e,o,u,a)}function l(r,t,n,e,o,i){var h=new yr(i);return h.update(n,e,o),h.finish(r,t),0}function w(r,t,n,o,i,h){var a=new Uint8Array(16);return l(a,0,n,o,i,h),e(r,t,a,0)}function p(r,t,n,e,o){var i;if(32>n)return-1;for(y(r,0,t,0,n,e,o),l(r,16,r,32,n-32,r),i=0;16>i;i++)r[i]=0;return 0}function g(r,t,n,e,o){var i,h=new Uint8Array(32);if(32>n)return-1;if(c(h,0,32,e,o),0!==w(t,16,t,32,n-32,h))return-1;for(y(r,0,t,0,n,e,o),i=0;32>i;i++)r[i]=0;return 0}function v(r,t){var n;for(n=0;16>n;n++)r[n]=0|t[n]}function b(r){var t,n,e=1;for(t=0;16>t;t++)n=r[t]+e+65535,e=Math.floor(n/65536),r[t]=n-65536*e;r[0]+=e-1+37*(e-1)}function d(r,t,n){for(var e,o=~(n-1),i=0;16>i;i++)e=o&(r[i]^t[i]),r[i]^=e,t[i]^=e}function A(r,t){var n,e,o,i=$(),h=$();for(n=0;16>n;n++)h[n]=t[n];for(b(h),b(h),b(h),e=0;2>e;e++){for(i[0]=h[0]-65517,n=1;15>n;n++)i[n]=h[n]-65535-(i[n-1]>>16&1),i[n-1]&=65535;i[15]=h[15]-32767-(i[14]>>16&1),o=i[15]>>16&1,i[14]&=65535,d(h,i,1-o)}for(n=0;16>n;n++)r[2*n]=255&h[n],r[2*n+1]=h[n]>>8}function _(r,t){var n=new Uint8Array(32),e=new Uint8Array(32);return A(n,r),A(e,t),o(n,0,e,0)}function U(r){var t=new Uint8Array(32);return A(t,r),1&t[0]}function E(r,t){var n;for(n=0;16>n;n++)r[n]=t[2*n]+(t[2*n+1]<<8);r[15]&=32767}function x(r,t,n){for(var e=0;16>e;e++)r[e]=t[e]+n[e]}function M(r,t,n){for(var e=0;16>e;e++)r[e]=t[e]-n[e]}function m(r,t,n){var e,o,i=0,h=0,a=0,f=0,s=0,u=0,c=0,y=0,l=0,w=0,p=0,g=0,v=0,b=0,d=0,A=0,_=0,U=0,E=0,x=0,M=0,m=0,B=0,S=0,K=0,T=0,Y=0,k=0,L=0,C=0,R=0,z=n[0],P=n[1],O=n[2],N=n[3],F=n[4],I=n[5],j=n[6],G=n[7],Z=n[8],V=n[9],q=n[10],X=n[11],D=n[12],H=n[13],J=n[14],Q=n[15];e=t[0],i+=e*z,h+=e*P,a+=e*O,f+=e*N,s+=e*F,u+=e*I,c+=e*j,y+=e*G,l+=e*Z,w+=e*V,p+=e*q,g+=e*X,v+=e*D,b+=e*H,d+=e*J,A+=e*Q,e=t[1],h+=e*z,a+=e*P,f+=e*O,s+=e*N,u+=e*F,c+=e*I,y+=e*j,l+=e*G,w+=e*Z,p+=e*V,g+=e*q,v+=e*X,b+=e*D,d+=e*H,A+=e*J,_+=e*Q,e=t[2],a+=e*z,f+=e*P,s+=e*O,u+=e*N,c+=e*F,y+=e*I,l+=e*j,w+=e*G,p+=e*Z,g+=e*V,v+=e*q,b+=e*X,d+=e*D,A+=e*H,_+=e*J,U+=e*Q,e=t[3],f+=e*z,s+=e*P,u+=e*O,c+=e*N,y+=e*F,l+=e*I,w+=e*j,p+=e*G,g+=e*Z,v+=e*V,b+=e*q,d+=e*X,A+=e*D,_+=e*H,U+=e*J,E+=e*Q,e=t[4],s+=e*z,u+=e*P,c+=e*O,y+=e*N,l+=e*F,w+=e*I,p+=e*j,g+=e*G,v+=e*Z,b+=e*V,d+=e*q,A+=e*X,_+=e*D,U+=e*H,E+=e*J,x+=e*Q,e=t[5],u+=e*z,c+=e*P,y+=e*O,l+=e*N,w+=e*F,p+=e*I,g+=e*j,v+=e*G,b+=e*Z,d+=e*V,A+=e*q,_+=e*X,U+=e*D,E+=e*H,x+=e*J,M+=e*Q,e=t[6],c+=e*z,y+=e*P,l+=e*O,w+=e*N,p+=e*F,g+=e*I,v+=e*j,b+=e*G,d+=e*Z,A+=e*V,_+=e*q,U+=e*X,E+=e*D,x+=e*H,M+=e*J,m+=e*Q,e=t[7],y+=e*z,l+=e*P,w+=e*O,p+=e*N,g+=e*F,v+=e*I,b+=e*j,d+=e*G,A+=e*Z,_+=e*V,U+=e*q,E+=e*X,x+=e*D,M+=e*H,m+=e*J,B+=e*Q,e=t[8],l+=e*z,w+=e*P,p+=e*O,g+=e*N,v+=e*F,b+=e*I,d+=e*j,A+=e*G,_+=e*Z,U+=e*V,E+=e*q,x+=e*X,M+=e*D,m+=e*H,B+=e*J,S+=e*Q,e=t[9],w+=e*z,p+=e*P,g+=e*O,v+=e*N,b+=e*F,d+=e*I,A+=e*j,_+=e*G,U+=e*Z,E+=e*V,x+=e*q,M+=e*X,m+=e*D,B+=e*H,S+=e*J,K+=e*Q,e=t[10],p+=e*z,g+=e*P,v+=e*O,b+=e*N,d+=e*F,A+=e*I,_+=e*j,U+=e*G,E+=e*Z,x+=e*V,M+=e*q,m+=e*X,B+=e*D,S+=e*H,K+=e*J,T+=e*Q,e=t[11],g+=e*z,v+=e*P,b+=e*O,d+=e*N,A+=e*F,_+=e*I,U+=e*j,E+=e*G,x+=e*Z,M+=e*V,m+=e*q,B+=e*X,S+=e*D,K+=e*H,T+=e*J,Y+=e*Q,e=t[12],v+=e*z,b+=e*P,d+=e*O,A+=e*N,_+=e*F,U+=e*I,E+=e*j,x+=e*G,M+=e*Z,m+=e*V,B+=e*q,S+=e*X,K+=e*D,T+=e*H,Y+=e*J,k+=e*Q,e=t[13],b+=e*z,d+=e*P,A+=e*O,_+=e*N,U+=e*F,E+=e*I,x+=e*j,M+=e*G,m+=e*Z,B+=e*V,S+=e*q,K+=e*X,T+=e*D,Y+=e*H,k+=e*J,L+=e*Q,e=t[14],d+=e*z,A+=e*P,_+=e*O,U+=e*N,E+=e*F,x+=e*I,M+=e*j,m+=e*G,B+=e*Z,S+=e*V,K+=e*q,T+=e*X,Y+=e*D,k+=e*H,L+=e*J,C+=e*Q,e=t[15],A+=e*z,_+=e*P,U+=e*O,E+=e*N,x+=e*F,M+=e*I,m+=e*j,B+=e*G,S+=e*Z,K+=e*V,T+=e*q,Y+=e*X,k+=e*D,L+=e*H,C+=e*J,R+=e*Q,i+=38*_,h+=38*U,a+=38*E,f+=38*x,s+=38*M,u+=38*m,c+=38*B,y+=38*S,l+=38*K,w+=38*T,p+=38*Y,g+=38*k,v+=38*L,b+=38*C,d+=38*R,o=1,e=i+o+65535,o=Math.floor(e/65536),i=e-65536*o,e=h+o+65535,o=Math.floor(e/65536),h=e-65536*o,e=a+o+65535,o=Math.floor(e/65536),a=e-65536*o,e=f+o+65535,o=Math.floor(e/65536),f=e-65536*o,e=s+o+65535,o=Math.floor(e/65536),s=e-65536*o,e=u+o+65535,o=Math.floor(e/65536),u=e-65536*o,e=c+o+65535,o=Math.floor(e/65536),c=e-65536*o,e=y+o+65535,o=Math.floor(e/65536),y=e-65536*o,e=l+o+65535,o=Math.floor(e/65536),l=e-65536*o,e=w+o+65535,o=Math.floor(e/65536),w=e-65536*o,e=p+o+65535,o=Math.floor(e/65536),p=e-65536*o,e=g+o+65535,o=Math.floor(e/65536),g=e-65536*o,e=v+o+65535,o=Math.floor(e/65536),v=e-65536*o,e=b+o+65535,o=Math.floor(e/65536),b=e-65536*o,e=d+o+65535,o=Math.floor(e/65536),d=e-65536*o,e=A+o+65535,o=Math.floor(e/65536),A=e-65536*o,i+=o-1+37*(o-1),o=1,e=i+o+65535,o=Math.floor(e/65536),i=e-65536*o,e=h+o+65535,o=Math.floor(e/65536),h=e-65536*o,e=a+o+65535,o=Math.floor(e/65536),a=e-65536*o,e=f+o+65535,o=Math.floor(e/65536),f=e-65536*o,e=s+o+65535,o=Math.floor(e/65536),s=e-65536*o,e=u+o+65535,o=Math.floor(e/65536),u=e-65536*o,e=c+o+65535,o=Math.floor(e/65536),c=e-65536*o,e=y+o+65535,o=Math.floor(e/65536),y=e-65536*o,e=l+o+65535,o=Math.floor(e/65536),l=e-65536*o,e=w+o+65535,o=Math.floor(e/65536),w=e-65536*o,e=p+o+65535,o=Math.floor(e/65536),p=e-65536*o,e=g+o+65535,o=Math.floor(e/65536),g=e-65536*o,e=v+o+65535,o=Math.floor(e/65536),v=e-65536*o,e=b+o+65535,o=Math.floor(e/65536),b=e-65536*o,e=d+o+65535,o=Math.floor(e/65536),d=e-65536*o,e=A+o+65535,o=Math.floor(e/65536),A=e-65536*o,i+=o-1+37*(o-1),r[0]=i,r[1]=h,r[2]=a,r[3]=f,r[4]=s,r[5]=u,r[6]=c,r[7]=y,r[8]=l,r[9]=w,r[10]=p,r[11]=g,r[12]=v,r[13]=b,r[14]=d,r[15]=A}function B(r,t){m(r,t,t)}function S(r,t){var n,e=$();for(n=0;16>n;n++)e[n]=t[n];for(n=253;n>=0;n--)B(e,e),2!==n&&4!==n&&m(e,e,t);for(n=0;16>n;n++)r[n]=e[n]}function K(r,t){var n,e=$();for(n=0;16>n;n++)e[n]=t[n];for(n=250;n>=0;n--)B(e,e),1!==n&&m(e,e,t);for(n=0;16>n;n++)r[n]=e[n]}function T(r,t,n){var e,o,i=new Uint8Array(32),h=new Float64Array(80),a=$(),f=$(),s=$(),u=$(),c=$(),y=$();for(o=0;31>o;o++)i[o]=t[o];for(i[31]=127&t[31]|64,i[0]&=248,E(h,n),o=0;16>o;o++)f[o]=h[o],u[o]=a[o]=s[o]=0;for(a[0]=u[0]=1,o=254;o>=0;--o)e=i[o>>>3]>>>(7&o)&1,d(a,f,e),d(s,u,e),x(c,a,s),M(a,a,s),x(s,f,u),M(f,f,u),B(u,c),B(y,a),m(a,s,a),m(s,f,c),x(c,a,s),M(a,a,s),B(f,a),M(s,u,y),m(a,s,ir),x(a,a,u),m(s,s,a),m(a,u,y),m(u,f,h),B(f,c),d(a,f,e),d(s,u,e);for(o=0;16>o;o++)h[o+16]=a[o],h[o+32]=s[o],h[o+48]=f[o],h[o+64]=u[o];var l=h.subarray(32),w=h.subarray(16);return S(l,l),m(w,w,l),A(r,w),0}function Y(r,t){return T(r,t,nr)}function k(r,t){return rr(t,32),Y(r,t)}function L(r,t,n){var e=new Uint8Array(32);return T(e,n,t),f(r,tr,e,cr)}function C(r,t,n,e,o,i){var h=new Uint8Array(32);return L(h,o,i),lr(r,t,n,e,h)}function R(r,t,n,e,o,i){var h=new Uint8Array(32);return L(h,o,i),wr(r,t,n,e,h)}function z(r,t,n,e){for(var o,i,h,a,f,s,u,c,y,l,w,p,g,v,b,d,A,_,U,E,x,M,m,B,S,K,T=new Int32Array(16),Y=new Int32Array(16),k=r[0],L=r[1],C=r[2],R=r[3],z=r[4],P=r[5],O=r[6],N=r[7],F=t[0],I=t[1],j=t[2],G=t[3],Z=t[4],V=t[5],q=t[6],X=t[7],D=0;e>=128;){for(U=0;16>U;U++)E=8*U+D,T[U]=n[E+0]<<24|n[E+1]<<16|n[E+2]<<8|n[E+3],Y[U]=n[E+4]<<24|n[E+5]<<16|n[E+6]<<8|n[E+7];for(U=0;80>U;U++)if(o=k,i=L,h=C,a=R,f=z,s=P,u=O,c=N,y=F,l=I,w=j,p=G,g=Z,v=V,b=q,d=X,x=N,M=X,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=(z>>>14|Z<<18)^(z>>>18|Z<<14)^(Z>>>9|z<<23),M=(Z>>>14|z<<18)^(Z>>>18|z<<14)^(z>>>9|Z<<23),m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,x=z&P^~z&O,M=Z&V^~Z&q,m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,x=pr[2*U],M=pr[2*U+1],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,x=T[U%16],M=Y[U%16],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,A=65535&S|K<<16,_=65535&m|B<<16,x=A,M=_,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=(k>>>28|F<<4)^(F>>>2|k<<30)^(F>>>7|k<<25),M=(F>>>28|k<<4)^(k>>>2|F<<30)^(k>>>7|F<<25),m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,x=k&L^k&C^L&C,M=F&I^F&j^I&j,m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,c=65535&S|K<<16,d=65535&m|B<<16,x=a,M=p,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=A,M=_,m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,a=65535&S|K<<16,p=65535&m|B<<16,L=o,C=i,R=h,z=a,P=f,O=s,N=u,k=c,I=y,j=l,G=w,Z=p,V=g,q=v,X=b,F=d,U%16===15)for(E=0;16>E;E++)x=T[E],M=Y[E],m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=T[(E+9)%16],M=Y[(E+9)%16],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,A=T[(E+1)%16],_=Y[(E+1)%16],x=(A>>>1|_<<31)^(A>>>8|_<<24)^A>>>7,M=(_>>>1|A<<31)^(_>>>8|A<<24)^(_>>>7|A<<25),m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,A=T[(E+14)%16],_=Y[(E+14)%16],x=(A>>>19|_<<13)^(_>>>29|A<<3)^A>>>6,M=(_>>>19|A<<13)^(A>>>29|_<<3)^(_>>>6|A<<26),m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,T[E]=65535&S|K<<16,Y[E]=65535&m|B<<16;x=k,M=F,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=r[0],M=t[0],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,r[0]=k=65535&S|K<<16,t[0]=F=65535&m|B<<16,x=L,M=I,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=r[1],M=t[1],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,r[1]=L=65535&S|K<<16,t[1]=I=65535&m|B<<16,x=C,M=j,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=r[2],M=t[2],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,r[2]=C=65535&S|K<<16,t[2]=j=65535&m|B<<16,x=R,M=G,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=r[3],M=t[3],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,r[3]=R=65535&S|K<<16,t[3]=G=65535&m|B<<16,x=z,M=Z,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=r[4],M=t[4],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,r[4]=z=65535&S|K<<16,t[4]=Z=65535&m|B<<16,x=P,M=V,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=r[5],M=t[5],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,r[5]=P=65535&S|K<<16,t[5]=V=65535&m|B<<16,x=O,M=q,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=r[6],M=t[6],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,r[6]=O=65535&S|K<<16,t[6]=q=65535&m|B<<16,x=N,M=X,m=65535&M,B=M>>>16,S=65535&x,K=x>>>16,x=r[7],M=t[7],m+=65535&M,B+=M>>>16,S+=65535&x,K+=x>>>16,B+=m>>>16,S+=B>>>16,K+=S>>>16,r[7]=N=65535&S|K<<16,t[7]=X=65535&m|B<<16,D+=128,e-=128}return e}function P(r,n,e){var o,i=new Int32Array(8),h=new Int32Array(8),a=new Uint8Array(256),f=e;for(i[0]=1779033703,i[1]=3144134277,i[2]=1013904242,i[3]=2773480762,i[4]=1359893119,i[5]=2600822924,i[6]=528734635,i[7]=1541459225,h[0]=4089235720,h[1]=2227873595,h[2]=4271175723,h[3]=1595750129,h[4]=2917565137,h[5]=725511199,h[6]=4215389547,h[7]=327033209,z(i,h,n,e),e%=128,o=0;e>o;o++)a[o]=n[f-e+o];for(a[e]=128,e=256-128*(112>e?1:0),a[e-9]=0,t(a,e-8,f/536870912|0,f<<3),z(i,h,a,e),o=0;8>o;o++)t(r,8*o,i[o],h[o]);return 0}function O(r,t){var n=$(),e=$(),o=$(),i=$(),h=$(),a=$(),f=$(),s=$(),u=$();M(n,r[1],r[0]),M(u,t[1],t[0]),m(n,n,u),x(e,r[0],r[1]),x(u,t[0],t[1]),m(e,e,u),m(o,r[3],t[3]),m(o,o,ar),m(i,r[2],t[2]),x(i,i,i),M(h,e,n),M(a,i,o),x(f,i,o),x(s,e,n),m(r[0],h,a),m(r[1],s,f),m(r[2],f,a),m(r[3],h,s)}function N(r,t,n){var e;for(e=0;4>e;e++)d(r[e],t[e],n)}function F(r,t){var n=$(),e=$(),o=$();S(o,t[2]),m(n,t[0],o),m(e,t[1],o),A(r,e),r[31]^=U(n)<<7}function I(r,t,n){var e,o;for(v(r[0],er),v(r[1],or),v(r[2],or),v(r[3],er),o=255;o>=0;--o)e=n[o/8|0]>>(7&o)&1,N(r,t,e),O(t,r),O(r,r),N(r,t,e)}function j(r,t){var n=[$(),$(),$(),$()];v(n[0],fr),v(n[1],sr),v(n[2],or),m(n[3],fr,sr),I(r,n,t)}function G(r,t,n){var e,o=new Uint8Array(64),i=[$(),$(),$(),$()];for(n||rr(t,32),P(o,t,32),o[0]&=248,o[31]&=127,o[31]|=64,j(i,o),F(r,i),e=0;32>e;e++)t[e+32]=r[e];return 0}function Z(r,t){var n,e,o,i;for(e=63;e>=32;--e){for(n=0,o=e-32,i=e-12;i>o;++o)t[o]+=n-16*t[e]*gr[o-(e-32)],n=t[o]+128>>8,t[o]-=256*n;t[o]+=n,t[e]=0}for(n=0,o=0;32>o;o++)t[o]+=n-(t[31]>>4)*gr[o],n=t[o]>>8,t[o]&=255;for(o=0;32>o;o++)t[o]-=n*gr[o];for(e=0;32>e;e++)t[e+1]+=t[e]>>8,r[e]=255&t[e]}function V(r){var t,n=new Float64Array(64);for(t=0;64>t;t++)n[t]=r[t];for(t=0;64>t;t++)r[t]=0;Z(r,n)}function q(r,t,n,e){var o,i,h=new Uint8Array(64),a=new Uint8Array(64),f=new Uint8Array(64),s=new Float64Array(64),u=[$(),$(),$(),$()];P(h,e,32),h[0]&=248,h[31]&=127,h[31]|=64;var c=n+64;for(o=0;n>o;o++)r[64+o]=t[o];for(o=0;32>o;o++)r[32+o]=h[32+o];for(P(f,r.subarray(32),n+32),V(f),j(u,f),F(r,u),o=32;64>o;o++)r[o]=e[o];for(P(a,r,n+64),V(a),o=0;64>o;o++)s[o]=0;for(o=0;32>o;o++)s[o]=f[o];for(o=0;32>o;o++)for(i=0;32>i;i++)s[o+i]+=a[o]*h[i];return Z(r.subarray(32),s),c}function X(r,t){var n=$(),e=$(),o=$(),i=$(),h=$(),a=$(),f=$();return v(r[2],or),E(r[1],t),B(o,r[1]),m(i,o,hr),M(o,o,r[2]),x(i,r[2],i),B(h,i),B(a,h),m(f,a,h),m(n,f,o),m(n,n,i),K(n,n),m(n,n,o),m(n,n,i),m(n,n,i),m(r[0],n,i),B(e,r[0]),m(e,e,i),_(e,o)&&m(r[0],r[0],ur),B(e,r[0]),m(e,e,i),_(e,o)?-1:(U(r[0])===t[31]>>7&&M(r[0],er,r[0]),m(r[3],r[0],r[1]),0)}function D(r,t,n,e){var i,h,a=new Uint8Array(32),f=new Uint8Array(64),s=[$(),$(),$(),$()],u=[$(),$(),$(),$()];if(h=-1,64>n)return-1;if(X(u,e))return-1;for(i=0;n>i;i++)r[i]=t[i];for(i=0;32>i;i++)r[i+32]=e[i];if(P(f,r,n),V(f),I(s,u,f),j(u,t.subarray(32)),O(s,u),F(a,s),n-=64,o(t,0,a,0)){for(i=0;n>i;i++)r[i]=0;return-1}for(i=0;n>i;i++)r[i]=t[i+64];return h=n}function H(r,t){if(r.length!==vr)throw new Error("bad key size");if(t.length!==br)throw new Error("bad nonce size")}function J(r,t){if(r.length!==Er)throw new Error("bad public key size");if(t.length!==xr)throw new Error("bad secret key size")}function Q(){var r,t;for(t=0;t>>13|n<<3),e=255&r[4]|(255&r[5])<<8,this.r[2]=7939&(n>>>10|e<<6),o=255&r[6]|(255&r[7])<<8,this.r[3]=8191&(e>>>7|o<<9),i=255&r[8]|(255&r[9])<<8,this.r[4]=255&(o>>>4|i<<12),this.r[5]=i>>>1&8190,h=255&r[10]|(255&r[11])<<8,this.r[6]=8191&(i>>>14|h<<2),a=255&r[12]|(255&r[13])<<8,this.r[7]=8065&(h>>>11|a<<5),f=255&r[14]|(255&r[15])<<8,this.r[8]=8191&(a>>>8|f<<8),this.r[9]=f>>>5&127,this.pad[0]=255&r[16]|(255&r[17])<<8,this.pad[1]=255&r[18]|(255&r[19])<<8,this.pad[2]=255&r[20]|(255&r[21])<<8,this.pad[3]=255&r[22]|(255&r[23])<<8,this.pad[4]=255&r[24]|(255&r[25])<<8,this.pad[5]=255&r[26]|(255&r[27])<<8,this.pad[6]=255&r[28]|(255&r[29])<<8,this.pad[7]=255&r[30]|(255&r[31])<<8};yr.prototype.blocks=function(r,t,n){for(var e,o,i,h,a,f,s,u,c,y,l,w,p,g,v,b,d,A,_,U=this.fin?0:2048,E=this.h[0],x=this.h[1],M=this.h[2],m=this.h[3],B=this.h[4],S=this.h[5],K=this.h[6],T=this.h[7],Y=this.h[8],k=this.h[9],L=this.r[0],C=this.r[1],R=this.r[2],z=this.r[3],P=this.r[4],O=this.r[5],N=this.r[6],F=this.r[7],I=this.r[8],j=this.r[9];n>=16;)e=255&r[t+0]|(255&r[t+1])<<8,E+=8191&e,o=255&r[t+2]|(255&r[t+3])<<8,x+=8191&(e>>>13|o<<3),i=255&r[t+4]|(255&r[t+5])<<8,M+=8191&(o>>>10|i<<6),h=255&r[t+6]|(255&r[t+7])<<8,m+=8191&(i>>>7|h<<9),a=255&r[t+8]|(255&r[t+9])<<8,B+=8191&(h>>>4|a<<12),S+=a>>>1&8191,f=255&r[t+10]|(255&r[t+11])<<8,K+=8191&(a>>>14|f<<2),s=255&r[t+12]|(255&r[t+13])<<8,T+=8191&(f>>>11|s<<5),u=255&r[t+14]|(255&r[t+15])<<8,Y+=8191&(s>>>8|u<<8),k+=u>>>5|U,c=0,y=c,y+=E*L,y+=5*x*j,y+=5*M*I,y+=5*m*F,y+=5*B*N,c=y>>>13,y&=8191,y+=5*S*O,y+=5*K*P,y+=5*T*z,y+=5*Y*R,y+=5*k*C,c+=y>>>13,y&=8191,l=c,l+=E*C,l+=x*L,l+=5*M*j,l+=5*m*I,l+=5*B*F,c=l>>>13,l&=8191,l+=5*S*N,l+=5*K*O,l+=5*T*P,l+=5*Y*z,l+=5*k*R,c+=l>>>13,l&=8191,w=c,w+=E*R,w+=x*C,w+=M*L,w+=5*m*j,w+=5*B*I,c=w>>>13,w&=8191,w+=5*S*F,w+=5*K*N,w+=5*T*O,w+=5*Y*P,w+=5*k*z,c+=w>>>13,w&=8191,p=c,p+=E*z,p+=x*R,p+=M*C,p+=m*L,p+=5*B*j,c=p>>>13,p&=8191,p+=5*S*I,p+=5*K*F,p+=5*T*N,p+=5*Y*O,p+=5*k*P,c+=p>>>13,p&=8191,g=c,g+=E*P,g+=x*z,g+=M*R,g+=m*C,g+=B*L,c=g>>>13,g&=8191,g+=5*S*j,g+=5*K*I,g+=5*T*F,g+=5*Y*N,g+=5*k*O,c+=g>>>13,g&=8191,v=c,v+=E*O,v+=x*P,v+=M*z,v+=m*R,v+=B*C,c=v>>>13,v&=8191,v+=S*L,v+=5*K*j,v+=5*T*I,v+=5*Y*F,v+=5*k*N,c+=v>>>13,v&=8191,b=c,b+=E*N,b+=x*O,b+=M*P,b+=m*z,b+=B*R,c=b>>>13,b&=8191,b+=S*C,b+=K*L,b+=5*T*j,b+=5*Y*I,b+=5*k*F,c+=b>>>13,b&=8191,d=c,d+=E*F,d+=x*N,d+=M*O,d+=m*P,d+=B*z,c=d>>>13,d&=8191,d+=S*R,d+=K*C,d+=T*L,d+=5*Y*j,d+=5*k*I,c+=d>>>13,d&=8191,A=c,A+=E*I,A+=x*F,A+=M*N,A+=m*O,A+=B*P,c=A>>>13,A&=8191,A+=S*z,A+=K*R,A+=T*C,A+=Y*L,A+=5*k*j,c+=A>>>13,A&=8191,_=c,_+=E*j,_+=x*I,_+=M*F,_+=m*N,_+=B*O,c=_>>>13,_&=8191,_+=S*P,_+=K*z,_+=T*R,_+=Y*C,_+=k*L,c+=_>>>13,_&=8191,c=(c<<2)+c|0,c=c+y|0,y=8191&c,c>>>=13,l+=c,E=y,x=l,M=w,m=p,B=g,S=v,K=b,T=d,Y=A,k=_,t+=16,n-=16;this.h[0]=E,this.h[1]=x,this.h[2]=M,this.h[3]=m,this.h[4]=B,this.h[5]=S,this.h[6]=K,this.h[7]=T,this.h[8]=Y,this.h[9]=k},yr.prototype.finish=function(r,t){var n,e,o,i,h=new Uint16Array(10);if(this.leftover){for(i=this.leftover,this.buffer[i++]=1;16>i;i++)this.buffer[i]=0;this.fin=1,this.blocks(this.buffer,0,16)}for(n=this.h[1]>>>13,this.h[1]&=8191,i=2;10>i;i++)this.h[i]+=n,n=this.h[i]>>>13,this.h[i]&=8191;for(this.h[0]+=5*n,n=this.h[0]>>>13,this.h[0]&=8191,this.h[1]+=n,n=this.h[1]>>>13,this.h[1]&=8191,this.h[2]+=n,h[0]=this.h[0]+5,n=h[0]>>>13,h[0]&=8191,i=1;10>i;i++)h[i]=this.h[i]+n,n=h[i]>>>13,h[i]&=8191;for(h[9]-=8192,e=(h[9]>>>15)-1,i=0;10>i;i++)h[i]&=e;for(e=~e,i=0;10>i;i++)this.h[i]=this.h[i]&e|h[i];for(this.h[0]=65535&(this.h[0]|this.h[1]<<13),this.h[1]=65535&(this.h[1]>>>3|this.h[2]<<10),this.h[2]=65535&(this.h[2]>>>6|this.h[3]<<7),this.h[3]=65535&(this.h[3]>>>9|this.h[4]<<4),this.h[4]=65535&(this.h[4]>>>12|this.h[5]<<1|this.h[6]<<14),this.h[5]=65535&(this.h[6]>>>2|this.h[7]<<11),this.h[6]=65535&(this.h[7]>>>5|this.h[8]<<8),this.h[7]=65535&(this.h[8]>>>8|this.h[9]<<5),o=this.h[0]+this.pad[0],this.h[0]=65535&o,i=1;8>i;i++)o=(this.h[i]+this.pad[i]|0)+(o>>>16)|0,this.h[i]=65535&o;r[t+0]=this.h[0]>>>0&255,r[t+1]=this.h[0]>>>8&255,r[t+2]=this.h[1]>>>0&255,r[t+3]=this.h[1]>>>8&255,r[t+4]=this.h[2]>>>0&255,r[t+5]=this.h[2]>>>8&255,r[t+6]=this.h[3]>>>0&255,r[t+7]=this.h[3]>>>8&255,r[t+8]=this.h[4]>>>0&255,r[t+9]=this.h[4]>>>8&255,r[t+10]=this.h[5]>>>0&255,r[t+11]=this.h[5]>>>8&255,r[t+12]=this.h[6]>>>0&255,r[t+13]=this.h[6]>>>8&255,r[t+14]=this.h[7]>>>0&255,r[t+15]=this.h[7]>>>8&255},yr.prototype.update=function(r,t,n){var e,o;if(this.leftover){for(o=16-this.leftover,o>n&&(o=n),e=0;o>e;e++)this.buffer[this.leftover+e]=r[t+e];if(n-=o,t+=o,this.leftover+=o,this.leftover<16)return;this.blocks(this.buffer,0,16),this.leftover=0}if(n>=16&&(o=n-n%16,this.blocks(r,t,o),t+=o,n-=o),n){for(e=0;n>e;e++)this.buffer[this.leftover+e]=r[t+e];this.leftover+=n}};var lr=p,wr=g,pr=[1116352408,3609767458,1899447441,602891725,3049323471,3964484399,3921009573,2173295548,961987163,4081628472,1508970993,3053834265,2453635748,2937671579,2870763221,3664609560,3624381080,2734883394,310598401,1164996542,607225278,1323610764,1426881987,3590304994,1925078388,4068182383,2162078206,991336113,2614888103,633803317,3248222580,3479774868,3835390401,2666613458,4022224774,944711139,264347078,2341262773,604807628,2007800933,770255983,1495990901,1249150122,1856431235,1555081692,3175218132,1996064986,2198950837,2554220882,3999719339,2821834349,766784016,2952996808,2566594879,3210313671,3203337956,3336571891,1034457026,3584528711,2466948901,113926993,3758326383,338241895,168717936,666307205,1188179964,773529912,1546045734,1294757372,1522805485,1396182291,2643833823,1695183700,2343527390,1986661051,1014477480,2177026350,1206759142,2456956037,344077627,2730485921,1290863460,2820302411,3158454273,3259730800,3505952657,3345764771,106217008,3516065817,3606008344,3600352804,1432725776,4094571909,1467031594,275423344,851169720,430227734,3100823752,506948616,1363258195,659060556,3750685593,883997877,3785050280,958139571,3318307427,1322822218,3812723403,1537002063,2003034995,1747873779,3602036899,1955562222,1575990012,2024104815,1125592928,2227730452,2716904306,2361852424,442776044,2428436474,593698344,2756734187,3733110249,3204031479,2999351573,3329325298,3815920427,3391569614,3928383900,3515267271,566280711,3940187606,3454069534,4118630271,4000239992,116418474,1914138554,174292421,2731055270,289380356,3203993006,460393269,320620315,685471733,587496836,852142971,1086792851,1017036298,365543100,1126000580,2618297676,1288033470,3409855158,1501505948,4234509866,1607167915,987167468,1816402316,1246189591],gr=new Float64Array([237,211,245,92,26,99,18,88,214,156,247,162,222,249,222,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16]),vr=32,br=24,dr=32,Ar=16,_r=32,Ur=32,Er=32,xr=32,Mr=32,mr=br,Br=dr,Sr=Ar,Kr=64,Tr=32,Yr=64,kr=32,Lr=64;r.lowlevel={crypto_core_hsalsa20:f,crypto_stream_xor:y,crypto_stream:c,crypto_stream_salsa20_xor:s,crypto_stream_salsa20:u,crypto_onetimeauth:l,crypto_onetimeauth_verify:w,crypto_verify_16:e,crypto_verify_32:o,crypto_secretbox:p,crypto_secretbox_open:g,crypto_scalarmult:T,crypto_scalarmult_base:Y,crypto_box_beforenm:L,crypto_box_afternm:lr,crypto_box:C,crypto_box_open:R,crypto_box_keypair:k,crypto_hash:P,crypto_sign:q,crypto_sign_keypair:G,crypto_sign_open:D,crypto_secretbox_KEYBYTES:vr,crypto_secretbox_NONCEBYTES:br,crypto_secretbox_ZEROBYTES:dr,crypto_secretbox_BOXZEROBYTES:Ar,crypto_scalarmult_BYTES:_r,crypto_scalarmult_SCALARBYTES:Ur,crypto_box_PUBLICKEYBYTES:Er,crypto_box_SECRETKEYBYTES:xr,crypto_box_BEFORENMBYTES:Mr,crypto_box_NONCEBYTES:mr,crypto_box_ZEROBYTES:Br,crypto_box_BOXZEROBYTES:Sr,crypto_sign_BYTES:Kr,crypto_sign_PUBLICKEYBYTES:Tr,crypto_sign_SECRETKEYBYTES:Yr,crypto_sign_SEEDBYTES:kr,crypto_hash_BYTES:Lr},r.util={},r.util.decodeUTF8=function(r){var t,n=unescape(encodeURIComponent(r)),e=new Uint8Array(n.length);for(t=0;tt;t++)n.push(String.fromCharCode(r[t]));return btoa(n.join(""))},r.util.decodeBase64=function(r){if("undefined"==typeof atob)return new Uint8Array(Array.prototype.slice.call(new Buffer(r,"base64"),0));var t,n=atob(r),e=new Uint8Array(n.length);for(t=0;te)return null;for(var o=new Uint8Array(e),i=0;ie;e++)o[e]=t[e];for(e=0;e=0},r.sign.keyPair=function(){var r=new Uint8Array(Tr),t=new Uint8Array(Yr);return G(r,t),{publicKey:r,secretKey:t}},r.sign.keyPair.fromSecretKey=function(r){if(Q(r),r.length!==Yr)throw new Error("bad secret key size");for(var t=new Uint8Array(Tr),n=0;ne;e++)n[e]=r[e];return G(t,n,!0),{publicKey:t,secretKey:n}},r.sign.publicKeyLength=Tr,r.sign.secretKeyLength=Yr,r.sign.seedLength=kr,r.sign.signatureLength=Kr,r.hash=function(r){Q(r);var t=new Uint8Array(Lr);return P(t,r,r.length),t},r.hash.hashLength=Lr,r.verify=function(r,t){return Q(r,t),0===r.length||0===t.length?!1:r.length!==t.length?!1:0===n(r,0,t,0,r.length)?!0:!1},r.setPRNG=function(r){rr=r},function(){var t;"undefined"!=typeof window?(window.crypto&&window.crypto.getRandomValues?t=window.crypto:window.msCrypto&&window.msCrypto.getRandomValues&&(t=window.msCrypto),t&&r.setPRNG(function(r,n){var e,o=new Uint8Array(n);for(t.getRandomValues(o),e=0;n>e;e++)r[e]=o[e];W(o)})):"undefined"!=typeof require&&(t=require("crypto"),t&&r.setPRNG(function(r,n){var e,o=t.randomBytes(n);for(e=0;n>e;e++)r[e]=o[e];W(o)}))}()}("undefined"!=typeof module&&module.exports?module.exports:window.nacl=window.nacl||{}); \ No newline at end of file diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl.js b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl.js deleted file mode 100644 index b8edbbee..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl.js +++ /dev/null @@ -1,1205 +0,0 @@ -(function(nacl) { -'use strict'; - -// Ported in 2014 by Dmitry Chestnykh and Devi Mandiri. -// Public domain. -// -// Implementation derived from TweetNaCl version 20140427. -// See for details: http://tweetnacl.cr.yp.to/ - -var u64 = function(h, l) { this.hi = h|0 >>> 0; this.lo = l|0 >>> 0; }; -var gf = function(init) { - var i, r = new Float64Array(16); - if (init) for (i = 0; i < init.length; i++) r[i] = init[i]; - return r; -}; - -// Pluggable, initialized in high-level API below. -var randombytes = function(/* x, n */) { throw new Error('no PRNG'); }; - -var _0 = new Uint8Array(16); -var _9 = new Uint8Array(32); _9[0] = 9; - -var gf0 = gf(), - gf1 = gf([1]), - _121665 = gf([0xdb41, 1]), - D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]), - D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]), - X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]), - Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]), - I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]); - -function L32(x, c) { return (x << c) | (x >>> (32 - c)); } - -function ld32(x, i) { - var u = x[i+3] & 0xff; - u = (u<<8)|(x[i+2] & 0xff); - u = (u<<8)|(x[i+1] & 0xff); - return (u<<8)|(x[i+0] & 0xff); -} - -function dl64(x, i) { - var h = (x[i] << 24) | (x[i+1] << 16) | (x[i+2] << 8) | x[i+3]; - var l = (x[i+4] << 24) | (x[i+5] << 16) | (x[i+6] << 8) | x[i+7]; - return new u64(h, l); -} - -function st32(x, j, u) { - var i; - for (i = 0; i < 4; i++) { x[j+i] = u & 255; u >>>= 8; } -} - -function ts64(x, i, u) { - x[i] = (u.hi >> 24) & 0xff; - x[i+1] = (u.hi >> 16) & 0xff; - x[i+2] = (u.hi >> 8) & 0xff; - x[i+3] = u.hi & 0xff; - x[i+4] = (u.lo >> 24) & 0xff; - x[i+5] = (u.lo >> 16) & 0xff; - x[i+6] = (u.lo >> 8) & 0xff; - x[i+7] = u.lo & 0xff; -} - -function vn(x, xi, y, yi, n) { - var i,d = 0; - for (i = 0; i < n; i++) d |= x[xi+i]^y[yi+i]; - return (1 & ((d - 1) >>> 8)) - 1; -} - -function crypto_verify_16(x, xi, y, yi) { - return vn(x,xi,y,yi,16); -} - -function crypto_verify_32(x, xi, y, yi) { - return vn(x,xi,y,yi,32); -} - -function core(out,inp,k,c,h) { - var w = new Uint32Array(16), x = new Uint32Array(16), - y = new Uint32Array(16), t = new Uint32Array(4); - var i, j, m; - - for (i = 0; i < 4; i++) { - x[5*i] = ld32(c, 4*i); - x[1+i] = ld32(k, 4*i); - x[6+i] = ld32(inp, 4*i); - x[11+i] = ld32(k, 16+4*i); - } - - for (i = 0; i < 16; i++) y[i] = x[i]; - - for (i = 0; i < 20; i++) { - for (j = 0; j < 4; j++) { - for (m = 0; m < 4; m++) t[m] = x[(5*j+4*m)%16]; - t[1] ^= L32((t[0]+t[3])|0, 7); - t[2] ^= L32((t[1]+t[0])|0, 9); - t[3] ^= L32((t[2]+t[1])|0,13); - t[0] ^= L32((t[3]+t[2])|0,18); - for (m = 0; m < 4; m++) w[4*j+(j+m)%4] = t[m]; - } - for (m = 0; m < 16; m++) x[m] = w[m]; - } - - if (h) { - for (i = 0; i < 16; i++) x[i] = (x[i] + y[i]) | 0; - for (i = 0; i < 4; i++) { - x[5*i] = (x[5*i] - ld32(c, 4*i)) | 0; - x[6+i] = (x[6+i] - ld32(inp, 4*i)) | 0; - } - for (i = 0; i < 4; i++) { - st32(out,4*i,x[5*i]); - st32(out,16+4*i,x[6+i]); - } - } else { - for (i = 0; i < 16; i++) st32(out, 4 * i, (x[i] + y[i]) | 0); - } -} - -function crypto_core_salsa20(out,inp,k,c) { - core(out,inp,k,c,false); - return 0; -} - -function crypto_core_hsalsa20(out,inp,k,c) { - core(out,inp,k,c,true); - return 0; -} - -var sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]); - // "expand 32-byte k" - -function crypto_stream_salsa20_xor(c,cpos,m,mpos,b,n,k) { - var z = new Uint8Array(16), x = new Uint8Array(64); - var u, i; - if (!b) return 0; - for (i = 0; i < 16; i++) z[i] = 0; - for (i = 0; i < 8; i++) z[i] = n[i]; - while (b >= 64) { - crypto_core_salsa20(x,z,k,sigma); - for (i = 0; i < 64; i++) c[cpos+i] = (m?m[mpos+i]:0) ^ x[i]; - u = 1; - for (i = 8; i < 16; i++) { - u = u + (z[i] & 0xff) | 0; - z[i] = u & 0xff; - u >>>= 8; - } - b -= 64; - cpos += 64; - if (m) mpos += 64; - } - if (b > 0) { - crypto_core_salsa20(x,z,k,sigma); - for (i = 0; i < b; i++) c[cpos+i] = (m?m[mpos+i]:0) ^ x[i]; - } - return 0; -} - -function crypto_stream_salsa20(c,cpos,d,n,k) { - return crypto_stream_salsa20_xor(c,cpos,null,0,d,n,k); -} - -function crypto_stream(c,cpos,d,n,k) { - var s = new Uint8Array(32); - crypto_core_hsalsa20(s,n,k,sigma); - return crypto_stream_salsa20(c,cpos,d,n.subarray(16),s); -} - -function crypto_stream_xor(c,cpos,m,mpos,d,n,k) { - var s = new Uint8Array(32); - crypto_core_hsalsa20(s,n,k,sigma); - return crypto_stream_salsa20_xor(c,cpos,m,mpos,d,n.subarray(16),s); -} - -function add1305(h, c) { - var j, u = 0; - for (j = 0; j < 17; j++) { - u = (u + ((h[j] + c[j]) | 0)) | 0; - h[j] = u & 255; - u >>>= 8; - } -} - -var minusp = new Uint32Array([ - 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252 -]); - -function crypto_onetimeauth(out, outpos, m, mpos, n, k) { - var s, i, j, u; - var x = new Uint32Array(17), r = new Uint32Array(17), - h = new Uint32Array(17), c = new Uint32Array(17), - g = new Uint32Array(17); - for (j = 0; j < 17; j++) r[j]=h[j]=0; - for (j = 0; j < 16; j++) r[j]=k[j]; - r[3]&=15; - r[4]&=252; - r[7]&=15; - r[8]&=252; - r[11]&=15; - r[12]&=252; - r[15]&=15; - - while (n > 0) { - for (j = 0; j < 17; j++) c[j] = 0; - for (j = 0; (j < 16) && (j < n); ++j) c[j] = m[mpos+j]; - c[j] = 1; - mpos += j; n -= j; - add1305(h,c); - for (i = 0; i < 17; i++) { - x[i] = 0; - for (j = 0; j < 17; j++) x[i] = (x[i] + (h[j] * ((j <= i) ? r[i - j] : ((320 * r[i + 17 - j])|0))) | 0) | 0; - } - for (i = 0; i < 17; i++) h[i] = x[i]; - u = 0; - for (j = 0; j < 16; j++) { - u = (u + h[j]) | 0; - h[j] = u & 255; - u >>>= 8; - } - u = (u + h[16]) | 0; h[16] = u & 3; - u = (5 * (u >>> 2)) | 0; - for (j = 0; j < 16; j++) { - u = (u + h[j]) | 0; - h[j] = u & 255; - u >>>= 8; - } - u = (u + h[16]) | 0; h[16] = u; - } - - for (j = 0; j < 17; j++) g[j] = h[j]; - add1305(h,minusp); - s = (-(h[16] >>> 7) | 0); - for (j = 0; j < 17; j++) h[j] ^= s & (g[j] ^ h[j]); - - for (j = 0; j < 16; j++) c[j] = k[j + 16]; - c[16] = 0; - add1305(h,c); - for (j = 0; j < 16; j++) out[outpos+j] = h[j]; - return 0; -} - -function crypto_onetimeauth_verify(h, hpos, m, mpos, n, k) { - var x = new Uint8Array(16); - crypto_onetimeauth(x,0,m,mpos,n,k); - return crypto_verify_16(h,hpos,x,0); -} - -function crypto_secretbox(c,m,d,n,k) { - var i; - if (d < 32) return -1; - crypto_stream_xor(c,0,m,0,d,n,k); - crypto_onetimeauth(c, 16, c, 32, d - 32, c); - for (i = 0; i < 16; i++) c[i] = 0; - return 0; -} - -function crypto_secretbox_open(m,c,d,n,k) { - var i; - var x = new Uint8Array(32); - if (d < 32) return -1; - crypto_stream(x,0,32,n,k); - if (crypto_onetimeauth_verify(c, 16,c, 32,d - 32,x) !== 0) return -1; - crypto_stream_xor(m,0,c,0,d,n,k); - for (i = 0; i < 32; i++) m[i] = 0; - return 0; -} - -function set25519(r, a) { - var i; - for (i = 0; i < 16; i++) r[i] = a[i]|0; -} - -function car25519(o) { - var c; - var i; - for (i = 0; i < 16; i++) { - o[i] += 65536; - c = Math.floor(o[i] / 65536); - o[(i+1)*(i<15?1:0)] += c - 1 + 37 * (c-1) * (i===15?1:0); - o[i] -= (c * 65536); - } -} - -function sel25519(p, q, b) { - var t, c = ~(b-1); - for (var i = 0; i < 16; i++) { - t = c & (p[i] ^ q[i]); - p[i] ^= t; - q[i] ^= t; - } -} - -function pack25519(o, n) { - var i, j, b; - var m = gf(), t = gf(); - for (i = 0; i < 16; i++) t[i] = n[i]; - car25519(t); - car25519(t); - car25519(t); - for (j = 0; j < 2; j++) { - m[0] = t[0] - 0xffed; - for (i = 1; i < 15; i++) { - m[i] = t[i] - 0xffff - ((m[i-1]>>16) & 1); - m[i-1] &= 0xffff; - } - m[15] = t[15] - 0x7fff - ((m[14]>>16) & 1); - b = (m[15]>>16) & 1; - m[14] &= 0xffff; - sel25519(t, m, 1-b); - } - for (i = 0; i < 16; i++) { - o[2*i] = t[i] & 0xff; - o[2*i+1] = t[i]>>8; - } -} - -function neq25519(a, b) { - var c = new Uint8Array(32), d = new Uint8Array(32); - pack25519(c, a); - pack25519(d, b); - return crypto_verify_32(c, 0, d, 0); -} - -function par25519(a) { - var d = new Uint8Array(32); - pack25519(d, a); - return d[0] & 1; -} - -function unpack25519(o, n) { - var i; - for (i = 0; i < 16; i++) o[i] = n[2*i] + (n[2*i+1] << 8); - o[15] &= 0x7fff; -} - -function A(o, a, b) { - var i; - for (i = 0; i < 16; i++) o[i] = (a[i] + b[i])|0; -} - -function Z(o, a, b) { - var i; - for (i = 0; i < 16; i++) o[i] = (a[i] - b[i])|0; -} - -function M(o, a, b) { - var i, j, t = new Float64Array(31); - for (i = 0; i < 31; i++) t[i] = 0; - for (i = 0; i < 16; i++) { - for (j = 0; j < 16; j++) { - t[i+j] += a[i] * b[j]; - } - } - for (i = 0; i < 15; i++) { - t[i] += 38 * t[i+16]; - } - for (i = 0; i < 16; i++) o[i] = t[i]; - car25519(o); - car25519(o); -} - -function S(o, a) { - M(o, a, a); -} - -function inv25519(o, i) { - var c = gf(); - var a; - for (a = 0; a < 16; a++) c[a] = i[a]; - for (a = 253; a >= 0; a--) { - S(c, c); - if(a !== 2 && a !== 4) M(c, c, i); - } - for (a = 0; a < 16; a++) o[a] = c[a]; -} - -function pow2523(o, i) { - var c = gf(); - var a; - for (a = 0; a < 16; a++) c[a] = i[a]; - for (a = 250; a >= 0; a--) { - S(c, c); - if(a !== 1) M(c, c, i); - } - for (a = 0; a < 16; a++) o[a] = c[a]; -} - -function crypto_scalarmult(q, n, p) { - var z = new Uint8Array(32); - var x = new Float64Array(80), r, i; - var a = gf(), b = gf(), c = gf(), - d = gf(), e = gf(), f = gf(); - for (i = 0; i < 31; i++) z[i] = n[i]; - z[31]=(n[31]&127)|64; - z[0]&=248; - unpack25519(x,p); - for (i = 0; i < 16; i++) { - b[i]=x[i]; - d[i]=a[i]=c[i]=0; - } - a[0]=d[0]=1; - for (i=254; i>=0; --i) { - r=(z[i>>>3]>>>(i&7))&1; - sel25519(a,b,r); - sel25519(c,d,r); - A(e,a,c); - Z(a,a,c); - A(c,b,d); - Z(b,b,d); - S(d,e); - S(f,a); - M(a,c,a); - M(c,b,e); - A(e,a,c); - Z(a,a,c); - S(b,a); - Z(c,d,f); - M(a,c,_121665); - A(a,a,d); - M(c,c,a); - M(a,d,f); - M(d,b,x); - S(b,e); - sel25519(a,b,r); - sel25519(c,d,r); - } - for (i = 0; i < 16; i++) { - x[i+16]=a[i]; - x[i+32]=c[i]; - x[i+48]=b[i]; - x[i+64]=d[i]; - } - var x32 = x.subarray(32); - var x16 = x.subarray(16); - inv25519(x32,x32); - M(x16,x16,x32); - pack25519(q,x16); - return 0; -} - -function crypto_scalarmult_base(q, n) { - return crypto_scalarmult(q, n, _9); -} - -function crypto_box_keypair(y, x) { - randombytes(x, 32); - return crypto_scalarmult_base(y, x); -} - -function crypto_box_beforenm(k, y, x) { - var s = new Uint8Array(32); - crypto_scalarmult(s, x, y); - return crypto_core_hsalsa20(k, _0, s, sigma); -} - -var crypto_box_afternm = crypto_secretbox; -var crypto_box_open_afternm = crypto_secretbox_open; - -function crypto_box(c, m, d, n, y, x) { - var k = new Uint8Array(32); - crypto_box_beforenm(k, y, x); - return crypto_box_afternm(c, m, d, n, k); -} - -function crypto_box_open(m, c, d, n, y, x) { - var k = new Uint8Array(32); - crypto_box_beforenm(k, y, x); - return crypto_box_open_afternm(m, c, d, n, k); -} - -function add64() { - var a = 0, b = 0, c = 0, d = 0, m16 = 65535, l, h, i; - for (i = 0; i < arguments.length; i++) { - l = arguments[i].lo; - h = arguments[i].hi; - a += (l & m16); b += (l >>> 16); - c += (h & m16); d += (h >>> 16); - } - - b += (a >>> 16); - c += (b >>> 16); - d += (c >>> 16); - - return new u64((c & m16) | (d << 16), (a & m16) | (b << 16)); -} - -function shr64(x, c) { - return new u64((x.hi >>> c), (x.lo >>> c) | (x.hi << (32 - c))); -} - -function xor64() { - var l = 0, h = 0, i; - for (i = 0; i < arguments.length; i++) { - l ^= arguments[i].lo; - h ^= arguments[i].hi; - } - return new u64(h, l); -} - -function R(x, c) { - var h, l, c1 = 32 - c; - if (c < 32) { - h = (x.hi >>> c) | (x.lo << c1); - l = (x.lo >>> c) | (x.hi << c1); - } else if (c < 64) { - h = (x.lo >>> c) | (x.hi << c1); - l = (x.hi >>> c) | (x.lo << c1); - } - return new u64(h, l); -} - -function Ch(x, y, z) { - var h = (x.hi & y.hi) ^ (~x.hi & z.hi), - l = (x.lo & y.lo) ^ (~x.lo & z.lo); - return new u64(h, l); -} - -function Maj(x, y, z) { - var h = (x.hi & y.hi) ^ (x.hi & z.hi) ^ (y.hi & z.hi), - l = (x.lo & y.lo) ^ (x.lo & z.lo) ^ (y.lo & z.lo); - return new u64(h, l); -} - -function Sigma0(x) { return xor64(R(x,28), R(x,34), R(x,39)); } -function Sigma1(x) { return xor64(R(x,14), R(x,18), R(x,41)); } -function sigma0(x) { return xor64(R(x, 1), R(x, 8), shr64(x,7)); } -function sigma1(x) { return xor64(R(x,19), R(x,61), shr64(x,6)); } - -var K = [ - new u64(0x428a2f98, 0xd728ae22), new u64(0x71374491, 0x23ef65cd), - new u64(0xb5c0fbcf, 0xec4d3b2f), new u64(0xe9b5dba5, 0x8189dbbc), - new u64(0x3956c25b, 0xf348b538), new u64(0x59f111f1, 0xb605d019), - new u64(0x923f82a4, 0xaf194f9b), new u64(0xab1c5ed5, 0xda6d8118), - new u64(0xd807aa98, 0xa3030242), new u64(0x12835b01, 0x45706fbe), - new u64(0x243185be, 0x4ee4b28c), new u64(0x550c7dc3, 0xd5ffb4e2), - new u64(0x72be5d74, 0xf27b896f), new u64(0x80deb1fe, 0x3b1696b1), - new u64(0x9bdc06a7, 0x25c71235), new u64(0xc19bf174, 0xcf692694), - new u64(0xe49b69c1, 0x9ef14ad2), new u64(0xefbe4786, 0x384f25e3), - new u64(0x0fc19dc6, 0x8b8cd5b5), new u64(0x240ca1cc, 0x77ac9c65), - new u64(0x2de92c6f, 0x592b0275), new u64(0x4a7484aa, 0x6ea6e483), - new u64(0x5cb0a9dc, 0xbd41fbd4), new u64(0x76f988da, 0x831153b5), - new u64(0x983e5152, 0xee66dfab), new u64(0xa831c66d, 0x2db43210), - new u64(0xb00327c8, 0x98fb213f), new u64(0xbf597fc7, 0xbeef0ee4), - new u64(0xc6e00bf3, 0x3da88fc2), new u64(0xd5a79147, 0x930aa725), - new u64(0x06ca6351, 0xe003826f), new u64(0x14292967, 0x0a0e6e70), - new u64(0x27b70a85, 0x46d22ffc), new u64(0x2e1b2138, 0x5c26c926), - new u64(0x4d2c6dfc, 0x5ac42aed), new u64(0x53380d13, 0x9d95b3df), - new u64(0x650a7354, 0x8baf63de), new u64(0x766a0abb, 0x3c77b2a8), - new u64(0x81c2c92e, 0x47edaee6), new u64(0x92722c85, 0x1482353b), - new u64(0xa2bfe8a1, 0x4cf10364), new u64(0xa81a664b, 0xbc423001), - new u64(0xc24b8b70, 0xd0f89791), new u64(0xc76c51a3, 0x0654be30), - new u64(0xd192e819, 0xd6ef5218), new u64(0xd6990624, 0x5565a910), - new u64(0xf40e3585, 0x5771202a), new u64(0x106aa070, 0x32bbd1b8), - new u64(0x19a4c116, 0xb8d2d0c8), new u64(0x1e376c08, 0x5141ab53), - new u64(0x2748774c, 0xdf8eeb99), new u64(0x34b0bcb5, 0xe19b48a8), - new u64(0x391c0cb3, 0xc5c95a63), new u64(0x4ed8aa4a, 0xe3418acb), - new u64(0x5b9cca4f, 0x7763e373), new u64(0x682e6ff3, 0xd6b2b8a3), - new u64(0x748f82ee, 0x5defb2fc), new u64(0x78a5636f, 0x43172f60), - new u64(0x84c87814, 0xa1f0ab72), new u64(0x8cc70208, 0x1a6439ec), - new u64(0x90befffa, 0x23631e28), new u64(0xa4506ceb, 0xde82bde9), - new u64(0xbef9a3f7, 0xb2c67915), new u64(0xc67178f2, 0xe372532b), - new u64(0xca273ece, 0xea26619c), new u64(0xd186b8c7, 0x21c0c207), - new u64(0xeada7dd6, 0xcde0eb1e), new u64(0xf57d4f7f, 0xee6ed178), - new u64(0x06f067aa, 0x72176fba), new u64(0x0a637dc5, 0xa2c898a6), - new u64(0x113f9804, 0xbef90dae), new u64(0x1b710b35, 0x131c471b), - new u64(0x28db77f5, 0x23047d84), new u64(0x32caab7b, 0x40c72493), - new u64(0x3c9ebe0a, 0x15c9bebc), new u64(0x431d67c4, 0x9c100d4c), - new u64(0x4cc5d4be, 0xcb3e42b6), new u64(0x597f299c, 0xfc657e2a), - new u64(0x5fcb6fab, 0x3ad6faec), new u64(0x6c44198c, 0x4a475817) -]; - -function crypto_hashblocks(x, m, n) { - var z = [], b = [], a = [], w = [], t, i, j; - - for (i = 0; i < 8; i++) z[i] = a[i] = dl64(x, 8*i); - - var pos = 0; - while (n >= 128) { - for (i = 0; i < 16; i++) w[i] = dl64(m, 8*i+pos); - for (i = 0; i < 80; i++) { - for (j = 0; j < 8; j++) b[j] = a[j]; - t = add64(a[7], Sigma1(a[4]), Ch(a[4], a[5], a[6]), K[i], w[i%16]); - b[7] = add64(t, Sigma0(a[0]), Maj(a[0], a[1], a[2])); - b[3] = add64(b[3], t); - for (j = 0; j < 8; j++) a[(j+1)%8] = b[j]; - if (i%16 === 15) { - for (j = 0; j < 16; j++) { - w[j] = add64(w[j], w[(j+9)%16], sigma0(w[(j+1)%16]), sigma1(w[(j+14)%16])); - } - } - } - - for (i = 0; i < 8; i++) { - a[i] = add64(a[i], z[i]); - z[i] = a[i]; - } - - pos += 128; - n -= 128; - } - - for (i = 0; i < 8; i++) ts64(x, 8*i, z[i]); - return n; -} - -var iv = new Uint8Array([ - 0x6a,0x09,0xe6,0x67,0xf3,0xbc,0xc9,0x08, - 0xbb,0x67,0xae,0x85,0x84,0xca,0xa7,0x3b, - 0x3c,0x6e,0xf3,0x72,0xfe,0x94,0xf8,0x2b, - 0xa5,0x4f,0xf5,0x3a,0x5f,0x1d,0x36,0xf1, - 0x51,0x0e,0x52,0x7f,0xad,0xe6,0x82,0xd1, - 0x9b,0x05,0x68,0x8c,0x2b,0x3e,0x6c,0x1f, - 0x1f,0x83,0xd9,0xab,0xfb,0x41,0xbd,0x6b, - 0x5b,0xe0,0xcd,0x19,0x13,0x7e,0x21,0x79 -]); - -function crypto_hash(out, m, n) { - var h = new Uint8Array(64), x = new Uint8Array(256); - var i, b = n; - - for (i = 0; i < 64; i++) h[i] = iv[i]; - - crypto_hashblocks(h, m, n); - n %= 128; - - for (i = 0; i < 256; i++) x[i] = 0; - for (i = 0; i < n; i++) x[i] = m[b-n+i]; - x[n] = 128; - - n = 256-128*(n<112?1:0); - x[n-9] = 0; - ts64(x, n-8, new u64((b / 0x20000000) | 0, b << 3)); - crypto_hashblocks(h, x, n); - - for (i = 0; i < 64; i++) out[i] = h[i]; - - return 0; -} - -function add(p, q) { - var a = gf(), b = gf(), c = gf(), - d = gf(), e = gf(), f = gf(), - g = gf(), h = gf(), t = gf(); - - Z(a, p[1], p[0]); - Z(t, q[1], q[0]); - M(a, a, t); - A(b, p[0], p[1]); - A(t, q[0], q[1]); - M(b, b, t); - M(c, p[3], q[3]); - M(c, c, D2); - M(d, p[2], q[2]); - A(d, d, d); - Z(e, b, a); - Z(f, d, c); - A(g, d, c); - A(h, b, a); - - M(p[0], e, f); - M(p[1], h, g); - M(p[2], g, f); - M(p[3], e, h); -} - -function cswap(p, q, b) { - var i; - for (i = 0; i < 4; i++) { - sel25519(p[i], q[i], b); - } -} - -function pack(r, p) { - var tx = gf(), ty = gf(), zi = gf(); - inv25519(zi, p[2]); - M(tx, p[0], zi); - M(ty, p[1], zi); - pack25519(r, ty); - r[31] ^= par25519(tx) << 7; -} - -function scalarmult(p, q, s) { - var b, i; - set25519(p[0], gf0); - set25519(p[1], gf1); - set25519(p[2], gf1); - set25519(p[3], gf0); - for (i = 255; i >= 0; --i) { - b = (s[(i/8)|0] >> (i&7)) & 1; - cswap(p, q, b); - add(q, p); - add(p, p); - cswap(p, q, b); - } -} - -function scalarbase(p, s) { - var q = [gf(), gf(), gf(), gf()]; - set25519(q[0], X); - set25519(q[1], Y); - set25519(q[2], gf1); - M(q[3], X, Y); - scalarmult(p, q, s); -} - -function crypto_sign_keypair(pk, sk, seeded) { - var d = new Uint8Array(64); - var p = [gf(), gf(), gf(), gf()]; - var i; - - if (!seeded) randombytes(sk, 32); - crypto_hash(d, sk, 32); - d[0] &= 248; - d[31] &= 127; - d[31] |= 64; - - scalarbase(p, d); - pack(pk, p); - - for (i = 0; i < 32; i++) sk[i+32] = pk[i]; - return 0; -} - -var L = new Float64Array([0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10]); - -function modL(r, x) { - var carry, i, j, k; - for (i = 63; i >= 32; --i) { - carry = 0; - for (j = i - 32, k = i - 12; j < k; ++j) { - x[j] += carry - 16 * x[i] * L[j - (i - 32)]; - carry = (x[j] + 128) >> 8; - x[j] -= carry * 256; - } - x[j] += carry; - x[i] = 0; - } - carry = 0; - for (j = 0; j < 32; j++) { - x[j] += carry - (x[31] >> 4) * L[j]; - carry = x[j] >> 8; - x[j] &= 255; - } - for (j = 0; j < 32; j++) x[j] -= carry * L[j]; - for (i = 0; i < 32; i++) { - x[i+1] += x[i] >> 8; - r[i] = x[i] & 255; - } -} - -function reduce(r) { - var x = new Float64Array(64), i; - for (i = 0; i < 64; i++) x[i] = r[i]; - for (i = 0; i < 64; i++) r[i] = 0; - modL(r, x); -} - -// Note: difference from C - smlen returned, not passed as argument. -function crypto_sign(sm, m, n, sk) { - var d = new Uint8Array(64), h = new Uint8Array(64), r = new Uint8Array(64); - var i, j, x = new Float64Array(64); - var p = [gf(), gf(), gf(), gf()]; - - crypto_hash(d, sk, 32); - d[0] &= 248; - d[31] &= 127; - d[31] |= 64; - - var smlen = n + 64; - for (i = 0; i < n; i++) sm[64 + i] = m[i]; - for (i = 0; i < 32; i++) sm[32 + i] = d[32 + i]; - - crypto_hash(r, sm.subarray(32), n+32); - reduce(r); - scalarbase(p, r); - pack(sm, p); - - for (i = 32; i < 64; i++) sm[i] = sk[i]; - crypto_hash(h, sm, n + 64); - reduce(h); - - for (i = 0; i < 64; i++) x[i] = 0; - for (i = 0; i < 32; i++) x[i] = r[i]; - for (i = 0; i < 32; i++) { - for (j = 0; j < 32; j++) { - x[i+j] += h[i] * d[j]; - } - } - - modL(sm.subarray(32), x); - return smlen; -} - -function unpackneg(r, p) { - var t = gf(), chk = gf(), num = gf(), - den = gf(), den2 = gf(), den4 = gf(), - den6 = gf(); - - set25519(r[2], gf1); - unpack25519(r[1], p); - S(num, r[1]); - M(den, num, D); - Z(num, num, r[2]); - A(den, r[2], den); - - S(den2, den); - S(den4, den2); - M(den6, den4, den2); - M(t, den6, num); - M(t, t, den); - - pow2523(t, t); - M(t, t, num); - M(t, t, den); - M(t, t, den); - M(r[0], t, den); - - S(chk, r[0]); - M(chk, chk, den); - if (neq25519(chk, num)) M(r[0], r[0], I); - - S(chk, r[0]); - M(chk, chk, den); - if (neq25519(chk, num)) return -1; - - if (par25519(r[0]) === (p[31]>>7)) Z(r[0], gf0, r[0]); - - M(r[3], r[0], r[1]); - return 0; -} - -function crypto_sign_open(m, sm, n, pk) { - var i, mlen; - var t = new Uint8Array(32), h = new Uint8Array(64); - var p = [gf(), gf(), gf(), gf()], - q = [gf(), gf(), gf(), gf()]; - - mlen = -1; - if (n < 64) return -1; - - if (unpackneg(q, pk)) return -1; - - for (i = 0; i < n; i++) m[i] = sm[i]; - for (i = 0; i < 32; i++) m[i+32] = pk[i]; - crypto_hash(h, m, n); - reduce(h); - scalarmult(p, q, h); - - scalarbase(q, sm.subarray(32)); - add(p, q); - pack(t, p); - - n -= 64; - if (crypto_verify_32(sm, 0, t, 0)) { - for (i = 0; i < n; i++) m[i] = 0; - return -1; - } - - for (i = 0; i < n; i++) m[i] = sm[i + 64]; - mlen = n; - return mlen; -} - -var crypto_secretbox_KEYBYTES = 32, - crypto_secretbox_NONCEBYTES = 24, - crypto_secretbox_ZEROBYTES = 32, - crypto_secretbox_BOXZEROBYTES = 16, - crypto_scalarmult_BYTES = 32, - crypto_scalarmult_SCALARBYTES = 32, - crypto_box_PUBLICKEYBYTES = 32, - crypto_box_SECRETKEYBYTES = 32, - crypto_box_BEFORENMBYTES = 32, - crypto_box_NONCEBYTES = crypto_secretbox_NONCEBYTES, - crypto_box_ZEROBYTES = crypto_secretbox_ZEROBYTES, - crypto_box_BOXZEROBYTES = crypto_secretbox_BOXZEROBYTES, - crypto_sign_BYTES = 64, - crypto_sign_PUBLICKEYBYTES = 32, - crypto_sign_SECRETKEYBYTES = 64, - crypto_sign_SEEDBYTES = 32, - crypto_hash_BYTES = 64; - -nacl.lowlevel = { - crypto_core_hsalsa20: crypto_core_hsalsa20, - crypto_stream_xor: crypto_stream_xor, - crypto_stream: crypto_stream, - crypto_stream_salsa20_xor: crypto_stream_salsa20_xor, - crypto_stream_salsa20: crypto_stream_salsa20, - crypto_onetimeauth: crypto_onetimeauth, - crypto_onetimeauth_verify: crypto_onetimeauth_verify, - crypto_verify_16: crypto_verify_16, - crypto_verify_32: crypto_verify_32, - crypto_secretbox: crypto_secretbox, - crypto_secretbox_open: crypto_secretbox_open, - crypto_scalarmult: crypto_scalarmult, - crypto_scalarmult_base: crypto_scalarmult_base, - crypto_box_beforenm: crypto_box_beforenm, - crypto_box_afternm: crypto_box_afternm, - crypto_box: crypto_box, - crypto_box_open: crypto_box_open, - crypto_box_keypair: crypto_box_keypair, - crypto_hash: crypto_hash, - crypto_sign: crypto_sign, - crypto_sign_keypair: crypto_sign_keypair, - crypto_sign_open: crypto_sign_open, - - crypto_secretbox_KEYBYTES: crypto_secretbox_KEYBYTES, - crypto_secretbox_NONCEBYTES: crypto_secretbox_NONCEBYTES, - crypto_secretbox_ZEROBYTES: crypto_secretbox_ZEROBYTES, - crypto_secretbox_BOXZEROBYTES: crypto_secretbox_BOXZEROBYTES, - crypto_scalarmult_BYTES: crypto_scalarmult_BYTES, - crypto_scalarmult_SCALARBYTES: crypto_scalarmult_SCALARBYTES, - crypto_box_PUBLICKEYBYTES: crypto_box_PUBLICKEYBYTES, - crypto_box_SECRETKEYBYTES: crypto_box_SECRETKEYBYTES, - crypto_box_BEFORENMBYTES: crypto_box_BEFORENMBYTES, - crypto_box_NONCEBYTES: crypto_box_NONCEBYTES, - crypto_box_ZEROBYTES: crypto_box_ZEROBYTES, - crypto_box_BOXZEROBYTES: crypto_box_BOXZEROBYTES, - crypto_sign_BYTES: crypto_sign_BYTES, - crypto_sign_PUBLICKEYBYTES: crypto_sign_PUBLICKEYBYTES, - crypto_sign_SECRETKEYBYTES: crypto_sign_SECRETKEYBYTES, - crypto_sign_SEEDBYTES: crypto_sign_SEEDBYTES, - crypto_hash_BYTES: crypto_hash_BYTES -}; - -/* High-level API */ - -function checkLengths(k, n) { - if (k.length !== crypto_secretbox_KEYBYTES) throw new Error('bad key size'); - if (n.length !== crypto_secretbox_NONCEBYTES) throw new Error('bad nonce size'); -} - -function checkBoxLengths(pk, sk) { - if (pk.length !== crypto_box_PUBLICKEYBYTES) throw new Error('bad public key size'); - if (sk.length !== crypto_box_SECRETKEYBYTES) throw new Error('bad secret key size'); -} - -function checkArrayTypes() { - var t, i; - for (i = 0; i < arguments.length; i++) { - if ((t = Object.prototype.toString.call(arguments[i])) !== '[object Uint8Array]') - throw new TypeError('unexpected type ' + t + ', use Uint8Array'); - } -} - -function cleanup(arr) { - for (var i = 0; i < arr.length; i++) arr[i] = 0; -} - -nacl.util = {}; - -nacl.util.decodeUTF8 = function(s) { - var i, d = unescape(encodeURIComponent(s)), b = new Uint8Array(d.length); - for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); - return b; -}; - -nacl.util.encodeUTF8 = function(arr) { - var i, s = []; - for (i = 0; i < arr.length; i++) s.push(String.fromCharCode(arr[i])); - return decodeURIComponent(escape(s.join(''))); -}; - -nacl.util.encodeBase64 = function(arr) { - if (typeof btoa === 'undefined') { - return (new Buffer(arr)).toString('base64'); - } else { - var i, s = [], len = arr.length; - for (i = 0; i < len; i++) s.push(String.fromCharCode(arr[i])); - return btoa(s.join('')); - } -}; - -nacl.util.decodeBase64 = function(s) { - if (typeof atob === 'undefined') { - return new Uint8Array(Array.prototype.slice.call(new Buffer(s, 'base64'), 0)); - } else { - var i, d = atob(s), b = new Uint8Array(d.length); - for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); - return b; - } -}; - -nacl.randomBytes = function(n) { - var b = new Uint8Array(n); - randombytes(b, n); - return b; -}; - -nacl.secretbox = function(msg, nonce, key) { - checkArrayTypes(msg, nonce, key); - checkLengths(key, nonce); - var m = new Uint8Array(crypto_secretbox_ZEROBYTES + msg.length); - var c = new Uint8Array(m.length); - for (var i = 0; i < msg.length; i++) m[i+crypto_secretbox_ZEROBYTES] = msg[i]; - crypto_secretbox(c, m, m.length, nonce, key); - return c.subarray(crypto_secretbox_BOXZEROBYTES); -}; - -nacl.secretbox.open = function(box, nonce, key) { - checkArrayTypes(box, nonce, key); - checkLengths(key, nonce); - var c = new Uint8Array(crypto_secretbox_BOXZEROBYTES + box.length); - var m = new Uint8Array(c.length); - for (var i = 0; i < box.length; i++) c[i+crypto_secretbox_BOXZEROBYTES] = box[i]; - if (c.length < 32) return false; - if (crypto_secretbox_open(m, c, c.length, nonce, key) !== 0) return false; - return m.subarray(crypto_secretbox_ZEROBYTES); -}; - -nacl.secretbox.keyLength = crypto_secretbox_KEYBYTES; -nacl.secretbox.nonceLength = crypto_secretbox_NONCEBYTES; -nacl.secretbox.overheadLength = crypto_secretbox_BOXZEROBYTES; - -nacl.scalarMult = function(n, p) { - checkArrayTypes(n, p); - if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); - if (p.length !== crypto_scalarmult_BYTES) throw new Error('bad p size'); - var q = new Uint8Array(crypto_scalarmult_BYTES); - crypto_scalarmult(q, n, p); - return q; -}; - -nacl.scalarMult.base = function(n) { - checkArrayTypes(n); - if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); - var q = new Uint8Array(crypto_scalarmult_BYTES); - crypto_scalarmult_base(q, n); - return q; -}; - -nacl.scalarMult.scalarLength = crypto_scalarmult_SCALARBYTES; -nacl.scalarMult.groupElementLength = crypto_scalarmult_BYTES; - -nacl.box = function(msg, nonce, publicKey, secretKey) { - var k = nacl.box.before(publicKey, secretKey); - return nacl.secretbox(msg, nonce, k); -}; - -nacl.box.before = function(publicKey, secretKey) { - checkArrayTypes(publicKey, secretKey); - checkBoxLengths(publicKey, secretKey); - var k = new Uint8Array(crypto_box_BEFORENMBYTES); - crypto_box_beforenm(k, publicKey, secretKey); - return k; -}; - -nacl.box.after = nacl.secretbox; - -nacl.box.open = function(msg, nonce, publicKey, secretKey) { - var k = nacl.box.before(publicKey, secretKey); - return nacl.secretbox.open(msg, nonce, k); -}; - -nacl.box.open.after = nacl.secretbox.open; - -nacl.box.keyPair = function() { - var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); - var sk = new Uint8Array(crypto_box_SECRETKEYBYTES); - crypto_box_keypair(pk, sk); - return {publicKey: pk, secretKey: sk}; -}; - -nacl.box.keyPair.fromSecretKey = function(secretKey) { - checkArrayTypes(secretKey); - if (secretKey.length !== crypto_box_SECRETKEYBYTES) - throw new Error('bad secret key size'); - var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); - crypto_scalarmult_base(pk, secretKey); - return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; -}; - -nacl.box.publicKeyLength = crypto_box_PUBLICKEYBYTES; -nacl.box.secretKeyLength = crypto_box_SECRETKEYBYTES; -nacl.box.sharedKeyLength = crypto_box_BEFORENMBYTES; -nacl.box.nonceLength = crypto_box_NONCEBYTES; -nacl.box.overheadLength = nacl.secretbox.overheadLength; - -nacl.sign = function(msg, secretKey) { - checkArrayTypes(msg, secretKey); - if (secretKey.length !== crypto_sign_SECRETKEYBYTES) - throw new Error('bad secret key size'); - var signedMsg = new Uint8Array(crypto_sign_BYTES+msg.length); - crypto_sign(signedMsg, msg, msg.length, secretKey); - return signedMsg; -}; - -nacl.sign.open = function(signedMsg, publicKey) { - if (arguments.length !== 2) - throw new Error('nacl.sign.open accepts 2 arguments; did you mean to use nacl.sign.detached.verify?'); - checkArrayTypes(signedMsg, publicKey); - if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) - throw new Error('bad public key size'); - var tmp = new Uint8Array(signedMsg.length); - var mlen = crypto_sign_open(tmp, signedMsg, signedMsg.length, publicKey); - if (mlen < 0) return null; - var m = new Uint8Array(mlen); - for (var i = 0; i < m.length; i++) m[i] = tmp[i]; - return m; -}; - -nacl.sign.detached = function(msg, secretKey) { - var signedMsg = nacl.sign(msg, secretKey); - var sig = new Uint8Array(crypto_sign_BYTES); - for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; - return sig; -}; - -nacl.sign.detached.verify = function(msg, sig, publicKey) { - checkArrayTypes(msg, sig, publicKey); - if (sig.length !== crypto_sign_BYTES) - throw new Error('bad signature size'); - if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) - throw new Error('bad public key size'); - var sm = new Uint8Array(crypto_sign_BYTES + msg.length); - var m = new Uint8Array(crypto_sign_BYTES + msg.length); - var i; - for (i = 0; i < crypto_sign_BYTES; i++) sm[i] = sig[i]; - for (i = 0; i < msg.length; i++) sm[i+crypto_sign_BYTES] = msg[i]; - return (crypto_sign_open(m, sm, sm.length, publicKey) >= 0); -}; - -nacl.sign.keyPair = function() { - var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); - var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); - crypto_sign_keypair(pk, sk); - return {publicKey: pk, secretKey: sk}; -}; - -nacl.sign.keyPair.fromSecretKey = function(secretKey) { - checkArrayTypes(secretKey); - if (secretKey.length !== crypto_sign_SECRETKEYBYTES) - throw new Error('bad secret key size'); - var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); - for (var i = 0; i < pk.length; i++) pk[i] = secretKey[32+i]; - return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; -}; - -nacl.sign.keyPair.fromSeed = function(seed) { - checkArrayTypes(seed); - if (seed.length !== crypto_sign_SEEDBYTES) - throw new Error('bad seed size'); - var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); - var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); - for (var i = 0; i < 32; i++) sk[i] = seed[i]; - crypto_sign_keypair(pk, sk, true); - return {publicKey: pk, secretKey: sk}; -}; - -nacl.sign.publicKeyLength = crypto_sign_PUBLICKEYBYTES; -nacl.sign.secretKeyLength = crypto_sign_SECRETKEYBYTES; -nacl.sign.seedLength = crypto_sign_SEEDBYTES; -nacl.sign.signatureLength = crypto_sign_BYTES; - -nacl.hash = function(msg) { - checkArrayTypes(msg); - var h = new Uint8Array(crypto_hash_BYTES); - crypto_hash(h, msg, msg.length); - return h; -}; - -nacl.hash.hashLength = crypto_hash_BYTES; - -nacl.verify = function(x, y) { - checkArrayTypes(x, y); - // Zero length arguments are considered not equal. - if (x.length === 0 || y.length === 0) return false; - if (x.length !== y.length) return false; - return (vn(x, 0, y, 0, x.length) === 0) ? true : false; -}; - -nacl.setPRNG = function(fn) { - randombytes = fn; -}; - -(function() { - // Initialize PRNG if environment provides CSPRNG. - // If not, methods calling randombytes will throw. - var crypto; - if (typeof window !== 'undefined') { - // Browser. - if (window.crypto && window.crypto.getRandomValues) { - crypto = window.crypto; // Standard - } else if (window.msCrypto && window.msCrypto.getRandomValues) { - crypto = window.msCrypto; // Internet Explorer 11+ - } - if (crypto) { - nacl.setPRNG(function(x, n) { - var i, v = new Uint8Array(n); - crypto.getRandomValues(v); - for (i = 0; i < n; i++) x[i] = v[i]; - cleanup(v); - }); - } - } else if (typeof require !== 'undefined') { - // Node.js. - crypto = require('crypto'); - if (crypto) { - nacl.setPRNG(function(x, n) { - var i, v = crypto.randomBytes(n); - for (i = 0; i < n; i++) x[i] = v[i]; - cleanup(v); - }); - } - } -})(); - -})(typeof module !== 'undefined' && module.exports ? module.exports : (window.nacl = window.nacl || {})); diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl.min.js b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl.min.js deleted file mode 100644 index 95d86950..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/nacl.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(r){"use strict";function n(r,n){return r<>>32-n}function e(r,n){var e=255&r[n+3];return e=e<<8|255&r[n+2],e=e<<8|255&r[n+1],e<<8|255&r[n+0]}function t(r,n){var e=r[n]<<24|r[n+1]<<16|r[n+2]<<8|r[n+3],t=r[n+4]<<24|r[n+5]<<16|r[n+6]<<8|r[n+7];return new lr(e,t)}function o(r,n,e){var t;for(t=0;4>t;t++)r[n+t]=255&e,e>>>=8}function i(r,n,e){r[n]=e.hi>>24&255,r[n+1]=e.hi>>16&255,r[n+2]=e.hi>>8&255,r[n+3]=255&e.hi,r[n+4]=e.lo>>24&255,r[n+5]=e.lo>>16&255,r[n+6]=e.lo>>8&255,r[n+7]=255&e.lo}function a(r,n,e,t,o){var i,a=0;for(i=0;o>i;i++)a|=r[n+i]^e[t+i];return(1&a-1>>>8)-1}function f(r,n,e,t){return a(r,n,e,t,16)}function u(r,n,e,t){return a(r,n,e,t,32)}function c(r,t,i,a,f){var u,c,w,y=new Uint32Array(16),s=new Uint32Array(16),l=new Uint32Array(16),h=new Uint32Array(4);for(u=0;4>u;u++)s[5*u]=e(a,4*u),s[1+u]=e(i,4*u),s[6+u]=e(t,4*u),s[11+u]=e(i,16+4*u);for(u=0;16>u;u++)l[u]=s[u];for(u=0;20>u;u++){for(c=0;4>c;c++){for(w=0;4>w;w++)h[w]=s[(5*c+4*w)%16];for(h[1]^=n(h[0]+h[3]|0,7),h[2]^=n(h[1]+h[0]|0,9),h[3]^=n(h[2]+h[1]|0,13),h[0]^=n(h[3]+h[2]|0,18),w=0;4>w;w++)y[4*c+(c+w)%4]=h[w]}for(w=0;16>w;w++)s[w]=y[w]}if(f){for(u=0;16>u;u++)s[u]=s[u]+l[u]|0;for(u=0;4>u;u++)s[5*u]=s[5*u]-e(a,4*u)|0,s[6+u]=s[6+u]-e(t,4*u)|0;for(u=0;4>u;u++)o(r,4*u,s[5*u]),o(r,16+4*u,s[6+u])}else for(u=0;16>u;u++)o(r,4*u,s[u]+l[u]|0)}function w(r,n,e,t){return c(r,n,e,t,!1),0}function y(r,n,e,t){return c(r,n,e,t,!0),0}function s(r,n,e,t,o,i,a){var f,u,c=new Uint8Array(16),y=new Uint8Array(64);if(!o)return 0;for(u=0;16>u;u++)c[u]=0;for(u=0;8>u;u++)c[u]=i[u];for(;o>=64;){for(w(y,c,a,Br),u=0;64>u;u++)r[n+u]=(e?e[t+u]:0)^y[u];for(f=1,u=8;16>u;u++)f=f+(255&c[u])|0,c[u]=255&f,f>>>=8;o-=64,n+=64,e&&(t+=64)}if(o>0)for(w(y,c,a,Br),u=0;o>u;u++)r[n+u]=(e?e[t+u]:0)^y[u];return 0}function l(r,n,e,t,o){return s(r,n,null,0,e,t,o)}function h(r,n,e,t,o){var i=new Uint8Array(32);return y(i,t,o,Br),l(r,n,e,t.subarray(16),i)}function g(r,n,e,t,o,i,a){var f=new Uint8Array(32);return y(f,i,a,Br),s(r,n,e,t,o,i.subarray(16),f)}function p(r,n){var e,t=0;for(e=0;17>e;e++)t=t+(r[e]+n[e]|0)|0,r[e]=255&t,t>>>=8}function v(r,n,e,t,o,i){var a,f,u,c,w=new Uint32Array(17),y=new Uint32Array(17),s=new Uint32Array(17),l=new Uint32Array(17),h=new Uint32Array(17);for(u=0;17>u;u++)y[u]=s[u]=0;for(u=0;16>u;u++)y[u]=i[u];for(y[3]&=15,y[4]&=252,y[7]&=15,y[8]&=252,y[11]&=15,y[12]&=252,y[15]&=15;o>0;){for(u=0;17>u;u++)l[u]=0;for(u=0;16>u&&o>u;++u)l[u]=e[t+u];for(l[u]=1,t+=u,o-=u,p(s,l),f=0;17>f;f++)for(w[f]=0,u=0;17>u;u++)w[f]=w[f]+s[u]*(f>=u?y[f-u]:320*y[f+17-u]|0)|0|0;for(f=0;17>f;f++)s[f]=w[f];for(c=0,u=0;16>u;u++)c=c+s[u]|0,s[u]=255&c,c>>>=8;for(c=c+s[16]|0,s[16]=3&c,c=5*(c>>>2)|0,u=0;16>u;u++)c=c+s[u]|0,s[u]=255&c,c>>>=8;c=c+s[16]|0,s[16]=c}for(u=0;17>u;u++)h[u]=s[u];for(p(s,Sr),a=0|-(s[16]>>>7),u=0;17>u;u++)s[u]^=a&(h[u]^s[u]);for(u=0;16>u;u++)l[u]=i[u+16];for(l[16]=0,p(s,l),u=0;16>u;u++)r[n+u]=s[u];return 0}function b(r,n,e,t,o,i){var a=new Uint8Array(16);return v(a,0,e,t,o,i),f(r,n,a,0)}function A(r,n,e,t,o){var i;if(32>e)return-1;for(g(r,0,n,0,e,t,o),v(r,16,r,32,e-32,r),i=0;16>i;i++)r[i]=0;return 0}function U(r,n,e,t,o){var i,a=new Uint8Array(32);if(32>e)return-1;if(h(a,0,32,t,o),0!==b(n,16,n,32,e-32,a))return-1;for(g(r,0,n,0,e,t,o),i=0;32>i;i++)r[i]=0;return 0}function _(r,n){var e;for(e=0;16>e;e++)r[e]=0|n[e]}function d(r){var n,e;for(e=0;16>e;e++)r[e]+=65536,n=Math.floor(r[e]/65536),r[(e+1)*(15>e?1:0)]+=n-1+37*(n-1)*(15===e?1:0),r[e]-=65536*n}function E(r,n,e){for(var t,o=~(e-1),i=0;16>i;i++)t=o&(r[i]^n[i]),r[i]^=t,n[i]^=t}function x(r,n){var e,t,o,i=hr(),a=hr();for(e=0;16>e;e++)a[e]=n[e];for(d(a),d(a),d(a),t=0;2>t;t++){for(i[0]=a[0]-65517,e=1;15>e;e++)i[e]=a[e]-65535-(i[e-1]>>16&1),i[e-1]&=65535;i[15]=a[15]-32767-(i[14]>>16&1),o=i[15]>>16&1,i[14]&=65535,E(a,i,1-o)}for(e=0;16>e;e++)r[2*e]=255&a[e],r[2*e+1]=a[e]>>8}function m(r,n){var e=new Uint8Array(32),t=new Uint8Array(32);return x(e,r),x(t,n),u(e,0,t,0)}function B(r){var n=new Uint8Array(32);return x(n,r),1&n[0]}function S(r,n){var e;for(e=0;16>e;e++)r[e]=n[2*e]+(n[2*e+1]<<8);r[15]&=32767}function K(r,n,e){var t;for(t=0;16>t;t++)r[t]=n[t]+e[t]|0}function T(r,n,e){var t;for(t=0;16>t;t++)r[t]=n[t]-e[t]|0}function Y(r,n,e){var t,o,i=new Float64Array(31);for(t=0;31>t;t++)i[t]=0;for(t=0;16>t;t++)for(o=0;16>o;o++)i[t+o]+=n[t]*e[o];for(t=0;15>t;t++)i[t]+=38*i[t+16];for(t=0;16>t;t++)r[t]=i[t];d(r),d(r)}function L(r,n){Y(r,n,n)}function C(r,n){var e,t=hr();for(e=0;16>e;e++)t[e]=n[e];for(e=253;e>=0;e--)L(t,t),2!==e&&4!==e&&Y(t,t,n);for(e=0;16>e;e++)r[e]=t[e]}function R(r,n){var e,t=hr();for(e=0;16>e;e++)t[e]=n[e];for(e=250;e>=0;e--)L(t,t),1!==e&&Y(t,t,n);for(e=0;16>e;e++)r[e]=t[e]}function k(r,n,e){var t,o,i=new Uint8Array(32),a=new Float64Array(80),f=hr(),u=hr(),c=hr(),w=hr(),y=hr(),s=hr();for(o=0;31>o;o++)i[o]=n[o];for(i[31]=127&n[31]|64,i[0]&=248,S(a,e),o=0;16>o;o++)u[o]=a[o],w[o]=f[o]=c[o]=0;for(f[0]=w[0]=1,o=254;o>=0;--o)t=i[o>>>3]>>>(7&o)&1,E(f,u,t),E(c,w,t),K(y,f,c),T(f,f,c),K(c,u,w),T(u,u,w),L(w,y),L(s,f),Y(f,c,f),Y(c,u,y),K(y,f,c),T(f,f,c),L(u,f),T(c,w,s),Y(f,c,Ur),K(f,f,w),Y(c,c,f),Y(f,w,s),Y(w,u,a),L(u,y),E(f,u,t),E(c,w,t);for(o=0;16>o;o++)a[o+16]=f[o],a[o+32]=c[o],a[o+48]=u[o],a[o+64]=w[o];var l=a.subarray(32),h=a.subarray(16);return C(l,l),Y(h,h,l),x(r,h),0}function z(r,n){return k(r,n,vr)}function P(r,n){return gr(n,32),z(r,n)}function O(r,n,e){var t=new Uint8Array(32);return k(t,e,n),y(r,pr,t,Br)}function F(r,n,e,t,o,i){var a=new Uint8Array(32);return O(a,o,i),Kr(r,n,e,t,a)}function N(r,n,e,t,o,i){var a=new Uint8Array(32);return O(a,o,i),Tr(r,n,e,t,a)}function M(){var r,n,e,t=0,o=0,i=0,a=0,f=65535;for(e=0;e>>16,i+=n&f,a+=n>>>16;return o+=t>>>16,i+=o>>>16,a+=i>>>16,new lr(i&f|a<<16,t&f|o<<16)}function j(r,n){return new lr(r.hi>>>n,r.lo>>>n|r.hi<<32-n)}function G(){var r,n=0,e=0;for(r=0;rn?(e=r.hi>>>n|r.lo<>>n|r.hi<n&&(e=r.lo>>>n|r.hi<>>n|r.lo<a;a++)u[a]=w[a]=t(r,8*a);for(var s=0;e>=128;){for(a=0;16>a;a++)y[a]=t(n,8*a+s);for(a=0;80>a;a++){for(f=0;8>f;f++)c[f]=w[f];for(o=M(w[7],X(w[4]),Z(w[4],w[5],w[6]),Yr[a],y[a%16]),c[7]=M(o,q(w[0]),V(w[0],w[1],w[2])),c[3]=M(c[3],o),f=0;8>f;f++)w[(f+1)%8]=c[f];if(a%16===15)for(f=0;16>f;f++)y[f]=M(y[f],y[(f+9)%16],D(y[(f+1)%16]),H(y[(f+14)%16]))}for(a=0;8>a;a++)w[a]=M(w[a],u[a]),u[a]=w[a];s+=128,e-=128}for(a=0;8>a;a++)i(r,8*a,u[a]);return e}function Q(r,n,e){var t,o=new Uint8Array(64),a=new Uint8Array(256),f=e;for(t=0;64>t;t++)o[t]=Lr[t];for(J(o,n,e),e%=128,t=0;256>t;t++)a[t]=0;for(t=0;e>t;t++)a[t]=n[f-e+t];for(a[e]=128,e=256-128*(112>e?1:0),a[e-9]=0,i(a,e-8,new lr(f/536870912|0,f<<3)),J(o,a,e),t=0;64>t;t++)r[t]=o[t];return 0}function W(r,n){var e=hr(),t=hr(),o=hr(),i=hr(),a=hr(),f=hr(),u=hr(),c=hr(),w=hr();T(e,r[1],r[0]),T(w,n[1],n[0]),Y(e,e,w),K(t,r[0],r[1]),K(w,n[0],n[1]),Y(t,t,w),Y(o,r[3],n[3]),Y(o,o,dr),Y(i,r[2],n[2]),K(i,i,i),T(a,t,e),T(f,i,o),K(u,i,o),K(c,t,e),Y(r[0],a,f),Y(r[1],c,u),Y(r[2],u,f),Y(r[3],a,c)}function $(r,n,e){var t;for(t=0;4>t;t++)E(r[t],n[t],e)}function rr(r,n){var e=hr(),t=hr(),o=hr();C(o,n[2]),Y(e,n[0],o),Y(t,n[1],o),x(r,t),r[31]^=B(e)<<7}function nr(r,n,e){var t,o;for(_(r[0],br),_(r[1],Ar),_(r[2],Ar),_(r[3],br),o=255;o>=0;--o)t=e[o/8|0]>>(7&o)&1,$(r,n,t),W(n,r),W(r,r),$(r,n,t)}function er(r,n){var e=[hr(),hr(),hr(),hr()];_(e[0],Er),_(e[1],xr),_(e[2],Ar),Y(e[3],Er,xr),nr(r,e,n)}function tr(r,n,e){var t,o=new Uint8Array(64),i=[hr(),hr(),hr(),hr()];for(e||gr(n,32),Q(o,n,32),o[0]&=248,o[31]&=127,o[31]|=64,er(i,o),rr(r,i),t=0;32>t;t++)n[t+32]=r[t];return 0}function or(r,n){var e,t,o,i;for(t=63;t>=32;--t){for(e=0,o=t-32,i=t-12;i>o;++o)n[o]+=e-16*n[t]*Cr[o-(t-32)],e=n[o]+128>>8,n[o]-=256*e;n[o]+=e,n[t]=0}for(e=0,o=0;32>o;o++)n[o]+=e-(n[31]>>4)*Cr[o],e=n[o]>>8,n[o]&=255;for(o=0;32>o;o++)n[o]-=e*Cr[o];for(t=0;32>t;t++)n[t+1]+=n[t]>>8,r[t]=255&n[t]}function ir(r){var n,e=new Float64Array(64);for(n=0;64>n;n++)e[n]=r[n];for(n=0;64>n;n++)r[n]=0;or(r,e)}function ar(r,n,e,t){var o,i,a=new Uint8Array(64),f=new Uint8Array(64),u=new Uint8Array(64),c=new Float64Array(64),w=[hr(),hr(),hr(),hr()];Q(a,t,32),a[0]&=248,a[31]&=127,a[31]|=64;var y=e+64;for(o=0;e>o;o++)r[64+o]=n[o];for(o=0;32>o;o++)r[32+o]=a[32+o];for(Q(u,r.subarray(32),e+32),ir(u),er(w,u),rr(r,w),o=32;64>o;o++)r[o]=t[o];for(Q(f,r,e+64),ir(f),o=0;64>o;o++)c[o]=0;for(o=0;32>o;o++)c[o]=u[o];for(o=0;32>o;o++)for(i=0;32>i;i++)c[o+i]+=f[o]*a[i];return or(r.subarray(32),c),y}function fr(r,n){var e=hr(),t=hr(),o=hr(),i=hr(),a=hr(),f=hr(),u=hr();return _(r[2],Ar),S(r[1],n),L(o,r[1]),Y(i,o,_r),T(o,o,r[2]),K(i,r[2],i),L(a,i),L(f,a),Y(u,f,a),Y(e,u,o),Y(e,e,i),R(e,e),Y(e,e,o),Y(e,e,i),Y(e,e,i),Y(r[0],e,i),L(t,r[0]),Y(t,t,i),m(t,o)&&Y(r[0],r[0],mr),L(t,r[0]),Y(t,t,i),m(t,o)?-1:(B(r[0])===n[31]>>7&&T(r[0],br,r[0]),Y(r[3],r[0],r[1]),0)}function ur(r,n,e,t){var o,i,a=new Uint8Array(32),f=new Uint8Array(64),c=[hr(),hr(),hr(),hr()],w=[hr(),hr(),hr(),hr()];if(i=-1,64>e)return-1;if(fr(w,t))return-1;for(o=0;e>o;o++)r[o]=n[o];for(o=0;32>o;o++)r[o+32]=t[o];if(Q(f,r,e),ir(f),nr(c,w,f),er(w,n.subarray(32)),W(c,w),rr(a,c),e-=64,u(n,0,a,0)){for(o=0;e>o;o++)r[o]=0;return-1}for(o=0;e>o;o++)r[o]=n[o+64];return i=e}function cr(r,n){if(r.length!==Rr)throw new Error("bad key size");if(n.length!==kr)throw new Error("bad nonce size")}function wr(r,n){if(r.length!==Nr)throw new Error("bad public key size");if(n.length!==Mr)throw new Error("bad secret key size")}function yr(){var r,n;for(n=0;nn;n++)e.push(String.fromCharCode(r[n]));return btoa(e.join(""))},r.util.decodeBase64=function(r){if("undefined"==typeof atob)return new Uint8Array(Array.prototype.slice.call(new Buffer(r,"base64"),0));var n,e=atob(r),t=new Uint8Array(e.length);for(n=0;nt)return null;for(var o=new Uint8Array(t),i=0;it;t++)o[t]=n[t];for(t=0;t=0},r.sign.keyPair=function(){var r=new Uint8Array(qr),n=new Uint8Array(Xr);return tr(r,n),{publicKey:r,secretKey:n}},r.sign.keyPair.fromSecretKey=function(r){if(yr(r),r.length!==Xr)throw new Error("bad secret key size");for(var n=new Uint8Array(qr),e=0;et;t++)e[t]=r[t];return tr(n,e,!0),{publicKey:n,secretKey:e}},r.sign.publicKeyLength=qr,r.sign.secretKeyLength=Xr,r.sign.seedLength=Dr,r.sign.signatureLength=Vr,r.hash=function(r){yr(r);var n=new Uint8Array(Hr);return Q(n,r,r.length),n},r.hash.hashLength=Hr,r.verify=function(r,n){return yr(r,n),0===r.length||0===n.length?!1:r.length!==n.length?!1:0===a(r,0,n,0,r.length)?!0:!1},r.setPRNG=function(r){gr=r},function(){var n;"undefined"!=typeof window?(window.crypto&&window.crypto.getRandomValues?n=window.crypto:window.msCrypto&&window.msCrypto.getRandomValues&&(n=window.msCrypto),n&&r.setPRNG(function(r,e){var t,o=new Uint8Array(e);for(n.getRandomValues(o),t=0;e>t;t++)r[t]=o[t];sr(o)})):"undefined"!=typeof require&&(n=require("crypto"),n&&r.setPRNG(function(r,e){var t,o=n.randomBytes(e);for(t=0;e>t;t++)r[t]=o[t];sr(o)}))}()}("undefined"!=typeof module&&module.exports?module.exports:window.nacl=window.nacl||{}); \ No newline at end of file diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/package.json b/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/package.json deleted file mode 100644 index bc307dc7..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/tweetnacl/package.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "name": "tweetnacl", - "version": "0.13.3", - "description": "Port of TweetNaCl cryptographic library to JavaScript", - "main": "nacl-fast.js", - "directories": { - "test": "test" - }, - "scripts": { - "build": "uglifyjs nacl.js -c -m -o nacl.min.js && uglifyjs nacl-fast.js -c -m -o nacl-fast.min.js", - "test": "tape test/*.js | faucet", - "testall": "make -C test/c && tape test/*.js test/c/*.js | faucet", - "browser": "browserify test/browser/init.js test/*.js | uglifyjs -c -m -o test/browser/_bundle.js 2>/dev/null", - "browser-quick": "browserify test/browser/init.js test/*.quick.js | uglifyjs -c -m -o test/browser/_bundle-quick.js 2>/dev/null", - "testling": "browserify test/browser/testling_init.js test/*.js | testling | faucet", - "firefox": "browserify test/browser/testling_init.js test/*.js | testling -x firefox | faucet", - "chrome": "browserify test/browser/testling_init.js test/*.js | testling -x google-chrome | faucet", - "bench": "node test/benchmark/bench.js", - "lint": "eslint nacl.js nacl-fast.js test/*.js test/benchmark/*.js" - }, - "testling": { - "files": "test/*.js", - "browsers": [ - "chrome/22..latest", - "firefox/16..latest", - "safari/latest", - "opera/11.0..latest", - "iphone/6..latest", - "ipad/6..latest", - "android-browser/latest" - ] - }, - "repository": { - "type": "git", - "url": "git+https://github.com/dchest/tweetnacl-js.git" - }, - "keywords": [ - "crypto", - "cryptography", - "curve25519", - "ed25519", - "encrypt", - "hash", - "key", - "nacl", - "poly1305", - "public", - "salsa20", - "signatures" - ], - "author": { - "name": "TweetNaCl-js contributors" - }, - "license": "Public domain", - "bugs": { - "url": "https://github.com/dchest/tweetnacl-js/issues" - }, - "homepage": "https://dchest.github.io/tweetnacl-js", - "devDependencies": { - "browserify": "^10.1.3", - "eslint": "^1.4.3", - "faucet": "0.0.1", - "tap-browser-color": "^0.1.2", - "tape": "^4.0.0", - "testling": "^1.7.1", - "uglify-js": "^2.4.21" - }, - "browser": { - "buffer": false, - "crypto": false - }, - "gitHead": "2bb422cb707fba4a5ec9654688564a4fb861b068", - "_id": "tweetnacl@0.13.3", - "_shasum": "d628b56f3bcc3d5ae74ba9d4c1a704def5ab4b56", - "_from": "tweetnacl@>=0.13.0 <0.14.0", - "_npmVersion": "2.14.7", - "_nodeVersion": "4.2.3", - "_npmUser": { - "name": "dchest", - "email": "dmitry@codingrobots.com" - }, - "dist": { - "shasum": "d628b56f3bcc3d5ae74ba9d4c1a704def5ab4b56", - "tarball": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz" - }, - "maintainers": [ - { - "name": "dchest", - "email": "dmitry@codingrobots.com" - } - ], - "_resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/http-signature/node_modules/sshpk/package.json b/node_modules/request/node_modules/http-signature/node_modules/sshpk/package.json deleted file mode 100644 index f46a6bd9..00000000 --- a/node_modules/request/node_modules/http-signature/node_modules/sshpk/package.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "name": "sshpk", - "version": "1.8.3", - "description": "A library for finding and using SSH public keys", - "main": "lib/index.js", - "scripts": { - "test": "tape test/*.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/arekinath/node-sshpk.git" - }, - "author": { - "name": "Joyent, Inc" - }, - "contributors": [ - { - "name": "Dave Eddy", - "email": "dave@daveeddy.com" - }, - { - "name": "Mark Cavage", - "email": "mcavage@gmail.com" - }, - { - "name": "Alex Wilson", - "email": "alex@cooperi.net" - } - ], - "license": "MIT", - "bugs": { - "url": "https://github.com/arekinath/node-sshpk/issues" - }, - "engines": { - "node": ">=0.10.0" - }, - "directories": { - "bin": "./bin", - "lib": "./lib", - "man": "./man/man1" - }, - "homepage": "https://github.com/arekinath/node-sshpk#readme", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "dashdash": "^1.12.0", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "tweetnacl": "~0.13.0", - "jodid25519": "^1.0.0", - "ecc-jsbn": "~0.1.1" - }, - "optionalDependencies": { - "jsbn": "~0.1.0", - "tweetnacl": "~0.13.0", - "jodid25519": "^1.0.0", - "ecc-jsbn": "~0.1.1" - }, - "devDependencies": { - "tape": "^3.5.0", - "benchmark": "^1.0.0", - "sinon": "^1.17.2", - "temp": "^0.8.2" - }, - "man": [ - "/Users/alex.wilson/dev/sshpk/man/man1/sshpk-conv.1", - "/Users/alex.wilson/dev/sshpk/man/man1/sshpk-sign.1", - "/Users/alex.wilson/dev/sshpk/man/man1/sshpk-verify.1" - ], - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "gitHead": "82d39066b2df4e8284350ff5ebb08c5b95c74652", - "_id": "sshpk@1.8.3", - "_shasum": "890cc9d614dc5292e5cb1a543b03c9abaa5c374e", - "_from": "sshpk@>=1.7.0 <2.0.0", - "_npmVersion": "2.15.1", - "_nodeVersion": "0.12.13", - "_npmUser": { - "name": "arekinath", - "email": "alex@cooperi.net" - }, - "dist": { - "shasum": "890cc9d614dc5292e5cb1a543b03c9abaa5c374e", - "tarball": "https://registry.npmjs.org/sshpk/-/sshpk-1.8.3.tgz" - }, - "maintainers": [ - { - "name": "arekinath", - "email": "alex@cooperi.net" - } - ], - "_npmOperationalInternal": { - "host": "packages-16-east.internal.npmjs.com", - "tmp": "tmp/sshpk-1.8.3.tgz_1461968607532_0.32797130732797086" - }, - "_resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.8.3.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/http-signature/package.json b/node_modules/request/node_modules/http-signature/package.json deleted file mode 100644 index 12b32f8e..00000000 --- a/node_modules/request/node_modules/http-signature/package.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "name": "http-signature", - "description": "Reference implementation of Joyent's HTTP Signature scheme.", - "version": "1.1.1", - "license": "MIT", - "author": { - "name": "Joyent, Inc" - }, - "contributors": [ - { - "name": "Mark Cavage", - "email": "mcavage@gmail.com" - }, - { - "name": "David I. Lehn", - "email": "dil@lehn.org" - }, - { - "name": "Patrick Mooney", - "email": "patrick.f.mooney@gmail.com" - } - ], - "repository": { - "type": "git", - "url": "git://github.com/joyent/node-http-signature.git" - }, - "homepage": "https://github.com/joyent/node-http-signature/", - "bugs": { - "url": "https://github.com/joyent/node-http-signature/issues" - }, - "keywords": [ - "https", - "request" - ], - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - }, - "main": "lib/index.js", - "scripts": { - "test": "tap test/*.js" - }, - "dependencies": { - "assert-plus": "^0.2.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "devDependencies": { - "node-uuid": "^1.4.1", - "tap": "0.4.2" - }, - "gitHead": "74d3f35e3aa436d83723c53b01e266f448e8149a", - "_id": "http-signature@1.1.1", - "_shasum": "df72e267066cd0ac67fb76adf8e134a8fbcf91bf", - "_from": "http-signature@>=1.1.0 <1.2.0", - "_npmVersion": "2.14.9", - "_nodeVersion": "0.12.9", - "_npmUser": { - "name": "arekinath", - "email": "alex@cooperi.net" - }, - "dist": { - "shasum": "df72e267066cd0ac67fb76adf8e134a8fbcf91bf", - "tarball": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" - }, - "maintainers": [ - { - "name": "arekinath", - "email": "alex@cooperi.net" - }, - { - "name": "mcavage", - "email": "mcavage@gmail.com" - }, - { - "name": "pfmooney", - "email": "patrick.f.mooney@gmail.com" - } - ], - "directories": {}, - "_resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/is-typedarray/LICENSE.md b/node_modules/request/node_modules/is-typedarray/LICENSE.md deleted file mode 100644 index ee27ba4b..00000000 --- a/node_modules/request/node_modules/is-typedarray/LICENSE.md +++ /dev/null @@ -1,18 +0,0 @@ -This software is released under the MIT license: - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/is-typedarray/README.md b/node_modules/request/node_modules/is-typedarray/README.md deleted file mode 100644 index 27528639..00000000 --- a/node_modules/request/node_modules/is-typedarray/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# is-typedarray [![locked](http://badges.github.io/stability-badges/dist/locked.svg)](http://github.com/badges/stability-badges) - -Detect whether or not an object is a -[Typed Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays). - -## Usage - -[![NPM](https://nodei.co/npm/is-typedarray.png)](https://nodei.co/npm/is-typedarray/) - -### isTypedArray(array) - -Returns `true` when array is a Typed Array, and `false` when it is not. - -## License - -MIT. See [LICENSE.md](http://github.com/hughsk/is-typedarray/blob/master/LICENSE.md) for details. diff --git a/node_modules/request/node_modules/is-typedarray/index.js b/node_modules/request/node_modules/is-typedarray/index.js deleted file mode 100644 index 58596036..00000000 --- a/node_modules/request/node_modules/is-typedarray/index.js +++ /dev/null @@ -1,41 +0,0 @@ -module.exports = isTypedArray -isTypedArray.strict = isStrictTypedArray -isTypedArray.loose = isLooseTypedArray - -var toString = Object.prototype.toString -var names = { - '[object Int8Array]': true - , '[object Int16Array]': true - , '[object Int32Array]': true - , '[object Uint8Array]': true - , '[object Uint8ClampedArray]': true - , '[object Uint16Array]': true - , '[object Uint32Array]': true - , '[object Float32Array]': true - , '[object Float64Array]': true -} - -function isTypedArray(arr) { - return ( - isStrictTypedArray(arr) - || isLooseTypedArray(arr) - ) -} - -function isStrictTypedArray(arr) { - return ( - arr instanceof Int8Array - || arr instanceof Int16Array - || arr instanceof Int32Array - || arr instanceof Uint8Array - || arr instanceof Uint8ClampedArray - || arr instanceof Uint16Array - || arr instanceof Uint32Array - || arr instanceof Float32Array - || arr instanceof Float64Array - ) -} - -function isLooseTypedArray(arr) { - return names[toString.call(arr)] -} diff --git a/node_modules/request/node_modules/is-typedarray/package.json b/node_modules/request/node_modules/is-typedarray/package.json deleted file mode 100644 index 750da78e..00000000 --- a/node_modules/request/node_modules/is-typedarray/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "is-typedarray", - "version": "1.0.0", - "description": "Detect whether or not an object is a Typed Array", - "main": "index.js", - "scripts": { - "test": "node test" - }, - "author": { - "name": "Hugh Kennedy", - "email": "hughskennedy@gmail.com", - "url": "http://hughsk.io/" - }, - "license": "MIT", - "dependencies": {}, - "devDependencies": { - "tape": "^2.13.1" - }, - "repository": { - "type": "git", - "url": "git://github.com/hughsk/is-typedarray.git" - }, - "keywords": [ - "typed", - "array", - "detect", - "is", - "util" - ], - "bugs": { - "url": "https://github.com/hughsk/is-typedarray/issues" - }, - "homepage": "https://github.com/hughsk/is-typedarray", - "gitHead": "0617cfa871686cf541af62b144f130488f44f6fe", - "_id": "is-typedarray@1.0.0", - "_shasum": "e479c80858df0c1b11ddda6940f96011fcda4a9a", - "_from": "is-typedarray@>=1.0.0 <1.1.0", - "_npmVersion": "2.7.5", - "_nodeVersion": "0.10.36", - "_npmUser": { - "name": "hughsk", - "email": "hughskennedy@gmail.com" - }, - "dist": { - "shasum": "e479c80858df0c1b11ddda6940f96011fcda4a9a", - "tarball": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - }, - "maintainers": [ - { - "name": "hughsk", - "email": "hughskennedy@gmail.com" - } - ], - "directories": {}, - "_resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/is-typedarray/test.js b/node_modules/request/node_modules/is-typedarray/test.js deleted file mode 100644 index b0c176fa..00000000 --- a/node_modules/request/node_modules/is-typedarray/test.js +++ /dev/null @@ -1,34 +0,0 @@ -var test = require('tape') -var ista = require('./') - -test('strict', function(t) { - t.ok(ista.strict(new Int8Array), 'Int8Array') - t.ok(ista.strict(new Int16Array), 'Int16Array') - t.ok(ista.strict(new Int32Array), 'Int32Array') - t.ok(ista.strict(new Uint8Array), 'Uint8Array') - t.ok(ista.strict(new Uint16Array), 'Uint16Array') - t.ok(ista.strict(new Uint32Array), 'Uint32Array') - t.ok(ista.strict(new Float32Array), 'Float32Array') - t.ok(ista.strict(new Float64Array), 'Float64Array') - - t.ok(!ista.strict(new Array), 'Array') - t.ok(!ista.strict([]), '[]') - - t.end() -}) - -test('loose', function(t) { - t.ok(ista.loose(new Int8Array), 'Int8Array') - t.ok(ista.loose(new Int16Array), 'Int16Array') - t.ok(ista.loose(new Int32Array), 'Int32Array') - t.ok(ista.loose(new Uint8Array), 'Uint8Array') - t.ok(ista.loose(new Uint16Array), 'Uint16Array') - t.ok(ista.loose(new Uint32Array), 'Uint32Array') - t.ok(ista.loose(new Float32Array), 'Float32Array') - t.ok(ista.loose(new Float64Array), 'Float64Array') - - t.ok(!ista.loose(new Array), 'Array') - t.ok(!ista.loose([]), '[]') - - t.end() -}) diff --git a/node_modules/request/node_modules/isstream/.jshintrc b/node_modules/request/node_modules/isstream/.jshintrc deleted file mode 100644 index c8ef3ca4..00000000 --- a/node_modules/request/node_modules/isstream/.jshintrc +++ /dev/null @@ -1,59 +0,0 @@ -{ - "predef": [ ] - , "bitwise": false - , "camelcase": false - , "curly": false - , "eqeqeq": false - , "forin": false - , "immed": false - , "latedef": false - , "noarg": true - , "noempty": true - , "nonew": true - , "plusplus": false - , "quotmark": true - , "regexp": false - , "undef": true - , "unused": true - , "strict": false - , "trailing": true - , "maxlen": 120 - , "asi": true - , "boss": true - , "debug": true - , "eqnull": true - , "esnext": true - , "evil": true - , "expr": true - , "funcscope": false - , "globalstrict": false - , "iterator": false - , "lastsemic": true - , "laxbreak": true - , "laxcomma": true - , "loopfunc": true - , "multistr": false - , "onecase": false - , "proto": false - , "regexdash": false - , "scripturl": true - , "smarttabs": false - , "shadow": false - , "sub": true - , "supernew": false - , "validthis": true - , "browser": true - , "couch": false - , "devel": false - , "dojo": false - , "mootools": false - , "node": true - , "nonstandard": true - , "prototypejs": false - , "rhino": false - , "worker": true - , "wsh": false - , "nomen": false - , "onevar": false - , "passfail": false -} \ No newline at end of file diff --git a/node_modules/request/node_modules/isstream/.npmignore b/node_modules/request/node_modules/isstream/.npmignore deleted file mode 100644 index aa1ec1ea..00000000 --- a/node_modules/request/node_modules/isstream/.npmignore +++ /dev/null @@ -1 +0,0 @@ -*.tgz diff --git a/node_modules/request/node_modules/isstream/.travis.yml b/node_modules/request/node_modules/isstream/.travis.yml deleted file mode 100644 index 1fec2ab9..00000000 --- a/node_modules/request/node_modules/isstream/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: node_js -node_js: - - "0.8" - - "0.10" - - "0.11" -branches: - only: - - master -notifications: - email: - - rod@vagg.org -script: npm test diff --git a/node_modules/request/node_modules/isstream/LICENSE.md b/node_modules/request/node_modules/isstream/LICENSE.md deleted file mode 100644 index 43f7153f..00000000 --- a/node_modules/request/node_modules/isstream/LICENSE.md +++ /dev/null @@ -1,11 +0,0 @@ -The MIT License (MIT) -===================== - -Copyright (c) 2015 Rod Vagg ---------------------------- - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/isstream/README.md b/node_modules/request/node_modules/isstream/README.md deleted file mode 100644 index 06770e82..00000000 --- a/node_modules/request/node_modules/isstream/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# isStream - -[![Build Status](https://secure.travis-ci.org/rvagg/isstream.png)](http://travis-ci.org/rvagg/isstream) - -**Test if an object is a `Stream`** - -[![NPM](https://nodei.co/npm/isstream.svg)](https://nodei.co/npm/isstream/) - -The missing `Stream.isStream(obj)`: determine if an object is standard Node.js `Stream`. Works for Node-core `Stream` objects (for 0.8, 0.10, 0.11, and in theory, older and newer versions) and all versions of **[readable-stream](https://github.com/isaacs/readable-stream)**. - -## Usage: - -```js -var isStream = require('isstream') -var Stream = require('stream') - -isStream(new Stream()) // true - -isStream({}) // false - -isStream(new Stream.Readable()) // true -isStream(new Stream.Writable()) // true -isStream(new Stream.Duplex()) // true -isStream(new Stream.Transform()) // true -isStream(new Stream.PassThrough()) // true -``` - -## But wait! There's more! - -You can also test for `isReadable(obj)`, `isWritable(obj)` and `isDuplex(obj)` to test for implementations of Streams2 (and Streams3) base classes. - -```js -var isReadable = require('isstream').isReadable -var isWritable = require('isstream').isWritable -var isDuplex = require('isstream').isDuplex -var Stream = require('stream') - -isReadable(new Stream()) // false -isWritable(new Stream()) // false -isDuplex(new Stream()) // false - -isReadable(new Stream.Readable()) // true -isReadable(new Stream.Writable()) // false -isReadable(new Stream.Duplex()) // true -isReadable(new Stream.Transform()) // true -isReadable(new Stream.PassThrough()) // true - -isWritable(new Stream.Readable()) // false -isWritable(new Stream.Writable()) // true -isWritable(new Stream.Duplex()) // true -isWritable(new Stream.Transform()) // true -isWritable(new Stream.PassThrough()) // true - -isDuplex(new Stream.Readable()) // false -isDuplex(new Stream.Writable()) // false -isDuplex(new Stream.Duplex()) // true -isDuplex(new Stream.Transform()) // true -isDuplex(new Stream.PassThrough()) // true -``` - -*Reminder: when implementing your own streams, please [use **readable-stream** rather than core streams](http://r.va.gg/2014/06/why-i-dont-use-nodes-core-stream-module.html).* - - -## License - -**isStream** is Copyright (c) 2015 Rod Vagg [@rvagg](https://twitter.com/rvagg) and licenced under the MIT licence. All rights not explicitly granted in the MIT license are reserved. See the included LICENSE.md file for more details. diff --git a/node_modules/request/node_modules/isstream/isstream.js b/node_modules/request/node_modules/isstream/isstream.js deleted file mode 100644 index a1d104a7..00000000 --- a/node_modules/request/node_modules/isstream/isstream.js +++ /dev/null @@ -1,27 +0,0 @@ -var stream = require('stream') - - -function isStream (obj) { - return obj instanceof stream.Stream -} - - -function isReadable (obj) { - return isStream(obj) && typeof obj._read == 'function' && typeof obj._readableState == 'object' -} - - -function isWritable (obj) { - return isStream(obj) && typeof obj._write == 'function' && typeof obj._writableState == 'object' -} - - -function isDuplex (obj) { - return isReadable(obj) && isWritable(obj) -} - - -module.exports = isStream -module.exports.isReadable = isReadable -module.exports.isWritable = isWritable -module.exports.isDuplex = isDuplex diff --git a/node_modules/request/node_modules/isstream/package.json b/node_modules/request/node_modules/isstream/package.json deleted file mode 100644 index 266fd5d1..00000000 --- a/node_modules/request/node_modules/isstream/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "isstream", - "version": "0.1.2", - "description": "Determine if an object is a Stream", - "main": "isstream.js", - "scripts": { - "test": "tar --xform 's/^package/readable-stream-1.0/' -zxf readable-stream-1.0.*.tgz && tar --xform 's/^package/readable-stream-1.1/' -zxf readable-stream-1.1.*.tgz && node test.js; rm -rf readable-stream-1.?/" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rvagg/isstream.git" - }, - "keywords": [ - "stream", - "type", - "streams", - "readable-stream", - "hippo" - ], - "devDependencies": { - "tape": "~2.12.3", - "core-util-is": "~1.0.0", - "isarray": "0.0.1", - "string_decoder": "~0.10.x", - "inherits": "~2.0.1" - }, - "author": { - "name": "Rod Vagg", - "email": "rod@vagg.org" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/rvagg/isstream/issues" - }, - "homepage": "https://github.com/rvagg/isstream", - "gitHead": "cd39cba6da939b4fc9110825203adc506422c3dc", - "_id": "isstream@0.1.2", - "_shasum": "47e63f7af55afa6f92e1500e690eb8b8529c099a", - "_from": "isstream@>=0.1.2 <0.2.0", - "_npmVersion": "2.6.1", - "_nodeVersion": "1.4.3", - "_npmUser": { - "name": "rvagg", - "email": "rod@vagg.org" - }, - "maintainers": [ - { - "name": "rvagg", - "email": "rod@vagg.org" - } - ], - "dist": { - "shasum": "47e63f7af55afa6f92e1500e690eb8b8529c099a", - "tarball": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/isstream/test.js b/node_modules/request/node_modules/isstream/test.js deleted file mode 100644 index 8c950c55..00000000 --- a/node_modules/request/node_modules/isstream/test.js +++ /dev/null @@ -1,168 +0,0 @@ -var tape = require('tape') - , EE = require('events').EventEmitter - , util = require('util') - - - , isStream = require('./') - , isReadable = require('./').isReadable - , isWritable = require('./').isWritable - , isDuplex = require('./').isDuplex - - , CoreStreams = require('stream') - , ReadableStream10 = require('./readable-stream-1.0/') - , ReadableStream11 = require('./readable-stream-1.1/') - - -function test (pass, type, stream) { - tape('isStream(' + type + ')', function (t) { - t.plan(1) - t.ok(pass === isStream(stream), type) - }) -} - - -function testReadable (pass, type, stream) { - tape('isReadable(' + type + ')', function (t) { - t.plan(1) - t.ok(pass === isReadable(stream), type) - }) -} - - -function testWritable (pass, type, stream) { - tape('isWritable(' + type + ')', function (t) { - t.plan(1) - t.ok(pass === isWritable(stream), type) - }) -} - - -function testDuplex (pass, type, stream) { - tape('isDuplex(' + type + ')', function (t) { - t.plan(1) - t.ok(pass === isDuplex(stream), type) - }) -} - - -[ undefined, null, '', true, false, 0, 1, 1.0, 'string', {}, function foo () {} ].forEach(function (o) { - test(false, 'non-stream / primitive: ' + (JSON.stringify(o) || (o && o.toString()) || o), o) -}) - - -test(false, 'fake stream obj', { pipe: function () {} }) - - -;(function () { - - // looks like a stream! - - function Stream () { - EE.call(this) - } - util.inherits(Stream, EE) - Stream.prototype.pipe = function () {} - Stream.Stream = Stream - - test(false, 'fake stream "new Stream()"', new Stream()) - -}()) - - -test(true, 'CoreStreams.Stream', new (CoreStreams.Stream)()) -test(true, 'CoreStreams.Readable', new (CoreStreams.Readable)()) -test(true, 'CoreStreams.Writable', new (CoreStreams.Writable)()) -test(true, 'CoreStreams.Duplex', new (CoreStreams.Duplex)()) -test(true, 'CoreStreams.Transform', new (CoreStreams.Transform)()) -test(true, 'CoreStreams.PassThrough', new (CoreStreams.PassThrough)()) - -test(true, 'ReadableStream10.Readable', new (ReadableStream10.Readable)()) -test(true, 'ReadableStream10.Writable', new (ReadableStream10.Writable)()) -test(true, 'ReadableStream10.Duplex', new (ReadableStream10.Duplex)()) -test(true, 'ReadableStream10.Transform', new (ReadableStream10.Transform)()) -test(true, 'ReadableStream10.PassThrough', new (ReadableStream10.PassThrough)()) - -test(true, 'ReadableStream11.Readable', new (ReadableStream11.Readable)()) -test(true, 'ReadableStream11.Writable', new (ReadableStream11.Writable)()) -test(true, 'ReadableStream11.Duplex', new (ReadableStream11.Duplex)()) -test(true, 'ReadableStream11.Transform', new (ReadableStream11.Transform)()) -test(true, 'ReadableStream11.PassThrough', new (ReadableStream11.PassThrough)()) - - -testReadable(false, 'CoreStreams.Stream', new (CoreStreams.Stream)()) -testReadable(true, 'CoreStreams.Readable', new (CoreStreams.Readable)()) -testReadable(false, 'CoreStreams.Writable', new (CoreStreams.Writable)()) -testReadable(true, 'CoreStreams.Duplex', new (CoreStreams.Duplex)()) -testReadable(true, 'CoreStreams.Transform', new (CoreStreams.Transform)()) -testReadable(true, 'CoreStreams.PassThrough', new (CoreStreams.PassThrough)()) - -testReadable(true, 'ReadableStream10.Readable', new (ReadableStream10.Readable)()) -testReadable(false, 'ReadableStream10.Writable', new (ReadableStream10.Writable)()) -testReadable(true, 'ReadableStream10.Duplex', new (ReadableStream10.Duplex)()) -testReadable(true, 'ReadableStream10.Transform', new (ReadableStream10.Transform)()) -testReadable(true, 'ReadableStream10.PassThrough', new (ReadableStream10.PassThrough)()) - -testReadable(true, 'ReadableStream11.Readable', new (ReadableStream11.Readable)()) -testReadable(false, 'ReadableStream11.Writable', new (ReadableStream11.Writable)()) -testReadable(true, 'ReadableStream11.Duplex', new (ReadableStream11.Duplex)()) -testReadable(true, 'ReadableStream11.Transform', new (ReadableStream11.Transform)()) -testReadable(true, 'ReadableStream11.PassThrough', new (ReadableStream11.PassThrough)()) - - -testWritable(false, 'CoreStreams.Stream', new (CoreStreams.Stream)()) -testWritable(false, 'CoreStreams.Readable', new (CoreStreams.Readable)()) -testWritable(true, 'CoreStreams.Writable', new (CoreStreams.Writable)()) -testWritable(true, 'CoreStreams.Duplex', new (CoreStreams.Duplex)()) -testWritable(true, 'CoreStreams.Transform', new (CoreStreams.Transform)()) -testWritable(true, 'CoreStreams.PassThrough', new (CoreStreams.PassThrough)()) - -testWritable(false, 'ReadableStream10.Readable', new (ReadableStream10.Readable)()) -testWritable(true, 'ReadableStream10.Writable', new (ReadableStream10.Writable)()) -testWritable(true, 'ReadableStream10.Duplex', new (ReadableStream10.Duplex)()) -testWritable(true, 'ReadableStream10.Transform', new (ReadableStream10.Transform)()) -testWritable(true, 'ReadableStream10.PassThrough', new (ReadableStream10.PassThrough)()) - -testWritable(false, 'ReadableStream11.Readable', new (ReadableStream11.Readable)()) -testWritable(true, 'ReadableStream11.Writable', new (ReadableStream11.Writable)()) -testWritable(true, 'ReadableStream11.Duplex', new (ReadableStream11.Duplex)()) -testWritable(true, 'ReadableStream11.Transform', new (ReadableStream11.Transform)()) -testWritable(true, 'ReadableStream11.PassThrough', new (ReadableStream11.PassThrough)()) - - -testDuplex(false, 'CoreStreams.Stream', new (CoreStreams.Stream)()) -testDuplex(false, 'CoreStreams.Readable', new (CoreStreams.Readable)()) -testDuplex(false, 'CoreStreams.Writable', new (CoreStreams.Writable)()) -testDuplex(true, 'CoreStreams.Duplex', new (CoreStreams.Duplex)()) -testDuplex(true, 'CoreStreams.Transform', new (CoreStreams.Transform)()) -testDuplex(true, 'CoreStreams.PassThrough', new (CoreStreams.PassThrough)()) - -testDuplex(false, 'ReadableStream10.Readable', new (ReadableStream10.Readable)()) -testDuplex(false, 'ReadableStream10.Writable', new (ReadableStream10.Writable)()) -testDuplex(true, 'ReadableStream10.Duplex', new (ReadableStream10.Duplex)()) -testDuplex(true, 'ReadableStream10.Transform', new (ReadableStream10.Transform)()) -testDuplex(true, 'ReadableStream10.PassThrough', new (ReadableStream10.PassThrough)()) - -testDuplex(false, 'ReadableStream11.Readable', new (ReadableStream11.Readable)()) -testDuplex(false, 'ReadableStream11.Writable', new (ReadableStream11.Writable)()) -testDuplex(true, 'ReadableStream11.Duplex', new (ReadableStream11.Duplex)()) -testDuplex(true, 'ReadableStream11.Transform', new (ReadableStream11.Transform)()) -testDuplex(true, 'ReadableStream11.PassThrough', new (ReadableStream11.PassThrough)()) - - -;[ CoreStreams, ReadableStream10, ReadableStream11 ].forEach(function (p) { - [ 'Stream', 'Readable', 'Writable', 'Duplex', 'Transform', 'PassThrough' ].forEach(function (k) { - if (!p[k]) - return - - function SubStream () { - p[k].call(this) - } - util.inherits(SubStream, p[k]) - - test(true, 'Stream subclass: ' + p.name + '.' + k, new SubStream()) - - }) -}) - - - diff --git a/node_modules/request/node_modules/json-stringify-safe/.npmignore b/node_modules/request/node_modules/json-stringify-safe/.npmignore deleted file mode 100644 index 17d6b367..00000000 --- a/node_modules/request/node_modules/json-stringify-safe/.npmignore +++ /dev/null @@ -1 +0,0 @@ -/*.tgz diff --git a/node_modules/request/node_modules/json-stringify-safe/CHANGELOG.md b/node_modules/request/node_modules/json-stringify-safe/CHANGELOG.md deleted file mode 100644 index 42bcb60a..00000000 --- a/node_modules/request/node_modules/json-stringify-safe/CHANGELOG.md +++ /dev/null @@ -1,14 +0,0 @@ -## Unreleased -- Fixes stringify to only take ancestors into account when checking - circularity. - It previously assumed every visited object was circular which led to [false - positives][issue9]. - Uses the tiny serializer I wrote for [Must.js][must] a year and a half ago. -- Fixes calling the `replacer` function in the proper context (`thisArg`). -- Fixes calling the `cycleReplacer` function in the proper context (`thisArg`). -- Speeds serializing by a factor of - Big-O(h-my-god-it-linearly-searched-every-object) it had ever seen. Searching - only the ancestors for a circular references speeds up things considerably. - -[must]: https://github.com/moll/js-must -[issue9]: https://github.com/isaacs/json-stringify-safe/issues/9 diff --git a/node_modules/request/node_modules/json-stringify-safe/LICENSE b/node_modules/request/node_modules/json-stringify-safe/LICENSE deleted file mode 100644 index 19129e31..00000000 --- a/node_modules/request/node_modules/json-stringify-safe/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/node_modules/request/node_modules/json-stringify-safe/Makefile b/node_modules/request/node_modules/json-stringify-safe/Makefile deleted file mode 100644 index 36088c72..00000000 --- a/node_modules/request/node_modules/json-stringify-safe/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -NODE_OPTS = -TEST_OPTS = - -love: - @echo "Feel like makin' love." - -test: - @node $(NODE_OPTS) ./node_modules/.bin/_mocha -R dot $(TEST_OPTS) - -spec: - @node $(NODE_OPTS) ./node_modules/.bin/_mocha -R spec $(TEST_OPTS) - -autotest: - @node $(NODE_OPTS) ./node_modules/.bin/_mocha -R dot --watch $(TEST_OPTS) - -autospec: - @node $(NODE_OPTS) ./node_modules/.bin/_mocha -R spec --watch $(TEST_OPTS) - -pack: - @file=$$(npm pack); echo "$$file"; tar tf "$$file" - -publish: - npm publish - -tag: - git tag "v$$(node -e 'console.log(require("./package").version)')" - -clean: - rm -f *.tgz - npm prune --production - -.PHONY: love -.PHONY: test spec autotest autospec -.PHONY: pack publish tag -.PHONY: clean diff --git a/node_modules/request/node_modules/json-stringify-safe/README.md b/node_modules/request/node_modules/json-stringify-safe/README.md deleted file mode 100644 index a11f302a..00000000 --- a/node_modules/request/node_modules/json-stringify-safe/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# json-stringify-safe - -Like JSON.stringify, but doesn't throw on circular references. - -## Usage - -Takes the same arguments as `JSON.stringify`. - -```javascript -var stringify = require('json-stringify-safe'); -var circularObj = {}; -circularObj.circularRef = circularObj; -circularObj.list = [ circularObj, circularObj ]; -console.log(stringify(circularObj, null, 2)); -``` - -Output: - -```json -{ - "circularRef": "[Circular]", - "list": [ - "[Circular]", - "[Circular]" - ] -} -``` - -## Details - -``` -stringify(obj, serializer, indent, decycler) -``` - -The first three arguments are the same as to JSON.stringify. The last -is an argument that's only used when the object has been seen already. - -The default `decycler` function returns the string `'[Circular]'`. -If, for example, you pass in `function(k,v){}` (return nothing) then it -will prune cycles. If you pass in `function(k,v){ return {foo: 'bar'}}`, -then cyclical objects will always be represented as `{"foo":"bar"}` in -the result. - -``` -stringify.getSerialize(serializer, decycler) -``` - -Returns a serializer that can be used elsewhere. This is the actual -function that's passed to JSON.stringify. - -**Note** that the function returned from `getSerialize` is stateful for now, so -do **not** use it more than once. diff --git a/node_modules/request/node_modules/json-stringify-safe/package.json b/node_modules/request/node_modules/json-stringify-safe/package.json deleted file mode 100644 index 1ba97c94..00000000 --- a/node_modules/request/node_modules/json-stringify-safe/package.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "json-stringify-safe", - "version": "5.0.1", - "description": "Like JSON.stringify, but doesn't blow up on circular refs.", - "keywords": [ - "json", - "stringify", - "circular", - "safe" - ], - "homepage": "https://github.com/isaacs/json-stringify-safe", - "bugs": { - "url": "https://github.com/isaacs/json-stringify-safe/issues" - }, - "author": { - "name": "Isaac Z. Schlueter", - "email": "i@izs.me", - "url": "http://blog.izs.me" - }, - "contributors": [ - { - "name": "Andri Möll", - "email": "andri@dot.ee", - "url": "http://themoll.com" - } - ], - "license": "ISC", - "repository": { - "type": "git", - "url": "git://github.com/isaacs/json-stringify-safe.git" - }, - "main": "stringify.js", - "scripts": { - "test": "node test.js" - }, - "devDependencies": { - "mocha": ">= 2.1.0 < 3", - "must": ">= 0.12 < 0.13", - "sinon": ">= 1.12.2 < 2" - }, - "gitHead": "3890dceab3ad14f8701e38ca74f38276abc76de5", - "_id": "json-stringify-safe@5.0.1", - "_shasum": "1296a2d58fd45f19a0f6ce01d65701e2c735b6eb", - "_from": "json-stringify-safe@>=5.0.1 <5.1.0", - "_npmVersion": "2.10.0", - "_nodeVersion": "2.0.1", - "_npmUser": { - "name": "isaacs", - "email": "isaacs@npmjs.com" - }, - "dist": { - "shasum": "1296a2d58fd45f19a0f6ce01d65701e2c735b6eb", - "tarball": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - }, - "maintainers": [ - { - "name": "isaacs", - "email": "i@izs.me" - }, - { - "name": "moll", - "email": "andri@dot.ee" - } - ], - "directories": {}, - "_resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/json-stringify-safe/stringify.js b/node_modules/request/node_modules/json-stringify-safe/stringify.js deleted file mode 100644 index 124a4521..00000000 --- a/node_modules/request/node_modules/json-stringify-safe/stringify.js +++ /dev/null @@ -1,27 +0,0 @@ -exports = module.exports = stringify -exports.getSerialize = serializer - -function stringify(obj, replacer, spaces, cycleReplacer) { - return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces) -} - -function serializer(replacer, cycleReplacer) { - var stack = [], keys = [] - - if (cycleReplacer == null) cycleReplacer = function(key, value) { - if (stack[0] === value) return "[Circular ~]" - return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]" - } - - return function(key, value) { - if (stack.length > 0) { - var thisPos = stack.indexOf(this) - ~thisPos ? stack.splice(thisPos + 1) : stack.push(this) - ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key) - if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value) - } - else stack.push(value) - - return replacer == null ? value : replacer.call(this, key, value) - } -} diff --git a/node_modules/request/node_modules/json-stringify-safe/test/mocha.opts b/node_modules/request/node_modules/json-stringify-safe/test/mocha.opts deleted file mode 100644 index 2544e586..00000000 --- a/node_modules/request/node_modules/json-stringify-safe/test/mocha.opts +++ /dev/null @@ -1,2 +0,0 @@ ---recursive ---require must diff --git a/node_modules/request/node_modules/json-stringify-safe/test/stringify_test.js b/node_modules/request/node_modules/json-stringify-safe/test/stringify_test.js deleted file mode 100644 index 5b325831..00000000 --- a/node_modules/request/node_modules/json-stringify-safe/test/stringify_test.js +++ /dev/null @@ -1,246 +0,0 @@ -var Sinon = require("sinon") -var stringify = require("..") -function jsonify(obj) { return JSON.stringify(obj, null, 2) } - -describe("Stringify", function() { - it("must stringify circular objects", function() { - var obj = {name: "Alice"} - obj.self = obj - var json = stringify(obj, null, 2) - json.must.eql(jsonify({name: "Alice", self: "[Circular ~]"})) - }) - - it("must stringify circular objects with intermediaries", function() { - var obj = {name: "Alice"} - obj.identity = {self: obj} - var json = stringify(obj, null, 2) - json.must.eql(jsonify({name: "Alice", identity: {self: "[Circular ~]"}})) - }) - - it("must stringify circular objects deeper", function() { - var obj = {name: "Alice", child: {name: "Bob"}} - obj.child.self = obj.child - - stringify(obj, null, 2).must.eql(jsonify({ - name: "Alice", - child: {name: "Bob", self: "[Circular ~.child]"} - })) - }) - - it("must stringify circular objects deeper with intermediaries", function() { - var obj = {name: "Alice", child: {name: "Bob"}} - obj.child.identity = {self: obj.child} - - stringify(obj, null, 2).must.eql(jsonify({ - name: "Alice", - child: {name: "Bob", identity: {self: "[Circular ~.child]"}} - })) - }) - - it("must stringify circular objects in an array", function() { - var obj = {name: "Alice"} - obj.self = [obj, obj] - - stringify(obj, null, 2).must.eql(jsonify({ - name: "Alice", self: ["[Circular ~]", "[Circular ~]"] - })) - }) - - it("must stringify circular objects deeper in an array", function() { - var obj = {name: "Alice", children: [{name: "Bob"}, {name: "Eve"}]} - obj.children[0].self = obj.children[0] - obj.children[1].self = obj.children[1] - - stringify(obj, null, 2).must.eql(jsonify({ - name: "Alice", - children: [ - {name: "Bob", self: "[Circular ~.children.0]"}, - {name: "Eve", self: "[Circular ~.children.1]"} - ] - })) - }) - - it("must stringify circular arrays", function() { - var obj = [] - obj.push(obj) - obj.push(obj) - var json = stringify(obj, null, 2) - json.must.eql(jsonify(["[Circular ~]", "[Circular ~]"])) - }) - - it("must stringify circular arrays with intermediaries", function() { - var obj = [] - obj.push({name: "Alice", self: obj}) - obj.push({name: "Bob", self: obj}) - - stringify(obj, null, 2).must.eql(jsonify([ - {name: "Alice", self: "[Circular ~]"}, - {name: "Bob", self: "[Circular ~]"} - ])) - }) - - it("must stringify repeated objects in objects", function() { - var obj = {} - var alice = {name: "Alice"} - obj.alice1 = alice - obj.alice2 = alice - - stringify(obj, null, 2).must.eql(jsonify({ - alice1: {name: "Alice"}, - alice2: {name: "Alice"} - })) - }) - - it("must stringify repeated objects in arrays", function() { - var alice = {name: "Alice"} - var obj = [alice, alice] - var json = stringify(obj, null, 2) - json.must.eql(jsonify([{name: "Alice"}, {name: "Alice"}])) - }) - - it("must call given decycler and use its output", function() { - var obj = {} - obj.a = obj - obj.b = obj - - var decycle = Sinon.spy(function() { return decycle.callCount }) - var json = stringify(obj, null, 2, decycle) - json.must.eql(jsonify({a: 1, b: 2}, null, 2)) - - decycle.callCount.must.equal(2) - decycle.thisValues[0].must.equal(obj) - decycle.args[0][0].must.equal("a") - decycle.args[0][1].must.equal(obj) - decycle.thisValues[1].must.equal(obj) - decycle.args[1][0].must.equal("b") - decycle.args[1][1].must.equal(obj) - }) - - it("must call replacer and use its output", function() { - var obj = {name: "Alice", child: {name: "Bob"}} - - var replacer = Sinon.spy(bangString) - var json = stringify(obj, replacer, 2) - json.must.eql(jsonify({name: "Alice!", child: {name: "Bob!"}})) - - replacer.callCount.must.equal(4) - replacer.args[0][0].must.equal("") - replacer.args[0][1].must.equal(obj) - replacer.thisValues[1].must.equal(obj) - replacer.args[1][0].must.equal("name") - replacer.args[1][1].must.equal("Alice") - replacer.thisValues[2].must.equal(obj) - replacer.args[2][0].must.equal("child") - replacer.args[2][1].must.equal(obj.child) - replacer.thisValues[3].must.equal(obj.child) - replacer.args[3][0].must.equal("name") - replacer.args[3][1].must.equal("Bob") - }) - - it("must call replacer after describing circular references", function() { - var obj = {name: "Alice"} - obj.self = obj - - var replacer = Sinon.spy(bangString) - var json = stringify(obj, replacer, 2) - json.must.eql(jsonify({name: "Alice!", self: "[Circular ~]!"})) - - replacer.callCount.must.equal(3) - replacer.args[0][0].must.equal("") - replacer.args[0][1].must.equal(obj) - replacer.thisValues[1].must.equal(obj) - replacer.args[1][0].must.equal("name") - replacer.args[1][1].must.equal("Alice") - replacer.thisValues[2].must.equal(obj) - replacer.args[2][0].must.equal("self") - replacer.args[2][1].must.equal("[Circular ~]") - }) - - it("must call given decycler and use its output for nested objects", - function() { - var obj = {} - obj.a = obj - obj.b = {self: obj} - - var decycle = Sinon.spy(function() { return decycle.callCount }) - var json = stringify(obj, null, 2, decycle) - json.must.eql(jsonify({a: 1, b: {self: 2}})) - - decycle.callCount.must.equal(2) - decycle.args[0][0].must.equal("a") - decycle.args[0][1].must.equal(obj) - decycle.args[1][0].must.equal("self") - decycle.args[1][1].must.equal(obj) - }) - - it("must use decycler's output when it returned null", function() { - var obj = {a: "b"} - obj.self = obj - obj.selves = [obj, obj] - - function decycle() { return null } - stringify(obj, null, 2, decycle).must.eql(jsonify({ - a: "b", - self: null, - selves: [null, null] - })) - }) - - it("must use decycler's output when it returned undefined", function() { - var obj = {a: "b"} - obj.self = obj - obj.selves = [obj, obj] - - function decycle() {} - stringify(obj, null, 2, decycle).must.eql(jsonify({ - a: "b", - selves: [null, null] - })) - }) - - it("must throw given a decycler that returns a cycle", function() { - var obj = {} - obj.self = obj - var err - function identity(key, value) { return value } - try { stringify(obj, null, 2, identity) } catch (ex) { err = ex } - err.must.be.an.instanceof(TypeError) - }) - - describe(".getSerialize", function() { - it("must stringify circular objects", function() { - var obj = {a: "b"} - obj.circularRef = obj - obj.list = [obj, obj] - - var json = JSON.stringify(obj, stringify.getSerialize(), 2) - json.must.eql(jsonify({ - "a": "b", - "circularRef": "[Circular ~]", - "list": ["[Circular ~]", "[Circular ~]"] - })) - }) - - // This is the behavior as of Mar 3, 2015. - // The serializer function keeps state inside the returned function and - // so far I'm not sure how to not do that. JSON.stringify's replacer is not - // called _after_ serialization. - xit("must return a function that could be called twice", function() { - var obj = {name: "Alice"} - obj.self = obj - - var json - var serializer = stringify.getSerialize() - - json = JSON.stringify(obj, serializer, 2) - json.must.eql(jsonify({name: "Alice", self: "[Circular ~]"})) - - json = JSON.stringify(obj, serializer, 2) - json.must.eql(jsonify({name: "Alice", self: "[Circular ~]"})) - }) - }) -}) - -function bangString(key, value) { - return typeof value == "string" ? value + "!" : value -} diff --git a/node_modules/request/node_modules/mime-types/HISTORY.md b/node_modules/request/node_modules/mime-types/HISTORY.md deleted file mode 100644 index 63bd4ea0..00000000 --- a/node_modules/request/node_modules/mime-types/HISTORY.md +++ /dev/null @@ -1,197 +0,0 @@ -2.1.11 / 2016-05-01 -=================== - - * deps: mime-db@~1.23.0 - - Add new mime types - -2.1.10 / 2016-02-15 -=================== - - * deps: mime-db@~1.22.0 - - Add new mime types - - Fix extension of `application/dash+xml` - - Update primary extension for `audio/mp4` - -2.1.9 / 2016-01-06 -================== - - * deps: mime-db@~1.21.0 - - Add new mime types - -2.1.8 / 2015-11-30 -================== - - * deps: mime-db@~1.20.0 - - Add new mime types - -2.1.7 / 2015-09-20 -================== - - * deps: mime-db@~1.19.0 - - Add new mime types - -2.1.6 / 2015-09-03 -================== - - * deps: mime-db@~1.18.0 - - Add new mime types - -2.1.5 / 2015-08-20 -================== - - * deps: mime-db@~1.17.0 - - Add new mime types - -2.1.4 / 2015-07-30 -================== - - * deps: mime-db@~1.16.0 - - Add new mime types - -2.1.3 / 2015-07-13 -================== - - * deps: mime-db@~1.15.0 - - Add new mime types - -2.1.2 / 2015-06-25 -================== - - * deps: mime-db@~1.14.0 - - Add new mime types - -2.1.1 / 2015-06-08 -================== - - * perf: fix deopt during mapping - -2.1.0 / 2015-06-07 -================== - - * Fix incorrectly treating extension-less file name as extension - - i.e. `'path/to/json'` will no longer return `application/json` - * Fix `.charset(type)` to accept parameters - * Fix `.charset(type)` to match case-insensitive - * Improve generation of extension to MIME mapping - * Refactor internals for readability and no argument reassignment - * Prefer `application/*` MIME types from the same source - * Prefer any type over `application/octet-stream` - * deps: mime-db@~1.13.0 - - Add nginx as a source - - Add new mime types - -2.0.14 / 2015-06-06 -=================== - - * deps: mime-db@~1.12.0 - - Add new mime types - -2.0.13 / 2015-05-31 -=================== - - * deps: mime-db@~1.11.0 - - Add new mime types - -2.0.12 / 2015-05-19 -=================== - - * deps: mime-db@~1.10.0 - - Add new mime types - -2.0.11 / 2015-05-05 -=================== - - * deps: mime-db@~1.9.1 - - Add new mime types - -2.0.10 / 2015-03-13 -=================== - - * deps: mime-db@~1.8.0 - - Add new mime types - -2.0.9 / 2015-02-09 -================== - - * deps: mime-db@~1.7.0 - - Add new mime types - - Community extensions ownership transferred from `node-mime` - -2.0.8 / 2015-01-29 -================== - - * deps: mime-db@~1.6.0 - - Add new mime types - -2.0.7 / 2014-12-30 -================== - - * deps: mime-db@~1.5.0 - - Add new mime types - - Fix various invalid MIME type entries - -2.0.6 / 2014-12-30 -================== - - * deps: mime-db@~1.4.0 - - Add new mime types - - Fix various invalid MIME type entries - - Remove example template MIME types - -2.0.5 / 2014-12-29 -================== - - * deps: mime-db@~1.3.1 - - Fix missing extensions - -2.0.4 / 2014-12-10 -================== - - * deps: mime-db@~1.3.0 - - Add new mime types - -2.0.3 / 2014-11-09 -================== - - * deps: mime-db@~1.2.0 - - Add new mime types - -2.0.2 / 2014-09-28 -================== - - * deps: mime-db@~1.1.0 - - Add new mime types - - Add additional compressible - - Update charsets - -2.0.1 / 2014-09-07 -================== - - * Support Node.js 0.6 - -2.0.0 / 2014-09-02 -================== - - * Use `mime-db` - * Remove `.define()` - -1.0.2 / 2014-08-04 -================== - - * Set charset=utf-8 for `text/javascript` - -1.0.1 / 2014-06-24 -================== - - * Add `text/jsx` type - -1.0.0 / 2014-05-12 -================== - - * Return `false` for unknown types - * Set charset=utf-8 for `application/json` - -0.1.0 / 2014-05-02 -================== - - * Initial release diff --git a/node_modules/request/node_modules/mime-types/LICENSE b/node_modules/request/node_modules/mime-types/LICENSE deleted file mode 100644 index 06166077..00000000 --- a/node_modules/request/node_modules/mime-types/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -(The MIT License) - -Copyright (c) 2014 Jonathan Ong -Copyright (c) 2015 Douglas Christopher Wilson - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/mime-types/README.md b/node_modules/request/node_modules/mime-types/README.md deleted file mode 100644 index e77d615d..00000000 --- a/node_modules/request/node_modules/mime-types/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# mime-types - -[![NPM Version][npm-image]][npm-url] -[![NPM Downloads][downloads-image]][downloads-url] -[![Node.js Version][node-version-image]][node-version-url] -[![Build Status][travis-image]][travis-url] -[![Test Coverage][coveralls-image]][coveralls-url] - -The ultimate javascript content-type utility. - -Similar to [node-mime](https://github.com/broofa/node-mime), except: - -- __No fallbacks.__ Instead of naively returning the first available type, `mime-types` simply returns `false`, - so do `var type = mime.lookup('unrecognized') || 'application/octet-stream'`. -- No `new Mime()` business, so you could do `var lookup = require('mime-types').lookup`. -- Additional mime types are added such as jade and stylus via [mime-db](https://github.com/jshttp/mime-db) -- No `.define()` functionality - -Otherwise, the API is compatible. - -## Install - -```sh -$ npm install mime-types -``` - -## Adding Types - -All mime types are based on [mime-db](https://github.com/jshttp/mime-db), -so open a PR there if you'd like to add mime types. - -## API - -```js -var mime = require('mime-types') -``` - -All functions return `false` if input is invalid or not found. - -### mime.lookup(path) - -Lookup the content-type associated with a file. - -```js -mime.lookup('json') // 'application/json' -mime.lookup('.md') // 'text/x-markdown' -mime.lookup('file.html') // 'text/html' -mime.lookup('folder/file.js') // 'application/javascript' -mime.lookup('folder/.htaccess') // false - -mime.lookup('cats') // false -``` - -### mime.contentType(type) - -Create a full content-type header given a content-type or extension. - -```js -mime.contentType('markdown') // 'text/x-markdown; charset=utf-8' -mime.contentType('file.json') // 'application/json; charset=utf-8' - -// from a full path -mime.contentType(path.extname('/path/to/file.json')) // 'application/json; charset=utf-8' -``` - -### mime.extension(type) - -Get the default extension for a content-type. - -```js -mime.extension('application/octet-stream') // 'bin' -``` - -### mime.charset(type) - -Lookup the implied default charset of a content-type. - -```js -mime.charset('text/x-markdown') // 'UTF-8' -``` - -### var type = mime.types[extension] - -A map of content-types by extension. - -### [extensions...] = mime.extensions[type] - -A map of extensions by content-type. - -## License - -[MIT](LICENSE) - -[npm-image]: https://img.shields.io/npm/v/mime-types.svg -[npm-url]: https://npmjs.org/package/mime-types -[node-version-image]: https://img.shields.io/node/v/mime-types.svg -[node-version-url]: https://nodejs.org/en/download/ -[travis-image]: https://img.shields.io/travis/jshttp/mime-types/master.svg -[travis-url]: https://travis-ci.org/jshttp/mime-types -[coveralls-image]: https://img.shields.io/coveralls/jshttp/mime-types/master.svg -[coveralls-url]: https://coveralls.io/r/jshttp/mime-types -[downloads-image]: https://img.shields.io/npm/dm/mime-types.svg -[downloads-url]: https://npmjs.org/package/mime-types diff --git a/node_modules/request/node_modules/mime-types/index.js b/node_modules/request/node_modules/mime-types/index.js deleted file mode 100644 index f7008b24..00000000 --- a/node_modules/request/node_modules/mime-types/index.js +++ /dev/null @@ -1,188 +0,0 @@ -/*! - * mime-types - * Copyright(c) 2014 Jonathan Ong - * Copyright(c) 2015 Douglas Christopher Wilson - * MIT Licensed - */ - -'use strict' - -/** - * Module dependencies. - * @private - */ - -var db = require('mime-db') -var extname = require('path').extname - -/** - * Module variables. - * @private - */ - -var extractTypeRegExp = /^\s*([^;\s]*)(?:;|\s|$)/ -var textTypeRegExp = /^text\//i - -/** - * Module exports. - * @public - */ - -exports.charset = charset -exports.charsets = { lookup: charset } -exports.contentType = contentType -exports.extension = extension -exports.extensions = Object.create(null) -exports.lookup = lookup -exports.types = Object.create(null) - -// Populate the extensions/types maps -populateMaps(exports.extensions, exports.types) - -/** - * Get the default charset for a MIME type. - * - * @param {string} type - * @return {boolean|string} - */ - -function charset(type) { - if (!type || typeof type !== 'string') { - return false - } - - // TODO: use media-typer - var match = extractTypeRegExp.exec(type) - var mime = match && db[match[1].toLowerCase()] - - if (mime && mime.charset) { - return mime.charset - } - - // default text/* to utf-8 - if (match && textTypeRegExp.test(match[1])) { - return 'UTF-8' - } - - return false -} - -/** - * Create a full Content-Type header given a MIME type or extension. - * - * @param {string} str - * @return {boolean|string} - */ - -function contentType(str) { - // TODO: should this even be in this module? - if (!str || typeof str !== 'string') { - return false - } - - var mime = str.indexOf('/') === -1 - ? exports.lookup(str) - : str - - if (!mime) { - return false - } - - // TODO: use content-type or other module - if (mime.indexOf('charset') === -1) { - var charset = exports.charset(mime) - if (charset) mime += '; charset=' + charset.toLowerCase() - } - - return mime -} - -/** - * Get the default extension for a MIME type. - * - * @param {string} type - * @return {boolean|string} - */ - -function extension(type) { - if (!type || typeof type !== 'string') { - return false - } - - // TODO: use media-typer - var match = extractTypeRegExp.exec(type) - - // get extensions - var exts = match && exports.extensions[match[1].toLowerCase()] - - if (!exts || !exts.length) { - return false - } - - return exts[0] -} - -/** - * Lookup the MIME type for a file path/extension. - * - * @param {string} path - * @return {boolean|string} - */ - -function lookup(path) { - if (!path || typeof path !== 'string') { - return false - } - - // get the extension ("ext" or ".ext" or full path) - var extension = extname('x.' + path) - .toLowerCase() - .substr(1) - - if (!extension) { - return false - } - - return exports.types[extension] || false -} - -/** - * Populate the extensions and types maps. - * @private - */ - -function populateMaps(extensions, types) { - // source preference (least -> most) - var preference = ['nginx', 'apache', undefined, 'iana'] - - Object.keys(db).forEach(function forEachMimeType(type) { - var mime = db[type] - var exts = mime.extensions - - if (!exts || !exts.length) { - return - } - - // mime -> extensions - extensions[type] = exts - - // extension -> mime - for (var i = 0; i < exts.length; i++) { - var extension = exts[i] - - if (types[extension]) { - var from = preference.indexOf(db[types[extension]].source) - var to = preference.indexOf(mime.source) - - if (types[extension] !== 'application/octet-stream' - && from > to || (from === to && types[extension].substr(0, 12) === 'application/')) { - // skip the remapping - continue - } - } - - // set the extension -> mime - types[extension] = type - } - }) -} diff --git a/node_modules/request/node_modules/mime-types/node_modules/mime-db/HISTORY.md b/node_modules/request/node_modules/mime-types/node_modules/mime-db/HISTORY.md deleted file mode 100644 index d6705ac8..00000000 --- a/node_modules/request/node_modules/mime-types/node_modules/mime-db/HISTORY.md +++ /dev/null @@ -1,341 +0,0 @@ -1.23.0 / 2016-05-01 -=================== - - * Add `application/efi` - * Add `application/vnd.3gpp.sms+xml` - * Add `application/vnd.3lightssoftware.imagescal` - * Add `application/vnd.coreos.ignition+json` - * Add `application/vnd.desmume.movie` - * Add `application/vnd.onepager` - * Add `application/vnd.vel+json` - * Add `text/prs.prop.logic` - * Add `video/encaprtp` - * Add `video/h265` - * Add `video/iso.segment` - * Add `video/raptorfec` - * Add `video/rtploopback` - * Add `video/vnd.radgamettools.bink` - * Add `video/vnd.radgamettools.smacker` - * Add `video/vp8` - * Add extension `.3gpp` to `audio/3gpp` - -1.22.0 / 2016-02-15 -=================== - - * Add `application/ppsp-tracker+json` - * Add `application/problem+json` - * Add `application/problem+xml` - * Add `application/vnd.hdt` - * Add `application/vnd.ms-printschematicket+xml` - * Add `model/vnd.rosette.annotated-data-model` - * Add `text/slim` - * Add extension `.rng` to `application/xml` - * Fix extension of `application/dash+xml` to be `.mpd` - * Update primary extension to `.m4a` for `audio/mp4` - -1.21.0 / 2016-01-06 -=================== - - * Add `application/emergencycalldata.comment+xml` - * Add `application/emergencycalldata.deviceinfo+xml` - * Add `application/emergencycalldata.providerinfo+xml` - * Add `application/emergencycalldata.serviceinfo+xml` - * Add `application/emergencycalldata.subscriberinfo+xml` - * Add `application/vnd.filmit.zfc` - * Add `application/vnd.google-apps.document` - * Add `application/vnd.google-apps.presentation` - * Add `application/vnd.google-apps.spreadsheet` - * Add `application/vnd.mapbox-vector-tile` - * Add `application/vnd.ms-printdevicecapabilities+xml` - * Add `application/vnd.ms-windows.devicepairing` - * Add `application/vnd.ms-windows.nwprinting.oob` - * Add `application/vnd.tml` - * Add `audio/evs` - -1.20.0 / 2015-11-10 -=================== - - * Add `application/cdni` - * Add `application/csvm+json` - * Add `application/rfc+xml` - * Add `application/vnd.3gpp.access-transfer-events+xml` - * Add `application/vnd.3gpp.srvcc-ext+xml` - * Add `application/vnd.ms-windows.wsd.oob` - * Add `application/vnd.oxli.countgraph` - * Add `application/vnd.pagerduty+json` - * Add `text/x-suse-ymp` - -1.19.0 / 2015-09-17 -=================== - - * Add `application/vnd.3gpp-prose-pc3ch+xml` - * Add `application/vnd.3gpp.srvcc-info+xml` - * Add `application/vnd.apple.pkpass` - * Add `application/vnd.drive+json` - -1.18.0 / 2015-09-03 -=================== - - * Add `application/pkcs12` - * Add `application/vnd.3gpp-prose+xml` - * Add `application/vnd.3gpp.mid-call+xml` - * Add `application/vnd.3gpp.state-and-event-info+xml` - * Add `application/vnd.anki` - * Add `application/vnd.firemonkeys.cloudcell` - * Add `application/vnd.openblox.game+xml` - * Add `application/vnd.openblox.game-binary` - -1.17.0 / 2015-08-13 -=================== - - * Add `application/x-msdos-program` - * Add `audio/g711-0` - * Add `image/vnd.mozilla.apng` - * Add extension `.exe` to `application/x-msdos-program` - -1.16.0 / 2015-07-29 -=================== - - * Add `application/vnd.uri-map` - -1.15.0 / 2015-07-13 -=================== - - * Add `application/x-httpd-php` - -1.14.0 / 2015-06-25 -=================== - - * Add `application/scim+json` - * Add `application/vnd.3gpp.ussd+xml` - * Add `application/vnd.biopax.rdf+xml` - * Add `text/x-processing` - -1.13.0 / 2015-06-07 -=================== - - * Add nginx as a source - * Add `application/x-cocoa` - * Add `application/x-java-archive-diff` - * Add `application/x-makeself` - * Add `application/x-perl` - * Add `application/x-pilot` - * Add `application/x-redhat-package-manager` - * Add `application/x-sea` - * Add `audio/x-m4a` - * Add `audio/x-realaudio` - * Add `image/x-jng` - * Add `text/mathml` - -1.12.0 / 2015-06-05 -=================== - - * Add `application/bdoc` - * Add `application/vnd.hyperdrive+json` - * Add `application/x-bdoc` - * Add extension `.rtf` to `text/rtf` - -1.11.0 / 2015-05-31 -=================== - - * Add `audio/wav` - * Add `audio/wave` - * Add extension `.litcoffee` to `text/coffeescript` - * Add extension `.sfd-hdstx` to `application/vnd.hydrostatix.sof-data` - * Add extension `.n-gage` to `application/vnd.nokia.n-gage.symbian.install` - -1.10.0 / 2015-05-19 -=================== - - * Add `application/vnd.balsamiq.bmpr` - * Add `application/vnd.microsoft.portable-executable` - * Add `application/x-ns-proxy-autoconfig` - -1.9.1 / 2015-04-19 -================== - - * Remove `.json` extension from `application/manifest+json` - - This is causing bugs downstream - -1.9.0 / 2015-04-19 -================== - - * Add `application/manifest+json` - * Add `application/vnd.micro+json` - * Add `image/vnd.zbrush.pcx` - * Add `image/x-ms-bmp` - -1.8.0 / 2015-03-13 -================== - - * Add `application/vnd.citationstyles.style+xml` - * Add `application/vnd.fastcopy-disk-image` - * Add `application/vnd.gov.sk.xmldatacontainer+xml` - * Add extension `.jsonld` to `application/ld+json` - -1.7.0 / 2015-02-08 -================== - - * Add `application/vnd.gerber` - * Add `application/vnd.msa-disk-image` - -1.6.1 / 2015-02-05 -================== - - * Community extensions ownership transferred from `node-mime` - -1.6.0 / 2015-01-29 -================== - - * Add `application/jose` - * Add `application/jose+json` - * Add `application/json-seq` - * Add `application/jwk+json` - * Add `application/jwk-set+json` - * Add `application/jwt` - * Add `application/rdap+json` - * Add `application/vnd.gov.sk.e-form+xml` - * Add `application/vnd.ims.imsccv1p3` - -1.5.0 / 2014-12-30 -================== - - * Add `application/vnd.oracle.resource+json` - * Fix various invalid MIME type entries - - `application/mbox+xml` - - `application/oscp-response` - - `application/vwg-multiplexed` - - `audio/g721` - -1.4.0 / 2014-12-21 -================== - - * Add `application/vnd.ims.imsccv1p2` - * Fix various invalid MIME type entries - - `application/vnd-acucobol` - - `application/vnd-curl` - - `application/vnd-dart` - - `application/vnd-dxr` - - `application/vnd-fdf` - - `application/vnd-mif` - - `application/vnd-sema` - - `application/vnd-wap-wmlc` - - `application/vnd.adobe.flash-movie` - - `application/vnd.dece-zip` - - `application/vnd.dvb_service` - - `application/vnd.micrografx-igx` - - `application/vnd.sealed-doc` - - `application/vnd.sealed-eml` - - `application/vnd.sealed-mht` - - `application/vnd.sealed-ppt` - - `application/vnd.sealed-tiff` - - `application/vnd.sealed-xls` - - `application/vnd.sealedmedia.softseal-html` - - `application/vnd.sealedmedia.softseal-pdf` - - `application/vnd.wap-slc` - - `application/vnd.wap-wbxml` - - `audio/vnd.sealedmedia.softseal-mpeg` - - `image/vnd-djvu` - - `image/vnd-svf` - - `image/vnd-wap-wbmp` - - `image/vnd.sealed-png` - - `image/vnd.sealedmedia.softseal-gif` - - `image/vnd.sealedmedia.softseal-jpg` - - `model/vnd-dwf` - - `model/vnd.parasolid.transmit-binary` - - `model/vnd.parasolid.transmit-text` - - `text/vnd-a` - - `text/vnd-curl` - - `text/vnd.wap-wml` - * Remove example template MIME types - - `application/example` - - `audio/example` - - `image/example` - - `message/example` - - `model/example` - - `multipart/example` - - `text/example` - - `video/example` - -1.3.1 / 2014-12-16 -================== - - * Fix missing extensions - - `application/json5` - - `text/hjson` - -1.3.0 / 2014-12-07 -================== - - * Add `application/a2l` - * Add `application/aml` - * Add `application/atfx` - * Add `application/atxml` - * Add `application/cdfx+xml` - * Add `application/dii` - * Add `application/json5` - * Add `application/lxf` - * Add `application/mf4` - * Add `application/vnd.apache.thrift.compact` - * Add `application/vnd.apache.thrift.json` - * Add `application/vnd.coffeescript` - * Add `application/vnd.enphase.envoy` - * Add `application/vnd.ims.imsccv1p1` - * Add `text/csv-schema` - * Add `text/hjson` - * Add `text/markdown` - * Add `text/yaml` - -1.2.0 / 2014-11-09 -================== - - * Add `application/cea` - * Add `application/dit` - * Add `application/vnd.gov.sk.e-form+zip` - * Add `application/vnd.tmd.mediaflex.api+xml` - * Type `application/epub+zip` is now IANA-registered - -1.1.2 / 2014-10-23 -================== - - * Rebuild database for `application/x-www-form-urlencoded` change - -1.1.1 / 2014-10-20 -================== - - * Mark `application/x-www-form-urlencoded` as compressible. - -1.1.0 / 2014-09-28 -================== - - * Add `application/font-woff2` - -1.0.3 / 2014-09-25 -================== - - * Fix engine requirement in package - -1.0.2 / 2014-09-25 -================== - - * Add `application/coap-group+json` - * Add `application/dcd` - * Add `application/vnd.apache.thrift.binary` - * Add `image/vnd.tencent.tap` - * Mark all JSON-derived types as compressible - * Update `text/vtt` data - -1.0.1 / 2014-08-30 -================== - - * Fix extension ordering - -1.0.0 / 2014-08-30 -================== - - * Add `application/atf` - * Add `application/merge-patch+json` - * Add `multipart/x-mixed-replace` - * Add `source: 'apache'` metadata - * Add `source: 'iana'` metadata - * Remove badly-assumed charset data diff --git a/node_modules/request/node_modules/mime-types/node_modules/mime-db/LICENSE b/node_modules/request/node_modules/mime-types/node_modules/mime-db/LICENSE deleted file mode 100644 index a7ae8ee9..00000000 --- a/node_modules/request/node_modules/mime-types/node_modules/mime-db/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ - -The MIT License (MIT) - -Copyright (c) 2014 Jonathan Ong me@jongleberry.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/node_modules/request/node_modules/mime-types/node_modules/mime-db/README.md b/node_modules/request/node_modules/mime-types/node_modules/mime-db/README.md deleted file mode 100644 index 7662440b..00000000 --- a/node_modules/request/node_modules/mime-types/node_modules/mime-db/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# mime-db - -[![NPM Version][npm-version-image]][npm-url] -[![NPM Downloads][npm-downloads-image]][npm-url] -[![Node.js Version][node-image]][node-url] -[![Build Status][travis-image]][travis-url] -[![Coverage Status][coveralls-image]][coveralls-url] - -This is a database of all mime types. -It consists of a single, public JSON file and does not include any logic, -allowing it to remain as un-opinionated as possible with an API. -It aggregates data from the following sources: - -- http://www.iana.org/assignments/media-types/media-types.xhtml -- http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -- http://hg.nginx.org/nginx/raw-file/default/conf/mime.types - -## Installation - -```bash -npm install mime-db -``` - -### Database Download - -If you're crazy enough to use this in the browser, you can just grab the -JSON file using [RawGit](https://rawgit.com/). It is recommended to replace -`master` with [a release tag](https://github.com/jshttp/mime-db/tags) as the -JSON format may change in the future. - -``` -https://cdn.rawgit.com/jshttp/mime-db/master/db.json -``` - -## Usage - -```js -var db = require('mime-db'); - -// grab data on .js files -var data = db['application/javascript']; -``` - -## Data Structure - -The JSON file is a map lookup for lowercased mime types. -Each mime type has the following properties: - -- `.source` - where the mime type is defined. - If not set, it's probably a custom media type. - - `apache` - [Apache common media types](http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types) - - `iana` - [IANA-defined media types](http://www.iana.org/assignments/media-types/media-types.xhtml) - - `nginx` - [nginx media types](http://hg.nginx.org/nginx/raw-file/default/conf/mime.types) -- `.extensions[]` - known extensions associated with this mime type. -- `.compressible` - whether a file of this type can be gzipped. -- `.charset` - the default charset associated with this type, if any. - -If unknown, every property could be `undefined`. - -## Contributing - -To edit the database, only make PRs against `src/custom.json` or -`src/custom-suffix.json`. - -To update the build, run `npm run build`. - -## Adding Custom Media Types - -The best way to get new media types included in this library is to register -them with the IANA. The community registration procedure is outlined in -[RFC 6838 section 5](http://tools.ietf.org/html/rfc6838#section-5). Types -registered with the IANA are automatically pulled into this library. - -[npm-version-image]: https://img.shields.io/npm/v/mime-db.svg -[npm-downloads-image]: https://img.shields.io/npm/dm/mime-db.svg -[npm-url]: https://npmjs.org/package/mime-db -[travis-image]: https://img.shields.io/travis/jshttp/mime-db/master.svg -[travis-url]: https://travis-ci.org/jshttp/mime-db -[coveralls-image]: https://img.shields.io/coveralls/jshttp/mime-db/master.svg -[coveralls-url]: https://coveralls.io/r/jshttp/mime-db?branch=master -[node-image]: https://img.shields.io/node/v/mime-db.svg -[node-url]: http://nodejs.org/download/ diff --git a/node_modules/request/node_modules/mime-types/node_modules/mime-db/db.json b/node_modules/request/node_modules/mime-types/node_modules/mime-db/db.json deleted file mode 100644 index 0a5a8a7b..00000000 --- a/node_modules/request/node_modules/mime-types/node_modules/mime-db/db.json +++ /dev/null @@ -1,6627 +0,0 @@ -{ - "application/1d-interleaved-parityfec": { - "source": "iana" - }, - "application/3gpdash-qoe-report+xml": { - "source": "iana" - }, - "application/3gpp-ims+xml": { - "source": "iana" - }, - "application/a2l": { - "source": "iana" - }, - "application/activemessage": { - "source": "iana" - }, - "application/alto-costmap+json": { - "source": "iana", - "compressible": true - }, - "application/alto-costmapfilter+json": { - "source": "iana", - "compressible": true - }, - "application/alto-directory+json": { - "source": "iana", - "compressible": true - }, - "application/alto-endpointcost+json": { - "source": "iana", - "compressible": true - }, - "application/alto-endpointcostparams+json": { - "source": "iana", - "compressible": true - }, - "application/alto-endpointprop+json": { - "source": "iana", - "compressible": true - }, - "application/alto-endpointpropparams+json": { - "source": "iana", - "compressible": true - }, - "application/alto-error+json": { - "source": "iana", - "compressible": true - }, - "application/alto-networkmap+json": { - "source": "iana", - "compressible": true - }, - "application/alto-networkmapfilter+json": { - "source": "iana", - "compressible": true - }, - "application/aml": { - "source": "iana" - }, - "application/andrew-inset": { - "source": "iana", - "extensions": ["ez"] - }, - "application/applefile": { - "source": "iana" - }, - "application/applixware": { - "source": "apache", - "extensions": ["aw"] - }, - "application/atf": { - "source": "iana" - }, - "application/atfx": { - "source": "iana" - }, - "application/atom+xml": { - "source": "iana", - "compressible": true, - "extensions": ["atom"] - }, - "application/atomcat+xml": { - "source": "iana", - "extensions": ["atomcat"] - }, - "application/atomdeleted+xml": { - "source": "iana" - }, - "application/atomicmail": { - "source": "iana" - }, - "application/atomsvc+xml": { - "source": "iana", - "extensions": ["atomsvc"] - }, - "application/atxml": { - "source": "iana" - }, - "application/auth-policy+xml": { - "source": "iana" - }, - "application/bacnet-xdd+zip": { - "source": "iana" - }, - "application/batch-smtp": { - "source": "iana" - }, - "application/bdoc": { - "compressible": false, - "extensions": ["bdoc"] - }, - "application/beep+xml": { - "source": "iana" - }, - "application/calendar+json": { - "source": "iana", - "compressible": true - }, - "application/calendar+xml": { - "source": "iana" - }, - "application/call-completion": { - "source": "iana" - }, - "application/cals-1840": { - "source": "iana" - }, - "application/cbor": { - "source": "iana" - }, - "application/ccmp+xml": { - "source": "iana" - }, - "application/ccxml+xml": { - "source": "iana", - "extensions": ["ccxml"] - }, - "application/cdfx+xml": { - "source": "iana" - }, - "application/cdmi-capability": { - "source": "iana", - "extensions": ["cdmia"] - }, - "application/cdmi-container": { - "source": "iana", - "extensions": ["cdmic"] - }, - "application/cdmi-domain": { - "source": "iana", - "extensions": ["cdmid"] - }, - "application/cdmi-object": { - "source": "iana", - "extensions": ["cdmio"] - }, - "application/cdmi-queue": { - "source": "iana", - "extensions": ["cdmiq"] - }, - "application/cdni": { - "source": "iana" - }, - "application/cea": { - "source": "iana" - }, - "application/cea-2018+xml": { - "source": "iana" - }, - "application/cellml+xml": { - "source": "iana" - }, - "application/cfw": { - "source": "iana" - }, - "application/cms": { - "source": "iana" - }, - "application/cnrp+xml": { - "source": "iana" - }, - "application/coap-group+json": { - "source": "iana", - "compressible": true - }, - "application/commonground": { - "source": "iana" - }, - "application/conference-info+xml": { - "source": "iana" - }, - "application/cpl+xml": { - "source": "iana" - }, - "application/csrattrs": { - "source": "iana" - }, - "application/csta+xml": { - "source": "iana" - }, - "application/cstadata+xml": { - "source": "iana" - }, - "application/csvm+json": { - "source": "iana", - "compressible": true - }, - "application/cu-seeme": { - "source": "apache", - "extensions": ["cu"] - }, - "application/cybercash": { - "source": "iana" - }, - "application/dart": { - "compressible": true - }, - "application/dash+xml": { - "source": "iana", - "extensions": ["mpd"] - }, - "application/dashdelta": { - "source": "iana" - }, - "application/davmount+xml": { - "source": "iana", - "extensions": ["davmount"] - }, - "application/dca-rft": { - "source": "iana" - }, - "application/dcd": { - "source": "iana" - }, - "application/dec-dx": { - "source": "iana" - }, - "application/dialog-info+xml": { - "source": "iana" - }, - "application/dicom": { - "source": "iana" - }, - "application/dii": { - "source": "iana" - }, - "application/dit": { - "source": "iana" - }, - "application/dns": { - "source": "iana" - }, - "application/docbook+xml": { - "source": "apache", - "extensions": ["dbk"] - }, - "application/dskpp+xml": { - "source": "iana" - }, - "application/dssc+der": { - "source": "iana", - "extensions": ["dssc"] - }, - "application/dssc+xml": { - "source": "iana", - "extensions": ["xdssc"] - }, - "application/dvcs": { - "source": "iana" - }, - "application/ecmascript": { - "source": "iana", - "compressible": true, - "extensions": ["ecma"] - }, - "application/edi-consent": { - "source": "iana" - }, - "application/edi-x12": { - "source": "iana", - "compressible": false - }, - "application/edifact": { - "source": "iana", - "compressible": false - }, - "application/efi": { - "source": "iana" - }, - "application/emergencycalldata.comment+xml": { - "source": "iana" - }, - "application/emergencycalldata.deviceinfo+xml": { - "source": "iana" - }, - "application/emergencycalldata.providerinfo+xml": { - "source": "iana" - }, - "application/emergencycalldata.serviceinfo+xml": { - "source": "iana" - }, - "application/emergencycalldata.subscriberinfo+xml": { - "source": "iana" - }, - "application/emma+xml": { - "source": "iana", - "extensions": ["emma"] - }, - "application/emotionml+xml": { - "source": "iana" - }, - "application/encaprtp": { - "source": "iana" - }, - "application/epp+xml": { - "source": "iana" - }, - "application/epub+zip": { - "source": "iana", - "extensions": ["epub"] - }, - "application/eshop": { - "source": "iana" - }, - "application/exi": { - "source": "iana", - "extensions": ["exi"] - }, - "application/fastinfoset": { - "source": "iana" - }, - "application/fastsoap": { - "source": "iana" - }, - "application/fdt+xml": { - "source": "iana" - }, - "application/fits": { - "source": "iana" - }, - "application/font-sfnt": { - "source": "iana" - }, - "application/font-tdpfr": { - "source": "iana", - "extensions": ["pfr"] - }, - "application/font-woff": { - "source": "iana", - "compressible": false, - "extensions": ["woff"] - }, - "application/font-woff2": { - "compressible": false, - "extensions": ["woff2"] - }, - "application/framework-attributes+xml": { - "source": "iana" - }, - "application/gml+xml": { - "source": "apache", - "extensions": ["gml"] - }, - "application/gpx+xml": { - "source": "apache", - "extensions": ["gpx"] - }, - "application/gxf": { - "source": "apache", - "extensions": ["gxf"] - }, - "application/gzip": { - "source": "iana", - "compressible": false - }, - "application/h224": { - "source": "iana" - }, - "application/held+xml": { - "source": "iana" - }, - "application/http": { - "source": "iana" - }, - "application/hyperstudio": { - "source": "iana", - "extensions": ["stk"] - }, - "application/ibe-key-request+xml": { - "source": "iana" - }, - "application/ibe-pkg-reply+xml": { - "source": "iana" - }, - "application/ibe-pp-data": { - "source": "iana" - }, - "application/iges": { - "source": "iana" - }, - "application/im-iscomposing+xml": { - "source": "iana" - }, - "application/index": { - "source": "iana" - }, - "application/index.cmd": { - "source": "iana" - }, - "application/index.obj": { - "source": "iana" - }, - "application/index.response": { - "source": "iana" - }, - "application/index.vnd": { - "source": "iana" - }, - "application/inkml+xml": { - "source": "iana", - "extensions": ["ink","inkml"] - }, - "application/iotp": { - "source": "iana" - }, - "application/ipfix": { - "source": "iana", - "extensions": ["ipfix"] - }, - "application/ipp": { - "source": "iana" - }, - "application/isup": { - "source": "iana" - }, - "application/its+xml": { - "source": "iana" - }, - "application/java-archive": { - "source": "apache", - "compressible": false, - "extensions": ["jar","war","ear"] - }, - "application/java-serialized-object": { - "source": "apache", - "compressible": false, - "extensions": ["ser"] - }, - "application/java-vm": { - "source": "apache", - "compressible": false, - "extensions": ["class"] - }, - "application/javascript": { - "source": "iana", - "charset": "UTF-8", - "compressible": true, - "extensions": ["js"] - }, - "application/jose": { - "source": "iana" - }, - "application/jose+json": { - "source": "iana", - "compressible": true - }, - "application/jrd+json": { - "source": "iana", - "compressible": true - }, - "application/json": { - "source": "iana", - "charset": "UTF-8", - "compressible": true, - "extensions": ["json","map"] - }, - "application/json-patch+json": { - "source": "iana", - "compressible": true - }, - "application/json-seq": { - "source": "iana" - }, - "application/json5": { - "extensions": ["json5"] - }, - "application/jsonml+json": { - "source": "apache", - "compressible": true, - "extensions": ["jsonml"] - }, - "application/jwk+json": { - "source": "iana", - "compressible": true - }, - "application/jwk-set+json": { - "source": "iana", - "compressible": true - }, - "application/jwt": { - "source": "iana" - }, - "application/kpml-request+xml": { - "source": "iana" - }, - "application/kpml-response+xml": { - "source": "iana" - }, - "application/ld+json": { - "source": "iana", - "compressible": true, - "extensions": ["jsonld"] - }, - "application/link-format": { - "source": "iana" - }, - "application/load-control+xml": { - "source": "iana" - }, - "application/lost+xml": { - "source": "iana", - "extensions": ["lostxml"] - }, - "application/lostsync+xml": { - "source": "iana" - }, - "application/lxf": { - "source": "iana" - }, - "application/mac-binhex40": { - "source": "iana", - "extensions": ["hqx"] - }, - "application/mac-compactpro": { - "source": "apache", - "extensions": ["cpt"] - }, - "application/macwriteii": { - "source": "iana" - }, - "application/mads+xml": { - "source": "iana", - "extensions": ["mads"] - }, - "application/manifest+json": { - "charset": "UTF-8", - "compressible": true, - "extensions": ["webmanifest"] - }, - "application/marc": { - "source": "iana", - "extensions": ["mrc"] - }, - "application/marcxml+xml": { - "source": "iana", - "extensions": ["mrcx"] - }, - "application/mathematica": { - "source": "iana", - "extensions": ["ma","nb","mb"] - }, - "application/mathml+xml": { - "source": "iana", - "extensions": ["mathml"] - }, - "application/mathml-content+xml": { - "source": "iana" - }, - "application/mathml-presentation+xml": { - "source": "iana" - }, - "application/mbms-associated-procedure-description+xml": { - "source": "iana" - }, - "application/mbms-deregister+xml": { - "source": "iana" - }, - "application/mbms-envelope+xml": { - "source": "iana" - }, - "application/mbms-msk+xml": { - "source": "iana" - }, - "application/mbms-msk-response+xml": { - "source": "iana" - }, - "application/mbms-protection-description+xml": { - "source": "iana" - }, - "application/mbms-reception-report+xml": { - "source": "iana" - }, - "application/mbms-register+xml": { - "source": "iana" - }, - "application/mbms-register-response+xml": { - "source": "iana" - }, - "application/mbms-schedule+xml": { - "source": "iana" - }, - "application/mbms-user-service-description+xml": { - "source": "iana" - }, - "application/mbox": { - "source": "iana", - "extensions": ["mbox"] - }, - "application/media-policy-dataset+xml": { - "source": "iana" - }, - "application/media_control+xml": { - "source": "iana" - }, - "application/mediaservercontrol+xml": { - "source": "iana", - "extensions": ["mscml"] - }, - "application/merge-patch+json": { - "source": "iana", - "compressible": true - }, - "application/metalink+xml": { - "source": "apache", - "extensions": ["metalink"] - }, - "application/metalink4+xml": { - "source": "iana", - "extensions": ["meta4"] - }, - "application/mets+xml": { - "source": "iana", - "extensions": ["mets"] - }, - "application/mf4": { - "source": "iana" - }, - "application/mikey": { - "source": "iana" - }, - "application/mods+xml": { - "source": "iana", - "extensions": ["mods"] - }, - "application/moss-keys": { - "source": "iana" - }, - "application/moss-signature": { - "source": "iana" - }, - "application/mosskey-data": { - "source": "iana" - }, - "application/mosskey-request": { - "source": "iana" - }, - "application/mp21": { - "source": "iana", - "extensions": ["m21","mp21"] - }, - "application/mp4": { - "source": "iana", - "extensions": ["mp4s","m4p"] - }, - "application/mpeg4-generic": { - "source": "iana" - }, - "application/mpeg4-iod": { - "source": "iana" - }, - "application/mpeg4-iod-xmt": { - "source": "iana" - }, - "application/mrb-consumer+xml": { - "source": "iana" - }, - "application/mrb-publish+xml": { - "source": "iana" - }, - "application/msc-ivr+xml": { - "source": "iana" - }, - "application/msc-mixer+xml": { - "source": "iana" - }, - "application/msword": { - "source": "iana", - "compressible": false, - "extensions": ["doc","dot"] - }, - "application/mxf": { - "source": "iana", - "extensions": ["mxf"] - }, - "application/nasdata": { - "source": "iana" - }, - "application/news-checkgroups": { - "source": "iana" - }, - "application/news-groupinfo": { - "source": "iana" - }, - "application/news-transmission": { - "source": "iana" - }, - "application/nlsml+xml": { - "source": "iana" - }, - "application/nss": { - "source": "iana" - }, - "application/ocsp-request": { - "source": "iana" - }, - "application/ocsp-response": { - "source": "iana" - }, - "application/octet-stream": { - "source": "iana", - "compressible": false, - "extensions": ["bin","dms","lrf","mar","so","dist","distz","pkg","bpk","dump","elc","deploy","exe","dll","deb","dmg","iso","img","msi","msp","msm","buffer"] - }, - "application/oda": { - "source": "iana", - "extensions": ["oda"] - }, - "application/odx": { - "source": "iana" - }, - "application/oebps-package+xml": { - "source": "iana", - "extensions": ["opf"] - }, - "application/ogg": { - "source": "iana", - "compressible": false, - "extensions": ["ogx"] - }, - "application/omdoc+xml": { - "source": "apache", - "extensions": ["omdoc"] - }, - "application/onenote": { - "source": "apache", - "extensions": ["onetoc","onetoc2","onetmp","onepkg"] - }, - "application/oxps": { - "source": "iana", - "extensions": ["oxps"] - }, - "application/p2p-overlay+xml": { - "source": "iana" - }, - "application/parityfec": { - "source": "iana" - }, - "application/patch-ops-error+xml": { - "source": "iana", - "extensions": ["xer"] - }, - "application/pdf": { - "source": "iana", - "compressible": false, - "extensions": ["pdf"] - }, - "application/pdx": { - "source": "iana" - }, - "application/pgp-encrypted": { - "source": "iana", - "compressible": false, - "extensions": ["pgp"] - }, - "application/pgp-keys": { - "source": "iana" - }, - "application/pgp-signature": { - "source": "iana", - "extensions": ["asc","sig"] - }, - "application/pics-rules": { - "source": "apache", - "extensions": ["prf"] - }, - "application/pidf+xml": { - "source": "iana" - }, - "application/pidf-diff+xml": { - "source": "iana" - }, - "application/pkcs10": { - "source": "iana", - "extensions": ["p10"] - }, - "application/pkcs12": { - "source": "iana" - }, - "application/pkcs7-mime": { - "source": "iana", - "extensions": ["p7m","p7c"] - }, - "application/pkcs7-signature": { - "source": "iana", - "extensions": ["p7s"] - }, - "application/pkcs8": { - "source": "iana", - "extensions": ["p8"] - }, - "application/pkix-attr-cert": { - "source": "iana", - "extensions": ["ac"] - }, - "application/pkix-cert": { - "source": "iana", - "extensions": ["cer"] - }, - "application/pkix-crl": { - "source": "iana", - "extensions": ["crl"] - }, - "application/pkix-pkipath": { - "source": "iana", - "extensions": ["pkipath"] - }, - "application/pkixcmp": { - "source": "iana", - "extensions": ["pki"] - }, - "application/pls+xml": { - "source": "iana", - "extensions": ["pls"] - }, - "application/poc-settings+xml": { - "source": "iana" - }, - "application/postscript": { - "source": "iana", - "compressible": true, - "extensions": ["ai","eps","ps"] - }, - "application/ppsp-tracker+json": { - "source": "iana", - "compressible": true - }, - "application/problem+json": { - "source": "iana", - "compressible": true - }, - "application/problem+xml": { - "source": "iana" - }, - "application/provenance+xml": { - "source": "iana" - }, - "application/prs.alvestrand.titrax-sheet": { - "source": "iana" - }, - "application/prs.cww": { - "source": "iana", - "extensions": ["cww"] - }, - "application/prs.hpub+zip": { - "source": "iana" - }, - "application/prs.nprend": { - "source": "iana" - }, - "application/prs.plucker": { - "source": "iana" - }, - "application/prs.rdf-xml-crypt": { - "source": "iana" - }, - "application/prs.xsf+xml": { - "source": "iana" - }, - "application/pskc+xml": { - "source": "iana", - "extensions": ["pskcxml"] - }, - "application/qsig": { - "source": "iana" - }, - "application/raptorfec": { - "source": "iana" - }, - "application/rdap+json": { - "source": "iana", - "compressible": true - }, - "application/rdf+xml": { - "source": "iana", - "compressible": true, - "extensions": ["rdf"] - }, - "application/reginfo+xml": { - "source": "iana", - "extensions": ["rif"] - }, - "application/relax-ng-compact-syntax": { - "source": "iana", - "extensions": ["rnc"] - }, - "application/remote-printing": { - "source": "iana" - }, - "application/reputon+json": { - "source": "iana", - "compressible": true - }, - "application/resource-lists+xml": { - "source": "iana", - "extensions": ["rl"] - }, - "application/resource-lists-diff+xml": { - "source": "iana", - "extensions": ["rld"] - }, - "application/rfc+xml": { - "source": "iana" - }, - "application/riscos": { - "source": "iana" - }, - "application/rlmi+xml": { - "source": "iana" - }, - "application/rls-services+xml": { - "source": "iana", - "extensions": ["rs"] - }, - "application/rpki-ghostbusters": { - "source": "iana", - "extensions": ["gbr"] - }, - "application/rpki-manifest": { - "source": "iana", - "extensions": ["mft"] - }, - "application/rpki-roa": { - "source": "iana", - "extensions": ["roa"] - }, - "application/rpki-updown": { - "source": "iana" - }, - "application/rsd+xml": { - "source": "apache", - "extensions": ["rsd"] - }, - "application/rss+xml": { - "source": "apache", - "compressible": true, - "extensions": ["rss"] - }, - "application/rtf": { - "source": "iana", - "compressible": true, - "extensions": ["rtf"] - }, - "application/rtploopback": { - "source": "iana" - }, - "application/rtx": { - "source": "iana" - }, - "application/samlassertion+xml": { - "source": "iana" - }, - "application/samlmetadata+xml": { - "source": "iana" - }, - "application/sbml+xml": { - "source": "iana", - "extensions": ["sbml"] - }, - "application/scaip+xml": { - "source": "iana" - }, - "application/scim+json": { - "source": "iana", - "compressible": true - }, - "application/scvp-cv-request": { - "source": "iana", - "extensions": ["scq"] - }, - "application/scvp-cv-response": { - "source": "iana", - "extensions": ["scs"] - }, - "application/scvp-vp-request": { - "source": "iana", - "extensions": ["spq"] - }, - "application/scvp-vp-response": { - "source": "iana", - "extensions": ["spp"] - }, - "application/sdp": { - "source": "iana", - "extensions": ["sdp"] - }, - "application/sep+xml": { - "source": "iana" - }, - "application/sep-exi": { - "source": "iana" - }, - "application/session-info": { - "source": "iana" - }, - "application/set-payment": { - "source": "iana" - }, - "application/set-payment-initiation": { - "source": "iana", - "extensions": ["setpay"] - }, - "application/set-registration": { - "source": "iana" - }, - "application/set-registration-initiation": { - "source": "iana", - "extensions": ["setreg"] - }, - "application/sgml": { - "source": "iana" - }, - "application/sgml-open-catalog": { - "source": "iana" - }, - "application/shf+xml": { - "source": "iana", - "extensions": ["shf"] - }, - "application/sieve": { - "source": "iana" - }, - "application/simple-filter+xml": { - "source": "iana" - }, - "application/simple-message-summary": { - "source": "iana" - }, - "application/simplesymbolcontainer": { - "source": "iana" - }, - "application/slate": { - "source": "iana" - }, - "application/smil": { - "source": "iana" - }, - "application/smil+xml": { - "source": "iana", - "extensions": ["smi","smil"] - }, - "application/smpte336m": { - "source": "iana" - }, - "application/soap+fastinfoset": { - "source": "iana" - }, - "application/soap+xml": { - "source": "iana", - "compressible": true - }, - "application/sparql-query": { - "source": "iana", - "extensions": ["rq"] - }, - "application/sparql-results+xml": { - "source": "iana", - "extensions": ["srx"] - }, - "application/spirits-event+xml": { - "source": "iana" - }, - "application/sql": { - "source": "iana" - }, - "application/srgs": { - "source": "iana", - "extensions": ["gram"] - }, - "application/srgs+xml": { - "source": "iana", - "extensions": ["grxml"] - }, - "application/sru+xml": { - "source": "iana", - "extensions": ["sru"] - }, - "application/ssdl+xml": { - "source": "apache", - "extensions": ["ssdl"] - }, - "application/ssml+xml": { - "source": "iana", - "extensions": ["ssml"] - }, - "application/tamp-apex-update": { - "source": "iana" - }, - "application/tamp-apex-update-confirm": { - "source": "iana" - }, - "application/tamp-community-update": { - "source": "iana" - }, - "application/tamp-community-update-confirm": { - "source": "iana" - }, - "application/tamp-error": { - "source": "iana" - }, - "application/tamp-sequence-adjust": { - "source": "iana" - }, - "application/tamp-sequence-adjust-confirm": { - "source": "iana" - }, - "application/tamp-status-query": { - "source": "iana" - }, - "application/tamp-status-response": { - "source": "iana" - }, - "application/tamp-update": { - "source": "iana" - }, - "application/tamp-update-confirm": { - "source": "iana" - }, - "application/tar": { - "compressible": true - }, - "application/tei+xml": { - "source": "iana", - "extensions": ["tei","teicorpus"] - }, - "application/thraud+xml": { - "source": "iana", - "extensions": ["tfi"] - }, - "application/timestamp-query": { - "source": "iana" - }, - "application/timestamp-reply": { - "source": "iana" - }, - "application/timestamped-data": { - "source": "iana", - "extensions": ["tsd"] - }, - "application/ttml+xml": { - "source": "iana" - }, - "application/tve-trigger": { - "source": "iana" - }, - "application/ulpfec": { - "source": "iana" - }, - "application/urc-grpsheet+xml": { - "source": "iana" - }, - "application/urc-ressheet+xml": { - "source": "iana" - }, - "application/urc-targetdesc+xml": { - "source": "iana" - }, - "application/urc-uisocketdesc+xml": { - "source": "iana" - }, - "application/vcard+json": { - "source": "iana", - "compressible": true - }, - "application/vcard+xml": { - "source": "iana" - }, - "application/vemmi": { - "source": "iana" - }, - "application/vividence.scriptfile": { - "source": "apache" - }, - "application/vnd.3gpp-prose+xml": { - "source": "iana" - }, - "application/vnd.3gpp-prose-pc3ch+xml": { - "source": "iana" - }, - "application/vnd.3gpp.access-transfer-events+xml": { - "source": "iana" - }, - "application/vnd.3gpp.bsf+xml": { - "source": "iana" - }, - "application/vnd.3gpp.mid-call+xml": { - "source": "iana" - }, - "application/vnd.3gpp.pic-bw-large": { - "source": "iana", - "extensions": ["plb"] - }, - "application/vnd.3gpp.pic-bw-small": { - "source": "iana", - "extensions": ["psb"] - }, - "application/vnd.3gpp.pic-bw-var": { - "source": "iana", - "extensions": ["pvb"] - }, - "application/vnd.3gpp.sms": { - "source": "iana" - }, - "application/vnd.3gpp.sms+xml": { - "source": "iana" - }, - "application/vnd.3gpp.srvcc-ext+xml": { - "source": "iana" - }, - "application/vnd.3gpp.srvcc-info+xml": { - "source": "iana" - }, - "application/vnd.3gpp.state-and-event-info+xml": { - "source": "iana" - }, - "application/vnd.3gpp.ussd+xml": { - "source": "iana" - }, - "application/vnd.3gpp2.bcmcsinfo+xml": { - "source": "iana" - }, - "application/vnd.3gpp2.sms": { - "source": "iana" - }, - "application/vnd.3gpp2.tcap": { - "source": "iana", - "extensions": ["tcap"] - }, - "application/vnd.3lightssoftware.imagescal": { - "source": "iana" - }, - "application/vnd.3m.post-it-notes": { - "source": "iana", - "extensions": ["pwn"] - }, - "application/vnd.accpac.simply.aso": { - "source": "iana", - "extensions": ["aso"] - }, - "application/vnd.accpac.simply.imp": { - "source": "iana", - "extensions": ["imp"] - }, - "application/vnd.acucobol": { - "source": "iana", - "extensions": ["acu"] - }, - "application/vnd.acucorp": { - "source": "iana", - "extensions": ["atc","acutc"] - }, - "application/vnd.adobe.air-application-installer-package+zip": { - "source": "apache", - "extensions": ["air"] - }, - "application/vnd.adobe.flash.movie": { - "source": "iana" - }, - "application/vnd.adobe.formscentral.fcdt": { - "source": "iana", - "extensions": ["fcdt"] - }, - "application/vnd.adobe.fxp": { - "source": "iana", - "extensions": ["fxp","fxpl"] - }, - "application/vnd.adobe.partial-upload": { - "source": "iana" - }, - "application/vnd.adobe.xdp+xml": { - "source": "iana", - "extensions": ["xdp"] - }, - "application/vnd.adobe.xfdf": { - "source": "iana", - "extensions": ["xfdf"] - }, - "application/vnd.aether.imp": { - "source": "iana" - }, - "application/vnd.ah-barcode": { - "source": "iana" - }, - "application/vnd.ahead.space": { - "source": "iana", - "extensions": ["ahead"] - }, - "application/vnd.airzip.filesecure.azf": { - "source": "iana", - "extensions": ["azf"] - }, - "application/vnd.airzip.filesecure.azs": { - "source": "iana", - "extensions": ["azs"] - }, - "application/vnd.amazon.ebook": { - "source": "apache", - "extensions": ["azw"] - }, - "application/vnd.americandynamics.acc": { - "source": "iana", - "extensions": ["acc"] - }, - "application/vnd.amiga.ami": { - "source": "iana", - "extensions": ["ami"] - }, - "application/vnd.amundsen.maze+xml": { - "source": "iana" - }, - "application/vnd.android.package-archive": { - "source": "apache", - "compressible": false, - "extensions": ["apk"] - }, - "application/vnd.anki": { - "source": "iana" - }, - "application/vnd.anser-web-certificate-issue-initiation": { - "source": "iana", - "extensions": ["cii"] - }, - "application/vnd.anser-web-funds-transfer-initiation": { - "source": "apache", - "extensions": ["fti"] - }, - "application/vnd.antix.game-component": { - "source": "iana", - "extensions": ["atx"] - }, - "application/vnd.apache.thrift.binary": { - "source": "iana" - }, - "application/vnd.apache.thrift.compact": { - "source": "iana" - }, - "application/vnd.apache.thrift.json": { - "source": "iana" - }, - "application/vnd.api+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.apple.installer+xml": { - "source": "iana", - "extensions": ["mpkg"] - }, - "application/vnd.apple.mpegurl": { - "source": "iana", - "extensions": ["m3u8"] - }, - "application/vnd.apple.pkpass": { - "compressible": false, - "extensions": ["pkpass"] - }, - "application/vnd.arastra.swi": { - "source": "iana" - }, - "application/vnd.aristanetworks.swi": { - "source": "iana", - "extensions": ["swi"] - }, - "application/vnd.artsquare": { - "source": "iana" - }, - "application/vnd.astraea-software.iota": { - "source": "iana", - "extensions": ["iota"] - }, - "application/vnd.audiograph": { - "source": "iana", - "extensions": ["aep"] - }, - "application/vnd.autopackage": { - "source": "iana" - }, - "application/vnd.avistar+xml": { - "source": "iana" - }, - "application/vnd.balsamiq.bmml+xml": { - "source": "iana" - }, - "application/vnd.balsamiq.bmpr": { - "source": "iana" - }, - "application/vnd.bekitzur-stech+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.biopax.rdf+xml": { - "source": "iana" - }, - "application/vnd.blueice.multipass": { - "source": "iana", - "extensions": ["mpm"] - }, - "application/vnd.bluetooth.ep.oob": { - "source": "iana" - }, - "application/vnd.bluetooth.le.oob": { - "source": "iana" - }, - "application/vnd.bmi": { - "source": "iana", - "extensions": ["bmi"] - }, - "application/vnd.businessobjects": { - "source": "iana", - "extensions": ["rep"] - }, - "application/vnd.cab-jscript": { - "source": "iana" - }, - "application/vnd.canon-cpdl": { - "source": "iana" - }, - "application/vnd.canon-lips": { - "source": "iana" - }, - "application/vnd.cendio.thinlinc.clientconf": { - "source": "iana" - }, - "application/vnd.century-systems.tcp_stream": { - "source": "iana" - }, - "application/vnd.chemdraw+xml": { - "source": "iana", - "extensions": ["cdxml"] - }, - "application/vnd.chipnuts.karaoke-mmd": { - "source": "iana", - "extensions": ["mmd"] - }, - "application/vnd.cinderella": { - "source": "iana", - "extensions": ["cdy"] - }, - "application/vnd.cirpack.isdn-ext": { - "source": "iana" - }, - "application/vnd.citationstyles.style+xml": { - "source": "iana" - }, - "application/vnd.claymore": { - "source": "iana", - "extensions": ["cla"] - }, - "application/vnd.cloanto.rp9": { - "source": "iana", - "extensions": ["rp9"] - }, - "application/vnd.clonk.c4group": { - "source": "iana", - "extensions": ["c4g","c4d","c4f","c4p","c4u"] - }, - "application/vnd.cluetrust.cartomobile-config": { - "source": "iana", - "extensions": ["c11amc"] - }, - "application/vnd.cluetrust.cartomobile-config-pkg": { - "source": "iana", - "extensions": ["c11amz"] - }, - "application/vnd.coffeescript": { - "source": "iana" - }, - "application/vnd.collection+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.collection.doc+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.collection.next+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.commerce-battelle": { - "source": "iana" - }, - "application/vnd.commonspace": { - "source": "iana", - "extensions": ["csp"] - }, - "application/vnd.contact.cmsg": { - "source": "iana", - "extensions": ["cdbcmsg"] - }, - "application/vnd.coreos.ignition+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.cosmocaller": { - "source": "iana", - "extensions": ["cmc"] - }, - "application/vnd.crick.clicker": { - "source": "iana", - "extensions": ["clkx"] - }, - "application/vnd.crick.clicker.keyboard": { - "source": "iana", - "extensions": ["clkk"] - }, - "application/vnd.crick.clicker.palette": { - "source": "iana", - "extensions": ["clkp"] - }, - "application/vnd.crick.clicker.template": { - "source": "iana", - "extensions": ["clkt"] - }, - "application/vnd.crick.clicker.wordbank": { - "source": "iana", - "extensions": ["clkw"] - }, - "application/vnd.criticaltools.wbs+xml": { - "source": "iana", - "extensions": ["wbs"] - }, - "application/vnd.ctc-posml": { - "source": "iana", - "extensions": ["pml"] - }, - "application/vnd.ctct.ws+xml": { - "source": "iana" - }, - "application/vnd.cups-pdf": { - "source": "iana" - }, - "application/vnd.cups-postscript": { - "source": "iana" - }, - "application/vnd.cups-ppd": { - "source": "iana", - "extensions": ["ppd"] - }, - "application/vnd.cups-raster": { - "source": "iana" - }, - "application/vnd.cups-raw": { - "source": "iana" - }, - "application/vnd.curl": { - "source": "iana" - }, - "application/vnd.curl.car": { - "source": "apache", - "extensions": ["car"] - }, - "application/vnd.curl.pcurl": { - "source": "apache", - "extensions": ["pcurl"] - }, - "application/vnd.cyan.dean.root+xml": { - "source": "iana" - }, - "application/vnd.cybank": { - "source": "iana" - }, - "application/vnd.dart": { - "source": "iana", - "compressible": true, - "extensions": ["dart"] - }, - "application/vnd.data-vision.rdz": { - "source": "iana", - "extensions": ["rdz"] - }, - "application/vnd.debian.binary-package": { - "source": "iana" - }, - "application/vnd.dece.data": { - "source": "iana", - "extensions": ["uvf","uvvf","uvd","uvvd"] - }, - "application/vnd.dece.ttml+xml": { - "source": "iana", - "extensions": ["uvt","uvvt"] - }, - "application/vnd.dece.unspecified": { - "source": "iana", - "extensions": ["uvx","uvvx"] - }, - "application/vnd.dece.zip": { - "source": "iana", - "extensions": ["uvz","uvvz"] - }, - "application/vnd.denovo.fcselayout-link": { - "source": "iana", - "extensions": ["fe_launch"] - }, - "application/vnd.desmume-movie": { - "source": "iana" - }, - "application/vnd.desmume.movie": { - "source": "apache" - }, - "application/vnd.dir-bi.plate-dl-nosuffix": { - "source": "iana" - }, - "application/vnd.dm.delegation+xml": { - "source": "iana" - }, - "application/vnd.dna": { - "source": "iana", - "extensions": ["dna"] - }, - "application/vnd.document+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.dolby.mlp": { - "source": "apache", - "extensions": ["mlp"] - }, - "application/vnd.dolby.mobile.1": { - "source": "iana" - }, - "application/vnd.dolby.mobile.2": { - "source": "iana" - }, - "application/vnd.doremir.scorecloud-binary-document": { - "source": "iana" - }, - "application/vnd.dpgraph": { - "source": "iana", - "extensions": ["dpg"] - }, - "application/vnd.dreamfactory": { - "source": "iana", - "extensions": ["dfac"] - }, - "application/vnd.drive+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.ds-keypoint": { - "source": "apache", - "extensions": ["kpxx"] - }, - "application/vnd.dtg.local": { - "source": "iana" - }, - "application/vnd.dtg.local.flash": { - "source": "iana" - }, - "application/vnd.dtg.local.html": { - "source": "iana" - }, - "application/vnd.dvb.ait": { - "source": "iana", - "extensions": ["ait"] - }, - "application/vnd.dvb.dvbj": { - "source": "iana" - }, - "application/vnd.dvb.esgcontainer": { - "source": "iana" - }, - "application/vnd.dvb.ipdcdftnotifaccess": { - "source": "iana" - }, - "application/vnd.dvb.ipdcesgaccess": { - "source": "iana" - }, - "application/vnd.dvb.ipdcesgaccess2": { - "source": "iana" - }, - "application/vnd.dvb.ipdcesgpdd": { - "source": "iana" - }, - "application/vnd.dvb.ipdcroaming": { - "source": "iana" - }, - "application/vnd.dvb.iptv.alfec-base": { - "source": "iana" - }, - "application/vnd.dvb.iptv.alfec-enhancement": { - "source": "iana" - }, - "application/vnd.dvb.notif-aggregate-root+xml": { - "source": "iana" - }, - "application/vnd.dvb.notif-container+xml": { - "source": "iana" - }, - "application/vnd.dvb.notif-generic+xml": { - "source": "iana" - }, - "application/vnd.dvb.notif-ia-msglist+xml": { - "source": "iana" - }, - "application/vnd.dvb.notif-ia-registration-request+xml": { - "source": "iana" - }, - "application/vnd.dvb.notif-ia-registration-response+xml": { - "source": "iana" - }, - "application/vnd.dvb.notif-init+xml": { - "source": "iana" - }, - "application/vnd.dvb.pfr": { - "source": "iana" - }, - "application/vnd.dvb.service": { - "source": "iana", - "extensions": ["svc"] - }, - "application/vnd.dxr": { - "source": "iana" - }, - "application/vnd.dynageo": { - "source": "iana", - "extensions": ["geo"] - }, - "application/vnd.dzr": { - "source": "iana" - }, - "application/vnd.easykaraoke.cdgdownload": { - "source": "iana" - }, - "application/vnd.ecdis-update": { - "source": "iana" - }, - "application/vnd.ecowin.chart": { - "source": "iana", - "extensions": ["mag"] - }, - "application/vnd.ecowin.filerequest": { - "source": "iana" - }, - "application/vnd.ecowin.fileupdate": { - "source": "iana" - }, - "application/vnd.ecowin.series": { - "source": "iana" - }, - "application/vnd.ecowin.seriesrequest": { - "source": "iana" - }, - "application/vnd.ecowin.seriesupdate": { - "source": "iana" - }, - "application/vnd.emclient.accessrequest+xml": { - "source": "iana" - }, - "application/vnd.enliven": { - "source": "iana", - "extensions": ["nml"] - }, - "application/vnd.enphase.envoy": { - "source": "iana" - }, - "application/vnd.eprints.data+xml": { - "source": "iana" - }, - "application/vnd.epson.esf": { - "source": "iana", - "extensions": ["esf"] - }, - "application/vnd.epson.msf": { - "source": "iana", - "extensions": ["msf"] - }, - "application/vnd.epson.quickanime": { - "source": "iana", - "extensions": ["qam"] - }, - "application/vnd.epson.salt": { - "source": "iana", - "extensions": ["slt"] - }, - "application/vnd.epson.ssf": { - "source": "iana", - "extensions": ["ssf"] - }, - "application/vnd.ericsson.quickcall": { - "source": "iana" - }, - "application/vnd.eszigno3+xml": { - "source": "iana", - "extensions": ["es3","et3"] - }, - "application/vnd.etsi.aoc+xml": { - "source": "iana" - }, - "application/vnd.etsi.asic-e+zip": { - "source": "iana" - }, - "application/vnd.etsi.asic-s+zip": { - "source": "iana" - }, - "application/vnd.etsi.cug+xml": { - "source": "iana" - }, - "application/vnd.etsi.iptvcommand+xml": { - "source": "iana" - }, - "application/vnd.etsi.iptvdiscovery+xml": { - "source": "iana" - }, - "application/vnd.etsi.iptvprofile+xml": { - "source": "iana" - }, - "application/vnd.etsi.iptvsad-bc+xml": { - "source": "iana" - }, - "application/vnd.etsi.iptvsad-cod+xml": { - "source": "iana" - }, - "application/vnd.etsi.iptvsad-npvr+xml": { - "source": "iana" - }, - "application/vnd.etsi.iptvservice+xml": { - "source": "iana" - }, - "application/vnd.etsi.iptvsync+xml": { - "source": "iana" - }, - "application/vnd.etsi.iptvueprofile+xml": { - "source": "iana" - }, - "application/vnd.etsi.mcid+xml": { - "source": "iana" - }, - "application/vnd.etsi.mheg5": { - "source": "iana" - }, - "application/vnd.etsi.overload-control-policy-dataset+xml": { - "source": "iana" - }, - "application/vnd.etsi.pstn+xml": { - "source": "iana" - }, - "application/vnd.etsi.sci+xml": { - "source": "iana" - }, - "application/vnd.etsi.simservs+xml": { - "source": "iana" - }, - "application/vnd.etsi.timestamp-token": { - "source": "iana" - }, - "application/vnd.etsi.tsl+xml": { - "source": "iana" - }, - "application/vnd.etsi.tsl.der": { - "source": "iana" - }, - "application/vnd.eudora.data": { - "source": "iana" - }, - "application/vnd.ezpix-album": { - "source": "iana", - "extensions": ["ez2"] - }, - "application/vnd.ezpix-package": { - "source": "iana", - "extensions": ["ez3"] - }, - "application/vnd.f-secure.mobile": { - "source": "iana" - }, - "application/vnd.fastcopy-disk-image": { - "source": "iana" - }, - "application/vnd.fdf": { - "source": "iana", - "extensions": ["fdf"] - }, - "application/vnd.fdsn.mseed": { - "source": "iana", - "extensions": ["mseed"] - }, - "application/vnd.fdsn.seed": { - "source": "iana", - "extensions": ["seed","dataless"] - }, - "application/vnd.ffsns": { - "source": "iana" - }, - "application/vnd.filmit.zfc": { - "source": "iana" - }, - "application/vnd.fints": { - "source": "iana" - }, - "application/vnd.firemonkeys.cloudcell": { - "source": "iana" - }, - "application/vnd.flographit": { - "source": "iana", - "extensions": ["gph"] - }, - "application/vnd.fluxtime.clip": { - "source": "iana", - "extensions": ["ftc"] - }, - "application/vnd.font-fontforge-sfd": { - "source": "iana" - }, - "application/vnd.framemaker": { - "source": "iana", - "extensions": ["fm","frame","maker","book"] - }, - "application/vnd.frogans.fnc": { - "source": "iana", - "extensions": ["fnc"] - }, - "application/vnd.frogans.ltf": { - "source": "iana", - "extensions": ["ltf"] - }, - "application/vnd.fsc.weblaunch": { - "source": "iana", - "extensions": ["fsc"] - }, - "application/vnd.fujitsu.oasys": { - "source": "iana", - "extensions": ["oas"] - }, - "application/vnd.fujitsu.oasys2": { - "source": "iana", - "extensions": ["oa2"] - }, - "application/vnd.fujitsu.oasys3": { - "source": "iana", - "extensions": ["oa3"] - }, - "application/vnd.fujitsu.oasysgp": { - "source": "iana", - "extensions": ["fg5"] - }, - "application/vnd.fujitsu.oasysprs": { - "source": "iana", - "extensions": ["bh2"] - }, - "application/vnd.fujixerox.art-ex": { - "source": "iana" - }, - "application/vnd.fujixerox.art4": { - "source": "iana" - }, - "application/vnd.fujixerox.ddd": { - "source": "iana", - "extensions": ["ddd"] - }, - "application/vnd.fujixerox.docuworks": { - "source": "iana", - "extensions": ["xdw"] - }, - "application/vnd.fujixerox.docuworks.binder": { - "source": "iana", - "extensions": ["xbd"] - }, - "application/vnd.fujixerox.docuworks.container": { - "source": "iana" - }, - "application/vnd.fujixerox.hbpl": { - "source": "iana" - }, - "application/vnd.fut-misnet": { - "source": "iana" - }, - "application/vnd.fuzzysheet": { - "source": "iana", - "extensions": ["fzs"] - }, - "application/vnd.genomatix.tuxedo": { - "source": "iana", - "extensions": ["txd"] - }, - "application/vnd.geo+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.geocube+xml": { - "source": "iana" - }, - "application/vnd.geogebra.file": { - "source": "iana", - "extensions": ["ggb"] - }, - "application/vnd.geogebra.tool": { - "source": "iana", - "extensions": ["ggt"] - }, - "application/vnd.geometry-explorer": { - "source": "iana", - "extensions": ["gex","gre"] - }, - "application/vnd.geonext": { - "source": "iana", - "extensions": ["gxt"] - }, - "application/vnd.geoplan": { - "source": "iana", - "extensions": ["g2w"] - }, - "application/vnd.geospace": { - "source": "iana", - "extensions": ["g3w"] - }, - "application/vnd.gerber": { - "source": "iana" - }, - "application/vnd.globalplatform.card-content-mgt": { - "source": "iana" - }, - "application/vnd.globalplatform.card-content-mgt-response": { - "source": "iana" - }, - "application/vnd.gmx": { - "source": "iana", - "extensions": ["gmx"] - }, - "application/vnd.google-apps.document": { - "compressible": false, - "extensions": ["gdoc"] - }, - "application/vnd.google-apps.presentation": { - "compressible": false, - "extensions": ["gslides"] - }, - "application/vnd.google-apps.spreadsheet": { - "compressible": false, - "extensions": ["gsheet"] - }, - "application/vnd.google-earth.kml+xml": { - "source": "iana", - "compressible": true, - "extensions": ["kml"] - }, - "application/vnd.google-earth.kmz": { - "source": "iana", - "compressible": false, - "extensions": ["kmz"] - }, - "application/vnd.gov.sk.e-form+xml": { - "source": "iana" - }, - "application/vnd.gov.sk.e-form+zip": { - "source": "iana" - }, - "application/vnd.gov.sk.xmldatacontainer+xml": { - "source": "iana" - }, - "application/vnd.grafeq": { - "source": "iana", - "extensions": ["gqf","gqs"] - }, - "application/vnd.gridmp": { - "source": "iana" - }, - "application/vnd.groove-account": { - "source": "iana", - "extensions": ["gac"] - }, - "application/vnd.groove-help": { - "source": "iana", - "extensions": ["ghf"] - }, - "application/vnd.groove-identity-message": { - "source": "iana", - "extensions": ["gim"] - }, - "application/vnd.groove-injector": { - "source": "iana", - "extensions": ["grv"] - }, - "application/vnd.groove-tool-message": { - "source": "iana", - "extensions": ["gtm"] - }, - "application/vnd.groove-tool-template": { - "source": "iana", - "extensions": ["tpl"] - }, - "application/vnd.groove-vcard": { - "source": "iana", - "extensions": ["vcg"] - }, - "application/vnd.hal+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.hal+xml": { - "source": "iana", - "extensions": ["hal"] - }, - "application/vnd.handheld-entertainment+xml": { - "source": "iana", - "extensions": ["zmm"] - }, - "application/vnd.hbci": { - "source": "iana", - "extensions": ["hbci"] - }, - "application/vnd.hcl-bireports": { - "source": "iana" - }, - "application/vnd.hdt": { - "source": "iana" - }, - "application/vnd.heroku+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.hhe.lesson-player": { - "source": "iana", - "extensions": ["les"] - }, - "application/vnd.hp-hpgl": { - "source": "iana", - "extensions": ["hpgl"] - }, - "application/vnd.hp-hpid": { - "source": "iana", - "extensions": ["hpid"] - }, - "application/vnd.hp-hps": { - "source": "iana", - "extensions": ["hps"] - }, - "application/vnd.hp-jlyt": { - "source": "iana", - "extensions": ["jlt"] - }, - "application/vnd.hp-pcl": { - "source": "iana", - "extensions": ["pcl"] - }, - "application/vnd.hp-pclxl": { - "source": "iana", - "extensions": ["pclxl"] - }, - "application/vnd.httphone": { - "source": "iana" - }, - "application/vnd.hydrostatix.sof-data": { - "source": "iana", - "extensions": ["sfd-hdstx"] - }, - "application/vnd.hyperdrive+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.hzn-3d-crossword": { - "source": "iana" - }, - "application/vnd.ibm.afplinedata": { - "source": "iana" - }, - "application/vnd.ibm.electronic-media": { - "source": "iana" - }, - "application/vnd.ibm.minipay": { - "source": "iana", - "extensions": ["mpy"] - }, - "application/vnd.ibm.modcap": { - "source": "iana", - "extensions": ["afp","listafp","list3820"] - }, - "application/vnd.ibm.rights-management": { - "source": "iana", - "extensions": ["irm"] - }, - "application/vnd.ibm.secure-container": { - "source": "iana", - "extensions": ["sc"] - }, - "application/vnd.iccprofile": { - "source": "iana", - "extensions": ["icc","icm"] - }, - "application/vnd.ieee.1905": { - "source": "iana" - }, - "application/vnd.igloader": { - "source": "iana", - "extensions": ["igl"] - }, - "application/vnd.immervision-ivp": { - "source": "iana", - "extensions": ["ivp"] - }, - "application/vnd.immervision-ivu": { - "source": "iana", - "extensions": ["ivu"] - }, - "application/vnd.ims.imsccv1p1": { - "source": "iana" - }, - "application/vnd.ims.imsccv1p2": { - "source": "iana" - }, - "application/vnd.ims.imsccv1p3": { - "source": "iana" - }, - "application/vnd.ims.lis.v2.result+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.ims.lti.v2.toolconsumerprofile+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.ims.lti.v2.toolproxy+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.ims.lti.v2.toolproxy.id+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.ims.lti.v2.toolsettings+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.ims.lti.v2.toolsettings.simple+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.informedcontrol.rms+xml": { - "source": "iana" - }, - "application/vnd.informix-visionary": { - "source": "iana" - }, - "application/vnd.infotech.project": { - "source": "iana" - }, - "application/vnd.infotech.project+xml": { - "source": "iana" - }, - "application/vnd.innopath.wamp.notification": { - "source": "iana" - }, - "application/vnd.insors.igm": { - "source": "iana", - "extensions": ["igm"] - }, - "application/vnd.intercon.formnet": { - "source": "iana", - "extensions": ["xpw","xpx"] - }, - "application/vnd.intergeo": { - "source": "iana", - "extensions": ["i2g"] - }, - "application/vnd.intertrust.digibox": { - "source": "iana" - }, - "application/vnd.intertrust.nncp": { - "source": "iana" - }, - "application/vnd.intu.qbo": { - "source": "iana", - "extensions": ["qbo"] - }, - "application/vnd.intu.qfx": { - "source": "iana", - "extensions": ["qfx"] - }, - "application/vnd.iptc.g2.catalogitem+xml": { - "source": "iana" - }, - "application/vnd.iptc.g2.conceptitem+xml": { - "source": "iana" - }, - "application/vnd.iptc.g2.knowledgeitem+xml": { - "source": "iana" - }, - "application/vnd.iptc.g2.newsitem+xml": { - "source": "iana" - }, - "application/vnd.iptc.g2.newsmessage+xml": { - "source": "iana" - }, - "application/vnd.iptc.g2.packageitem+xml": { - "source": "iana" - }, - "application/vnd.iptc.g2.planningitem+xml": { - "source": "iana" - }, - "application/vnd.ipunplugged.rcprofile": { - "source": "iana", - "extensions": ["rcprofile"] - }, - "application/vnd.irepository.package+xml": { - "source": "iana", - "extensions": ["irp"] - }, - "application/vnd.is-xpr": { - "source": "iana", - "extensions": ["xpr"] - }, - "application/vnd.isac.fcs": { - "source": "iana", - "extensions": ["fcs"] - }, - "application/vnd.jam": { - "source": "iana", - "extensions": ["jam"] - }, - "application/vnd.japannet-directory-service": { - "source": "iana" - }, - "application/vnd.japannet-jpnstore-wakeup": { - "source": "iana" - }, - "application/vnd.japannet-payment-wakeup": { - "source": "iana" - }, - "application/vnd.japannet-registration": { - "source": "iana" - }, - "application/vnd.japannet-registration-wakeup": { - "source": "iana" - }, - "application/vnd.japannet-setstore-wakeup": { - "source": "iana" - }, - "application/vnd.japannet-verification": { - "source": "iana" - }, - "application/vnd.japannet-verification-wakeup": { - "source": "iana" - }, - "application/vnd.jcp.javame.midlet-rms": { - "source": "iana", - "extensions": ["rms"] - }, - "application/vnd.jisp": { - "source": "iana", - "extensions": ["jisp"] - }, - "application/vnd.joost.joda-archive": { - "source": "iana", - "extensions": ["joda"] - }, - "application/vnd.jsk.isdn-ngn": { - "source": "iana" - }, - "application/vnd.kahootz": { - "source": "iana", - "extensions": ["ktz","ktr"] - }, - "application/vnd.kde.karbon": { - "source": "iana", - "extensions": ["karbon"] - }, - "application/vnd.kde.kchart": { - "source": "iana", - "extensions": ["chrt"] - }, - "application/vnd.kde.kformula": { - "source": "iana", - "extensions": ["kfo"] - }, - "application/vnd.kde.kivio": { - "source": "iana", - "extensions": ["flw"] - }, - "application/vnd.kde.kontour": { - "source": "iana", - "extensions": ["kon"] - }, - "application/vnd.kde.kpresenter": { - "source": "iana", - "extensions": ["kpr","kpt"] - }, - "application/vnd.kde.kspread": { - "source": "iana", - "extensions": ["ksp"] - }, - "application/vnd.kde.kword": { - "source": "iana", - "extensions": ["kwd","kwt"] - }, - "application/vnd.kenameaapp": { - "source": "iana", - "extensions": ["htke"] - }, - "application/vnd.kidspiration": { - "source": "iana", - "extensions": ["kia"] - }, - "application/vnd.kinar": { - "source": "iana", - "extensions": ["kne","knp"] - }, - "application/vnd.koan": { - "source": "iana", - "extensions": ["skp","skd","skt","skm"] - }, - "application/vnd.kodak-descriptor": { - "source": "iana", - "extensions": ["sse"] - }, - "application/vnd.las.las+xml": { - "source": "iana", - "extensions": ["lasxml"] - }, - "application/vnd.liberty-request+xml": { - "source": "iana" - }, - "application/vnd.llamagraphics.life-balance.desktop": { - "source": "iana", - "extensions": ["lbd"] - }, - "application/vnd.llamagraphics.life-balance.exchange+xml": { - "source": "iana", - "extensions": ["lbe"] - }, - "application/vnd.lotus-1-2-3": { - "source": "iana", - "extensions": ["123"] - }, - "application/vnd.lotus-approach": { - "source": "iana", - "extensions": ["apr"] - }, - "application/vnd.lotus-freelance": { - "source": "iana", - "extensions": ["pre"] - }, - "application/vnd.lotus-notes": { - "source": "iana", - "extensions": ["nsf"] - }, - "application/vnd.lotus-organizer": { - "source": "iana", - "extensions": ["org"] - }, - "application/vnd.lotus-screencam": { - "source": "iana", - "extensions": ["scm"] - }, - "application/vnd.lotus-wordpro": { - "source": "iana", - "extensions": ["lwp"] - }, - "application/vnd.macports.portpkg": { - "source": "iana", - "extensions": ["portpkg"] - }, - "application/vnd.mapbox-vector-tile": { - "source": "iana" - }, - "application/vnd.marlin.drm.actiontoken+xml": { - "source": "iana" - }, - "application/vnd.marlin.drm.conftoken+xml": { - "source": "iana" - }, - "application/vnd.marlin.drm.license+xml": { - "source": "iana" - }, - "application/vnd.marlin.drm.mdcf": { - "source": "iana" - }, - "application/vnd.mason+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.maxmind.maxmind-db": { - "source": "iana" - }, - "application/vnd.mcd": { - "source": "iana", - "extensions": ["mcd"] - }, - "application/vnd.medcalcdata": { - "source": "iana", - "extensions": ["mc1"] - }, - "application/vnd.mediastation.cdkey": { - "source": "iana", - "extensions": ["cdkey"] - }, - "application/vnd.meridian-slingshot": { - "source": "iana" - }, - "application/vnd.mfer": { - "source": "iana", - "extensions": ["mwf"] - }, - "application/vnd.mfmp": { - "source": "iana", - "extensions": ["mfm"] - }, - "application/vnd.micro+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.micrografx.flo": { - "source": "iana", - "extensions": ["flo"] - }, - "application/vnd.micrografx.igx": { - "source": "iana", - "extensions": ["igx"] - }, - "application/vnd.microsoft.portable-executable": { - "source": "iana" - }, - "application/vnd.miele+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.mif": { - "source": "iana", - "extensions": ["mif"] - }, - "application/vnd.minisoft-hp3000-save": { - "source": "iana" - }, - "application/vnd.mitsubishi.misty-guard.trustweb": { - "source": "iana" - }, - "application/vnd.mobius.daf": { - "source": "iana", - "extensions": ["daf"] - }, - "application/vnd.mobius.dis": { - "source": "iana", - "extensions": ["dis"] - }, - "application/vnd.mobius.mbk": { - "source": "iana", - "extensions": ["mbk"] - }, - "application/vnd.mobius.mqy": { - "source": "iana", - "extensions": ["mqy"] - }, - "application/vnd.mobius.msl": { - "source": "iana", - "extensions": ["msl"] - }, - "application/vnd.mobius.plc": { - "source": "iana", - "extensions": ["plc"] - }, - "application/vnd.mobius.txf": { - "source": "iana", - "extensions": ["txf"] - }, - "application/vnd.mophun.application": { - "source": "iana", - "extensions": ["mpn"] - }, - "application/vnd.mophun.certificate": { - "source": "iana", - "extensions": ["mpc"] - }, - "application/vnd.motorola.flexsuite": { - "source": "iana" - }, - "application/vnd.motorola.flexsuite.adsi": { - "source": "iana" - }, - "application/vnd.motorola.flexsuite.fis": { - "source": "iana" - }, - "application/vnd.motorola.flexsuite.gotap": { - "source": "iana" - }, - "application/vnd.motorola.flexsuite.kmr": { - "source": "iana" - }, - "application/vnd.motorola.flexsuite.ttc": { - "source": "iana" - }, - "application/vnd.motorola.flexsuite.wem": { - "source": "iana" - }, - "application/vnd.motorola.iprm": { - "source": "iana" - }, - "application/vnd.mozilla.xul+xml": { - "source": "iana", - "compressible": true, - "extensions": ["xul"] - }, - "application/vnd.ms-3mfdocument": { - "source": "iana" - }, - "application/vnd.ms-artgalry": { - "source": "iana", - "extensions": ["cil"] - }, - "application/vnd.ms-asf": { - "source": "iana" - }, - "application/vnd.ms-cab-compressed": { - "source": "iana", - "extensions": ["cab"] - }, - "application/vnd.ms-color.iccprofile": { - "source": "apache" - }, - "application/vnd.ms-excel": { - "source": "iana", - "compressible": false, - "extensions": ["xls","xlm","xla","xlc","xlt","xlw"] - }, - "application/vnd.ms-excel.addin.macroenabled.12": { - "source": "iana", - "extensions": ["xlam"] - }, - "application/vnd.ms-excel.sheet.binary.macroenabled.12": { - "source": "iana", - "extensions": ["xlsb"] - }, - "application/vnd.ms-excel.sheet.macroenabled.12": { - "source": "iana", - "extensions": ["xlsm"] - }, - "application/vnd.ms-excel.template.macroenabled.12": { - "source": "iana", - "extensions": ["xltm"] - }, - "application/vnd.ms-fontobject": { - "source": "iana", - "compressible": true, - "extensions": ["eot"] - }, - "application/vnd.ms-htmlhelp": { - "source": "iana", - "extensions": ["chm"] - }, - "application/vnd.ms-ims": { - "source": "iana", - "extensions": ["ims"] - }, - "application/vnd.ms-lrm": { - "source": "iana", - "extensions": ["lrm"] - }, - "application/vnd.ms-office.activex+xml": { - "source": "iana" - }, - "application/vnd.ms-officetheme": { - "source": "iana", - "extensions": ["thmx"] - }, - "application/vnd.ms-opentype": { - "source": "apache", - "compressible": true - }, - "application/vnd.ms-package.obfuscated-opentype": { - "source": "apache" - }, - "application/vnd.ms-pki.seccat": { - "source": "apache", - "extensions": ["cat"] - }, - "application/vnd.ms-pki.stl": { - "source": "apache", - "extensions": ["stl"] - }, - "application/vnd.ms-playready.initiator+xml": { - "source": "iana" - }, - "application/vnd.ms-powerpoint": { - "source": "iana", - "compressible": false, - "extensions": ["ppt","pps","pot"] - }, - "application/vnd.ms-powerpoint.addin.macroenabled.12": { - "source": "iana", - "extensions": ["ppam"] - }, - "application/vnd.ms-powerpoint.presentation.macroenabled.12": { - "source": "iana", - "extensions": ["pptm"] - }, - "application/vnd.ms-powerpoint.slide.macroenabled.12": { - "source": "iana", - "extensions": ["sldm"] - }, - "application/vnd.ms-powerpoint.slideshow.macroenabled.12": { - "source": "iana", - "extensions": ["ppsm"] - }, - "application/vnd.ms-powerpoint.template.macroenabled.12": { - "source": "iana", - "extensions": ["potm"] - }, - "application/vnd.ms-printdevicecapabilities+xml": { - "source": "iana" - }, - "application/vnd.ms-printing.printticket+xml": { - "source": "apache" - }, - "application/vnd.ms-printschematicket+xml": { - "source": "iana" - }, - "application/vnd.ms-project": { - "source": "iana", - "extensions": ["mpp","mpt"] - }, - "application/vnd.ms-tnef": { - "source": "iana" - }, - "application/vnd.ms-windows.devicepairing": { - "source": "iana" - }, - "application/vnd.ms-windows.nwprinting.oob": { - "source": "iana" - }, - "application/vnd.ms-windows.printerpairing": { - "source": "iana" - }, - "application/vnd.ms-windows.wsd.oob": { - "source": "iana" - }, - "application/vnd.ms-wmdrm.lic-chlg-req": { - "source": "iana" - }, - "application/vnd.ms-wmdrm.lic-resp": { - "source": "iana" - }, - "application/vnd.ms-wmdrm.meter-chlg-req": { - "source": "iana" - }, - "application/vnd.ms-wmdrm.meter-resp": { - "source": "iana" - }, - "application/vnd.ms-word.document.macroenabled.12": { - "source": "iana", - "extensions": ["docm"] - }, - "application/vnd.ms-word.template.macroenabled.12": { - "source": "iana", - "extensions": ["dotm"] - }, - "application/vnd.ms-works": { - "source": "iana", - "extensions": ["wps","wks","wcm","wdb"] - }, - "application/vnd.ms-wpl": { - "source": "iana", - "extensions": ["wpl"] - }, - "application/vnd.ms-xpsdocument": { - "source": "iana", - "compressible": false, - "extensions": ["xps"] - }, - "application/vnd.msa-disk-image": { - "source": "iana" - }, - "application/vnd.mseq": { - "source": "iana", - "extensions": ["mseq"] - }, - "application/vnd.msign": { - "source": "iana" - }, - "application/vnd.multiad.creator": { - "source": "iana" - }, - "application/vnd.multiad.creator.cif": { - "source": "iana" - }, - "application/vnd.music-niff": { - "source": "iana" - }, - "application/vnd.musician": { - "source": "iana", - "extensions": ["mus"] - }, - "application/vnd.muvee.style": { - "source": "iana", - "extensions": ["msty"] - }, - "application/vnd.mynfc": { - "source": "iana", - "extensions": ["taglet"] - }, - "application/vnd.ncd.control": { - "source": "iana" - }, - "application/vnd.ncd.reference": { - "source": "iana" - }, - "application/vnd.nervana": { - "source": "iana" - }, - "application/vnd.netfpx": { - "source": "iana" - }, - "application/vnd.neurolanguage.nlu": { - "source": "iana", - "extensions": ["nlu"] - }, - "application/vnd.nintendo.nitro.rom": { - "source": "iana" - }, - "application/vnd.nintendo.snes.rom": { - "source": "iana" - }, - "application/vnd.nitf": { - "source": "iana", - "extensions": ["ntf","nitf"] - }, - "application/vnd.noblenet-directory": { - "source": "iana", - "extensions": ["nnd"] - }, - "application/vnd.noblenet-sealer": { - "source": "iana", - "extensions": ["nns"] - }, - "application/vnd.noblenet-web": { - "source": "iana", - "extensions": ["nnw"] - }, - "application/vnd.nokia.catalogs": { - "source": "iana" - }, - "application/vnd.nokia.conml+wbxml": { - "source": "iana" - }, - "application/vnd.nokia.conml+xml": { - "source": "iana" - }, - "application/vnd.nokia.iptv.config+xml": { - "source": "iana" - }, - "application/vnd.nokia.isds-radio-presets": { - "source": "iana" - }, - "application/vnd.nokia.landmark+wbxml": { - "source": "iana" - }, - "application/vnd.nokia.landmark+xml": { - "source": "iana" - }, - "application/vnd.nokia.landmarkcollection+xml": { - "source": "iana" - }, - "application/vnd.nokia.n-gage.ac+xml": { - "source": "iana" - }, - "application/vnd.nokia.n-gage.data": { - "source": "iana", - "extensions": ["ngdat"] - }, - "application/vnd.nokia.n-gage.symbian.install": { - "source": "iana", - "extensions": ["n-gage"] - }, - "application/vnd.nokia.ncd": { - "source": "iana" - }, - "application/vnd.nokia.pcd+wbxml": { - "source": "iana" - }, - "application/vnd.nokia.pcd+xml": { - "source": "iana" - }, - "application/vnd.nokia.radio-preset": { - "source": "iana", - "extensions": ["rpst"] - }, - "application/vnd.nokia.radio-presets": { - "source": "iana", - "extensions": ["rpss"] - }, - "application/vnd.novadigm.edm": { - "source": "iana", - "extensions": ["edm"] - }, - "application/vnd.novadigm.edx": { - "source": "iana", - "extensions": ["edx"] - }, - "application/vnd.novadigm.ext": { - "source": "iana", - "extensions": ["ext"] - }, - "application/vnd.ntt-local.content-share": { - "source": "iana" - }, - "application/vnd.ntt-local.file-transfer": { - "source": "iana" - }, - "application/vnd.ntt-local.ogw_remote-access": { - "source": "iana" - }, - "application/vnd.ntt-local.sip-ta_remote": { - "source": "iana" - }, - "application/vnd.ntt-local.sip-ta_tcp_stream": { - "source": "iana" - }, - "application/vnd.oasis.opendocument.chart": { - "source": "iana", - "extensions": ["odc"] - }, - "application/vnd.oasis.opendocument.chart-template": { - "source": "iana", - "extensions": ["otc"] - }, - "application/vnd.oasis.opendocument.database": { - "source": "iana", - "extensions": ["odb"] - }, - "application/vnd.oasis.opendocument.formula": { - "source": "iana", - "extensions": ["odf"] - }, - "application/vnd.oasis.opendocument.formula-template": { - "source": "iana", - "extensions": ["odft"] - }, - "application/vnd.oasis.opendocument.graphics": { - "source": "iana", - "compressible": false, - "extensions": ["odg"] - }, - "application/vnd.oasis.opendocument.graphics-template": { - "source": "iana", - "extensions": ["otg"] - }, - "application/vnd.oasis.opendocument.image": { - "source": "iana", - "extensions": ["odi"] - }, - "application/vnd.oasis.opendocument.image-template": { - "source": "iana", - "extensions": ["oti"] - }, - "application/vnd.oasis.opendocument.presentation": { - "source": "iana", - "compressible": false, - "extensions": ["odp"] - }, - "application/vnd.oasis.opendocument.presentation-template": { - "source": "iana", - "extensions": ["otp"] - }, - "application/vnd.oasis.opendocument.spreadsheet": { - "source": "iana", - "compressible": false, - "extensions": ["ods"] - }, - "application/vnd.oasis.opendocument.spreadsheet-template": { - "source": "iana", - "extensions": ["ots"] - }, - "application/vnd.oasis.opendocument.text": { - "source": "iana", - "compressible": false, - "extensions": ["odt"] - }, - "application/vnd.oasis.opendocument.text-master": { - "source": "iana", - "extensions": ["odm"] - }, - "application/vnd.oasis.opendocument.text-template": { - "source": "iana", - "extensions": ["ott"] - }, - "application/vnd.oasis.opendocument.text-web": { - "source": "iana", - "extensions": ["oth"] - }, - "application/vnd.obn": { - "source": "iana" - }, - "application/vnd.oftn.l10n+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.oipf.contentaccessdownload+xml": { - "source": "iana" - }, - "application/vnd.oipf.contentaccessstreaming+xml": { - "source": "iana" - }, - "application/vnd.oipf.cspg-hexbinary": { - "source": "iana" - }, - "application/vnd.oipf.dae.svg+xml": { - "source": "iana" - }, - "application/vnd.oipf.dae.xhtml+xml": { - "source": "iana" - }, - "application/vnd.oipf.mippvcontrolmessage+xml": { - "source": "iana" - }, - "application/vnd.oipf.pae.gem": { - "source": "iana" - }, - "application/vnd.oipf.spdiscovery+xml": { - "source": "iana" - }, - "application/vnd.oipf.spdlist+xml": { - "source": "iana" - }, - "application/vnd.oipf.ueprofile+xml": { - "source": "iana" - }, - "application/vnd.oipf.userprofile+xml": { - "source": "iana" - }, - "application/vnd.olpc-sugar": { - "source": "iana", - "extensions": ["xo"] - }, - "application/vnd.oma-scws-config": { - "source": "iana" - }, - "application/vnd.oma-scws-http-request": { - "source": "iana" - }, - "application/vnd.oma-scws-http-response": { - "source": "iana" - }, - "application/vnd.oma.bcast.associated-procedure-parameter+xml": { - "source": "iana" - }, - "application/vnd.oma.bcast.drm-trigger+xml": { - "source": "iana" - }, - "application/vnd.oma.bcast.imd+xml": { - "source": "iana" - }, - "application/vnd.oma.bcast.ltkm": { - "source": "iana" - }, - "application/vnd.oma.bcast.notification+xml": { - "source": "iana" - }, - "application/vnd.oma.bcast.provisioningtrigger": { - "source": "iana" - }, - "application/vnd.oma.bcast.sgboot": { - "source": "iana" - }, - "application/vnd.oma.bcast.sgdd+xml": { - "source": "iana" - }, - "application/vnd.oma.bcast.sgdu": { - "source": "iana" - }, - "application/vnd.oma.bcast.simple-symbol-container": { - "source": "iana" - }, - "application/vnd.oma.bcast.smartcard-trigger+xml": { - "source": "iana" - }, - "application/vnd.oma.bcast.sprov+xml": { - "source": "iana" - }, - "application/vnd.oma.bcast.stkm": { - "source": "iana" - }, - "application/vnd.oma.cab-address-book+xml": { - "source": "iana" - }, - "application/vnd.oma.cab-feature-handler+xml": { - "source": "iana" - }, - "application/vnd.oma.cab-pcc+xml": { - "source": "iana" - }, - "application/vnd.oma.cab-subs-invite+xml": { - "source": "iana" - }, - "application/vnd.oma.cab-user-prefs+xml": { - "source": "iana" - }, - "application/vnd.oma.dcd": { - "source": "iana" - }, - "application/vnd.oma.dcdc": { - "source": "iana" - }, - "application/vnd.oma.dd2+xml": { - "source": "iana", - "extensions": ["dd2"] - }, - "application/vnd.oma.drm.risd+xml": { - "source": "iana" - }, - "application/vnd.oma.group-usage-list+xml": { - "source": "iana" - }, - "application/vnd.oma.pal+xml": { - "source": "iana" - }, - "application/vnd.oma.poc.detailed-progress-report+xml": { - "source": "iana" - }, - "application/vnd.oma.poc.final-report+xml": { - "source": "iana" - }, - "application/vnd.oma.poc.groups+xml": { - "source": "iana" - }, - "application/vnd.oma.poc.invocation-descriptor+xml": { - "source": "iana" - }, - "application/vnd.oma.poc.optimized-progress-report+xml": { - "source": "iana" - }, - "application/vnd.oma.push": { - "source": "iana" - }, - "application/vnd.oma.scidm.messages+xml": { - "source": "iana" - }, - "application/vnd.oma.xcap-directory+xml": { - "source": "iana" - }, - "application/vnd.omads-email+xml": { - "source": "iana" - }, - "application/vnd.omads-file+xml": { - "source": "iana" - }, - "application/vnd.omads-folder+xml": { - "source": "iana" - }, - "application/vnd.omaloc-supl-init": { - "source": "iana" - }, - "application/vnd.onepager": { - "source": "iana" - }, - "application/vnd.openblox.game+xml": { - "source": "iana" - }, - "application/vnd.openblox.game-binary": { - "source": "iana" - }, - "application/vnd.openeye.oeb": { - "source": "iana" - }, - "application/vnd.openofficeorg.extension": { - "source": "apache", - "extensions": ["oxt"] - }, - "application/vnd.openxmlformats-officedocument.custom-properties+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.customxmlproperties+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.drawing+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.drawingml.chart+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.extended-properties+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml-template": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.comments+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.presentation": { - "source": "iana", - "compressible": false, - "extensions": ["pptx"] - }, - "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.presprops+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.slide": { - "source": "iana", - "extensions": ["sldx"] - }, - "application/vnd.openxmlformats-officedocument.presentationml.slide+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.slideshow": { - "source": "iana", - "extensions": ["ppsx"] - }, - "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.tags+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.template": { - "source": "apache", - "extensions": ["potx"] - }, - "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml-template": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": { - "source": "iana", - "compressible": false, - "extensions": ["xlsx"] - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.template": { - "source": "apache", - "extensions": ["xltx"] - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.theme+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.themeoverride+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.vmldrawing": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml-template": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": { - "source": "iana", - "compressible": false, - "extensions": ["docx"] - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.template": { - "source": "apache", - "extensions": ["dotx"] - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-package.core-properties+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml": { - "source": "iana" - }, - "application/vnd.openxmlformats-package.relationships+xml": { - "source": "iana" - }, - "application/vnd.oracle.resource+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.orange.indata": { - "source": "iana" - }, - "application/vnd.osa.netdeploy": { - "source": "iana" - }, - "application/vnd.osgeo.mapguide.package": { - "source": "iana", - "extensions": ["mgp"] - }, - "application/vnd.osgi.bundle": { - "source": "iana" - }, - "application/vnd.osgi.dp": { - "source": "iana", - "extensions": ["dp"] - }, - "application/vnd.osgi.subsystem": { - "source": "iana", - "extensions": ["esa"] - }, - "application/vnd.otps.ct-kip+xml": { - "source": "iana" - }, - "application/vnd.oxli.countgraph": { - "source": "iana" - }, - "application/vnd.pagerduty+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.palm": { - "source": "iana", - "extensions": ["pdb","pqa","oprc"] - }, - "application/vnd.panoply": { - "source": "iana" - }, - "application/vnd.paos+xml": { - "source": "iana" - }, - "application/vnd.paos.xml": { - "source": "apache" - }, - "application/vnd.pawaafile": { - "source": "iana", - "extensions": ["paw"] - }, - "application/vnd.pcos": { - "source": "iana" - }, - "application/vnd.pg.format": { - "source": "iana", - "extensions": ["str"] - }, - "application/vnd.pg.osasli": { - "source": "iana", - "extensions": ["ei6"] - }, - "application/vnd.piaccess.application-licence": { - "source": "iana" - }, - "application/vnd.picsel": { - "source": "iana", - "extensions": ["efif"] - }, - "application/vnd.pmi.widget": { - "source": "iana", - "extensions": ["wg"] - }, - "application/vnd.poc.group-advertisement+xml": { - "source": "iana" - }, - "application/vnd.pocketlearn": { - "source": "iana", - "extensions": ["plf"] - }, - "application/vnd.powerbuilder6": { - "source": "iana", - "extensions": ["pbd"] - }, - "application/vnd.powerbuilder6-s": { - "source": "iana" - }, - "application/vnd.powerbuilder7": { - "source": "iana" - }, - "application/vnd.powerbuilder7-s": { - "source": "iana" - }, - "application/vnd.powerbuilder75": { - "source": "iana" - }, - "application/vnd.powerbuilder75-s": { - "source": "iana" - }, - "application/vnd.preminet": { - "source": "iana" - }, - "application/vnd.previewsystems.box": { - "source": "iana", - "extensions": ["box"] - }, - "application/vnd.proteus.magazine": { - "source": "iana", - "extensions": ["mgz"] - }, - "application/vnd.publishare-delta-tree": { - "source": "iana", - "extensions": ["qps"] - }, - "application/vnd.pvi.ptid1": { - "source": "iana", - "extensions": ["ptid"] - }, - "application/vnd.pwg-multiplexed": { - "source": "iana" - }, - "application/vnd.pwg-xhtml-print+xml": { - "source": "iana" - }, - "application/vnd.qualcomm.brew-app-res": { - "source": "iana" - }, - "application/vnd.quark.quarkxpress": { - "source": "iana", - "extensions": ["qxd","qxt","qwd","qwt","qxl","qxb"] - }, - "application/vnd.quobject-quoxdocument": { - "source": "iana" - }, - "application/vnd.radisys.moml+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-audit+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-audit-conf+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-audit-conn+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-audit-dialog+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-audit-stream+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-conf+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-dialog+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-dialog-base+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-dialog-fax-detect+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-dialog-fax-sendrecv+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-dialog-group+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-dialog-speech+xml": { - "source": "iana" - }, - "application/vnd.radisys.msml-dialog-transform+xml": { - "source": "iana" - }, - "application/vnd.rainstor.data": { - "source": "iana" - }, - "application/vnd.rapid": { - "source": "iana" - }, - "application/vnd.realvnc.bed": { - "source": "iana", - "extensions": ["bed"] - }, - "application/vnd.recordare.musicxml": { - "source": "iana", - "extensions": ["mxl"] - }, - "application/vnd.recordare.musicxml+xml": { - "source": "iana", - "extensions": ["musicxml"] - }, - "application/vnd.renlearn.rlprint": { - "source": "iana" - }, - "application/vnd.rig.cryptonote": { - "source": "iana", - "extensions": ["cryptonote"] - }, - "application/vnd.rim.cod": { - "source": "apache", - "extensions": ["cod"] - }, - "application/vnd.rn-realmedia": { - "source": "apache", - "extensions": ["rm"] - }, - "application/vnd.rn-realmedia-vbr": { - "source": "apache", - "extensions": ["rmvb"] - }, - "application/vnd.route66.link66+xml": { - "source": "iana", - "extensions": ["link66"] - }, - "application/vnd.rs-274x": { - "source": "iana" - }, - "application/vnd.ruckus.download": { - "source": "iana" - }, - "application/vnd.s3sms": { - "source": "iana" - }, - "application/vnd.sailingtracker.track": { - "source": "iana", - "extensions": ["st"] - }, - "application/vnd.sbm.cid": { - "source": "iana" - }, - "application/vnd.sbm.mid2": { - "source": "iana" - }, - "application/vnd.scribus": { - "source": "iana" - }, - "application/vnd.sealed.3df": { - "source": "iana" - }, - "application/vnd.sealed.csf": { - "source": "iana" - }, - "application/vnd.sealed.doc": { - "source": "iana" - }, - "application/vnd.sealed.eml": { - "source": "iana" - }, - "application/vnd.sealed.mht": { - "source": "iana" - }, - "application/vnd.sealed.net": { - "source": "iana" - }, - "application/vnd.sealed.ppt": { - "source": "iana" - }, - "application/vnd.sealed.tiff": { - "source": "iana" - }, - "application/vnd.sealed.xls": { - "source": "iana" - }, - "application/vnd.sealedmedia.softseal.html": { - "source": "iana" - }, - "application/vnd.sealedmedia.softseal.pdf": { - "source": "iana" - }, - "application/vnd.seemail": { - "source": "iana", - "extensions": ["see"] - }, - "application/vnd.sema": { - "source": "iana", - "extensions": ["sema"] - }, - "application/vnd.semd": { - "source": "iana", - "extensions": ["semd"] - }, - "application/vnd.semf": { - "source": "iana", - "extensions": ["semf"] - }, - "application/vnd.shana.informed.formdata": { - "source": "iana", - "extensions": ["ifm"] - }, - "application/vnd.shana.informed.formtemplate": { - "source": "iana", - "extensions": ["itp"] - }, - "application/vnd.shana.informed.interchange": { - "source": "iana", - "extensions": ["iif"] - }, - "application/vnd.shana.informed.package": { - "source": "iana", - "extensions": ["ipk"] - }, - "application/vnd.simtech-mindmapper": { - "source": "iana", - "extensions": ["twd","twds"] - }, - "application/vnd.siren+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.smaf": { - "source": "iana", - "extensions": ["mmf"] - }, - "application/vnd.smart.notebook": { - "source": "iana" - }, - "application/vnd.smart.teacher": { - "source": "iana", - "extensions": ["teacher"] - }, - "application/vnd.software602.filler.form+xml": { - "source": "iana" - }, - "application/vnd.software602.filler.form-xml-zip": { - "source": "iana" - }, - "application/vnd.solent.sdkm+xml": { - "source": "iana", - "extensions": ["sdkm","sdkd"] - }, - "application/vnd.spotfire.dxp": { - "source": "iana", - "extensions": ["dxp"] - }, - "application/vnd.spotfire.sfs": { - "source": "iana", - "extensions": ["sfs"] - }, - "application/vnd.sss-cod": { - "source": "iana" - }, - "application/vnd.sss-dtf": { - "source": "iana" - }, - "application/vnd.sss-ntf": { - "source": "iana" - }, - "application/vnd.stardivision.calc": { - "source": "apache", - "extensions": ["sdc"] - }, - "application/vnd.stardivision.draw": { - "source": "apache", - "extensions": ["sda"] - }, - "application/vnd.stardivision.impress": { - "source": "apache", - "extensions": ["sdd"] - }, - "application/vnd.stardivision.math": { - "source": "apache", - "extensions": ["smf"] - }, - "application/vnd.stardivision.writer": { - "source": "apache", - "extensions": ["sdw","vor"] - }, - "application/vnd.stardivision.writer-global": { - "source": "apache", - "extensions": ["sgl"] - }, - "application/vnd.stepmania.package": { - "source": "iana", - "extensions": ["smzip"] - }, - "application/vnd.stepmania.stepchart": { - "source": "iana", - "extensions": ["sm"] - }, - "application/vnd.street-stream": { - "source": "iana" - }, - "application/vnd.sun.wadl+xml": { - "source": "iana" - }, - "application/vnd.sun.xml.calc": { - "source": "apache", - "extensions": ["sxc"] - }, - "application/vnd.sun.xml.calc.template": { - "source": "apache", - "extensions": ["stc"] - }, - "application/vnd.sun.xml.draw": { - "source": "apache", - "extensions": ["sxd"] - }, - "application/vnd.sun.xml.draw.template": { - "source": "apache", - "extensions": ["std"] - }, - "application/vnd.sun.xml.impress": { - "source": "apache", - "extensions": ["sxi"] - }, - "application/vnd.sun.xml.impress.template": { - "source": "apache", - "extensions": ["sti"] - }, - "application/vnd.sun.xml.math": { - "source": "apache", - "extensions": ["sxm"] - }, - "application/vnd.sun.xml.writer": { - "source": "apache", - "extensions": ["sxw"] - }, - "application/vnd.sun.xml.writer.global": { - "source": "apache", - "extensions": ["sxg"] - }, - "application/vnd.sun.xml.writer.template": { - "source": "apache", - "extensions": ["stw"] - }, - "application/vnd.sus-calendar": { - "source": "iana", - "extensions": ["sus","susp"] - }, - "application/vnd.svd": { - "source": "iana", - "extensions": ["svd"] - }, - "application/vnd.swiftview-ics": { - "source": "iana" - }, - "application/vnd.symbian.install": { - "source": "apache", - "extensions": ["sis","sisx"] - }, - "application/vnd.syncml+xml": { - "source": "iana", - "extensions": ["xsm"] - }, - "application/vnd.syncml.dm+wbxml": { - "source": "iana", - "extensions": ["bdm"] - }, - "application/vnd.syncml.dm+xml": { - "source": "iana", - "extensions": ["xdm"] - }, - "application/vnd.syncml.dm.notification": { - "source": "iana" - }, - "application/vnd.syncml.dmddf+wbxml": { - "source": "iana" - }, - "application/vnd.syncml.dmddf+xml": { - "source": "iana" - }, - "application/vnd.syncml.dmtnds+wbxml": { - "source": "iana" - }, - "application/vnd.syncml.dmtnds+xml": { - "source": "iana" - }, - "application/vnd.syncml.ds.notification": { - "source": "iana" - }, - "application/vnd.tao.intent-module-archive": { - "source": "iana", - "extensions": ["tao"] - }, - "application/vnd.tcpdump.pcap": { - "source": "iana", - "extensions": ["pcap","cap","dmp"] - }, - "application/vnd.tmd.mediaflex.api+xml": { - "source": "iana" - }, - "application/vnd.tml": { - "source": "iana" - }, - "application/vnd.tmobile-livetv": { - "source": "iana", - "extensions": ["tmo"] - }, - "application/vnd.trid.tpt": { - "source": "iana", - "extensions": ["tpt"] - }, - "application/vnd.triscape.mxs": { - "source": "iana", - "extensions": ["mxs"] - }, - "application/vnd.trueapp": { - "source": "iana", - "extensions": ["tra"] - }, - "application/vnd.truedoc": { - "source": "iana" - }, - "application/vnd.ubisoft.webplayer": { - "source": "iana" - }, - "application/vnd.ufdl": { - "source": "iana", - "extensions": ["ufd","ufdl"] - }, - "application/vnd.uiq.theme": { - "source": "iana", - "extensions": ["utz"] - }, - "application/vnd.umajin": { - "source": "iana", - "extensions": ["umj"] - }, - "application/vnd.unity": { - "source": "iana", - "extensions": ["unityweb"] - }, - "application/vnd.uoml+xml": { - "source": "iana", - "extensions": ["uoml"] - }, - "application/vnd.uplanet.alert": { - "source": "iana" - }, - "application/vnd.uplanet.alert-wbxml": { - "source": "iana" - }, - "application/vnd.uplanet.bearer-choice": { - "source": "iana" - }, - "application/vnd.uplanet.bearer-choice-wbxml": { - "source": "iana" - }, - "application/vnd.uplanet.cacheop": { - "source": "iana" - }, - "application/vnd.uplanet.cacheop-wbxml": { - "source": "iana" - }, - "application/vnd.uplanet.channel": { - "source": "iana" - }, - "application/vnd.uplanet.channel-wbxml": { - "source": "iana" - }, - "application/vnd.uplanet.list": { - "source": "iana" - }, - "application/vnd.uplanet.list-wbxml": { - "source": "iana" - }, - "application/vnd.uplanet.listcmd": { - "source": "iana" - }, - "application/vnd.uplanet.listcmd-wbxml": { - "source": "iana" - }, - "application/vnd.uplanet.signal": { - "source": "iana" - }, - "application/vnd.uri-map": { - "source": "iana" - }, - "application/vnd.valve.source.material": { - "source": "iana" - }, - "application/vnd.vcx": { - "source": "iana", - "extensions": ["vcx"] - }, - "application/vnd.vd-study": { - "source": "iana" - }, - "application/vnd.vectorworks": { - "source": "iana" - }, - "application/vnd.vel+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.verimatrix.vcas": { - "source": "iana" - }, - "application/vnd.vidsoft.vidconference": { - "source": "iana" - }, - "application/vnd.visio": { - "source": "iana", - "extensions": ["vsd","vst","vss","vsw"] - }, - "application/vnd.visionary": { - "source": "iana", - "extensions": ["vis"] - }, - "application/vnd.vividence.scriptfile": { - "source": "iana" - }, - "application/vnd.vsf": { - "source": "iana", - "extensions": ["vsf"] - }, - "application/vnd.wap.sic": { - "source": "iana" - }, - "application/vnd.wap.slc": { - "source": "iana" - }, - "application/vnd.wap.wbxml": { - "source": "iana", - "extensions": ["wbxml"] - }, - "application/vnd.wap.wmlc": { - "source": "iana", - "extensions": ["wmlc"] - }, - "application/vnd.wap.wmlscriptc": { - "source": "iana", - "extensions": ["wmlsc"] - }, - "application/vnd.webturbo": { - "source": "iana", - "extensions": ["wtb"] - }, - "application/vnd.wfa.p2p": { - "source": "iana" - }, - "application/vnd.wfa.wsc": { - "source": "iana" - }, - "application/vnd.windows.devicepairing": { - "source": "iana" - }, - "application/vnd.wmc": { - "source": "iana" - }, - "application/vnd.wmf.bootstrap": { - "source": "iana" - }, - "application/vnd.wolfram.mathematica": { - "source": "iana" - }, - "application/vnd.wolfram.mathematica.package": { - "source": "iana" - }, - "application/vnd.wolfram.player": { - "source": "iana", - "extensions": ["nbp"] - }, - "application/vnd.wordperfect": { - "source": "iana", - "extensions": ["wpd"] - }, - "application/vnd.wqd": { - "source": "iana", - "extensions": ["wqd"] - }, - "application/vnd.wrq-hp3000-labelled": { - "source": "iana" - }, - "application/vnd.wt.stf": { - "source": "iana", - "extensions": ["stf"] - }, - "application/vnd.wv.csp+wbxml": { - "source": "iana" - }, - "application/vnd.wv.csp+xml": { - "source": "iana" - }, - "application/vnd.wv.ssp+xml": { - "source": "iana" - }, - "application/vnd.xacml+json": { - "source": "iana", - "compressible": true - }, - "application/vnd.xara": { - "source": "iana", - "extensions": ["xar"] - }, - "application/vnd.xfdl": { - "source": "iana", - "extensions": ["xfdl"] - }, - "application/vnd.xfdl.webform": { - "source": "iana" - }, - "application/vnd.xmi+xml": { - "source": "iana" - }, - "application/vnd.xmpie.cpkg": { - "source": "iana" - }, - "application/vnd.xmpie.dpkg": { - "source": "iana" - }, - "application/vnd.xmpie.plan": { - "source": "iana" - }, - "application/vnd.xmpie.ppkg": { - "source": "iana" - }, - "application/vnd.xmpie.xlim": { - "source": "iana" - }, - "application/vnd.yamaha.hv-dic": { - "source": "iana", - "extensions": ["hvd"] - }, - "application/vnd.yamaha.hv-script": { - "source": "iana", - "extensions": ["hvs"] - }, - "application/vnd.yamaha.hv-voice": { - "source": "iana", - "extensions": ["hvp"] - }, - "application/vnd.yamaha.openscoreformat": { - "source": "iana", - "extensions": ["osf"] - }, - "application/vnd.yamaha.openscoreformat.osfpvg+xml": { - "source": "iana", - "extensions": ["osfpvg"] - }, - "application/vnd.yamaha.remote-setup": { - "source": "iana" - }, - "application/vnd.yamaha.smaf-audio": { - "source": "iana", - "extensions": ["saf"] - }, - "application/vnd.yamaha.smaf-phrase": { - "source": "iana", - "extensions": ["spf"] - }, - "application/vnd.yamaha.through-ngn": { - "source": "iana" - }, - "application/vnd.yamaha.tunnel-udpencap": { - "source": "iana" - }, - "application/vnd.yaoweme": { - "source": "iana" - }, - "application/vnd.yellowriver-custom-menu": { - "source": "iana", - "extensions": ["cmp"] - }, - "application/vnd.zul": { - "source": "iana", - "extensions": ["zir","zirz"] - }, - "application/vnd.zzazz.deck+xml": { - "source": "iana", - "extensions": ["zaz"] - }, - "application/voicexml+xml": { - "source": "iana", - "extensions": ["vxml"] - }, - "application/vq-rtcpxr": { - "source": "iana" - }, - "application/watcherinfo+xml": { - "source": "iana" - }, - "application/whoispp-query": { - "source": "iana" - }, - "application/whoispp-response": { - "source": "iana" - }, - "application/widget": { - "source": "iana", - "extensions": ["wgt"] - }, - "application/winhlp": { - "source": "apache", - "extensions": ["hlp"] - }, - "application/wita": { - "source": "iana" - }, - "application/wordperfect5.1": { - "source": "iana" - }, - "application/wsdl+xml": { - "source": "iana", - "extensions": ["wsdl"] - }, - "application/wspolicy+xml": { - "source": "iana", - "extensions": ["wspolicy"] - }, - "application/x-7z-compressed": { - "source": "apache", - "compressible": false, - "extensions": ["7z"] - }, - "application/x-abiword": { - "source": "apache", - "extensions": ["abw"] - }, - "application/x-ace-compressed": { - "source": "apache", - "extensions": ["ace"] - }, - "application/x-amf": { - "source": "apache" - }, - "application/x-apple-diskimage": { - "source": "apache", - "extensions": ["dmg"] - }, - "application/x-authorware-bin": { - "source": "apache", - "extensions": ["aab","x32","u32","vox"] - }, - "application/x-authorware-map": { - "source": "apache", - "extensions": ["aam"] - }, - "application/x-authorware-seg": { - "source": "apache", - "extensions": ["aas"] - }, - "application/x-bcpio": { - "source": "apache", - "extensions": ["bcpio"] - }, - "application/x-bdoc": { - "compressible": false, - "extensions": ["bdoc"] - }, - "application/x-bittorrent": { - "source": "apache", - "extensions": ["torrent"] - }, - "application/x-blorb": { - "source": "apache", - "extensions": ["blb","blorb"] - }, - "application/x-bzip": { - "source": "apache", - "compressible": false, - "extensions": ["bz"] - }, - "application/x-bzip2": { - "source": "apache", - "compressible": false, - "extensions": ["bz2","boz"] - }, - "application/x-cbr": { - "source": "apache", - "extensions": ["cbr","cba","cbt","cbz","cb7"] - }, - "application/x-cdlink": { - "source": "apache", - "extensions": ["vcd"] - }, - "application/x-cfs-compressed": { - "source": "apache", - "extensions": ["cfs"] - }, - "application/x-chat": { - "source": "apache", - "extensions": ["chat"] - }, - "application/x-chess-pgn": { - "source": "apache", - "extensions": ["pgn"] - }, - "application/x-chrome-extension": { - "extensions": ["crx"] - }, - "application/x-cocoa": { - "source": "nginx", - "extensions": ["cco"] - }, - "application/x-compress": { - "source": "apache" - }, - "application/x-conference": { - "source": "apache", - "extensions": ["nsc"] - }, - "application/x-cpio": { - "source": "apache", - "extensions": ["cpio"] - }, - "application/x-csh": { - "source": "apache", - "extensions": ["csh"] - }, - "application/x-deb": { - "compressible": false - }, - "application/x-debian-package": { - "source": "apache", - "extensions": ["deb","udeb"] - }, - "application/x-dgc-compressed": { - "source": "apache", - "extensions": ["dgc"] - }, - "application/x-director": { - "source": "apache", - "extensions": ["dir","dcr","dxr","cst","cct","cxt","w3d","fgd","swa"] - }, - "application/x-doom": { - "source": "apache", - "extensions": ["wad"] - }, - "application/x-dtbncx+xml": { - "source": "apache", - "extensions": ["ncx"] - }, - "application/x-dtbook+xml": { - "source": "apache", - "extensions": ["dtb"] - }, - "application/x-dtbresource+xml": { - "source": "apache", - "extensions": ["res"] - }, - "application/x-dvi": { - "source": "apache", - "compressible": false, - "extensions": ["dvi"] - }, - "application/x-envoy": { - "source": "apache", - "extensions": ["evy"] - }, - "application/x-eva": { - "source": "apache", - "extensions": ["eva"] - }, - "application/x-font-bdf": { - "source": "apache", - "extensions": ["bdf"] - }, - "application/x-font-dos": { - "source": "apache" - }, - "application/x-font-framemaker": { - "source": "apache" - }, - "application/x-font-ghostscript": { - "source": "apache", - "extensions": ["gsf"] - }, - "application/x-font-libgrx": { - "source": "apache" - }, - "application/x-font-linux-psf": { - "source": "apache", - "extensions": ["psf"] - }, - "application/x-font-otf": { - "source": "apache", - "compressible": true, - "extensions": ["otf"] - }, - "application/x-font-pcf": { - "source": "apache", - "extensions": ["pcf"] - }, - "application/x-font-snf": { - "source": "apache", - "extensions": ["snf"] - }, - "application/x-font-speedo": { - "source": "apache" - }, - "application/x-font-sunos-news": { - "source": "apache" - }, - "application/x-font-ttf": { - "source": "apache", - "compressible": true, - "extensions": ["ttf","ttc"] - }, - "application/x-font-type1": { - "source": "apache", - "extensions": ["pfa","pfb","pfm","afm"] - }, - "application/x-font-vfont": { - "source": "apache" - }, - "application/x-freearc": { - "source": "apache", - "extensions": ["arc"] - }, - "application/x-futuresplash": { - "source": "apache", - "extensions": ["spl"] - }, - "application/x-gca-compressed": { - "source": "apache", - "extensions": ["gca"] - }, - "application/x-glulx": { - "source": "apache", - "extensions": ["ulx"] - }, - "application/x-gnumeric": { - "source": "apache", - "extensions": ["gnumeric"] - }, - "application/x-gramps-xml": { - "source": "apache", - "extensions": ["gramps"] - }, - "application/x-gtar": { - "source": "apache", - "extensions": ["gtar"] - }, - "application/x-gzip": { - "source": "apache" - }, - "application/x-hdf": { - "source": "apache", - "extensions": ["hdf"] - }, - "application/x-httpd-php": { - "compressible": true, - "extensions": ["php"] - }, - "application/x-install-instructions": { - "source": "apache", - "extensions": ["install"] - }, - "application/x-iso9660-image": { - "source": "apache", - "extensions": ["iso"] - }, - "application/x-java-archive-diff": { - "source": "nginx", - "extensions": ["jardiff"] - }, - "application/x-java-jnlp-file": { - "source": "apache", - "compressible": false, - "extensions": ["jnlp"] - }, - "application/x-javascript": { - "compressible": true - }, - "application/x-latex": { - "source": "apache", - "compressible": false, - "extensions": ["latex"] - }, - "application/x-lua-bytecode": { - "extensions": ["luac"] - }, - "application/x-lzh-compressed": { - "source": "apache", - "extensions": ["lzh","lha"] - }, - "application/x-makeself": { - "source": "nginx", - "extensions": ["run"] - }, - "application/x-mie": { - "source": "apache", - "extensions": ["mie"] - }, - "application/x-mobipocket-ebook": { - "source": "apache", - "extensions": ["prc","mobi"] - }, - "application/x-mpegurl": { - "compressible": false - }, - "application/x-ms-application": { - "source": "apache", - "extensions": ["application"] - }, - "application/x-ms-shortcut": { - "source": "apache", - "extensions": ["lnk"] - }, - "application/x-ms-wmd": { - "source": "apache", - "extensions": ["wmd"] - }, - "application/x-ms-wmz": { - "source": "apache", - "extensions": ["wmz"] - }, - "application/x-ms-xbap": { - "source": "apache", - "extensions": ["xbap"] - }, - "application/x-msaccess": { - "source": "apache", - "extensions": ["mdb"] - }, - "application/x-msbinder": { - "source": "apache", - "extensions": ["obd"] - }, - "application/x-mscardfile": { - "source": "apache", - "extensions": ["crd"] - }, - "application/x-msclip": { - "source": "apache", - "extensions": ["clp"] - }, - "application/x-msdos-program": { - "extensions": ["exe"] - }, - "application/x-msdownload": { - "source": "apache", - "extensions": ["exe","dll","com","bat","msi"] - }, - "application/x-msmediaview": { - "source": "apache", - "extensions": ["mvb","m13","m14"] - }, - "application/x-msmetafile": { - "source": "apache", - "extensions": ["wmf","wmz","emf","emz"] - }, - "application/x-msmoney": { - "source": "apache", - "extensions": ["mny"] - }, - "application/x-mspublisher": { - "source": "apache", - "extensions": ["pub"] - }, - "application/x-msschedule": { - "source": "apache", - "extensions": ["scd"] - }, - "application/x-msterminal": { - "source": "apache", - "extensions": ["trm"] - }, - "application/x-mswrite": { - "source": "apache", - "extensions": ["wri"] - }, - "application/x-netcdf": { - "source": "apache", - "extensions": ["nc","cdf"] - }, - "application/x-ns-proxy-autoconfig": { - "compressible": true, - "extensions": ["pac"] - }, - "application/x-nzb": { - "source": "apache", - "extensions": ["nzb"] - }, - "application/x-perl": { - "source": "nginx", - "extensions": ["pl","pm"] - }, - "application/x-pilot": { - "source": "nginx", - "extensions": ["prc","pdb"] - }, - "application/x-pkcs12": { - "source": "apache", - "compressible": false, - "extensions": ["p12","pfx"] - }, - "application/x-pkcs7-certificates": { - "source": "apache", - "extensions": ["p7b","spc"] - }, - "application/x-pkcs7-certreqresp": { - "source": "apache", - "extensions": ["p7r"] - }, - "application/x-rar-compressed": { - "source": "apache", - "compressible": false, - "extensions": ["rar"] - }, - "application/x-redhat-package-manager": { - "source": "nginx", - "extensions": ["rpm"] - }, - "application/x-research-info-systems": { - "source": "apache", - "extensions": ["ris"] - }, - "application/x-sea": { - "source": "nginx", - "extensions": ["sea"] - }, - "application/x-sh": { - "source": "apache", - "compressible": true, - "extensions": ["sh"] - }, - "application/x-shar": { - "source": "apache", - "extensions": ["shar"] - }, - "application/x-shockwave-flash": { - "source": "apache", - "compressible": false, - "extensions": ["swf"] - }, - "application/x-silverlight-app": { - "source": "apache", - "extensions": ["xap"] - }, - "application/x-sql": { - "source": "apache", - "extensions": ["sql"] - }, - "application/x-stuffit": { - "source": "apache", - "compressible": false, - "extensions": ["sit"] - }, - "application/x-stuffitx": { - "source": "apache", - "extensions": ["sitx"] - }, - "application/x-subrip": { - "source": "apache", - "extensions": ["srt"] - }, - "application/x-sv4cpio": { - "source": "apache", - "extensions": ["sv4cpio"] - }, - "application/x-sv4crc": { - "source": "apache", - "extensions": ["sv4crc"] - }, - "application/x-t3vm-image": { - "source": "apache", - "extensions": ["t3"] - }, - "application/x-tads": { - "source": "apache", - "extensions": ["gam"] - }, - "application/x-tar": { - "source": "apache", - "compressible": true, - "extensions": ["tar"] - }, - "application/x-tcl": { - "source": "apache", - "extensions": ["tcl","tk"] - }, - "application/x-tex": { - "source": "apache", - "extensions": ["tex"] - }, - "application/x-tex-tfm": { - "source": "apache", - "extensions": ["tfm"] - }, - "application/x-texinfo": { - "source": "apache", - "extensions": ["texinfo","texi"] - }, - "application/x-tgif": { - "source": "apache", - "extensions": ["obj"] - }, - "application/x-ustar": { - "source": "apache", - "extensions": ["ustar"] - }, - "application/x-wais-source": { - "source": "apache", - "extensions": ["src"] - }, - "application/x-web-app-manifest+json": { - "compressible": true, - "extensions": ["webapp"] - }, - "application/x-www-form-urlencoded": { - "source": "iana", - "compressible": true - }, - "application/x-x509-ca-cert": { - "source": "apache", - "extensions": ["der","crt","pem"] - }, - "application/x-xfig": { - "source": "apache", - "extensions": ["fig"] - }, - "application/x-xliff+xml": { - "source": "apache", - "extensions": ["xlf"] - }, - "application/x-xpinstall": { - "source": "apache", - "compressible": false, - "extensions": ["xpi"] - }, - "application/x-xz": { - "source": "apache", - "extensions": ["xz"] - }, - "application/x-zmachine": { - "source": "apache", - "extensions": ["z1","z2","z3","z4","z5","z6","z7","z8"] - }, - "application/x400-bp": { - "source": "iana" - }, - "application/xacml+xml": { - "source": "iana" - }, - "application/xaml+xml": { - "source": "apache", - "extensions": ["xaml"] - }, - "application/xcap-att+xml": { - "source": "iana" - }, - "application/xcap-caps+xml": { - "source": "iana" - }, - "application/xcap-diff+xml": { - "source": "iana", - "extensions": ["xdf"] - }, - "application/xcap-el+xml": { - "source": "iana" - }, - "application/xcap-error+xml": { - "source": "iana" - }, - "application/xcap-ns+xml": { - "source": "iana" - }, - "application/xcon-conference-info+xml": { - "source": "iana" - }, - "application/xcon-conference-info-diff+xml": { - "source": "iana" - }, - "application/xenc+xml": { - "source": "iana", - "extensions": ["xenc"] - }, - "application/xhtml+xml": { - "source": "iana", - "compressible": true, - "extensions": ["xhtml","xht"] - }, - "application/xhtml-voice+xml": { - "source": "apache" - }, - "application/xml": { - "source": "iana", - "compressible": true, - "extensions": ["xml","xsl","xsd","rng"] - }, - "application/xml-dtd": { - "source": "iana", - "compressible": true, - "extensions": ["dtd"] - }, - "application/xml-external-parsed-entity": { - "source": "iana" - }, - "application/xml-patch+xml": { - "source": "iana" - }, - "application/xmpp+xml": { - "source": "iana" - }, - "application/xop+xml": { - "source": "iana", - "compressible": true, - "extensions": ["xop"] - }, - "application/xproc+xml": { - "source": "apache", - "extensions": ["xpl"] - }, - "application/xslt+xml": { - "source": "iana", - "extensions": ["xslt"] - }, - "application/xspf+xml": { - "source": "apache", - "extensions": ["xspf"] - }, - "application/xv+xml": { - "source": "iana", - "extensions": ["mxml","xhvml","xvml","xvm"] - }, - "application/yang": { - "source": "iana", - "extensions": ["yang"] - }, - "application/yin+xml": { - "source": "iana", - "extensions": ["yin"] - }, - "application/zip": { - "source": "iana", - "compressible": false, - "extensions": ["zip"] - }, - "application/zlib": { - "source": "iana" - }, - "audio/1d-interleaved-parityfec": { - "source": "iana" - }, - "audio/32kadpcm": { - "source": "iana" - }, - "audio/3gpp": { - "source": "iana", - "compressible": false, - "extensions": ["3gpp"] - }, - "audio/3gpp2": { - "source": "iana" - }, - "audio/ac3": { - "source": "iana" - }, - "audio/adpcm": { - "source": "apache", - "extensions": ["adp"] - }, - "audio/amr": { - "source": "iana" - }, - "audio/amr-wb": { - "source": "iana" - }, - "audio/amr-wb+": { - "source": "iana" - }, - "audio/aptx": { - "source": "iana" - }, - "audio/asc": { - "source": "iana" - }, - "audio/atrac-advanced-lossless": { - "source": "iana" - }, - "audio/atrac-x": { - "source": "iana" - }, - "audio/atrac3": { - "source": "iana" - }, - "audio/basic": { - "source": "iana", - "compressible": false, - "extensions": ["au","snd"] - }, - "audio/bv16": { - "source": "iana" - }, - "audio/bv32": { - "source": "iana" - }, - "audio/clearmode": { - "source": "iana" - }, - "audio/cn": { - "source": "iana" - }, - "audio/dat12": { - "source": "iana" - }, - "audio/dls": { - "source": "iana" - }, - "audio/dsr-es201108": { - "source": "iana" - }, - "audio/dsr-es202050": { - "source": "iana" - }, - "audio/dsr-es202211": { - "source": "iana" - }, - "audio/dsr-es202212": { - "source": "iana" - }, - "audio/dv": { - "source": "iana" - }, - "audio/dvi4": { - "source": "iana" - }, - "audio/eac3": { - "source": "iana" - }, - "audio/encaprtp": { - "source": "iana" - }, - "audio/evrc": { - "source": "iana" - }, - "audio/evrc-qcp": { - "source": "iana" - }, - "audio/evrc0": { - "source": "iana" - }, - "audio/evrc1": { - "source": "iana" - }, - "audio/evrcb": { - "source": "iana" - }, - "audio/evrcb0": { - "source": "iana" - }, - "audio/evrcb1": { - "source": "iana" - }, - "audio/evrcnw": { - "source": "iana" - }, - "audio/evrcnw0": { - "source": "iana" - }, - "audio/evrcnw1": { - "source": "iana" - }, - "audio/evrcwb": { - "source": "iana" - }, - "audio/evrcwb0": { - "source": "iana" - }, - "audio/evrcwb1": { - "source": "iana" - }, - "audio/evs": { - "source": "iana" - }, - "audio/fwdred": { - "source": "iana" - }, - "audio/g711-0": { - "source": "iana" - }, - "audio/g719": { - "source": "iana" - }, - "audio/g722": { - "source": "iana" - }, - "audio/g7221": { - "source": "iana" - }, - "audio/g723": { - "source": "iana" - }, - "audio/g726-16": { - "source": "iana" - }, - "audio/g726-24": { - "source": "iana" - }, - "audio/g726-32": { - "source": "iana" - }, - "audio/g726-40": { - "source": "iana" - }, - "audio/g728": { - "source": "iana" - }, - "audio/g729": { - "source": "iana" - }, - "audio/g7291": { - "source": "iana" - }, - "audio/g729d": { - "source": "iana" - }, - "audio/g729e": { - "source": "iana" - }, - "audio/gsm": { - "source": "iana" - }, - "audio/gsm-efr": { - "source": "iana" - }, - "audio/gsm-hr-08": { - "source": "iana" - }, - "audio/ilbc": { - "source": "iana" - }, - "audio/ip-mr_v2.5": { - "source": "iana" - }, - "audio/isac": { - "source": "apache" - }, - "audio/l16": { - "source": "iana" - }, - "audio/l20": { - "source": "iana" - }, - "audio/l24": { - "source": "iana", - "compressible": false - }, - "audio/l8": { - "source": "iana" - }, - "audio/lpc": { - "source": "iana" - }, - "audio/midi": { - "source": "apache", - "extensions": ["mid","midi","kar","rmi"] - }, - "audio/mobile-xmf": { - "source": "iana" - }, - "audio/mp4": { - "source": "iana", - "compressible": false, - "extensions": ["m4a","mp4a"] - }, - "audio/mp4a-latm": { - "source": "iana" - }, - "audio/mpa": { - "source": "iana" - }, - "audio/mpa-robust": { - "source": "iana" - }, - "audio/mpeg": { - "source": "iana", - "compressible": false, - "extensions": ["mpga","mp2","mp2a","mp3","m2a","m3a"] - }, - "audio/mpeg4-generic": { - "source": "iana" - }, - "audio/musepack": { - "source": "apache" - }, - "audio/ogg": { - "source": "iana", - "compressible": false, - "extensions": ["oga","ogg","spx"] - }, - "audio/opus": { - "source": "iana" - }, - "audio/parityfec": { - "source": "iana" - }, - "audio/pcma": { - "source": "iana" - }, - "audio/pcma-wb": { - "source": "iana" - }, - "audio/pcmu": { - "source": "iana" - }, - "audio/pcmu-wb": { - "source": "iana" - }, - "audio/prs.sid": { - "source": "iana" - }, - "audio/qcelp": { - "source": "iana" - }, - "audio/raptorfec": { - "source": "iana" - }, - "audio/red": { - "source": "iana" - }, - "audio/rtp-enc-aescm128": { - "source": "iana" - }, - "audio/rtp-midi": { - "source": "iana" - }, - "audio/rtploopback": { - "source": "iana" - }, - "audio/rtx": { - "source": "iana" - }, - "audio/s3m": { - "source": "apache", - "extensions": ["s3m"] - }, - "audio/silk": { - "source": "apache", - "extensions": ["sil"] - }, - "audio/smv": { - "source": "iana" - }, - "audio/smv-qcp": { - "source": "iana" - }, - "audio/smv0": { - "source": "iana" - }, - "audio/sp-midi": { - "source": "iana" - }, - "audio/speex": { - "source": "iana" - }, - "audio/t140c": { - "source": "iana" - }, - "audio/t38": { - "source": "iana" - }, - "audio/telephone-event": { - "source": "iana" - }, - "audio/tone": { - "source": "iana" - }, - "audio/uemclip": { - "source": "iana" - }, - "audio/ulpfec": { - "source": "iana" - }, - "audio/vdvi": { - "source": "iana" - }, - "audio/vmr-wb": { - "source": "iana" - }, - "audio/vnd.3gpp.iufp": { - "source": "iana" - }, - "audio/vnd.4sb": { - "source": "iana" - }, - "audio/vnd.audiokoz": { - "source": "iana" - }, - "audio/vnd.celp": { - "source": "iana" - }, - "audio/vnd.cisco.nse": { - "source": "iana" - }, - "audio/vnd.cmles.radio-events": { - "source": "iana" - }, - "audio/vnd.cns.anp1": { - "source": "iana" - }, - "audio/vnd.cns.inf1": { - "source": "iana" - }, - "audio/vnd.dece.audio": { - "source": "iana", - "extensions": ["uva","uvva"] - }, - "audio/vnd.digital-winds": { - "source": "iana", - "extensions": ["eol"] - }, - "audio/vnd.dlna.adts": { - "source": "iana" - }, - "audio/vnd.dolby.heaac.1": { - "source": "iana" - }, - "audio/vnd.dolby.heaac.2": { - "source": "iana" - }, - "audio/vnd.dolby.mlp": { - "source": "iana" - }, - "audio/vnd.dolby.mps": { - "source": "iana" - }, - "audio/vnd.dolby.pl2": { - "source": "iana" - }, - "audio/vnd.dolby.pl2x": { - "source": "iana" - }, - "audio/vnd.dolby.pl2z": { - "source": "iana" - }, - "audio/vnd.dolby.pulse.1": { - "source": "iana" - }, - "audio/vnd.dra": { - "source": "iana", - "extensions": ["dra"] - }, - "audio/vnd.dts": { - "source": "iana", - "extensions": ["dts"] - }, - "audio/vnd.dts.hd": { - "source": "iana", - "extensions": ["dtshd"] - }, - "audio/vnd.dvb.file": { - "source": "iana" - }, - "audio/vnd.everad.plj": { - "source": "iana" - }, - "audio/vnd.hns.audio": { - "source": "iana" - }, - "audio/vnd.lucent.voice": { - "source": "iana", - "extensions": ["lvp"] - }, - "audio/vnd.ms-playready.media.pya": { - "source": "iana", - "extensions": ["pya"] - }, - "audio/vnd.nokia.mobile-xmf": { - "source": "iana" - }, - "audio/vnd.nortel.vbk": { - "source": "iana" - }, - "audio/vnd.nuera.ecelp4800": { - "source": "iana", - "extensions": ["ecelp4800"] - }, - "audio/vnd.nuera.ecelp7470": { - "source": "iana", - "extensions": ["ecelp7470"] - }, - "audio/vnd.nuera.ecelp9600": { - "source": "iana", - "extensions": ["ecelp9600"] - }, - "audio/vnd.octel.sbc": { - "source": "iana" - }, - "audio/vnd.qcelp": { - "source": "iana" - }, - "audio/vnd.rhetorex.32kadpcm": { - "source": "iana" - }, - "audio/vnd.rip": { - "source": "iana", - "extensions": ["rip"] - }, - "audio/vnd.rn-realaudio": { - "compressible": false - }, - "audio/vnd.sealedmedia.softseal.mpeg": { - "source": "iana" - }, - "audio/vnd.vmx.cvsd": { - "source": "iana" - }, - "audio/vnd.wave": { - "compressible": false - }, - "audio/vorbis": { - "source": "iana", - "compressible": false - }, - "audio/vorbis-config": { - "source": "iana" - }, - "audio/wav": { - "compressible": false, - "extensions": ["wav"] - }, - "audio/wave": { - "compressible": false, - "extensions": ["wav"] - }, - "audio/webm": { - "source": "apache", - "compressible": false, - "extensions": ["weba"] - }, - "audio/x-aac": { - "source": "apache", - "compressible": false, - "extensions": ["aac"] - }, - "audio/x-aiff": { - "source": "apache", - "extensions": ["aif","aiff","aifc"] - }, - "audio/x-caf": { - "source": "apache", - "compressible": false, - "extensions": ["caf"] - }, - "audio/x-flac": { - "source": "apache", - "extensions": ["flac"] - }, - "audio/x-m4a": { - "source": "nginx", - "extensions": ["m4a"] - }, - "audio/x-matroska": { - "source": "apache", - "extensions": ["mka"] - }, - "audio/x-mpegurl": { - "source": "apache", - "extensions": ["m3u"] - }, - "audio/x-ms-wax": { - "source": "apache", - "extensions": ["wax"] - }, - "audio/x-ms-wma": { - "source": "apache", - "extensions": ["wma"] - }, - "audio/x-pn-realaudio": { - "source": "apache", - "extensions": ["ram","ra"] - }, - "audio/x-pn-realaudio-plugin": { - "source": "apache", - "extensions": ["rmp"] - }, - "audio/x-realaudio": { - "source": "nginx", - "extensions": ["ra"] - }, - "audio/x-tta": { - "source": "apache" - }, - "audio/x-wav": { - "source": "apache", - "extensions": ["wav"] - }, - "audio/xm": { - "source": "apache", - "extensions": ["xm"] - }, - "chemical/x-cdx": { - "source": "apache", - "extensions": ["cdx"] - }, - "chemical/x-cif": { - "source": "apache", - "extensions": ["cif"] - }, - "chemical/x-cmdf": { - "source": "apache", - "extensions": ["cmdf"] - }, - "chemical/x-cml": { - "source": "apache", - "extensions": ["cml"] - }, - "chemical/x-csml": { - "source": "apache", - "extensions": ["csml"] - }, - "chemical/x-pdb": { - "source": "apache" - }, - "chemical/x-xyz": { - "source": "apache", - "extensions": ["xyz"] - }, - "font/opentype": { - "compressible": true, - "extensions": ["otf"] - }, - "image/bmp": { - "source": "apache", - "compressible": true, - "extensions": ["bmp"] - }, - "image/cgm": { - "source": "iana", - "extensions": ["cgm"] - }, - "image/fits": { - "source": "iana" - }, - "image/g3fax": { - "source": "iana", - "extensions": ["g3"] - }, - "image/gif": { - "source": "iana", - "compressible": false, - "extensions": ["gif"] - }, - "image/ief": { - "source": "iana", - "extensions": ["ief"] - }, - "image/jp2": { - "source": "iana" - }, - "image/jpeg": { - "source": "iana", - "compressible": false, - "extensions": ["jpeg","jpg","jpe"] - }, - "image/jpm": { - "source": "iana" - }, - "image/jpx": { - "source": "iana" - }, - "image/ktx": { - "source": "iana", - "extensions": ["ktx"] - }, - "image/naplps": { - "source": "iana" - }, - "image/pjpeg": { - "compressible": false - }, - "image/png": { - "source": "iana", - "compressible": false, - "extensions": ["png"] - }, - "image/prs.btif": { - "source": "iana", - "extensions": ["btif"] - }, - "image/prs.pti": { - "source": "iana" - }, - "image/pwg-raster": { - "source": "iana" - }, - "image/sgi": { - "source": "apache", - "extensions": ["sgi"] - }, - "image/svg+xml": { - "source": "iana", - "compressible": true, - "extensions": ["svg","svgz"] - }, - "image/t38": { - "source": "iana" - }, - "image/tiff": { - "source": "iana", - "compressible": false, - "extensions": ["tiff","tif"] - }, - "image/tiff-fx": { - "source": "iana" - }, - "image/vnd.adobe.photoshop": { - "source": "iana", - "compressible": true, - "extensions": ["psd"] - }, - "image/vnd.airzip.accelerator.azv": { - "source": "iana" - }, - "image/vnd.cns.inf2": { - "source": "iana" - }, - "image/vnd.dece.graphic": { - "source": "iana", - "extensions": ["uvi","uvvi","uvg","uvvg"] - }, - "image/vnd.djvu": { - "source": "iana", - "extensions": ["djvu","djv"] - }, - "image/vnd.dvb.subtitle": { - "source": "iana", - "extensions": ["sub"] - }, - "image/vnd.dwg": { - "source": "iana", - "extensions": ["dwg"] - }, - "image/vnd.dxf": { - "source": "iana", - "extensions": ["dxf"] - }, - "image/vnd.fastbidsheet": { - "source": "iana", - "extensions": ["fbs"] - }, - "image/vnd.fpx": { - "source": "iana", - "extensions": ["fpx"] - }, - "image/vnd.fst": { - "source": "iana", - "extensions": ["fst"] - }, - "image/vnd.fujixerox.edmics-mmr": { - "source": "iana", - "extensions": ["mmr"] - }, - "image/vnd.fujixerox.edmics-rlc": { - "source": "iana", - "extensions": ["rlc"] - }, - "image/vnd.globalgraphics.pgb": { - "source": "iana" - }, - "image/vnd.microsoft.icon": { - "source": "iana" - }, - "image/vnd.mix": { - "source": "iana" - }, - "image/vnd.mozilla.apng": { - "source": "iana" - }, - "image/vnd.ms-modi": { - "source": "iana", - "extensions": ["mdi"] - }, - "image/vnd.ms-photo": { - "source": "apache", - "extensions": ["wdp"] - }, - "image/vnd.net-fpx": { - "source": "iana", - "extensions": ["npx"] - }, - "image/vnd.radiance": { - "source": "iana" - }, - "image/vnd.sealed.png": { - "source": "iana" - }, - "image/vnd.sealedmedia.softseal.gif": { - "source": "iana" - }, - "image/vnd.sealedmedia.softseal.jpg": { - "source": "iana" - }, - "image/vnd.svf": { - "source": "iana" - }, - "image/vnd.tencent.tap": { - "source": "iana" - }, - "image/vnd.valve.source.texture": { - "source": "iana" - }, - "image/vnd.wap.wbmp": { - "source": "iana", - "extensions": ["wbmp"] - }, - "image/vnd.xiff": { - "source": "iana", - "extensions": ["xif"] - }, - "image/vnd.zbrush.pcx": { - "source": "iana" - }, - "image/webp": { - "source": "apache", - "extensions": ["webp"] - }, - "image/x-3ds": { - "source": "apache", - "extensions": ["3ds"] - }, - "image/x-cmu-raster": { - "source": "apache", - "extensions": ["ras"] - }, - "image/x-cmx": { - "source": "apache", - "extensions": ["cmx"] - }, - "image/x-freehand": { - "source": "apache", - "extensions": ["fh","fhc","fh4","fh5","fh7"] - }, - "image/x-icon": { - "source": "apache", - "compressible": true, - "extensions": ["ico"] - }, - "image/x-jng": { - "source": "nginx", - "extensions": ["jng"] - }, - "image/x-mrsid-image": { - "source": "apache", - "extensions": ["sid"] - }, - "image/x-ms-bmp": { - "source": "nginx", - "compressible": true, - "extensions": ["bmp"] - }, - "image/x-pcx": { - "source": "apache", - "extensions": ["pcx"] - }, - "image/x-pict": { - "source": "apache", - "extensions": ["pic","pct"] - }, - "image/x-portable-anymap": { - "source": "apache", - "extensions": ["pnm"] - }, - "image/x-portable-bitmap": { - "source": "apache", - "extensions": ["pbm"] - }, - "image/x-portable-graymap": { - "source": "apache", - "extensions": ["pgm"] - }, - "image/x-portable-pixmap": { - "source": "apache", - "extensions": ["ppm"] - }, - "image/x-rgb": { - "source": "apache", - "extensions": ["rgb"] - }, - "image/x-tga": { - "source": "apache", - "extensions": ["tga"] - }, - "image/x-xbitmap": { - "source": "apache", - "extensions": ["xbm"] - }, - "image/x-xcf": { - "compressible": false - }, - "image/x-xpixmap": { - "source": "apache", - "extensions": ["xpm"] - }, - "image/x-xwindowdump": { - "source": "apache", - "extensions": ["xwd"] - }, - "message/cpim": { - "source": "iana" - }, - "message/delivery-status": { - "source": "iana" - }, - "message/disposition-notification": { - "source": "iana" - }, - "message/external-body": { - "source": "iana" - }, - "message/feedback-report": { - "source": "iana" - }, - "message/global": { - "source": "iana" - }, - "message/global-delivery-status": { - "source": "iana" - }, - "message/global-disposition-notification": { - "source": "iana" - }, - "message/global-headers": { - "source": "iana" - }, - "message/http": { - "source": "iana", - "compressible": false - }, - "message/imdn+xml": { - "source": "iana", - "compressible": true - }, - "message/news": { - "source": "iana" - }, - "message/partial": { - "source": "iana", - "compressible": false - }, - "message/rfc822": { - "source": "iana", - "compressible": true, - "extensions": ["eml","mime"] - }, - "message/s-http": { - "source": "iana" - }, - "message/sip": { - "source": "iana" - }, - "message/sipfrag": { - "source": "iana" - }, - "message/tracking-status": { - "source": "iana" - }, - "message/vnd.si.simp": { - "source": "iana" - }, - "message/vnd.wfa.wsc": { - "source": "iana" - }, - "model/iges": { - "source": "iana", - "compressible": false, - "extensions": ["igs","iges"] - }, - "model/mesh": { - "source": "iana", - "compressible": false, - "extensions": ["msh","mesh","silo"] - }, - "model/vnd.collada+xml": { - "source": "iana", - "extensions": ["dae"] - }, - "model/vnd.dwf": { - "source": "iana", - "extensions": ["dwf"] - }, - "model/vnd.flatland.3dml": { - "source": "iana" - }, - "model/vnd.gdl": { - "source": "iana", - "extensions": ["gdl"] - }, - "model/vnd.gs-gdl": { - "source": "apache" - }, - "model/vnd.gs.gdl": { - "source": "iana" - }, - "model/vnd.gtw": { - "source": "iana", - "extensions": ["gtw"] - }, - "model/vnd.moml+xml": { - "source": "iana" - }, - "model/vnd.mts": { - "source": "iana", - "extensions": ["mts"] - }, - "model/vnd.opengex": { - "source": "iana" - }, - "model/vnd.parasolid.transmit.binary": { - "source": "iana" - }, - "model/vnd.parasolid.transmit.text": { - "source": "iana" - }, - "model/vnd.rosette.annotated-data-model": { - "source": "iana" - }, - "model/vnd.valve.source.compiled-map": { - "source": "iana" - }, - "model/vnd.vtu": { - "source": "iana", - "extensions": ["vtu"] - }, - "model/vrml": { - "source": "iana", - "compressible": false, - "extensions": ["wrl","vrml"] - }, - "model/x3d+binary": { - "source": "apache", - "compressible": false, - "extensions": ["x3db","x3dbz"] - }, - "model/x3d+fastinfoset": { - "source": "iana" - }, - "model/x3d+vrml": { - "source": "apache", - "compressible": false, - "extensions": ["x3dv","x3dvz"] - }, - "model/x3d+xml": { - "source": "iana", - "compressible": true, - "extensions": ["x3d","x3dz"] - }, - "model/x3d-vrml": { - "source": "iana" - }, - "multipart/alternative": { - "source": "iana", - "compressible": false - }, - "multipart/appledouble": { - "source": "iana" - }, - "multipart/byteranges": { - "source": "iana" - }, - "multipart/digest": { - "source": "iana" - }, - "multipart/encrypted": { - "source": "iana", - "compressible": false - }, - "multipart/form-data": { - "source": "iana", - "compressible": false - }, - "multipart/header-set": { - "source": "iana" - }, - "multipart/mixed": { - "source": "iana", - "compressible": false - }, - "multipart/parallel": { - "source": "iana" - }, - "multipart/related": { - "source": "iana", - "compressible": false - }, - "multipart/report": { - "source": "iana" - }, - "multipart/signed": { - "source": "iana", - "compressible": false - }, - "multipart/voice-message": { - "source": "iana" - }, - "multipart/x-mixed-replace": { - "source": "iana" - }, - "text/1d-interleaved-parityfec": { - "source": "iana" - }, - "text/cache-manifest": { - "source": "iana", - "compressible": true, - "extensions": ["appcache","manifest"] - }, - "text/calendar": { - "source": "iana", - "extensions": ["ics","ifb"] - }, - "text/calender": { - "compressible": true - }, - "text/cmd": { - "compressible": true - }, - "text/coffeescript": { - "extensions": ["coffee","litcoffee"] - }, - "text/css": { - "source": "iana", - "compressible": true, - "extensions": ["css"] - }, - "text/csv": { - "source": "iana", - "compressible": true, - "extensions": ["csv"] - }, - "text/csv-schema": { - "source": "iana" - }, - "text/directory": { - "source": "iana" - }, - "text/dns": { - "source": "iana" - }, - "text/ecmascript": { - "source": "iana" - }, - "text/encaprtp": { - "source": "iana" - }, - "text/enriched": { - "source": "iana" - }, - "text/fwdred": { - "source": "iana" - }, - "text/grammar-ref-list": { - "source": "iana" - }, - "text/hjson": { - "extensions": ["hjson"] - }, - "text/html": { - "source": "iana", - "compressible": true, - "extensions": ["html","htm","shtml"] - }, - "text/jade": { - "extensions": ["jade"] - }, - "text/javascript": { - "source": "iana", - "compressible": true - }, - "text/jcr-cnd": { - "source": "iana" - }, - "text/jsx": { - "compressible": true, - "extensions": ["jsx"] - }, - "text/less": { - "extensions": ["less"] - }, - "text/markdown": { - "source": "iana" - }, - "text/mathml": { - "source": "nginx", - "extensions": ["mml"] - }, - "text/mizar": { - "source": "iana" - }, - "text/n3": { - "source": "iana", - "compressible": true, - "extensions": ["n3"] - }, - "text/parameters": { - "source": "iana" - }, - "text/parityfec": { - "source": "iana" - }, - "text/plain": { - "source": "iana", - "compressible": true, - "extensions": ["txt","text","conf","def","list","log","in","ini"] - }, - "text/provenance-notation": { - "source": "iana" - }, - "text/prs.fallenstein.rst": { - "source": "iana" - }, - "text/prs.lines.tag": { - "source": "iana", - "extensions": ["dsc"] - }, - "text/prs.prop.logic": { - "source": "iana" - }, - "text/raptorfec": { - "source": "iana" - }, - "text/red": { - "source": "iana" - }, - "text/rfc822-headers": { - "source": "iana" - }, - "text/richtext": { - "source": "iana", - "compressible": true, - "extensions": ["rtx"] - }, - "text/rtf": { - "source": "iana", - "compressible": true, - "extensions": ["rtf"] - }, - "text/rtp-enc-aescm128": { - "source": "iana" - }, - "text/rtploopback": { - "source": "iana" - }, - "text/rtx": { - "source": "iana" - }, - "text/sgml": { - "source": "iana", - "extensions": ["sgml","sgm"] - }, - "text/slim": { - "extensions": ["slim","slm"] - }, - "text/stylus": { - "extensions": ["stylus","styl"] - }, - "text/t140": { - "source": "iana" - }, - "text/tab-separated-values": { - "source": "iana", - "compressible": true, - "extensions": ["tsv"] - }, - "text/troff": { - "source": "iana", - "extensions": ["t","tr","roff","man","me","ms"] - }, - "text/turtle": { - "source": "iana", - "extensions": ["ttl"] - }, - "text/ulpfec": { - "source": "iana" - }, - "text/uri-list": { - "source": "iana", - "compressible": true, - "extensions": ["uri","uris","urls"] - }, - "text/vcard": { - "source": "iana", - "compressible": true, - "extensions": ["vcard"] - }, - "text/vnd.a": { - "source": "iana" - }, - "text/vnd.abc": { - "source": "iana" - }, - "text/vnd.curl": { - "source": "iana", - "extensions": ["curl"] - }, - "text/vnd.curl.dcurl": { - "source": "apache", - "extensions": ["dcurl"] - }, - "text/vnd.curl.mcurl": { - "source": "apache", - "extensions": ["mcurl"] - }, - "text/vnd.curl.scurl": { - "source": "apache", - "extensions": ["scurl"] - }, - "text/vnd.debian.copyright": { - "source": "iana" - }, - "text/vnd.dmclientscript": { - "source": "iana" - }, - "text/vnd.dvb.subtitle": { - "source": "iana", - "extensions": ["sub"] - }, - "text/vnd.esmertec.theme-descriptor": { - "source": "iana" - }, - "text/vnd.fly": { - "source": "iana", - "extensions": ["fly"] - }, - "text/vnd.fmi.flexstor": { - "source": "iana", - "extensions": ["flx"] - }, - "text/vnd.graphviz": { - "source": "iana", - "extensions": ["gv"] - }, - "text/vnd.in3d.3dml": { - "source": "iana", - "extensions": ["3dml"] - }, - "text/vnd.in3d.spot": { - "source": "iana", - "extensions": ["spot"] - }, - "text/vnd.iptc.newsml": { - "source": "iana" - }, - "text/vnd.iptc.nitf": { - "source": "iana" - }, - "text/vnd.latex-z": { - "source": "iana" - }, - "text/vnd.motorola.reflex": { - "source": "iana" - }, - "text/vnd.ms-mediapackage": { - "source": "iana" - }, - "text/vnd.net2phone.commcenter.command": { - "source": "iana" - }, - "text/vnd.radisys.msml-basic-layout": { - "source": "iana" - }, - "text/vnd.si.uricatalogue": { - "source": "iana" - }, - "text/vnd.sun.j2me.app-descriptor": { - "source": "iana", - "extensions": ["jad"] - }, - "text/vnd.trolltech.linguist": { - "source": "iana" - }, - "text/vnd.wap.si": { - "source": "iana" - }, - "text/vnd.wap.sl": { - "source": "iana" - }, - "text/vnd.wap.wml": { - "source": "iana", - "extensions": ["wml"] - }, - "text/vnd.wap.wmlscript": { - "source": "iana", - "extensions": ["wmls"] - }, - "text/vtt": { - "charset": "UTF-8", - "compressible": true, - "extensions": ["vtt"] - }, - "text/x-asm": { - "source": "apache", - "extensions": ["s","asm"] - }, - "text/x-c": { - "source": "apache", - "extensions": ["c","cc","cxx","cpp","h","hh","dic"] - }, - "text/x-component": { - "source": "nginx", - "extensions": ["htc"] - }, - "text/x-fortran": { - "source": "apache", - "extensions": ["f","for","f77","f90"] - }, - "text/x-gwt-rpc": { - "compressible": true - }, - "text/x-handlebars-template": { - "extensions": ["hbs"] - }, - "text/x-java-source": { - "source": "apache", - "extensions": ["java"] - }, - "text/x-jquery-tmpl": { - "compressible": true - }, - "text/x-lua": { - "extensions": ["lua"] - }, - "text/x-markdown": { - "compressible": true, - "extensions": ["markdown","md","mkd"] - }, - "text/x-nfo": { - "source": "apache", - "extensions": ["nfo"] - }, - "text/x-opml": { - "source": "apache", - "extensions": ["opml"] - }, - "text/x-pascal": { - "source": "apache", - "extensions": ["p","pas"] - }, - "text/x-processing": { - "compressible": true, - "extensions": ["pde"] - }, - "text/x-sass": { - "extensions": ["sass"] - }, - "text/x-scss": { - "extensions": ["scss"] - }, - "text/x-setext": { - "source": "apache", - "extensions": ["etx"] - }, - "text/x-sfv": { - "source": "apache", - "extensions": ["sfv"] - }, - "text/x-suse-ymp": { - "compressible": true, - "extensions": ["ymp"] - }, - "text/x-uuencode": { - "source": "apache", - "extensions": ["uu"] - }, - "text/x-vcalendar": { - "source": "apache", - "extensions": ["vcs"] - }, - "text/x-vcard": { - "source": "apache", - "extensions": ["vcf"] - }, - "text/xml": { - "source": "iana", - "compressible": true, - "extensions": ["xml"] - }, - "text/xml-external-parsed-entity": { - "source": "iana" - }, - "text/yaml": { - "extensions": ["yaml","yml"] - }, - "video/1d-interleaved-parityfec": { - "source": "apache" - }, - "video/3gpp": { - "source": "apache", - "extensions": ["3gp","3gpp"] - }, - "video/3gpp-tt": { - "source": "apache" - }, - "video/3gpp2": { - "source": "apache", - "extensions": ["3g2"] - }, - "video/bmpeg": { - "source": "apache" - }, - "video/bt656": { - "source": "apache" - }, - "video/celb": { - "source": "apache" - }, - "video/dv": { - "source": "apache" - }, - "video/encaprtp": { - "source": "apache" - }, - "video/h261": { - "source": "apache", - "extensions": ["h261"] - }, - "video/h263": { - "source": "apache", - "extensions": ["h263"] - }, - "video/h263-1998": { - "source": "apache" - }, - "video/h263-2000": { - "source": "apache" - }, - "video/h264": { - "source": "apache", - "extensions": ["h264"] - }, - "video/h264-rcdo": { - "source": "apache" - }, - "video/h264-svc": { - "source": "apache" - }, - "video/h265": { - "source": "apache" - }, - "video/iso.segment": { - "source": "apache" - }, - "video/jpeg": { - "source": "apache", - "extensions": ["jpgv"] - }, - "video/jpeg2000": { - "source": "apache" - }, - "video/jpm": { - "source": "apache", - "extensions": ["jpm","jpgm"] - }, - "video/mj2": { - "source": "apache", - "extensions": ["mj2","mjp2"] - }, - "video/mp1s": { - "source": "apache" - }, - "video/mp2p": { - "source": "apache" - }, - "video/mp2t": { - "source": "apache", - "extensions": ["ts"] - }, - "video/mp4": { - "source": "apache", - "compressible": false, - "extensions": ["mp4","mp4v","mpg4"] - }, - "video/mp4v-es": { - "source": "apache" - }, - "video/mpeg": { - "source": "apache", - "compressible": false, - "extensions": ["mpeg","mpg","mpe","m1v","m2v"] - }, - "video/mpeg4-generic": { - "source": "apache" - }, - "video/mpv": { - "source": "apache" - }, - "video/nv": { - "source": "apache" - }, - "video/ogg": { - "source": "apache", - "compressible": false, - "extensions": ["ogv"] - }, - "video/parityfec": { - "source": "apache" - }, - "video/pointer": { - "source": "apache" - }, - "video/quicktime": { - "source": "apache", - "compressible": false, - "extensions": ["qt","mov"] - }, - "video/raptorfec": { - "source": "apache" - }, - "video/raw": { - "source": "apache" - }, - "video/rtp-enc-aescm128": { - "source": "apache" - }, - "video/rtploopback": { - "source": "apache" - }, - "video/rtx": { - "source": "apache" - }, - "video/smpte292m": { - "source": "apache" - }, - "video/ulpfec": { - "source": "apache" - }, - "video/vc1": { - "source": "apache" - }, - "video/vnd.cctv": { - "source": "apache" - }, - "video/vnd.dece.hd": { - "source": "apache", - "extensions": ["uvh","uvvh"] - }, - "video/vnd.dece.mobile": { - "source": "apache", - "extensions": ["uvm","uvvm"] - }, - "video/vnd.dece.mp4": { - "source": "apache" - }, - "video/vnd.dece.pd": { - "source": "apache", - "extensions": ["uvp","uvvp"] - }, - "video/vnd.dece.sd": { - "source": "apache", - "extensions": ["uvs","uvvs"] - }, - "video/vnd.dece.video": { - "source": "apache", - "extensions": ["uvv","uvvv"] - }, - "video/vnd.directv.mpeg": { - "source": "apache" - }, - "video/vnd.directv.mpeg-tts": { - "source": "apache" - }, - "video/vnd.dlna.mpeg-tts": { - "source": "apache" - }, - "video/vnd.dvb.file": { - "source": "apache", - "extensions": ["dvb"] - }, - "video/vnd.fvt": { - "source": "apache", - "extensions": ["fvt"] - }, - "video/vnd.hns.video": { - "source": "apache" - }, - "video/vnd.iptvforum.1dparityfec-1010": { - "source": "apache" - }, - "video/vnd.iptvforum.1dparityfec-2005": { - "source": "apache" - }, - "video/vnd.iptvforum.2dparityfec-1010": { - "source": "apache" - }, - "video/vnd.iptvforum.2dparityfec-2005": { - "source": "apache" - }, - "video/vnd.iptvforum.ttsavc": { - "source": "apache" - }, - "video/vnd.iptvforum.ttsmpeg2": { - "source": "apache" - }, - "video/vnd.motorola.video": { - "source": "apache" - }, - "video/vnd.motorola.videop": { - "source": "apache" - }, - "video/vnd.mpegurl": { - "source": "apache", - "extensions": ["mxu","m4u"] - }, - "video/vnd.ms-playready.media.pyv": { - "source": "apache", - "extensions": ["pyv"] - }, - "video/vnd.nokia.interleaved-multimedia": { - "source": "apache" - }, - "video/vnd.nokia.videovoip": { - "source": "apache" - }, - "video/vnd.objectvideo": { - "source": "apache" - }, - "video/vnd.radgamettools.bink": { - "source": "apache" - }, - "video/vnd.radgamettools.smacker": { - "source": "apache" - }, - "video/vnd.sealed.mpeg1": { - "source": "apache" - }, - "video/vnd.sealed.mpeg4": { - "source": "apache" - }, - "video/vnd.sealed.swf": { - "source": "apache" - }, - "video/vnd.sealedmedia.softseal.mov": { - "source": "apache" - }, - "video/vnd.uvvu.mp4": { - "source": "apache", - "extensions": ["uvu","uvvu"] - }, - "video/vnd.vivo": { - "source": "apache", - "extensions": ["viv"] - }, - "video/vp8": { - "source": "apache" - }, - "video/webm": { - "source": "apache", - "compressible": false, - "extensions": ["webm"] - }, - "video/x-f4v": { - "source": "apache", - "extensions": ["f4v"] - }, - "video/x-fli": { - "source": "apache", - "extensions": ["fli"] - }, - "video/x-flv": { - "source": "apache", - "compressible": false, - "extensions": ["flv"] - }, - "video/x-m4v": { - "source": "apache", - "extensions": ["m4v"] - }, - "video/x-matroska": { - "source": "apache", - "compressible": false, - "extensions": ["mkv","mk3d","mks"] - }, - "video/x-mng": { - "source": "apache", - "extensions": ["mng"] - }, - "video/x-ms-asf": { - "source": "apache", - "extensions": ["asf","asx"] - }, - "video/x-ms-vob": { - "source": "apache", - "extensions": ["vob"] - }, - "video/x-ms-wm": { - "source": "apache", - "extensions": ["wm"] - }, - "video/x-ms-wmv": { - "source": "apache", - "compressible": false, - "extensions": ["wmv"] - }, - "video/x-ms-wmx": { - "source": "apache", - "extensions": ["wmx"] - }, - "video/x-ms-wvx": { - "source": "apache", - "extensions": ["wvx"] - }, - "video/x-msvideo": { - "source": "apache", - "extensions": ["avi"] - }, - "video/x-sgi-movie": { - "source": "apache", - "extensions": ["movie"] - }, - "video/x-smv": { - "source": "apache", - "extensions": ["smv"] - }, - "x-conference/x-cooltalk": { - "source": "apache", - "extensions": ["ice"] - }, - "x-shader/x-fragment": { - "compressible": true - }, - "x-shader/x-vertex": { - "compressible": true - } -} diff --git a/node_modules/request/node_modules/mime-types/node_modules/mime-db/index.js b/node_modules/request/node_modules/mime-types/node_modules/mime-db/index.js deleted file mode 100644 index 551031f6..00000000 --- a/node_modules/request/node_modules/mime-types/node_modules/mime-db/index.js +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * mime-db - * Copyright(c) 2014 Jonathan Ong - * MIT Licensed - */ - -/** - * Module exports. - */ - -module.exports = require('./db.json') diff --git a/node_modules/request/node_modules/mime-types/node_modules/mime-db/package.json b/node_modules/request/node_modules/mime-types/node_modules/mime-db/package.json deleted file mode 100644 index d1af1d09..00000000 --- a/node_modules/request/node_modules/mime-types/node_modules/mime-db/package.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "name": "mime-db", - "description": "Media Type Database", - "version": "1.23.0", - "contributors": [ - { - "name": "Douglas Christopher Wilson", - "email": "doug@somethingdoug.com" - }, - { - "name": "Jonathan Ong", - "email": "me@jongleberry.com", - "url": "http://jongleberry.com" - }, - { - "name": "Robert Kieffer", - "email": "robert@broofa.com", - "url": "http://github.com/broofa" - } - ], - "license": "MIT", - "keywords": [ - "mime", - "db", - "type", - "types", - "database", - "charset", - "charsets" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/jshttp/mime-db.git" - }, - "devDependencies": { - "bluebird": "3.3.5", - "co": "4.6.0", - "cogent": "1.0.1", - "csv-parse": "1.1.0", - "gnode": "0.1.2", - "istanbul": "0.4.3", - "mocha": "1.21.5", - "raw-body": "2.1.6", - "stream-to-array": "2.3.0" - }, - "files": [ - "HISTORY.md", - "LICENSE", - "README.md", - "db.json", - "index.js" - ], - "engines": { - "node": ">= 0.6" - }, - "scripts": { - "build": "node scripts/build", - "fetch": "gnode scripts/fetch-apache && gnode scripts/fetch-iana && gnode scripts/fetch-nginx", - "test": "mocha --reporter spec --bail --check-leaks test/", - "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", - "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/", - "update": "npm run fetch && npm run build" - }, - "gitHead": "ba0d99fd05b3bfdc2ebcd78f858c25cb7db6af41", - "bugs": { - "url": "https://github.com/jshttp/mime-db/issues" - }, - "homepage": "https://github.com/jshttp/mime-db#readme", - "_id": "mime-db@1.23.0", - "_shasum": "a31b4070adaea27d732ea333740a64d0ec9a6659", - "_from": "mime-db@>=1.23.0 <1.24.0", - "_npmVersion": "2.15.1", - "_nodeVersion": "4.4.3", - "_npmUser": { - "name": "dougwilson", - "email": "doug@somethingdoug.com" - }, - "dist": { - "shasum": "a31b4070adaea27d732ea333740a64d0ec9a6659", - "tarball": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" - }, - "maintainers": [ - { - "name": "dougwilson", - "email": "doug@somethingdoug.com" - }, - { - "name": "jongleberry", - "email": "jonathanrichardong@gmail.com" - } - ], - "_npmOperationalInternal": { - "host": "packages-16-east.internal.npmjs.com", - "tmp": "tmp/mime-db-1.23.0.tgz_1462163798086_0.43938886746764183" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/mime-types/package.json b/node_modules/request/node_modules/mime-types/package.json deleted file mode 100644 index 14188b80..00000000 --- a/node_modules/request/node_modules/mime-types/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "name": "mime-types", - "description": "The ultimate javascript content-type utility.", - "version": "2.1.11", - "contributors": [ - { - "name": "Douglas Christopher Wilson", - "email": "doug@somethingdoug.com" - }, - { - "name": "Jeremiah Senkpiel", - "email": "fishrock123@rocketmail.com", - "url": "https://searchbeam.jit.su" - }, - { - "name": "Jonathan Ong", - "email": "me@jongleberry.com", - "url": "http://jongleberry.com" - } - ], - "license": "MIT", - "keywords": [ - "mime", - "types" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/jshttp/mime-types.git" - }, - "dependencies": { - "mime-db": "~1.23.0" - }, - "devDependencies": { - "istanbul": "0.4.3", - "mocha": "1.21.5" - }, - "files": [ - "HISTORY.md", - "LICENSE", - "index.js" - ], - "engines": { - "node": ">= 0.6" - }, - "scripts": { - "test": "mocha --reporter spec test/test.js", - "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot test/test.js", - "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter dot test/test.js" - }, - "gitHead": "298ffcf490a5d6e60edea7bf7a69036df04846b1", - "bugs": { - "url": "https://github.com/jshttp/mime-types/issues" - }, - "homepage": "https://github.com/jshttp/mime-types#readme", - "_id": "mime-types@2.1.11", - "_shasum": "c259c471bda808a85d6cd193b430a5fae4473b3c", - "_from": "mime-types@>=2.1.7 <2.2.0", - "_npmVersion": "2.15.1", - "_nodeVersion": "4.4.3", - "_npmUser": { - "name": "dougwilson", - "email": "doug@somethingdoug.com" - }, - "dist": { - "shasum": "c259c471bda808a85d6cd193b430a5fae4473b3c", - "tarball": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz" - }, - "maintainers": [ - { - "name": "dougwilson", - "email": "doug@somethingdoug.com" - }, - { - "name": "fishrock123", - "email": "fishrock123@rocketmail.com" - }, - { - "name": "jongleberry", - "email": "jonathanrichardong@gmail.com" - } - ], - "_npmOperationalInternal": { - "host": "packages-12-west.internal.npmjs.com", - "tmp": "tmp/mime-types-2.1.11.tgz_1462165365027_0.7217204745393246" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/node-uuid/.npmignore b/node_modules/request/node_modules/node-uuid/.npmignore deleted file mode 100644 index 88861393..00000000 --- a/node_modules/request/node_modules/node-uuid/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -.DS_Store -.nyc_output -coverage diff --git a/node_modules/request/node_modules/node-uuid/LICENSE.md b/node_modules/request/node_modules/node-uuid/LICENSE.md deleted file mode 100644 index 652609b3..00000000 --- a/node_modules/request/node_modules/node-uuid/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2010-2012 Robert Kieffer - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/node_modules/request/node_modules/node-uuid/README.md b/node_modules/request/node_modules/node-uuid/README.md deleted file mode 100644 index 5cd85550..00000000 --- a/node_modules/request/node_modules/node-uuid/README.md +++ /dev/null @@ -1,254 +0,0 @@ -# node-uuid - -Simple, fast generation of [RFC4122](http://www.ietf.org/rfc/rfc4122.txt) UUIDS. - -Features: - -* Generate RFC4122 version 1 or version 4 UUIDs -* Runs in node.js and all browsers. -* Registered as a [ComponentJS](https://github.com/component/component) [component](https://github.com/component/component/wiki/Components) ('broofa/node-uuid'). -* Cryptographically strong random # generation - * `crypto.randomBytes(n)` in node.js - * `window.crypto.getRandomValues(ta)` in [supported browsers](https://developer.mozilla.org/en-US/docs/Web/API/RandomSource/getRandomValues#Browser_Compatibility) -* 1.1K minified and gzip'ed (Want something smaller? Check this [crazy shit](https://gist.github.com/982883) out! ) -* [Annotated source code](http://broofa.github.com/node-uuid/docs/uuid.html) -* Comes with a Command Line Interface for generating uuids on the command line - -## Getting Started - -Install it in your browser: - -```html - -``` - -Or in node.js: - -``` -npm install node-uuid -``` - -```javascript -var uuid = require('node-uuid'); -``` - -Then create some ids ... - -```javascript -// Generate a v1 (time-based) id -uuid.v1(); // -> '6c84fb90-12c4-11e1-840d-7b25c5ee775a' - -// Generate a v4 (random) id -uuid.v4(); // -> '110ec58a-a0f2-4ac4-8393-c866d813b8d1' -``` - -## API - -### uuid.v1([`options` [, `buffer` [, `offset`]]]) - -Generate and return a RFC4122 v1 (timestamp-based) UUID. - -* `options` - (Object) Optional uuid state to apply. Properties may include: - - * `node` - (Array) Node id as Array of 6 bytes (per 4.1.6). Default: Randomly generated ID. See note 1. - * `clockseq` - (Number between 0 - 0x3fff) RFC clock sequence. Default: An internally maintained clockseq is used. - * `msecs` - (Number | Date) Time in milliseconds since unix Epoch. Default: The current time is used. - * `nsecs` - (Number between 0-9999) additional time, in 100-nanosecond units. Ignored if `msecs` is unspecified. Default: internal uuid counter is used, as per 4.2.1.2. - -* `buffer` - (Array | Buffer) Array or buffer where UUID bytes are to be written. -* `offset` - (Number) Starting index in `buffer` at which to begin writing. - -Returns `buffer`, if specified, otherwise the string form of the UUID - -Notes: - -1. The randomly generated node id is only guaranteed to stay constant for the lifetime of the current JS runtime. (Future versions of this module may use persistent storage mechanisms to extend this guarantee.) - -Example: Generate string UUID with fully-specified options - -```javascript -uuid.v1({ - node: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab], - clockseq: 0x1234, - msecs: new Date('2011-11-01').getTime(), - nsecs: 5678 -}); // -> "710b962e-041c-11e1-9234-0123456789ab" -``` - -Example: In-place generation of two binary IDs - -```javascript -// Generate two ids in an array -var arr = new Array(32); // -> [] -uuid.v1(null, arr, 0); // -> [02 a2 ce 90 14 32 11 e1 85 58 0b 48 8e 4f c1 15] -uuid.v1(null, arr, 16); // -> [02 a2 ce 90 14 32 11 e1 85 58 0b 48 8e 4f c1 15 02 a3 1c b0 14 32 11 e1 85 58 0b 48 8e 4f c1 15] - -// Optionally use uuid.unparse() to get stringify the ids -uuid.unparse(buffer); // -> '02a2ce90-1432-11e1-8558-0b488e4fc115' -uuid.unparse(buffer, 16) // -> '02a31cb0-1432-11e1-8558-0b488e4fc115' -``` - -### uuid.v4([`options` [, `buffer` [, `offset`]]]) - -Generate and return a RFC4122 v4 UUID. - -* `options` - (Object) Optional uuid state to apply. Properties may include: - - * `random` - (Number[16]) Array of 16 numbers (0-255) to use in place of randomly generated values - * `rng` - (Function) Random # generator to use. Set to one of the built-in generators - `uuid.mathRNG` (all platforms), `uuid.nodeRNG` (node.js only), `uuid.whatwgRNG` (WebKit only) - or a custom function that returns an array[16] of byte values. - -* `buffer` - (Array | Buffer) Array or buffer where UUID bytes are to be written. -* `offset` - (Number) Starting index in `buffer` at which to begin writing. - -Returns `buffer`, if specified, otherwise the string form of the UUID - -Example: Generate string UUID with fully-specified options - -```javascript -uuid.v4({ - random: [ - 0x10, 0x91, 0x56, 0xbe, 0xc4, 0xfb, 0xc1, 0xea, - 0x71, 0xb4, 0xef, 0xe1, 0x67, 0x1c, 0x58, 0x36 - ] -}); -// -> "109156be-c4fb-41ea-b1b4-efe1671c5836" -``` - -Example: Generate two IDs in a single buffer - -```javascript -var buffer = new Array(32); // (or 'new Buffer' in node.js) -uuid.v4(null, buffer, 0); -uuid.v4(null, buffer, 16); -``` - -### uuid.parse(id[, buffer[, offset]]) -### uuid.unparse(buffer[, offset]) - -Parse and unparse UUIDs - - * `id` - (String) UUID(-like) string - * `buffer` - (Array | Buffer) Array or buffer where UUID bytes are to be written. Default: A new Array or Buffer is used - * `offset` - (Number) Starting index in `buffer` at which to begin writing. Default: 0 - -Example parsing and unparsing a UUID string - -```javascript -var bytes = uuid.parse('797ff043-11eb-11e1-80d6-510998755d10'); // -> -var string = uuid.unparse(bytes); // -> '797ff043-11eb-11e1-80d6-510998755d10' -``` - -### uuid.noConflict() - -(Browsers only) Set `uuid` property back to it's previous value. - -Returns the node-uuid object. - -Example: - -```javascript -var myUuid = uuid.noConflict(); -myUuid.v1(); // -> '6c84fb90-12c4-11e1-840d-7b25c5ee775a' -``` - -## Deprecated APIs - -Support for the following v1.2 APIs is available in v1.3, but is deprecated and will be removed in the next major version. - -### uuid([format [, buffer [, offset]]]) - -uuid() has become uuid.v4(), and the `format` argument is now implicit in the `buffer` argument. (i.e. if you specify a buffer, the format is assumed to be binary). - -### uuid.BufferClass - -The class of container created when generating binary uuid data if no buffer argument is specified. This is expected to go away, with no replacement API. - -## Command Line Interface - -To use the executable, it's probably best to install this library globally. - -`npm install -g node-uuid` - -Usage: - -``` -USAGE: uuid [version] [options] - - -options: - ---help Display this message and exit -``` - -`version` must be an RFC4122 version that is supported by this library, which is currently version 1 and version 4 (denoted by "v1" and "v4", respectively). `version` defaults to version 4 when not supplied. - -### Examples - -``` -> uuid -3a91f950-dec8-4688-ba14-5b7bbfc7a563 -``` - -``` -> uuid v1 -9d0b43e0-7696-11e3-964b-250efa37a98e -``` - -``` -> uuid v4 -6790ac7c-24ac-4f98-8464-42f6d98a53ae -``` - -## Testing - -In node.js - -``` -npm test -``` - -In Browser - -``` -open test/test.html -``` - -### Benchmarking - -Requires node.js - -``` -npm install uuid uuid-js -node benchmark/benchmark.js -``` - -For a more complete discussion of node-uuid performance, please see the `benchmark/README.md` file, and the [benchmark wiki](https://github.com/broofa/node-uuid/wiki/Benchmark) - -For browser performance [checkout the JSPerf tests](http://jsperf.com/node-uuid-performance). - -## Release notes - -### 1.4.6 - -* Properly detect node crypto and whatwg crypto -* Workaround phantomjs/browserify bug -* Explicit check for `window` rather implicit this-global -* Issue warning if Math.random() is being used -* "use strict"; -* A few jshint / stylistic updates (=== and such) - -### 1.4.0 - -* Improved module context detection -* Removed public RNG functions - -### 1.3.2 - -* Improve tests and handling of v1() options (Issue #24) -* Expose RNG option to allow for perf testing with different generators - -### 1.3.0 - -* Support for version 1 ids, thanks to [@ctavan](https://github.com/ctavan)! -* Support for node.js crypto API -* De-emphasizing performance in favor of a) cryptographic quality PRNGs where available and b) more manageable code diff --git a/node_modules/request/node_modules/node-uuid/benchmark/README.md b/node_modules/request/node_modules/node-uuid/benchmark/README.md deleted file mode 100644 index aaeb2ea0..00000000 --- a/node_modules/request/node_modules/node-uuid/benchmark/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# node-uuid Benchmarks - -### Results - -To see the results of our benchmarks visit https://github.com/broofa/node-uuid/wiki/Benchmark - -### Run them yourself - -node-uuid comes with some benchmarks to measure performance of generating UUIDs. These can be run using node.js. node-uuid is being benchmarked against some other uuid modules, that are available through npm namely `uuid` and `uuid-js`. - -To prepare and run the benchmark issue; - -``` -npm install uuid uuid-js -node benchmark/benchmark.js -``` - -You'll see an output like this one: - -``` -# v4 -nodeuuid.v4(): 854700 uuids/second -nodeuuid.v4('binary'): 788643 uuids/second -nodeuuid.v4('binary', buffer): 1336898 uuids/second -uuid(): 479386 uuids/second -uuid('binary'): 582072 uuids/second -uuidjs.create(4): 312304 uuids/second - -# v1 -nodeuuid.v1(): 938086 uuids/second -nodeuuid.v1('binary'): 683060 uuids/second -nodeuuid.v1('binary', buffer): 1644736 uuids/second -uuidjs.create(1): 190621 uuids/second -``` - -* The `uuid()` entries are for Nikhil Marathe's [uuid module](https://bitbucket.org/nikhilm/uuidjs) which is a wrapper around the native libuuid library. -* The `uuidjs()` entries are for Patrick Negri's [uuid-js module](https://github.com/pnegri/uuid-js) which is a pure javascript implementation based on [UUID.js](https://github.com/LiosK/UUID.js) by LiosK. - -If you want to get more reliable results you can run the benchmark multiple times and write the output into a log file: - -``` -for i in {0..9}; do node benchmark/benchmark.js >> benchmark/bench_0.4.12.log; done; -``` - -If you're interested in how performance varies between different node versions, you can issue the above command multiple times. - -You can then use the shell script `bench.sh` provided in this directory to calculate the averages over all benchmark runs and draw a nice plot: - -``` -(cd benchmark/ && ./bench.sh) -``` - -This assumes you have [gnuplot](http://www.gnuplot.info/) and [ImageMagick](http://www.imagemagick.org/) installed. You'll find a nice `bench.png` graph in the `benchmark/` directory then. diff --git a/node_modules/request/node_modules/node-uuid/benchmark/bench.gnu b/node_modules/request/node_modules/node-uuid/benchmark/bench.gnu deleted file mode 100644 index a342fbbe..00000000 --- a/node_modules/request/node_modules/node-uuid/benchmark/bench.gnu +++ /dev/null @@ -1,174 +0,0 @@ -#!/opt/local/bin/gnuplot -persist -# -# -# G N U P L O T -# Version 4.4 patchlevel 3 -# last modified March 2011 -# System: Darwin 10.8.0 -# -# Copyright (C) 1986-1993, 1998, 2004, 2007-2010 -# Thomas Williams, Colin Kelley and many others -# -# gnuplot home: http://www.gnuplot.info -# faq, bugs, etc: type "help seeking-assistance" -# immediate help: type "help" -# plot window: hit 'h' -set terminal postscript eps noenhanced defaultplex \ - leveldefault color colortext \ - solid linewidth 1.2 butt noclip \ - palfuncparam 2000,0.003 \ - "Helvetica" 14 -set output 'bench.eps' -unset clip points -set clip one -unset clip two -set bar 1.000000 front -set border 31 front linetype -1 linewidth 1.000 -set xdata -set ydata -set zdata -set x2data -set y2data -set timefmt x "%d/%m/%y,%H:%M" -set timefmt y "%d/%m/%y,%H:%M" -set timefmt z "%d/%m/%y,%H:%M" -set timefmt x2 "%d/%m/%y,%H:%M" -set timefmt y2 "%d/%m/%y,%H:%M" -set timefmt cb "%d/%m/%y,%H:%M" -set boxwidth -set style fill empty border -set style rectangle back fc lt -3 fillstyle solid 1.00 border lt -1 -set style circle radius graph 0.02, first 0, 0 -set dummy x,y -set format x "% g" -set format y "% g" -set format x2 "% g" -set format y2 "% g" -set format z "% g" -set format cb "% g" -set angles radians -unset grid -set key title "" -set key outside left top horizontal Right noreverse enhanced autotitles columnhead nobox -set key noinvert samplen 4 spacing 1 width 0 height 0 -set key maxcolumns 2 maxrows 0 -unset label -unset arrow -set style increment default -unset style line -set style line 1 linetype 1 linewidth 2.000 pointtype 1 pointsize default pointinterval 0 -unset style arrow -set style histogram clustered gap 2 title offset character 0, 0, 0 -unset logscale -set offsets graph 0.05, 0.15, 0, 0 -set pointsize 1.5 -set pointintervalbox 1 -set encoding default -unset polar -unset parametric -unset decimalsign -set view 60, 30, 1, 1 -set samples 100, 100 -set isosamples 10, 10 -set surface -unset contour -set clabel '%8.3g' -set mapping cartesian -set datafile separator whitespace -unset hidden3d -set cntrparam order 4 -set cntrparam linear -set cntrparam levels auto 5 -set cntrparam points 5 -set size ratio 0 1,1 -set origin 0,0 -set style data points -set style function lines -set xzeroaxis linetype -2 linewidth 1.000 -set yzeroaxis linetype -2 linewidth 1.000 -set zzeroaxis linetype -2 linewidth 1.000 -set x2zeroaxis linetype -2 linewidth 1.000 -set y2zeroaxis linetype -2 linewidth 1.000 -set ticslevel 0.5 -set mxtics default -set mytics default -set mztics default -set mx2tics default -set my2tics default -set mcbtics default -set xtics border in scale 1,0.5 mirror norotate offset character 0, 0, 0 -set xtics norangelimit -set xtics () -set ytics border in scale 1,0.5 mirror norotate offset character 0, 0, 0 -set ytics autofreq norangelimit -set ztics border in scale 1,0.5 nomirror norotate offset character 0, 0, 0 -set ztics autofreq norangelimit -set nox2tics -set noy2tics -set cbtics border in scale 1,0.5 mirror norotate offset character 0, 0, 0 -set cbtics autofreq norangelimit -set title "" -set title offset character 0, 0, 0 font "" norotate -set timestamp bottom -set timestamp "" -set timestamp offset character 0, 0, 0 font "" norotate -set rrange [ * : * ] noreverse nowriteback # (currently [8.98847e+307:-8.98847e+307] ) -set autoscale rfixmin -set autoscale rfixmax -set trange [ * : * ] noreverse nowriteback # (currently [-5.00000:5.00000] ) -set autoscale tfixmin -set autoscale tfixmax -set urange [ * : * ] noreverse nowriteback # (currently [-10.0000:10.0000] ) -set autoscale ufixmin -set autoscale ufixmax -set vrange [ * : * ] noreverse nowriteback # (currently [-10.0000:10.0000] ) -set autoscale vfixmin -set autoscale vfixmax -set xlabel "" -set xlabel offset character 0, 0, 0 font "" textcolor lt -1 norotate -set x2label "" -set x2label offset character 0, 0, 0 font "" textcolor lt -1 norotate -set xrange [ * : * ] noreverse nowriteback # (currently [-0.150000:3.15000] ) -set autoscale xfixmin -set autoscale xfixmax -set x2range [ * : * ] noreverse nowriteback # (currently [0.00000:3.00000] ) -set autoscale x2fixmin -set autoscale x2fixmax -set ylabel "" -set ylabel offset character 0, 0, 0 font "" textcolor lt -1 rotate by -270 -set y2label "" -set y2label offset character 0, 0, 0 font "" textcolor lt -1 rotate by -270 -set yrange [ 0.00000 : 1.90000e+06 ] noreverse nowriteback # (currently [:] ) -set autoscale yfixmin -set autoscale yfixmax -set y2range [ * : * ] noreverse nowriteback # (currently [0.00000:1.90000e+06] ) -set autoscale y2fixmin -set autoscale y2fixmax -set zlabel "" -set zlabel offset character 0, 0, 0 font "" textcolor lt -1 norotate -set zrange [ * : * ] noreverse nowriteback # (currently [-10.0000:10.0000] ) -set autoscale zfixmin -set autoscale zfixmax -set cblabel "" -set cblabel offset character 0, 0, 0 font "" textcolor lt -1 rotate by -270 -set cbrange [ * : * ] noreverse nowriteback # (currently [8.98847e+307:-8.98847e+307] ) -set autoscale cbfixmin -set autoscale cbfixmax -set zero 1e-08 -set lmargin -1 -set bmargin -1 -set rmargin -1 -set tmargin -1 -set pm3d explicit at s -set pm3d scansautomatic -set pm3d interpolate 1,1 flush begin noftriangles nohidden3d corners2color mean -set palette positive nops_allcF maxcolors 0 gamma 1.5 color model RGB -set palette rgbformulae 7, 5, 15 -set colorbox default -set colorbox vertical origin screen 0.9, 0.2, 0 size screen 0.05, 0.6, 0 front bdefault -set loadpath -set fontpath -set fit noerrorvariables -GNUTERM = "aqua" -plot 'bench_results.txt' using 2:xticlabel(1) w lp lw 2, '' using 3:xticlabel(1) w lp lw 2, '' using 4:xticlabel(1) w lp lw 2, '' using 5:xticlabel(1) w lp lw 2, '' using 6:xticlabel(1) w lp lw 2, '' using 7:xticlabel(1) w lp lw 2, '' using 8:xticlabel(1) w lp lw 2, '' using 9:xticlabel(1) w lp lw 2 -# EOF diff --git a/node_modules/request/node_modules/node-uuid/benchmark/bench.sh b/node_modules/request/node_modules/node-uuid/benchmark/bench.sh deleted file mode 100755 index d870a0cb..00000000 --- a/node_modules/request/node_modules/node-uuid/benchmark/bench.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -# for a given node version run: -# for i in {0..9}; do node benchmark.js >> bench_0.6.2.log; done; - -PATTERNS=('nodeuuid.v1()' "nodeuuid.v1('binary'," 'nodeuuid.v4()' "nodeuuid.v4('binary'," "uuid()" "uuid('binary')" 'uuidjs.create(1)' 'uuidjs.create(4)' '140byte') -FILES=(node_uuid_v1_string node_uuid_v1_buf node_uuid_v4_string node_uuid_v4_buf libuuid_v4_string libuuid_v4_binary uuidjs_v1_string uuidjs_v4_string 140byte_es) -INDICES=(2 3 2 3 2 2 2 2 2) -VERSIONS=$( ls bench_*.log | sed -e 's/^bench_\([0-9\.]*\)\.log/\1/' | tr "\\n" " " ) -TMPJOIN="tmp_join" -OUTPUT="bench_results.txt" - -for I in ${!FILES[*]}; do - F=${FILES[$I]} - P=${PATTERNS[$I]} - INDEX=${INDICES[$I]} - echo "version $F" > $F - for V in $VERSIONS; do - (VAL=$( grep "$P" bench_$V.log | LC_ALL=en_US awk '{ sum += $'$INDEX' } END { print sum/NR }' ); echo $V $VAL) >> $F - done - if [ $I == 0 ]; then - cat $F > $TMPJOIN - else - join $TMPJOIN $F > $OUTPUT - cp $OUTPUT $TMPJOIN - fi - rm $F -done - -rm $TMPJOIN - -gnuplot bench.gnu -convert -density 200 -resize 800x560 -flatten bench.eps bench.png -rm bench.eps diff --git a/node_modules/request/node_modules/node-uuid/benchmark/benchmark-native.c b/node_modules/request/node_modules/node-uuid/benchmark/benchmark-native.c deleted file mode 100644 index dbfc75f6..00000000 --- a/node_modules/request/node_modules/node-uuid/benchmark/benchmark-native.c +++ /dev/null @@ -1,34 +0,0 @@ -/* -Test performance of native C UUID generation - -To Compile: cc -luuid benchmark-native.c -o benchmark-native -*/ - -#include -#include -#include -#include - -int main() { - uuid_t myid; - char buf[36+1]; - int i; - struct timeval t; - double start, finish; - - gettimeofday(&t, NULL); - start = t.tv_sec + t.tv_usec/1e6; - - int n = 2e5; - for (i = 0; i < n; i++) { - uuid_generate(myid); - uuid_unparse(myid, buf); - } - - gettimeofday(&t, NULL); - finish = t.tv_sec + t.tv_usec/1e6; - double dur = finish - start; - - printf("%d uuids/sec", (int)(n/dur)); - return 0; -} diff --git a/node_modules/request/node_modules/node-uuid/benchmark/benchmark.js b/node_modules/request/node_modules/node-uuid/benchmark/benchmark.js deleted file mode 100644 index 40e6efbe..00000000 --- a/node_modules/request/node_modules/node-uuid/benchmark/benchmark.js +++ /dev/null @@ -1,84 +0,0 @@ -try { - var nodeuuid = require('../uuid'); -} catch (e) { - console.error('node-uuid require failed - skipping tests'); -} - -try { - var uuid = require('uuid'); -} catch (e) { - console.error('uuid require failed - skipping tests'); -} - -try { - var uuidjs = require('uuid-js'); -} catch (e) { - console.error('uuid-js require failed - skipping tests'); -} - -var N = 5e5; - -function rate(msg, t) { - console.log(msg + ': ' + - (N / (Date.now() - t) * 1e3 | 0) + - ' uuids/second'); -} - -console.log('# v4'); - -// node-uuid - string form -if (nodeuuid) { - for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v4(); - rate('nodeuuid.v4() - using node.js crypto RNG', t); - - for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v4({rng: nodeuuid.mathRNG}); - rate('nodeuuid.v4() - using Math.random() RNG', t); - - for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v4('binary'); - rate('nodeuuid.v4(\'binary\')', t); - - var buffer = new nodeuuid.BufferClass(16); - for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v4('binary', buffer); - rate('nodeuuid.v4(\'binary\', buffer)', t); -} - -// libuuid - string form -if (uuid) { - for (var i = 0, t = Date.now(); i < N; i++) uuid(); - rate('uuid()', t); - - for (var i = 0, t = Date.now(); i < N; i++) uuid('binary'); - rate('uuid(\'binary\')', t); -} - -// uuid-js - string form -if (uuidjs) { - for (var i = 0, t = Date.now(); i < N; i++) uuidjs.create(4); - rate('uuidjs.create(4)', t); -} - -// 140byte.es -for (var i = 0, t = Date.now(); i < N; i++) 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(s,r){r=Math.random()*16|0;return (s=='x'?r:r&0x3|0x8).toString(16)}); -rate('140byte.es_v4', t); - -console.log(''); -console.log('# v1'); - -// node-uuid - v1 string form -if (nodeuuid) { - for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v1(); - rate('nodeuuid.v1()', t); - - for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v1('binary'); - rate('nodeuuid.v1(\'binary\')', t); - - var buffer = new nodeuuid.BufferClass(16); - for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v1('binary', buffer); - rate('nodeuuid.v1(\'binary\', buffer)', t); -} - -// uuid-js - v1 string form -if (uuidjs) { - for (var i = 0, t = Date.now(); i < N; i++) uuidjs.create(1); - rate('uuidjs.create(1)', t); -} diff --git a/node_modules/request/node_modules/node-uuid/bin/uuid b/node_modules/request/node_modules/node-uuid/bin/uuid deleted file mode 100755 index f732e991..00000000 --- a/node_modules/request/node_modules/node-uuid/bin/uuid +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env node - -var path = require('path'); -var uuid = require(path.join(__dirname, '..')); - -var arg = process.argv[2]; - -if ('--help' === arg) { - console.log('\n USAGE: uuid [version] [options]\n\n'); - console.log(' options:\n'); - console.log(' --help Display this message and exit\n'); - process.exit(0); -} - -if (null == arg) { - console.log(uuid()); - process.exit(0); -} - -if ('v1' !== arg && 'v4' !== arg) { - console.error('Version must be RFC4122 version 1 or version 4, denoted as "v1" or "v4"'); - process.exit(1); -} - -console.log(uuid[arg]()); -process.exit(0); diff --git a/node_modules/request/node_modules/node-uuid/bower.json b/node_modules/request/node_modules/node-uuid/bower.json deleted file mode 100644 index c0925e19..00000000 --- a/node_modules/request/node_modules/node-uuid/bower.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "node-uuid", - "version": "1.4.7", - "homepage": "https://github.com/broofa/node-uuid", - "authors": [ - "Robert Kieffer " - ], - "description": "Rigorous implementation of RFC4122 (v1 and v4) UUIDs.", - "main": "uuid.js", - "keywords": [ - "uuid", - "gid", - "rfc4122" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ] -} diff --git a/node_modules/request/node_modules/node-uuid/component.json b/node_modules/request/node_modules/node-uuid/component.json deleted file mode 100644 index 3ff46336..00000000 --- a/node_modules/request/node_modules/node-uuid/component.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "node-uuid", - "repo": "broofa/node-uuid", - "description": "Rigorous implementation of RFC4122 (v1 and v4) UUIDs.", - "version": "1.4.7", - "author": "Robert Kieffer ", - "contributors": [ - { - "name": "Christoph Tavan ", - "github": "https://github.com/ctavan" - } - ], - "keywords": [ - "uuid", - "guid", - "rfc4122" - ], - "dependencies": {}, - "development": {}, - "main": "uuid.js", - "scripts": [ - "uuid.js" - ], - "license": "MIT" -} \ No newline at end of file diff --git a/node_modules/request/node_modules/node-uuid/package.json b/node_modules/request/node_modules/node-uuid/package.json deleted file mode 100644 index 2f578874..00000000 --- a/node_modules/request/node_modules/node-uuid/package.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "author": { - "name": "Robert Kieffer", - "email": "robert@broofa.com" - }, - "bin": { - "uuid": "./bin/uuid" - }, - "bugs": { - "url": "https://github.com/broofa/node-uuid/issues" - }, - "contributors": [ - { - "name": "AJ ONeal", - "email": "coolaj86@gmail.com" - }, - { - "name": "Christoph Tavan", - "email": "dev@tavan.de" - } - ], - "dependencies": {}, - "description": "Rigorous implementation of RFC4122 (v1 and v4) UUIDs.", - "devDependencies": { - "nyc": "^2.2.0" - }, - "directories": {}, - "homepage": "https://github.com/broofa/node-uuid", - "installable": true, - "keywords": [ - "guid", - "rfc4122", - "uuid" - ], - "lib": ".", - "licenses": [ - { - "type": "MIT", - "url": "https://raw.github.com/broofa/node-uuid/master/LICENSE.md" - } - ], - "main": "./uuid.js", - "maintainers": [ - { - "name": "broofa", - "email": "robert@broofa.com" - }, - { - "name": "coolaj86", - "email": "coolaj86@gmail.com" - } - ], - "name": "node-uuid", - "optionalDependencies": {}, - "repository": { - "type": "git", - "url": "git+https://github.com/broofa/node-uuid.git" - }, - "scripts": { - "coverage": "nyc npm test && nyc report", - "test": "node test/test.js" - }, - "url": "http://github.com/broofa/node-uuid", - "version": "1.4.7", - "gitHead": "309512573ec1c60143c257157479a20f7f1f51cd", - "_id": "node-uuid@1.4.7", - "_shasum": "6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f", - "_from": "node-uuid@>=1.4.7 <1.5.0", - "_npmVersion": "3.3.6", - "_nodeVersion": "5.0.0", - "_npmUser": { - "name": "coolaj86", - "email": "coolaj86@gmail.com" - }, - "dist": { - "shasum": "6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f", - "tarball": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" - }, - "_resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/node-uuid/test/compare_v1.js b/node_modules/request/node_modules/node-uuid/test/compare_v1.js deleted file mode 100644 index 05af8221..00000000 --- a/node_modules/request/node_modules/node-uuid/test/compare_v1.js +++ /dev/null @@ -1,63 +0,0 @@ -var assert = require('assert'), - nodeuuid = require('../uuid'), - uuidjs = require('uuid-js'), - libuuid = require('uuid').generate, - util = require('util'), - exec = require('child_process').exec, - os = require('os'); - -// On Mac Os X / macports there's only the ossp-uuid package that provides uuid -// On Linux there's uuid-runtime which provides uuidgen -var uuidCmd = os.type() === 'Darwin' ? 'uuid -1' : 'uuidgen -t'; - -function compare(ids) { - console.log(ids); - for (var i = 0; i < ids.length; i++) { - var id = ids[i].split('-'); - id = [id[2], id[1], id[0]].join(''); - ids[i] = id; - } - var sorted = ([].concat(ids)).sort(); - - if (sorted.toString() !== ids.toString()) { - console.log('Warning: sorted !== ids'); - } else { - console.log('everything in order!'); - } -} - -// Test time order of v1 uuids -var ids = []; -while (ids.length < 10e3) ids.push(nodeuuid.v1()); - -var max = 10; -console.log('node-uuid:'); -ids = []; -for (var i = 0; i < max; i++) ids.push(nodeuuid.v1()); -compare(ids); - -console.log(''); -console.log('uuidjs:'); -ids = []; -for (var i = 0; i < max; i++) ids.push(uuidjs.create(1).toString()); -compare(ids); - -console.log(''); -console.log('libuuid:'); -ids = []; -var count = 0; -var last = function() { - compare(ids); -} -var cb = function(err, stdout, stderr) { - ids.push(stdout.substring(0, stdout.length-1)); - count++; - if (count < max) { - return next(); - } - last(); -}; -var next = function() { - exec(uuidCmd, cb); -}; -next(); diff --git a/node_modules/request/node_modules/node-uuid/test/test.html b/node_modules/request/node_modules/node-uuid/test/test.html deleted file mode 100644 index d80326ec..00000000 --- a/node_modules/request/node_modules/node-uuid/test/test.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/node_modules/request/node_modules/node-uuid/test/test.js b/node_modules/request/node_modules/node-uuid/test/test.js deleted file mode 100644 index 5f1113d8..00000000 --- a/node_modules/request/node_modules/node-uuid/test/test.js +++ /dev/null @@ -1,231 +0,0 @@ -if (!this.uuid) { - // node.js - uuid = require('../uuid'); - if (!/_rb/.test(uuid._rng.toString())) { - throw new Error("should use crypto for node.js"); - } -} - -// -// x-platform log/assert shims -// - -function _log(msg, type) { - type = type || 'log'; - - if (typeof(document) != 'undefined') { - document.write('
' + msg.replace(/\n/g, '
') + '
'); - } - if (typeof(console) != 'undefined') { - var color = { - log: '\033[39m', - warn: '\033[33m', - error: '\033[31m' - }; - console[type](color[type] + msg + color.log); - } -} - -function log(msg) {_log(msg, 'log');} -function warn(msg) {_log(msg, 'warn');} -function error(msg) {_log(msg, 'error');} - -function assert(res, msg) { - if (!res) { - error('FAIL: ' + msg); - } else { - log('Pass: ' + msg); - } -} - -// -// Unit tests -// - -// Verify ordering of v1 ids created with explicit times -var TIME = 1321644961388; // 2011-11-18 11:36:01.388-08:00 - -function compare(name, ids) { - ids = ids.map(function(id) { - return id.split('-').reverse().join('-'); - }).sort(); - var sorted = ([].concat(ids)).sort(); - - assert(sorted.toString() == ids.toString(), name + ' have expected order'); -} - -// Verify ordering of v1 ids created using default behavior -compare('uuids with current time', [ - uuid.v1(), - uuid.v1(), - uuid.v1(), - uuid.v1(), - uuid.v1() -]); - -// Verify ordering of v1 ids created with explicit times -compare('uuids with time option', [ - uuid.v1({msecs: TIME - 10*3600*1000}), - uuid.v1({msecs: TIME - 1}), - uuid.v1({msecs: TIME}), - uuid.v1({msecs: TIME + 1}), - uuid.v1({msecs: TIME + 28*24*3600*1000}) -]); - -assert( - uuid.v1({msecs: TIME}) != uuid.v1({msecs: TIME}), - 'IDs created at same msec are different' -); - -// Verify throw if too many ids created -var thrown = false; -try { - uuid.v1({msecs: TIME, nsecs: 10000}); -} catch (e) { - thrown = true; -} -assert(thrown, 'Exception thrown when > 10K ids created in 1 ms'); - -// Verify clock regression bumps clockseq -var uidt = uuid.v1({msecs: TIME}); -var uidtb = uuid.v1({msecs: TIME - 1}); -assert( - parseInt(uidtb.split('-')[3], 16) - parseInt(uidt.split('-')[3], 16) === 1, - 'Clock regression by msec increments the clockseq' -); - -// Verify clock regression bumps clockseq -var uidtn = uuid.v1({msecs: TIME, nsecs: 10}); -var uidtnb = uuid.v1({msecs: TIME, nsecs: 9}); -assert( - parseInt(uidtnb.split('-')[3], 16) - parseInt(uidtn.split('-')[3], 16) === 1, - 'Clock regression by nsec increments the clockseq' -); - -// Verify explicit options produce expected id -var id = uuid.v1({ - msecs: 1321651533573, - nsecs: 5432, - clockseq: 0x385c, - node: [ 0x61, 0xcd, 0x3c, 0xbb, 0x32, 0x10 ] -}); -assert(id == 'd9428888-122b-11e1-b85c-61cd3cbb3210', 'Explicit options produce expected id'); - -// Verify adjacent ids across a msec boundary are 1 time unit apart -var u0 = uuid.v1({msecs: TIME, nsecs: 9999}); -var u1 = uuid.v1({msecs: TIME + 1, nsecs: 0}); - -var before = u0.split('-')[0], after = u1.split('-')[0]; -var dt = parseInt(after, 16) - parseInt(before, 16); -assert(dt === 1, 'Ids spanning 1ms boundary are 100ns apart'); - -// -// Test parse/unparse -// - -id = '00112233445566778899aabbccddeeff'; -assert(uuid.unparse(uuid.parse(id.substr(0,10))) == - '00112233-4400-0000-0000-000000000000', 'Short parse'); -assert(uuid.unparse(uuid.parse('(this is the uuid -> ' + id + id)) == - '00112233-4455-6677-8899-aabbccddeeff', 'Dirty parse'); - -// -// Perf tests -// - -var generators = { - v1: uuid.v1, - v4: uuid.v4 -}; - -var UUID_FORMAT = { - v1: /[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i, - v4: /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i -}; - -var N = 1e4; - -// Get %'age an actual value differs from the ideal value -function divergence(actual, ideal) { - return Math.round(100*100*(actual - ideal)/ideal)/100; -} - -function rate(msg, t) { - log(msg + ': ' + (N / (Date.now() - t) * 1e3 | 0) + ' uuids\/second'); -} - -for (var version in generators) { - var counts = {}, max = 0; - var generator = generators[version]; - var format = UUID_FORMAT[version]; - - log('\nSanity check ' + N + ' ' + version + ' uuids'); - for (var i = 0, ok = 0; i < N; i++) { - id = generator(); - if (!format.test(id)) { - throw Error(id + ' is not a valid UUID string'); - } - - if (id != uuid.unparse(uuid.parse(id))) { - assert(fail, id + ' is not a valid id'); - } - - // Count digits for our randomness check - if (version == 'v4') { - var digits = id.replace(/-/g, '').split(''); - for (var j = digits.length-1; j >= 0; j--) { - var c = digits[j]; - max = Math.max(max, counts[c] = (counts[c] || 0) + 1); - } - } - } - - // Check randomness for v4 UUIDs - if (version == 'v4') { - // Limit that we get worried about randomness. (Purely empirical choice, this!) - var limit = 2*100*Math.sqrt(1/N); - - log('\nChecking v4 randomness. Distribution of Hex Digits (% deviation from ideal)'); - - for (var i = 0; i < 16; i++) { - var c = i.toString(16); - var bar = '', n = counts[c], p = Math.round(n/max*100|0); - - // 1-3,5-8, and D-F: 1:16 odds over 30 digits - var ideal = N*30/16; - if (i == 4) { - // 4: 1:1 odds on 1 digit, plus 1:16 odds on 30 digits - ideal = N*(1 + 30/16); - } else if (i >= 8 && i <= 11) { - // 8-B: 1:4 odds on 1 digit, plus 1:16 odds on 30 digits - ideal = N*(1/4 + 30/16); - } else { - // Otherwise: 1:16 odds on 30 digits - ideal = N*30/16; - } - var d = divergence(n, ideal); - - // Draw bar using UTF squares (just for grins) - var s = n/max*50 | 0; - while (s--) bar += '='; - - assert(Math.abs(d) < limit, c + ' |' + bar + '| ' + counts[c] + ' (' + d + '% < ' + limit + '%)'); - } - } -} - -// Perf tests -for (var version in generators) { - log('\nPerformance testing ' + version + ' UUIDs'); - var generator = generators[version]; - var buf = new uuid.BufferClass(16); - - for (var i = 0, t = Date.now(); i < N; i++) generator(); - rate('uuid.' + version + '()', t); - - for (var i = 0, t = Date.now(); i < N; i++) generator('binary'); - rate('uuid.' + version + '(\'binary\')', t); - - for (var i = 0, t = Date.now(); i < N; i++) generator('binary', buf); - rate('uuid.' + version + '(\'binary\', buffer)', t); -} diff --git a/node_modules/request/node_modules/node-uuid/uuid.js b/node_modules/request/node_modules/node-uuid/uuid.js deleted file mode 100644 index 89c5b8fb..00000000 --- a/node_modules/request/node_modules/node-uuid/uuid.js +++ /dev/null @@ -1,272 +0,0 @@ -// uuid.js -// -// Copyright (c) 2010-2012 Robert Kieffer -// MIT License - http://opensource.org/licenses/mit-license.php - -/*global window, require, define */ -(function(_window) { - 'use strict'; - - // Unique ID creation requires a high quality random # generator. We feature - // detect to determine the best RNG source, normalizing to a function that - // returns 128-bits of randomness, since that's what's usually required - var _rng, _mathRNG, _nodeRNG, _whatwgRNG, _previousRoot; - - function setupBrowser() { - // Allow for MSIE11 msCrypto - var _crypto = _window.crypto || _window.msCrypto; - - if (!_rng && _crypto && _crypto.getRandomValues) { - // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto - // - // Moderately fast, high quality - try { - var _rnds8 = new Uint8Array(16); - _whatwgRNG = _rng = function whatwgRNG() { - _crypto.getRandomValues(_rnds8); - return _rnds8; - }; - _rng(); - } catch(e) {} - } - - if (!_rng) { - // Math.random()-based (RNG) - // - // If all else fails, use Math.random(). It's fast, but is of unspecified - // quality. - var _rnds = new Array(16); - _mathRNG = _rng = function() { - for (var i = 0, r; i < 16; i++) { - if ((i & 0x03) === 0) { r = Math.random() * 0x100000000; } - _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff; - } - - return _rnds; - }; - if ('undefined' !== typeof console && console.warn) { - console.warn("[SECURITY] node-uuid: crypto not usable, falling back to insecure Math.random()"); - } - } - } - - function setupNode() { - // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html - // - // Moderately fast, high quality - if ('function' === typeof require) { - try { - var _rb = require('crypto').randomBytes; - _nodeRNG = _rng = _rb && function() {return _rb(16);}; - _rng(); - } catch(e) {} - } - } - - if (_window) { - setupBrowser(); - } else { - setupNode(); - } - - // Buffer class to use - var BufferClass = ('function' === typeof Buffer) ? Buffer : Array; - - // Maps for number <-> hex string conversion - var _byteToHex = []; - var _hexToByte = {}; - for (var i = 0; i < 256; i++) { - _byteToHex[i] = (i + 0x100).toString(16).substr(1); - _hexToByte[_byteToHex[i]] = i; - } - - // **`parse()` - Parse a UUID into it's component bytes** - function parse(s, buf, offset) { - var i = (buf && offset) || 0, ii = 0; - - buf = buf || []; - s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { - if (ii < 16) { // Don't overflow! - buf[i + ii++] = _hexToByte[oct]; - } - }); - - // Zero out remaining bytes if string was short - while (ii < 16) { - buf[i + ii++] = 0; - } - - return buf; - } - - // **`unparse()` - Convert UUID byte array (ala parse()) into a string** - function unparse(buf, offset) { - var i = offset || 0, bth = _byteToHex; - return bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]]; - } - - // **`v1()` - Generate time-based UUID** - // - // Inspired by https://github.com/LiosK/UUID.js - // and http://docs.python.org/library/uuid.html - - // random #'s we need to init node and clockseq - var _seedBytes = _rng(); - - // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) - var _nodeId = [ - _seedBytes[0] | 0x01, - _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] - ]; - - // Per 4.2.2, randomize (14 bit) clockseq - var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; - - // Previous uuid creation time - var _lastMSecs = 0, _lastNSecs = 0; - - // See https://github.com/broofa/node-uuid for API details - function v1(options, buf, offset) { - var i = buf && offset || 0; - var b = buf || []; - - options = options || {}; - - var clockseq = (options.clockseq != null) ? options.clockseq : _clockseq; - - // UUID timestamps are 100 nano-second units since the Gregorian epoch, - // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so - // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' - // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. - var msecs = (options.msecs != null) ? options.msecs : new Date().getTime(); - - // Per 4.2.1.2, use count of uuid's generated during the current clock - // cycle to simulate higher resolution clock - var nsecs = (options.nsecs != null) ? options.nsecs : _lastNSecs + 1; - - // Time since last uuid creation (in msecs) - var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; - - // Per 4.2.1.2, Bump clockseq on clock regression - if (dt < 0 && options.clockseq == null) { - clockseq = clockseq + 1 & 0x3fff; - } - - // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new - // time interval - if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) { - nsecs = 0; - } - - // Per 4.2.1.2 Throw error if too many uuids are requested - if (nsecs >= 10000) { - throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); - } - - _lastMSecs = msecs; - _lastNSecs = nsecs; - _clockseq = clockseq; - - // Per 4.1.4 - Convert from unix epoch to Gregorian epoch - msecs += 12219292800000; - - // `time_low` - var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; - b[i++] = tl >>> 24 & 0xff; - b[i++] = tl >>> 16 & 0xff; - b[i++] = tl >>> 8 & 0xff; - b[i++] = tl & 0xff; - - // `time_mid` - var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; - b[i++] = tmh >>> 8 & 0xff; - b[i++] = tmh & 0xff; - - // `time_high_and_version` - b[i++] = tmh >>> 24 & 0xf | 0x10; // include version - b[i++] = tmh >>> 16 & 0xff; - - // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) - b[i++] = clockseq >>> 8 | 0x80; - - // `clock_seq_low` - b[i++] = clockseq & 0xff; - - // `node` - var node = options.node || _nodeId; - for (var n = 0; n < 6; n++) { - b[i + n] = node[n]; - } - - return buf ? buf : unparse(b); - } - - // **`v4()` - Generate random UUID** - - // See https://github.com/broofa/node-uuid for API details - function v4(options, buf, offset) { - // Deprecated - 'format' argument, as supported in v1.2 - var i = buf && offset || 0; - - if (typeof(options) === 'string') { - buf = (options === 'binary') ? new BufferClass(16) : null; - options = null; - } - options = options || {}; - - var rnds = options.random || (options.rng || _rng)(); - - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - rnds[6] = (rnds[6] & 0x0f) | 0x40; - rnds[8] = (rnds[8] & 0x3f) | 0x80; - - // Copy bytes to buffer, if provided - if (buf) { - for (var ii = 0; ii < 16; ii++) { - buf[i + ii] = rnds[ii]; - } - } - - return buf || unparse(rnds); - } - - // Export public API - var uuid = v4; - uuid.v1 = v1; - uuid.v4 = v4; - uuid.parse = parse; - uuid.unparse = unparse; - uuid.BufferClass = BufferClass; - uuid._rng = _rng; - uuid._mathRNG = _mathRNG; - uuid._nodeRNG = _nodeRNG; - uuid._whatwgRNG = _whatwgRNG; - - if (('undefined' !== typeof module) && module.exports) { - // Publish as node.js module - module.exports = uuid; - } else if (typeof define === 'function' && define.amd) { - // Publish as AMD module - define(function() {return uuid;}); - - - } else { - // Publish as global (in browsers) - _previousRoot = _window.uuid; - - // **`noConflict()` - (browser only) to reset global 'uuid' var** - uuid.noConflict = function() { - _window.uuid = _previousRoot; - return uuid; - }; - - _window.uuid = uuid; - } -})('undefined' !== typeof window ? window : null); diff --git a/node_modules/request/node_modules/oauth-sign/LICENSE b/node_modules/request/node_modules/oauth-sign/LICENSE deleted file mode 100644 index a4a9aee0..00000000 --- a/node_modules/request/node_modules/oauth-sign/LICENSE +++ /dev/null @@ -1,55 +0,0 @@ -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and - -You must cause any modified files to carry prominent notices stating that You changed the files; and - -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/node_modules/request/node_modules/oauth-sign/README.md b/node_modules/request/node_modules/oauth-sign/README.md deleted file mode 100644 index 34c4a85d..00000000 --- a/node_modules/request/node_modules/oauth-sign/README.md +++ /dev/null @@ -1,4 +0,0 @@ -oauth-sign -========== - -OAuth 1 signing. Formerly a vendor lib in mikeal/request, now a standalone module. diff --git a/node_modules/request/node_modules/oauth-sign/index.js b/node_modules/request/node_modules/oauth-sign/index.js deleted file mode 100644 index dadcba97..00000000 --- a/node_modules/request/node_modules/oauth-sign/index.js +++ /dev/null @@ -1,136 +0,0 @@ -var crypto = require('crypto') - , qs = require('querystring') - ; - -function sha1 (key, body) { - return crypto.createHmac('sha1', key).update(body).digest('base64') -} - -function rsa (key, body) { - return crypto.createSign("RSA-SHA1").update(body).sign(key, 'base64'); -} - -function rfc3986 (str) { - return encodeURIComponent(str) - .replace(/!/g,'%21') - .replace(/\*/g,'%2A') - .replace(/\(/g,'%28') - .replace(/\)/g,'%29') - .replace(/'/g,'%27') - ; -} - -// Maps object to bi-dimensional array -// Converts { foo: 'A', bar: [ 'b', 'B' ]} to -// [ ['foo', 'A'], ['bar', 'b'], ['bar', 'B'] ] -function map (obj) { - var key, val, arr = [] - for (key in obj) { - val = obj[key] - if (Array.isArray(val)) - for (var i = 0; i < val.length; i++) - arr.push([key, val[i]]) - else if (typeof val === "object") - for (var prop in val) - arr.push([key + '[' + prop + ']', val[prop]]); - else - arr.push([key, val]) - } - return arr -} - -// Compare function for sort -function compare (a, b) { - return a > b ? 1 : a < b ? -1 : 0 -} - -function generateBase (httpMethod, base_uri, params) { - // adapted from https://dev.twitter.com/docs/auth/oauth and - // https://dev.twitter.com/docs/auth/creating-signature - - // Parameter normalization - // http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 - var normalized = map(params) - // 1. First, the name and value of each parameter are encoded - .map(function (p) { - return [ rfc3986(p[0]), rfc3986(p[1] || '') ] - }) - // 2. The parameters are sorted by name, using ascending byte value - // ordering. If two or more parameters share the same name, they - // are sorted by their value. - .sort(function (a, b) { - return compare(a[0], b[0]) || compare(a[1], b[1]) - }) - // 3. The name of each parameter is concatenated to its corresponding - // value using an "=" character (ASCII code 61) as a separator, even - // if the value is empty. - .map(function (p) { return p.join('=') }) - // 4. The sorted name/value pairs are concatenated together into a - // single string by using an "&" character (ASCII code 38) as - // separator. - .join('&') - - var base = [ - rfc3986(httpMethod ? httpMethod.toUpperCase() : 'GET'), - rfc3986(base_uri), - rfc3986(normalized) - ].join('&') - - return base -} - -function hmacsign (httpMethod, base_uri, params, consumer_secret, token_secret) { - var base = generateBase(httpMethod, base_uri, params) - var key = [ - consumer_secret || '', - token_secret || '' - ].map(rfc3986).join('&') - - return sha1(key, base) -} - -function rsasign (httpMethod, base_uri, params, private_key, token_secret) { - var base = generateBase(httpMethod, base_uri, params) - var key = private_key || '' - - return rsa(key, base) -} - -function plaintext (consumer_secret, token_secret) { - var key = [ - consumer_secret || '', - token_secret || '' - ].map(rfc3986).join('&') - - return key -} - -function sign (signMethod, httpMethod, base_uri, params, consumer_secret, token_secret) { - var method - var skipArgs = 1 - - switch (signMethod) { - case 'RSA-SHA1': - method = rsasign - break - case 'HMAC-SHA1': - method = hmacsign - break - case 'PLAINTEXT': - method = plaintext - skipArgs = 4 - break - default: - throw new Error("Signature method not supported: " + signMethod) - } - - return method.apply(null, [].slice.call(arguments, skipArgs)) -} - -exports.hmacsign = hmacsign -exports.rsasign = rsasign -exports.plaintext = plaintext -exports.sign = sign -exports.rfc3986 = rfc3986 -exports.generateBase = generateBase - diff --git a/node_modules/request/node_modules/oauth-sign/package.json b/node_modules/request/node_modules/oauth-sign/package.json deleted file mode 100644 index cf1eeb77..00000000 --- a/node_modules/request/node_modules/oauth-sign/package.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "author": { - "name": "Mikeal Rogers", - "email": "mikeal.rogers@gmail.com", - "url": "http://www.futurealoof.com" - }, - "name": "oauth-sign", - "description": "OAuth 1 signing. Formerly a vendor lib in mikeal/request, now a standalone module.", - "version": "0.8.2", - "license": "Apache-2.0", - "repository": { - "url": "git+https://github.com/mikeal/oauth-sign.git" - }, - "main": "index.js", - "files": [ - "index.js" - ], - "dependencies": {}, - "devDependencies": {}, - "optionalDependencies": {}, - "engines": { - "node": "*" - }, - "scripts": { - "test": "node test.js" - }, - "gitHead": "0b034206316132f57e26970152c2fb18e71bddd5", - "bugs": { - "url": "https://github.com/mikeal/oauth-sign/issues" - }, - "homepage": "https://github.com/mikeal/oauth-sign#readme", - "_id": "oauth-sign@0.8.2", - "_shasum": "46a6ab7f0aead8deae9ec0565780b7d4efeb9d43", - "_from": "oauth-sign@>=0.8.1 <0.9.0", - "_npmVersion": "2.15.3", - "_nodeVersion": "5.9.0", - "_npmUser": { - "name": "simov", - "email": "simeonvelichkov@gmail.com" - }, - "dist": { - "shasum": "46a6ab7f0aead8deae9ec0565780b7d4efeb9d43", - "tarball": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz" - }, - "maintainers": [ - { - "name": "mikeal", - "email": "mikeal.rogers@gmail.com" - }, - { - "name": "nylen", - "email": "jnylen@gmail.com" - }, - { - "name": "simov", - "email": "simeonvelichkov@gmail.com" - } - ], - "_npmOperationalInternal": { - "host": "packages-12-west.internal.npmjs.com", - "tmp": "tmp/oauth-sign-0.8.2.tgz_1462396399020_0.8175400267355144" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/qs/.eslintignore b/node_modules/request/node_modules/qs/.eslintignore deleted file mode 100644 index 1521c8b7..00000000 --- a/node_modules/request/node_modules/qs/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/node_modules/request/node_modules/qs/.eslintrc b/node_modules/request/node_modules/qs/.eslintrc deleted file mode 100644 index 24a8fe48..00000000 --- a/node_modules/request/node_modules/qs/.eslintrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "root": true, - - "extends": "@ljharb", - - "rules": { - "complexity": [2, 19], - "consistent-return": [1], - "max-params": [2, 9], - "max-statements": [2, 33], - "no-extra-parens": [1], - "no-continue": [1], - "no-magic-numbers": 0, - "no-restricted-syntax": [2, "BreakStatement", "DebuggerStatement", "ForInStatement", "LabeledStatement", "WithStatement"], - "operator-linebreak": 1 - } -} diff --git a/node_modules/request/node_modules/qs/.npmignore b/node_modules/request/node_modules/qs/.npmignore deleted file mode 100644 index 7e1574dc..00000000 --- a/node_modules/request/node_modules/qs/.npmignore +++ /dev/null @@ -1,18 +0,0 @@ -.idea -*.iml -npm-debug.log -dump.rdb -node_modules -results.tap -results.xml -npm-shrinkwrap.json -config.json -.DS_Store -*/.DS_Store -*/*/.DS_Store -._* -*/._* -*/*/._* -coverage.* -lib-cov -complexity.md diff --git a/node_modules/request/node_modules/qs/.travis.yml b/node_modules/request/node_modules/qs/.travis.yml deleted file mode 100644 index 63bdc12b..00000000 --- a/node_modules/request/node_modules/qs/.travis.yml +++ /dev/null @@ -1,69 +0,0 @@ -language: node_js -node_js: - - "5.3" - - "5.2" - - "5.1" - - "5.0" - - "4.2" - - "4.1" - - "4.0" - - "iojs-v3.3" - - "iojs-v3.2" - - "iojs-v3.1" - - "iojs-v3.0" - - "iojs-v2.5" - - "iojs-v2.4" - - "iojs-v2.3" - - "iojs-v2.2" - - "iojs-v2.1" - - "iojs-v2.0" - - "iojs-v1.8" - - "iojs-v1.7" - - "iojs-v1.6" - - "iojs-v1.5" - - "iojs-v1.4" - - "iojs-v1.3" - - "iojs-v1.2" - - "iojs-v1.1" - - "iojs-v1.0" - - "0.12" - - "0.11" - - "0.10" - - "0.9" - - "0.8" - - "0.6" - - "0.4" -before_install: - - 'if [ "${TRAVIS_NODE_VERSION}" != "0.9" ]; then case "$(npm --version)" in 1.*) npm install -g npm@1.4.28 ;; 2.*) npm install -g npm@2 ;; esac ; fi' - - 'if [ "${TRAVIS_NODE_VERSION}" != "0.6" ] && [ "${TRAVIS_NODE_VERSION}" != "0.9" ]; then npm install -g npm; fi' -script: - - 'if [ "${TRAVIS_NODE_VERSION}" != "4.2" ]; then npm run tests-only ; else npm test ; fi' -sudo: false -matrix: - fast_finish: true - allow_failures: - - node_js: "5.2" - - node_js: "5.1" - - node_js: "5.0" - - node_js: "4.1" - - node_js: "4.0" - - node_js: "iojs-v3.2" - - node_js: "iojs-v3.1" - - node_js: "iojs-v3.0" - - node_js: "iojs-v2.4" - - node_js: "iojs-v2.3" - - node_js: "iojs-v2.2" - - node_js: "iojs-v2.1" - - node_js: "iojs-v2.0" - - node_js: "iojs-v1.7" - - node_js: "iojs-v1.6" - - node_js: "iojs-v1.5" - - node_js: "iojs-v1.4" - - node_js: "iojs-v1.3" - - node_js: "iojs-v1.2" - - node_js: "iojs-v1.1" - - node_js: "iojs-v1.0" - - node_js: "0.11" - - node_js: "0.9" - - node_js: "0.6" - - node_js: "0.4" diff --git a/node_modules/request/node_modules/qs/CHANGELOG.md b/node_modules/request/node_modules/qs/CHANGELOG.md deleted file mode 100644 index 5c66ea44..00000000 --- a/node_modules/request/node_modules/qs/CHANGELOG.md +++ /dev/null @@ -1,115 +0,0 @@ -## [**6.1.0**](https://github.com/ljharb/qs/issues?milestone=34&state=closed) -- [New] allowDots option for `stringify` (#151) -- [Fix] "sort" option should work at a depth of 3 or more (#151) -- [Fix] Restore `dist` directory; will be removed in v7 (#148) - -## [**6.0.2**](https://github.com/ljharb/qs/issues?milestone=33&state=closed) -- Revert ES6 requirement and restore support for node down to v0.8. - -## [**6.0.1**](https://github.com/ljharb/qs/issues?milestone=32&state=closed) -- [**#127**](https://github.com/ljharb/qs/pull/127) Fix engines definition in package.json - -## [**6.0.0**](https://github.com/ljharb/qs/issues?milestone=31&state=closed) -- [**#124**](https://github.com/ljharb/qs/issues/124) Use ES6 and drop support for node < v4 - -## [**5.2.0**](https://github.com/ljharb/qs/issues?milestone=30&state=closed) -- [**#64**](https://github.com/ljharb/qs/issues/64) Add option to sort object keys in the query string - -## [**5.1.0**](https://github.com/ljharb/qs/issues?milestone=29&state=closed) -- [**#117**](https://github.com/ljharb/qs/issues/117) make URI encoding stringified results optional -- [**#106**](https://github.com/ljharb/qs/issues/106) Add flag `skipNulls` to optionally skip null values in stringify - -## [**5.0.0**](https://github.com/ljharb/qs/issues?milestone=28&state=closed) -- [**#114**](https://github.com/ljharb/qs/issues/114) default allowDots to false -- [**#100**](https://github.com/ljharb/qs/issues/100) include dist to npm - -## [**4.0.0**](https://github.com/ljharb/qs/issues?milestone=26&state=closed) -- [**#98**](https://github.com/ljharb/qs/issues/98) make returning plain objects and allowing prototype overwriting properties optional - -## [**3.1.0**](https://github.com/ljharb/qs/issues?milestone=24&state=closed) -- [**#89**](https://github.com/ljharb/qs/issues/89) Add option to disable "Transform dot notation to bracket notation" - -## [**3.0.0**](https://github.com/ljharb/qs/issues?milestone=23&state=closed) -- [**#80**](https://github.com/ljharb/qs/issues/80) qs.parse silently drops properties -- [**#77**](https://github.com/ljharb/qs/issues/77) Perf boost -- [**#60**](https://github.com/ljharb/qs/issues/60) Add explicit option to disable array parsing -- [**#74**](https://github.com/ljharb/qs/issues/74) Bad parse when turning array into object -- [**#81**](https://github.com/ljharb/qs/issues/81) Add a `filter` option -- [**#68**](https://github.com/ljharb/qs/issues/68) Fixed issue with recursion and passing strings into objects. -- [**#66**](https://github.com/ljharb/qs/issues/66) Add mixed array and object dot notation support Closes: #47 -- [**#76**](https://github.com/ljharb/qs/issues/76) RFC 3986 -- [**#85**](https://github.com/ljharb/qs/issues/85) No equal sign -- [**#84**](https://github.com/ljharb/qs/issues/84) update license attribute - -## [**2.4.1**](https://github.com/ljharb/qs/issues?milestone=20&state=closed) -- [**#73**](https://github.com/ljharb/qs/issues/73) Property 'hasOwnProperty' of object # is not a function - -## [**2.4.0**](https://github.com/ljharb/qs/issues?milestone=19&state=closed) -- [**#70**](https://github.com/ljharb/qs/issues/70) Add arrayFormat option - -## [**2.3.3**](https://github.com/ljharb/qs/issues?milestone=18&state=closed) -- [**#59**](https://github.com/ljharb/qs/issues/59) make sure array indexes are >= 0, closes #57 -- [**#58**](https://github.com/ljharb/qs/issues/58) make qs usable for browser loader - -## [**2.3.2**](https://github.com/ljharb/qs/issues?milestone=17&state=closed) -- [**#55**](https://github.com/ljharb/qs/issues/55) allow merging a string into an object - -## [**2.3.1**](https://github.com/ljharb/qs/issues?milestone=16&state=closed) -- [**#52**](https://github.com/ljharb/qs/issues/52) Return "undefined" and "false" instead of throwing "TypeError". - -## [**2.3.0**](https://github.com/ljharb/qs/issues?milestone=15&state=closed) -- [**#50**](https://github.com/ljharb/qs/issues/50) add option to omit array indices, closes #46 - -## [**2.2.5**](https://github.com/ljharb/qs/issues?milestone=14&state=closed) -- [**#39**](https://github.com/ljharb/qs/issues/39) Is there an alternative to Buffer.isBuffer? -- [**#49**](https://github.com/ljharb/qs/issues/49) refactor utils.merge, fixes #45 -- [**#41**](https://github.com/ljharb/qs/issues/41) avoid browserifying Buffer, for #39 - -## [**2.2.4**](https://github.com/ljharb/qs/issues?milestone=13&state=closed) -- [**#38**](https://github.com/ljharb/qs/issues/38) how to handle object keys beginning with a number - -## [**2.2.3**](https://github.com/ljharb/qs/issues?milestone=12&state=closed) -- [**#37**](https://github.com/ljharb/qs/issues/37) parser discards first empty value in array -- [**#36**](https://github.com/ljharb/qs/issues/36) Update to lab 4.x - -## [**2.2.2**](https://github.com/ljharb/qs/issues?milestone=11&state=closed) -- [**#33**](https://github.com/ljharb/qs/issues/33) Error when plain object in a value -- [**#34**](https://github.com/ljharb/qs/issues/34) use Object.prototype.hasOwnProperty.call instead of obj.hasOwnProperty -- [**#24**](https://github.com/ljharb/qs/issues/24) Changelog? Semver? - -## [**2.2.1**](https://github.com/ljharb/qs/issues?milestone=10&state=closed) -- [**#32**](https://github.com/ljharb/qs/issues/32) account for circular references properly, closes #31 -- [**#31**](https://github.com/ljharb/qs/issues/31) qs.parse stackoverflow on circular objects - -## [**2.2.0**](https://github.com/ljharb/qs/issues?milestone=9&state=closed) -- [**#26**](https://github.com/ljharb/qs/issues/26) Don't use Buffer global if it's not present -- [**#30**](https://github.com/ljharb/qs/issues/30) Bug when merging non-object values into arrays -- [**#29**](https://github.com/ljharb/qs/issues/29) Don't call Utils.clone at the top of Utils.merge -- [**#23**](https://github.com/ljharb/qs/issues/23) Ability to not limit parameters? - -## [**2.1.0**](https://github.com/ljharb/qs/issues?milestone=8&state=closed) -- [**#22**](https://github.com/ljharb/qs/issues/22) Enable using a RegExp as delimiter - -## [**2.0.0**](https://github.com/ljharb/qs/issues?milestone=7&state=closed) -- [**#18**](https://github.com/ljharb/qs/issues/18) Why is there arrayLimit? -- [**#20**](https://github.com/ljharb/qs/issues/20) Configurable parametersLimit -- [**#21**](https://github.com/ljharb/qs/issues/21) make all limits optional, for #18, for #20 - -## [**1.2.2**](https://github.com/ljharb/qs/issues?milestone=6&state=closed) -- [**#19**](https://github.com/ljharb/qs/issues/19) Don't overwrite null values - -## [**1.2.1**](https://github.com/ljharb/qs/issues?milestone=5&state=closed) -- [**#16**](https://github.com/ljharb/qs/issues/16) ignore non-string delimiters -- [**#15**](https://github.com/ljharb/qs/issues/15) Close code block - -## [**1.2.0**](https://github.com/ljharb/qs/issues?milestone=4&state=closed) -- [**#12**](https://github.com/ljharb/qs/issues/12) Add optional delim argument -- [**#13**](https://github.com/ljharb/qs/issues/13) fix #11: flattened keys in array are now correctly parsed - -## [**1.1.0**](https://github.com/ljharb/qs/issues?milestone=3&state=closed) -- [**#7**](https://github.com/ljharb/qs/issues/7) Empty values of a POST array disappear after being submitted -- [**#9**](https://github.com/ljharb/qs/issues/9) Should not omit equals signs (=) when value is null -- [**#6**](https://github.com/ljharb/qs/issues/6) Minor grammar fix in README - -## [**1.0.2**](https://github.com/ljharb/qs/issues?milestone=2&state=closed) -- [**#5**](https://github.com/ljharb/qs/issues/5) array holes incorrectly copied into object on large index diff --git a/node_modules/request/node_modules/qs/CONTRIBUTING.md b/node_modules/request/node_modules/qs/CONTRIBUTING.md deleted file mode 100644 index 89283615..00000000 --- a/node_modules/request/node_modules/qs/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -Please view our [hapijs contributing guide](https://github.com/hapijs/hapi/blob/master/CONTRIBUTING.md). diff --git a/node_modules/request/node_modules/qs/LICENSE b/node_modules/request/node_modules/qs/LICENSE deleted file mode 100644 index d4569487..00000000 --- a/node_modules/request/node_modules/qs/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -Copyright (c) 2014 Nathan LaFreniere and other contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * The names of any contributors may not be used to endorse or promote - products derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - * * * - -The complete list of contributors can be found at: https://github.com/hapijs/qs/graphs/contributors diff --git a/node_modules/request/node_modules/qs/README.md b/node_modules/request/node_modules/qs/README.md deleted file mode 100644 index 335eafb4..00000000 --- a/node_modules/request/node_modules/qs/README.md +++ /dev/null @@ -1,335 +0,0 @@ -# qs - -A querystring parsing and stringifying library with some added security. - -[![Build Status](https://api.travis-ci.org/ljharb/qs.svg)](http://travis-ci.org/ljharb/qs) - -Lead Maintainer: [Jordan Harband](https://github.com/ljharb) - -The **qs** module was originally created and maintained by [TJ Holowaychuk](https://github.com/visionmedia/node-querystring). - -## Usage - -```javascript -var qs = require('qs'); -var assert = require('assert'); - -var obj = qs.parse('a=c'); -assert.deepEqual(obj, { a: 'c' }); - -var str = qs.stringify(obj); -assert.equal(str, 'a=c'); -``` - -### Parsing Objects - -[](#preventEval) -```javascript -qs.parse(string, [options]); -``` - -**qs** allows you to create nested objects within your query strings, by surrounding the name of sub-keys with square brackets `[]`. -For example, the string `'foo[bar]=baz'` converts to: - -```javascript -assert.deepEqual(qs.parse('foo[bar]=baz'), { - foo: { - bar: 'baz' - } -}); -``` - -When using the `plainObjects` option the parsed value is returned as a plain object, created via `Object.create(null)` and as such you should be aware that prototype methods will not exist on it and a user may set those names to whatever value they like: - -```javascript -var plainObject = qs.parse('a[hasOwnProperty]=b', { plainObjects: true }); -assert.deepEqual(plainObject, { a: { hasOwnProperty: 'b' } }); -``` - -By default parameters that would overwrite properties on the object prototype are ignored, if you wish to keep the data from those fields either use `plainObjects` as mentioned above, or set `allowPrototypes` to `true` which will allow user input to overwrite those properties. *WARNING* It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten. Always be careful with this option. - -```javascript -var protoObject = qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true }); -assert.deepEqual(protoObject, { a: { hasOwnProperty: 'b' } }); -``` - -URI encoded strings work too: - -```javascript -assert.deepEqual(qs.parse('a%5Bb%5D=c'), { - a: { b: 'c' } -}); -``` - -You can also nest your objects, like `'foo[bar][baz]=foobarbaz'`: - -```javascript -assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), { - foo: { - bar: { - baz: 'foobarbaz' - } - } -}); -``` - -By default, when nesting objects **qs** will only parse up to 5 children deep. This means if you attempt to parse a string like -`'a[b][c][d][e][f][g][h][i]=j'` your resulting object will be: - -```javascript -var expected = { - a: { - b: { - c: { - d: { - e: { - f: { - '[g][h][i]': 'j' - } - } - } - } - } - } -}; -var string = 'a[b][c][d][e][f][g][h][i]=j'; -assert.deepEqual(qs.parse(string), expected); -``` - -This depth can be overridden by passing a `depth` option to `qs.parse(string, [options])`: - -```javascript -var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 }); -assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }); -``` - -The depth limit helps mitigate abuse when **qs** is used to parse user input, and it is recommended to keep it a reasonably small number. - -For similar reasons, by default **qs** will only parse up to 1000 parameters. This can be overridden by passing a `parameterLimit` option: - -```javascript -var limited = qs.parse('a=b&c=d', { parameterLimit: 1 }); -assert.deepEqual(limited, { a: 'b' }); -``` - -An optional delimiter can also be passed: - -```javascript -var delimited = qs.parse('a=b;c=d', { delimiter: ';' }); -assert.deepEqual(delimited, { a: 'b', c: 'd' }); -``` - -Delimiters can be a regular expression too: - -```javascript -var regexed = qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ }); -assert.deepEqual(regexed, { a: 'b', c: 'd', e: 'f' }); -``` - -Option `allowDots` can be used to enable dot notation: - -```javascript -var withDots = qs.parse('a.b=c', { allowDots: true }); -assert.deepEqual(withDots, { a: { b: 'c' } }); -``` - -### Parsing Arrays - -**qs** can also parse arrays using a similar `[]` notation: - -```javascript -var withArray = qs.parse('a[]=b&a[]=c'); -assert.deepEqual(withArray, { a: ['b', 'c'] }); -``` - -You may specify an index as well: - -```javascript -var withIndexes = qs.parse('a[1]=c&a[0]=b'); -assert.deepEqual(withIndexes, { a: ['b', 'c'] }); -``` - -Note that the only difference between an index in an array and a key in an object is that the value between the brackets must be a number -to create an array. When creating arrays with specific indices, **qs** will compact a sparse array to only the existing values preserving -their order: - -```javascript -var noSparse = qs.parse('a[1]=b&a[15]=c'); -assert.deepEqual(noSparse, { a: ['b', 'c'] }); -``` - -Note that an empty string is also a value, and will be preserved: - -```javascript -var withEmptyString = qs.parse('a[]=&a[]=b'); -assert.deepEqual(withEmptyString, { a: ['', 'b'] }); - -var withIndexedEmptyString = qs.parse('a[0]=b&a[1]=&a[2]=c'); -assert.deepEqual(withIndexedEmptyString, { a: ['b', '', 'c'] }); -``` - -**qs** will also limit specifying indices in an array to a maximum index of `20`. Any array members with an index of greater than `20` will -instead be converted to an object with the index as the key: - -```javascript -var withMaxIndex = qs.parse('a[100]=b'); -assert.deepEqual(withMaxIndex, { a: { '100': 'b' } }); -``` - -This limit can be overridden by passing an `arrayLimit` option: - -```javascript -var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 }); -assert.deepEqual(withArrayLimit, { a: { '1': 'b' } }); -``` - -To disable array parsing entirely, set `parseArrays` to `false`. - -```javascript -var noParsingArrays = qs.parse('a[]=b', { parseArrays: false }); -assert.deepEqual(noParsingArrays, { a: { '0': 'b' } }); -``` - -If you mix notations, **qs** will merge the two items into an object: - -```javascript -var mixedNotation = qs.parse('a[0]=b&a[b]=c'); -assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } }); -``` - -You can also create arrays of objects: - -```javascript -var arraysOfObjects = qs.parse('a[][b]=c'); -assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] }); -``` - -### Stringifying - -[](#preventEval) -```javascript -qs.stringify(object, [options]); -``` - -When stringifying, **qs** by default URI encodes output. Objects are stringified as you would expect: - -```javascript -assert.equal(qs.stringify({ a: 'b' }), 'a=b'); -assert.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); -``` - -This encoding can be disabled by setting the `encode` option to `false`: - -```javascript -var unencoded = qs.stringify({ a: { b: 'c' } }, { encode: false }); -assert.equal(unencoded, 'a[b]=c'); -``` - -Examples beyond this point will be shown as though the output is not URI encoded for clarity. Please note that the return values in these cases *will* be URI encoded during real usage. - -When arrays are stringified, by default they are given explicit indices: - -```javascript -qs.stringify({ a: ['b', 'c', 'd'] }); -// 'a[0]=b&a[1]=c&a[2]=d' -``` - -You may override this by setting the `indices` option to `false`: - -```javascript -qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }); -// 'a=b&a=c&a=d' -``` - -You may use the `arrayFormat` option to specify the format of the output array - -```javascript -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }) -// 'a[0]=b&a[1]=c' -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }) -// 'a[]=b&a[]=c' -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }) -// 'a=b&a=c' -``` - -Empty strings and null values will omit the value, but the equals sign (=) remains in place: - -```javascript -assert.equal(qs.stringify({ a: '' }), 'a='); -``` - -Properties that are set to `undefined` will be omitted entirely: - -```javascript -assert.equal(qs.stringify({ a: null, b: undefined }), 'a='); -``` - -The delimiter may be overridden with stringify as well: - -```javascript -assert.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d'); -``` - -Finally, you can use the `filter` option to restrict which keys will be included in the stringified output. -If you pass a function, it will be called for each key to obtain the replacement value. Otherwise, if you -pass an array, it will be used to select properties and array indices for stringification: - -```javascript -function filterFunc(prefix, value) { - if (prefix == 'b') { - // Return an `undefined` value to omit a property. - return; - } - if (prefix == 'e[f]') { - return value.getTime(); - } - if (prefix == 'e[g][0]') { - return value * 2; - } - return value; -} -qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc }); -// 'a=b&c=d&e[f]=123&e[g][0]=4' -qs.stringify({ a: 'b', c: 'd', e: 'f' }, { filter: ['a', 'e'] }); -// 'a=b&e=f' -qs.stringify({ a: ['b', 'c', 'd'], e: 'f' }, { filter: ['a', 0, 2] }); -// 'a[0]=b&a[2]=d' -``` - -### Handling of `null` values - -By default, `null` values are treated like empty strings: - -```javascript -var withNull = qs.stringify({ a: null, b: '' }); -assert.equal(withNull, 'a=&b='); -``` - -Parsing does not distinguish between parameters with and without equal signs. Both are converted to empty strings. - -```javascript -var equalsInsensitive = qs.parse('a&b='); -assert.deepEqual(equalsInsensitive, { a: '', b: '' }); -``` - -To distinguish between `null` values and empty strings use the `strictNullHandling` flag. In the result string the `null` -values have no `=` sign: - -```javascript -var strictNull = qs.stringify({ a: null, b: '' }, { strictNullHandling: true }); -assert.equal(strictNull, 'a&b='); -``` - -To parse values without `=` back to `null` use the `strictNullHandling` flag: - -```javascript -var parsedStrictNull = qs.parse('a&b=', { strictNullHandling: true }); -assert.deepEqual(parsedStrictNull, { a: null, b: '' }); -``` - -To completely skip rendering keys with `null` values, use the `skipNulls` flag: - -```javascript -var nullsSkipped = qs.stringify({ a: 'b', c: null}, { skipNulls: true }); -assert.equal(nullsSkipped, 'a=b'); -``` diff --git a/node_modules/request/node_modules/qs/bower.json b/node_modules/request/node_modules/qs/bower.json deleted file mode 100644 index 44f05064..00000000 --- a/node_modules/request/node_modules/qs/bower.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "qs", - "main": "dist/qs.js", - "homepage": "https://github.com/hapijs/qs", - "authors": [ - "Nathan LaFreniere " - ], - "description": "A querystring parser that supports nesting and arrays, with a depth limit", - "keywords": [ - "querystring", - "qs" - ], - "license": "BSD-3-Clause", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ] -} diff --git a/node_modules/request/node_modules/qs/component.json b/node_modules/request/node_modules/qs/component.json deleted file mode 100644 index cb8d93fb..00000000 --- a/node_modules/request/node_modules/qs/component.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "qs", - "repository": "hapijs/qs", - "description": "query-string parser / stringifier with nesting support", - "version": "6.1.0", - "keywords": ["querystring", "query", "parser"], - "main": "lib/index.js", - "scripts": [ - "lib/index.js", - "lib/parse.js", - "lib/stringify.js", - "lib/utils.js" - ], - "license": "BSD-3-Clause" -} diff --git a/node_modules/request/node_modules/qs/dist/qs.js b/node_modules/request/node_modules/qs/dist/qs.js deleted file mode 100644 index bb8ea31a..00000000 --- a/node_modules/request/node_modules/qs/dist/qs.js +++ /dev/null @@ -1,476 +0,0 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Qs = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0 && - (options.parseArrays && index <= options.arrayLimit) - ) { - obj = []; - obj[index] = internals.parseObject(chain, val, options); - } else { - obj[cleanRoot] = internals.parseObject(chain, val, options); - } - } - - return obj; -}; - -internals.parseKeys = function (givenKey, val, options) { - if (!givenKey) { - return; - } - - // Transform dot notation to bracket notation - var key = options.allowDots ? givenKey.replace(/\.([^\.\[]+)/g, '[$1]') : givenKey; - - // The regex chunks - - var parent = /^([^\[\]]*)/; - var child = /(\[[^\[\]]*\])/g; - - // Get the parent - - var segment = parent.exec(key); - - // Stash the parent if it exists - - var keys = []; - if (segment[1]) { - // If we aren't using plain objects, optionally prefix keys - // that would overwrite object prototype properties - if (!options.plainObjects && Object.prototype.hasOwnProperty(segment[1])) { - if (!options.allowPrototypes) { - return; - } - } - - keys.push(segment[1]); - } - - // Loop through children appending to the array until we hit depth - - var i = 0; - while ((segment = child.exec(key)) !== null && i < options.depth) { - i += 1; - if (!options.plainObjects && Object.prototype.hasOwnProperty(segment[1].replace(/\[|\]/g, ''))) { - if (!options.allowPrototypes) { - continue; - } - } - keys.push(segment[1]); - } - - // If there's a remainder, just add whatever is left - - if (segment) { - keys.push('[' + key.slice(segment.index) + ']'); - } - - return internals.parseObject(keys, val, options); -}; - -module.exports = function (str, opts) { - var options = opts || {}; - options.delimiter = typeof options.delimiter === 'string' || Utils.isRegExp(options.delimiter) ? options.delimiter : internals.delimiter; - options.depth = typeof options.depth === 'number' ? options.depth : internals.depth; - options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : internals.arrayLimit; - options.parseArrays = options.parseArrays !== false; - options.allowDots = typeof options.allowDots === 'boolean' ? options.allowDots : internals.allowDots; - options.plainObjects = typeof options.plainObjects === 'boolean' ? options.plainObjects : internals.plainObjects; - options.allowPrototypes = typeof options.allowPrototypes === 'boolean' ? options.allowPrototypes : internals.allowPrototypes; - options.parameterLimit = typeof options.parameterLimit === 'number' ? options.parameterLimit : internals.parameterLimit; - options.strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : internals.strictNullHandling; - - if ( - str === '' || - str === null || - typeof str === 'undefined' - ) { - return options.plainObjects ? Object.create(null) : {}; - } - - var tempObj = typeof str === 'string' ? internals.parseValues(str, options) : str; - var obj = options.plainObjects ? Object.create(null) : {}; - - // Iterate over the keys and setup the new object - - var keys = Object.keys(tempObj); - for (var i = 0; i < keys.length; ++i) { - var key = keys[i]; - var newObj = internals.parseKeys(key, tempObj[key], options); - obj = Utils.merge(obj, newObj, options); - } - - return Utils.compact(obj); -}; - -},{"./utils":4}],3:[function(require,module,exports){ -'use strict'; - -var Utils = require('./utils'); - -var internals = { - delimiter: '&', - arrayPrefixGenerators: { - brackets: function (prefix) { - return prefix + '[]'; - }, - indices: function (prefix, key) { - return prefix + '[' + key + ']'; - }, - repeat: function (prefix) { - return prefix; - } - }, - strictNullHandling: false, - skipNulls: false, - encode: true -}; - -internals.stringify = function (object, prefix, generateArrayPrefix, strictNullHandling, skipNulls, encode, filter, sort, allowDots) { - var obj = object; - if (typeof filter === 'function') { - obj = filter(prefix, obj); - } else if (Utils.isBuffer(obj)) { - obj = String(obj); - } else if (obj instanceof Date) { - obj = obj.toISOString(); - } else if (obj === null) { - if (strictNullHandling) { - return encode ? Utils.encode(prefix) : prefix; - } - - obj = ''; - } - - if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') { - if (encode) { - return [Utils.encode(prefix) + '=' + Utils.encode(obj)]; - } - return [prefix + '=' + obj]; - } - - var values = []; - - if (typeof obj === 'undefined') { - return values; - } - - var objKeys; - if (Array.isArray(filter)) { - objKeys = filter; - } else { - var keys = Object.keys(obj); - objKeys = sort ? keys.sort(sort) : keys; - } - - for (var i = 0; i < objKeys.length; ++i) { - var key = objKeys[i]; - - if (skipNulls && obj[key] === null) { - continue; - } - - if (Array.isArray(obj)) { - values = values.concat(internals.stringify(obj[key], generateArrayPrefix(prefix, key), generateArrayPrefix, strictNullHandling, skipNulls, encode, filter, sort, allowDots)); - } else { - values = values.concat(internals.stringify(obj[key], prefix + (allowDots ? '.' + key : '[' + key + ']'), generateArrayPrefix, strictNullHandling, skipNulls, encode, filter, sort, allowDots)); - } - } - - return values; -}; - -module.exports = function (object, opts) { - var obj = object; - var options = opts || {}; - var delimiter = typeof options.delimiter === 'undefined' ? internals.delimiter : options.delimiter; - var strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : internals.strictNullHandling; - var skipNulls = typeof options.skipNulls === 'boolean' ? options.skipNulls : internals.skipNulls; - var encode = typeof options.encode === 'boolean' ? options.encode : internals.encode; - var sort = typeof options.sort === 'function' ? options.sort : null; - var allowDots = typeof options.allowDots === 'undefined' ? false : options.allowDots; - var objKeys; - var filter; - if (typeof options.filter === 'function') { - filter = options.filter; - obj = filter('', obj); - } else if (Array.isArray(options.filter)) { - objKeys = filter = options.filter; - } - - var keys = []; - - if (typeof obj !== 'object' || obj === null) { - return ''; - } - - var arrayFormat; - if (options.arrayFormat in internals.arrayPrefixGenerators) { - arrayFormat = options.arrayFormat; - } else if ('indices' in options) { - arrayFormat = options.indices ? 'indices' : 'repeat'; - } else { - arrayFormat = 'indices'; - } - - var generateArrayPrefix = internals.arrayPrefixGenerators[arrayFormat]; - - if (!objKeys) { - objKeys = Object.keys(obj); - } - - if (sort) { - objKeys.sort(sort); - } - - for (var i = 0; i < objKeys.length; ++i) { - var key = objKeys[i]; - - if (skipNulls && obj[key] === null) { - continue; - } - - keys = keys.concat(internals.stringify(obj[key], key, generateArrayPrefix, strictNullHandling, skipNulls, encode, filter, sort, allowDots)); - } - - return keys.join(delimiter); -}; - -},{"./utils":4}],4:[function(require,module,exports){ -'use strict'; - -var hexTable = (function () { - var array = new Array(256); - for (var i = 0; i < 256; ++i) { - array[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); - } - - return array; -}()); - -exports.arrayToObject = function (source, options) { - var obj = options.plainObjects ? Object.create(null) : {}; - for (var i = 0; i < source.length; ++i) { - if (typeof source[i] !== 'undefined') { - obj[i] = source[i]; - } - } - - return obj; -}; - -exports.merge = function (target, source, options) { - if (!source) { - return target; - } - - if (typeof source !== 'object') { - if (Array.isArray(target)) { - target.push(source); - } else if (typeof target === 'object') { - target[source] = true; - } else { - return [target, source]; - } - - return target; - } - - if (typeof target !== 'object') { - return [target].concat(source); - } - - var mergeTarget = target; - if (Array.isArray(target) && !Array.isArray(source)) { - mergeTarget = exports.arrayToObject(target, options); - } - - return Object.keys(source).reduce(function (acc, key) { - var value = source[key]; - - if (Object.prototype.hasOwnProperty.call(acc, key)) { - acc[key] = exports.merge(acc[key], value, options); - } else { - acc[key] = value; - } - return acc; - }, mergeTarget); -}; - -exports.decode = function (str) { - try { - return decodeURIComponent(str.replace(/\+/g, ' ')); - } catch (e) { - return str; - } -}; - -exports.encode = function (str) { - // This code was originally written by Brian White (mscdex) for the io.js core querystring library. - // It has been adapted here for stricter adherence to RFC 3986 - if (str.length === 0) { - return str; - } - - var string = typeof str === 'string' ? str : String(str); - - var out = ''; - for (var i = 0; i < string.length; ++i) { - var c = string.charCodeAt(i); - - if ( - c === 0x2D || // - - c === 0x2E || // . - c === 0x5F || // _ - c === 0x7E || // ~ - (c >= 0x30 && c <= 0x39) || // 0-9 - (c >= 0x41 && c <= 0x5A) || // a-z - (c >= 0x61 && c <= 0x7A) // A-Z - ) { - out += string.charAt(i); - continue; - } - - if (c < 0x80) { - out = out + hexTable[c]; - continue; - } - - if (c < 0x800) { - out = out + (hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]); - continue; - } - - if (c < 0xD800 || c >= 0xE000) { - out = out + (hexTable[0xE0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]); - continue; - } - - i += 1; - c = 0x10000 + (((c & 0x3FF) << 10) | (string.charCodeAt(i) & 0x3FF)); - out += (hexTable[0xF0 | (c >> 18)] + hexTable[0x80 | ((c >> 12) & 0x3F)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]); - } - - return out; -}; - -exports.compact = function (obj, references) { - if (typeof obj !== 'object' || obj === null) { - return obj; - } - - var refs = references || []; - var lookup = refs.indexOf(obj); - if (lookup !== -1) { - return refs[lookup]; - } - - refs.push(obj); - - if (Array.isArray(obj)) { - var compacted = []; - - for (var i = 0; i < obj.length; ++i) { - if (typeof obj[i] !== 'undefined') { - compacted.push(obj[i]); - } - } - - return compacted; - } - - var keys = Object.keys(obj); - for (var j = 0; j < keys.length; ++j) { - var key = keys[j]; - obj[key] = exports.compact(obj[key], refs); - } - - return obj; -}; - -exports.isRegExp = function (obj) { - return Object.prototype.toString.call(obj) === '[object RegExp]'; -}; - -exports.isBuffer = function (obj) { - if (obj === null || typeof obj === 'undefined') { - return false; - } - - return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); -}; - -},{}]},{},[1])(1) -}); \ No newline at end of file diff --git a/node_modules/request/node_modules/qs/lib/index.js b/node_modules/request/node_modules/qs/lib/index.js deleted file mode 100755 index 19019590..00000000 --- a/node_modules/request/node_modules/qs/lib/index.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -var Stringify = require('./stringify'); -var Parse = require('./parse'); - -module.exports = { - stringify: Stringify, - parse: Parse -}; diff --git a/node_modules/request/node_modules/qs/lib/parse.js b/node_modules/request/node_modules/qs/lib/parse.js deleted file mode 100755 index 9b6cbd22..00000000 --- a/node_modules/request/node_modules/qs/lib/parse.js +++ /dev/null @@ -1,164 +0,0 @@ -'use strict'; - -var Utils = require('./utils'); - -var internals = { - delimiter: '&', - depth: 5, - arrayLimit: 20, - parameterLimit: 1000, - strictNullHandling: false, - plainObjects: false, - allowPrototypes: false, - allowDots: false -}; - -internals.parseValues = function (str, options) { - var obj = {}; - var parts = str.split(options.delimiter, options.parameterLimit === Infinity ? undefined : options.parameterLimit); - - for (var i = 0; i < parts.length; ++i) { - var part = parts[i]; - var pos = part.indexOf(']=') === -1 ? part.indexOf('=') : part.indexOf(']=') + 1; - - if (pos === -1) { - obj[Utils.decode(part)] = ''; - - if (options.strictNullHandling) { - obj[Utils.decode(part)] = null; - } - } else { - var key = Utils.decode(part.slice(0, pos)); - var val = Utils.decode(part.slice(pos + 1)); - - if (Object.prototype.hasOwnProperty.call(obj, key)) { - obj[key] = [].concat(obj[key]).concat(val); - } else { - obj[key] = val; - } - } - } - - return obj; -}; - -internals.parseObject = function (chain, val, options) { - if (!chain.length) { - return val; - } - - var root = chain.shift(); - - var obj; - if (root === '[]') { - obj = []; - obj = obj.concat(internals.parseObject(chain, val, options)); - } else { - obj = options.plainObjects ? Object.create(null) : {}; - var cleanRoot = root[0] === '[' && root[root.length - 1] === ']' ? root.slice(1, root.length - 1) : root; - var index = parseInt(cleanRoot, 10); - if ( - !isNaN(index) && - root !== cleanRoot && - String(index) === cleanRoot && - index >= 0 && - (options.parseArrays && index <= options.arrayLimit) - ) { - obj = []; - obj[index] = internals.parseObject(chain, val, options); - } else { - obj[cleanRoot] = internals.parseObject(chain, val, options); - } - } - - return obj; -}; - -internals.parseKeys = function (givenKey, val, options) { - if (!givenKey) { - return; - } - - // Transform dot notation to bracket notation - var key = options.allowDots ? givenKey.replace(/\.([^\.\[]+)/g, '[$1]') : givenKey; - - // The regex chunks - - var parent = /^([^\[\]]*)/; - var child = /(\[[^\[\]]*\])/g; - - // Get the parent - - var segment = parent.exec(key); - - // Stash the parent if it exists - - var keys = []; - if (segment[1]) { - // If we aren't using plain objects, optionally prefix keys - // that would overwrite object prototype properties - if (!options.plainObjects && Object.prototype.hasOwnProperty(segment[1])) { - if (!options.allowPrototypes) { - return; - } - } - - keys.push(segment[1]); - } - - // Loop through children appending to the array until we hit depth - - var i = 0; - while ((segment = child.exec(key)) !== null && i < options.depth) { - i += 1; - if (!options.plainObjects && Object.prototype.hasOwnProperty(segment[1].replace(/\[|\]/g, ''))) { - if (!options.allowPrototypes) { - continue; - } - } - keys.push(segment[1]); - } - - // If there's a remainder, just add whatever is left - - if (segment) { - keys.push('[' + key.slice(segment.index) + ']'); - } - - return internals.parseObject(keys, val, options); -}; - -module.exports = function (str, opts) { - var options = opts || {}; - options.delimiter = typeof options.delimiter === 'string' || Utils.isRegExp(options.delimiter) ? options.delimiter : internals.delimiter; - options.depth = typeof options.depth === 'number' ? options.depth : internals.depth; - options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : internals.arrayLimit; - options.parseArrays = options.parseArrays !== false; - options.allowDots = typeof options.allowDots === 'boolean' ? options.allowDots : internals.allowDots; - options.plainObjects = typeof options.plainObjects === 'boolean' ? options.plainObjects : internals.plainObjects; - options.allowPrototypes = typeof options.allowPrototypes === 'boolean' ? options.allowPrototypes : internals.allowPrototypes; - options.parameterLimit = typeof options.parameterLimit === 'number' ? options.parameterLimit : internals.parameterLimit; - options.strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : internals.strictNullHandling; - - if ( - str === '' || - str === null || - typeof str === 'undefined' - ) { - return options.plainObjects ? Object.create(null) : {}; - } - - var tempObj = typeof str === 'string' ? internals.parseValues(str, options) : str; - var obj = options.plainObjects ? Object.create(null) : {}; - - // Iterate over the keys and setup the new object - - var keys = Object.keys(tempObj); - for (var i = 0; i < keys.length; ++i) { - var key = keys[i]; - var newObj = internals.parseKeys(key, tempObj[key], options); - obj = Utils.merge(obj, newObj, options); - } - - return Utils.compact(obj); -}; diff --git a/node_modules/request/node_modules/qs/lib/stringify.js b/node_modules/request/node_modules/qs/lib/stringify.js deleted file mode 100755 index 892dad45..00000000 --- a/node_modules/request/node_modules/qs/lib/stringify.js +++ /dev/null @@ -1,131 +0,0 @@ -'use strict'; - -var Utils = require('./utils'); - -var internals = { - delimiter: '&', - arrayPrefixGenerators: { - brackets: function (prefix) { - return prefix + '[]'; - }, - indices: function (prefix, key) { - return prefix + '[' + key + ']'; - }, - repeat: function (prefix) { - return prefix; - } - }, - strictNullHandling: false, - skipNulls: false, - encode: true -}; - -internals.stringify = function (object, prefix, generateArrayPrefix, strictNullHandling, skipNulls, encode, filter, sort, allowDots) { - var obj = object; - if (typeof filter === 'function') { - obj = filter(prefix, obj); - } else if (Utils.isBuffer(obj)) { - obj = String(obj); - } else if (obj instanceof Date) { - obj = obj.toISOString(); - } else if (obj === null) { - if (strictNullHandling) { - return encode ? Utils.encode(prefix) : prefix; - } - - obj = ''; - } - - if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') { - if (encode) { - return [Utils.encode(prefix) + '=' + Utils.encode(obj)]; - } - return [prefix + '=' + obj]; - } - - var values = []; - - if (typeof obj === 'undefined') { - return values; - } - - var objKeys; - if (Array.isArray(filter)) { - objKeys = filter; - } else { - var keys = Object.keys(obj); - objKeys = sort ? keys.sort(sort) : keys; - } - - for (var i = 0; i < objKeys.length; ++i) { - var key = objKeys[i]; - - if (skipNulls && obj[key] === null) { - continue; - } - - if (Array.isArray(obj)) { - values = values.concat(internals.stringify(obj[key], generateArrayPrefix(prefix, key), generateArrayPrefix, strictNullHandling, skipNulls, encode, filter, sort, allowDots)); - } else { - values = values.concat(internals.stringify(obj[key], prefix + (allowDots ? '.' + key : '[' + key + ']'), generateArrayPrefix, strictNullHandling, skipNulls, encode, filter, sort, allowDots)); - } - } - - return values; -}; - -module.exports = function (object, opts) { - var obj = object; - var options = opts || {}; - var delimiter = typeof options.delimiter === 'undefined' ? internals.delimiter : options.delimiter; - var strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : internals.strictNullHandling; - var skipNulls = typeof options.skipNulls === 'boolean' ? options.skipNulls : internals.skipNulls; - var encode = typeof options.encode === 'boolean' ? options.encode : internals.encode; - var sort = typeof options.sort === 'function' ? options.sort : null; - var allowDots = typeof options.allowDots === 'undefined' ? false : options.allowDots; - var objKeys; - var filter; - if (typeof options.filter === 'function') { - filter = options.filter; - obj = filter('', obj); - } else if (Array.isArray(options.filter)) { - objKeys = filter = options.filter; - } - - var keys = []; - - if (typeof obj !== 'object' || obj === null) { - return ''; - } - - var arrayFormat; - if (options.arrayFormat in internals.arrayPrefixGenerators) { - arrayFormat = options.arrayFormat; - } else if ('indices' in options) { - arrayFormat = options.indices ? 'indices' : 'repeat'; - } else { - arrayFormat = 'indices'; - } - - var generateArrayPrefix = internals.arrayPrefixGenerators[arrayFormat]; - - if (!objKeys) { - objKeys = Object.keys(obj); - } - - if (sort) { - objKeys.sort(sort); - } - - for (var i = 0; i < objKeys.length; ++i) { - var key = objKeys[i]; - - if (skipNulls && obj[key] === null) { - continue; - } - - keys = keys.concat(internals.stringify(obj[key], key, generateArrayPrefix, strictNullHandling, skipNulls, encode, filter, sort, allowDots)); - } - - return keys.join(delimiter); -}; diff --git a/node_modules/request/node_modules/qs/lib/utils.js b/node_modules/request/node_modules/qs/lib/utils.js deleted file mode 100755 index 5d433560..00000000 --- a/node_modules/request/node_modules/qs/lib/utils.js +++ /dev/null @@ -1,162 +0,0 @@ -'use strict'; - -var hexTable = (function () { - var array = new Array(256); - for (var i = 0; i < 256; ++i) { - array[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); - } - - return array; -}()); - -exports.arrayToObject = function (source, options) { - var obj = options.plainObjects ? Object.create(null) : {}; - for (var i = 0; i < source.length; ++i) { - if (typeof source[i] !== 'undefined') { - obj[i] = source[i]; - } - } - - return obj; -}; - -exports.merge = function (target, source, options) { - if (!source) { - return target; - } - - if (typeof source !== 'object') { - if (Array.isArray(target)) { - target.push(source); - } else if (typeof target === 'object') { - target[source] = true; - } else { - return [target, source]; - } - - return target; - } - - if (typeof target !== 'object') { - return [target].concat(source); - } - - var mergeTarget = target; - if (Array.isArray(target) && !Array.isArray(source)) { - mergeTarget = exports.arrayToObject(target, options); - } - - return Object.keys(source).reduce(function (acc, key) { - var value = source[key]; - - if (Object.prototype.hasOwnProperty.call(acc, key)) { - acc[key] = exports.merge(acc[key], value, options); - } else { - acc[key] = value; - } - return acc; - }, mergeTarget); -}; - -exports.decode = function (str) { - try { - return decodeURIComponent(str.replace(/\+/g, ' ')); - } catch (e) { - return str; - } -}; - -exports.encode = function (str) { - // This code was originally written by Brian White (mscdex) for the io.js core querystring library. - // It has been adapted here for stricter adherence to RFC 3986 - if (str.length === 0) { - return str; - } - - var string = typeof str === 'string' ? str : String(str); - - var out = ''; - for (var i = 0; i < string.length; ++i) { - var c = string.charCodeAt(i); - - if ( - c === 0x2D || // - - c === 0x2E || // . - c === 0x5F || // _ - c === 0x7E || // ~ - (c >= 0x30 && c <= 0x39) || // 0-9 - (c >= 0x41 && c <= 0x5A) || // a-z - (c >= 0x61 && c <= 0x7A) // A-Z - ) { - out += string.charAt(i); - continue; - } - - if (c < 0x80) { - out = out + hexTable[c]; - continue; - } - - if (c < 0x800) { - out = out + (hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]); - continue; - } - - if (c < 0xD800 || c >= 0xE000) { - out = out + (hexTable[0xE0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]); - continue; - } - - i += 1; - c = 0x10000 + (((c & 0x3FF) << 10) | (string.charCodeAt(i) & 0x3FF)); - out += (hexTable[0xF0 | (c >> 18)] + hexTable[0x80 | ((c >> 12) & 0x3F)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]); - } - - return out; -}; - -exports.compact = function (obj, references) { - if (typeof obj !== 'object' || obj === null) { - return obj; - } - - var refs = references || []; - var lookup = refs.indexOf(obj); - if (lookup !== -1) { - return refs[lookup]; - } - - refs.push(obj); - - if (Array.isArray(obj)) { - var compacted = []; - - for (var i = 0; i < obj.length; ++i) { - if (typeof obj[i] !== 'undefined') { - compacted.push(obj[i]); - } - } - - return compacted; - } - - var keys = Object.keys(obj); - for (var j = 0; j < keys.length; ++j) { - var key = keys[j]; - obj[key] = exports.compact(obj[key], refs); - } - - return obj; -}; - -exports.isRegExp = function (obj) { - return Object.prototype.toString.call(obj) === '[object RegExp]'; -}; - -exports.isBuffer = function (obj) { - if (obj === null || typeof obj === 'undefined') { - return false; - } - - return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); -}; diff --git a/node_modules/request/node_modules/qs/package.json b/node_modules/request/node_modules/qs/package.json deleted file mode 100644 index ed6b9f20..00000000 --- a/node_modules/request/node_modules/qs/package.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "name": "qs", - "description": "A querystring parser that supports nesting and arrays, with a depth limit", - "homepage": "https://github.com/ljharb/qs", - "version": "6.1.0", - "repository": { - "type": "git", - "url": "git+https://github.com/ljharb/qs.git" - }, - "main": "lib/index.js", - "contributors": [ - { - "name": "Jordan Harband", - "email": "ljharb@gmail.com", - "url": "http://ljharb.codes" - } - ], - "keywords": [ - "querystring", - "qs" - ], - "engines": { - "node": ">=0.6" - }, - "dependencies": {}, - "devDependencies": { - "browserify": "^12.0.1", - "tape": "^4.3.0", - "covert": "^1.1.0", - "mkdirp": "^0.5.1", - "eslint": "^1.10.3", - "@ljharb/eslint-config": "^1.6.1", - "parallelshell": "^2.0.0", - "evalmd": "^0.0.16" - }, - "scripts": { - "test": "parallelshell 'npm run readme' 'npm run lint' 'npm run coverage'", - "tests-only": "node test", - "readme": "evalmd README.md", - "lint": "eslint lib/*.js text/*.js", - "coverage": "covert test", - "dist": "mkdirp dist && browserify --standalone Qs lib/index.js > dist/qs.js", - "prepublish": "npm run dist" - }, - "license": "BSD-3-Clause", - "gitHead": "5bd79545edb33d6a43398fec7df9ecef2da005ea", - "bugs": { - "url": "https://github.com/ljharb/qs/issues" - }, - "_id": "qs@6.1.0", - "_shasum": "ec1d1626b24278d99f0fdf4549e524e24eceeb26", - "_from": "qs@>=6.1.0 <6.2.0", - "_npmVersion": "3.3.12", - "_nodeVersion": "5.5.0", - "_npmUser": { - "name": "ljharb", - "email": "ljharb@gmail.com" - }, - "dist": { - "shasum": "ec1d1626b24278d99f0fdf4549e524e24eceeb26", - "tarball": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz" - }, - "maintainers": [ - { - "name": "hueniverse", - "email": "eran@hammer.io" - }, - { - "name": "ljharb", - "email": "ljharb@gmail.com" - }, - { - "name": "nlf", - "email": "quitlahok@gmail.com" - } - ], - "_npmOperationalInternal": { - "host": "packages-5-east.internal.npmjs.com", - "tmp": "tmp/qs-6.1.0.tgz_1454565583082_0.44599376199766994" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/qs/test/index.js b/node_modules/request/node_modules/qs/test/index.js deleted file mode 100644 index b6a7d952..00000000 --- a/node_modules/request/node_modules/qs/test/index.js +++ /dev/null @@ -1,5 +0,0 @@ -require('./parse'); - -require('./stringify'); - -require('./utils'); diff --git a/node_modules/request/node_modules/qs/test/parse.js b/node_modules/request/node_modules/qs/test/parse.js deleted file mode 100755 index 5665d074..00000000 --- a/node_modules/request/node_modules/qs/test/parse.js +++ /dev/null @@ -1,393 +0,0 @@ -'use strict'; - -var test = require('tape'); -var qs = require('../'); - -test('parse()', function (t) { - t.test('parses a simple string', function (st) { - st.deepEqual(qs.parse('0=foo'), { '0': 'foo' }); - st.deepEqual(qs.parse('foo=c++'), { foo: 'c ' }); - st.deepEqual(qs.parse('a[>=]=23'), { a: { '>=': '23' } }); - st.deepEqual(qs.parse('a[<=>]==23'), { a: { '<=>': '=23' } }); - st.deepEqual(qs.parse('a[==]=23'), { a: { '==': '23' } }); - st.deepEqual(qs.parse('foo', { strictNullHandling: true }), { foo: null }); - st.deepEqual(qs.parse('foo'), { foo: '' }); - st.deepEqual(qs.parse('foo='), { foo: '' }); - st.deepEqual(qs.parse('foo=bar'), { foo: 'bar' }); - st.deepEqual(qs.parse(' foo = bar = baz '), { ' foo ': ' bar = baz ' }); - st.deepEqual(qs.parse('foo=bar=baz'), { foo: 'bar=baz' }); - st.deepEqual(qs.parse('foo=bar&bar=baz'), { foo: 'bar', bar: 'baz' }); - st.deepEqual(qs.parse('foo2=bar2&baz2='), { foo2: 'bar2', baz2: '' }); - st.deepEqual(qs.parse('foo=bar&baz', { strictNullHandling: true }), { foo: 'bar', baz: null }); - st.deepEqual(qs.parse('foo=bar&baz'), { foo: 'bar', baz: '' }); - st.deepEqual(qs.parse('cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'), { - cht: 'p3', - chd: 't:60,40', - chs: '250x100', - chl: 'Hello|World' - }); - st.end(); - }); - - t.test('allows enabling dot notation', function (st) { - st.deepEqual(qs.parse('a.b=c'), { 'a.b': 'c' }); - st.deepEqual(qs.parse('a.b=c', { allowDots: true }), { a: { b: 'c' } }); - st.end(); - }); - - t.deepEqual(qs.parse('a[b]=c'), { a: { b: 'c' } }, 'parses a single nested string'); - t.deepEqual(qs.parse('a[b][c]=d'), { a: { b: { c: 'd' } } }, 'parses a double nested string'); - t.deepEqual( - qs.parse('a[b][c][d][e][f][g][h]=i'), - { a: { b: { c: { d: { e: { f: { '[g][h]': 'i' } } } } } } }, - 'defaults to a depth of 5' - ); - - t.test('only parses one level when depth = 1', function (st) { - st.deepEqual(qs.parse('a[b][c]=d', { depth: 1 }), { a: { b: { '[c]': 'd' } } }); - st.deepEqual(qs.parse('a[b][c][d]=e', { depth: 1 }), { a: { b: { '[c][d]': 'e' } } }); - st.end(); - }); - - t.deepEqual(qs.parse('a=b&a=c'), { a: ['b', 'c'] }, 'parses a simple array'); - - t.test('parses an explicit array', function (st) { - st.deepEqual(qs.parse('a[]=b'), { a: ['b'] }); - st.deepEqual(qs.parse('a[]=b&a[]=c'), { a: ['b', 'c'] }); - st.deepEqual(qs.parse('a[]=b&a[]=c&a[]=d'), { a: ['b', 'c', 'd'] }); - st.end(); - }); - - t.test('parses a mix of simple and explicit arrays', function (st) { - st.deepEqual(qs.parse('a=b&a[]=c'), { a: ['b', 'c'] }); - st.deepEqual(qs.parse('a[]=b&a=c'), { a: ['b', 'c'] }); - st.deepEqual(qs.parse('a[0]=b&a=c'), { a: ['b', 'c'] }); - st.deepEqual(qs.parse('a=b&a[0]=c'), { a: ['b', 'c'] }); - st.deepEqual(qs.parse('a[1]=b&a=c'), { a: ['b', 'c'] }); - st.deepEqual(qs.parse('a=b&a[1]=c'), { a: ['b', 'c'] }); - st.end(); - }); - - t.test('parses a nested array', function (st) { - st.deepEqual(qs.parse('a[b][]=c&a[b][]=d'), { a: { b: ['c', 'd'] } }); - st.deepEqual(qs.parse('a[>=]=25'), { a: { '>=': '25' } }); - st.end(); - }); - - t.test('allows to specify array indices', function (st) { - st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] }); - st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] }); - st.deepEqual(qs.parse('a[1]=c'), { a: ['c'] }); - st.end(); - }); - - t.test('limits specific array indices to 20', function (st) { - st.deepEqual(qs.parse('a[20]=a'), { a: ['a'] }); - st.deepEqual(qs.parse('a[21]=a'), { a: { '21': 'a' } }); - st.end(); - }); - - t.deepEqual(qs.parse('a[12b]=c'), { a: { '12b': 'c' } }, 'supports keys that begin with a number'); - - t.test('supports encoded = signs', function (st) { - st.deepEqual(qs.parse('he%3Dllo=th%3Dere'), { 'he=llo': 'th=ere' }); - st.end(); - }); - - t.test('is ok with url encoded strings', function (st) { - st.deepEqual(qs.parse('a[b%20c]=d'), { a: { 'b c': 'd' } }); - st.deepEqual(qs.parse('a[b]=c%20d'), { a: { b: 'c d' } }); - st.end(); - }); - - t.test('allows brackets in the value', function (st) { - st.deepEqual(qs.parse('pets=["tobi"]'), { pets: '["tobi"]' }); - st.deepEqual(qs.parse('operators=[">=", "<="]'), { operators: '[">=", "<="]' }); - st.end(); - }); - - t.test('allows empty values', function (st) { - st.deepEqual(qs.parse(''), {}); - st.deepEqual(qs.parse(null), {}); - st.deepEqual(qs.parse(undefined), {}); - st.end(); - }); - - t.test('transforms arrays to objects', function (st) { - st.deepEqual(qs.parse('foo[0]=bar&foo[bad]=baz'), { foo: { '0': 'bar', bad: 'baz' } }); - st.deepEqual(qs.parse('foo[bad]=baz&foo[0]=bar'), { foo: { bad: 'baz', '0': 'bar' } }); - st.deepEqual(qs.parse('foo[bad]=baz&foo[]=bar'), { foo: { bad: 'baz', '0': 'bar' } }); - st.deepEqual(qs.parse('foo[]=bar&foo[bad]=baz'), { foo: { '0': 'bar', bad: 'baz' } }); - st.deepEqual(qs.parse('foo[bad]=baz&foo[]=bar&foo[]=foo'), { foo: { bad: 'baz', '0': 'bar', '1': 'foo' } }); - st.deepEqual(qs.parse('foo[0][a]=a&foo[0][b]=b&foo[1][a]=aa&foo[1][b]=bb'), { foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] }); - st.deepEqual(qs.parse('a[]=b&a[t]=u&a[hasOwnProperty]=c'), { a: { '0': 'b', t: 'u', c: true } }); - st.deepEqual(qs.parse('a[]=b&a[hasOwnProperty]=c&a[x]=y'), { a: { '0': 'b', '1': 'c', x: 'y' } }); - st.end(); - }); - - t.test('transforms arrays to objects (dot notation)', function (st) { - st.deepEqual(qs.parse('foo[0].baz=bar&fool.bad=baz', { allowDots: true }), { foo: [{ baz: 'bar' }], fool: { bad: 'baz' } }); - st.deepEqual(qs.parse('foo[0].baz=bar&fool.bad.boo=baz', { allowDots: true }), { foo: [{ baz: 'bar' }], fool: { bad: { boo: 'baz' } } }); - st.deepEqual(qs.parse('foo[0][0].baz=bar&fool.bad=baz', { allowDots: true }), { foo: [[{ baz: 'bar' }]], fool: { bad: 'baz' } }); - st.deepEqual(qs.parse('foo[0].baz[0]=15&foo[0].bar=2', { allowDots: true }), { foo: [{ baz: ['15'], bar: '2' }] }); - st.deepEqual(qs.parse('foo[0].baz[0]=15&foo[0].baz[1]=16&foo[0].bar=2', { allowDots: true }), { foo: [{ baz: ['15', '16'], bar: '2' }] }); - st.deepEqual(qs.parse('foo.bad=baz&foo[0]=bar', { allowDots: true }), { foo: { bad: 'baz', '0': 'bar' } }); - st.deepEqual(qs.parse('foo.bad=baz&foo[]=bar', { allowDots: true }), { foo: { bad: 'baz', '0': 'bar' } }); - st.deepEqual(qs.parse('foo[]=bar&foo.bad=baz', { allowDots: true }), { foo: { '0': 'bar', bad: 'baz' } }); - st.deepEqual(qs.parse('foo.bad=baz&foo[]=bar&foo[]=foo', { allowDots: true }), { foo: { bad: 'baz', '0': 'bar', '1': 'foo' } }); - st.deepEqual(qs.parse('foo[0].a=a&foo[0].b=b&foo[1].a=aa&foo[1].b=bb', { allowDots: true }), { foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] }); - st.end(); - }); - - t.deepEqual(qs.parse('a[b]=c&a=d'), { a: { b: 'c', d: true } }, 'can add keys to objects'); - - t.test('correctly prunes undefined values when converting an array to an object', function (st) { - st.deepEqual(qs.parse('a[2]=b&a[99999999]=c'), { a: { '2': 'b', '99999999': 'c' } }); - st.end(); - }); - - t.test('supports malformed uri characters', function (st) { - st.deepEqual(qs.parse('{%:%}', { strictNullHandling: true }), { '{%:%}': null }); - st.deepEqual(qs.parse('{%:%}='), { '{%:%}': '' }); - st.deepEqual(qs.parse('foo=%:%}'), { foo: '%:%}' }); - st.end(); - }); - - t.test('doesn\'t produce empty keys', function (st) { - st.deepEqual(qs.parse('_r=1&'), { '_r': '1' }); - st.end(); - }); - - t.test('cannot access Object prototype', function (st) { - qs.parse('constructor[prototype][bad]=bad'); - qs.parse('bad[constructor][prototype][bad]=bad'); - st.equal(typeof Object.prototype.bad, 'undefined'); - st.end(); - }); - - t.test('parses arrays of objects', function (st) { - st.deepEqual(qs.parse('a[][b]=c'), { a: [{ b: 'c' }] }); - st.deepEqual(qs.parse('a[0][b]=c'), { a: [{ b: 'c' }] }); - st.end(); - }); - - t.test('allows for empty strings in arrays', function (st) { - st.deepEqual(qs.parse('a[]=b&a[]=&a[]=c'), { a: ['b', '', 'c'] }); - st.deepEqual(qs.parse('a[0]=b&a[1]&a[2]=c&a[19]=', { strictNullHandling: true }), { a: ['b', null, 'c', ''] }); - st.deepEqual(qs.parse('a[0]=b&a[1]=&a[2]=c&a[19]', { strictNullHandling: true }), { a: ['b', '', 'c', null] }); - st.deepEqual(qs.parse('a[]=&a[]=b&a[]=c'), { a: ['', 'b', 'c'] }); - st.end(); - }); - - t.test('compacts sparse arrays', function (st) { - st.deepEqual(qs.parse('a[10]=1&a[2]=2'), { a: ['2', '1'] }); - st.end(); - }); - - t.test('parses semi-parsed strings', function (st) { - st.deepEqual(qs.parse({ 'a[b]': 'c' }), { a: { b: 'c' } }); - st.deepEqual(qs.parse({ 'a[b]': 'c', 'a[d]': 'e' }), { a: { b: 'c', d: 'e' } }); - st.end(); - }); - - t.test('parses buffers correctly', function (st) { - var b = new Buffer('test'); - st.deepEqual(qs.parse({ a: b }), { a: b }); - st.end(); - }); - - t.test('continues parsing when no parent is found', function (st) { - st.deepEqual(qs.parse('[]=&a=b'), { '0': '', a: 'b' }); - st.deepEqual(qs.parse('[]&a=b', { strictNullHandling: true }), { '0': null, a: 'b' }); - st.deepEqual(qs.parse('[foo]=bar'), { foo: 'bar' }); - st.end(); - }); - - t.test('does not error when parsing a very long array', function (st) { - var str = 'a[]=a'; - while (Buffer.byteLength(str) < 128 * 1024) { - str = str + '&' + str; - } - - st.doesNotThrow(function () { qs.parse(str); }); - - st.end(); - }); - - t.test('should not throw when a native prototype has an enumerable property', { parallel: false }, function (st) { - Object.prototype.crash = ''; - Array.prototype.crash = ''; - st.doesNotThrow(qs.parse.bind(null, 'a=b')); - st.deepEqual(qs.parse('a=b'), { a: 'b' }); - st.doesNotThrow(qs.parse.bind(null, 'a[][b]=c')); - st.deepEqual(qs.parse('a[][b]=c'), { a: [{ b: 'c' }] }); - delete Object.prototype.crash; - delete Array.prototype.crash; - st.end(); - }); - - t.test('parses a string with an alternative string delimiter', function (st) { - st.deepEqual(qs.parse('a=b;c=d', { delimiter: ';' }), { a: 'b', c: 'd' }); - st.end(); - }); - - t.test('parses a string with an alternative RegExp delimiter', function (st) { - st.deepEqual(qs.parse('a=b; c=d', { delimiter: /[;,] */ }), { a: 'b', c: 'd' }); - st.end(); - }); - - t.test('does not use non-splittable objects as delimiters', function (st) { - st.deepEqual(qs.parse('a=b&c=d', { delimiter: true }), { a: 'b', c: 'd' }); - st.end(); - }); - - t.test('allows overriding parameter limit', function (st) { - st.deepEqual(qs.parse('a=b&c=d', { parameterLimit: 1 }), { a: 'b' }); - st.end(); - }); - - t.test('allows setting the parameter limit to Infinity', function (st) { - st.deepEqual(qs.parse('a=b&c=d', { parameterLimit: Infinity }), { a: 'b', c: 'd' }); - st.end(); - }); - - t.test('allows overriding array limit', function (st) { - st.deepEqual(qs.parse('a[0]=b', { arrayLimit: -1 }), { a: { '0': 'b' } }); - st.deepEqual(qs.parse('a[-1]=b', { arrayLimit: -1 }), { a: { '-1': 'b' } }); - st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayLimit: 0 }), { a: { '0': 'b', '1': 'c' } }); - st.end(); - }); - - t.test('allows disabling array parsing', function (st) { - st.deepEqual(qs.parse('a[0]=b&a[1]=c', { parseArrays: false }), { a: { '0': 'b', '1': 'c' } }); - st.end(); - }); - - t.test('parses an object', function (st) { - var input = { - 'user[name]': { 'pop[bob]': 3 }, - 'user[email]': null - }; - - var expected = { - user: { - name: { 'pop[bob]': 3 }, - email: null - } - }; - - var result = qs.parse(input); - - st.deepEqual(result, expected); - st.end(); - }); - - t.test('parses an object in dot notation', function (st) { - var input = { - 'user.name': { 'pop[bob]': 3 }, - 'user.email.': null - }; - - var expected = { - user: { - name: { 'pop[bob]': 3 }, - email: null - } - }; - - var result = qs.parse(input, { allowDots: true }); - - st.deepEqual(result, expected); - st.end(); - }); - - t.test('parses an object and not child values', function (st) { - var input = { - 'user[name]': { 'pop[bob]': { 'test': 3 } }, - 'user[email]': null - }; - - var expected = { - user: { - name: { 'pop[bob]': { 'test': 3 } }, - email: null - } - }; - - var result = qs.parse(input); - - st.deepEqual(result, expected); - st.end(); - }); - - t.test('does not blow up when Buffer global is missing', function (st) { - var tempBuffer = global.Buffer; - delete global.Buffer; - var result = qs.parse('a=b&c=d'); - global.Buffer = tempBuffer; - st.deepEqual(result, { a: 'b', c: 'd' }); - st.end(); - }); - - t.test('does not crash when parsing circular references', function (st) { - var a = {}; - a.b = a; - - var parsed; - - st.doesNotThrow(function () { - parsed = qs.parse({ 'foo[bar]': 'baz', 'foo[baz]': a }); - }); - - st.equal('foo' in parsed, true, 'parsed has "foo" property'); - st.equal('bar' in parsed.foo, true); - st.equal('baz' in parsed.foo, true); - st.equal(parsed.foo.bar, 'baz'); - st.deepEqual(parsed.foo.baz, a); - st.end(); - }); - - t.test('parses plain objects correctly', function (st) { - var a = Object.create(null); - a.b = 'c'; - - st.deepEqual(qs.parse(a), { b: 'c' }); - var result = qs.parse({ a: a }); - st.equal('a' in result, true, 'result has "a" property'); - st.deepEqual(result.a, a); - st.end(); - }); - - t.test('parses dates correctly', function (st) { - var now = new Date(); - st.deepEqual(qs.parse({ a: now }), { a: now }); - st.end(); - }); - - t.test('parses regular expressions correctly', function (st) { - var re = /^test$/; - st.deepEqual(qs.parse({ a: re }), { a: re }); - st.end(); - }); - - t.test('can allow overwriting prototype properties', function (st) { - st.deepEqual(qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true }), { a: { hasOwnProperty: 'b' } }, { prototype: false }); - st.deepEqual(qs.parse('hasOwnProperty=b', { allowPrototypes: true }), { hasOwnProperty: 'b' }, { prototype: false }); - st.end(); - }); - - t.test('can return plain objects', function (st) { - var expected = Object.create(null); - expected.a = Object.create(null); - expected.a.b = 'c'; - expected.a.hasOwnProperty = 'd'; - st.deepEqual(qs.parse('a[b]=c&a[hasOwnProperty]=d', { plainObjects: true }), expected); - st.deepEqual(qs.parse(null, { plainObjects: true }), Object.create(null)); - var expectedArray = Object.create(null); - expectedArray.a = Object.create(null); - expectedArray.a['0'] = 'b'; - expectedArray.a.c = 'd'; - st.deepEqual(qs.parse('a[]=b&a[c]=d', { plainObjects: true }), expectedArray); - st.end(); - }); -}); diff --git a/node_modules/request/node_modules/qs/test/stringify.js b/node_modules/request/node_modules/qs/test/stringify.js deleted file mode 100755 index da72839d..00000000 --- a/node_modules/request/node_modules/qs/test/stringify.js +++ /dev/null @@ -1,259 +0,0 @@ -'use strict'; - -var test = require('tape'); -var qs = require('../'); - -test('stringify()', function (t) { - t.test('stringifies a querystring object', function (st) { - st.equal(qs.stringify({ a: 'b' }), 'a=b'); - st.equal(qs.stringify({ a: 1 }), 'a=1'); - st.equal(qs.stringify({ a: 1, b: 2 }), 'a=1&b=2'); - st.equal(qs.stringify({ a: 'A_Z' }), 'a=A_Z'); - st.equal(qs.stringify({ a: '€' }), 'a=%E2%82%AC'); - st.equal(qs.stringify({ a: '' }), 'a=%EE%80%80'); - st.equal(qs.stringify({ a: 'א' }), 'a=%D7%90'); - st.equal(qs.stringify({ a: '𐐷' }), 'a=%F0%90%90%B7'); - st.end(); - }); - - t.test('stringifies a nested object', function (st) { - st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); - st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e'); - st.end(); - }); - - t.test('stringifies a nested object with dots notation', function (st) { - st.equal(qs.stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c'); - st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e'); - st.end(); - }); - - t.test('stringifies an array value', function (st) { - st.equal(qs.stringify({ a: ['b', 'c', 'd'] }), 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d'); - st.end(); - }); - - t.test('omits nulls when asked', function (st) { - st.equal(qs.stringify({ a: 'b', c: null }, { skipNulls: true }), 'a=b'); - st.end(); - }); - - - t.test('omits nested nulls when asked', function (st) { - st.equal(qs.stringify({ a: { b: 'c', d: null } }, { skipNulls: true }), 'a%5Bb%5D=c'); - st.end(); - }); - - t.test('omits array indices when asked', function (st) { - st.equal(qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d'); - st.end(); - }); - - t.test('stringifies a nested array value', function (st) { - st.equal(qs.stringify({ a: { b: ['c', 'd'] } }), 'a%5Bb%5D%5B0%5D=c&a%5Bb%5D%5B1%5D=d'); - st.end(); - }); - - t.test('stringifies a nested array value with dots notation', function (st) { - st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encode: false }), 'a.b[0]=c&a.b[1]=d'); - st.end(); - }); - - t.test('stringifies an object inside an array', function (st) { - st.equal(qs.stringify({ a: [{ b: 'c' }] }), 'a%5B0%5D%5Bb%5D=c'); - st.equal(qs.stringify({ a: [{ b: { c: [1] } }] }), 'a%5B0%5D%5Bb%5D%5Bc%5D%5B0%5D=1'); - st.end(); - }); - - t.test('stringifies an object inside an array with dots notation', function (st) { - st.equal(qs.stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false }), 'a[0].b=c'); - st.equal(qs.stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false }), 'a[0].b.c[0]=1'); - st.end(); - }); - - t.test('does not omit object keys when indices = false', function (st) { - st.equal(qs.stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c'); - st.end(); - }); - - t.test('uses indices notation for arrays when indices=true', function (st) { - st.equal(qs.stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c'); - st.end(); - }); - - t.test('uses indices notation for arrays when no arrayFormat is specified', function (st) { - st.equal(qs.stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c'); - st.end(); - }); - - t.test('uses indices notation for arrays when no arrayFormat=indices', function (st) { - st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c'); - st.end(); - }); - - t.test('uses repeat notation for arrays when no arrayFormat=repeat', function (st) { - st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c'); - st.end(); - }); - - t.test('uses brackets notation for arrays when no arrayFormat=brackets', function (st) { - st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c'); - st.end(); - }); - - t.test('stringifies a complicated object', function (st) { - st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e'); - st.end(); - }); - - t.test('stringifies an empty value', function (st) { - st.equal(qs.stringify({ a: '' }), 'a='); - st.equal(qs.stringify({ a: null }, { strictNullHandling: true }), 'a'); - - st.equal(qs.stringify({ a: '', b: '' }), 'a=&b='); - st.equal(qs.stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b='); - - st.equal(qs.stringify({ a: { b: '' } }), 'a%5Bb%5D='); - st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D'); - st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D='); - - st.end(); - }); - - t.test('stringifies an empty object', function (st) { - var obj = Object.create(null); - obj.a = 'b'; - st.equal(qs.stringify(obj), 'a=b'); - st.end(); - }); - - t.test('returns an empty string for invalid input', function (st) { - st.equal(qs.stringify(undefined), ''); - st.equal(qs.stringify(false), ''); - st.equal(qs.stringify(null), ''); - st.equal(qs.stringify(''), ''); - st.end(); - }); - - t.test('stringifies an object with an empty object as a child', function (st) { - var obj = { - a: Object.create(null) - }; - - obj.a.b = 'c'; - st.equal(qs.stringify(obj), 'a%5Bb%5D=c'); - st.end(); - }); - - t.test('drops keys with a value of undefined', function (st) { - st.equal(qs.stringify({ a: undefined }), ''); - - st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), 'a%5Bc%5D'); - st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), 'a%5Bc%5D='); - st.equal(qs.stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D='); - st.end(); - }); - - t.test('url encodes values', function (st) { - st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c'); - st.end(); - }); - - t.test('stringifies a date', function (st) { - var now = new Date(); - var str = 'a=' + encodeURIComponent(now.toISOString()); - st.equal(qs.stringify({ a: now }), str); - st.end(); - }); - - t.test('stringifies the weird object from qs', function (st) { - st.equal(qs.stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F'); - st.end(); - }); - - t.test('skips properties that are part of the object prototype', function (st) { - Object.prototype.crash = 'test'; - st.equal(qs.stringify({ a: 'b' }), 'a=b'); - st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); - delete Object.prototype.crash; - st.end(); - }); - - t.test('stringifies boolean values', function (st) { - st.equal(qs.stringify({ a: true }), 'a=true'); - st.equal(qs.stringify({ a: { b: true } }), 'a%5Bb%5D=true'); - st.equal(qs.stringify({ b: false }), 'b=false'); - st.equal(qs.stringify({ b: { c: false } }), 'b%5Bc%5D=false'); - st.end(); - }); - - t.test('stringifies buffer values', function (st) { - st.equal(qs.stringify({ a: new Buffer('test') }), 'a=test'); - st.equal(qs.stringify({ a: { b: new Buffer('test') } }), 'a%5Bb%5D=test'); - st.end(); - }); - - t.test('stringifies an object using an alternative delimiter', function (st) { - st.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d'); - st.end(); - }); - - t.test('doesn\'t blow up when Buffer global is missing', function (st) { - var tempBuffer = global.Buffer; - delete global.Buffer; - var result = qs.stringify({ a: 'b', c: 'd' }); - global.Buffer = tempBuffer; - st.equal(result, 'a=b&c=d'); - st.end(); - }); - - t.test('selects properties when filter=array', function (st) { - st.equal(qs.stringify({ a: 'b' }, { filter: ['a'] }), 'a=b'); - st.equal(qs.stringify({ a: 1 }, { filter: [] }), ''); - st.equal(qs.stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] }), 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3'); - st.end(); - }); - - t.test('supports custom representations when filter=function', function (st) { - var calls = 0; - var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } }; - var filterFunc = function (prefix, value) { - calls++; - if (calls === 1) { - st.equal(prefix, '', 'prefix is empty'); - st.equal(value, obj); - } else if (prefix === 'c') { - return; - } else if (value instanceof Date) { - st.equal(prefix, 'e[f]'); - return value.getTime(); - } - return value; - }; - - st.equal(qs.stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000'); - st.equal(calls, 5); - st.end(); - }); - - t.test('can disable uri encoding', function (st) { - st.equal(qs.stringify({ a: 'b' }, { encode: false }), 'a=b'); - st.equal(qs.stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c'); - st.equal(qs.stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), 'a=b&c'); - st.end(); - }); - - t.test('can sort the keys', function (st) { - var sort = function (a, b) { return a.localeCompare(b); }; - st.equal(qs.stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y'); - st.equal(qs.stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a'); - st.end(); - }); - - t.test('can sort the keys at depth 3 or more too', function (st) { - var sort = function (a, b) { return a.localeCompare(b); }; - st.equal(qs.stringify({ a: 'a', z: { zj: {zjb: 'zjb', zja: 'zja'}, zi: {zib: 'zib', zia: 'zia'} }, b: 'b' }, { sort: sort, encode: false }), 'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'); - st.equal(qs.stringify({ a: 'a', z: { zj: {zjb: 'zjb', zja: 'zja'}, zi: {zib: 'zib', zia: 'zia'} }, b: 'b' }, { sort: null, encode: false }), 'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'); - st.end(); - }); -}); diff --git a/node_modules/request/node_modules/qs/test/utils.js b/node_modules/request/node_modules/qs/test/utils.js deleted file mode 100755 index 4a8d8246..00000000 --- a/node_modules/request/node_modules/qs/test/utils.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -var test = require('tape'); -var utils = require('../lib/utils'); - -test('merge()', function (t) { - t.deepEqual(utils.merge({ a: 'b' }, { a: 'c' }), { a: ['b', 'c'] }, 'merges two objects with the same key'); - t.end(); -}); diff --git a/node_modules/request/node_modules/stringstream/.npmignore b/node_modules/request/node_modules/stringstream/.npmignore deleted file mode 100644 index 7dccd970..00000000 --- a/node_modules/request/node_modules/stringstream/.npmignore +++ /dev/null @@ -1,15 +0,0 @@ -lib-cov -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.gz - -pids -logs -results - -node_modules -npm-debug.log \ No newline at end of file diff --git a/node_modules/request/node_modules/stringstream/.travis.yml b/node_modules/request/node_modules/stringstream/.travis.yml deleted file mode 100644 index f1d0f13c..00000000 --- a/node_modules/request/node_modules/stringstream/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -node_js: - - 0.4 - - 0.6 diff --git a/node_modules/request/node_modules/stringstream/LICENSE.txt b/node_modules/request/node_modules/stringstream/LICENSE.txt deleted file mode 100644 index ab861acd..00000000 --- a/node_modules/request/node_modules/stringstream/LICENSE.txt +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2012 Michael Hart (michael.hart.au@gmail.com) - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/request/node_modules/stringstream/README.md b/node_modules/request/node_modules/stringstream/README.md deleted file mode 100644 index 32fc9825..00000000 --- a/node_modules/request/node_modules/stringstream/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Decode streams into strings The Right Way(tm) - -```javascript -var fs = require('fs') -var zlib = require('zlib') -var strs = require('stringstream') - -var utf8Stream = fs.createReadStream('massiveLogFile.gz') - .pipe(zlib.createGunzip()) - .pipe(strs('utf8')) -``` - -No need to deal with `setEncoding()` weirdness, just compose streams -like they were supposed to be! - -Handles input and output encoding: - -```javascript -// Stream from utf8 to hex to base64... Why not, ay. -var hex64Stream = fs.createReadStream('myFile') - .pipe(strs('utf8', 'hex')) - .pipe(strs('hex', 'base64')) -``` - -Also deals with `base64` output correctly by aligning each emitted data -chunk so that there are no dangling `=` characters: - -```javascript -var stream = fs.createReadStream('myFile').pipe(strs('base64')) - -var base64Str = '' - -stream.on('data', function(data) { base64Str += data }) -stream.on('end', function() { - console.log('My base64 encoded file is: ' + base64Str) // Wouldn't work with setEncoding() - console.log('Original file is: ' + new Buffer(base64Str, 'base64')) -}) -``` diff --git a/node_modules/request/node_modules/stringstream/example.js b/node_modules/request/node_modules/stringstream/example.js deleted file mode 100644 index f82b85ed..00000000 --- a/node_modules/request/node_modules/stringstream/example.js +++ /dev/null @@ -1,27 +0,0 @@ -var fs = require('fs') -var zlib = require('zlib') -var strs = require('stringstream') - -var utf8Stream = fs.createReadStream('massiveLogFile.gz') - .pipe(zlib.createGunzip()) - .pipe(strs('utf8')) - -utf8Stream.pipe(process.stdout) - -// Stream from utf8 to hex to base64... Why not, ay. -var hex64Stream = fs.createReadStream('myFile') - .pipe(strs('utf8', 'hex')) - .pipe(strs('hex', 'base64')) - -hex64Stream.pipe(process.stdout) - -// Deals with base64 correctly by aligning chunks -var stream = fs.createReadStream('myFile').pipe(strs('base64')) - -var base64Str = '' - -stream.on('data', function(data) { base64Str += data }) -stream.on('end', function() { - console.log('My base64 encoded file is: ' + base64Str) // Wouldn't work with setEncoding() - console.log('Original file is: ' + new Buffer(base64Str, 'base64')) -}) diff --git a/node_modules/request/node_modules/stringstream/package.json b/node_modules/request/node_modules/stringstream/package.json deleted file mode 100644 index 2c6d7cdc..00000000 --- a/node_modules/request/node_modules/stringstream/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "stringstream", - "version": "0.0.5", - "description": "Encode and decode streams into string streams", - "author": { - "name": "Michael Hart", - "email": "michael.hart.au@gmail.com", - "url": "http://github.com/mhart" - }, - "main": "stringstream.js", - "keywords": [ - "string", - "stream", - "base64", - "gzip" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/mhart/StringStream.git" - }, - "license": "MIT", - "gitHead": "1efe3bf507bf3a1161f8473908b60e881d41422b", - "bugs": { - "url": "https://github.com/mhart/StringStream/issues" - }, - "homepage": "https://github.com/mhart/StringStream#readme", - "_id": "stringstream@0.0.5", - "scripts": {}, - "_shasum": "4e484cd4de5a0bbbee18e46307710a8a81621878", - "_from": "stringstream@>=0.0.4 <0.1.0", - "_npmVersion": "2.14.8", - "_nodeVersion": "4.2.1", - "_npmUser": { - "name": "hichaelmart", - "email": "michael.hart.au@gmail.com" - }, - "maintainers": [ - { - "name": "hichaelmart", - "email": "michael.hart.au@gmail.com" - } - ], - "dist": { - "shasum": "4e484cd4de5a0bbbee18e46307710a8a81621878", - "tarball": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/stringstream/stringstream.js b/node_modules/request/node_modules/stringstream/stringstream.js deleted file mode 100644 index 4ece1275..00000000 --- a/node_modules/request/node_modules/stringstream/stringstream.js +++ /dev/null @@ -1,102 +0,0 @@ -var util = require('util') -var Stream = require('stream') -var StringDecoder = require('string_decoder').StringDecoder - -module.exports = StringStream -module.exports.AlignedStringDecoder = AlignedStringDecoder - -function StringStream(from, to) { - if (!(this instanceof StringStream)) return new StringStream(from, to) - - Stream.call(this) - - if (from == null) from = 'utf8' - - this.readable = this.writable = true - this.paused = false - this.toEncoding = (to == null ? from : to) - this.fromEncoding = (to == null ? '' : from) - this.decoder = new AlignedStringDecoder(this.toEncoding) -} -util.inherits(StringStream, Stream) - -StringStream.prototype.write = function(data) { - if (!this.writable) { - var err = new Error('stream not writable') - err.code = 'EPIPE' - this.emit('error', err) - return false - } - if (this.fromEncoding) { - if (Buffer.isBuffer(data)) data = data.toString() - data = new Buffer(data, this.fromEncoding) - } - var string = this.decoder.write(data) - if (string.length) this.emit('data', string) - return !this.paused -} - -StringStream.prototype.flush = function() { - if (this.decoder.flush) { - var string = this.decoder.flush() - if (string.length) this.emit('data', string) - } -} - -StringStream.prototype.end = function() { - if (!this.writable && !this.readable) return - this.flush() - this.emit('end') - this.writable = this.readable = false - this.destroy() -} - -StringStream.prototype.destroy = function() { - this.decoder = null - this.writable = this.readable = false - this.emit('close') -} - -StringStream.prototype.pause = function() { - this.paused = true -} - -StringStream.prototype.resume = function () { - if (this.paused) this.emit('drain') - this.paused = false -} - -function AlignedStringDecoder(encoding) { - StringDecoder.call(this, encoding) - - switch (this.encoding) { - case 'base64': - this.write = alignedWrite - this.alignedBuffer = new Buffer(3) - this.alignedBytes = 0 - break - } -} -util.inherits(AlignedStringDecoder, StringDecoder) - -AlignedStringDecoder.prototype.flush = function() { - if (!this.alignedBuffer || !this.alignedBytes) return '' - var leftover = this.alignedBuffer.toString(this.encoding, 0, this.alignedBytes) - this.alignedBytes = 0 - return leftover -} - -function alignedWrite(buffer) { - var rem = (this.alignedBytes + buffer.length) % this.alignedBuffer.length - if (!rem && !this.alignedBytes) return buffer.toString(this.encoding) - - var returnBuffer = new Buffer(this.alignedBytes + buffer.length - rem) - - this.alignedBuffer.copy(returnBuffer, 0, 0, this.alignedBytes) - buffer.copy(returnBuffer, this.alignedBytes, 0, buffer.length - rem) - - buffer.copy(this.alignedBuffer, 0, buffer.length - rem, buffer.length) - this.alignedBytes = rem - - return returnBuffer.toString(this.encoding) -} diff --git a/node_modules/request/node_modules/tough-cookie/LICENSE b/node_modules/request/node_modules/tough-cookie/LICENSE deleted file mode 100644 index 1bc286fb..00000000 --- a/node_modules/request/node_modules/tough-cookie/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2015, Salesforce.com, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -=== - -The following exceptions apply: - -=== - -`public_suffix_list.dat` was obtained from - via -. The license for this file is MPL/2.0. The header of -that file reads as follows: - - // This Source Code Form is subject to the terms of the Mozilla Public - // License, v. 2.0. If a copy of the MPL was not distributed with this - // file, You can obtain one at http://mozilla.org/MPL/2.0/. diff --git a/node_modules/request/node_modules/tough-cookie/README.md b/node_modules/request/node_modules/tough-cookie/README.md deleted file mode 100644 index 9899dbf6..00000000 --- a/node_modules/request/node_modules/tough-cookie/README.md +++ /dev/null @@ -1,492 +0,0 @@ -[RFC6265](https://tools.ietf.org/html/rfc6265) Cookies and CookieJar for Node.js - -[![Build Status](https://travis-ci.org/SalesforceEng/tough-cookie.png?branch=master)](https://travis-ci.org/SalesforceEng/tough-cookie) - -[![NPM Stats](https://nodei.co/npm/tough-cookie.png?downloads=true&stars=true)](https://npmjs.org/package/tough-cookie) -![NPM Downloads](https://nodei.co/npm-dl/tough-cookie.png?months=9) - -# Synopsis - -``` javascript -var tough = require('tough-cookie'); -var Cookie = tough.Cookie; -var cookie = Cookie.parse(header); -cookie.value = 'somethingdifferent'; -header = cookie.toString(); - -var cookiejar = new tough.CookieJar(); -cookiejar.setCookie(cookie, 'http://currentdomain.example.com/path', cb); -// ... -cookiejar.getCookies('http://example.com/otherpath',function(err,cookies) { - res.headers['cookie'] = cookies.join('; '); -}); -``` - -# Installation - -It's _so_ easy! - -`npm install tough-cookie` - -Why the name? NPM modules `cookie`, `cookies` and `cookiejar` were already taken. - -# API - -## tough - -Functions on the module you get from `require('tough-cookie')`. All can be used as pure functions and don't need to be "bound". - -**Note**: prior to 1.0.x, several of these functions took a `strict` parameter. This has since been removed from the API as it was no longer necessary. - -### `parseDate(string)` - -Parse a cookie date string into a `Date`. Parses according to RFC6265 Section 5.1.1, not `Date.parse()`. - -### `formatDate(date)` - -Format a Date into a RFC1123 string (the RFC6265-recommended format). - -### `canonicalDomain(str)` - -Transforms a domain-name into a canonical domain-name. The canonical domain-name is a trimmed, lowercased, stripped-of-leading-dot and optionally punycode-encoded domain-name (Section 5.1.2 of RFC6265). For the most part, this function is idempotent (can be run again on its output without ill effects). - -### `domainMatch(str,domStr[,canonicalize=true])` - -Answers "does this real domain match the domain in a cookie?". The `str` is the "current" domain-name and the `domStr` is the "cookie" domain-name. Matches according to RFC6265 Section 5.1.3, but it helps to think of it as a "suffix match". - -The `canonicalize` parameter will run the other two paramters through `canonicalDomain` or not. - -### `defaultPath(path)` - -Given a current request/response path, gives the Path apropriate for storing in a cookie. This is basically the "directory" of a "file" in the path, but is specified by Section 5.1.4 of the RFC. - -The `path` parameter MUST be _only_ the pathname part of a URI (i.e. excludes the hostname, query, fragment, etc.). This is the `.pathname` property of node's `uri.parse()` output. - -### `pathMatch(reqPath,cookiePath)` - -Answers "does the request-path path-match a given cookie-path?" as per RFC6265 Section 5.1.4. Returns a boolean. - -This is essentially a prefix-match where `cookiePath` is a prefix of `reqPath`. - -### `parse(cookieString[, options])` - -alias for `Cookie.parse(cookieString[, options])` - -### `fromJSON(string)` - -alias for `Cookie.fromJSON(string)` - -### `getPublicSuffix(hostname)` - -Returns the public suffix of this hostname. The public suffix is the shortest domain-name upon which a cookie can be set. Returns `null` if the hostname cannot have cookies set for it. - -For example: `www.example.com` and `www.subdomain.example.com` both have public suffix `example.com`. - -For further information, see http://publicsuffix.org/. This module derives its list from that site. - -### `cookieCompare(a,b)` - -For use with `.sort()`, sorts a list of cookies into the recommended order given in the RFC (Section 5.4 step 2). The sort algorithm is, in order of precedence: - -* Longest `.path` -* oldest `.creation` (which has a 1ms precision, same as `Date`) -* lowest `.creationIndex` (to get beyond the 1ms precision) - -``` javascript -var cookies = [ /* unsorted array of Cookie objects */ ]; -cookies = cookies.sort(cookieCompare); -``` - -**Note**: Since JavaScript's `Date` is limited to a 1ms precision, cookies within the same milisecond are entirely possible. This is especially true when using the `now` option to `.setCookie()`. The `.creationIndex` property is a per-process global counter, assigned during construction with `new Cookie()`. This preserves the spirit of the RFC sorting: older cookies go first. This works great for `MemoryCookieStore`, since `Set-Cookie` headers are parsed in order, but may not be so great for distributed systems. Sophisticated `Store`s may wish to set this to some other _logical clock_ such that if cookies A and B are created in the same millisecond, but cookie A is created before cookie B, then `A.creationIndex < B.creationIndex`. If you want to alter the global counter, which you probably _shouldn't_ do, it's stored in `Cookie.cookiesCreated`. - -### `permuteDomain(domain)` - -Generates a list of all possible domains that `domainMatch()` the parameter. May be handy for implementing cookie stores. - -### `permutePath(path)` - -Generates a list of all possible paths that `pathMatch()` the parameter. May be handy for implementing cookie stores. - - -## Cookie - -Exported via `tough.Cookie`. - -### `Cookie.parse(cookieString[, options])` - -Parses a single Cookie or Set-Cookie HTTP header into a `Cookie` object. Returns `undefined` if the string can't be parsed. - -The options parameter is not required and currently has only one property: - - * _loose_ - boolean - if `true` enable parsing of key-less cookies like `=abc` and `=`, which are not RFC-compliant. - -If options is not an object, it is ignored, which means you can use `Array#map` with it. - -Here's how to process the Set-Cookie header(s) on a node HTTP/HTTPS response: - -``` javascript -if (res.headers['set-cookie'] instanceof Array) - cookies = res.headers['set-cookie'].map(Cookie.parse); -else - cookies = [Cookie.parse(res.headers['set-cookie'])]; -``` - -### Properties - -Cookie object properties: - - * _key_ - string - the name or key of the cookie (default "") - * _value_ - string - the value of the cookie (default "") - * _expires_ - `Date` - if set, the `Expires=` attribute of the cookie (defaults to the string `"Infinity"`). See `setExpires()` - * _maxAge_ - seconds - if set, the `Max-Age=` attribute _in seconds_ of the cookie. May also be set to strings `"Infinity"` and `"-Infinity"` for non-expiry and immediate-expiry, respectively. See `setMaxAge()` - * _domain_ - string - the `Domain=` attribute of the cookie - * _path_ - string - the `Path=` of the cookie - * _secure_ - boolean - the `Secure` cookie flag - * _httpOnly_ - boolean - the `HttpOnly` cookie flag - * _extensions_ - `Array` - any unrecognized cookie attributes as strings (even if equal-signs inside) - * _creation_ - `Date` - when this cookie was constructed - * _creationIndex_ - number - set at construction, used to provide greater sort precision (please see `cookieCompare(a,b)` for a full explanation) - -After a cookie has been passed through `CookieJar.setCookie()` it will have the following additional attributes: - - * _hostOnly_ - boolean - is this a host-only cookie (i.e. no Domain field was set, but was instead implied) - * _pathIsDefault_ - boolean - if true, there was no Path field on the cookie and `defaultPath()` was used to derive one. - * _creation_ - `Date` - **modified** from construction to when the cookie was added to the jar - * _lastAccessed_ - `Date` - last time the cookie got accessed. Will affect cookie cleaning once implemented. Using `cookiejar.getCookies(...)` will update this attribute. - -### `Cookie([{properties}])` - -Receives an options object that can contain any of the above Cookie properties, uses the default for unspecified properties. - -### `.toString()` - -encode to a Set-Cookie header value. The Expires cookie field is set using `formatDate()`, but is omitted entirely if `.expires` is `Infinity`. - -### `.cookieString()` - -encode to a Cookie header value (i.e. the `.key` and `.value` properties joined with '='). - -### `.setExpires(String)` - -sets the expiry based on a date-string passed through `parseDate()`. If parseDate returns `null` (i.e. can't parse this date string), `.expires` is set to `"Infinity"` (a string) is set. - -### `.setMaxAge(number)` - -sets the maxAge in seconds. Coerces `-Infinity` to `"-Infinity"` and `Infinity` to `"Infinity"` so it JSON serializes correctly. - -### `.expiryTime([now=Date.now()])` - -### `.expiryDate([now=Date.now()])` - -expiryTime() Computes the absolute unix-epoch milliseconds that this cookie expires. expiryDate() works similarly, except it returns a `Date` object. Note that in both cases the `now` parameter should be milliseconds. - -Max-Age takes precedence over Expires (as per the RFC). The `.creation` attribute -- or, by default, the `now` paramter -- is used to offset the `.maxAge` attribute. - -If Expires (`.expires`) is set, that's returned. - -Otherwise, `expiryTime()` returns `Infinity` and `expiryDate()` returns a `Date` object for "Tue, 19 Jan 2038 03:14:07 GMT" (latest date that can be expressed by a 32-bit `time_t`; the common limit for most user-agents). - -### `.TTL([now=Date.now()])` - -compute the TTL relative to `now` (milliseconds). The same precedence rules as for `expiryTime`/`expiryDate` apply. - -The "number" `Infinity` is returned for cookies without an explicit expiry and `0` is returned if the cookie is expired. Otherwise a time-to-live in milliseconds is returned. - -### `.canonicalizedDoman()` - -### `.cdomain()` - -return the canonicalized `.domain` field. This is lower-cased and punycode (RFC3490) encoded if the domain has any non-ASCII characters. - -### `.toJSON()` - -For convenience in using `JSON.serialize(cookie)`. Returns a plain-old `Object` that can be JSON-serialized. - -Any `Date` properties (i.e., `.expires`, `.creation`, and `.lastAccessed`) are exported in ISO format (`.toISOString()`). - -**NOTE**: Custom `Cookie` properties will be discarded. In tough-cookie 1.x, since there was no `.toJSON` method explicitly defined, all enumerable properties were captured. If you want a property to be serialized, add the property name to the `Cookie.serializableProperties` Array. - -### `Cookie.fromJSON(strOrObj)` - -Does the reverse of `cookie.toJSON()`. If passed a string, will `JSON.parse()` that first. - -Any `Date` properties (i.e., `.expires`, `.creation`, and `.lastAccessed`) are parsed via `Date.parse()`, not the tough-cookie `parseDate`, since it's JavaScript/JSON-y timestamps being handled at this layer. - -Returns `null` upon JSON parsing error. - -### `.clone()` - -Does a deep clone of this cookie, exactly implemented as `Cookie.fromJSON(cookie.toJSON())`. - -### `.validate()` - -Status: *IN PROGRESS*. Works for a few things, but is by no means comprehensive. - -validates cookie attributes for semantic correctness. Useful for "lint" checking any Set-Cookie headers you generate. For now, it returns a boolean, but eventually could return a reason string -- you can future-proof with this construct: - -``` javascript -if (cookie.validate() === true) { - // it's tasty -} else { - // yuck! -} -``` - - -## CookieJar - -Exported via `tough.CookieJar`. - -### `CookieJar([store],[options])` - -Simply use `new CookieJar()`. If you'd like to use a custom store, pass that to the constructor otherwise a `MemoryCookieStore` will be created and used. - -The `options` object can be omitted and can have the following properties: - - * _rejectPublicSuffixes_ - boolean - default `true` - reject cookies with domains like "com" and "co.uk" - * _looseMode_ - boolean - default `false` - accept malformed cookies like `bar` and `=bar`, which have an implied empty name. - This is not in the standard, but is used sometimes on the web and is accepted by (most) browsers. - -Since eventually this module would like to support database/remote/etc. CookieJars, continuation passing style is used for CookieJar methods. - -### `.setCookie(cookieOrString, currentUrl, [{options},] cb(err,cookie))` - -Attempt to set the cookie in the cookie jar. If the operation fails, an error will be given to the callback `cb`, otherwise the cookie is passed through. The cookie will have updated `.creation`, `.lastAccessed` and `.hostOnly` properties. - -The `options` object can be omitted and can have the following properties: - - * _http_ - boolean - default `true` - indicates if this is an HTTP or non-HTTP API. Affects HttpOnly cookies. - * _secure_ - boolean - autodetect from url - indicates if this is a "Secure" API. If the currentUrl starts with `https:` or `wss:` then this is defaulted to `true`, otherwise `false`. - * _now_ - Date - default `new Date()` - what to use for the creation/access time of cookies - * _ignoreError_ - boolean - default `false` - silently ignore things like parse errors and invalid domains. `Store` errors aren't ignored by this option. - -As per the RFC, the `.hostOnly` property is set if there was no "Domain=" parameter in the cookie string (or `.domain` was null on the Cookie object). The `.domain` property is set to the fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an exact hostname match (not a `domainMatch` as per usual). - -### `.setCookieSync(cookieOrString, currentUrl, [{options}])` - -Synchronous version of `setCookie`; only works with synchronous stores (e.g. the default `MemoryCookieStore`). - -### `.getCookies(currentUrl, [{options},] cb(err,cookies))` - -Retrieve the list of cookies that can be sent in a Cookie header for the current url. - -If an error is encountered, that's passed as `err` to the callback, otherwise an `Array` of `Cookie` objects is passed. The array is sorted with `cookieCompare()` unless the `{sort:false}` option is given. - -The `options` object can be omitted and can have the following properties: - - * _http_ - boolean - default `true` - indicates if this is an HTTP or non-HTTP API. Affects HttpOnly cookies. - * _secure_ - boolean - autodetect from url - indicates if this is a "Secure" API. If the currentUrl starts with `https:` or `wss:` then this is defaulted to `true`, otherwise `false`. - * _now_ - Date - default `new Date()` - what to use for the creation/access time of cookies - * _expire_ - boolean - default `true` - perform expiry-time checking of cookies and asynchronously remove expired cookies from the store. Using `false` will return expired cookies and **not** remove them from the store (which is useful for replaying Set-Cookie headers, potentially). - * _allPaths_ - boolean - default `false` - if `true`, do not scope cookies by path. The default uses RFC-compliant path scoping. **Note**: may not be supported by the underlying store (the default `MemoryCookieStore` supports it). - -The `.lastAccessed` property of the returned cookies will have been updated. - -### `.getCookiesSync(currentUrl, [{options}])` - -Synchronous version of `getCookies`; only works with synchronous stores (e.g. the default `MemoryCookieStore`). - -### `.getCookieString(...)` - -Accepts the same options as `.getCookies()` but passes a string suitable for a Cookie header rather than an array to the callback. Simply maps the `Cookie` array via `.cookieString()`. - -### `.getCookieStringSync(...)` - -Synchronous version of `getCookieString`; only works with synchronous stores (e.g. the default `MemoryCookieStore`). - -### `.getSetCookieStrings(...)` - -Returns an array of strings suitable for **Set-Cookie** headers. Accepts the same options as `.getCookies()`. Simply maps the cookie array via `.toString()`. - -### `.getSetCookieStringsSync(...)` - -Synchronous version of `getSetCookieStrings`; only works with synchronous stores (e.g. the default `MemoryCookieStore`). - -### `.serialize(cb(err,serializedObject))` - -Serialize the Jar if the underlying store supports `.getAllCookies`. - -**NOTE**: Custom `Cookie` properties will be discarded. If you want a property to be serialized, add the property name to the `Cookie.serializableProperties` Array. - -See [Serialization Format]. - -### `.serializeSync()` - -Sync version of .serialize - -### `.toJSON()` - -Alias of .serializeSync() for the convenience of `JSON.stringify(cookiejar)`. - -### `CookieJar.deserialize(serialized, [store], cb(err,object))` - -A new Jar is created and the serialized Cookies are added to the underlying store. Each `Cookie` is added via `store.putCookie` in the order in which they appear in the serialization. - -The `store` argument is optional, but should be an instance of `Store`. By default, a new instance of `MemoryCookieStore` is created. - -As a convenience, if `serialized` is a string, it is passed through `JSON.parse` first. If that throws an error, this is passed to the callback. - -### `CookieJar.deserializeSync(serialized, [store])` - -Sync version of `.deserialize`. _Note_ that the `store` must be synchronous for this to work. - -### `CookieJar.fromJSON(string)` - -Alias of `.deserializeSync` to provide consistency with `Cookie.fromJSON()`. - -### `.clone([store,]cb(err,newJar))` - -Produces a deep clone of this jar. Modifications to the original won't affect the clone, and vice versa. - -The `store` argument is optional, but should be an instance of `Store`. By default, a new instance of `MemoryCookieStore` is created. Transferring between store types is supported so long as the source implements `.getAllCookies()` and the destination implements `.putCookie()`. - -### `.cloneSync([store])` - -Synchronous version of `.clone`, returning a new `CookieJar` instance. - -The `store` argument is optional, but must be a _synchronous_ `Store` instance if specified. If not passed, a new instance of `MemoryCookieStore` is used. - -The _source_ and _destination_ must both be synchronous `Store`s. If one or both stores are asynchronous, use `.clone` instead. Recall that `MemoryCookieStore` supports both synchronous and asynchronous API calls. - -## Store - -Base class for CookieJar stores. Available as `tough.Store`. - -## Store API - -The storage model for each `CookieJar` instance can be replaced with a custom implementation. The default is `MemoryCookieStore` which can be found in the `lib/memstore.js` file. The API uses continuation-passing-style to allow for asynchronous stores. - -Stores should inherit from the base `Store` class, which is available as `require('tough-cookie').Store`. - -Stores are asynchronous by default, but if `store.synchronous` is set to `true`, then the `*Sync` methods on the of the containing `CookieJar` can be used (however, the continuation-passing style - -All `domain` parameters will have been normalized before calling. - -The Cookie store must have all of the following methods. - -### `store.findCookie(domain, path, key, cb(err,cookie))` - -Retrieve a cookie with the given domain, path and key (a.k.a. name). The RFC maintains that exactly one of these cookies should exist in a store. If the store is using versioning, this means that the latest/newest such cookie should be returned. - -Callback takes an error and the resulting `Cookie` object. If no cookie is found then `null` MUST be passed instead (i.e. not an error). - -### `store.findCookies(domain, path, cb(err,cookies))` - -Locates cookies matching the given domain and path. This is most often called in the context of `cookiejar.getCookies()` above. - -If no cookies are found, the callback MUST be passed an empty array. - -The resulting list will be checked for applicability to the current request according to the RFC (domain-match, path-match, http-only-flag, secure-flag, expiry, etc.), so it's OK to use an optimistic search algorithm when implementing this method. However, the search algorithm used SHOULD try to find cookies that `domainMatch()` the domain and `pathMatch()` the path in order to limit the amount of checking that needs to be done. - -As of version 0.9.12, the `allPaths` option to `cookiejar.getCookies()` above will cause the path here to be `null`. If the path is `null`, path-matching MUST NOT be performed (i.e. domain-matching only). - -### `store.putCookie(cookie, cb(err))` - -Adds a new cookie to the store. The implementation SHOULD replace any existing cookie with the same `.domain`, `.path`, and `.key` properties -- depending on the nature of the implementation, it's possible that between the call to `fetchCookie` and `putCookie` that a duplicate `putCookie` can occur. - -The `cookie` object MUST NOT be modified; the caller will have already updated the `.creation` and `.lastAccessed` properties. - -Pass an error if the cookie cannot be stored. - -### `store.updateCookie(oldCookie, newCookie, cb(err))` - -Update an existing cookie. The implementation MUST update the `.value` for a cookie with the same `domain`, `.path` and `.key`. The implementation SHOULD check that the old value in the store is equivalent to `oldCookie` - how the conflict is resolved is up to the store. - -The `.lastAccessed` property will always be different between the two objects (to the precision possible via JavaScript's clock). Both `.creation` and `.creationIndex` are guaranteed to be the same. Stores MAY ignore or defer the `.lastAccessed` change at the cost of affecting how cookies are selected for automatic deletion (e.g., least-recently-used, which is up to the store to implement). - -Stores may wish to optimize changing the `.value` of the cookie in the store versus storing a new cookie. If the implementation doesn't define this method a stub that calls `putCookie(newCookie,cb)` will be added to the store object. - -The `newCookie` and `oldCookie` objects MUST NOT be modified. - -Pass an error if the newCookie cannot be stored. - -### `store.removeCookie(domain, path, key, cb(err))` - -Remove a cookie from the store (see notes on `findCookie` about the uniqueness constraint). - -The implementation MUST NOT pass an error if the cookie doesn't exist; only pass an error due to the failure to remove an existing cookie. - -### `store.removeCookies(domain, path, cb(err))` - -Removes matching cookies from the store. The `path` parameter is optional, and if missing means all paths in a domain should be removed. - -Pass an error ONLY if removing any existing cookies failed. - -### `store.getAllCookies(cb(err, cookies))` - -Produces an `Array` of all cookies during `jar.serialize()`. The items in the array can be true `Cookie` objects or generic `Object`s with the [Serialization Format] data structure. - -Cookies SHOULD be returned in creation order to preserve sorting via `compareCookies()`. For reference, `MemoryCookieStore` will sort by `.creationIndex` since it uses true `Cookie` objects internally. If you don't return the cookies in creation order, they'll still be sorted by creation time, but this only has a precision of 1ms. See `compareCookies` for more detail. - -Pass an error if retrieval fails. - -## MemoryCookieStore - -Inherits from `Store`. - -A just-in-memory CookieJar synchronous store implementation, used by default. Despite being a synchronous implementation, it's usable with both the synchronous and asynchronous forms of the `CookieJar` API. - -# Serialization Format - -**NOTE**: if you want to have custom `Cookie` properties serialized, add the property name to `Cookie.serializableProperties`. - -```js - { - // The version of tough-cookie that serialized this jar. - version: 'tough-cookie@1.x.y', - - // add the store type, to make humans happy: - storeType: 'MemoryCookieStore', - - // CookieJar configuration: - rejectPublicSuffixes: true, - // ... future items go here - - // Gets filled from jar.store.getAllCookies(): - cookies: [ - { - key: 'string', - value: 'string', - // ... - /* other Cookie.serializableProperties go here */ - } - ] - } -``` - -# Copyright and License - -(tl;dr: BSD-3-Clause with some MPL/2.0) - -```text - Copyright (c) 2015, Salesforce.com, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - 3. Neither the name of Salesforce.com nor the names of its contributors may - be used to endorse or promote products derived from this software without - specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. -``` - -Portions may be licensed under different licenses (in particular `public_suffix_list.dat` is MPL/2.0); please read that file and the LICENSE file for full details. diff --git a/node_modules/request/node_modules/tough-cookie/lib/cookie.js b/node_modules/request/node_modules/tough-cookie/lib/cookie.js deleted file mode 100644 index 12da297a..00000000 --- a/node_modules/request/node_modules/tough-cookie/lib/cookie.js +++ /dev/null @@ -1,1342 +0,0 @@ -/*! - * Copyright (c) 2015, Salesforce.com, Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. Neither the name of Salesforce.com nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -'use strict'; -var net = require('net'); -var urlParse = require('url').parse; -var pubsuffix = require('./pubsuffix'); -var Store = require('./store').Store; -var MemoryCookieStore = require('./memstore').MemoryCookieStore; -var pathMatch = require('./pathMatch').pathMatch; -var VERSION = require('../package.json').version; - -var punycode; -try { - punycode = require('punycode'); -} catch(e) { - console.warn("cookie: can't load punycode; won't use punycode for domain normalization"); -} - -var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/; - -// From RFC6265 S4.1.1 -// note that it excludes \x3B ";" -var COOKIE_OCTET = /[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/; -var COOKIE_OCTETS = new RegExp('^'+COOKIE_OCTET.source+'+$'); - -var CONTROL_CHARS = /[\x00-\x1F]/; - -// Double quotes are part of the value (see: S4.1.1). -// '\r', '\n' and '\0' should be treated as a terminator in the "relaxed" mode -// (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60) -// '=' and ';' are attribute/values separators -// (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L64) -var COOKIE_PAIR = /^(([^=;]+))\s*=\s*([^\n\r\0]*)/; - -// Used to parse non-RFC-compliant cookies like '=abc' when given the `loose` -// option in Cookie.parse: -var LOOSE_COOKIE_PAIR = /^((?:=)?([^=;]*)\s*=\s*)?([^\n\r\0]*)/; - -// RFC6265 S4.1.1 defines path value as 'any CHAR except CTLs or ";"' -// Note ';' is \x3B -var PATH_VALUE = /[\x20-\x3A\x3C-\x7E]+/; - -// Used for checking whether or not there is a trailing semi-colon -var TRAILING_SEMICOLON = /;+$/; - -var DAY_OF_MONTH = /^(\d{1,2})[^\d]*$/; -var TIME = /^(\d{1,2})[^\d]*:(\d{1,2})[^\d]*:(\d{1,2})[^\d]*$/; -var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i; - -var MONTH_TO_NUM = { - jan:0, feb:1, mar:2, apr:3, may:4, jun:5, - jul:6, aug:7, sep:8, oct:9, nov:10, dec:11 -}; -var NUM_TO_MONTH = [ - 'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec' -]; -var NUM_TO_DAY = [ - 'Sun','Mon','Tue','Wed','Thu','Fri','Sat' -]; - -var YEAR = /^(\d{2}|\d{4})$/; // 2 to 4 digits - -var MAX_TIME = 2147483647000; // 31-bit max -var MIN_TIME = 0; // 31-bit min - - -// RFC6265 S5.1.1 date parser: -function parseDate(str) { - if (!str) { - return; - } - - /* RFC6265 S5.1.1: - * 2. Process each date-token sequentially in the order the date-tokens - * appear in the cookie-date - */ - var tokens = str.split(DATE_DELIM); - if (!tokens) { - return; - } - - var hour = null; - var minutes = null; - var seconds = null; - var day = null; - var month = null; - var year = null; - - for (var i=0; i 23 || minutes > 59 || seconds > 59) { - return; - } - - continue; - } - } - - /* 2.2. If the found-day-of-month flag is not set and the date-token matches - * the day-of-month production, set the found-day-of- month flag and set - * the day-of-month-value to the number denoted by the date-token. Skip - * the remaining sub-steps and continue to the next date-token. - */ - if (day === null) { - result = DAY_OF_MONTH.exec(token); - if (result) { - day = parseInt(result, 10); - /* RFC6265 S5.1.1.5: - * [fail if] the day-of-month-value is less than 1 or greater than 31 - */ - if(day < 1 || day > 31) { - return; - } - continue; - } - } - - /* 2.3. If the found-month flag is not set and the date-token matches the - * month production, set the found-month flag and set the month-value to - * the month denoted by the date-token. Skip the remaining sub-steps and - * continue to the next date-token. - */ - if (month === null) { - result = MONTH.exec(token); - if (result) { - month = MONTH_TO_NUM[result[1].toLowerCase()]; - continue; - } - } - - /* 2.4. If the found-year flag is not set and the date-token matches the year - * production, set the found-year flag and set the year-value to the number - * denoted by the date-token. Skip the remaining sub-steps and continue to - * the next date-token. - */ - if (year === null) { - result = YEAR.exec(token); - if (result) { - year = parseInt(result[0], 10); - /* From S5.1.1: - * 3. If the year-value is greater than or equal to 70 and less - * than or equal to 99, increment the year-value by 1900. - * 4. If the year-value is greater than or equal to 0 and less - * than or equal to 69, increment the year-value by 2000. - */ - if (70 <= year && year <= 99) { - year += 1900; - } else if (0 <= year && year <= 69) { - year += 2000; - } - - if (year < 1601) { - return; // 5. ... the year-value is less than 1601 - } - } - } - } - - if (seconds === null || day === null || month === null || year === null) { - return; // 5. ... at least one of the found-day-of-month, found-month, found- - // year, or found-time flags is not set, - } - - return new Date(Date.UTC(year, month, day, hour, minutes, seconds)); -} - -function formatDate(date) { - var d = date.getUTCDate(); d = d >= 10 ? d : '0'+d; - var h = date.getUTCHours(); h = h >= 10 ? h : '0'+h; - var m = date.getUTCMinutes(); m = m >= 10 ? m : '0'+m; - var s = date.getUTCSeconds(); s = s >= 10 ? s : '0'+s; - return NUM_TO_DAY[date.getUTCDay()] + ', ' + - d+' '+ NUM_TO_MONTH[date.getUTCMonth()] +' '+ date.getUTCFullYear() +' '+ - h+':'+m+':'+s+' GMT'; -} - -// S5.1.2 Canonicalized Host Names -function canonicalDomain(str) { - if (str == null) { - return null; - } - str = str.trim().replace(/^\./,''); // S4.1.2.3 & S5.2.3: ignore leading . - - // convert to IDN if any non-ASCII characters - if (punycode && /[^\u0001-\u007f]/.test(str)) { - str = punycode.toASCII(str); - } - - return str.toLowerCase(); -} - -// S5.1.3 Domain Matching -function domainMatch(str, domStr, canonicalize) { - if (str == null || domStr == null) { - return null; - } - if (canonicalize !== false) { - str = canonicalDomain(str); - domStr = canonicalDomain(domStr); - } - - /* - * "The domain string and the string are identical. (Note that both the - * domain string and the string will have been canonicalized to lower case at - * this point)" - */ - if (str == domStr) { - return true; - } - - /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */ - - /* "* The string is a host name (i.e., not an IP address)." */ - if (net.isIP(str)) { - return false; - } - - /* "* The domain string is a suffix of the string" */ - var idx = str.indexOf(domStr); - if (idx <= 0) { - return false; // it's a non-match (-1) or prefix (0) - } - - // e.g "a.b.c".indexOf("b.c") === 2 - // 5 === 3+2 - if (str.length !== domStr.length + idx) { // it's not a suffix - return false; - } - - /* "* The last character of the string that is not included in the domain - * string is a %x2E (".") character." */ - if (str.substr(idx-1,1) !== '.') { - return false; - } - - return true; -} - - -// RFC6265 S5.1.4 Paths and Path-Match - -/* - * "The user agent MUST use an algorithm equivalent to the following algorithm - * to compute the default-path of a cookie:" - * - * Assumption: the path (and not query part or absolute uri) is passed in. - */ -function defaultPath(path) { - // "2. If the uri-path is empty or if the first character of the uri-path is not - // a %x2F ("/") character, output %x2F ("/") and skip the remaining steps. - if (!path || path.substr(0,1) !== "/") { - return "/"; - } - - // "3. If the uri-path contains no more than one %x2F ("/") character, output - // %x2F ("/") and skip the remaining step." - if (path === "/") { - return path; - } - - var rightSlash = path.lastIndexOf("/"); - if (rightSlash === 0) { - return "/"; - } - - // "4. Output the characters of the uri-path from the first character up to, - // but not including, the right-most %x2F ("/")." - return path.slice(0, rightSlash); -} - - -function parse(str, options) { - if (!options || typeof options !== 'object') { - options = {}; - } - str = str.trim(); - - // S4.1.1 Trailing semi-colons are not part of the specification. - var semiColonCheck = TRAILING_SEMICOLON.exec(str); - if (semiColonCheck) { - str = str.slice(0, semiColonCheck.index); - } - - // We use a regex to parse the "name-value-pair" part of S5.2 - var firstSemi = str.indexOf(';'); // S5.2 step 1 - var pairRe = options.loose ? LOOSE_COOKIE_PAIR : COOKIE_PAIR; - var result = pairRe.exec(firstSemi === -1 ? str : str.substr(0,firstSemi)); - - // Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")" - // constraints as well as trimming any whitespace. - if (!result) { - return; - } - - var c = new Cookie(); - if (result[1]) { - c.key = result[2].trim(); - } else { - c.key = ''; - } - c.value = result[3].trim(); - if (CONTROL_CHARS.test(c.key) || CONTROL_CHARS.test(c.value)) { - return; - } - - if (firstSemi === -1) { - return c; - } - - // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string - // (including the %x3B (";") in question)." plus later on in the same section - // "discard the first ";" and trim". - var unparsed = str.slice(firstSemi).replace(/^\s*;\s*/,'').trim(); - - // "If the unparsed-attributes string is empty, skip the rest of these - // steps." - if (unparsed.length === 0) { - return c; - } - - /* - * S5.2 says that when looping over the items "[p]rocess the attribute-name - * and attribute-value according to the requirements in the following - * subsections" for every item. Plus, for many of the individual attributes - * in S5.3 it says to use the "attribute-value of the last attribute in the - * cookie-attribute-list". Therefore, in this implementation, we overwrite - * the previous value. - */ - var cookie_avs = unparsed.split(/\s*;\s*/); - while (cookie_avs.length) { - var av = cookie_avs.shift(); - var av_sep = av.indexOf('='); - var av_key, av_value; - - if (av_sep === -1) { - av_key = av; - av_value = null; - } else { - av_key = av.substr(0,av_sep); - av_value = av.substr(av_sep+1); - } - - av_key = av_key.trim().toLowerCase(); - - if (av_value) { - av_value = av_value.trim(); - } - - switch(av_key) { - case 'expires': // S5.2.1 - if (av_value) { - var exp = parseDate(av_value); - // "If the attribute-value failed to parse as a cookie date, ignore the - // cookie-av." - if (exp) { - // over and underflow not realistically a concern: V8's getTime() seems to - // store something larger than a 32-bit time_t (even with 32-bit node) - c.expires = exp; - } - } - break; - - case 'max-age': // S5.2.2 - if (av_value) { - // "If the first character of the attribute-value is not a DIGIT or a "-" - // character ...[or]... If the remainder of attribute-value contains a - // non-DIGIT character, ignore the cookie-av." - if (/^-?[0-9]+$/.test(av_value)) { - var delta = parseInt(av_value, 10); - // "If delta-seconds is less than or equal to zero (0), let expiry-time - // be the earliest representable date and time." - c.setMaxAge(delta); - } - } - break; - - case 'domain': // S5.2.3 - // "If the attribute-value is empty, the behavior is undefined. However, - // the user agent SHOULD ignore the cookie-av entirely." - if (av_value) { - // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E - // (".") character." - var domain = av_value.trim().replace(/^\./, ''); - if (domain) { - // "Convert the cookie-domain to lower case." - c.domain = domain.toLowerCase(); - } - } - break; - - case 'path': // S5.2.4 - /* - * "If the attribute-value is empty or if the first character of the - * attribute-value is not %x2F ("/"): - * Let cookie-path be the default-path. - * Otherwise: - * Let cookie-path be the attribute-value." - * - * We'll represent the default-path as null since it depends on the - * context of the parsing. - */ - c.path = av_value && av_value[0] === "/" ? av_value : null; - break; - - case 'secure': // S5.2.5 - /* - * "If the attribute-name case-insensitively matches the string "Secure", - * the user agent MUST append an attribute to the cookie-attribute-list - * with an attribute-name of Secure and an empty attribute-value." - */ - c.secure = true; - break; - - case 'httponly': // S5.2.6 -- effectively the same as 'secure' - c.httpOnly = true; - break; - - default: - c.extensions = c.extensions || []; - c.extensions.push(av); - break; - } - } - - return c; -} - -// avoid the V8 deoptimization monster! -function jsonParse(str) { - var obj; - try { - obj = JSON.parse(str); - } catch (e) { - return e; - } - return obj; -} - -function fromJSON(str) { - if (!str) { - return null; - } - - var obj; - if (typeof str === 'string') { - obj = jsonParse(str); - if (obj instanceof Error) { - return null; - } - } else { - // assume it's an Object - obj = str; - } - - var c = new Cookie(); - for (var i=0; i 1) { - var lindex = path.lastIndexOf('/'); - if (lindex === 0) { - break; - } - path = path.substr(0,lindex); - permutations.push(path); - } - permutations.push('/'); - return permutations; -} - -function getCookieContext(url) { - if (url instanceof Object) { - return url; - } - // NOTE: decodeURI will throw on malformed URIs (see GH-32). - // Therefore, we will just skip decoding for such URIs. - try { - url = decodeURI(url); - } - catch(err) { - // Silently swallow error - } - - return urlParse(url); -} - -function Cookie(options) { - options = options || {}; - - Object.keys(options).forEach(function(prop) { - if (Cookie.prototype.hasOwnProperty(prop) && - Cookie.prototype[prop] !== options[prop] && - prop.substr(0,1) !== '_') - { - this[prop] = options[prop]; - } - }, this); - - this.creation = this.creation || new Date(); - - // used to break creation ties in cookieCompare(): - Object.defineProperty(this, 'creationIndex', { - configurable: false, - enumerable: false, // important for assert.deepEqual checks - writable: true, - value: ++Cookie.cookiesCreated - }); -} - -Cookie.cookiesCreated = 0; // incremented each time a cookie is created - -Cookie.parse = parse; -Cookie.fromJSON = fromJSON; - -Cookie.prototype.key = ""; -Cookie.prototype.value = ""; - -// the order in which the RFC has them: -Cookie.prototype.expires = "Infinity"; // coerces to literal Infinity -Cookie.prototype.maxAge = null; // takes precedence over expires for TTL -Cookie.prototype.domain = null; -Cookie.prototype.path = null; -Cookie.prototype.secure = false; -Cookie.prototype.httpOnly = false; -Cookie.prototype.extensions = null; - -// set by the CookieJar: -Cookie.prototype.hostOnly = null; // boolean when set -Cookie.prototype.pathIsDefault = null; // boolean when set -Cookie.prototype.creation = null; // Date when set; defaulted by Cookie.parse -Cookie.prototype.lastAccessed = null; // Date when set -Object.defineProperty(Cookie.prototype, 'creationIndex', { - configurable: true, - enumerable: false, - writable: true, - value: 0 -}); - -Cookie.serializableProperties = Object.keys(Cookie.prototype) - .filter(function(prop) { - return !( - Cookie.prototype[prop] instanceof Function || - prop === 'creationIndex' || - prop.substr(0,1) === '_' - ); - }); - -Cookie.prototype.inspect = function inspect() { - var now = Date.now(); - return 'Cookie="'+this.toString() + - '; hostOnly='+(this.hostOnly != null ? this.hostOnly : '?') + - '; aAge='+(this.lastAccessed ? (now-this.lastAccessed.getTime())+'ms' : '?') + - '; cAge='+(this.creation ? (now-this.creation.getTime())+'ms' : '?') + - '"'; -}; - -Cookie.prototype.toJSON = function() { - var obj = {}; - - var props = Cookie.serializableProperties; - for (var i=0; i suffixLen) { - var publicSuffix = parts.slice(0,suffixLen+1).reverse().join('.'); - return converted ? punycode.toUnicode(publicSuffix) : publicSuffix; - } - - return null; -}; - -// The following generated structure is used under the MPL version 2.0 -// See public-suffix.txt for more information - -var index = module.exports.index = Object.freeze( -{"ac":true,"com.ac":true,"edu.ac":true,"gov.ac":true,"net.ac":true,"mil.ac":true,"org.ac":true,"ad":true,"nom.ad":true,"ae":true,"co.ae":true,"net.ae":true,"org.ae":true,"sch.ae":true,"ac.ae":true,"gov.ae":true,"mil.ae":true,"aero":true,"accident-investigation.aero":true,"accident-prevention.aero":true,"aerobatic.aero":true,"aeroclub.aero":true,"aerodrome.aero":true,"agents.aero":true,"aircraft.aero":true,"airline.aero":true,"airport.aero":true,"air-surveillance.aero":true,"airtraffic.aero":true,"air-traffic-control.aero":true,"ambulance.aero":true,"amusement.aero":true,"association.aero":true,"author.aero":true,"ballooning.aero":true,"broker.aero":true,"caa.aero":true,"cargo.aero":true,"catering.aero":true,"certification.aero":true,"championship.aero":true,"charter.aero":true,"civilaviation.aero":true,"club.aero":true,"conference.aero":true,"consultant.aero":true,"consulting.aero":true,"control.aero":true,"council.aero":true,"crew.aero":true,"design.aero":true,"dgca.aero":true,"educator.aero":true,"emergency.aero":true,"engine.aero":true,"engineer.aero":true,"entertainment.aero":true,"equipment.aero":true,"exchange.aero":true,"express.aero":true,"federation.aero":true,"flight.aero":true,"freight.aero":true,"fuel.aero":true,"gliding.aero":true,"government.aero":true,"groundhandling.aero":true,"group.aero":true,"hanggliding.aero":true,"homebuilt.aero":true,"insurance.aero":true,"journal.aero":true,"journalist.aero":true,"leasing.aero":true,"logistics.aero":true,"magazine.aero":true,"maintenance.aero":true,"marketplace.aero":true,"media.aero":true,"microlight.aero":true,"modelling.aero":true,"navigation.aero":true,"parachuting.aero":true,"paragliding.aero":true,"passenger-association.aero":true,"pilot.aero":true,"press.aero":true,"production.aero":true,"recreation.aero":true,"repbody.aero":true,"res.aero":true,"research.aero":true,"rotorcraft.aero":true,"safety.aero":true,"scientist.aero":true,"services.aero":true,"show.aero":true,"skydiving.aero":true,"software.aero":true,"student.aero":true,"taxi.aero":true,"trader.aero":true,"trading.aero":true,"trainer.aero":true,"union.aero":true,"workinggroup.aero":true,"works.aero":true,"af":true,"gov.af":true,"com.af":true,"org.af":true,"net.af":true,"edu.af":true,"ag":true,"com.ag":true,"org.ag":true,"net.ag":true,"co.ag":true,"nom.ag":true,"ai":true,"off.ai":true,"com.ai":true,"net.ai":true,"org.ai":true,"al":true,"com.al":true,"edu.al":true,"gov.al":true,"mil.al":true,"net.al":true,"org.al":true,"am":true,"an":true,"com.an":true,"net.an":true,"org.an":true,"edu.an":true,"ao":true,"ed.ao":true,"gv.ao":true,"og.ao":true,"co.ao":true,"pb.ao":true,"it.ao":true,"aq":true,"ar":true,"com.ar":true,"edu.ar":true,"gob.ar":true,"gov.ar":true,"int.ar":true,"mil.ar":true,"net.ar":true,"org.ar":true,"tur.ar":true,"arpa":true,"e164.arpa":true,"in-addr.arpa":true,"ip6.arpa":true,"iris.arpa":true,"uri.arpa":true,"urn.arpa":true,"as":true,"gov.as":true,"asia":true,"at":true,"ac.at":true,"co.at":true,"gv.at":true,"or.at":true,"au":true,"com.au":true,"net.au":true,"org.au":true,"edu.au":true,"gov.au":true,"asn.au":true,"id.au":true,"info.au":true,"conf.au":true,"oz.au":true,"act.au":true,"nsw.au":true,"nt.au":true,"qld.au":true,"sa.au":true,"tas.au":true,"vic.au":true,"wa.au":true,"act.edu.au":true,"nsw.edu.au":true,"nt.edu.au":true,"qld.edu.au":true,"sa.edu.au":true,"tas.edu.au":true,"vic.edu.au":true,"wa.edu.au":true,"qld.gov.au":true,"sa.gov.au":true,"tas.gov.au":true,"vic.gov.au":true,"wa.gov.au":true,"aw":true,"com.aw":true,"ax":true,"az":true,"com.az":true,"net.az":true,"int.az":true,"gov.az":true,"org.az":true,"edu.az":true,"info.az":true,"pp.az":true,"mil.az":true,"name.az":true,"pro.az":true,"biz.az":true,"ba":true,"org.ba":true,"net.ba":true,"edu.ba":true,"gov.ba":true,"mil.ba":true,"unsa.ba":true,"unbi.ba":true,"co.ba":true,"com.ba":true,"rs.ba":true,"bb":true,"biz.bb":true,"co.bb":true,"com.bb":true,"edu.bb":true,"gov.bb":true,"info.bb":true,"net.bb":true,"org.bb":true,"store.bb":true,"tv.bb":true,"*.bd":true,"be":true,"ac.be":true,"bf":true,"gov.bf":true,"bg":true,"a.bg":true,"b.bg":true,"c.bg":true,"d.bg":true,"e.bg":true,"f.bg":true,"g.bg":true,"h.bg":true,"i.bg":true,"j.bg":true,"k.bg":true,"l.bg":true,"m.bg":true,"n.bg":true,"o.bg":true,"p.bg":true,"q.bg":true,"r.bg":true,"s.bg":true,"t.bg":true,"u.bg":true,"v.bg":true,"w.bg":true,"x.bg":true,"y.bg":true,"z.bg":true,"0.bg":true,"1.bg":true,"2.bg":true,"3.bg":true,"4.bg":true,"5.bg":true,"6.bg":true,"7.bg":true,"8.bg":true,"9.bg":true,"bh":true,"com.bh":true,"edu.bh":true,"net.bh":true,"org.bh":true,"gov.bh":true,"bi":true,"co.bi":true,"com.bi":true,"edu.bi":true,"or.bi":true,"org.bi":true,"biz":true,"bj":true,"asso.bj":true,"barreau.bj":true,"gouv.bj":true,"bm":true,"com.bm":true,"edu.bm":true,"gov.bm":true,"net.bm":true,"org.bm":true,"*.bn":true,"bo":true,"com.bo":true,"edu.bo":true,"gov.bo":true,"gob.bo":true,"int.bo":true,"org.bo":true,"net.bo":true,"mil.bo":true,"tv.bo":true,"br":true,"adm.br":true,"adv.br":true,"agr.br":true,"am.br":true,"arq.br":true,"art.br":true,"ato.br":true,"b.br":true,"bio.br":true,"blog.br":true,"bmd.br":true,"cim.br":true,"cng.br":true,"cnt.br":true,"com.br":true,"coop.br":true,"ecn.br":true,"eco.br":true,"edu.br":true,"emp.br":true,"eng.br":true,"esp.br":true,"etc.br":true,"eti.br":true,"far.br":true,"flog.br":true,"fm.br":true,"fnd.br":true,"fot.br":true,"fst.br":true,"g12.br":true,"ggf.br":true,"gov.br":true,"imb.br":true,"ind.br":true,"inf.br":true,"jor.br":true,"jus.br":true,"leg.br":true,"lel.br":true,"mat.br":true,"med.br":true,"mil.br":true,"mp.br":true,"mus.br":true,"net.br":true,"*.nom.br":true,"not.br":true,"ntr.br":true,"odo.br":true,"org.br":true,"ppg.br":true,"pro.br":true,"psc.br":true,"psi.br":true,"qsl.br":true,"radio.br":true,"rec.br":true,"slg.br":true,"srv.br":true,"taxi.br":true,"teo.br":true,"tmp.br":true,"trd.br":true,"tur.br":true,"tv.br":true,"vet.br":true,"vlog.br":true,"wiki.br":true,"zlg.br":true,"bs":true,"com.bs":true,"net.bs":true,"org.bs":true,"edu.bs":true,"gov.bs":true,"bt":true,"com.bt":true,"edu.bt":true,"gov.bt":true,"net.bt":true,"org.bt":true,"bv":true,"bw":true,"co.bw":true,"org.bw":true,"by":true,"gov.by":true,"mil.by":true,"com.by":true,"of.by":true,"bz":true,"com.bz":true,"net.bz":true,"org.bz":true,"edu.bz":true,"gov.bz":true,"ca":true,"ab.ca":true,"bc.ca":true,"mb.ca":true,"nb.ca":true,"nf.ca":true,"nl.ca":true,"ns.ca":true,"nt.ca":true,"nu.ca":true,"on.ca":true,"pe.ca":true,"qc.ca":true,"sk.ca":true,"yk.ca":true,"gc.ca":true,"cat":true,"cc":true,"cd":true,"gov.cd":true,"cf":true,"cg":true,"ch":true,"ci":true,"org.ci":true,"or.ci":true,"com.ci":true,"co.ci":true,"edu.ci":true,"ed.ci":true,"ac.ci":true,"net.ci":true,"go.ci":true,"asso.ci":true,"xn--aroport-bya.ci":true,"int.ci":true,"presse.ci":true,"md.ci":true,"gouv.ci":true,"*.ck":true,"www.ck":false,"cl":true,"gov.cl":true,"gob.cl":true,"co.cl":true,"mil.cl":true,"cm":true,"co.cm":true,"com.cm":true,"gov.cm":true,"net.cm":true,"cn":true,"ac.cn":true,"com.cn":true,"edu.cn":true,"gov.cn":true,"net.cn":true,"org.cn":true,"mil.cn":true,"xn--55qx5d.cn":true,"xn--io0a7i.cn":true,"xn--od0alg.cn":true,"ah.cn":true,"bj.cn":true,"cq.cn":true,"fj.cn":true,"gd.cn":true,"gs.cn":true,"gz.cn":true,"gx.cn":true,"ha.cn":true,"hb.cn":true,"he.cn":true,"hi.cn":true,"hl.cn":true,"hn.cn":true,"jl.cn":true,"js.cn":true,"jx.cn":true,"ln.cn":true,"nm.cn":true,"nx.cn":true,"qh.cn":true,"sc.cn":true,"sd.cn":true,"sh.cn":true,"sn.cn":true,"sx.cn":true,"tj.cn":true,"xj.cn":true,"xz.cn":true,"yn.cn":true,"zj.cn":true,"hk.cn":true,"mo.cn":true,"tw.cn":true,"co":true,"arts.co":true,"com.co":true,"edu.co":true,"firm.co":true,"gov.co":true,"info.co":true,"int.co":true,"mil.co":true,"net.co":true,"nom.co":true,"org.co":true,"rec.co":true,"web.co":true,"com":true,"coop":true,"cr":true,"ac.cr":true,"co.cr":true,"ed.cr":true,"fi.cr":true,"go.cr":true,"or.cr":true,"sa.cr":true,"cu":true,"com.cu":true,"edu.cu":true,"org.cu":true,"net.cu":true,"gov.cu":true,"inf.cu":true,"cv":true,"cw":true,"com.cw":true,"edu.cw":true,"net.cw":true,"org.cw":true,"cx":true,"gov.cx":true,"ac.cy":true,"biz.cy":true,"com.cy":true,"ekloges.cy":true,"gov.cy":true,"ltd.cy":true,"name.cy":true,"net.cy":true,"org.cy":true,"parliament.cy":true,"press.cy":true,"pro.cy":true,"tm.cy":true,"cz":true,"de":true,"dj":true,"dk":true,"dm":true,"com.dm":true,"net.dm":true,"org.dm":true,"edu.dm":true,"gov.dm":true,"do":true,"art.do":true,"com.do":true,"edu.do":true,"gob.do":true,"gov.do":true,"mil.do":true,"net.do":true,"org.do":true,"sld.do":true,"web.do":true,"dz":true,"com.dz":true,"org.dz":true,"net.dz":true,"gov.dz":true,"edu.dz":true,"asso.dz":true,"pol.dz":true,"art.dz":true,"ec":true,"com.ec":true,"info.ec":true,"net.ec":true,"fin.ec":true,"k12.ec":true,"med.ec":true,"pro.ec":true,"org.ec":true,"edu.ec":true,"gov.ec":true,"gob.ec":true,"mil.ec":true,"edu":true,"ee":true,"edu.ee":true,"gov.ee":true,"riik.ee":true,"lib.ee":true,"med.ee":true,"com.ee":true,"pri.ee":true,"aip.ee":true,"org.ee":true,"fie.ee":true,"eg":true,"com.eg":true,"edu.eg":true,"eun.eg":true,"gov.eg":true,"mil.eg":true,"name.eg":true,"net.eg":true,"org.eg":true,"sci.eg":true,"*.er":true,"es":true,"com.es":true,"nom.es":true,"org.es":true,"gob.es":true,"edu.es":true,"et":true,"com.et":true,"gov.et":true,"org.et":true,"edu.et":true,"biz.et":true,"name.et":true,"info.et":true,"net.et":true,"eu":true,"fi":true,"aland.fi":true,"*.fj":true,"*.fk":true,"fm":true,"fo":true,"fr":true,"com.fr":true,"asso.fr":true,"nom.fr":true,"prd.fr":true,"presse.fr":true,"tm.fr":true,"aeroport.fr":true,"assedic.fr":true,"avocat.fr":true,"avoues.fr":true,"cci.fr":true,"chambagri.fr":true,"chirurgiens-dentistes.fr":true,"experts-comptables.fr":true,"geometre-expert.fr":true,"gouv.fr":true,"greta.fr":true,"huissier-justice.fr":true,"medecin.fr":true,"notaires.fr":true,"pharmacien.fr":true,"port.fr":true,"veterinaire.fr":true,"ga":true,"gb":true,"gd":true,"ge":true,"com.ge":true,"edu.ge":true,"gov.ge":true,"org.ge":true,"mil.ge":true,"net.ge":true,"pvt.ge":true,"gf":true,"gg":true,"co.gg":true,"net.gg":true,"org.gg":true,"gh":true,"com.gh":true,"edu.gh":true,"gov.gh":true,"org.gh":true,"mil.gh":true,"gi":true,"com.gi":true,"ltd.gi":true,"gov.gi":true,"mod.gi":true,"edu.gi":true,"org.gi":true,"gl":true,"co.gl":true,"com.gl":true,"edu.gl":true,"net.gl":true,"org.gl":true,"gm":true,"gn":true,"ac.gn":true,"com.gn":true,"edu.gn":true,"gov.gn":true,"org.gn":true,"net.gn":true,"gov":true,"gp":true,"com.gp":true,"net.gp":true,"mobi.gp":true,"edu.gp":true,"org.gp":true,"asso.gp":true,"gq":true,"gr":true,"com.gr":true,"edu.gr":true,"net.gr":true,"org.gr":true,"gov.gr":true,"gs":true,"gt":true,"com.gt":true,"edu.gt":true,"gob.gt":true,"ind.gt":true,"mil.gt":true,"net.gt":true,"org.gt":true,"*.gu":true,"gw":true,"gy":true,"co.gy":true,"com.gy":true,"net.gy":true,"hk":true,"com.hk":true,"edu.hk":true,"gov.hk":true,"idv.hk":true,"net.hk":true,"org.hk":true,"xn--55qx5d.hk":true,"xn--wcvs22d.hk":true,"xn--lcvr32d.hk":true,"xn--mxtq1m.hk":true,"xn--gmqw5a.hk":true,"xn--ciqpn.hk":true,"xn--gmq050i.hk":true,"xn--zf0avx.hk":true,"xn--io0a7i.hk":true,"xn--mk0axi.hk":true,"xn--od0alg.hk":true,"xn--od0aq3b.hk":true,"xn--tn0ag.hk":true,"xn--uc0atv.hk":true,"xn--uc0ay4a.hk":true,"hm":true,"hn":true,"com.hn":true,"edu.hn":true,"org.hn":true,"net.hn":true,"mil.hn":true,"gob.hn":true,"hr":true,"iz.hr":true,"from.hr":true,"name.hr":true,"com.hr":true,"ht":true,"com.ht":true,"shop.ht":true,"firm.ht":true,"info.ht":true,"adult.ht":true,"net.ht":true,"pro.ht":true,"org.ht":true,"med.ht":true,"art.ht":true,"coop.ht":true,"pol.ht":true,"asso.ht":true,"edu.ht":true,"rel.ht":true,"gouv.ht":true,"perso.ht":true,"hu":true,"co.hu":true,"info.hu":true,"org.hu":true,"priv.hu":true,"sport.hu":true,"tm.hu":true,"2000.hu":true,"agrar.hu":true,"bolt.hu":true,"casino.hu":true,"city.hu":true,"erotica.hu":true,"erotika.hu":true,"film.hu":true,"forum.hu":true,"games.hu":true,"hotel.hu":true,"ingatlan.hu":true,"jogasz.hu":true,"konyvelo.hu":true,"lakas.hu":true,"media.hu":true,"news.hu":true,"reklam.hu":true,"sex.hu":true,"shop.hu":true,"suli.hu":true,"szex.hu":true,"tozsde.hu":true,"utazas.hu":true,"video.hu":true,"id":true,"ac.id":true,"biz.id":true,"co.id":true,"desa.id":true,"go.id":true,"mil.id":true,"my.id":true,"net.id":true,"or.id":true,"sch.id":true,"web.id":true,"ie":true,"gov.ie":true,"il":true,"ac.il":true,"co.il":true,"gov.il":true,"idf.il":true,"k12.il":true,"muni.il":true,"net.il":true,"org.il":true,"im":true,"ac.im":true,"co.im":true,"com.im":true,"ltd.co.im":true,"net.im":true,"org.im":true,"plc.co.im":true,"tt.im":true,"tv.im":true,"in":true,"co.in":true,"firm.in":true,"net.in":true,"org.in":true,"gen.in":true,"ind.in":true,"nic.in":true,"ac.in":true,"edu.in":true,"res.in":true,"gov.in":true,"mil.in":true,"info":true,"int":true,"eu.int":true,"io":true,"com.io":true,"iq":true,"gov.iq":true,"edu.iq":true,"mil.iq":true,"com.iq":true,"org.iq":true,"net.iq":true,"ir":true,"ac.ir":true,"co.ir":true,"gov.ir":true,"id.ir":true,"net.ir":true,"org.ir":true,"sch.ir":true,"xn--mgba3a4f16a.ir":true,"xn--mgba3a4fra.ir":true,"is":true,"net.is":true,"com.is":true,"edu.is":true,"gov.is":true,"org.is":true,"int.is":true,"it":true,"gov.it":true,"edu.it":true,"abr.it":true,"abruzzo.it":true,"aosta-valley.it":true,"aostavalley.it":true,"bas.it":true,"basilicata.it":true,"cal.it":true,"calabria.it":true,"cam.it":true,"campania.it":true,"emilia-romagna.it":true,"emiliaromagna.it":true,"emr.it":true,"friuli-v-giulia.it":true,"friuli-ve-giulia.it":true,"friuli-vegiulia.it":true,"friuli-venezia-giulia.it":true,"friuli-veneziagiulia.it":true,"friuli-vgiulia.it":true,"friuliv-giulia.it":true,"friulive-giulia.it":true,"friulivegiulia.it":true,"friulivenezia-giulia.it":true,"friuliveneziagiulia.it":true,"friulivgiulia.it":true,"fvg.it":true,"laz.it":true,"lazio.it":true,"lig.it":true,"liguria.it":true,"lom.it":true,"lombardia.it":true,"lombardy.it":true,"lucania.it":true,"mar.it":true,"marche.it":true,"mol.it":true,"molise.it":true,"piedmont.it":true,"piemonte.it":true,"pmn.it":true,"pug.it":true,"puglia.it":true,"sar.it":true,"sardegna.it":true,"sardinia.it":true,"sic.it":true,"sicilia.it":true,"sicily.it":true,"taa.it":true,"tos.it":true,"toscana.it":true,"trentino-a-adige.it":true,"trentino-aadige.it":true,"trentino-alto-adige.it":true,"trentino-altoadige.it":true,"trentino-s-tirol.it":true,"trentino-stirol.it":true,"trentino-sud-tirol.it":true,"trentino-sudtirol.it":true,"trentino-sued-tirol.it":true,"trentino-suedtirol.it":true,"trentinoa-adige.it":true,"trentinoaadige.it":true,"trentinoalto-adige.it":true,"trentinoaltoadige.it":true,"trentinos-tirol.it":true,"trentinostirol.it":true,"trentinosud-tirol.it":true,"trentinosudtirol.it":true,"trentinosued-tirol.it":true,"trentinosuedtirol.it":true,"tuscany.it":true,"umb.it":true,"umbria.it":true,"val-d-aosta.it":true,"val-daosta.it":true,"vald-aosta.it":true,"valdaosta.it":true,"valle-aosta.it":true,"valle-d-aosta.it":true,"valle-daosta.it":true,"valleaosta.it":true,"valled-aosta.it":true,"valledaosta.it":true,"vallee-aoste.it":true,"valleeaoste.it":true,"vao.it":true,"vda.it":true,"ven.it":true,"veneto.it":true,"ag.it":true,"agrigento.it":true,"al.it":true,"alessandria.it":true,"alto-adige.it":true,"altoadige.it":true,"an.it":true,"ancona.it":true,"andria-barletta-trani.it":true,"andria-trani-barletta.it":true,"andriabarlettatrani.it":true,"andriatranibarletta.it":true,"ao.it":true,"aosta.it":true,"aoste.it":true,"ap.it":true,"aq.it":true,"aquila.it":true,"ar.it":true,"arezzo.it":true,"ascoli-piceno.it":true,"ascolipiceno.it":true,"asti.it":true,"at.it":true,"av.it":true,"avellino.it":true,"ba.it":true,"balsan.it":true,"bari.it":true,"barletta-trani-andria.it":true,"barlettatraniandria.it":true,"belluno.it":true,"benevento.it":true,"bergamo.it":true,"bg.it":true,"bi.it":true,"biella.it":true,"bl.it":true,"bn.it":true,"bo.it":true,"bologna.it":true,"bolzano.it":true,"bozen.it":true,"br.it":true,"brescia.it":true,"brindisi.it":true,"bs.it":true,"bt.it":true,"bz.it":true,"ca.it":true,"cagliari.it":true,"caltanissetta.it":true,"campidano-medio.it":true,"campidanomedio.it":true,"campobasso.it":true,"carbonia-iglesias.it":true,"carboniaiglesias.it":true,"carrara-massa.it":true,"carraramassa.it":true,"caserta.it":true,"catania.it":true,"catanzaro.it":true,"cb.it":true,"ce.it":true,"cesena-forli.it":true,"cesenaforli.it":true,"ch.it":true,"chieti.it":true,"ci.it":true,"cl.it":true,"cn.it":true,"co.it":true,"como.it":true,"cosenza.it":true,"cr.it":true,"cremona.it":true,"crotone.it":true,"cs.it":true,"ct.it":true,"cuneo.it":true,"cz.it":true,"dell-ogliastra.it":true,"dellogliastra.it":true,"en.it":true,"enna.it":true,"fc.it":true,"fe.it":true,"fermo.it":true,"ferrara.it":true,"fg.it":true,"fi.it":true,"firenze.it":true,"florence.it":true,"fm.it":true,"foggia.it":true,"forli-cesena.it":true,"forlicesena.it":true,"fr.it":true,"frosinone.it":true,"ge.it":true,"genoa.it":true,"genova.it":true,"go.it":true,"gorizia.it":true,"gr.it":true,"grosseto.it":true,"iglesias-carbonia.it":true,"iglesiascarbonia.it":true,"im.it":true,"imperia.it":true,"is.it":true,"isernia.it":true,"kr.it":true,"la-spezia.it":true,"laquila.it":true,"laspezia.it":true,"latina.it":true,"lc.it":true,"le.it":true,"lecce.it":true,"lecco.it":true,"li.it":true,"livorno.it":true,"lo.it":true,"lodi.it":true,"lt.it":true,"lu.it":true,"lucca.it":true,"macerata.it":true,"mantova.it":true,"massa-carrara.it":true,"massacarrara.it":true,"matera.it":true,"mb.it":true,"mc.it":true,"me.it":true,"medio-campidano.it":true,"mediocampidano.it":true,"messina.it":true,"mi.it":true,"milan.it":true,"milano.it":true,"mn.it":true,"mo.it":true,"modena.it":true,"monza-brianza.it":true,"monza-e-della-brianza.it":true,"monza.it":true,"monzabrianza.it":true,"monzaebrianza.it":true,"monzaedellabrianza.it":true,"ms.it":true,"mt.it":true,"na.it":true,"naples.it":true,"napoli.it":true,"no.it":true,"novara.it":true,"nu.it":true,"nuoro.it":true,"og.it":true,"ogliastra.it":true,"olbia-tempio.it":true,"olbiatempio.it":true,"or.it":true,"oristano.it":true,"ot.it":true,"pa.it":true,"padova.it":true,"padua.it":true,"palermo.it":true,"parma.it":true,"pavia.it":true,"pc.it":true,"pd.it":true,"pe.it":true,"perugia.it":true,"pesaro-urbino.it":true,"pesarourbino.it":true,"pescara.it":true,"pg.it":true,"pi.it":true,"piacenza.it":true,"pisa.it":true,"pistoia.it":true,"pn.it":true,"po.it":true,"pordenone.it":true,"potenza.it":true,"pr.it":true,"prato.it":true,"pt.it":true,"pu.it":true,"pv.it":true,"pz.it":true,"ra.it":true,"ragusa.it":true,"ravenna.it":true,"rc.it":true,"re.it":true,"reggio-calabria.it":true,"reggio-emilia.it":true,"reggiocalabria.it":true,"reggioemilia.it":true,"rg.it":true,"ri.it":true,"rieti.it":true,"rimini.it":true,"rm.it":true,"rn.it":true,"ro.it":true,"roma.it":true,"rome.it":true,"rovigo.it":true,"sa.it":true,"salerno.it":true,"sassari.it":true,"savona.it":true,"si.it":true,"siena.it":true,"siracusa.it":true,"so.it":true,"sondrio.it":true,"sp.it":true,"sr.it":true,"ss.it":true,"suedtirol.it":true,"sv.it":true,"ta.it":true,"taranto.it":true,"te.it":true,"tempio-olbia.it":true,"tempioolbia.it":true,"teramo.it":true,"terni.it":true,"tn.it":true,"to.it":true,"torino.it":true,"tp.it":true,"tr.it":true,"trani-andria-barletta.it":true,"trani-barletta-andria.it":true,"traniandriabarletta.it":true,"tranibarlettaandria.it":true,"trapani.it":true,"trentino.it":true,"trento.it":true,"treviso.it":true,"trieste.it":true,"ts.it":true,"turin.it":true,"tv.it":true,"ud.it":true,"udine.it":true,"urbino-pesaro.it":true,"urbinopesaro.it":true,"va.it":true,"varese.it":true,"vb.it":true,"vc.it":true,"ve.it":true,"venezia.it":true,"venice.it":true,"verbania.it":true,"vercelli.it":true,"verona.it":true,"vi.it":true,"vibo-valentia.it":true,"vibovalentia.it":true,"vicenza.it":true,"viterbo.it":true,"vr.it":true,"vs.it":true,"vt.it":true,"vv.it":true,"je":true,"co.je":true,"net.je":true,"org.je":true,"*.jm":true,"jo":true,"com.jo":true,"org.jo":true,"net.jo":true,"edu.jo":true,"sch.jo":true,"gov.jo":true,"mil.jo":true,"name.jo":true,"jobs":true,"jp":true,"ac.jp":true,"ad.jp":true,"co.jp":true,"ed.jp":true,"go.jp":true,"gr.jp":true,"lg.jp":true,"ne.jp":true,"or.jp":true,"aichi.jp":true,"akita.jp":true,"aomori.jp":true,"chiba.jp":true,"ehime.jp":true,"fukui.jp":true,"fukuoka.jp":true,"fukushima.jp":true,"gifu.jp":true,"gunma.jp":true,"hiroshima.jp":true,"hokkaido.jp":true,"hyogo.jp":true,"ibaraki.jp":true,"ishikawa.jp":true,"iwate.jp":true,"kagawa.jp":true,"kagoshima.jp":true,"kanagawa.jp":true,"kochi.jp":true,"kumamoto.jp":true,"kyoto.jp":true,"mie.jp":true,"miyagi.jp":true,"miyazaki.jp":true,"nagano.jp":true,"nagasaki.jp":true,"nara.jp":true,"niigata.jp":true,"oita.jp":true,"okayama.jp":true,"okinawa.jp":true,"osaka.jp":true,"saga.jp":true,"saitama.jp":true,"shiga.jp":true,"shimane.jp":true,"shizuoka.jp":true,"tochigi.jp":true,"tokushima.jp":true,"tokyo.jp":true,"tottori.jp":true,"toyama.jp":true,"wakayama.jp":true,"yamagata.jp":true,"yamaguchi.jp":true,"yamanashi.jp":true,"xn--4pvxs.jp":true,"xn--vgu402c.jp":true,"xn--c3s14m.jp":true,"xn--f6qx53a.jp":true,"xn--8pvr4u.jp":true,"xn--uist22h.jp":true,"xn--djrs72d6uy.jp":true,"xn--mkru45i.jp":true,"xn--0trq7p7nn.jp":true,"xn--8ltr62k.jp":true,"xn--2m4a15e.jp":true,"xn--efvn9s.jp":true,"xn--32vp30h.jp":true,"xn--4it797k.jp":true,"xn--1lqs71d.jp":true,"xn--5rtp49c.jp":true,"xn--5js045d.jp":true,"xn--ehqz56n.jp":true,"xn--1lqs03n.jp":true,"xn--qqqt11m.jp":true,"xn--kbrq7o.jp":true,"xn--pssu33l.jp":true,"xn--ntsq17g.jp":true,"xn--uisz3g.jp":true,"xn--6btw5a.jp":true,"xn--1ctwo.jp":true,"xn--6orx2r.jp":true,"xn--rht61e.jp":true,"xn--rht27z.jp":true,"xn--djty4k.jp":true,"xn--nit225k.jp":true,"xn--rht3d.jp":true,"xn--klty5x.jp":true,"xn--kltx9a.jp":true,"xn--kltp7d.jp":true,"xn--uuwu58a.jp":true,"xn--zbx025d.jp":true,"xn--ntso0iqx3a.jp":true,"xn--elqq16h.jp":true,"xn--4it168d.jp":true,"xn--klt787d.jp":true,"xn--rny31h.jp":true,"xn--7t0a264c.jp":true,"xn--5rtq34k.jp":true,"xn--k7yn95e.jp":true,"xn--tor131o.jp":true,"xn--d5qv7z876c.jp":true,"*.kawasaki.jp":true,"*.kitakyushu.jp":true,"*.kobe.jp":true,"*.nagoya.jp":true,"*.sapporo.jp":true,"*.sendai.jp":true,"*.yokohama.jp":true,"city.kawasaki.jp":false,"city.kitakyushu.jp":false,"city.kobe.jp":false,"city.nagoya.jp":false,"city.sapporo.jp":false,"city.sendai.jp":false,"city.yokohama.jp":false,"aisai.aichi.jp":true,"ama.aichi.jp":true,"anjo.aichi.jp":true,"asuke.aichi.jp":true,"chiryu.aichi.jp":true,"chita.aichi.jp":true,"fuso.aichi.jp":true,"gamagori.aichi.jp":true,"handa.aichi.jp":true,"hazu.aichi.jp":true,"hekinan.aichi.jp":true,"higashiura.aichi.jp":true,"ichinomiya.aichi.jp":true,"inazawa.aichi.jp":true,"inuyama.aichi.jp":true,"isshiki.aichi.jp":true,"iwakura.aichi.jp":true,"kanie.aichi.jp":true,"kariya.aichi.jp":true,"kasugai.aichi.jp":true,"kira.aichi.jp":true,"kiyosu.aichi.jp":true,"komaki.aichi.jp":true,"konan.aichi.jp":true,"kota.aichi.jp":true,"mihama.aichi.jp":true,"miyoshi.aichi.jp":true,"nishio.aichi.jp":true,"nisshin.aichi.jp":true,"obu.aichi.jp":true,"oguchi.aichi.jp":true,"oharu.aichi.jp":true,"okazaki.aichi.jp":true,"owariasahi.aichi.jp":true,"seto.aichi.jp":true,"shikatsu.aichi.jp":true,"shinshiro.aichi.jp":true,"shitara.aichi.jp":true,"tahara.aichi.jp":true,"takahama.aichi.jp":true,"tobishima.aichi.jp":true,"toei.aichi.jp":true,"togo.aichi.jp":true,"tokai.aichi.jp":true,"tokoname.aichi.jp":true,"toyoake.aichi.jp":true,"toyohashi.aichi.jp":true,"toyokawa.aichi.jp":true,"toyone.aichi.jp":true,"toyota.aichi.jp":true,"tsushima.aichi.jp":true,"yatomi.aichi.jp":true,"akita.akita.jp":true,"daisen.akita.jp":true,"fujisato.akita.jp":true,"gojome.akita.jp":true,"hachirogata.akita.jp":true,"happou.akita.jp":true,"higashinaruse.akita.jp":true,"honjo.akita.jp":true,"honjyo.akita.jp":true,"ikawa.akita.jp":true,"kamikoani.akita.jp":true,"kamioka.akita.jp":true,"katagami.akita.jp":true,"kazuno.akita.jp":true,"kitaakita.akita.jp":true,"kosaka.akita.jp":true,"kyowa.akita.jp":true,"misato.akita.jp":true,"mitane.akita.jp":true,"moriyoshi.akita.jp":true,"nikaho.akita.jp":true,"noshiro.akita.jp":true,"odate.akita.jp":true,"oga.akita.jp":true,"ogata.akita.jp":true,"semboku.akita.jp":true,"yokote.akita.jp":true,"yurihonjo.akita.jp":true,"aomori.aomori.jp":true,"gonohe.aomori.jp":true,"hachinohe.aomori.jp":true,"hashikami.aomori.jp":true,"hiranai.aomori.jp":true,"hirosaki.aomori.jp":true,"itayanagi.aomori.jp":true,"kuroishi.aomori.jp":true,"misawa.aomori.jp":true,"mutsu.aomori.jp":true,"nakadomari.aomori.jp":true,"noheji.aomori.jp":true,"oirase.aomori.jp":true,"owani.aomori.jp":true,"rokunohe.aomori.jp":true,"sannohe.aomori.jp":true,"shichinohe.aomori.jp":true,"shingo.aomori.jp":true,"takko.aomori.jp":true,"towada.aomori.jp":true,"tsugaru.aomori.jp":true,"tsuruta.aomori.jp":true,"abiko.chiba.jp":true,"asahi.chiba.jp":true,"chonan.chiba.jp":true,"chosei.chiba.jp":true,"choshi.chiba.jp":true,"chuo.chiba.jp":true,"funabashi.chiba.jp":true,"futtsu.chiba.jp":true,"hanamigawa.chiba.jp":true,"ichihara.chiba.jp":true,"ichikawa.chiba.jp":true,"ichinomiya.chiba.jp":true,"inzai.chiba.jp":true,"isumi.chiba.jp":true,"kamagaya.chiba.jp":true,"kamogawa.chiba.jp":true,"kashiwa.chiba.jp":true,"katori.chiba.jp":true,"katsuura.chiba.jp":true,"kimitsu.chiba.jp":true,"kisarazu.chiba.jp":true,"kozaki.chiba.jp":true,"kujukuri.chiba.jp":true,"kyonan.chiba.jp":true,"matsudo.chiba.jp":true,"midori.chiba.jp":true,"mihama.chiba.jp":true,"minamiboso.chiba.jp":true,"mobara.chiba.jp":true,"mutsuzawa.chiba.jp":true,"nagara.chiba.jp":true,"nagareyama.chiba.jp":true,"narashino.chiba.jp":true,"narita.chiba.jp":true,"noda.chiba.jp":true,"oamishirasato.chiba.jp":true,"omigawa.chiba.jp":true,"onjuku.chiba.jp":true,"otaki.chiba.jp":true,"sakae.chiba.jp":true,"sakura.chiba.jp":true,"shimofusa.chiba.jp":true,"shirako.chiba.jp":true,"shiroi.chiba.jp":true,"shisui.chiba.jp":true,"sodegaura.chiba.jp":true,"sosa.chiba.jp":true,"tako.chiba.jp":true,"tateyama.chiba.jp":true,"togane.chiba.jp":true,"tohnosho.chiba.jp":true,"tomisato.chiba.jp":true,"urayasu.chiba.jp":true,"yachimata.chiba.jp":true,"yachiyo.chiba.jp":true,"yokaichiba.chiba.jp":true,"yokoshibahikari.chiba.jp":true,"yotsukaido.chiba.jp":true,"ainan.ehime.jp":true,"honai.ehime.jp":true,"ikata.ehime.jp":true,"imabari.ehime.jp":true,"iyo.ehime.jp":true,"kamijima.ehime.jp":true,"kihoku.ehime.jp":true,"kumakogen.ehime.jp":true,"masaki.ehime.jp":true,"matsuno.ehime.jp":true,"matsuyama.ehime.jp":true,"namikata.ehime.jp":true,"niihama.ehime.jp":true,"ozu.ehime.jp":true,"saijo.ehime.jp":true,"seiyo.ehime.jp":true,"shikokuchuo.ehime.jp":true,"tobe.ehime.jp":true,"toon.ehime.jp":true,"uchiko.ehime.jp":true,"uwajima.ehime.jp":true,"yawatahama.ehime.jp":true,"echizen.fukui.jp":true,"eiheiji.fukui.jp":true,"fukui.fukui.jp":true,"ikeda.fukui.jp":true,"katsuyama.fukui.jp":true,"mihama.fukui.jp":true,"minamiechizen.fukui.jp":true,"obama.fukui.jp":true,"ohi.fukui.jp":true,"ono.fukui.jp":true,"sabae.fukui.jp":true,"sakai.fukui.jp":true,"takahama.fukui.jp":true,"tsuruga.fukui.jp":true,"wakasa.fukui.jp":true,"ashiya.fukuoka.jp":true,"buzen.fukuoka.jp":true,"chikugo.fukuoka.jp":true,"chikuho.fukuoka.jp":true,"chikujo.fukuoka.jp":true,"chikushino.fukuoka.jp":true,"chikuzen.fukuoka.jp":true,"chuo.fukuoka.jp":true,"dazaifu.fukuoka.jp":true,"fukuchi.fukuoka.jp":true,"hakata.fukuoka.jp":true,"higashi.fukuoka.jp":true,"hirokawa.fukuoka.jp":true,"hisayama.fukuoka.jp":true,"iizuka.fukuoka.jp":true,"inatsuki.fukuoka.jp":true,"kaho.fukuoka.jp":true,"kasuga.fukuoka.jp":true,"kasuya.fukuoka.jp":true,"kawara.fukuoka.jp":true,"keisen.fukuoka.jp":true,"koga.fukuoka.jp":true,"kurate.fukuoka.jp":true,"kurogi.fukuoka.jp":true,"kurume.fukuoka.jp":true,"minami.fukuoka.jp":true,"miyako.fukuoka.jp":true,"miyama.fukuoka.jp":true,"miyawaka.fukuoka.jp":true,"mizumaki.fukuoka.jp":true,"munakata.fukuoka.jp":true,"nakagawa.fukuoka.jp":true,"nakama.fukuoka.jp":true,"nishi.fukuoka.jp":true,"nogata.fukuoka.jp":true,"ogori.fukuoka.jp":true,"okagaki.fukuoka.jp":true,"okawa.fukuoka.jp":true,"oki.fukuoka.jp":true,"omuta.fukuoka.jp":true,"onga.fukuoka.jp":true,"onojo.fukuoka.jp":true,"oto.fukuoka.jp":true,"saigawa.fukuoka.jp":true,"sasaguri.fukuoka.jp":true,"shingu.fukuoka.jp":true,"shinyoshitomi.fukuoka.jp":true,"shonai.fukuoka.jp":true,"soeda.fukuoka.jp":true,"sue.fukuoka.jp":true,"tachiarai.fukuoka.jp":true,"tagawa.fukuoka.jp":true,"takata.fukuoka.jp":true,"toho.fukuoka.jp":true,"toyotsu.fukuoka.jp":true,"tsuiki.fukuoka.jp":true,"ukiha.fukuoka.jp":true,"umi.fukuoka.jp":true,"usui.fukuoka.jp":true,"yamada.fukuoka.jp":true,"yame.fukuoka.jp":true,"yanagawa.fukuoka.jp":true,"yukuhashi.fukuoka.jp":true,"aizubange.fukushima.jp":true,"aizumisato.fukushima.jp":true,"aizuwakamatsu.fukushima.jp":true,"asakawa.fukushima.jp":true,"bandai.fukushima.jp":true,"date.fukushima.jp":true,"fukushima.fukushima.jp":true,"furudono.fukushima.jp":true,"futaba.fukushima.jp":true,"hanawa.fukushima.jp":true,"higashi.fukushima.jp":true,"hirata.fukushima.jp":true,"hirono.fukushima.jp":true,"iitate.fukushima.jp":true,"inawashiro.fukushima.jp":true,"ishikawa.fukushima.jp":true,"iwaki.fukushima.jp":true,"izumizaki.fukushima.jp":true,"kagamiishi.fukushima.jp":true,"kaneyama.fukushima.jp":true,"kawamata.fukushima.jp":true,"kitakata.fukushima.jp":true,"kitashiobara.fukushima.jp":true,"koori.fukushima.jp":true,"koriyama.fukushima.jp":true,"kunimi.fukushima.jp":true,"miharu.fukushima.jp":true,"mishima.fukushima.jp":true,"namie.fukushima.jp":true,"nango.fukushima.jp":true,"nishiaizu.fukushima.jp":true,"nishigo.fukushima.jp":true,"okuma.fukushima.jp":true,"omotego.fukushima.jp":true,"ono.fukushima.jp":true,"otama.fukushima.jp":true,"samegawa.fukushima.jp":true,"shimogo.fukushima.jp":true,"shirakawa.fukushima.jp":true,"showa.fukushima.jp":true,"soma.fukushima.jp":true,"sukagawa.fukushima.jp":true,"taishin.fukushima.jp":true,"tamakawa.fukushima.jp":true,"tanagura.fukushima.jp":true,"tenei.fukushima.jp":true,"yabuki.fukushima.jp":true,"yamato.fukushima.jp":true,"yamatsuri.fukushima.jp":true,"yanaizu.fukushima.jp":true,"yugawa.fukushima.jp":true,"anpachi.gifu.jp":true,"ena.gifu.jp":true,"gifu.gifu.jp":true,"ginan.gifu.jp":true,"godo.gifu.jp":true,"gujo.gifu.jp":true,"hashima.gifu.jp":true,"hichiso.gifu.jp":true,"hida.gifu.jp":true,"higashishirakawa.gifu.jp":true,"ibigawa.gifu.jp":true,"ikeda.gifu.jp":true,"kakamigahara.gifu.jp":true,"kani.gifu.jp":true,"kasahara.gifu.jp":true,"kasamatsu.gifu.jp":true,"kawaue.gifu.jp":true,"kitagata.gifu.jp":true,"mino.gifu.jp":true,"minokamo.gifu.jp":true,"mitake.gifu.jp":true,"mizunami.gifu.jp":true,"motosu.gifu.jp":true,"nakatsugawa.gifu.jp":true,"ogaki.gifu.jp":true,"sakahogi.gifu.jp":true,"seki.gifu.jp":true,"sekigahara.gifu.jp":true,"shirakawa.gifu.jp":true,"tajimi.gifu.jp":true,"takayama.gifu.jp":true,"tarui.gifu.jp":true,"toki.gifu.jp":true,"tomika.gifu.jp":true,"wanouchi.gifu.jp":true,"yamagata.gifu.jp":true,"yaotsu.gifu.jp":true,"yoro.gifu.jp":true,"annaka.gunma.jp":true,"chiyoda.gunma.jp":true,"fujioka.gunma.jp":true,"higashiagatsuma.gunma.jp":true,"isesaki.gunma.jp":true,"itakura.gunma.jp":true,"kanna.gunma.jp":true,"kanra.gunma.jp":true,"katashina.gunma.jp":true,"kawaba.gunma.jp":true,"kiryu.gunma.jp":true,"kusatsu.gunma.jp":true,"maebashi.gunma.jp":true,"meiwa.gunma.jp":true,"midori.gunma.jp":true,"minakami.gunma.jp":true,"naganohara.gunma.jp":true,"nakanojo.gunma.jp":true,"nanmoku.gunma.jp":true,"numata.gunma.jp":true,"oizumi.gunma.jp":true,"ora.gunma.jp":true,"ota.gunma.jp":true,"shibukawa.gunma.jp":true,"shimonita.gunma.jp":true,"shinto.gunma.jp":true,"showa.gunma.jp":true,"takasaki.gunma.jp":true,"takayama.gunma.jp":true,"tamamura.gunma.jp":true,"tatebayashi.gunma.jp":true,"tomioka.gunma.jp":true,"tsukiyono.gunma.jp":true,"tsumagoi.gunma.jp":true,"ueno.gunma.jp":true,"yoshioka.gunma.jp":true,"asaminami.hiroshima.jp":true,"daiwa.hiroshima.jp":true,"etajima.hiroshima.jp":true,"fuchu.hiroshima.jp":true,"fukuyama.hiroshima.jp":true,"hatsukaichi.hiroshima.jp":true,"higashihiroshima.hiroshima.jp":true,"hongo.hiroshima.jp":true,"jinsekikogen.hiroshima.jp":true,"kaita.hiroshima.jp":true,"kui.hiroshima.jp":true,"kumano.hiroshima.jp":true,"kure.hiroshima.jp":true,"mihara.hiroshima.jp":true,"miyoshi.hiroshima.jp":true,"naka.hiroshima.jp":true,"onomichi.hiroshima.jp":true,"osakikamijima.hiroshima.jp":true,"otake.hiroshima.jp":true,"saka.hiroshima.jp":true,"sera.hiroshima.jp":true,"seranishi.hiroshima.jp":true,"shinichi.hiroshima.jp":true,"shobara.hiroshima.jp":true,"takehara.hiroshima.jp":true,"abashiri.hokkaido.jp":true,"abira.hokkaido.jp":true,"aibetsu.hokkaido.jp":true,"akabira.hokkaido.jp":true,"akkeshi.hokkaido.jp":true,"asahikawa.hokkaido.jp":true,"ashibetsu.hokkaido.jp":true,"ashoro.hokkaido.jp":true,"assabu.hokkaido.jp":true,"atsuma.hokkaido.jp":true,"bibai.hokkaido.jp":true,"biei.hokkaido.jp":true,"bifuka.hokkaido.jp":true,"bihoro.hokkaido.jp":true,"biratori.hokkaido.jp":true,"chippubetsu.hokkaido.jp":true,"chitose.hokkaido.jp":true,"date.hokkaido.jp":true,"ebetsu.hokkaido.jp":true,"embetsu.hokkaido.jp":true,"eniwa.hokkaido.jp":true,"erimo.hokkaido.jp":true,"esan.hokkaido.jp":true,"esashi.hokkaido.jp":true,"fukagawa.hokkaido.jp":true,"fukushima.hokkaido.jp":true,"furano.hokkaido.jp":true,"furubira.hokkaido.jp":true,"haboro.hokkaido.jp":true,"hakodate.hokkaido.jp":true,"hamatonbetsu.hokkaido.jp":true,"hidaka.hokkaido.jp":true,"higashikagura.hokkaido.jp":true,"higashikawa.hokkaido.jp":true,"hiroo.hokkaido.jp":true,"hokuryu.hokkaido.jp":true,"hokuto.hokkaido.jp":true,"honbetsu.hokkaido.jp":true,"horokanai.hokkaido.jp":true,"horonobe.hokkaido.jp":true,"ikeda.hokkaido.jp":true,"imakane.hokkaido.jp":true,"ishikari.hokkaido.jp":true,"iwamizawa.hokkaido.jp":true,"iwanai.hokkaido.jp":true,"kamifurano.hokkaido.jp":true,"kamikawa.hokkaido.jp":true,"kamishihoro.hokkaido.jp":true,"kamisunagawa.hokkaido.jp":true,"kamoenai.hokkaido.jp":true,"kayabe.hokkaido.jp":true,"kembuchi.hokkaido.jp":true,"kikonai.hokkaido.jp":true,"kimobetsu.hokkaido.jp":true,"kitahiroshima.hokkaido.jp":true,"kitami.hokkaido.jp":true,"kiyosato.hokkaido.jp":true,"koshimizu.hokkaido.jp":true,"kunneppu.hokkaido.jp":true,"kuriyama.hokkaido.jp":true,"kuromatsunai.hokkaido.jp":true,"kushiro.hokkaido.jp":true,"kutchan.hokkaido.jp":true,"kyowa.hokkaido.jp":true,"mashike.hokkaido.jp":true,"matsumae.hokkaido.jp":true,"mikasa.hokkaido.jp":true,"minamifurano.hokkaido.jp":true,"mombetsu.hokkaido.jp":true,"moseushi.hokkaido.jp":true,"mukawa.hokkaido.jp":true,"muroran.hokkaido.jp":true,"naie.hokkaido.jp":true,"nakagawa.hokkaido.jp":true,"nakasatsunai.hokkaido.jp":true,"nakatombetsu.hokkaido.jp":true,"nanae.hokkaido.jp":true,"nanporo.hokkaido.jp":true,"nayoro.hokkaido.jp":true,"nemuro.hokkaido.jp":true,"niikappu.hokkaido.jp":true,"niki.hokkaido.jp":true,"nishiokoppe.hokkaido.jp":true,"noboribetsu.hokkaido.jp":true,"numata.hokkaido.jp":true,"obihiro.hokkaido.jp":true,"obira.hokkaido.jp":true,"oketo.hokkaido.jp":true,"okoppe.hokkaido.jp":true,"otaru.hokkaido.jp":true,"otobe.hokkaido.jp":true,"otofuke.hokkaido.jp":true,"otoineppu.hokkaido.jp":true,"oumu.hokkaido.jp":true,"ozora.hokkaido.jp":true,"pippu.hokkaido.jp":true,"rankoshi.hokkaido.jp":true,"rebun.hokkaido.jp":true,"rikubetsu.hokkaido.jp":true,"rishiri.hokkaido.jp":true,"rishirifuji.hokkaido.jp":true,"saroma.hokkaido.jp":true,"sarufutsu.hokkaido.jp":true,"shakotan.hokkaido.jp":true,"shari.hokkaido.jp":true,"shibecha.hokkaido.jp":true,"shibetsu.hokkaido.jp":true,"shikabe.hokkaido.jp":true,"shikaoi.hokkaido.jp":true,"shimamaki.hokkaido.jp":true,"shimizu.hokkaido.jp":true,"shimokawa.hokkaido.jp":true,"shinshinotsu.hokkaido.jp":true,"shintoku.hokkaido.jp":true,"shiranuka.hokkaido.jp":true,"shiraoi.hokkaido.jp":true,"shiriuchi.hokkaido.jp":true,"sobetsu.hokkaido.jp":true,"sunagawa.hokkaido.jp":true,"taiki.hokkaido.jp":true,"takasu.hokkaido.jp":true,"takikawa.hokkaido.jp":true,"takinoue.hokkaido.jp":true,"teshikaga.hokkaido.jp":true,"tobetsu.hokkaido.jp":true,"tohma.hokkaido.jp":true,"tomakomai.hokkaido.jp":true,"tomari.hokkaido.jp":true,"toya.hokkaido.jp":true,"toyako.hokkaido.jp":true,"toyotomi.hokkaido.jp":true,"toyoura.hokkaido.jp":true,"tsubetsu.hokkaido.jp":true,"tsukigata.hokkaido.jp":true,"urakawa.hokkaido.jp":true,"urausu.hokkaido.jp":true,"uryu.hokkaido.jp":true,"utashinai.hokkaido.jp":true,"wakkanai.hokkaido.jp":true,"wassamu.hokkaido.jp":true,"yakumo.hokkaido.jp":true,"yoichi.hokkaido.jp":true,"aioi.hyogo.jp":true,"akashi.hyogo.jp":true,"ako.hyogo.jp":true,"amagasaki.hyogo.jp":true,"aogaki.hyogo.jp":true,"asago.hyogo.jp":true,"ashiya.hyogo.jp":true,"awaji.hyogo.jp":true,"fukusaki.hyogo.jp":true,"goshiki.hyogo.jp":true,"harima.hyogo.jp":true,"himeji.hyogo.jp":true,"ichikawa.hyogo.jp":true,"inagawa.hyogo.jp":true,"itami.hyogo.jp":true,"kakogawa.hyogo.jp":true,"kamigori.hyogo.jp":true,"kamikawa.hyogo.jp":true,"kasai.hyogo.jp":true,"kasuga.hyogo.jp":true,"kawanishi.hyogo.jp":true,"miki.hyogo.jp":true,"minamiawaji.hyogo.jp":true,"nishinomiya.hyogo.jp":true,"nishiwaki.hyogo.jp":true,"ono.hyogo.jp":true,"sanda.hyogo.jp":true,"sannan.hyogo.jp":true,"sasayama.hyogo.jp":true,"sayo.hyogo.jp":true,"shingu.hyogo.jp":true,"shinonsen.hyogo.jp":true,"shiso.hyogo.jp":true,"sumoto.hyogo.jp":true,"taishi.hyogo.jp":true,"taka.hyogo.jp":true,"takarazuka.hyogo.jp":true,"takasago.hyogo.jp":true,"takino.hyogo.jp":true,"tamba.hyogo.jp":true,"tatsuno.hyogo.jp":true,"toyooka.hyogo.jp":true,"yabu.hyogo.jp":true,"yashiro.hyogo.jp":true,"yoka.hyogo.jp":true,"yokawa.hyogo.jp":true,"ami.ibaraki.jp":true,"asahi.ibaraki.jp":true,"bando.ibaraki.jp":true,"chikusei.ibaraki.jp":true,"daigo.ibaraki.jp":true,"fujishiro.ibaraki.jp":true,"hitachi.ibaraki.jp":true,"hitachinaka.ibaraki.jp":true,"hitachiomiya.ibaraki.jp":true,"hitachiota.ibaraki.jp":true,"ibaraki.ibaraki.jp":true,"ina.ibaraki.jp":true,"inashiki.ibaraki.jp":true,"itako.ibaraki.jp":true,"iwama.ibaraki.jp":true,"joso.ibaraki.jp":true,"kamisu.ibaraki.jp":true,"kasama.ibaraki.jp":true,"kashima.ibaraki.jp":true,"kasumigaura.ibaraki.jp":true,"koga.ibaraki.jp":true,"miho.ibaraki.jp":true,"mito.ibaraki.jp":true,"moriya.ibaraki.jp":true,"naka.ibaraki.jp":true,"namegata.ibaraki.jp":true,"oarai.ibaraki.jp":true,"ogawa.ibaraki.jp":true,"omitama.ibaraki.jp":true,"ryugasaki.ibaraki.jp":true,"sakai.ibaraki.jp":true,"sakuragawa.ibaraki.jp":true,"shimodate.ibaraki.jp":true,"shimotsuma.ibaraki.jp":true,"shirosato.ibaraki.jp":true,"sowa.ibaraki.jp":true,"suifu.ibaraki.jp":true,"takahagi.ibaraki.jp":true,"tamatsukuri.ibaraki.jp":true,"tokai.ibaraki.jp":true,"tomobe.ibaraki.jp":true,"tone.ibaraki.jp":true,"toride.ibaraki.jp":true,"tsuchiura.ibaraki.jp":true,"tsukuba.ibaraki.jp":true,"uchihara.ibaraki.jp":true,"ushiku.ibaraki.jp":true,"yachiyo.ibaraki.jp":true,"yamagata.ibaraki.jp":true,"yawara.ibaraki.jp":true,"yuki.ibaraki.jp":true,"anamizu.ishikawa.jp":true,"hakui.ishikawa.jp":true,"hakusan.ishikawa.jp":true,"kaga.ishikawa.jp":true,"kahoku.ishikawa.jp":true,"kanazawa.ishikawa.jp":true,"kawakita.ishikawa.jp":true,"komatsu.ishikawa.jp":true,"nakanoto.ishikawa.jp":true,"nanao.ishikawa.jp":true,"nomi.ishikawa.jp":true,"nonoichi.ishikawa.jp":true,"noto.ishikawa.jp":true,"shika.ishikawa.jp":true,"suzu.ishikawa.jp":true,"tsubata.ishikawa.jp":true,"tsurugi.ishikawa.jp":true,"uchinada.ishikawa.jp":true,"wajima.ishikawa.jp":true,"fudai.iwate.jp":true,"fujisawa.iwate.jp":true,"hanamaki.iwate.jp":true,"hiraizumi.iwate.jp":true,"hirono.iwate.jp":true,"ichinohe.iwate.jp":true,"ichinoseki.iwate.jp":true,"iwaizumi.iwate.jp":true,"iwate.iwate.jp":true,"joboji.iwate.jp":true,"kamaishi.iwate.jp":true,"kanegasaki.iwate.jp":true,"karumai.iwate.jp":true,"kawai.iwate.jp":true,"kitakami.iwate.jp":true,"kuji.iwate.jp":true,"kunohe.iwate.jp":true,"kuzumaki.iwate.jp":true,"miyako.iwate.jp":true,"mizusawa.iwate.jp":true,"morioka.iwate.jp":true,"ninohe.iwate.jp":true,"noda.iwate.jp":true,"ofunato.iwate.jp":true,"oshu.iwate.jp":true,"otsuchi.iwate.jp":true,"rikuzentakata.iwate.jp":true,"shiwa.iwate.jp":true,"shizukuishi.iwate.jp":true,"sumita.iwate.jp":true,"tanohata.iwate.jp":true,"tono.iwate.jp":true,"yahaba.iwate.jp":true,"yamada.iwate.jp":true,"ayagawa.kagawa.jp":true,"higashikagawa.kagawa.jp":true,"kanonji.kagawa.jp":true,"kotohira.kagawa.jp":true,"manno.kagawa.jp":true,"marugame.kagawa.jp":true,"mitoyo.kagawa.jp":true,"naoshima.kagawa.jp":true,"sanuki.kagawa.jp":true,"tadotsu.kagawa.jp":true,"takamatsu.kagawa.jp":true,"tonosho.kagawa.jp":true,"uchinomi.kagawa.jp":true,"utazu.kagawa.jp":true,"zentsuji.kagawa.jp":true,"akune.kagoshima.jp":true,"amami.kagoshima.jp":true,"hioki.kagoshima.jp":true,"isa.kagoshima.jp":true,"isen.kagoshima.jp":true,"izumi.kagoshima.jp":true,"kagoshima.kagoshima.jp":true,"kanoya.kagoshima.jp":true,"kawanabe.kagoshima.jp":true,"kinko.kagoshima.jp":true,"kouyama.kagoshima.jp":true,"makurazaki.kagoshima.jp":true,"matsumoto.kagoshima.jp":true,"minamitane.kagoshima.jp":true,"nakatane.kagoshima.jp":true,"nishinoomote.kagoshima.jp":true,"satsumasendai.kagoshima.jp":true,"soo.kagoshima.jp":true,"tarumizu.kagoshima.jp":true,"yusui.kagoshima.jp":true,"aikawa.kanagawa.jp":true,"atsugi.kanagawa.jp":true,"ayase.kanagawa.jp":true,"chigasaki.kanagawa.jp":true,"ebina.kanagawa.jp":true,"fujisawa.kanagawa.jp":true,"hadano.kanagawa.jp":true,"hakone.kanagawa.jp":true,"hiratsuka.kanagawa.jp":true,"isehara.kanagawa.jp":true,"kaisei.kanagawa.jp":true,"kamakura.kanagawa.jp":true,"kiyokawa.kanagawa.jp":true,"matsuda.kanagawa.jp":true,"minamiashigara.kanagawa.jp":true,"miura.kanagawa.jp":true,"nakai.kanagawa.jp":true,"ninomiya.kanagawa.jp":true,"odawara.kanagawa.jp":true,"oi.kanagawa.jp":true,"oiso.kanagawa.jp":true,"sagamihara.kanagawa.jp":true,"samukawa.kanagawa.jp":true,"tsukui.kanagawa.jp":true,"yamakita.kanagawa.jp":true,"yamato.kanagawa.jp":true,"yokosuka.kanagawa.jp":true,"yugawara.kanagawa.jp":true,"zama.kanagawa.jp":true,"zushi.kanagawa.jp":true,"aki.kochi.jp":true,"geisei.kochi.jp":true,"hidaka.kochi.jp":true,"higashitsuno.kochi.jp":true,"ino.kochi.jp":true,"kagami.kochi.jp":true,"kami.kochi.jp":true,"kitagawa.kochi.jp":true,"kochi.kochi.jp":true,"mihara.kochi.jp":true,"motoyama.kochi.jp":true,"muroto.kochi.jp":true,"nahari.kochi.jp":true,"nakamura.kochi.jp":true,"nankoku.kochi.jp":true,"nishitosa.kochi.jp":true,"niyodogawa.kochi.jp":true,"ochi.kochi.jp":true,"okawa.kochi.jp":true,"otoyo.kochi.jp":true,"otsuki.kochi.jp":true,"sakawa.kochi.jp":true,"sukumo.kochi.jp":true,"susaki.kochi.jp":true,"tosa.kochi.jp":true,"tosashimizu.kochi.jp":true,"toyo.kochi.jp":true,"tsuno.kochi.jp":true,"umaji.kochi.jp":true,"yasuda.kochi.jp":true,"yusuhara.kochi.jp":true,"amakusa.kumamoto.jp":true,"arao.kumamoto.jp":true,"aso.kumamoto.jp":true,"choyo.kumamoto.jp":true,"gyokuto.kumamoto.jp":true,"hitoyoshi.kumamoto.jp":true,"kamiamakusa.kumamoto.jp":true,"kashima.kumamoto.jp":true,"kikuchi.kumamoto.jp":true,"kosa.kumamoto.jp":true,"kumamoto.kumamoto.jp":true,"mashiki.kumamoto.jp":true,"mifune.kumamoto.jp":true,"minamata.kumamoto.jp":true,"minamioguni.kumamoto.jp":true,"nagasu.kumamoto.jp":true,"nishihara.kumamoto.jp":true,"oguni.kumamoto.jp":true,"ozu.kumamoto.jp":true,"sumoto.kumamoto.jp":true,"takamori.kumamoto.jp":true,"uki.kumamoto.jp":true,"uto.kumamoto.jp":true,"yamaga.kumamoto.jp":true,"yamato.kumamoto.jp":true,"yatsushiro.kumamoto.jp":true,"ayabe.kyoto.jp":true,"fukuchiyama.kyoto.jp":true,"higashiyama.kyoto.jp":true,"ide.kyoto.jp":true,"ine.kyoto.jp":true,"joyo.kyoto.jp":true,"kameoka.kyoto.jp":true,"kamo.kyoto.jp":true,"kita.kyoto.jp":true,"kizu.kyoto.jp":true,"kumiyama.kyoto.jp":true,"kyotamba.kyoto.jp":true,"kyotanabe.kyoto.jp":true,"kyotango.kyoto.jp":true,"maizuru.kyoto.jp":true,"minami.kyoto.jp":true,"minamiyamashiro.kyoto.jp":true,"miyazu.kyoto.jp":true,"muko.kyoto.jp":true,"nagaokakyo.kyoto.jp":true,"nakagyo.kyoto.jp":true,"nantan.kyoto.jp":true,"oyamazaki.kyoto.jp":true,"sakyo.kyoto.jp":true,"seika.kyoto.jp":true,"tanabe.kyoto.jp":true,"uji.kyoto.jp":true,"ujitawara.kyoto.jp":true,"wazuka.kyoto.jp":true,"yamashina.kyoto.jp":true,"yawata.kyoto.jp":true,"asahi.mie.jp":true,"inabe.mie.jp":true,"ise.mie.jp":true,"kameyama.mie.jp":true,"kawagoe.mie.jp":true,"kiho.mie.jp":true,"kisosaki.mie.jp":true,"kiwa.mie.jp":true,"komono.mie.jp":true,"kumano.mie.jp":true,"kuwana.mie.jp":true,"matsusaka.mie.jp":true,"meiwa.mie.jp":true,"mihama.mie.jp":true,"minamiise.mie.jp":true,"misugi.mie.jp":true,"miyama.mie.jp":true,"nabari.mie.jp":true,"shima.mie.jp":true,"suzuka.mie.jp":true,"tado.mie.jp":true,"taiki.mie.jp":true,"taki.mie.jp":true,"tamaki.mie.jp":true,"toba.mie.jp":true,"tsu.mie.jp":true,"udono.mie.jp":true,"ureshino.mie.jp":true,"watarai.mie.jp":true,"yokkaichi.mie.jp":true,"furukawa.miyagi.jp":true,"higashimatsushima.miyagi.jp":true,"ishinomaki.miyagi.jp":true,"iwanuma.miyagi.jp":true,"kakuda.miyagi.jp":true,"kami.miyagi.jp":true,"kawasaki.miyagi.jp":true,"kesennuma.miyagi.jp":true,"marumori.miyagi.jp":true,"matsushima.miyagi.jp":true,"minamisanriku.miyagi.jp":true,"misato.miyagi.jp":true,"murata.miyagi.jp":true,"natori.miyagi.jp":true,"ogawara.miyagi.jp":true,"ohira.miyagi.jp":true,"onagawa.miyagi.jp":true,"osaki.miyagi.jp":true,"rifu.miyagi.jp":true,"semine.miyagi.jp":true,"shibata.miyagi.jp":true,"shichikashuku.miyagi.jp":true,"shikama.miyagi.jp":true,"shiogama.miyagi.jp":true,"shiroishi.miyagi.jp":true,"tagajo.miyagi.jp":true,"taiwa.miyagi.jp":true,"tome.miyagi.jp":true,"tomiya.miyagi.jp":true,"wakuya.miyagi.jp":true,"watari.miyagi.jp":true,"yamamoto.miyagi.jp":true,"zao.miyagi.jp":true,"aya.miyazaki.jp":true,"ebino.miyazaki.jp":true,"gokase.miyazaki.jp":true,"hyuga.miyazaki.jp":true,"kadogawa.miyazaki.jp":true,"kawaminami.miyazaki.jp":true,"kijo.miyazaki.jp":true,"kitagawa.miyazaki.jp":true,"kitakata.miyazaki.jp":true,"kitaura.miyazaki.jp":true,"kobayashi.miyazaki.jp":true,"kunitomi.miyazaki.jp":true,"kushima.miyazaki.jp":true,"mimata.miyazaki.jp":true,"miyakonojo.miyazaki.jp":true,"miyazaki.miyazaki.jp":true,"morotsuka.miyazaki.jp":true,"nichinan.miyazaki.jp":true,"nishimera.miyazaki.jp":true,"nobeoka.miyazaki.jp":true,"saito.miyazaki.jp":true,"shiiba.miyazaki.jp":true,"shintomi.miyazaki.jp":true,"takaharu.miyazaki.jp":true,"takanabe.miyazaki.jp":true,"takazaki.miyazaki.jp":true,"tsuno.miyazaki.jp":true,"achi.nagano.jp":true,"agematsu.nagano.jp":true,"anan.nagano.jp":true,"aoki.nagano.jp":true,"asahi.nagano.jp":true,"azumino.nagano.jp":true,"chikuhoku.nagano.jp":true,"chikuma.nagano.jp":true,"chino.nagano.jp":true,"fujimi.nagano.jp":true,"hakuba.nagano.jp":true,"hara.nagano.jp":true,"hiraya.nagano.jp":true,"iida.nagano.jp":true,"iijima.nagano.jp":true,"iiyama.nagano.jp":true,"iizuna.nagano.jp":true,"ikeda.nagano.jp":true,"ikusaka.nagano.jp":true,"ina.nagano.jp":true,"karuizawa.nagano.jp":true,"kawakami.nagano.jp":true,"kiso.nagano.jp":true,"kisofukushima.nagano.jp":true,"kitaaiki.nagano.jp":true,"komagane.nagano.jp":true,"komoro.nagano.jp":true,"matsukawa.nagano.jp":true,"matsumoto.nagano.jp":true,"miasa.nagano.jp":true,"minamiaiki.nagano.jp":true,"minamimaki.nagano.jp":true,"minamiminowa.nagano.jp":true,"minowa.nagano.jp":true,"miyada.nagano.jp":true,"miyota.nagano.jp":true,"mochizuki.nagano.jp":true,"nagano.nagano.jp":true,"nagawa.nagano.jp":true,"nagiso.nagano.jp":true,"nakagawa.nagano.jp":true,"nakano.nagano.jp":true,"nozawaonsen.nagano.jp":true,"obuse.nagano.jp":true,"ogawa.nagano.jp":true,"okaya.nagano.jp":true,"omachi.nagano.jp":true,"omi.nagano.jp":true,"ookuwa.nagano.jp":true,"ooshika.nagano.jp":true,"otaki.nagano.jp":true,"otari.nagano.jp":true,"sakae.nagano.jp":true,"sakaki.nagano.jp":true,"saku.nagano.jp":true,"sakuho.nagano.jp":true,"shimosuwa.nagano.jp":true,"shinanomachi.nagano.jp":true,"shiojiri.nagano.jp":true,"suwa.nagano.jp":true,"suzaka.nagano.jp":true,"takagi.nagano.jp":true,"takamori.nagano.jp":true,"takayama.nagano.jp":true,"tateshina.nagano.jp":true,"tatsuno.nagano.jp":true,"togakushi.nagano.jp":true,"togura.nagano.jp":true,"tomi.nagano.jp":true,"ueda.nagano.jp":true,"wada.nagano.jp":true,"yamagata.nagano.jp":true,"yamanouchi.nagano.jp":true,"yasaka.nagano.jp":true,"yasuoka.nagano.jp":true,"chijiwa.nagasaki.jp":true,"futsu.nagasaki.jp":true,"goto.nagasaki.jp":true,"hasami.nagasaki.jp":true,"hirado.nagasaki.jp":true,"iki.nagasaki.jp":true,"isahaya.nagasaki.jp":true,"kawatana.nagasaki.jp":true,"kuchinotsu.nagasaki.jp":true,"matsuura.nagasaki.jp":true,"nagasaki.nagasaki.jp":true,"obama.nagasaki.jp":true,"omura.nagasaki.jp":true,"oseto.nagasaki.jp":true,"saikai.nagasaki.jp":true,"sasebo.nagasaki.jp":true,"seihi.nagasaki.jp":true,"shimabara.nagasaki.jp":true,"shinkamigoto.nagasaki.jp":true,"togitsu.nagasaki.jp":true,"tsushima.nagasaki.jp":true,"unzen.nagasaki.jp":true,"ando.nara.jp":true,"gose.nara.jp":true,"heguri.nara.jp":true,"higashiyoshino.nara.jp":true,"ikaruga.nara.jp":true,"ikoma.nara.jp":true,"kamikitayama.nara.jp":true,"kanmaki.nara.jp":true,"kashiba.nara.jp":true,"kashihara.nara.jp":true,"katsuragi.nara.jp":true,"kawai.nara.jp":true,"kawakami.nara.jp":true,"kawanishi.nara.jp":true,"koryo.nara.jp":true,"kurotaki.nara.jp":true,"mitsue.nara.jp":true,"miyake.nara.jp":true,"nara.nara.jp":true,"nosegawa.nara.jp":true,"oji.nara.jp":true,"ouda.nara.jp":true,"oyodo.nara.jp":true,"sakurai.nara.jp":true,"sango.nara.jp":true,"shimoichi.nara.jp":true,"shimokitayama.nara.jp":true,"shinjo.nara.jp":true,"soni.nara.jp":true,"takatori.nara.jp":true,"tawaramoto.nara.jp":true,"tenkawa.nara.jp":true,"tenri.nara.jp":true,"uda.nara.jp":true,"yamatokoriyama.nara.jp":true,"yamatotakada.nara.jp":true,"yamazoe.nara.jp":true,"yoshino.nara.jp":true,"aga.niigata.jp":true,"agano.niigata.jp":true,"gosen.niigata.jp":true,"itoigawa.niigata.jp":true,"izumozaki.niigata.jp":true,"joetsu.niigata.jp":true,"kamo.niigata.jp":true,"kariwa.niigata.jp":true,"kashiwazaki.niigata.jp":true,"minamiuonuma.niigata.jp":true,"mitsuke.niigata.jp":true,"muika.niigata.jp":true,"murakami.niigata.jp":true,"myoko.niigata.jp":true,"nagaoka.niigata.jp":true,"niigata.niigata.jp":true,"ojiya.niigata.jp":true,"omi.niigata.jp":true,"sado.niigata.jp":true,"sanjo.niigata.jp":true,"seiro.niigata.jp":true,"seirou.niigata.jp":true,"sekikawa.niigata.jp":true,"shibata.niigata.jp":true,"tagami.niigata.jp":true,"tainai.niigata.jp":true,"tochio.niigata.jp":true,"tokamachi.niigata.jp":true,"tsubame.niigata.jp":true,"tsunan.niigata.jp":true,"uonuma.niigata.jp":true,"yahiko.niigata.jp":true,"yoita.niigata.jp":true,"yuzawa.niigata.jp":true,"beppu.oita.jp":true,"bungoono.oita.jp":true,"bungotakada.oita.jp":true,"hasama.oita.jp":true,"hiji.oita.jp":true,"himeshima.oita.jp":true,"hita.oita.jp":true,"kamitsue.oita.jp":true,"kokonoe.oita.jp":true,"kuju.oita.jp":true,"kunisaki.oita.jp":true,"kusu.oita.jp":true,"oita.oita.jp":true,"saiki.oita.jp":true,"taketa.oita.jp":true,"tsukumi.oita.jp":true,"usa.oita.jp":true,"usuki.oita.jp":true,"yufu.oita.jp":true,"akaiwa.okayama.jp":true,"asakuchi.okayama.jp":true,"bizen.okayama.jp":true,"hayashima.okayama.jp":true,"ibara.okayama.jp":true,"kagamino.okayama.jp":true,"kasaoka.okayama.jp":true,"kibichuo.okayama.jp":true,"kumenan.okayama.jp":true,"kurashiki.okayama.jp":true,"maniwa.okayama.jp":true,"misaki.okayama.jp":true,"nagi.okayama.jp":true,"niimi.okayama.jp":true,"nishiawakura.okayama.jp":true,"okayama.okayama.jp":true,"satosho.okayama.jp":true,"setouchi.okayama.jp":true,"shinjo.okayama.jp":true,"shoo.okayama.jp":true,"soja.okayama.jp":true,"takahashi.okayama.jp":true,"tamano.okayama.jp":true,"tsuyama.okayama.jp":true,"wake.okayama.jp":true,"yakage.okayama.jp":true,"aguni.okinawa.jp":true,"ginowan.okinawa.jp":true,"ginoza.okinawa.jp":true,"gushikami.okinawa.jp":true,"haebaru.okinawa.jp":true,"higashi.okinawa.jp":true,"hirara.okinawa.jp":true,"iheya.okinawa.jp":true,"ishigaki.okinawa.jp":true,"ishikawa.okinawa.jp":true,"itoman.okinawa.jp":true,"izena.okinawa.jp":true,"kadena.okinawa.jp":true,"kin.okinawa.jp":true,"kitadaito.okinawa.jp":true,"kitanakagusuku.okinawa.jp":true,"kumejima.okinawa.jp":true,"kunigami.okinawa.jp":true,"minamidaito.okinawa.jp":true,"motobu.okinawa.jp":true,"nago.okinawa.jp":true,"naha.okinawa.jp":true,"nakagusuku.okinawa.jp":true,"nakijin.okinawa.jp":true,"nanjo.okinawa.jp":true,"nishihara.okinawa.jp":true,"ogimi.okinawa.jp":true,"okinawa.okinawa.jp":true,"onna.okinawa.jp":true,"shimoji.okinawa.jp":true,"taketomi.okinawa.jp":true,"tarama.okinawa.jp":true,"tokashiki.okinawa.jp":true,"tomigusuku.okinawa.jp":true,"tonaki.okinawa.jp":true,"urasoe.okinawa.jp":true,"uruma.okinawa.jp":true,"yaese.okinawa.jp":true,"yomitan.okinawa.jp":true,"yonabaru.okinawa.jp":true,"yonaguni.okinawa.jp":true,"zamami.okinawa.jp":true,"abeno.osaka.jp":true,"chihayaakasaka.osaka.jp":true,"chuo.osaka.jp":true,"daito.osaka.jp":true,"fujiidera.osaka.jp":true,"habikino.osaka.jp":true,"hannan.osaka.jp":true,"higashiosaka.osaka.jp":true,"higashisumiyoshi.osaka.jp":true,"higashiyodogawa.osaka.jp":true,"hirakata.osaka.jp":true,"ibaraki.osaka.jp":true,"ikeda.osaka.jp":true,"izumi.osaka.jp":true,"izumiotsu.osaka.jp":true,"izumisano.osaka.jp":true,"kadoma.osaka.jp":true,"kaizuka.osaka.jp":true,"kanan.osaka.jp":true,"kashiwara.osaka.jp":true,"katano.osaka.jp":true,"kawachinagano.osaka.jp":true,"kishiwada.osaka.jp":true,"kita.osaka.jp":true,"kumatori.osaka.jp":true,"matsubara.osaka.jp":true,"minato.osaka.jp":true,"minoh.osaka.jp":true,"misaki.osaka.jp":true,"moriguchi.osaka.jp":true,"neyagawa.osaka.jp":true,"nishi.osaka.jp":true,"nose.osaka.jp":true,"osakasayama.osaka.jp":true,"sakai.osaka.jp":true,"sayama.osaka.jp":true,"sennan.osaka.jp":true,"settsu.osaka.jp":true,"shijonawate.osaka.jp":true,"shimamoto.osaka.jp":true,"suita.osaka.jp":true,"tadaoka.osaka.jp":true,"taishi.osaka.jp":true,"tajiri.osaka.jp":true,"takaishi.osaka.jp":true,"takatsuki.osaka.jp":true,"tondabayashi.osaka.jp":true,"toyonaka.osaka.jp":true,"toyono.osaka.jp":true,"yao.osaka.jp":true,"ariake.saga.jp":true,"arita.saga.jp":true,"fukudomi.saga.jp":true,"genkai.saga.jp":true,"hamatama.saga.jp":true,"hizen.saga.jp":true,"imari.saga.jp":true,"kamimine.saga.jp":true,"kanzaki.saga.jp":true,"karatsu.saga.jp":true,"kashima.saga.jp":true,"kitagata.saga.jp":true,"kitahata.saga.jp":true,"kiyama.saga.jp":true,"kouhoku.saga.jp":true,"kyuragi.saga.jp":true,"nishiarita.saga.jp":true,"ogi.saga.jp":true,"omachi.saga.jp":true,"ouchi.saga.jp":true,"saga.saga.jp":true,"shiroishi.saga.jp":true,"taku.saga.jp":true,"tara.saga.jp":true,"tosu.saga.jp":true,"yoshinogari.saga.jp":true,"arakawa.saitama.jp":true,"asaka.saitama.jp":true,"chichibu.saitama.jp":true,"fujimi.saitama.jp":true,"fujimino.saitama.jp":true,"fukaya.saitama.jp":true,"hanno.saitama.jp":true,"hanyu.saitama.jp":true,"hasuda.saitama.jp":true,"hatogaya.saitama.jp":true,"hatoyama.saitama.jp":true,"hidaka.saitama.jp":true,"higashichichibu.saitama.jp":true,"higashimatsuyama.saitama.jp":true,"honjo.saitama.jp":true,"ina.saitama.jp":true,"iruma.saitama.jp":true,"iwatsuki.saitama.jp":true,"kamiizumi.saitama.jp":true,"kamikawa.saitama.jp":true,"kamisato.saitama.jp":true,"kasukabe.saitama.jp":true,"kawagoe.saitama.jp":true,"kawaguchi.saitama.jp":true,"kawajima.saitama.jp":true,"kazo.saitama.jp":true,"kitamoto.saitama.jp":true,"koshigaya.saitama.jp":true,"kounosu.saitama.jp":true,"kuki.saitama.jp":true,"kumagaya.saitama.jp":true,"matsubushi.saitama.jp":true,"minano.saitama.jp":true,"misato.saitama.jp":true,"miyashiro.saitama.jp":true,"miyoshi.saitama.jp":true,"moroyama.saitama.jp":true,"nagatoro.saitama.jp":true,"namegawa.saitama.jp":true,"niiza.saitama.jp":true,"ogano.saitama.jp":true,"ogawa.saitama.jp":true,"ogose.saitama.jp":true,"okegawa.saitama.jp":true,"omiya.saitama.jp":true,"otaki.saitama.jp":true,"ranzan.saitama.jp":true,"ryokami.saitama.jp":true,"saitama.saitama.jp":true,"sakado.saitama.jp":true,"satte.saitama.jp":true,"sayama.saitama.jp":true,"shiki.saitama.jp":true,"shiraoka.saitama.jp":true,"soka.saitama.jp":true,"sugito.saitama.jp":true,"toda.saitama.jp":true,"tokigawa.saitama.jp":true,"tokorozawa.saitama.jp":true,"tsurugashima.saitama.jp":true,"urawa.saitama.jp":true,"warabi.saitama.jp":true,"yashio.saitama.jp":true,"yokoze.saitama.jp":true,"yono.saitama.jp":true,"yorii.saitama.jp":true,"yoshida.saitama.jp":true,"yoshikawa.saitama.jp":true,"yoshimi.saitama.jp":true,"aisho.shiga.jp":true,"gamo.shiga.jp":true,"higashiomi.shiga.jp":true,"hikone.shiga.jp":true,"koka.shiga.jp":true,"konan.shiga.jp":true,"kosei.shiga.jp":true,"koto.shiga.jp":true,"kusatsu.shiga.jp":true,"maibara.shiga.jp":true,"moriyama.shiga.jp":true,"nagahama.shiga.jp":true,"nishiazai.shiga.jp":true,"notogawa.shiga.jp":true,"omihachiman.shiga.jp":true,"otsu.shiga.jp":true,"ritto.shiga.jp":true,"ryuoh.shiga.jp":true,"takashima.shiga.jp":true,"takatsuki.shiga.jp":true,"torahime.shiga.jp":true,"toyosato.shiga.jp":true,"yasu.shiga.jp":true,"akagi.shimane.jp":true,"ama.shimane.jp":true,"gotsu.shimane.jp":true,"hamada.shimane.jp":true,"higashiizumo.shimane.jp":true,"hikawa.shimane.jp":true,"hikimi.shimane.jp":true,"izumo.shimane.jp":true,"kakinoki.shimane.jp":true,"masuda.shimane.jp":true,"matsue.shimane.jp":true,"misato.shimane.jp":true,"nishinoshima.shimane.jp":true,"ohda.shimane.jp":true,"okinoshima.shimane.jp":true,"okuizumo.shimane.jp":true,"shimane.shimane.jp":true,"tamayu.shimane.jp":true,"tsuwano.shimane.jp":true,"unnan.shimane.jp":true,"yakumo.shimane.jp":true,"yasugi.shimane.jp":true,"yatsuka.shimane.jp":true,"arai.shizuoka.jp":true,"atami.shizuoka.jp":true,"fuji.shizuoka.jp":true,"fujieda.shizuoka.jp":true,"fujikawa.shizuoka.jp":true,"fujinomiya.shizuoka.jp":true,"fukuroi.shizuoka.jp":true,"gotemba.shizuoka.jp":true,"haibara.shizuoka.jp":true,"hamamatsu.shizuoka.jp":true,"higashiizu.shizuoka.jp":true,"ito.shizuoka.jp":true,"iwata.shizuoka.jp":true,"izu.shizuoka.jp":true,"izunokuni.shizuoka.jp":true,"kakegawa.shizuoka.jp":true,"kannami.shizuoka.jp":true,"kawanehon.shizuoka.jp":true,"kawazu.shizuoka.jp":true,"kikugawa.shizuoka.jp":true,"kosai.shizuoka.jp":true,"makinohara.shizuoka.jp":true,"matsuzaki.shizuoka.jp":true,"minamiizu.shizuoka.jp":true,"mishima.shizuoka.jp":true,"morimachi.shizuoka.jp":true,"nishiizu.shizuoka.jp":true,"numazu.shizuoka.jp":true,"omaezaki.shizuoka.jp":true,"shimada.shizuoka.jp":true,"shimizu.shizuoka.jp":true,"shimoda.shizuoka.jp":true,"shizuoka.shizuoka.jp":true,"susono.shizuoka.jp":true,"yaizu.shizuoka.jp":true,"yoshida.shizuoka.jp":true,"ashikaga.tochigi.jp":true,"bato.tochigi.jp":true,"haga.tochigi.jp":true,"ichikai.tochigi.jp":true,"iwafune.tochigi.jp":true,"kaminokawa.tochigi.jp":true,"kanuma.tochigi.jp":true,"karasuyama.tochigi.jp":true,"kuroiso.tochigi.jp":true,"mashiko.tochigi.jp":true,"mibu.tochigi.jp":true,"moka.tochigi.jp":true,"motegi.tochigi.jp":true,"nasu.tochigi.jp":true,"nasushiobara.tochigi.jp":true,"nikko.tochigi.jp":true,"nishikata.tochigi.jp":true,"nogi.tochigi.jp":true,"ohira.tochigi.jp":true,"ohtawara.tochigi.jp":true,"oyama.tochigi.jp":true,"sakura.tochigi.jp":true,"sano.tochigi.jp":true,"shimotsuke.tochigi.jp":true,"shioya.tochigi.jp":true,"takanezawa.tochigi.jp":true,"tochigi.tochigi.jp":true,"tsuga.tochigi.jp":true,"ujiie.tochigi.jp":true,"utsunomiya.tochigi.jp":true,"yaita.tochigi.jp":true,"aizumi.tokushima.jp":true,"anan.tokushima.jp":true,"ichiba.tokushima.jp":true,"itano.tokushima.jp":true,"kainan.tokushima.jp":true,"komatsushima.tokushima.jp":true,"matsushige.tokushima.jp":true,"mima.tokushima.jp":true,"minami.tokushima.jp":true,"miyoshi.tokushima.jp":true,"mugi.tokushima.jp":true,"nakagawa.tokushima.jp":true,"naruto.tokushima.jp":true,"sanagochi.tokushima.jp":true,"shishikui.tokushima.jp":true,"tokushima.tokushima.jp":true,"wajiki.tokushima.jp":true,"adachi.tokyo.jp":true,"akiruno.tokyo.jp":true,"akishima.tokyo.jp":true,"aogashima.tokyo.jp":true,"arakawa.tokyo.jp":true,"bunkyo.tokyo.jp":true,"chiyoda.tokyo.jp":true,"chofu.tokyo.jp":true,"chuo.tokyo.jp":true,"edogawa.tokyo.jp":true,"fuchu.tokyo.jp":true,"fussa.tokyo.jp":true,"hachijo.tokyo.jp":true,"hachioji.tokyo.jp":true,"hamura.tokyo.jp":true,"higashikurume.tokyo.jp":true,"higashimurayama.tokyo.jp":true,"higashiyamato.tokyo.jp":true,"hino.tokyo.jp":true,"hinode.tokyo.jp":true,"hinohara.tokyo.jp":true,"inagi.tokyo.jp":true,"itabashi.tokyo.jp":true,"katsushika.tokyo.jp":true,"kita.tokyo.jp":true,"kiyose.tokyo.jp":true,"kodaira.tokyo.jp":true,"koganei.tokyo.jp":true,"kokubunji.tokyo.jp":true,"komae.tokyo.jp":true,"koto.tokyo.jp":true,"kouzushima.tokyo.jp":true,"kunitachi.tokyo.jp":true,"machida.tokyo.jp":true,"meguro.tokyo.jp":true,"minato.tokyo.jp":true,"mitaka.tokyo.jp":true,"mizuho.tokyo.jp":true,"musashimurayama.tokyo.jp":true,"musashino.tokyo.jp":true,"nakano.tokyo.jp":true,"nerima.tokyo.jp":true,"ogasawara.tokyo.jp":true,"okutama.tokyo.jp":true,"ome.tokyo.jp":true,"oshima.tokyo.jp":true,"ota.tokyo.jp":true,"setagaya.tokyo.jp":true,"shibuya.tokyo.jp":true,"shinagawa.tokyo.jp":true,"shinjuku.tokyo.jp":true,"suginami.tokyo.jp":true,"sumida.tokyo.jp":true,"tachikawa.tokyo.jp":true,"taito.tokyo.jp":true,"tama.tokyo.jp":true,"toshima.tokyo.jp":true,"chizu.tottori.jp":true,"hino.tottori.jp":true,"kawahara.tottori.jp":true,"koge.tottori.jp":true,"kotoura.tottori.jp":true,"misasa.tottori.jp":true,"nanbu.tottori.jp":true,"nichinan.tottori.jp":true,"sakaiminato.tottori.jp":true,"tottori.tottori.jp":true,"wakasa.tottori.jp":true,"yazu.tottori.jp":true,"yonago.tottori.jp":true,"asahi.toyama.jp":true,"fuchu.toyama.jp":true,"fukumitsu.toyama.jp":true,"funahashi.toyama.jp":true,"himi.toyama.jp":true,"imizu.toyama.jp":true,"inami.toyama.jp":true,"johana.toyama.jp":true,"kamiichi.toyama.jp":true,"kurobe.toyama.jp":true,"nakaniikawa.toyama.jp":true,"namerikawa.toyama.jp":true,"nanto.toyama.jp":true,"nyuzen.toyama.jp":true,"oyabe.toyama.jp":true,"taira.toyama.jp":true,"takaoka.toyama.jp":true,"tateyama.toyama.jp":true,"toga.toyama.jp":true,"tonami.toyama.jp":true,"toyama.toyama.jp":true,"unazuki.toyama.jp":true,"uozu.toyama.jp":true,"yamada.toyama.jp":true,"arida.wakayama.jp":true,"aridagawa.wakayama.jp":true,"gobo.wakayama.jp":true,"hashimoto.wakayama.jp":true,"hidaka.wakayama.jp":true,"hirogawa.wakayama.jp":true,"inami.wakayama.jp":true,"iwade.wakayama.jp":true,"kainan.wakayama.jp":true,"kamitonda.wakayama.jp":true,"katsuragi.wakayama.jp":true,"kimino.wakayama.jp":true,"kinokawa.wakayama.jp":true,"kitayama.wakayama.jp":true,"koya.wakayama.jp":true,"koza.wakayama.jp":true,"kozagawa.wakayama.jp":true,"kudoyama.wakayama.jp":true,"kushimoto.wakayama.jp":true,"mihama.wakayama.jp":true,"misato.wakayama.jp":true,"nachikatsuura.wakayama.jp":true,"shingu.wakayama.jp":true,"shirahama.wakayama.jp":true,"taiji.wakayama.jp":true,"tanabe.wakayama.jp":true,"wakayama.wakayama.jp":true,"yuasa.wakayama.jp":true,"yura.wakayama.jp":true,"asahi.yamagata.jp":true,"funagata.yamagata.jp":true,"higashine.yamagata.jp":true,"iide.yamagata.jp":true,"kahoku.yamagata.jp":true,"kaminoyama.yamagata.jp":true,"kaneyama.yamagata.jp":true,"kawanishi.yamagata.jp":true,"mamurogawa.yamagata.jp":true,"mikawa.yamagata.jp":true,"murayama.yamagata.jp":true,"nagai.yamagata.jp":true,"nakayama.yamagata.jp":true,"nanyo.yamagata.jp":true,"nishikawa.yamagata.jp":true,"obanazawa.yamagata.jp":true,"oe.yamagata.jp":true,"oguni.yamagata.jp":true,"ohkura.yamagata.jp":true,"oishida.yamagata.jp":true,"sagae.yamagata.jp":true,"sakata.yamagata.jp":true,"sakegawa.yamagata.jp":true,"shinjo.yamagata.jp":true,"shirataka.yamagata.jp":true,"shonai.yamagata.jp":true,"takahata.yamagata.jp":true,"tendo.yamagata.jp":true,"tozawa.yamagata.jp":true,"tsuruoka.yamagata.jp":true,"yamagata.yamagata.jp":true,"yamanobe.yamagata.jp":true,"yonezawa.yamagata.jp":true,"yuza.yamagata.jp":true,"abu.yamaguchi.jp":true,"hagi.yamaguchi.jp":true,"hikari.yamaguchi.jp":true,"hofu.yamaguchi.jp":true,"iwakuni.yamaguchi.jp":true,"kudamatsu.yamaguchi.jp":true,"mitou.yamaguchi.jp":true,"nagato.yamaguchi.jp":true,"oshima.yamaguchi.jp":true,"shimonoseki.yamaguchi.jp":true,"shunan.yamaguchi.jp":true,"tabuse.yamaguchi.jp":true,"tokuyama.yamaguchi.jp":true,"toyota.yamaguchi.jp":true,"ube.yamaguchi.jp":true,"yuu.yamaguchi.jp":true,"chuo.yamanashi.jp":true,"doshi.yamanashi.jp":true,"fuefuki.yamanashi.jp":true,"fujikawa.yamanashi.jp":true,"fujikawaguchiko.yamanashi.jp":true,"fujiyoshida.yamanashi.jp":true,"hayakawa.yamanashi.jp":true,"hokuto.yamanashi.jp":true,"ichikawamisato.yamanashi.jp":true,"kai.yamanashi.jp":true,"kofu.yamanashi.jp":true,"koshu.yamanashi.jp":true,"kosuge.yamanashi.jp":true,"minami-alps.yamanashi.jp":true,"minobu.yamanashi.jp":true,"nakamichi.yamanashi.jp":true,"nanbu.yamanashi.jp":true,"narusawa.yamanashi.jp":true,"nirasaki.yamanashi.jp":true,"nishikatsura.yamanashi.jp":true,"oshino.yamanashi.jp":true,"otsuki.yamanashi.jp":true,"showa.yamanashi.jp":true,"tabayama.yamanashi.jp":true,"tsuru.yamanashi.jp":true,"uenohara.yamanashi.jp":true,"yamanakako.yamanashi.jp":true,"yamanashi.yamanashi.jp":true,"*.ke":true,"kg":true,"org.kg":true,"net.kg":true,"com.kg":true,"edu.kg":true,"gov.kg":true,"mil.kg":true,"*.kh":true,"ki":true,"edu.ki":true,"biz.ki":true,"net.ki":true,"org.ki":true,"gov.ki":true,"info.ki":true,"com.ki":true,"km":true,"org.km":true,"nom.km":true,"gov.km":true,"prd.km":true,"tm.km":true,"edu.km":true,"mil.km":true,"ass.km":true,"com.km":true,"coop.km":true,"asso.km":true,"presse.km":true,"medecin.km":true,"notaires.km":true,"pharmaciens.km":true,"veterinaire.km":true,"gouv.km":true,"kn":true,"net.kn":true,"org.kn":true,"edu.kn":true,"gov.kn":true,"kp":true,"com.kp":true,"edu.kp":true,"gov.kp":true,"org.kp":true,"rep.kp":true,"tra.kp":true,"kr":true,"ac.kr":true,"co.kr":true,"es.kr":true,"go.kr":true,"hs.kr":true,"kg.kr":true,"mil.kr":true,"ms.kr":true,"ne.kr":true,"or.kr":true,"pe.kr":true,"re.kr":true,"sc.kr":true,"busan.kr":true,"chungbuk.kr":true,"chungnam.kr":true,"daegu.kr":true,"daejeon.kr":true,"gangwon.kr":true,"gwangju.kr":true,"gyeongbuk.kr":true,"gyeonggi.kr":true,"gyeongnam.kr":true,"incheon.kr":true,"jeju.kr":true,"jeonbuk.kr":true,"jeonnam.kr":true,"seoul.kr":true,"ulsan.kr":true,"*.kw":true,"ky":true,"edu.ky":true,"gov.ky":true,"com.ky":true,"org.ky":true,"net.ky":true,"kz":true,"org.kz":true,"edu.kz":true,"net.kz":true,"gov.kz":true,"mil.kz":true,"com.kz":true,"la":true,"int.la":true,"net.la":true,"info.la":true,"edu.la":true,"gov.la":true,"per.la":true,"com.la":true,"org.la":true,"lb":true,"com.lb":true,"edu.lb":true,"gov.lb":true,"net.lb":true,"org.lb":true,"lc":true,"com.lc":true,"net.lc":true,"co.lc":true,"org.lc":true,"edu.lc":true,"gov.lc":true,"li":true,"lk":true,"gov.lk":true,"sch.lk":true,"net.lk":true,"int.lk":true,"com.lk":true,"org.lk":true,"edu.lk":true,"ngo.lk":true,"soc.lk":true,"web.lk":true,"ltd.lk":true,"assn.lk":true,"grp.lk":true,"hotel.lk":true,"ac.lk":true,"lr":true,"com.lr":true,"edu.lr":true,"gov.lr":true,"org.lr":true,"net.lr":true,"ls":true,"co.ls":true,"org.ls":true,"lt":true,"gov.lt":true,"lu":true,"lv":true,"com.lv":true,"edu.lv":true,"gov.lv":true,"org.lv":true,"mil.lv":true,"id.lv":true,"net.lv":true,"asn.lv":true,"conf.lv":true,"ly":true,"com.ly":true,"net.ly":true,"gov.ly":true,"plc.ly":true,"edu.ly":true,"sch.ly":true,"med.ly":true,"org.ly":true,"id.ly":true,"ma":true,"co.ma":true,"net.ma":true,"gov.ma":true,"org.ma":true,"ac.ma":true,"press.ma":true,"mc":true,"tm.mc":true,"asso.mc":true,"md":true,"me":true,"co.me":true,"net.me":true,"org.me":true,"edu.me":true,"ac.me":true,"gov.me":true,"its.me":true,"priv.me":true,"mg":true,"org.mg":true,"nom.mg":true,"gov.mg":true,"prd.mg":true,"tm.mg":true,"edu.mg":true,"mil.mg":true,"com.mg":true,"co.mg":true,"mh":true,"mil":true,"mk":true,"com.mk":true,"org.mk":true,"net.mk":true,"edu.mk":true,"gov.mk":true,"inf.mk":true,"name.mk":true,"ml":true,"com.ml":true,"edu.ml":true,"gouv.ml":true,"gov.ml":true,"net.ml":true,"org.ml":true,"presse.ml":true,"*.mm":true,"mn":true,"gov.mn":true,"edu.mn":true,"org.mn":true,"mo":true,"com.mo":true,"net.mo":true,"org.mo":true,"edu.mo":true,"gov.mo":true,"mobi":true,"mp":true,"mq":true,"mr":true,"gov.mr":true,"ms":true,"com.ms":true,"edu.ms":true,"gov.ms":true,"net.ms":true,"org.ms":true,"mt":true,"com.mt":true,"edu.mt":true,"net.mt":true,"org.mt":true,"mu":true,"com.mu":true,"net.mu":true,"org.mu":true,"gov.mu":true,"ac.mu":true,"co.mu":true,"or.mu":true,"museum":true,"academy.museum":true,"agriculture.museum":true,"air.museum":true,"airguard.museum":true,"alabama.museum":true,"alaska.museum":true,"amber.museum":true,"ambulance.museum":true,"american.museum":true,"americana.museum":true,"americanantiques.museum":true,"americanart.museum":true,"amsterdam.museum":true,"and.museum":true,"annefrank.museum":true,"anthro.museum":true,"anthropology.museum":true,"antiques.museum":true,"aquarium.museum":true,"arboretum.museum":true,"archaeological.museum":true,"archaeology.museum":true,"architecture.museum":true,"art.museum":true,"artanddesign.museum":true,"artcenter.museum":true,"artdeco.museum":true,"arteducation.museum":true,"artgallery.museum":true,"arts.museum":true,"artsandcrafts.museum":true,"asmatart.museum":true,"assassination.museum":true,"assisi.museum":true,"association.museum":true,"astronomy.museum":true,"atlanta.museum":true,"austin.museum":true,"australia.museum":true,"automotive.museum":true,"aviation.museum":true,"axis.museum":true,"badajoz.museum":true,"baghdad.museum":true,"bahn.museum":true,"bale.museum":true,"baltimore.museum":true,"barcelona.museum":true,"baseball.museum":true,"basel.museum":true,"baths.museum":true,"bauern.museum":true,"beauxarts.museum":true,"beeldengeluid.museum":true,"bellevue.museum":true,"bergbau.museum":true,"berkeley.museum":true,"berlin.museum":true,"bern.museum":true,"bible.museum":true,"bilbao.museum":true,"bill.museum":true,"birdart.museum":true,"birthplace.museum":true,"bonn.museum":true,"boston.museum":true,"botanical.museum":true,"botanicalgarden.museum":true,"botanicgarden.museum":true,"botany.museum":true,"brandywinevalley.museum":true,"brasil.museum":true,"bristol.museum":true,"british.museum":true,"britishcolumbia.museum":true,"broadcast.museum":true,"brunel.museum":true,"brussel.museum":true,"brussels.museum":true,"bruxelles.museum":true,"building.museum":true,"burghof.museum":true,"bus.museum":true,"bushey.museum":true,"cadaques.museum":true,"california.museum":true,"cambridge.museum":true,"can.museum":true,"canada.museum":true,"capebreton.museum":true,"carrier.museum":true,"cartoonart.museum":true,"casadelamoneda.museum":true,"castle.museum":true,"castres.museum":true,"celtic.museum":true,"center.museum":true,"chattanooga.museum":true,"cheltenham.museum":true,"chesapeakebay.museum":true,"chicago.museum":true,"children.museum":true,"childrens.museum":true,"childrensgarden.museum":true,"chiropractic.museum":true,"chocolate.museum":true,"christiansburg.museum":true,"cincinnati.museum":true,"cinema.museum":true,"circus.museum":true,"civilisation.museum":true,"civilization.museum":true,"civilwar.museum":true,"clinton.museum":true,"clock.museum":true,"coal.museum":true,"coastaldefence.museum":true,"cody.museum":true,"coldwar.museum":true,"collection.museum":true,"colonialwilliamsburg.museum":true,"coloradoplateau.museum":true,"columbia.museum":true,"columbus.museum":true,"communication.museum":true,"communications.museum":true,"community.museum":true,"computer.museum":true,"computerhistory.museum":true,"xn--comunicaes-v6a2o.museum":true,"contemporary.museum":true,"contemporaryart.museum":true,"convent.museum":true,"copenhagen.museum":true,"corporation.museum":true,"xn--correios-e-telecomunicaes-ghc29a.museum":true,"corvette.museum":true,"costume.museum":true,"countryestate.museum":true,"county.museum":true,"crafts.museum":true,"cranbrook.museum":true,"creation.museum":true,"cultural.museum":true,"culturalcenter.museum":true,"culture.museum":true,"cyber.museum":true,"cymru.museum":true,"dali.museum":true,"dallas.museum":true,"database.museum":true,"ddr.museum":true,"decorativearts.museum":true,"delaware.museum":true,"delmenhorst.museum":true,"denmark.museum":true,"depot.museum":true,"design.museum":true,"detroit.museum":true,"dinosaur.museum":true,"discovery.museum":true,"dolls.museum":true,"donostia.museum":true,"durham.museum":true,"eastafrica.museum":true,"eastcoast.museum":true,"education.museum":true,"educational.museum":true,"egyptian.museum":true,"eisenbahn.museum":true,"elburg.museum":true,"elvendrell.museum":true,"embroidery.museum":true,"encyclopedic.museum":true,"england.museum":true,"entomology.museum":true,"environment.museum":true,"environmentalconservation.museum":true,"epilepsy.museum":true,"essex.museum":true,"estate.museum":true,"ethnology.museum":true,"exeter.museum":true,"exhibition.museum":true,"family.museum":true,"farm.museum":true,"farmequipment.museum":true,"farmers.museum":true,"farmstead.museum":true,"field.museum":true,"figueres.museum":true,"filatelia.museum":true,"film.museum":true,"fineart.museum":true,"finearts.museum":true,"finland.museum":true,"flanders.museum":true,"florida.museum":true,"force.museum":true,"fortmissoula.museum":true,"fortworth.museum":true,"foundation.museum":true,"francaise.museum":true,"frankfurt.museum":true,"franziskaner.museum":true,"freemasonry.museum":true,"freiburg.museum":true,"fribourg.museum":true,"frog.museum":true,"fundacio.museum":true,"furniture.museum":true,"gallery.museum":true,"garden.museum":true,"gateway.museum":true,"geelvinck.museum":true,"gemological.museum":true,"geology.museum":true,"georgia.museum":true,"giessen.museum":true,"glas.museum":true,"glass.museum":true,"gorge.museum":true,"grandrapids.museum":true,"graz.museum":true,"guernsey.museum":true,"halloffame.museum":true,"hamburg.museum":true,"handson.museum":true,"harvestcelebration.museum":true,"hawaii.museum":true,"health.museum":true,"heimatunduhren.museum":true,"hellas.museum":true,"helsinki.museum":true,"hembygdsforbund.museum":true,"heritage.museum":true,"histoire.museum":true,"historical.museum":true,"historicalsociety.museum":true,"historichouses.museum":true,"historisch.museum":true,"historisches.museum":true,"history.museum":true,"historyofscience.museum":true,"horology.museum":true,"house.museum":true,"humanities.museum":true,"illustration.museum":true,"imageandsound.museum":true,"indian.museum":true,"indiana.museum":true,"indianapolis.museum":true,"indianmarket.museum":true,"intelligence.museum":true,"interactive.museum":true,"iraq.museum":true,"iron.museum":true,"isleofman.museum":true,"jamison.museum":true,"jefferson.museum":true,"jerusalem.museum":true,"jewelry.museum":true,"jewish.museum":true,"jewishart.museum":true,"jfk.museum":true,"journalism.museum":true,"judaica.museum":true,"judygarland.museum":true,"juedisches.museum":true,"juif.museum":true,"karate.museum":true,"karikatur.museum":true,"kids.museum":true,"koebenhavn.museum":true,"koeln.museum":true,"kunst.museum":true,"kunstsammlung.museum":true,"kunstunddesign.museum":true,"labor.museum":true,"labour.museum":true,"lajolla.museum":true,"lancashire.museum":true,"landes.museum":true,"lans.museum":true,"xn--lns-qla.museum":true,"larsson.museum":true,"lewismiller.museum":true,"lincoln.museum":true,"linz.museum":true,"living.museum":true,"livinghistory.museum":true,"localhistory.museum":true,"london.museum":true,"losangeles.museum":true,"louvre.museum":true,"loyalist.museum":true,"lucerne.museum":true,"luxembourg.museum":true,"luzern.museum":true,"mad.museum":true,"madrid.museum":true,"mallorca.museum":true,"manchester.museum":true,"mansion.museum":true,"mansions.museum":true,"manx.museum":true,"marburg.museum":true,"maritime.museum":true,"maritimo.museum":true,"maryland.museum":true,"marylhurst.museum":true,"media.museum":true,"medical.museum":true,"medizinhistorisches.museum":true,"meeres.museum":true,"memorial.museum":true,"mesaverde.museum":true,"michigan.museum":true,"midatlantic.museum":true,"military.museum":true,"mill.museum":true,"miners.museum":true,"mining.museum":true,"minnesota.museum":true,"missile.museum":true,"missoula.museum":true,"modern.museum":true,"moma.museum":true,"money.museum":true,"monmouth.museum":true,"monticello.museum":true,"montreal.museum":true,"moscow.museum":true,"motorcycle.museum":true,"muenchen.museum":true,"muenster.museum":true,"mulhouse.museum":true,"muncie.museum":true,"museet.museum":true,"museumcenter.museum":true,"museumvereniging.museum":true,"music.museum":true,"national.museum":true,"nationalfirearms.museum":true,"nationalheritage.museum":true,"nativeamerican.museum":true,"naturalhistory.museum":true,"naturalhistorymuseum.museum":true,"naturalsciences.museum":true,"nature.museum":true,"naturhistorisches.museum":true,"natuurwetenschappen.museum":true,"naumburg.museum":true,"naval.museum":true,"nebraska.museum":true,"neues.museum":true,"newhampshire.museum":true,"newjersey.museum":true,"newmexico.museum":true,"newport.museum":true,"newspaper.museum":true,"newyork.museum":true,"niepce.museum":true,"norfolk.museum":true,"north.museum":true,"nrw.museum":true,"nuernberg.museum":true,"nuremberg.museum":true,"nyc.museum":true,"nyny.museum":true,"oceanographic.museum":true,"oceanographique.museum":true,"omaha.museum":true,"online.museum":true,"ontario.museum":true,"openair.museum":true,"oregon.museum":true,"oregontrail.museum":true,"otago.museum":true,"oxford.museum":true,"pacific.museum":true,"paderborn.museum":true,"palace.museum":true,"paleo.museum":true,"palmsprings.museum":true,"panama.museum":true,"paris.museum":true,"pasadena.museum":true,"pharmacy.museum":true,"philadelphia.museum":true,"philadelphiaarea.museum":true,"philately.museum":true,"phoenix.museum":true,"photography.museum":true,"pilots.museum":true,"pittsburgh.museum":true,"planetarium.museum":true,"plantation.museum":true,"plants.museum":true,"plaza.museum":true,"portal.museum":true,"portland.museum":true,"portlligat.museum":true,"posts-and-telecommunications.museum":true,"preservation.museum":true,"presidio.museum":true,"press.museum":true,"project.museum":true,"public.museum":true,"pubol.museum":true,"quebec.museum":true,"railroad.museum":true,"railway.museum":true,"research.museum":true,"resistance.museum":true,"riodejaneiro.museum":true,"rochester.museum":true,"rockart.museum":true,"roma.museum":true,"russia.museum":true,"saintlouis.museum":true,"salem.museum":true,"salvadordali.museum":true,"salzburg.museum":true,"sandiego.museum":true,"sanfrancisco.museum":true,"santabarbara.museum":true,"santacruz.museum":true,"santafe.museum":true,"saskatchewan.museum":true,"satx.museum":true,"savannahga.museum":true,"schlesisches.museum":true,"schoenbrunn.museum":true,"schokoladen.museum":true,"school.museum":true,"schweiz.museum":true,"science.museum":true,"scienceandhistory.museum":true,"scienceandindustry.museum":true,"sciencecenter.museum":true,"sciencecenters.museum":true,"science-fiction.museum":true,"sciencehistory.museum":true,"sciences.museum":true,"sciencesnaturelles.museum":true,"scotland.museum":true,"seaport.museum":true,"settlement.museum":true,"settlers.museum":true,"shell.museum":true,"sherbrooke.museum":true,"sibenik.museum":true,"silk.museum":true,"ski.museum":true,"skole.museum":true,"society.museum":true,"sologne.museum":true,"soundandvision.museum":true,"southcarolina.museum":true,"southwest.museum":true,"space.museum":true,"spy.museum":true,"square.museum":true,"stadt.museum":true,"stalbans.museum":true,"starnberg.museum":true,"state.museum":true,"stateofdelaware.museum":true,"station.museum":true,"steam.museum":true,"steiermark.museum":true,"stjohn.museum":true,"stockholm.museum":true,"stpetersburg.museum":true,"stuttgart.museum":true,"suisse.museum":true,"surgeonshall.museum":true,"surrey.museum":true,"svizzera.museum":true,"sweden.museum":true,"sydney.museum":true,"tank.museum":true,"tcm.museum":true,"technology.museum":true,"telekommunikation.museum":true,"television.museum":true,"texas.museum":true,"textile.museum":true,"theater.museum":true,"time.museum":true,"timekeeping.museum":true,"topology.museum":true,"torino.museum":true,"touch.museum":true,"town.museum":true,"transport.museum":true,"tree.museum":true,"trolley.museum":true,"trust.museum":true,"trustee.museum":true,"uhren.museum":true,"ulm.museum":true,"undersea.museum":true,"university.museum":true,"usa.museum":true,"usantiques.museum":true,"usarts.museum":true,"uscountryestate.museum":true,"usculture.museum":true,"usdecorativearts.museum":true,"usgarden.museum":true,"ushistory.museum":true,"ushuaia.museum":true,"uslivinghistory.museum":true,"utah.museum":true,"uvic.museum":true,"valley.museum":true,"vantaa.museum":true,"versailles.museum":true,"viking.museum":true,"village.museum":true,"virginia.museum":true,"virtual.museum":true,"virtuel.museum":true,"vlaanderen.museum":true,"volkenkunde.museum":true,"wales.museum":true,"wallonie.museum":true,"war.museum":true,"washingtondc.museum":true,"watchandclock.museum":true,"watch-and-clock.museum":true,"western.museum":true,"westfalen.museum":true,"whaling.museum":true,"wildlife.museum":true,"williamsburg.museum":true,"windmill.museum":true,"workshop.museum":true,"york.museum":true,"yorkshire.museum":true,"yosemite.museum":true,"youth.museum":true,"zoological.museum":true,"zoology.museum":true,"xn--9dbhblg6di.museum":true,"xn--h1aegh.museum":true,"mv":true,"aero.mv":true,"biz.mv":true,"com.mv":true,"coop.mv":true,"edu.mv":true,"gov.mv":true,"info.mv":true,"int.mv":true,"mil.mv":true,"museum.mv":true,"name.mv":true,"net.mv":true,"org.mv":true,"pro.mv":true,"mw":true,"ac.mw":true,"biz.mw":true,"co.mw":true,"com.mw":true,"coop.mw":true,"edu.mw":true,"gov.mw":true,"int.mw":true,"museum.mw":true,"net.mw":true,"org.mw":true,"mx":true,"com.mx":true,"org.mx":true,"gob.mx":true,"edu.mx":true,"net.mx":true,"my":true,"com.my":true,"net.my":true,"org.my":true,"gov.my":true,"edu.my":true,"mil.my":true,"name.my":true,"*.mz":true,"teledata.mz":false,"na":true,"info.na":true,"pro.na":true,"name.na":true,"school.na":true,"or.na":true,"dr.na":true,"us.na":true,"mx.na":true,"ca.na":true,"in.na":true,"cc.na":true,"tv.na":true,"ws.na":true,"mobi.na":true,"co.na":true,"com.na":true,"org.na":true,"name":true,"nc":true,"asso.nc":true,"ne":true,"net":true,"nf":true,"com.nf":true,"net.nf":true,"per.nf":true,"rec.nf":true,"web.nf":true,"arts.nf":true,"firm.nf":true,"info.nf":true,"other.nf":true,"store.nf":true,"ng":true,"com.ng":true,"edu.ng":true,"name.ng":true,"net.ng":true,"org.ng":true,"sch.ng":true,"gov.ng":true,"mil.ng":true,"mobi.ng":true,"*.ni":true,"nl":true,"bv.nl":true,"no":true,"fhs.no":true,"vgs.no":true,"fylkesbibl.no":true,"folkebibl.no":true,"museum.no":true,"idrett.no":true,"priv.no":true,"mil.no":true,"stat.no":true,"dep.no":true,"kommune.no":true,"herad.no":true,"aa.no":true,"ah.no":true,"bu.no":true,"fm.no":true,"hl.no":true,"hm.no":true,"jan-mayen.no":true,"mr.no":true,"nl.no":true,"nt.no":true,"of.no":true,"ol.no":true,"oslo.no":true,"rl.no":true,"sf.no":true,"st.no":true,"svalbard.no":true,"tm.no":true,"tr.no":true,"va.no":true,"vf.no":true,"gs.aa.no":true,"gs.ah.no":true,"gs.bu.no":true,"gs.fm.no":true,"gs.hl.no":true,"gs.hm.no":true,"gs.jan-mayen.no":true,"gs.mr.no":true,"gs.nl.no":true,"gs.nt.no":true,"gs.of.no":true,"gs.ol.no":true,"gs.oslo.no":true,"gs.rl.no":true,"gs.sf.no":true,"gs.st.no":true,"gs.svalbard.no":true,"gs.tm.no":true,"gs.tr.no":true,"gs.va.no":true,"gs.vf.no":true,"akrehamn.no":true,"xn--krehamn-dxa.no":true,"algard.no":true,"xn--lgrd-poac.no":true,"arna.no":true,"brumunddal.no":true,"bryne.no":true,"bronnoysund.no":true,"xn--brnnysund-m8ac.no":true,"drobak.no":true,"xn--drbak-wua.no":true,"egersund.no":true,"fetsund.no":true,"floro.no":true,"xn--flor-jra.no":true,"fredrikstad.no":true,"hokksund.no":true,"honefoss.no":true,"xn--hnefoss-q1a.no":true,"jessheim.no":true,"jorpeland.no":true,"xn--jrpeland-54a.no":true,"kirkenes.no":true,"kopervik.no":true,"krokstadelva.no":true,"langevag.no":true,"xn--langevg-jxa.no":true,"leirvik.no":true,"mjondalen.no":true,"xn--mjndalen-64a.no":true,"mo-i-rana.no":true,"mosjoen.no":true,"xn--mosjen-eya.no":true,"nesoddtangen.no":true,"orkanger.no":true,"osoyro.no":true,"xn--osyro-wua.no":true,"raholt.no":true,"xn--rholt-mra.no":true,"sandnessjoen.no":true,"xn--sandnessjen-ogb.no":true,"skedsmokorset.no":true,"slattum.no":true,"spjelkavik.no":true,"stathelle.no":true,"stavern.no":true,"stjordalshalsen.no":true,"xn--stjrdalshalsen-sqb.no":true,"tananger.no":true,"tranby.no":true,"vossevangen.no":true,"afjord.no":true,"xn--fjord-lra.no":true,"agdenes.no":true,"al.no":true,"xn--l-1fa.no":true,"alesund.no":true,"xn--lesund-hua.no":true,"alstahaug.no":true,"alta.no":true,"xn--lt-liac.no":true,"alaheadju.no":true,"xn--laheadju-7ya.no":true,"alvdal.no":true,"amli.no":true,"xn--mli-tla.no":true,"amot.no":true,"xn--mot-tla.no":true,"andebu.no":true,"andoy.no":true,"xn--andy-ira.no":true,"andasuolo.no":true,"ardal.no":true,"xn--rdal-poa.no":true,"aremark.no":true,"arendal.no":true,"xn--s-1fa.no":true,"aseral.no":true,"xn--seral-lra.no":true,"asker.no":true,"askim.no":true,"askvoll.no":true,"askoy.no":true,"xn--asky-ira.no":true,"asnes.no":true,"xn--snes-poa.no":true,"audnedaln.no":true,"aukra.no":true,"aure.no":true,"aurland.no":true,"aurskog-holand.no":true,"xn--aurskog-hland-jnb.no":true,"austevoll.no":true,"austrheim.no":true,"averoy.no":true,"xn--avery-yua.no":true,"balestrand.no":true,"ballangen.no":true,"balat.no":true,"xn--blt-elab.no":true,"balsfjord.no":true,"bahccavuotna.no":true,"xn--bhccavuotna-k7a.no":true,"bamble.no":true,"bardu.no":true,"beardu.no":true,"beiarn.no":true,"bajddar.no":true,"xn--bjddar-pta.no":true,"baidar.no":true,"xn--bidr-5nac.no":true,"berg.no":true,"bergen.no":true,"berlevag.no":true,"xn--berlevg-jxa.no":true,"bearalvahki.no":true,"xn--bearalvhki-y4a.no":true,"bindal.no":true,"birkenes.no":true,"bjarkoy.no":true,"xn--bjarky-fya.no":true,"bjerkreim.no":true,"bjugn.no":true,"bodo.no":true,"xn--bod-2na.no":true,"badaddja.no":true,"xn--bdddj-mrabd.no":true,"budejju.no":true,"bokn.no":true,"bremanger.no":true,"bronnoy.no":true,"xn--brnny-wuac.no":true,"bygland.no":true,"bykle.no":true,"barum.no":true,"xn--brum-voa.no":true,"bo.telemark.no":true,"xn--b-5ga.telemark.no":true,"bo.nordland.no":true,"xn--b-5ga.nordland.no":true,"bievat.no":true,"xn--bievt-0qa.no":true,"bomlo.no":true,"xn--bmlo-gra.no":true,"batsfjord.no":true,"xn--btsfjord-9za.no":true,"bahcavuotna.no":true,"xn--bhcavuotna-s4a.no":true,"dovre.no":true,"drammen.no":true,"drangedal.no":true,"dyroy.no":true,"xn--dyry-ira.no":true,"donna.no":true,"xn--dnna-gra.no":true,"eid.no":true,"eidfjord.no":true,"eidsberg.no":true,"eidskog.no":true,"eidsvoll.no":true,"eigersund.no":true,"elverum.no":true,"enebakk.no":true,"engerdal.no":true,"etne.no":true,"etnedal.no":true,"evenes.no":true,"evenassi.no":true,"xn--eveni-0qa01ga.no":true,"evje-og-hornnes.no":true,"farsund.no":true,"fauske.no":true,"fuossko.no":true,"fuoisku.no":true,"fedje.no":true,"fet.no":true,"finnoy.no":true,"xn--finny-yua.no":true,"fitjar.no":true,"fjaler.no":true,"fjell.no":true,"flakstad.no":true,"flatanger.no":true,"flekkefjord.no":true,"flesberg.no":true,"flora.no":true,"fla.no":true,"xn--fl-zia.no":true,"folldal.no":true,"forsand.no":true,"fosnes.no":true,"frei.no":true,"frogn.no":true,"froland.no":true,"frosta.no":true,"frana.no":true,"xn--frna-woa.no":true,"froya.no":true,"xn--frya-hra.no":true,"fusa.no":true,"fyresdal.no":true,"forde.no":true,"xn--frde-gra.no":true,"gamvik.no":true,"gangaviika.no":true,"xn--ggaviika-8ya47h.no":true,"gaular.no":true,"gausdal.no":true,"gildeskal.no":true,"xn--gildeskl-g0a.no":true,"giske.no":true,"gjemnes.no":true,"gjerdrum.no":true,"gjerstad.no":true,"gjesdal.no":true,"gjovik.no":true,"xn--gjvik-wua.no":true,"gloppen.no":true,"gol.no":true,"gran.no":true,"grane.no":true,"granvin.no":true,"gratangen.no":true,"grimstad.no":true,"grong.no":true,"kraanghke.no":true,"xn--kranghke-b0a.no":true,"grue.no":true,"gulen.no":true,"hadsel.no":true,"halden.no":true,"halsa.no":true,"hamar.no":true,"hamaroy.no":true,"habmer.no":true,"xn--hbmer-xqa.no":true,"hapmir.no":true,"xn--hpmir-xqa.no":true,"hammerfest.no":true,"hammarfeasta.no":true,"xn--hmmrfeasta-s4ac.no":true,"haram.no":true,"hareid.no":true,"harstad.no":true,"hasvik.no":true,"aknoluokta.no":true,"xn--koluokta-7ya57h.no":true,"hattfjelldal.no":true,"aarborte.no":true,"haugesund.no":true,"hemne.no":true,"hemnes.no":true,"hemsedal.no":true,"heroy.more-og-romsdal.no":true,"xn--hery-ira.xn--mre-og-romsdal-qqb.no":true,"heroy.nordland.no":true,"xn--hery-ira.nordland.no":true,"hitra.no":true,"hjartdal.no":true,"hjelmeland.no":true,"hobol.no":true,"xn--hobl-ira.no":true,"hof.no":true,"hol.no":true,"hole.no":true,"holmestrand.no":true,"holtalen.no":true,"xn--holtlen-hxa.no":true,"hornindal.no":true,"horten.no":true,"hurdal.no":true,"hurum.no":true,"hvaler.no":true,"hyllestad.no":true,"hagebostad.no":true,"xn--hgebostad-g3a.no":true,"hoyanger.no":true,"xn--hyanger-q1a.no":true,"hoylandet.no":true,"xn--hylandet-54a.no":true,"ha.no":true,"xn--h-2fa.no":true,"ibestad.no":true,"inderoy.no":true,"xn--indery-fya.no":true,"iveland.no":true,"jevnaker.no":true,"jondal.no":true,"jolster.no":true,"xn--jlster-bya.no":true,"karasjok.no":true,"karasjohka.no":true,"xn--krjohka-hwab49j.no":true,"karlsoy.no":true,"galsa.no":true,"xn--gls-elac.no":true,"karmoy.no":true,"xn--karmy-yua.no":true,"kautokeino.no":true,"guovdageaidnu.no":true,"klepp.no":true,"klabu.no":true,"xn--klbu-woa.no":true,"kongsberg.no":true,"kongsvinger.no":true,"kragero.no":true,"xn--krager-gya.no":true,"kristiansand.no":true,"kristiansund.no":true,"krodsherad.no":true,"xn--krdsherad-m8a.no":true,"kvalsund.no":true,"rahkkeravju.no":true,"xn--rhkkervju-01af.no":true,"kvam.no":true,"kvinesdal.no":true,"kvinnherad.no":true,"kviteseid.no":true,"kvitsoy.no":true,"xn--kvitsy-fya.no":true,"kvafjord.no":true,"xn--kvfjord-nxa.no":true,"giehtavuoatna.no":true,"kvanangen.no":true,"xn--kvnangen-k0a.no":true,"navuotna.no":true,"xn--nvuotna-hwa.no":true,"kafjord.no":true,"xn--kfjord-iua.no":true,"gaivuotna.no":true,"xn--givuotna-8ya.no":true,"larvik.no":true,"lavangen.no":true,"lavagis.no":true,"loabat.no":true,"xn--loabt-0qa.no":true,"lebesby.no":true,"davvesiida.no":true,"leikanger.no":true,"leirfjord.no":true,"leka.no":true,"leksvik.no":true,"lenvik.no":true,"leangaviika.no":true,"xn--leagaviika-52b.no":true,"lesja.no":true,"levanger.no":true,"lier.no":true,"lierne.no":true,"lillehammer.no":true,"lillesand.no":true,"lindesnes.no":true,"lindas.no":true,"xn--linds-pra.no":true,"lom.no":true,"loppa.no":true,"lahppi.no":true,"xn--lhppi-xqa.no":true,"lund.no":true,"lunner.no":true,"luroy.no":true,"xn--lury-ira.no":true,"luster.no":true,"lyngdal.no":true,"lyngen.no":true,"ivgu.no":true,"lardal.no":true,"lerdal.no":true,"xn--lrdal-sra.no":true,"lodingen.no":true,"xn--ldingen-q1a.no":true,"lorenskog.no":true,"xn--lrenskog-54a.no":true,"loten.no":true,"xn--lten-gra.no":true,"malvik.no":true,"masoy.no":true,"xn--msy-ula0h.no":true,"muosat.no":true,"xn--muost-0qa.no":true,"mandal.no":true,"marker.no":true,"marnardal.no":true,"masfjorden.no":true,"meland.no":true,"meldal.no":true,"melhus.no":true,"meloy.no":true,"xn--mely-ira.no":true,"meraker.no":true,"xn--merker-kua.no":true,"moareke.no":true,"xn--moreke-jua.no":true,"midsund.no":true,"midtre-gauldal.no":true,"modalen.no":true,"modum.no":true,"molde.no":true,"moskenes.no":true,"moss.no":true,"mosvik.no":true,"malselv.no":true,"xn--mlselv-iua.no":true,"malatvuopmi.no":true,"xn--mlatvuopmi-s4a.no":true,"namdalseid.no":true,"aejrie.no":true,"namsos.no":true,"namsskogan.no":true,"naamesjevuemie.no":true,"xn--nmesjevuemie-tcba.no":true,"laakesvuemie.no":true,"nannestad.no":true,"narvik.no":true,"narviika.no":true,"naustdal.no":true,"nedre-eiker.no":true,"nes.akershus.no":true,"nes.buskerud.no":true,"nesna.no":true,"nesodden.no":true,"nesseby.no":true,"unjarga.no":true,"xn--unjrga-rta.no":true,"nesset.no":true,"nissedal.no":true,"nittedal.no":true,"nord-aurdal.no":true,"nord-fron.no":true,"nord-odal.no":true,"norddal.no":true,"nordkapp.no":true,"davvenjarga.no":true,"xn--davvenjrga-y4a.no":true,"nordre-land.no":true,"nordreisa.no":true,"raisa.no":true,"xn--risa-5na.no":true,"nore-og-uvdal.no":true,"notodden.no":true,"naroy.no":true,"xn--nry-yla5g.no":true,"notteroy.no":true,"xn--nttery-byae.no":true,"odda.no":true,"oksnes.no":true,"xn--ksnes-uua.no":true,"oppdal.no":true,"oppegard.no":true,"xn--oppegrd-ixa.no":true,"orkdal.no":true,"orland.no":true,"xn--rland-uua.no":true,"orskog.no":true,"xn--rskog-uua.no":true,"orsta.no":true,"xn--rsta-fra.no":true,"os.hedmark.no":true,"os.hordaland.no":true,"osen.no":true,"osteroy.no":true,"xn--ostery-fya.no":true,"ostre-toten.no":true,"xn--stre-toten-zcb.no":true,"overhalla.no":true,"ovre-eiker.no":true,"xn--vre-eiker-k8a.no":true,"oyer.no":true,"xn--yer-zna.no":true,"oygarden.no":true,"xn--ygarden-p1a.no":true,"oystre-slidre.no":true,"xn--ystre-slidre-ujb.no":true,"porsanger.no":true,"porsangu.no":true,"xn--porsgu-sta26f.no":true,"porsgrunn.no":true,"radoy.no":true,"xn--rady-ira.no":true,"rakkestad.no":true,"rana.no":true,"ruovat.no":true,"randaberg.no":true,"rauma.no":true,"rendalen.no":true,"rennebu.no":true,"rennesoy.no":true,"xn--rennesy-v1a.no":true,"rindal.no":true,"ringebu.no":true,"ringerike.no":true,"ringsaker.no":true,"rissa.no":true,"risor.no":true,"xn--risr-ira.no":true,"roan.no":true,"rollag.no":true,"rygge.no":true,"ralingen.no":true,"xn--rlingen-mxa.no":true,"rodoy.no":true,"xn--rdy-0nab.no":true,"romskog.no":true,"xn--rmskog-bya.no":true,"roros.no":true,"xn--rros-gra.no":true,"rost.no":true,"xn--rst-0na.no":true,"royken.no":true,"xn--ryken-vua.no":true,"royrvik.no":true,"xn--ryrvik-bya.no":true,"rade.no":true,"xn--rde-ula.no":true,"salangen.no":true,"siellak.no":true,"saltdal.no":true,"salat.no":true,"xn--slt-elab.no":true,"xn--slat-5na.no":true,"samnanger.no":true,"sande.more-og-romsdal.no":true,"sande.xn--mre-og-romsdal-qqb.no":true,"sande.vestfold.no":true,"sandefjord.no":true,"sandnes.no":true,"sandoy.no":true,"xn--sandy-yua.no":true,"sarpsborg.no":true,"sauda.no":true,"sauherad.no":true,"sel.no":true,"selbu.no":true,"selje.no":true,"seljord.no":true,"sigdal.no":true,"siljan.no":true,"sirdal.no":true,"skaun.no":true,"skedsmo.no":true,"ski.no":true,"skien.no":true,"skiptvet.no":true,"skjervoy.no":true,"xn--skjervy-v1a.no":true,"skierva.no":true,"xn--skierv-uta.no":true,"skjak.no":true,"xn--skjk-soa.no":true,"skodje.no":true,"skanland.no":true,"xn--sknland-fxa.no":true,"skanit.no":true,"xn--sknit-yqa.no":true,"smola.no":true,"xn--smla-hra.no":true,"snillfjord.no":true,"snasa.no":true,"xn--snsa-roa.no":true,"snoasa.no":true,"snaase.no":true,"xn--snase-nra.no":true,"sogndal.no":true,"sokndal.no":true,"sola.no":true,"solund.no":true,"songdalen.no":true,"sortland.no":true,"spydeberg.no":true,"stange.no":true,"stavanger.no":true,"steigen.no":true,"steinkjer.no":true,"stjordal.no":true,"xn--stjrdal-s1a.no":true,"stokke.no":true,"stor-elvdal.no":true,"stord.no":true,"stordal.no":true,"storfjord.no":true,"omasvuotna.no":true,"strand.no":true,"stranda.no":true,"stryn.no":true,"sula.no":true,"suldal.no":true,"sund.no":true,"sunndal.no":true,"surnadal.no":true,"sveio.no":true,"svelvik.no":true,"sykkylven.no":true,"sogne.no":true,"xn--sgne-gra.no":true,"somna.no":true,"xn--smna-gra.no":true,"sondre-land.no":true,"xn--sndre-land-0cb.no":true,"sor-aurdal.no":true,"xn--sr-aurdal-l8a.no":true,"sor-fron.no":true,"xn--sr-fron-q1a.no":true,"sor-odal.no":true,"xn--sr-odal-q1a.no":true,"sor-varanger.no":true,"xn--sr-varanger-ggb.no":true,"matta-varjjat.no":true,"xn--mtta-vrjjat-k7af.no":true,"sorfold.no":true,"xn--srfold-bya.no":true,"sorreisa.no":true,"xn--srreisa-q1a.no":true,"sorum.no":true,"xn--srum-gra.no":true,"tana.no":true,"deatnu.no":true,"time.no":true,"tingvoll.no":true,"tinn.no":true,"tjeldsund.no":true,"dielddanuorri.no":true,"tjome.no":true,"xn--tjme-hra.no":true,"tokke.no":true,"tolga.no":true,"torsken.no":true,"tranoy.no":true,"xn--trany-yua.no":true,"tromso.no":true,"xn--troms-zua.no":true,"tromsa.no":true,"romsa.no":true,"trondheim.no":true,"troandin.no":true,"trysil.no":true,"trana.no":true,"xn--trna-woa.no":true,"trogstad.no":true,"xn--trgstad-r1a.no":true,"tvedestrand.no":true,"tydal.no":true,"tynset.no":true,"tysfjord.no":true,"divtasvuodna.no":true,"divttasvuotna.no":true,"tysnes.no":true,"tysvar.no":true,"xn--tysvr-vra.no":true,"tonsberg.no":true,"xn--tnsberg-q1a.no":true,"ullensaker.no":true,"ullensvang.no":true,"ulvik.no":true,"utsira.no":true,"vadso.no":true,"xn--vads-jra.no":true,"cahcesuolo.no":true,"xn--hcesuolo-7ya35b.no":true,"vaksdal.no":true,"valle.no":true,"vang.no":true,"vanylven.no":true,"vardo.no":true,"xn--vard-jra.no":true,"varggat.no":true,"xn--vrggt-xqad.no":true,"vefsn.no":true,"vaapste.no":true,"vega.no":true,"vegarshei.no":true,"xn--vegrshei-c0a.no":true,"vennesla.no":true,"verdal.no":true,"verran.no":true,"vestby.no":true,"vestnes.no":true,"vestre-slidre.no":true,"vestre-toten.no":true,"vestvagoy.no":true,"xn--vestvgy-ixa6o.no":true,"vevelstad.no":true,"vik.no":true,"vikna.no":true,"vindafjord.no":true,"volda.no":true,"voss.no":true,"varoy.no":true,"xn--vry-yla5g.no":true,"vagan.no":true,"xn--vgan-qoa.no":true,"voagat.no":true,"vagsoy.no":true,"xn--vgsy-qoa0j.no":true,"vaga.no":true,"xn--vg-yiab.no":true,"valer.ostfold.no":true,"xn--vler-qoa.xn--stfold-9xa.no":true,"valer.hedmark.no":true,"xn--vler-qoa.hedmark.no":true,"*.np":true,"nr":true,"biz.nr":true,"info.nr":true,"gov.nr":true,"edu.nr":true,"org.nr":true,"net.nr":true,"com.nr":true,"nu":true,"nz":true,"ac.nz":true,"co.nz":true,"cri.nz":true,"geek.nz":true,"gen.nz":true,"govt.nz":true,"health.nz":true,"iwi.nz":true,"kiwi.nz":true,"maori.nz":true,"mil.nz":true,"xn--mori-qsa.nz":true,"net.nz":true,"org.nz":true,"parliament.nz":true,"school.nz":true,"om":true,"co.om":true,"com.om":true,"edu.om":true,"gov.om":true,"med.om":true,"museum.om":true,"net.om":true,"org.om":true,"pro.om":true,"org":true,"pa":true,"ac.pa":true,"gob.pa":true,"com.pa":true,"org.pa":true,"sld.pa":true,"edu.pa":true,"net.pa":true,"ing.pa":true,"abo.pa":true,"med.pa":true,"nom.pa":true,"pe":true,"edu.pe":true,"gob.pe":true,"nom.pe":true,"mil.pe":true,"org.pe":true,"com.pe":true,"net.pe":true,"pf":true,"com.pf":true,"org.pf":true,"edu.pf":true,"*.pg":true,"ph":true,"com.ph":true,"net.ph":true,"org.ph":true,"gov.ph":true,"edu.ph":true,"ngo.ph":true,"mil.ph":true,"i.ph":true,"pk":true,"com.pk":true,"net.pk":true,"edu.pk":true,"org.pk":true,"fam.pk":true,"biz.pk":true,"web.pk":true,"gov.pk":true,"gob.pk":true,"gok.pk":true,"gon.pk":true,"gop.pk":true,"gos.pk":true,"info.pk":true,"pl":true,"com.pl":true,"net.pl":true,"org.pl":true,"aid.pl":true,"agro.pl":true,"atm.pl":true,"auto.pl":true,"biz.pl":true,"edu.pl":true,"gmina.pl":true,"gsm.pl":true,"info.pl":true,"mail.pl":true,"miasta.pl":true,"media.pl":true,"mil.pl":true,"nieruchomosci.pl":true,"nom.pl":true,"pc.pl":true,"powiat.pl":true,"priv.pl":true,"realestate.pl":true,"rel.pl":true,"sex.pl":true,"shop.pl":true,"sklep.pl":true,"sos.pl":true,"szkola.pl":true,"targi.pl":true,"tm.pl":true,"tourism.pl":true,"travel.pl":true,"turystyka.pl":true,"gov.pl":true,"ap.gov.pl":true,"ic.gov.pl":true,"is.gov.pl":true,"us.gov.pl":true,"kmpsp.gov.pl":true,"kppsp.gov.pl":true,"kwpsp.gov.pl":true,"psp.gov.pl":true,"wskr.gov.pl":true,"kwp.gov.pl":true,"mw.gov.pl":true,"ug.gov.pl":true,"um.gov.pl":true,"umig.gov.pl":true,"ugim.gov.pl":true,"upow.gov.pl":true,"uw.gov.pl":true,"starostwo.gov.pl":true,"pa.gov.pl":true,"po.gov.pl":true,"psse.gov.pl":true,"pup.gov.pl":true,"rzgw.gov.pl":true,"sa.gov.pl":true,"so.gov.pl":true,"sr.gov.pl":true,"wsa.gov.pl":true,"sko.gov.pl":true,"uzs.gov.pl":true,"wiih.gov.pl":true,"winb.gov.pl":true,"pinb.gov.pl":true,"wios.gov.pl":true,"witd.gov.pl":true,"wzmiuw.gov.pl":true,"piw.gov.pl":true,"wiw.gov.pl":true,"griw.gov.pl":true,"wif.gov.pl":true,"oum.gov.pl":true,"sdn.gov.pl":true,"zp.gov.pl":true,"uppo.gov.pl":true,"mup.gov.pl":true,"wuoz.gov.pl":true,"konsulat.gov.pl":true,"oirm.gov.pl":true,"augustow.pl":true,"babia-gora.pl":true,"bedzin.pl":true,"beskidy.pl":true,"bialowieza.pl":true,"bialystok.pl":true,"bielawa.pl":true,"bieszczady.pl":true,"boleslawiec.pl":true,"bydgoszcz.pl":true,"bytom.pl":true,"cieszyn.pl":true,"czeladz.pl":true,"czest.pl":true,"dlugoleka.pl":true,"elblag.pl":true,"elk.pl":true,"glogow.pl":true,"gniezno.pl":true,"gorlice.pl":true,"grajewo.pl":true,"ilawa.pl":true,"jaworzno.pl":true,"jelenia-gora.pl":true,"jgora.pl":true,"kalisz.pl":true,"kazimierz-dolny.pl":true,"karpacz.pl":true,"kartuzy.pl":true,"kaszuby.pl":true,"katowice.pl":true,"kepno.pl":true,"ketrzyn.pl":true,"klodzko.pl":true,"kobierzyce.pl":true,"kolobrzeg.pl":true,"konin.pl":true,"konskowola.pl":true,"kutno.pl":true,"lapy.pl":true,"lebork.pl":true,"legnica.pl":true,"lezajsk.pl":true,"limanowa.pl":true,"lomza.pl":true,"lowicz.pl":true,"lubin.pl":true,"lukow.pl":true,"malbork.pl":true,"malopolska.pl":true,"mazowsze.pl":true,"mazury.pl":true,"mielec.pl":true,"mielno.pl":true,"mragowo.pl":true,"naklo.pl":true,"nowaruda.pl":true,"nysa.pl":true,"olawa.pl":true,"olecko.pl":true,"olkusz.pl":true,"olsztyn.pl":true,"opoczno.pl":true,"opole.pl":true,"ostroda.pl":true,"ostroleka.pl":true,"ostrowiec.pl":true,"ostrowwlkp.pl":true,"pila.pl":true,"pisz.pl":true,"podhale.pl":true,"podlasie.pl":true,"polkowice.pl":true,"pomorze.pl":true,"pomorskie.pl":true,"prochowice.pl":true,"pruszkow.pl":true,"przeworsk.pl":true,"pulawy.pl":true,"radom.pl":true,"rawa-maz.pl":true,"rybnik.pl":true,"rzeszow.pl":true,"sanok.pl":true,"sejny.pl":true,"slask.pl":true,"slupsk.pl":true,"sosnowiec.pl":true,"stalowa-wola.pl":true,"skoczow.pl":true,"starachowice.pl":true,"stargard.pl":true,"suwalki.pl":true,"swidnica.pl":true,"swiebodzin.pl":true,"swinoujscie.pl":true,"szczecin.pl":true,"szczytno.pl":true,"tarnobrzeg.pl":true,"tgory.pl":true,"turek.pl":true,"tychy.pl":true,"ustka.pl":true,"walbrzych.pl":true,"warmia.pl":true,"warszawa.pl":true,"waw.pl":true,"wegrow.pl":true,"wielun.pl":true,"wlocl.pl":true,"wloclawek.pl":true,"wodzislaw.pl":true,"wolomin.pl":true,"wroclaw.pl":true,"zachpomor.pl":true,"zagan.pl":true,"zarow.pl":true,"zgora.pl":true,"zgorzelec.pl":true,"pm":true,"pn":true,"gov.pn":true,"co.pn":true,"org.pn":true,"edu.pn":true,"net.pn":true,"post":true,"pr":true,"com.pr":true,"net.pr":true,"org.pr":true,"gov.pr":true,"edu.pr":true,"isla.pr":true,"pro.pr":true,"biz.pr":true,"info.pr":true,"name.pr":true,"est.pr":true,"prof.pr":true,"ac.pr":true,"pro":true,"aca.pro":true,"bar.pro":true,"cpa.pro":true,"jur.pro":true,"law.pro":true,"med.pro":true,"eng.pro":true,"ps":true,"edu.ps":true,"gov.ps":true,"sec.ps":true,"plo.ps":true,"com.ps":true,"org.ps":true,"net.ps":true,"pt":true,"net.pt":true,"gov.pt":true,"org.pt":true,"edu.pt":true,"int.pt":true,"publ.pt":true,"com.pt":true,"nome.pt":true,"pw":true,"co.pw":true,"ne.pw":true,"or.pw":true,"ed.pw":true,"go.pw":true,"belau.pw":true,"py":true,"com.py":true,"coop.py":true,"edu.py":true,"gov.py":true,"mil.py":true,"net.py":true,"org.py":true,"qa":true,"com.qa":true,"edu.qa":true,"gov.qa":true,"mil.qa":true,"name.qa":true,"net.qa":true,"org.qa":true,"sch.qa":true,"re":true,"com.re":true,"asso.re":true,"nom.re":true,"ro":true,"com.ro":true,"org.ro":true,"tm.ro":true,"nt.ro":true,"nom.ro":true,"info.ro":true,"rec.ro":true,"arts.ro":true,"firm.ro":true,"store.ro":true,"www.ro":true,"rs":true,"co.rs":true,"org.rs":true,"edu.rs":true,"ac.rs":true,"gov.rs":true,"in.rs":true,"ru":true,"ac.ru":true,"com.ru":true,"edu.ru":true,"int.ru":true,"net.ru":true,"org.ru":true,"pp.ru":true,"adygeya.ru":true,"altai.ru":true,"amur.ru":true,"arkhangelsk.ru":true,"astrakhan.ru":true,"bashkiria.ru":true,"belgorod.ru":true,"bir.ru":true,"bryansk.ru":true,"buryatia.ru":true,"cbg.ru":true,"chel.ru":true,"chelyabinsk.ru":true,"chita.ru":true,"chukotka.ru":true,"chuvashia.ru":true,"dagestan.ru":true,"dudinka.ru":true,"e-burg.ru":true,"grozny.ru":true,"irkutsk.ru":true,"ivanovo.ru":true,"izhevsk.ru":true,"jar.ru":true,"joshkar-ola.ru":true,"kalmykia.ru":true,"kaluga.ru":true,"kamchatka.ru":true,"karelia.ru":true,"kazan.ru":true,"kchr.ru":true,"kemerovo.ru":true,"khabarovsk.ru":true,"khakassia.ru":true,"khv.ru":true,"kirov.ru":true,"koenig.ru":true,"komi.ru":true,"kostroma.ru":true,"krasnoyarsk.ru":true,"kuban.ru":true,"kurgan.ru":true,"kursk.ru":true,"lipetsk.ru":true,"magadan.ru":true,"mari.ru":true,"mari-el.ru":true,"marine.ru":true,"mordovia.ru":true,"msk.ru":true,"murmansk.ru":true,"nalchik.ru":true,"nnov.ru":true,"nov.ru":true,"novosibirsk.ru":true,"nsk.ru":true,"omsk.ru":true,"orenburg.ru":true,"oryol.ru":true,"palana.ru":true,"penza.ru":true,"perm.ru":true,"ptz.ru":true,"rnd.ru":true,"ryazan.ru":true,"sakhalin.ru":true,"samara.ru":true,"saratov.ru":true,"simbirsk.ru":true,"smolensk.ru":true,"spb.ru":true,"stavropol.ru":true,"stv.ru":true,"surgut.ru":true,"tambov.ru":true,"tatarstan.ru":true,"tom.ru":true,"tomsk.ru":true,"tsaritsyn.ru":true,"tsk.ru":true,"tula.ru":true,"tuva.ru":true,"tver.ru":true,"tyumen.ru":true,"udm.ru":true,"udmurtia.ru":true,"ulan-ude.ru":true,"vladikavkaz.ru":true,"vladimir.ru":true,"vladivostok.ru":true,"volgograd.ru":true,"vologda.ru":true,"voronezh.ru":true,"vrn.ru":true,"vyatka.ru":true,"yakutia.ru":true,"yamal.ru":true,"yaroslavl.ru":true,"yekaterinburg.ru":true,"yuzhno-sakhalinsk.ru":true,"amursk.ru":true,"baikal.ru":true,"cmw.ru":true,"fareast.ru":true,"jamal.ru":true,"kms.ru":true,"k-uralsk.ru":true,"kustanai.ru":true,"kuzbass.ru":true,"magnitka.ru":true,"mytis.ru":true,"nakhodka.ru":true,"nkz.ru":true,"norilsk.ru":true,"oskol.ru":true,"pyatigorsk.ru":true,"rubtsovsk.ru":true,"snz.ru":true,"syzran.ru":true,"vdonsk.ru":true,"zgrad.ru":true,"gov.ru":true,"mil.ru":true,"test.ru":true,"rw":true,"gov.rw":true,"net.rw":true,"edu.rw":true,"ac.rw":true,"com.rw":true,"co.rw":true,"int.rw":true,"mil.rw":true,"gouv.rw":true,"sa":true,"com.sa":true,"net.sa":true,"org.sa":true,"gov.sa":true,"med.sa":true,"pub.sa":true,"edu.sa":true,"sch.sa":true,"sb":true,"com.sb":true,"edu.sb":true,"gov.sb":true,"net.sb":true,"org.sb":true,"sc":true,"com.sc":true,"gov.sc":true,"net.sc":true,"org.sc":true,"edu.sc":true,"sd":true,"com.sd":true,"net.sd":true,"org.sd":true,"edu.sd":true,"med.sd":true,"tv.sd":true,"gov.sd":true,"info.sd":true,"se":true,"a.se":true,"ac.se":true,"b.se":true,"bd.se":true,"brand.se":true,"c.se":true,"d.se":true,"e.se":true,"f.se":true,"fh.se":true,"fhsk.se":true,"fhv.se":true,"g.se":true,"h.se":true,"i.se":true,"k.se":true,"komforb.se":true,"kommunalforbund.se":true,"komvux.se":true,"l.se":true,"lanbib.se":true,"m.se":true,"n.se":true,"naturbruksgymn.se":true,"o.se":true,"org.se":true,"p.se":true,"parti.se":true,"pp.se":true,"press.se":true,"r.se":true,"s.se":true,"t.se":true,"tm.se":true,"u.se":true,"w.se":true,"x.se":true,"y.se":true,"z.se":true,"sg":true,"com.sg":true,"net.sg":true,"org.sg":true,"gov.sg":true,"edu.sg":true,"per.sg":true,"sh":true,"com.sh":true,"net.sh":true,"gov.sh":true,"org.sh":true,"mil.sh":true,"si":true,"sj":true,"sk":true,"sl":true,"com.sl":true,"net.sl":true,"edu.sl":true,"gov.sl":true,"org.sl":true,"sm":true,"sn":true,"art.sn":true,"com.sn":true,"edu.sn":true,"gouv.sn":true,"org.sn":true,"perso.sn":true,"univ.sn":true,"so":true,"com.so":true,"net.so":true,"org.so":true,"sr":true,"st":true,"co.st":true,"com.st":true,"consulado.st":true,"edu.st":true,"embaixada.st":true,"gov.st":true,"mil.st":true,"net.st":true,"org.st":true,"principe.st":true,"saotome.st":true,"store.st":true,"su":true,"adygeya.su":true,"arkhangelsk.su":true,"balashov.su":true,"bashkiria.su":true,"bryansk.su":true,"dagestan.su":true,"grozny.su":true,"ivanovo.su":true,"kalmykia.su":true,"kaluga.su":true,"karelia.su":true,"khakassia.su":true,"krasnodar.su":true,"kurgan.su":true,"lenug.su":true,"mordovia.su":true,"msk.su":true,"murmansk.su":true,"nalchik.su":true,"nov.su":true,"obninsk.su":true,"penza.su":true,"pokrovsk.su":true,"sochi.su":true,"spb.su":true,"togliatti.su":true,"troitsk.su":true,"tula.su":true,"tuva.su":true,"vladikavkaz.su":true,"vladimir.su":true,"vologda.su":true,"sv":true,"com.sv":true,"edu.sv":true,"gob.sv":true,"org.sv":true,"red.sv":true,"sx":true,"gov.sx":true,"sy":true,"edu.sy":true,"gov.sy":true,"net.sy":true,"mil.sy":true,"com.sy":true,"org.sy":true,"sz":true,"co.sz":true,"ac.sz":true,"org.sz":true,"tc":true,"td":true,"tel":true,"tf":true,"tg":true,"th":true,"ac.th":true,"co.th":true,"go.th":true,"in.th":true,"mi.th":true,"net.th":true,"or.th":true,"tj":true,"ac.tj":true,"biz.tj":true,"co.tj":true,"com.tj":true,"edu.tj":true,"go.tj":true,"gov.tj":true,"int.tj":true,"mil.tj":true,"name.tj":true,"net.tj":true,"nic.tj":true,"org.tj":true,"test.tj":true,"web.tj":true,"tk":true,"tl":true,"gov.tl":true,"tm":true,"com.tm":true,"co.tm":true,"org.tm":true,"net.tm":true,"nom.tm":true,"gov.tm":true,"mil.tm":true,"edu.tm":true,"tn":true,"com.tn":true,"ens.tn":true,"fin.tn":true,"gov.tn":true,"ind.tn":true,"intl.tn":true,"nat.tn":true,"net.tn":true,"org.tn":true,"info.tn":true,"perso.tn":true,"tourism.tn":true,"edunet.tn":true,"rnrt.tn":true,"rns.tn":true,"rnu.tn":true,"mincom.tn":true,"agrinet.tn":true,"defense.tn":true,"turen.tn":true,"to":true,"com.to":true,"gov.to":true,"net.to":true,"org.to":true,"edu.to":true,"mil.to":true,"tp":true,"tr":true,"com.tr":true,"info.tr":true,"biz.tr":true,"net.tr":true,"org.tr":true,"web.tr":true,"gen.tr":true,"tv.tr":true,"av.tr":true,"dr.tr":true,"bbs.tr":true,"name.tr":true,"tel.tr":true,"gov.tr":true,"bel.tr":true,"pol.tr":true,"mil.tr":true,"k12.tr":true,"edu.tr":true,"kep.tr":true,"nc.tr":true,"gov.nc.tr":true,"travel":true,"tt":true,"co.tt":true,"com.tt":true,"org.tt":true,"net.tt":true,"biz.tt":true,"info.tt":true,"pro.tt":true,"int.tt":true,"coop.tt":true,"jobs.tt":true,"mobi.tt":true,"travel.tt":true,"museum.tt":true,"aero.tt":true,"name.tt":true,"gov.tt":true,"edu.tt":true,"tv":true,"tw":true,"edu.tw":true,"gov.tw":true,"mil.tw":true,"com.tw":true,"net.tw":true,"org.tw":true,"idv.tw":true,"game.tw":true,"ebiz.tw":true,"club.tw":true,"xn--zf0ao64a.tw":true,"xn--uc0atv.tw":true,"xn--czrw28b.tw":true,"tz":true,"ac.tz":true,"co.tz":true,"go.tz":true,"hotel.tz":true,"info.tz":true,"me.tz":true,"mil.tz":true,"mobi.tz":true,"ne.tz":true,"or.tz":true,"sc.tz":true,"tv.tz":true,"ua":true,"com.ua":true,"edu.ua":true,"gov.ua":true,"in.ua":true,"net.ua":true,"org.ua":true,"cherkassy.ua":true,"cherkasy.ua":true,"chernigov.ua":true,"chernihiv.ua":true,"chernivtsi.ua":true,"chernovtsy.ua":true,"ck.ua":true,"cn.ua":true,"cr.ua":true,"crimea.ua":true,"cv.ua":true,"dn.ua":true,"dnepropetrovsk.ua":true,"dnipropetrovsk.ua":true,"dominic.ua":true,"donetsk.ua":true,"dp.ua":true,"if.ua":true,"ivano-frankivsk.ua":true,"kh.ua":true,"kharkiv.ua":true,"kharkov.ua":true,"kherson.ua":true,"khmelnitskiy.ua":true,"khmelnytskyi.ua":true,"kiev.ua":true,"kirovograd.ua":true,"km.ua":true,"kr.ua":true,"krym.ua":true,"ks.ua":true,"kv.ua":true,"kyiv.ua":true,"lg.ua":true,"lt.ua":true,"lugansk.ua":true,"lutsk.ua":true,"lv.ua":true,"lviv.ua":true,"mk.ua":true,"mykolaiv.ua":true,"nikolaev.ua":true,"od.ua":true,"odesa.ua":true,"odessa.ua":true,"pl.ua":true,"poltava.ua":true,"rivne.ua":true,"rovno.ua":true,"rv.ua":true,"sb.ua":true,"sebastopol.ua":true,"sevastopol.ua":true,"sm.ua":true,"sumy.ua":true,"te.ua":true,"ternopil.ua":true,"uz.ua":true,"uzhgorod.ua":true,"vinnica.ua":true,"vinnytsia.ua":true,"vn.ua":true,"volyn.ua":true,"yalta.ua":true,"zaporizhzhe.ua":true,"zaporizhzhia.ua":true,"zhitomir.ua":true,"zhytomyr.ua":true,"zp.ua":true,"zt.ua":true,"ug":true,"co.ug":true,"or.ug":true,"ac.ug":true,"sc.ug":true,"go.ug":true,"ne.ug":true,"com.ug":true,"org.ug":true,"uk":true,"ac.uk":true,"co.uk":true,"gov.uk":true,"ltd.uk":true,"me.uk":true,"net.uk":true,"nhs.uk":true,"org.uk":true,"plc.uk":true,"police.uk":true,"*.sch.uk":true,"us":true,"dni.us":true,"fed.us":true,"isa.us":true,"kids.us":true,"nsn.us":true,"ak.us":true,"al.us":true,"ar.us":true,"as.us":true,"az.us":true,"ca.us":true,"co.us":true,"ct.us":true,"dc.us":true,"de.us":true,"fl.us":true,"ga.us":true,"gu.us":true,"hi.us":true,"ia.us":true,"id.us":true,"il.us":true,"in.us":true,"ks.us":true,"ky.us":true,"la.us":true,"ma.us":true,"md.us":true,"me.us":true,"mi.us":true,"mn.us":true,"mo.us":true,"ms.us":true,"mt.us":true,"nc.us":true,"nd.us":true,"ne.us":true,"nh.us":true,"nj.us":true,"nm.us":true,"nv.us":true,"ny.us":true,"oh.us":true,"ok.us":true,"or.us":true,"pa.us":true,"pr.us":true,"ri.us":true,"sc.us":true,"sd.us":true,"tn.us":true,"tx.us":true,"ut.us":true,"vi.us":true,"vt.us":true,"va.us":true,"wa.us":true,"wi.us":true,"wv.us":true,"wy.us":true,"k12.ak.us":true,"k12.al.us":true,"k12.ar.us":true,"k12.as.us":true,"k12.az.us":true,"k12.ca.us":true,"k12.co.us":true,"k12.ct.us":true,"k12.dc.us":true,"k12.de.us":true,"k12.fl.us":true,"k12.ga.us":true,"k12.gu.us":true,"k12.ia.us":true,"k12.id.us":true,"k12.il.us":true,"k12.in.us":true,"k12.ks.us":true,"k12.ky.us":true,"k12.la.us":true,"k12.ma.us":true,"k12.md.us":true,"k12.me.us":true,"k12.mi.us":true,"k12.mn.us":true,"k12.mo.us":true,"k12.ms.us":true,"k12.mt.us":true,"k12.nc.us":true,"k12.ne.us":true,"k12.nh.us":true,"k12.nj.us":true,"k12.nm.us":true,"k12.nv.us":true,"k12.ny.us":true,"k12.oh.us":true,"k12.ok.us":true,"k12.or.us":true,"k12.pa.us":true,"k12.pr.us":true,"k12.ri.us":true,"k12.sc.us":true,"k12.tn.us":true,"k12.tx.us":true,"k12.ut.us":true,"k12.vi.us":true,"k12.vt.us":true,"k12.va.us":true,"k12.wa.us":true,"k12.wi.us":true,"k12.wy.us":true,"cc.ak.us":true,"cc.al.us":true,"cc.ar.us":true,"cc.as.us":true,"cc.az.us":true,"cc.ca.us":true,"cc.co.us":true,"cc.ct.us":true,"cc.dc.us":true,"cc.de.us":true,"cc.fl.us":true,"cc.ga.us":true,"cc.gu.us":true,"cc.hi.us":true,"cc.ia.us":true,"cc.id.us":true,"cc.il.us":true,"cc.in.us":true,"cc.ks.us":true,"cc.ky.us":true,"cc.la.us":true,"cc.ma.us":true,"cc.md.us":true,"cc.me.us":true,"cc.mi.us":true,"cc.mn.us":true,"cc.mo.us":true,"cc.ms.us":true,"cc.mt.us":true,"cc.nc.us":true,"cc.nd.us":true,"cc.ne.us":true,"cc.nh.us":true,"cc.nj.us":true,"cc.nm.us":true,"cc.nv.us":true,"cc.ny.us":true,"cc.oh.us":true,"cc.ok.us":true,"cc.or.us":true,"cc.pa.us":true,"cc.pr.us":true,"cc.ri.us":true,"cc.sc.us":true,"cc.sd.us":true,"cc.tn.us":true,"cc.tx.us":true,"cc.ut.us":true,"cc.vi.us":true,"cc.vt.us":true,"cc.va.us":true,"cc.wa.us":true,"cc.wi.us":true,"cc.wv.us":true,"cc.wy.us":true,"lib.ak.us":true,"lib.al.us":true,"lib.ar.us":true,"lib.as.us":true,"lib.az.us":true,"lib.ca.us":true,"lib.co.us":true,"lib.ct.us":true,"lib.dc.us":true,"lib.de.us":true,"lib.fl.us":true,"lib.ga.us":true,"lib.gu.us":true,"lib.hi.us":true,"lib.ia.us":true,"lib.id.us":true,"lib.il.us":true,"lib.in.us":true,"lib.ks.us":true,"lib.ky.us":true,"lib.la.us":true,"lib.ma.us":true,"lib.md.us":true,"lib.me.us":true,"lib.mi.us":true,"lib.mn.us":true,"lib.mo.us":true,"lib.ms.us":true,"lib.mt.us":true,"lib.nc.us":true,"lib.nd.us":true,"lib.ne.us":true,"lib.nh.us":true,"lib.nj.us":true,"lib.nm.us":true,"lib.nv.us":true,"lib.ny.us":true,"lib.oh.us":true,"lib.ok.us":true,"lib.or.us":true,"lib.pa.us":true,"lib.pr.us":true,"lib.ri.us":true,"lib.sc.us":true,"lib.sd.us":true,"lib.tn.us":true,"lib.tx.us":true,"lib.ut.us":true,"lib.vi.us":true,"lib.vt.us":true,"lib.va.us":true,"lib.wa.us":true,"lib.wi.us":true,"lib.wy.us":true,"pvt.k12.ma.us":true,"chtr.k12.ma.us":true,"paroch.k12.ma.us":true,"uy":true,"com.uy":true,"edu.uy":true,"gub.uy":true,"mil.uy":true,"net.uy":true,"org.uy":true,"uz":true,"co.uz":true,"com.uz":true,"net.uz":true,"org.uz":true,"va":true,"vc":true,"com.vc":true,"net.vc":true,"org.vc":true,"gov.vc":true,"mil.vc":true,"edu.vc":true,"ve":true,"arts.ve":true,"co.ve":true,"com.ve":true,"e12.ve":true,"edu.ve":true,"firm.ve":true,"gob.ve":true,"gov.ve":true,"info.ve":true,"int.ve":true,"mil.ve":true,"net.ve":true,"org.ve":true,"rec.ve":true,"store.ve":true,"tec.ve":true,"web.ve":true,"vg":true,"vi":true,"co.vi":true,"com.vi":true,"k12.vi":true,"net.vi":true,"org.vi":true,"vn":true,"com.vn":true,"net.vn":true,"org.vn":true,"edu.vn":true,"gov.vn":true,"int.vn":true,"ac.vn":true,"biz.vn":true,"info.vn":true,"name.vn":true,"pro.vn":true,"health.vn":true,"vu":true,"com.vu":true,"edu.vu":true,"net.vu":true,"org.vu":true,"wf":true,"ws":true,"com.ws":true,"net.ws":true,"org.ws":true,"gov.ws":true,"edu.ws":true,"yt":true,"xn--mgbaam7a8h":true,"xn--y9a3aq":true,"xn--54b7fta0cc":true,"xn--90ais":true,"xn--fiqs8s":true,"xn--fiqz9s":true,"xn--lgbbat1ad8j":true,"xn--wgbh1c":true,"xn--node":true,"xn--qxam":true,"xn--j6w193g":true,"xn--h2brj9c":true,"xn--mgbbh1a71e":true,"xn--fpcrj9c3d":true,"xn--gecrj9c":true,"xn--s9brj9c":true,"xn--45brj9c":true,"xn--xkc2dl3a5ee0h":true,"xn--mgba3a4f16a":true,"xn--mgba3a4fra":true,"xn--mgbtx2b":true,"xn--mgbayh7gpa":true,"xn--3e0b707e":true,"xn--80ao21a":true,"xn--fzc2c9e2c":true,"xn--xkc2al3hye2a":true,"xn--mgbc0a9azcg":true,"xn--d1alf":true,"xn--l1acc":true,"xn--mix891f":true,"xn--mix082f":true,"xn--mgbx4cd0ab":true,"xn--mgb9awbf":true,"xn--mgbai9azgqp6j":true,"xn--mgbai9a5eva00b":true,"xn--ygbi2ammx":true,"xn--90a3ac":true,"xn--o1ac.xn--90a3ac":true,"xn--c1avg.xn--90a3ac":true,"xn--90azh.xn--90a3ac":true,"xn--d1at.xn--90a3ac":true,"xn--o1ach.xn--90a3ac":true,"xn--80au.xn--90a3ac":true,"xn--p1ai":true,"xn--wgbl6a":true,"xn--mgberp4a5d4ar":true,"xn--mgberp4a5d4a87g":true,"xn--mgbqly7c0a67fbc":true,"xn--mgbqly7cvafr":true,"xn--mgbpl2fh":true,"xn--yfro4i67o":true,"xn--clchc0ea0b2g2a9gcd":true,"xn--ogbpf8fl":true,"xn--mgbtf8fl":true,"xn--o3cw4h":true,"xn--pgbs0dh":true,"xn--kpry57d":true,"xn--kprw13d":true,"xn--nnx388a":true,"xn--j1amh":true,"xn--mgb2ddes":true,"xxx":true,"*.ye":true,"ac.za":true,"agrica.za":true,"alt.za":true,"co.za":true,"edu.za":true,"gov.za":true,"grondar.za":true,"law.za":true,"mil.za":true,"net.za":true,"ngo.za":true,"nis.za":true,"nom.za":true,"org.za":true,"school.za":true,"tm.za":true,"web.za":true,"*.zm":true,"*.zw":true,"aaa":true,"aarp":true,"abarth":true,"abb":true,"abbott":true,"abbvie":true,"abc":true,"able":true,"abogado":true,"abudhabi":true,"academy":true,"accenture":true,"accountant":true,"accountants":true,"aco":true,"active":true,"actor":true,"adac":true,"ads":true,"adult":true,"aeg":true,"aetna":true,"afamilycompany":true,"afl":true,"africa":true,"africamagic":true,"agakhan":true,"agency":true,"aig":true,"aigo":true,"airbus":true,"airforce":true,"airtel":true,"akdn":true,"alfaromeo":true,"alibaba":true,"alipay":true,"allfinanz":true,"allstate":true,"ally":true,"alsace":true,"alstom":true,"americanexpress":true,"americanfamily":true,"amex":true,"amfam":true,"amica":true,"amsterdam":true,"analytics":true,"android":true,"anquan":true,"anz":true,"aol":true,"apartments":true,"app":true,"apple":true,"aquarelle":true,"aramco":true,"archi":true,"army":true,"arte":true,"asda":true,"associates":true,"athleta":true,"attorney":true,"auction":true,"audi":true,"audible":true,"audio":true,"auspost":true,"author":true,"auto":true,"autos":true,"avianca":true,"aws":true,"axa":true,"azure":true,"baby":true,"baidu":true,"banamex":true,"bananarepublic":true,"band":true,"bank":true,"bar":true,"barcelona":true,"barclaycard":true,"barclays":true,"barefoot":true,"bargains":true,"basketball":true,"bauhaus":true,"bayern":true,"bbc":true,"bbt":true,"bbva":true,"bcg":true,"bcn":true,"beats":true,"beer":true,"bentley":true,"berlin":true,"best":true,"bestbuy":true,"bet":true,"bharti":true,"bible":true,"bid":true,"bike":true,"bing":true,"bingo":true,"bio":true,"black":true,"blackfriday":true,"blanco":true,"blockbuster":true,"blog":true,"bloomberg":true,"blue":true,"bms":true,"bmw":true,"bnl":true,"bnpparibas":true,"boats":true,"boehringer":true,"bofa":true,"bom":true,"bond":true,"boo":true,"book":true,"booking":true,"boots":true,"bosch":true,"bostik":true,"bot":true,"boutique":true,"bradesco":true,"bridgestone":true,"broadway":true,"broker":true,"brother":true,"brussels":true,"budapest":true,"bugatti":true,"build":true,"builders":true,"business":true,"buy":true,"buzz":true,"bzh":true,"cab":true,"cafe":true,"cal":true,"call":true,"calvinklein":true,"camera":true,"camp":true,"cancerresearch":true,"canon":true,"capetown":true,"capital":true,"capitalone":true,"car":true,"caravan":true,"cards":true,"care":true,"career":true,"careers":true,"cars":true,"cartier":true,"casa":true,"case":true,"caseih":true,"cash":true,"casino":true,"catering":true,"cba":true,"cbn":true,"cbre":true,"cbs":true,"ceb":true,"center":true,"ceo":true,"cern":true,"cfa":true,"cfd":true,"chanel":true,"channel":true,"chase":true,"chat":true,"cheap":true,"chintai":true,"chloe":true,"christmas":true,"chrome":true,"chrysler":true,"church":true,"cipriani":true,"circle":true,"cisco":true,"citadel":true,"citi":true,"citic":true,"city":true,"cityeats":true,"claims":true,"cleaning":true,"click":true,"clinic":true,"clothing":true,"cloud":true,"club":true,"clubmed":true,"coach":true,"codes":true,"coffee":true,"college":true,"cologne":true,"comcast":true,"commbank":true,"community":true,"company":true,"computer":true,"comsec":true,"condos":true,"construction":true,"consulting":true,"contact":true,"contractors":true,"cooking":true,"cookingchannel":true,"cool":true,"corsica":true,"country":true,"coupon":true,"coupons":true,"courses":true,"credit":true,"creditcard":true,"creditunion":true,"cricket":true,"crown":true,"crs":true,"cruises":true,"csc":true,"cuisinella":true,"cymru":true,"cyou":true,"dabur":true,"dad":true,"dance":true,"date":true,"dating":true,"datsun":true,"day":true,"dclk":true,"dds":true,"deal":true,"dealer":true,"deals":true,"degree":true,"delivery":true,"dell":true,"deloitte":true,"delta":true,"democrat":true,"dental":true,"dentist":true,"desi":true,"design":true,"dev":true,"dhl":true,"diamonds":true,"diet":true,"digital":true,"direct":true,"directory":true,"discount":true,"discover":true,"dish":true,"dnp":true,"docs":true,"dodge":true,"dog":true,"doha":true,"domains":true,"doosan":true,"dot":true,"download":true,"drive":true,"dstv":true,"dtv":true,"dubai":true,"duck":true,"dunlop":true,"duns":true,"dupont":true,"durban":true,"dvag":true,"dwg":true,"earth":true,"eat":true,"edeka":true,"education":true,"email":true,"emerck":true,"emerson":true,"energy":true,"engineer":true,"engineering":true,"enterprises":true,"epost":true,"epson":true,"equipment":true,"ericsson":true,"erni":true,"esq":true,"estate":true,"esurance":true,"etisalat":true,"eurovision":true,"eus":true,"events":true,"everbank":true,"exchange":true,"expert":true,"exposed":true,"express":true,"extraspace":true,"fage":true,"fail":true,"fairwinds":true,"faith":true,"family":true,"fan":true,"fans":true,"farm":true,"farmers":true,"fashion":true,"fast":true,"fedex":true,"feedback":true,"ferrari":true,"ferrero":true,"fiat":true,"fidelity":true,"fido":true,"film":true,"final":true,"finance":true,"financial":true,"fire":true,"firestone":true,"firmdale":true,"fish":true,"fishing":true,"fit":true,"fitness":true,"flickr":true,"flights":true,"flir":true,"florist":true,"flowers":true,"flsmidth":true,"fly":true,"foo":true,"foodnetwork":true,"football":true,"ford":true,"forex":true,"forsale":true,"forum":true,"foundation":true,"fox":true,"fresenius":true,"frl":true,"frogans":true,"frontdoor":true,"frontier":true,"ftr":true,"fujitsu":true,"fujixerox":true,"fund":true,"furniture":true,"futbol":true,"fyi":true,"gal":true,"gallery":true,"gallo":true,"gallup":true,"game":true,"games":true,"gap":true,"garden":true,"gbiz":true,"gdn":true,"gea":true,"gent":true,"genting":true,"george":true,"ggee":true,"gift":true,"gifts":true,"gives":true,"giving":true,"glade":true,"glass":true,"gle":true,"global":true,"globo":true,"gmail":true,"gmo":true,"gmx":true,"godaddy":true,"gold":true,"goldpoint":true,"golf":true,"goo":true,"goodhands":true,"goodyear":true,"goog":true,"google":true,"gop":true,"got":true,"gotv":true,"grainger":true,"graphics":true,"gratis":true,"green":true,"gripe":true,"group":true,"guardian":true,"gucci":true,"guge":true,"guide":true,"guitars":true,"guru":true,"hamburg":true,"hangout":true,"haus":true,"hbo":true,"hdfc":true,"hdfcbank":true,"health":true,"healthcare":true,"help":true,"helsinki":true,"here":true,"hermes":true,"hgtv":true,"hiphop":true,"hisamitsu":true,"hitachi":true,"hiv":true,"hkt":true,"hockey":true,"holdings":true,"holiday":true,"homedepot":true,"homegoods":true,"homes":true,"homesense":true,"honda":true,"honeywell":true,"horse":true,"host":true,"hosting":true,"hot":true,"hoteles":true,"hotmail":true,"house":true,"how":true,"hsbc":true,"htc":true,"hughes":true,"hyatt":true,"hyundai":true,"ibm":true,"icbc":true,"ice":true,"icu":true,"ieee":true,"ifm":true,"iinet":true,"ikano":true,"imamat":true,"imdb":true,"immo":true,"immobilien":true,"industries":true,"infiniti":true,"ing":true,"ink":true,"institute":true,"insurance":true,"insure":true,"intel":true,"international":true,"intuit":true,"investments":true,"ipiranga":true,"irish":true,"iselect":true,"ismaili":true,"ist":true,"istanbul":true,"itau":true,"itv":true,"iveco":true,"iwc":true,"jaguar":true,"java":true,"jcb":true,"jcp":true,"jeep":true,"jetzt":true,"jewelry":true,"jio":true,"jlc":true,"jll":true,"jmp":true,"jnj":true,"joburg":true,"jot":true,"joy":true,"jpmorgan":true,"jprs":true,"juegos":true,"juniper":true,"kaufen":true,"kddi":true,"kerryhotels":true,"kerrylogistics":true,"kerryproperties":true,"kfh":true,"kia":true,"kim":true,"kinder":true,"kindle":true,"kitchen":true,"kiwi":true,"koeln":true,"komatsu":true,"kosher":true,"kpmg":true,"kpn":true,"krd":true,"kred":true,"kuokgroup":true,"kyknet":true,"kyoto":true,"lacaixa":true,"ladbrokes":true,"lamborghini":true,"lancaster":true,"lancia":true,"lancome":true,"land":true,"landrover":true,"lanxess":true,"lasalle":true,"lat":true,"latino":true,"latrobe":true,"law":true,"lawyer":true,"lds":true,"lease":true,"leclerc":true,"lefrak":true,"legal":true,"lego":true,"lexus":true,"lgbt":true,"liaison":true,"lidl":true,"life":true,"lifeinsurance":true,"lifestyle":true,"lighting":true,"like":true,"lilly":true,"limited":true,"limo":true,"lincoln":true,"linde":true,"link":true,"lipsy":true,"live":true,"living":true,"lixil":true,"loan":true,"loans":true,"locker":true,"locus":true,"loft":true,"lol":true,"london":true,"lotte":true,"lotto":true,"love":true,"lpl":true,"lplfinancial":true,"ltd":true,"ltda":true,"lundbeck":true,"lupin":true,"luxe":true,"luxury":true,"macys":true,"madrid":true,"maif":true,"maison":true,"makeup":true,"man":true,"management":true,"mango":true,"market":true,"marketing":true,"markets":true,"marriott":true,"marshalls":true,"maserati":true,"mattel":true,"mba":true,"mcd":true,"mcdonalds":true,"mckinsey":true,"med":true,"media":true,"meet":true,"melbourne":true,"meme":true,"memorial":true,"men":true,"menu":true,"meo":true,"metlife":true,"miami":true,"microsoft":true,"mini":true,"mint":true,"mit":true,"mitsubishi":true,"mlb":true,"mls":true,"mma":true,"mnet":true,"mobily":true,"moda":true,"moe":true,"moi":true,"mom":true,"monash":true,"money":true,"monster":true,"montblanc":true,"mopar":true,"mormon":true,"mortgage":true,"moscow":true,"moto":true,"motorcycles":true,"mov":true,"movie":true,"movistar":true,"msd":true,"mtn":true,"mtpc":true,"mtr":true,"multichoice":true,"mutual":true,"mutuelle":true,"mzansimagic":true,"nab":true,"nadex":true,"nagoya":true,"naspers":true,"nationwide":true,"natura":true,"navy":true,"nba":true,"nec":true,"netbank":true,"netflix":true,"network":true,"neustar":true,"new":true,"newholland":true,"news":true,"next":true,"nextdirect":true,"nexus":true,"nfl":true,"ngo":true,"nhk":true,"nico":true,"nike":true,"nikon":true,"ninja":true,"nissan":true,"nokia":true,"northwesternmutual":true,"norton":true,"now":true,"nowruz":true,"nowtv":true,"nra":true,"nrw":true,"ntt":true,"nyc":true,"obi":true,"observer":true,"off":true,"office":true,"okinawa":true,"olayan":true,"olayangroup":true,"oldnavy":true,"ollo":true,"omega":true,"one":true,"ong":true,"onl":true,"online":true,"onyourside":true,"ooo":true,"open":true,"oracle":true,"orange":true,"organic":true,"orientexpress":true,"osaka":true,"otsuka":true,"ott":true,"ovh":true,"page":true,"pamperedchef":true,"panasonic":true,"panerai":true,"paris":true,"pars":true,"partners":true,"parts":true,"party":true,"passagens":true,"pay":true,"payu":true,"pccw":true,"pet":true,"pfizer":true,"pharmacy":true,"philips":true,"photo":true,"photography":true,"photos":true,"physio":true,"piaget":true,"pics":true,"pictet":true,"pictures":true,"pid":true,"pin":true,"ping":true,"pink":true,"pioneer":true,"pizza":true,"place":true,"play":true,"playstation":true,"plumbing":true,"plus":true,"pnc":true,"pohl":true,"poker":true,"politie":true,"porn":true,"pramerica":true,"praxi":true,"press":true,"prime":true,"prod":true,"productions":true,"prof":true,"progressive":true,"promo":true,"properties":true,"property":true,"protection":true,"pru":true,"prudential":true,"pub":true,"qpon":true,"quebec":true,"quest":true,"qvc":true,"racing":true,"raid":true,"read":true,"realestate":true,"realtor":true,"realty":true,"recipes":true,"red":true,"redstone":true,"redumbrella":true,"rehab":true,"reise":true,"reisen":true,"reit":true,"reliance":true,"ren":true,"rent":true,"rentals":true,"repair":true,"report":true,"republican":true,"rest":true,"restaurant":true,"review":true,"reviews":true,"rexroth":true,"rich":true,"richardli":true,"ricoh":true,"rightathome":true,"ril":true,"rio":true,"rip":true,"rocher":true,"rocks":true,"rodeo":true,"rogers":true,"room":true,"rsvp":true,"ruhr":true,"run":true,"rwe":true,"ryukyu":true,"saarland":true,"safe":true,"safety":true,"sakura":true,"sale":true,"salon":true,"samsclub":true,"samsung":true,"sandvik":true,"sandvikcoromant":true,"sanofi":true,"sap":true,"sapo":true,"sarl":true,"sas":true,"save":true,"saxo":true,"sbi":true,"sbs":true,"sca":true,"scb":true,"schaeffler":true,"schmidt":true,"scholarships":true,"school":true,"schule":true,"schwarz":true,"science":true,"scjohnson":true,"scor":true,"scot":true,"seat":true,"secure":true,"security":true,"seek":true,"sener":true,"services":true,"ses":true,"seven":true,"sew":true,"sex":true,"sexy":true,"sfr":true,"shangrila":true,"sharp":true,"shaw":true,"shell":true,"shia":true,"shiksha":true,"shoes":true,"shouji":true,"show":true,"showtime":true,"shriram":true,"silk":true,"sina":true,"singles":true,"site":true,"ski":true,"skin":true,"sky":true,"skype":true,"sling":true,"smart":true,"smile":true,"sncf":true,"soccer":true,"social":true,"softbank":true,"software":true,"sohu":true,"solar":true,"solutions":true,"song":true,"sony":true,"soy":true,"space":true,"spiegel":true,"spot":true,"spreadbetting":true,"srl":true,"srt":true,"stada":true,"staples":true,"star":true,"starhub":true,"statebank":true,"statefarm":true,"statoil":true,"stc":true,"stcgroup":true,"stockholm":true,"storage":true,"store":true,"studio":true,"study":true,"style":true,"sucks":true,"supersport":true,"supplies":true,"supply":true,"support":true,"surf":true,"surgery":true,"suzuki":true,"swatch":true,"swiftcover":true,"swiss":true,"sydney":true,"symantec":true,"systems":true,"tab":true,"taipei":true,"talk":true,"taobao":true,"target":true,"tatamotors":true,"tatar":true,"tattoo":true,"tax":true,"taxi":true,"tci":true,"tdk":true,"team":true,"tech":true,"technology":true,"telecity":true,"telefonica":true,"temasek":true,"tennis":true,"teva":true,"thd":true,"theater":true,"theatre":true,"theguardian":true,"tiaa":true,"tickets":true,"tienda":true,"tiffany":true,"tips":true,"tires":true,"tirol":true,"tjmaxx":true,"tjx":true,"tkmaxx":true,"tmall":true,"today":true,"tokyo":true,"tools":true,"top":true,"toray":true,"toshiba":true,"total":true,"tours":true,"town":true,"toyota":true,"toys":true,"trade":true,"trading":true,"training":true,"travelchannel":true,"travelers":true,"travelersinsurance":true,"trust":true,"trv":true,"tube":true,"tui":true,"tunes":true,"tushu":true,"tvs":true,"ubank":true,"ubs":true,"uconnect":true,"university":true,"uno":true,"uol":true,"ups":true,"vacations":true,"vana":true,"vanguard":true,"vegas":true,"ventures":true,"verisign":true,"versicherung":true,"vet":true,"viajes":true,"video":true,"vig":true,"viking":true,"villas":true,"vin":true,"vip":true,"virgin":true,"visa":true,"vision":true,"vista":true,"vistaprint":true,"viva":true,"vivo":true,"vlaanderen":true,"vodka":true,"volkswagen":true,"vote":true,"voting":true,"voto":true,"voyage":true,"vuelos":true,"wales":true,"walmart":true,"walter":true,"wang":true,"wanggou":true,"warman":true,"watch":true,"watches":true,"weather":true,"weatherchannel":true,"webcam":true,"weber":true,"website":true,"wed":true,"wedding":true,"weibo":true,"weir":true,"whoswho":true,"wien":true,"wiki":true,"williamhill":true,"win":true,"windows":true,"wine":true,"winners":true,"wme":true,"wolterskluwer":true,"woodside":true,"work":true,"works":true,"world":true,"wtc":true,"wtf":true,"xbox":true,"xerox":true,"xfinity":true,"xihuan":true,"xin":true,"xn--11b4c3d":true,"xn--1ck2e1b":true,"xn--1qqw23a":true,"xn--30rr7y":true,"xn--3bst00m":true,"xn--3ds443g":true,"xn--3oq18vl8pn36a":true,"xn--3pxu8k":true,"xn--42c2d9a":true,"xn--45q11c":true,"xn--4gbrim":true,"xn--4gq48lf9j":true,"xn--55qw42g":true,"xn--55qx5d":true,"xn--5su34j936bgsg":true,"xn--5tzm5g":true,"xn--6frz82g":true,"xn--6qq986b3xl":true,"xn--80adxhks":true,"xn--80asehdb":true,"xn--80aswg":true,"xn--8y0a063a":true,"xn--9dbq2a":true,"xn--9et52u":true,"xn--9krt00a":true,"xn--b4w605ferd":true,"xn--bck1b9a5dre4c":true,"xn--c1avg":true,"xn--c2br7g":true,"xn--cck2b3b":true,"xn--cg4bki":true,"xn--czr694b":true,"xn--czrs0t":true,"xn--czru2d":true,"xn--d1acj3b":true,"xn--eckvdtc9d":true,"xn--efvy88h":true,"xn--estv75g":true,"xn--fct429k":true,"xn--fhbei":true,"xn--fiq228c5hs":true,"xn--fiq64b":true,"xn--fjq720a":true,"xn--flw351e":true,"xn--fzys8d69uvgm":true,"xn--g2xx48c":true,"xn--gckr3f0f":true,"xn--hxt814e":true,"xn--i1b6b1a6a2e":true,"xn--imr513n":true,"xn--io0a7i":true,"xn--j1aef":true,"xn--jlq61u9w7b":true,"xn--jvr189m":true,"xn--kcrx77d1x4a":true,"xn--kpu716f":true,"xn--kput3i":true,"xn--mgba3a3ejt":true,"xn--mgba7c0bbn0a":true,"xn--mgbaakc7dvf":true,"xn--mgbab2bd":true,"xn--mgbb9fbpob":true,"xn--mgbca7dzdo":true,"xn--mgbt3dhd":true,"xn--mk1bu44c":true,"xn--mxtq1m":true,"xn--ngbc5azd":true,"xn--ngbe9e0a":true,"xn--nqv7f":true,"xn--nqv7fs00ema":true,"xn--nyqy26a":true,"xn--p1acf":true,"xn--pbt977c":true,"xn--pssy2u":true,"xn--q9jyb4c":true,"xn--qcka1pmc":true,"xn--rhqv96g":true,"xn--rovu88b":true,"xn--ses554g":true,"xn--t60b56a":true,"xn--tckwe":true,"xn--unup4y":true,"xn--vermgensberater-ctb":true,"xn--vermgensberatung-pwb":true,"xn--vhquv":true,"xn--vuq861b":true,"xn--w4r85el8fhu5dnra":true,"xn--w4rs40l":true,"xn--xhq521b":true,"xn--zfr164b":true,"xperia":true,"xyz":true,"yachts":true,"yahoo":true,"yamaxun":true,"yandex":true,"yodobashi":true,"yoga":true,"yokohama":true,"you":true,"youtube":true,"yun":true,"zappos":true,"zara":true,"zero":true,"zip":true,"zippo":true,"zone":true,"zuerich":true,"cloudfront.net":true,"ap-northeast-1.compute.amazonaws.com":true,"ap-southeast-1.compute.amazonaws.com":true,"ap-southeast-2.compute.amazonaws.com":true,"cn-north-1.compute.amazonaws.cn":true,"compute.amazonaws.cn":true,"compute.amazonaws.com":true,"compute-1.amazonaws.com":true,"eu-west-1.compute.amazonaws.com":true,"eu-central-1.compute.amazonaws.com":true,"sa-east-1.compute.amazonaws.com":true,"us-east-1.amazonaws.com":true,"us-gov-west-1.compute.amazonaws.com":true,"us-west-1.compute.amazonaws.com":true,"us-west-2.compute.amazonaws.com":true,"z-1.compute-1.amazonaws.com":true,"z-2.compute-1.amazonaws.com":true,"elasticbeanstalk.com":true,"elb.amazonaws.com":true,"s3.amazonaws.com":true,"s3-ap-northeast-1.amazonaws.com":true,"s3-ap-southeast-1.amazonaws.com":true,"s3-ap-southeast-2.amazonaws.com":true,"s3-external-1.amazonaws.com":true,"s3-external-2.amazonaws.com":true,"s3-fips-us-gov-west-1.amazonaws.com":true,"s3-eu-central-1.amazonaws.com":true,"s3-eu-west-1.amazonaws.com":true,"s3-sa-east-1.amazonaws.com":true,"s3-us-gov-west-1.amazonaws.com":true,"s3-us-west-1.amazonaws.com":true,"s3-us-west-2.amazonaws.com":true,"s3.cn-north-1.amazonaws.com.cn":true,"s3.eu-central-1.amazonaws.com":true,"betainabox.com":true,"ae.org":true,"ar.com":true,"br.com":true,"cn.com":true,"com.de":true,"com.se":true,"de.com":true,"eu.com":true,"gb.com":true,"gb.net":true,"hu.com":true,"hu.net":true,"jp.net":true,"jpn.com":true,"kr.com":true,"mex.com":true,"no.com":true,"qc.com":true,"ru.com":true,"sa.com":true,"se.com":true,"se.net":true,"uk.com":true,"uk.net":true,"us.com":true,"uy.com":true,"za.bz":true,"za.com":true,"africa.com":true,"gr.com":true,"in.net":true,"us.org":true,"co.com":true,"c.la":true,"cloudcontrolled.com":true,"cloudcontrolapp.com":true,"co.ca":true,"c.cdn77.org":true,"cdn77-ssl.net":true,"r.cdn77.net":true,"rsc.cdn77.org":true,"ssl.origin.cdn77-secure.org":true,"co.nl":true,"co.no":true,"*.platform.sh":true,"cupcake.is":true,"dreamhosters.com":true,"duckdns.org":true,"dyndns-at-home.com":true,"dyndns-at-work.com":true,"dyndns-blog.com":true,"dyndns-free.com":true,"dyndns-home.com":true,"dyndns-ip.com":true,"dyndns-mail.com":true,"dyndns-office.com":true,"dyndns-pics.com":true,"dyndns-remote.com":true,"dyndns-server.com":true,"dyndns-web.com":true,"dyndns-wiki.com":true,"dyndns-work.com":true,"dyndns.biz":true,"dyndns.info":true,"dyndns.org":true,"dyndns.tv":true,"at-band-camp.net":true,"ath.cx":true,"barrel-of-knowledge.info":true,"barrell-of-knowledge.info":true,"better-than.tv":true,"blogdns.com":true,"blogdns.net":true,"blogdns.org":true,"blogsite.org":true,"boldlygoingnowhere.org":true,"broke-it.net":true,"buyshouses.net":true,"cechire.com":true,"dnsalias.com":true,"dnsalias.net":true,"dnsalias.org":true,"dnsdojo.com":true,"dnsdojo.net":true,"dnsdojo.org":true,"does-it.net":true,"doesntexist.com":true,"doesntexist.org":true,"dontexist.com":true,"dontexist.net":true,"dontexist.org":true,"doomdns.com":true,"doomdns.org":true,"dvrdns.org":true,"dyn-o-saur.com":true,"dynalias.com":true,"dynalias.net":true,"dynalias.org":true,"dynathome.net":true,"dyndns.ws":true,"endofinternet.net":true,"endofinternet.org":true,"endoftheinternet.org":true,"est-a-la-maison.com":true,"est-a-la-masion.com":true,"est-le-patron.com":true,"est-mon-blogueur.com":true,"for-better.biz":true,"for-more.biz":true,"for-our.info":true,"for-some.biz":true,"for-the.biz":true,"forgot.her.name":true,"forgot.his.name":true,"from-ak.com":true,"from-al.com":true,"from-ar.com":true,"from-az.net":true,"from-ca.com":true,"from-co.net":true,"from-ct.com":true,"from-dc.com":true,"from-de.com":true,"from-fl.com":true,"from-ga.com":true,"from-hi.com":true,"from-ia.com":true,"from-id.com":true,"from-il.com":true,"from-in.com":true,"from-ks.com":true,"from-ky.com":true,"from-la.net":true,"from-ma.com":true,"from-md.com":true,"from-me.org":true,"from-mi.com":true,"from-mn.com":true,"from-mo.com":true,"from-ms.com":true,"from-mt.com":true,"from-nc.com":true,"from-nd.com":true,"from-ne.com":true,"from-nh.com":true,"from-nj.com":true,"from-nm.com":true,"from-nv.com":true,"from-ny.net":true,"from-oh.com":true,"from-ok.com":true,"from-or.com":true,"from-pa.com":true,"from-pr.com":true,"from-ri.com":true,"from-sc.com":true,"from-sd.com":true,"from-tn.com":true,"from-tx.com":true,"from-ut.com":true,"from-va.com":true,"from-vt.com":true,"from-wa.com":true,"from-wi.com":true,"from-wv.com":true,"from-wy.com":true,"ftpaccess.cc":true,"fuettertdasnetz.de":true,"game-host.org":true,"game-server.cc":true,"getmyip.com":true,"gets-it.net":true,"go.dyndns.org":true,"gotdns.com":true,"gotdns.org":true,"groks-the.info":true,"groks-this.info":true,"ham-radio-op.net":true,"here-for-more.info":true,"hobby-site.com":true,"hobby-site.org":true,"home.dyndns.org":true,"homedns.org":true,"homeftp.net":true,"homeftp.org":true,"homeip.net":true,"homelinux.com":true,"homelinux.net":true,"homelinux.org":true,"homeunix.com":true,"homeunix.net":true,"homeunix.org":true,"iamallama.com":true,"in-the-band.net":true,"is-a-anarchist.com":true,"is-a-blogger.com":true,"is-a-bookkeeper.com":true,"is-a-bruinsfan.org":true,"is-a-bulls-fan.com":true,"is-a-candidate.org":true,"is-a-caterer.com":true,"is-a-celticsfan.org":true,"is-a-chef.com":true,"is-a-chef.net":true,"is-a-chef.org":true,"is-a-conservative.com":true,"is-a-cpa.com":true,"is-a-cubicle-slave.com":true,"is-a-democrat.com":true,"is-a-designer.com":true,"is-a-doctor.com":true,"is-a-financialadvisor.com":true,"is-a-geek.com":true,"is-a-geek.net":true,"is-a-geek.org":true,"is-a-green.com":true,"is-a-guru.com":true,"is-a-hard-worker.com":true,"is-a-hunter.com":true,"is-a-knight.org":true,"is-a-landscaper.com":true,"is-a-lawyer.com":true,"is-a-liberal.com":true,"is-a-libertarian.com":true,"is-a-linux-user.org":true,"is-a-llama.com":true,"is-a-musician.com":true,"is-a-nascarfan.com":true,"is-a-nurse.com":true,"is-a-painter.com":true,"is-a-patsfan.org":true,"is-a-personaltrainer.com":true,"is-a-photographer.com":true,"is-a-player.com":true,"is-a-republican.com":true,"is-a-rockstar.com":true,"is-a-socialist.com":true,"is-a-soxfan.org":true,"is-a-student.com":true,"is-a-teacher.com":true,"is-a-techie.com":true,"is-a-therapist.com":true,"is-an-accountant.com":true,"is-an-actor.com":true,"is-an-actress.com":true,"is-an-anarchist.com":true,"is-an-artist.com":true,"is-an-engineer.com":true,"is-an-entertainer.com":true,"is-by.us":true,"is-certified.com":true,"is-found.org":true,"is-gone.com":true,"is-into-anime.com":true,"is-into-cars.com":true,"is-into-cartoons.com":true,"is-into-games.com":true,"is-leet.com":true,"is-lost.org":true,"is-not-certified.com":true,"is-saved.org":true,"is-slick.com":true,"is-uberleet.com":true,"is-very-bad.org":true,"is-very-evil.org":true,"is-very-good.org":true,"is-very-nice.org":true,"is-very-sweet.org":true,"is-with-theband.com":true,"isa-geek.com":true,"isa-geek.net":true,"isa-geek.org":true,"isa-hockeynut.com":true,"issmarterthanyou.com":true,"isteingeek.de":true,"istmein.de":true,"kicks-ass.net":true,"kicks-ass.org":true,"knowsitall.info":true,"land-4-sale.us":true,"lebtimnetz.de":true,"leitungsen.de":true,"likes-pie.com":true,"likescandy.com":true,"merseine.nu":true,"mine.nu":true,"misconfused.org":true,"mypets.ws":true,"myphotos.cc":true,"neat-url.com":true,"office-on-the.net":true,"on-the-web.tv":true,"podzone.net":true,"podzone.org":true,"readmyblog.org":true,"saves-the-whales.com":true,"scrapper-site.net":true,"scrapping.cc":true,"selfip.biz":true,"selfip.com":true,"selfip.info":true,"selfip.net":true,"selfip.org":true,"sells-for-less.com":true,"sells-for-u.com":true,"sells-it.net":true,"sellsyourhome.org":true,"servebbs.com":true,"servebbs.net":true,"servebbs.org":true,"serveftp.net":true,"serveftp.org":true,"servegame.org":true,"shacknet.nu":true,"simple-url.com":true,"space-to-rent.com":true,"stuff-4-sale.org":true,"stuff-4-sale.us":true,"teaches-yoga.com":true,"thruhere.net":true,"traeumtgerade.de":true,"webhop.biz":true,"webhop.info":true,"webhop.net":true,"webhop.org":true,"worse-than.tv":true,"writesthisblog.com":true,"eu.org":true,"al.eu.org":true,"asso.eu.org":true,"at.eu.org":true,"au.eu.org":true,"be.eu.org":true,"bg.eu.org":true,"ca.eu.org":true,"cd.eu.org":true,"ch.eu.org":true,"cn.eu.org":true,"cy.eu.org":true,"cz.eu.org":true,"de.eu.org":true,"dk.eu.org":true,"edu.eu.org":true,"ee.eu.org":true,"es.eu.org":true,"fi.eu.org":true,"fr.eu.org":true,"gr.eu.org":true,"hr.eu.org":true,"hu.eu.org":true,"ie.eu.org":true,"il.eu.org":true,"in.eu.org":true,"int.eu.org":true,"is.eu.org":true,"it.eu.org":true,"jp.eu.org":true,"kr.eu.org":true,"lt.eu.org":true,"lu.eu.org":true,"lv.eu.org":true,"mc.eu.org":true,"me.eu.org":true,"mk.eu.org":true,"mt.eu.org":true,"my.eu.org":true,"net.eu.org":true,"ng.eu.org":true,"nl.eu.org":true,"no.eu.org":true,"nz.eu.org":true,"paris.eu.org":true,"pl.eu.org":true,"pt.eu.org":true,"q-a.eu.org":true,"ro.eu.org":true,"ru.eu.org":true,"se.eu.org":true,"si.eu.org":true,"sk.eu.org":true,"tr.eu.org":true,"uk.eu.org":true,"us.eu.org":true,"a.ssl.fastly.net":true,"b.ssl.fastly.net":true,"global.ssl.fastly.net":true,"a.prod.fastly.net":true,"global.prod.fastly.net":true,"firebaseapp.com":true,"flynnhub.com":true,"service.gov.uk":true,"github.io":true,"githubusercontent.com":true,"ro.com":true,"appspot.com":true,"blogspot.ae":true,"blogspot.al":true,"blogspot.am":true,"blogspot.ba":true,"blogspot.be":true,"blogspot.bg":true,"blogspot.bj":true,"blogspot.ca":true,"blogspot.cf":true,"blogspot.ch":true,"blogspot.cl":true,"blogspot.co.at":true,"blogspot.co.id":true,"blogspot.co.il":true,"blogspot.co.ke":true,"blogspot.co.nz":true,"blogspot.co.uk":true,"blogspot.co.za":true,"blogspot.com":true,"blogspot.com.ar":true,"blogspot.com.au":true,"blogspot.com.br":true,"blogspot.com.by":true,"blogspot.com.co":true,"blogspot.com.cy":true,"blogspot.com.ee":true,"blogspot.com.eg":true,"blogspot.com.es":true,"blogspot.com.mt":true,"blogspot.com.ng":true,"blogspot.com.tr":true,"blogspot.com.uy":true,"blogspot.cv":true,"blogspot.cz":true,"blogspot.de":true,"blogspot.dk":true,"blogspot.fi":true,"blogspot.fr":true,"blogspot.gr":true,"blogspot.hk":true,"blogspot.hr":true,"blogspot.hu":true,"blogspot.ie":true,"blogspot.in":true,"blogspot.is":true,"blogspot.it":true,"blogspot.jp":true,"blogspot.kr":true,"blogspot.li":true,"blogspot.lt":true,"blogspot.lu":true,"blogspot.md":true,"blogspot.mk":true,"blogspot.mr":true,"blogspot.mx":true,"blogspot.my":true,"blogspot.nl":true,"blogspot.no":true,"blogspot.pe":true,"blogspot.pt":true,"blogspot.qa":true,"blogspot.re":true,"blogspot.ro":true,"blogspot.rs":true,"blogspot.ru":true,"blogspot.se":true,"blogspot.sg":true,"blogspot.si":true,"blogspot.sk":true,"blogspot.sn":true,"blogspot.td":true,"blogspot.tw":true,"blogspot.ug":true,"blogspot.vn":true,"codespot.com":true,"googleapis.com":true,"googlecode.com":true,"pagespeedmobilizer.com":true,"withgoogle.com":true,"withyoutube.com":true,"herokuapp.com":true,"herokussl.com":true,"iki.fi":true,"biz.at":true,"info.at":true,"co.pl":true,"azurewebsites.net":true,"azure-mobile.net":true,"cloudapp.net":true,"bmoattachments.org":true,"4u.com":true,"nfshost.com":true,"nyc.mn":true,"nid.io":true,"operaunite.com":true,"outsystemscloud.com":true,"art.pl":true,"gliwice.pl":true,"krakow.pl":true,"poznan.pl":true,"wroc.pl":true,"zakopane.pl":true,"pantheon.io":true,"gotpantheon.com":true,"priv.at":true,"qa2.com":true,"rhcloud.com":true,"sandcats.io":true,"biz.ua":true,"co.ua":true,"pp.ua":true,"sinaapp.com":true,"vipsinaapp.com":true,"1kapp.com":true,"gda.pl":true,"gdansk.pl":true,"gdynia.pl":true,"med.pl":true,"sopot.pl":true,"hk.com":true,"hk.org":true,"ltd.hk":true,"inc.hk":true,"yolasite.com":true,"za.net":true,"za.org":true}); - -// END of automatically generated file diff --git a/node_modules/request/node_modules/tough-cookie/lib/store.js b/node_modules/request/node_modules/tough-cookie/lib/store.js deleted file mode 100644 index bce52925..00000000 --- a/node_modules/request/node_modules/tough-cookie/lib/store.js +++ /dev/null @@ -1,71 +0,0 @@ -/*! - * Copyright (c) 2015, Salesforce.com, Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. Neither the name of Salesforce.com nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -'use strict'; -/*jshint unused:false */ - -function Store() { -} -exports.Store = Store; - -// Stores may be synchronous, but are still required to use a -// Continuation-Passing Style API. The CookieJar itself will expose a "*Sync" -// API that converts from synchronous-callbacks to imperative style. -Store.prototype.synchronous = false; - -Store.prototype.findCookie = function(domain, path, key, cb) { - throw new Error('findCookie is not implemented'); -}; - -Store.prototype.findCookies = function(domain, path, cb) { - throw new Error('findCookies is not implemented'); -}; - -Store.prototype.putCookie = function(cookie, cb) { - throw new Error('putCookie is not implemented'); -}; - -Store.prototype.updateCookie = function(oldCookie, newCookie, cb) { - // recommended default implementation: - // return this.putCookie(newCookie, cb); - throw new Error('updateCookie is not implemented'); -}; - -Store.prototype.removeCookie = function(domain, path, key, cb) { - throw new Error('removeCookie is not implemented'); -}; - -Store.prototype.removeCookies = function(domain, path, cb) { - throw new Error('removeCookies is not implemented'); -}; - -Store.prototype.getAllCookies = function(cb) { - throw new Error('getAllCookies is not implemented (therefore jar cannot be serialized)'); -}; diff --git a/node_modules/request/node_modules/tough-cookie/package.json b/node_modules/request/node_modules/tough-cookie/package.json deleted file mode 100644 index cc1aebfc..00000000 --- a/node_modules/request/node_modules/tough-cookie/package.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "author": { - "name": "Jeremy Stashewsky", - "email": "jstashewsky@salesforce.com" - }, - "contributors": [ - { - "name": "Alexander Savin" - }, - { - "name": "Ian Livingstone" - }, - { - "name": "Ivan Nikulin" - }, - { - "name": "Lalit Kapoor" - }, - { - "name": "Sam Thompson" - }, - { - "name": "Sebastian Mayr" - } - ], - "license": "BSD-3-Clause", - "name": "tough-cookie", - "description": "RFC6265 Cookies and Cookie Jar for node.js", - "keywords": [ - "HTTP", - "cookie", - "cookies", - "set-cookie", - "cookiejar", - "jar", - "RFC6265", - "RFC2965" - ], - "version": "2.2.2", - "homepage": "https://github.com/SalesforceEng/tough-cookie", - "repository": { - "type": "git", - "url": "git://github.com/SalesforceEng/tough-cookie.git" - }, - "bugs": { - "url": "https://github.com/SalesforceEng/tough-cookie/issues" - }, - "main": "./lib/cookie", - "files": [ - "lib" - ], - "scripts": { - "suffixup": "curl -o public_suffix_list.dat https://publicsuffix.org/list/public_suffix_list.dat && ./generate-pubsuffix.js", - "test": "vows test/*_test.js" - }, - "engines": { - "node": ">=0.10.0" - }, - "devDependencies": { - "async": "^1.4.2", - "vows": "^0.8.1" - }, - "gitHead": "cc46628c4d7d2e8c372ecba29293ca8a207ec192", - "_id": "tough-cookie@2.2.2", - "_shasum": "c83a1830f4e5ef0b93ef2a3488e724f8de016ac7", - "_from": "tough-cookie@>=2.2.0 <2.3.0", - "_npmVersion": "3.3.12", - "_nodeVersion": "5.1.1", - "_npmUser": { - "name": "jstash", - "email": "jstash@gmail.com" - }, - "dist": { - "shasum": "c83a1830f4e5ef0b93ef2a3488e724f8de016ac7", - "tarball": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" - }, - "maintainers": [ - { - "name": "jstash", - "email": "jeremy@goinstant.com" - }, - { - "name": "goinstant", - "email": "services@goinstant.com" - } - ], - "_npmOperationalInternal": { - "host": "packages-13-west.internal.npmjs.com", - "tmp": "tmp/tough-cookie-2.2.2.tgz_1457564639182_0.5129188685677946" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/node_modules/tunnel-agent/LICENSE b/node_modules/request/node_modules/tunnel-agent/LICENSE deleted file mode 100644 index a4a9aee0..00000000 --- a/node_modules/request/node_modules/tunnel-agent/LICENSE +++ /dev/null @@ -1,55 +0,0 @@ -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and - -You must cause any modified files to carry prominent notices stating that You changed the files; and - -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/node_modules/request/node_modules/tunnel-agent/README.md b/node_modules/request/node_modules/tunnel-agent/README.md deleted file mode 100644 index bb533d56..00000000 --- a/node_modules/request/node_modules/tunnel-agent/README.md +++ /dev/null @@ -1,4 +0,0 @@ -tunnel-agent -============ - -HTTP proxy tunneling agent. Formerly part of mikeal/request, now a standalone module. diff --git a/node_modules/request/node_modules/tunnel-agent/index.js b/node_modules/request/node_modules/tunnel-agent/index.js deleted file mode 100644 index 68013ac1..00000000 --- a/node_modules/request/node_modules/tunnel-agent/index.js +++ /dev/null @@ -1,243 +0,0 @@ -'use strict' - -var net = require('net') - , tls = require('tls') - , http = require('http') - , https = require('https') - , events = require('events') - , assert = require('assert') - , util = require('util') - ; - -exports.httpOverHttp = httpOverHttp -exports.httpsOverHttp = httpsOverHttp -exports.httpOverHttps = httpOverHttps -exports.httpsOverHttps = httpsOverHttps - - -function httpOverHttp(options) { - var agent = new TunnelingAgent(options) - agent.request = http.request - return agent -} - -function httpsOverHttp(options) { - var agent = new TunnelingAgent(options) - agent.request = http.request - agent.createSocket = createSecureSocket - agent.defaultPort = 443 - return agent -} - -function httpOverHttps(options) { - var agent = new TunnelingAgent(options) - agent.request = https.request - return agent -} - -function httpsOverHttps(options) { - var agent = new TunnelingAgent(options) - agent.request = https.request - agent.createSocket = createSecureSocket - agent.defaultPort = 443 - return agent -} - - -function TunnelingAgent(options) { - var self = this - self.options = options || {} - self.proxyOptions = self.options.proxy || {} - self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets - self.requests = [] - self.sockets = [] - - self.on('free', function onFree(socket, host, port) { - for (var i = 0, len = self.requests.length; i < len; ++i) { - var pending = self.requests[i] - if (pending.host === host && pending.port === port) { - // Detect the request to connect same origin server, - // reuse the connection. - self.requests.splice(i, 1) - pending.request.onSocket(socket) - return - } - } - socket.destroy() - self.removeSocket(socket) - }) -} -util.inherits(TunnelingAgent, events.EventEmitter) - -TunnelingAgent.prototype.addRequest = function addRequest(req, options) { - var self = this - - // Legacy API: addRequest(req, host, port, path) - if (typeof options === 'string') { - options = { - host: options, - port: arguments[2], - path: arguments[3] - }; - } - - if (self.sockets.length >= this.maxSockets) { - // We are over limit so we'll add it to the queue. - self.requests.push({host: options.host, port: options.port, request: req}) - return - } - - // If we are under maxSockets create a new one. - self.createConnection({host: options.host, port: options.port, request: req}) -} - -TunnelingAgent.prototype.createConnection = function createConnection(pending) { - var self = this - - self.createSocket(pending, function(socket) { - socket.on('free', onFree) - socket.on('close', onCloseOrRemove) - socket.on('agentRemove', onCloseOrRemove) - pending.request.onSocket(socket) - - function onFree() { - self.emit('free', socket, pending.host, pending.port) - } - - function onCloseOrRemove(err) { - self.removeSocket(socket) - socket.removeListener('free', onFree) - socket.removeListener('close', onCloseOrRemove) - socket.removeListener('agentRemove', onCloseOrRemove) - } - }) -} - -TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { - var self = this - var placeholder = {} - self.sockets.push(placeholder) - - var connectOptions = mergeOptions({}, self.proxyOptions, - { method: 'CONNECT' - , path: options.host + ':' + options.port - , agent: false - } - ) - if (connectOptions.proxyAuth) { - connectOptions.headers = connectOptions.headers || {} - connectOptions.headers['Proxy-Authorization'] = 'Basic ' + - new Buffer(connectOptions.proxyAuth).toString('base64') - } - - debug('making CONNECT request') - var connectReq = self.request(connectOptions) - connectReq.useChunkedEncodingByDefault = false // for v0.6 - connectReq.once('response', onResponse) // for v0.6 - connectReq.once('upgrade', onUpgrade) // for v0.6 - connectReq.once('connect', onConnect) // for v0.7 or later - connectReq.once('error', onError) - connectReq.end() - - function onResponse(res) { - // Very hacky. This is necessary to avoid http-parser leaks. - res.upgrade = true - } - - function onUpgrade(res, socket, head) { - // Hacky. - process.nextTick(function() { - onConnect(res, socket, head) - }) - } - - function onConnect(res, socket, head) { - connectReq.removeAllListeners() - socket.removeAllListeners() - - if (res.statusCode === 200) { - assert.equal(head.length, 0) - debug('tunneling connection has established') - self.sockets[self.sockets.indexOf(placeholder)] = socket - cb(socket) - } else { - debug('tunneling socket could not be established, statusCode=%d', res.statusCode) - var error = new Error('tunneling socket could not be established, ' + 'statusCode=' + res.statusCode) - error.code = 'ECONNRESET' - options.request.emit('error', error) - self.removeSocket(placeholder) - } - } - - function onError(cause) { - connectReq.removeAllListeners() - - debug('tunneling socket could not be established, cause=%s\n', cause.message, cause.stack) - var error = new Error('tunneling socket could not be established, ' + 'cause=' + cause.message) - error.code = 'ECONNRESET' - options.request.emit('error', error) - self.removeSocket(placeholder) - } -} - -TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { - var pos = this.sockets.indexOf(socket) - if (pos === -1) return - - this.sockets.splice(pos, 1) - - var pending = this.requests.shift() - if (pending) { - // If we have pending requests and a socket gets closed a new one - // needs to be created to take over in the pool for the one that closed. - this.createConnection(pending) - } -} - -function createSecureSocket(options, cb) { - var self = this - TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { - // 0 is dummy port for v0.6 - var secureSocket = tls.connect(0, mergeOptions({}, self.options, - { servername: options.host - , socket: socket - } - )) - self.sockets[self.sockets.indexOf(socket)] = secureSocket - cb(secureSocket) - }) -} - - -function mergeOptions(target) { - for (var i = 1, len = arguments.length; i < len; ++i) { - var overrides = arguments[i] - if (typeof overrides === 'object') { - var keys = Object.keys(overrides) - for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { - var k = keys[j] - if (overrides[k] !== undefined) { - target[k] = overrides[k] - } - } - } - } - return target -} - - -var debug -if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { - debug = function() { - var args = Array.prototype.slice.call(arguments) - if (typeof args[0] === 'string') { - args[0] = 'TUNNEL: ' + args[0] - } else { - args.unshift('TUNNEL:') - } - console.error.apply(console, args) - } -} else { - debug = function() {} -} -exports.debug = debug // for test diff --git a/node_modules/request/node_modules/tunnel-agent/package.json b/node_modules/request/node_modules/tunnel-agent/package.json deleted file mode 100644 index 6677690f..00000000 --- a/node_modules/request/node_modules/tunnel-agent/package.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "author": { - "name": "Mikeal Rogers", - "email": "mikeal.rogers@gmail.com", - "url": "http://www.futurealoof.com" - }, - "name": "tunnel-agent", - "license": "Apache-2.0", - "description": "HTTP proxy tunneling agent. Formerly part of mikeal/request, now a standalone module.", - "version": "0.4.3", - "repository": { - "url": "git+https://github.com/mikeal/tunnel-agent.git" - }, - "main": "index.js", - "files": [ - "index.js" - ], - "dependencies": {}, - "devDependencies": {}, - "optionalDependencies": {}, - "engines": { - "node": "*" - }, - "gitHead": "e72d830f5ed388a2a71d37ce062c38e3fb34bdde", - "bugs": { - "url": "https://github.com/mikeal/tunnel-agent/issues" - }, - "homepage": "https://github.com/mikeal/tunnel-agent#readme", - "_id": "tunnel-agent@0.4.3", - "scripts": {}, - "_shasum": "6373db76909fe570e08d73583365ed828a74eeeb", - "_from": "tunnel-agent@>=0.4.1 <0.5.0", - "_npmVersion": "2.15.3", - "_nodeVersion": "5.9.0", - "_npmUser": { - "name": "simov", - "email": "simeonvelichkov@gmail.com" - }, - "dist": { - "shasum": "6373db76909fe570e08d73583365ed828a74eeeb", - "tarball": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz" - }, - "maintainers": [ - { - "name": "mikeal", - "email": "mikeal.rogers@gmail.com" - }, - { - "name": "nylen", - "email": "jnylen@gmail.com" - }, - { - "name": "fredkschott", - "email": "fkschott@gmail.com" - }, - { - "name": "simov", - "email": "simeonvelichkov@gmail.com" - } - ], - "_npmOperationalInternal": { - "host": "packages-16-east.internal.npmjs.com", - "tmp": "tmp/tunnel-agent-0.4.3.tgz_1462396470295_0.23639482469297945" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/package.json b/node_modules/request/package.json deleted file mode 100644 index 8d61e1ee..00000000 --- a/node_modules/request/package.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "name": "request", - "description": "Simplified HTTP request client.", - "tags": [ - "http", - "simple", - "util", - "utility" - ], - "version": "2.72.0", - "author": { - "name": "Mikeal Rogers", - "email": "mikeal.rogers@gmail.com" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/request/request.git" - }, - "bugs": { - "url": "http://github.com/request/request/issues" - }, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - }, - "main": "index.js", - "dependencies": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "bl": "~1.1.2", - "caseless": "~0.11.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", - "forever-agent": "~0.6.1", - "form-data": "~1.0.0-rc3", - "har-validator": "~2.0.6", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "node-uuid": "~1.4.7", - "oauth-sign": "~0.8.1", - "qs": "~6.1.0", - "stringstream": "~0.0.4", - "tough-cookie": "~2.2.0", - "tunnel-agent": "~0.4.1" - }, - "scripts": { - "test": "npm run lint && npm run test-ci && npm run test-browser", - "test-ci": "taper tests/test-*.js", - "test-cov": "istanbul cover tape tests/test-*.js", - "test-browser": "node tests/browser/start.js", - "lint": "eslint lib/ *.js tests/ && echo Lint passed." - }, - "devDependencies": { - "bluebird": "^3.2.1", - "browserify": "^12.0.2", - "browserify-istanbul": "^2.0.0", - "buffer-equal": "^1.0.0", - "codecov.io": "^0.1.6", - "coveralls": "^2.11.4", - "eslint": "^2.5.3", - "function-bind": "^1.0.2", - "istanbul": "^0.4.0", - "karma": "^0.13.10", - "karma-browserify": "^5.0.1", - "karma-cli": "^0.1.1", - "karma-coverage": "^0.5.3", - "karma-phantomjs-launcher": "^1.0.0", - "karma-tap": "^1.0.3", - "phantomjs-prebuilt": "^2.1.3", - "rimraf": "^2.2.8", - "server-destroy": "^1.0.1", - "tape": "^4.2.0", - "taper": "^0.4.0" - }, - "gitHead": "6dcac13642955577592fdafb5ff3cdc8a6ff1b1b", - "homepage": "https://github.com/request/request#readme", - "_id": "request@2.72.0", - "_shasum": "0ce3a179512620b10441f14c82e21c12c0ddb4e1", - "_from": "request@latest", - "_npmVersion": "3.8.5", - "_nodeVersion": "5.9.0", - "_npmUser": { - "name": "simov", - "email": "simeonvelichkov@gmail.com" - }, - "dist": { - "shasum": "0ce3a179512620b10441f14c82e21c12c0ddb4e1", - "tarball": "https://registry.npmjs.org/request/-/request-2.72.0.tgz" - }, - "maintainers": [ - { - "name": "mikeal", - "email": "mikeal.rogers@gmail.com" - }, - { - "name": "nylen", - "email": "jnylen@gmail.com" - }, - { - "name": "fredkschott", - "email": "fkschott@gmail.com" - }, - { - "name": "simov", - "email": "simeonvelichkov@gmail.com" - } - ], - "_npmOperationalInternal": { - "host": "packages-12-west.internal.npmjs.com", - "tmp": "tmp/request-2.72.0.tgz_1460901215210_0.9173020373564214" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/request/-/request-2.72.0.tgz", - "readme": "ERROR: No README data found!" -} diff --git a/node_modules/request/request.js b/node_modules/request/request.js deleted file mode 100644 index 124157e8..00000000 --- a/node_modules/request/request.js +++ /dev/null @@ -1,1458 +0,0 @@ -'use strict' - -var http = require('http') - , https = require('https') - , url = require('url') - , util = require('util') - , stream = require('stream') - , zlib = require('zlib') - , bl = require('bl') - , hawk = require('hawk') - , aws2 = require('aws-sign2') - , httpSignature = require('http-signature') - , mime = require('mime-types') - , stringstream = require('stringstream') - , caseless = require('caseless') - , ForeverAgent = require('forever-agent') - , FormData = require('form-data') - , extend = require('extend') - , isstream = require('isstream') - , isTypedArray = require('is-typedarray').strict - , helpers = require('./lib/helpers') - , cookies = require('./lib/cookies') - , getProxyFromURI = require('./lib/getProxyFromURI') - , Querystring = require('./lib/querystring').Querystring - , Har = require('./lib/har').Har - , Auth = require('./lib/auth').Auth - , OAuth = require('./lib/oauth').OAuth - , Multipart = require('./lib/multipart').Multipart - , Redirect = require('./lib/redirect').Redirect - , Tunnel = require('./lib/tunnel').Tunnel - -var safeStringify = helpers.safeStringify - , isReadStream = helpers.isReadStream - , toBase64 = helpers.toBase64 - , defer = helpers.defer - , copy = helpers.copy - , version = helpers.version - , globalCookieJar = cookies.jar() - - -var globalPool = {} - -function filterForNonReserved(reserved, options) { - // Filter out properties that are not reserved. - // Reserved values are passed in at call site. - - var object = {} - for (var i in options) { - var notReserved = (reserved.indexOf(i) === -1) - if (notReserved) { - object[i] = options[i] - } - } - return object -} - -function filterOutReservedFunctions(reserved, options) { - // Filter out properties that are functions and are reserved. - // Reserved values are passed in at call site. - - var object = {} - for (var i in options) { - var isReserved = !(reserved.indexOf(i) === -1) - var isFunction = (typeof options[i] === 'function') - if (!(isReserved && isFunction)) { - object[i] = options[i] - } - } - return object - -} - -// Function for properly handling a connection error -function connectionErrorHandler(error) { - var socket = this - if (socket.res) { - if (socket.res.request) { - socket.res.request.emit('error', error) - } else { - socket.res.emit('error', error) - } - } else { - socket._httpMessage.emit('error', error) - } -} - -// Return a simpler request object to allow serialization -function requestToJSON() { - var self = this - return { - uri: self.uri, - method: self.method, - headers: self.headers - } -} - -// Return a simpler response object to allow serialization -function responseToJSON() { - var self = this - return { - statusCode: self.statusCode, - body: self.body, - headers: self.headers, - request: requestToJSON.call(self.request) - } -} - -function Request (options) { - // if given the method property in options, set property explicitMethod to true - - // extend the Request instance with any non-reserved properties - // remove any reserved functions from the options object - // set Request instance to be readable and writable - // call init - - var self = this - - // start with HAR, then override with additional options - if (options.har) { - self._har = new Har(self) - options = self._har.options(options) - } - - stream.Stream.call(self) - var reserved = Object.keys(Request.prototype) - var nonReserved = filterForNonReserved(reserved, options) - - extend(self, nonReserved) - options = filterOutReservedFunctions(reserved, options) - - self.readable = true - self.writable = true - if (options.method) { - self.explicitMethod = true - } - self._qs = new Querystring(self) - self._auth = new Auth(self) - self._oauth = new OAuth(self) - self._multipart = new Multipart(self) - self._redirect = new Redirect(self) - self._tunnel = new Tunnel(self) - self.init(options) -} - -util.inherits(Request, stream.Stream) - -// Debugging -Request.debug = process.env.NODE_DEBUG && /\brequest\b/.test(process.env.NODE_DEBUG) -function debug() { - if (Request.debug) { - console.error('REQUEST %s', util.format.apply(util, arguments)) - } -} -Request.prototype.debug = debug - -Request.prototype.init = function (options) { - // init() contains all the code to setup the request object. - // the actual outgoing request is not started until start() is called - // this function is called from both the constructor and on redirect. - var self = this - if (!options) { - options = {} - } - self.headers = self.headers ? copy(self.headers) : {} - - // Delete headers with value undefined since they break - // ClientRequest.OutgoingMessage.setHeader in node 0.12 - for (var headerName in self.headers) { - if (typeof self.headers[headerName] === 'undefined') { - delete self.headers[headerName] - } - } - - caseless.httpify(self, self.headers) - - if (!self.method) { - self.method = options.method || 'GET' - } - if (!self.localAddress) { - self.localAddress = options.localAddress - } - - self._qs.init(options) - - debug(options) - if (!self.pool && self.pool !== false) { - self.pool = globalPool - } - self.dests = self.dests || [] - self.__isRequestRequest = true - - // Protect against double callback - if (!self._callback && self.callback) { - self._callback = self.callback - self.callback = function () { - if (self._callbackCalled) { - return // Print a warning maybe? - } - self._callbackCalled = true - self._callback.apply(self, arguments) - } - self.on('error', self.callback.bind()) - self.on('complete', self.callback.bind(self, null)) - } - - // People use this property instead all the time, so support it - if (!self.uri && self.url) { - self.uri = self.url - delete self.url - } - - // If there's a baseUrl, then use it as the base URL (i.e. uri must be - // specified as a relative path and is appended to baseUrl). - if (self.baseUrl) { - if (typeof self.baseUrl !== 'string') { - return self.emit('error', new Error('options.baseUrl must be a string')) - } - - if (typeof self.uri !== 'string') { - return self.emit('error', new Error('options.uri must be a string when using options.baseUrl')) - } - - if (self.uri.indexOf('//') === 0 || self.uri.indexOf('://') !== -1) { - return self.emit('error', new Error('options.uri must be a path when using options.baseUrl')) - } - - // Handle all cases to make sure that there's only one slash between - // baseUrl and uri. - var baseUrlEndsWithSlash = self.baseUrl.lastIndexOf('/') === self.baseUrl.length - 1 - var uriStartsWithSlash = self.uri.indexOf('/') === 0 - - if (baseUrlEndsWithSlash && uriStartsWithSlash) { - self.uri = self.baseUrl + self.uri.slice(1) - } else if (baseUrlEndsWithSlash || uriStartsWithSlash) { - self.uri = self.baseUrl + self.uri - } else if (self.uri === '') { - self.uri = self.baseUrl - } else { - self.uri = self.baseUrl + '/' + self.uri - } - delete self.baseUrl - } - - // A URI is needed by this point, emit error if we haven't been able to get one - if (!self.uri) { - return self.emit('error', new Error('options.uri is a required argument')) - } - - // If a string URI/URL was given, parse it into a URL object - if (typeof self.uri === 'string') { - self.uri = url.parse(self.uri) - } - - // Some URL objects are not from a URL parsed string and need href added - if (!self.uri.href) { - self.uri.href = url.format(self.uri) - } - - // DEPRECATED: Warning for users of the old Unix Sockets URL Scheme - if (self.uri.protocol === 'unix:') { - return self.emit('error', new Error('`unix://` URL scheme is no longer supported. Please use the format `http://unix:SOCKET:PATH`')) - } - - // Support Unix Sockets - if (self.uri.host === 'unix') { - self.enableUnixSocket() - } - - if (self.strictSSL === false) { - self.rejectUnauthorized = false - } - - if (!self.uri.pathname) {self.uri.pathname = '/'} - - if (!(self.uri.host || (self.uri.hostname && self.uri.port)) && !self.uri.isUnix) { - // Invalid URI: it may generate lot of bad errors, like 'TypeError: Cannot call method `indexOf` of undefined' in CookieJar - // Detect and reject it as soon as possible - var faultyUri = url.format(self.uri) - var message = 'Invalid URI "' + faultyUri + '"' - if (Object.keys(options).length === 0) { - // No option ? This can be the sign of a redirect - // As this is a case where the user cannot do anything (they didn't call request directly with this URL) - // they should be warned that it can be caused by a redirection (can save some hair) - message += '. This can be caused by a crappy redirection.' - } - // This error was fatal - self.abort() - return self.emit('error', new Error(message)) - } - - if (!self.hasOwnProperty('proxy')) { - self.proxy = getProxyFromURI(self.uri) - } - - self.tunnel = self._tunnel.isEnabled() - if (self.proxy) { - self._tunnel.setup(options) - } - - self._redirect.onRequest(options) - - self.setHost = false - if (!self.hasHeader('host')) { - var hostHeaderName = self.originalHostHeaderName || 'host' - self.setHeader(hostHeaderName, self.uri.hostname) - if (self.uri.port) { - if ( !(self.uri.port === 80 && self.uri.protocol === 'http:') && - !(self.uri.port === 443 && self.uri.protocol === 'https:') ) { - self.setHeader(hostHeaderName, self.getHeader('host') + (':' + self.uri.port) ) - } - } - self.setHost = true - } - - self.jar(self._jar || options.jar) - - if (!self.uri.port) { - if (self.uri.protocol === 'http:') {self.uri.port = 80} - else if (self.uri.protocol === 'https:') {self.uri.port = 443} - } - - if (self.proxy && !self.tunnel) { - self.port = self.proxy.port - self.host = self.proxy.hostname - } else { - self.port = self.uri.port - self.host = self.uri.hostname - } - - if (options.form) { - self.form(options.form) - } - - if (options.formData) { - var formData = options.formData - var requestForm = self.form() - var appendFormValue = function (key, value) { - if (value.hasOwnProperty('value') && value.hasOwnProperty('options')) { - requestForm.append(key, value.value, value.options) - } else { - requestForm.append(key, value) - } - } - for (var formKey in formData) { - if (formData.hasOwnProperty(formKey)) { - var formValue = formData[formKey] - if (formValue instanceof Array) { - for (var j = 0; j < formValue.length; j++) { - appendFormValue(formKey, formValue[j]) - } - } else { - appendFormValue(formKey, formValue) - } - } - } - } - - if (options.qs) { - self.qs(options.qs) - } - - if (self.uri.path) { - self.path = self.uri.path - } else { - self.path = self.uri.pathname + (self.uri.search || '') - } - - if (self.path.length === 0) { - self.path = '/' - } - - // Auth must happen last in case signing is dependent on other headers - if (options.aws) { - self.aws(options.aws) - } - - if (options.hawk) { - self.hawk(options.hawk) - } - - if (options.httpSignature) { - self.httpSignature(options.httpSignature) - } - - if (options.auth) { - if (Object.prototype.hasOwnProperty.call(options.auth, 'username')) { - options.auth.user = options.auth.username - } - if (Object.prototype.hasOwnProperty.call(options.auth, 'password')) { - options.auth.pass = options.auth.password - } - - self.auth( - options.auth.user, - options.auth.pass, - options.auth.sendImmediately, - options.auth.bearer - ) - } - - if (self.gzip && !self.hasHeader('accept-encoding')) { - self.setHeader('accept-encoding', 'gzip, deflate') - } - - if (self.uri.auth && !self.hasHeader('authorization')) { - var uriAuthPieces = self.uri.auth.split(':').map(function(item) {return self._qs.unescape(item)}) - self.auth(uriAuthPieces[0], uriAuthPieces.slice(1).join(':'), true) - } - - if (!self.tunnel && self.proxy && self.proxy.auth && !self.hasHeader('proxy-authorization')) { - var proxyAuthPieces = self.proxy.auth.split(':').map(function(item) {return self._qs.unescape(item)}) - var authHeader = 'Basic ' + toBase64(proxyAuthPieces.join(':')) - self.setHeader('proxy-authorization', authHeader) - } - - if (self.proxy && !self.tunnel) { - self.path = (self.uri.protocol + '//' + self.uri.host + self.path) - } - - if (options.json) { - self.json(options.json) - } - if (options.multipart) { - self.multipart(options.multipart) - } - - if (options.time) { - self.timing = true - self.elapsedTime = self.elapsedTime || 0 - } - - function setContentLength () { - if (isTypedArray(self.body)) { - self.body = new Buffer(self.body) - } - - if (!self.hasHeader('content-length')) { - var length - if (typeof self.body === 'string') { - length = Buffer.byteLength(self.body) - } - else if (Array.isArray(self.body)) { - length = self.body.reduce(function (a, b) {return a + b.length}, 0) - } - else { - length = self.body.length - } - - if (length) { - self.setHeader('content-length', length) - } else { - self.emit('error', new Error('Argument error, options.body.')) - } - } - } - if (self.body && !isstream(self.body)) { - setContentLength() - } - - if (options.oauth) { - self.oauth(options.oauth) - } else if (self._oauth.params && self.hasHeader('authorization')) { - self.oauth(self._oauth.params) - } - - var protocol = self.proxy && !self.tunnel ? self.proxy.protocol : self.uri.protocol - , defaultModules = {'http:':http, 'https:':https} - , httpModules = self.httpModules || {} - - self.httpModule = httpModules[protocol] || defaultModules[protocol] - - if (!self.httpModule) { - return self.emit('error', new Error('Invalid protocol: ' + protocol)) - } - - if (options.ca) { - self.ca = options.ca - } - - if (!self.agent) { - if (options.agentOptions) { - self.agentOptions = options.agentOptions - } - - if (options.agentClass) { - self.agentClass = options.agentClass - } else if (options.forever) { - var v = version() - // use ForeverAgent in node 0.10- only - if (v.major === 0 && v.minor <= 10) { - self.agentClass = protocol === 'http:' ? ForeverAgent : ForeverAgent.SSL - } else { - self.agentClass = self.httpModule.Agent - self.agentOptions = self.agentOptions || {} - self.agentOptions.keepAlive = true - } - } else { - self.agentClass = self.httpModule.Agent - } - } - - if (self.pool === false) { - self.agent = false - } else { - self.agent = self.agent || self.getNewAgent() - } - - self.on('pipe', function (src) { - if (self.ntick && self._started) { - self.emit('error', new Error('You cannot pipe to this stream after the outbound request has started.')) - } - self.src = src - if (isReadStream(src)) { - if (!self.hasHeader('content-type')) { - self.setHeader('content-type', mime.lookup(src.path)) - } - } else { - if (src.headers) { - for (var i in src.headers) { - if (!self.hasHeader(i)) { - self.setHeader(i, src.headers[i]) - } - } - } - if (self._json && !self.hasHeader('content-type')) { - self.setHeader('content-type', 'application/json') - } - if (src.method && !self.explicitMethod) { - self.method = src.method - } - } - - // self.on('pipe', function () { - // console.error('You have already piped to this stream. Pipeing twice is likely to break the request.') - // }) - }) - - defer(function () { - if (self._aborted) { - return - } - - var end = function () { - if (self._form) { - if (!self._auth.hasAuth) { - self._form.pipe(self) - } - else if (self._auth.hasAuth && self._auth.sentAuth) { - self._form.pipe(self) - } - } - if (self._multipart && self._multipart.chunked) { - self._multipart.body.pipe(self) - } - if (self.body) { - if (isstream(self.body)) { - self.body.pipe(self) - } else { - setContentLength() - if (Array.isArray(self.body)) { - self.body.forEach(function (part) { - self.write(part) - }) - } else { - self.write(self.body) - } - self.end() - } - } else if (self.requestBodyStream) { - console.warn('options.requestBodyStream is deprecated, please pass the request object to stream.pipe.') - self.requestBodyStream.pipe(self) - } else if (!self.src) { - if (self._auth.hasAuth && !self._auth.sentAuth) { - self.end() - return - } - if (self.method !== 'GET' && typeof self.method !== 'undefined') { - self.setHeader('content-length', 0) - } - self.end() - } - } - - if (self._form && !self.hasHeader('content-length')) { - // Before ending the request, we had to compute the length of the whole form, asyncly - self.setHeader(self._form.getHeaders(), true) - self._form.getLength(function (err, length) { - if (!err && !isNaN(length)) { - self.setHeader('content-length', length) - } - end() - }) - } else { - end() - } - - self.ntick = true - }) - -} - -Request.prototype.getNewAgent = function () { - var self = this - var Agent = self.agentClass - var options = {} - if (self.agentOptions) { - for (var i in self.agentOptions) { - options[i] = self.agentOptions[i] - } - } - if (self.ca) { - options.ca = self.ca - } - if (self.ciphers) { - options.ciphers = self.ciphers - } - if (self.secureProtocol) { - options.secureProtocol = self.secureProtocol - } - if (self.secureOptions) { - options.secureOptions = self.secureOptions - } - if (typeof self.rejectUnauthorized !== 'undefined') { - options.rejectUnauthorized = self.rejectUnauthorized - } - - if (self.cert && self.key) { - options.key = self.key - options.cert = self.cert - } - - if (self.pfx) { - options.pfx = self.pfx - } - - if (self.passphrase) { - options.passphrase = self.passphrase - } - - var poolKey = '' - - // different types of agents are in different pools - if (Agent !== self.httpModule.Agent) { - poolKey += Agent.name - } - - // ca option is only relevant if proxy or destination are https - var proxy = self.proxy - if (typeof proxy === 'string') { - proxy = url.parse(proxy) - } - var isHttps = (proxy && proxy.protocol === 'https:') || this.uri.protocol === 'https:' - - if (isHttps) { - if (options.ca) { - if (poolKey) { - poolKey += ':' - } - poolKey += options.ca - } - - if (typeof options.rejectUnauthorized !== 'undefined') { - if (poolKey) { - poolKey += ':' - } - poolKey += options.rejectUnauthorized - } - - if (options.cert) { - if (poolKey) { - poolKey += ':' - } - poolKey += options.cert.toString('ascii') + options.key.toString('ascii') - } - - if (options.pfx) { - if (poolKey) { - poolKey += ':' - } - poolKey += options.pfx.toString('ascii') - } - - if (options.ciphers) { - if (poolKey) { - poolKey += ':' - } - poolKey += options.ciphers - } - - if (options.secureProtocol) { - if (poolKey) { - poolKey += ':' - } - poolKey += options.secureProtocol - } - - if (options.secureOptions) { - if (poolKey) { - poolKey += ':' - } - poolKey += options.secureOptions - } - } - - if (self.pool === globalPool && !poolKey && Object.keys(options).length === 0 && self.httpModule.globalAgent) { - // not doing anything special. Use the globalAgent - return self.httpModule.globalAgent - } - - // we're using a stored agent. Make sure it's protocol-specific - poolKey = self.uri.protocol + poolKey - - // generate a new agent for this setting if none yet exists - if (!self.pool[poolKey]) { - self.pool[poolKey] = new Agent(options) - // properly set maxSockets on new agents - if (self.pool.maxSockets) { - self.pool[poolKey].maxSockets = self.pool.maxSockets - } - } - - return self.pool[poolKey] -} - -Request.prototype.start = function () { - // start() is called once we are ready to send the outgoing HTTP request. - // this is usually called on the first write(), end() or on nextTick() - var self = this - - if (self._aborted) { - return - } - - self._started = true - self.method = self.method || 'GET' - self.href = self.uri.href - - if (self.src && self.src.stat && self.src.stat.size && !self.hasHeader('content-length')) { - self.setHeader('content-length', self.src.stat.size) - } - if (self._aws) { - self.aws(self._aws, true) - } - - // We have a method named auth, which is completely different from the http.request - // auth option. If we don't remove it, we're gonna have a bad time. - var reqOptions = copy(self) - delete reqOptions.auth - - debug('make request', self.uri.href) - - try { - self.req = self.httpModule.request(reqOptions) - } catch (err) { - self.emit('error', err) - return - } - - if (self.timing) { - self.startTime = new Date().getTime() - } - - if (self.timeout && !self.timeoutTimer) { - var timeout = self.timeout < 0 ? 0 : self.timeout - // Set a timeout in memory - this block will throw if the server takes more - // than `timeout` to write the HTTP status and headers (corresponding to - // the on('response') event on the client). NB: this measures wall-clock - // time, not the time between bytes sent by the server. - self.timeoutTimer = setTimeout(function () { - var connectTimeout = self.req.socket && self.req.socket.readable === false - self.abort() - var e = new Error('ETIMEDOUT') - e.code = 'ETIMEDOUT' - e.connect = connectTimeout - self.emit('error', e) - }, timeout) - - if (self.req.setTimeout) { // only works on node 0.6+ - // Set an additional timeout on the socket, via the `setsockopt` syscall. - // This timeout sets the amount of time to wait *between* bytes sent - // from the server, and may or may not correspond to the wall-clock time - // elapsed from the start of the request. - // - // In particular, it's useful for erroring if the server fails to send - // data halfway through streaming a response. - self.req.setTimeout(timeout, function () { - if (self.req) { - self.req.abort() - var e = new Error('ESOCKETTIMEDOUT') - e.code = 'ESOCKETTIMEDOUT' - e.connect = false - self.emit('error', e) - } - }) - } - } - - self.req.on('response', self.onRequestResponse.bind(self)) - self.req.on('error', self.onRequestError.bind(self)) - self.req.on('drain', function() { - self.emit('drain') - }) - self.req.on('socket', function(socket) { - self.emit('socket', socket) - }) - - self.on('end', function() { - if ( self.req.connection ) { - self.req.connection.removeListener('error', connectionErrorHandler) - } - }) - self.emit('request', self.req) -} - -Request.prototype.onRequestError = function (error) { - var self = this - if (self._aborted) { - return - } - if (self.req && self.req._reusedSocket && error.code === 'ECONNRESET' - && self.agent.addRequestNoreuse) { - self.agent = { addRequest: self.agent.addRequestNoreuse.bind(self.agent) } - self.start() - self.req.end() - return - } - if (self.timeout && self.timeoutTimer) { - clearTimeout(self.timeoutTimer) - self.timeoutTimer = null - } - self.emit('error', error) -} - -Request.prototype.onRequestResponse = function (response) { - var self = this - debug('onRequestResponse', self.uri.href, response.statusCode, response.headers) - response.on('end', function() { - if (self.timing) { - self.elapsedTime += (new Date().getTime() - self.startTime) - debug('elapsed time', self.elapsedTime) - response.elapsedTime = self.elapsedTime - } - debug('response end', self.uri.href, response.statusCode, response.headers) - }) - - // The check on response.connection is a workaround for browserify. - if (response.connection && response.connection.listeners('error').indexOf(connectionErrorHandler) === -1) { - response.connection.setMaxListeners(0) - response.connection.once('error', connectionErrorHandler) - } - if (self._aborted) { - debug('aborted', self.uri.href) - response.resume() - return - } - - self.response = response - response.request = self - response.toJSON = responseToJSON - - // XXX This is different on 0.10, because SSL is strict by default - if (self.httpModule === https && - self.strictSSL && (!response.hasOwnProperty('socket') || - !response.socket.authorized)) { - debug('strict ssl error', self.uri.href) - var sslErr = response.hasOwnProperty('socket') ? response.socket.authorizationError : self.uri.href + ' does not support SSL' - self.emit('error', new Error('SSL Error: ' + sslErr)) - return - } - - // Save the original host before any redirect (if it changes, we need to - // remove any authorization headers). Also remember the case of the header - // name because lots of broken servers expect Host instead of host and we - // want the caller to be able to specify this. - self.originalHost = self.getHeader('host') - if (!self.originalHostHeaderName) { - self.originalHostHeaderName = self.hasHeader('host') - } - if (self.setHost) { - self.removeHeader('host') - } - if (self.timeout && self.timeoutTimer) { - clearTimeout(self.timeoutTimer) - self.timeoutTimer = null - } - - var targetCookieJar = (self._jar && self._jar.setCookie) ? self._jar : globalCookieJar - var addCookie = function (cookie) { - //set the cookie if it's domain in the href's domain. - try { - targetCookieJar.setCookie(cookie, self.uri.href, {ignoreError: true}) - } catch (e) { - self.emit('error', e) - } - } - - response.caseless = caseless(response.headers) - - if (response.caseless.has('set-cookie') && (!self._disableCookies)) { - var headerName = response.caseless.has('set-cookie') - if (Array.isArray(response.headers[headerName])) { - response.headers[headerName].forEach(addCookie) - } else { - addCookie(response.headers[headerName]) - } - } - - if (self._redirect.onResponse(response)) { - return // Ignore the rest of the response - } else { - // Be a good stream and emit end when the response is finished. - // Hack to emit end on close because of a core bug that never fires end - response.on('close', function () { - if (!self._ended) { - self.response.emit('end') - } - }) - - response.on('end', function () { - self._ended = true - }) - - var noBody = function (code) { - return ( - self.method === 'HEAD' - // Informational - || (code >= 100 && code < 200) - // No Content - || code === 204 - // Not Modified - || code === 304 - ) - } - - var responseContent - if (self.gzip && !noBody(response.statusCode)) { - var contentEncoding = response.headers['content-encoding'] || 'identity' - contentEncoding = contentEncoding.trim().toLowerCase() - - if (contentEncoding === 'gzip') { - responseContent = zlib.createGunzip() - response.pipe(responseContent) - } else if (contentEncoding === 'deflate') { - responseContent = zlib.createInflate() - response.pipe(responseContent) - } else { - // Since previous versions didn't check for Content-Encoding header, - // ignore any invalid values to preserve backwards-compatibility - if (contentEncoding !== 'identity') { - debug('ignoring unrecognized Content-Encoding ' + contentEncoding) - } - responseContent = response - } - } else { - responseContent = response - } - - if (self.encoding) { - if (self.dests.length !== 0) { - console.error('Ignoring encoding parameter as this stream is being piped to another stream which makes the encoding option invalid.') - } else if (responseContent.setEncoding) { - responseContent.setEncoding(self.encoding) - } else { - // Should only occur on node pre-v0.9.4 (joyent/node@9b5abe5) with - // zlib streams. - // If/When support for 0.9.4 is dropped, this should be unnecessary. - responseContent = responseContent.pipe(stringstream(self.encoding)) - } - } - - if (self._paused) { - responseContent.pause() - } - - self.responseContent = responseContent - - self.emit('response', response) - - self.dests.forEach(function (dest) { - self.pipeDest(dest) - }) - - responseContent.on('data', function (chunk) { - self._destdata = true - self.emit('data', chunk) - }) - responseContent.on('end', function (chunk) { - self.emit('end', chunk) - }) - responseContent.on('error', function (error) { - self.emit('error', error) - }) - responseContent.on('close', function () {self.emit('close')}) - - if (self.callback) { - self.readResponseBody(response) - } - //if no callback - else { - self.on('end', function () { - if (self._aborted) { - debug('aborted', self.uri.href) - return - } - self.emit('complete', response) - }) - } - } - debug('finish init function', self.uri.href) -} - -Request.prototype.readResponseBody = function (response) { - var self = this - debug('reading response\'s body') - var buffer = bl() - , strings = [] - - self.on('data', function (chunk) { - if (Buffer.isBuffer(chunk)) { - buffer.append(chunk) - } else { - strings.push(chunk) - } - }) - self.on('end', function () { - debug('end event', self.uri.href) - if (self._aborted) { - debug('aborted', self.uri.href) - // `buffer` is defined in the parent scope and used in a closure it exists for the life of the request. - // This can lead to leaky behavior if the user retains a reference to the request object. - buffer.destroy() - return - } - - if (buffer.length) { - debug('has body', self.uri.href, buffer.length) - if (self.encoding === null) { - // response.body = buffer - // can't move to this until https://github.com/rvagg/bl/issues/13 - response.body = buffer.slice() - } else { - response.body = buffer.toString(self.encoding) - } - // `buffer` is defined in the parent scope and used in a closure it exists for the life of the Request. - // This can lead to leaky behavior if the user retains a reference to the request object. - buffer.destroy() - } else if (strings.length) { - // The UTF8 BOM [0xEF,0xBB,0xBF] is converted to [0xFE,0xFF] in the JS UTC16/UCS2 representation. - // Strip this value out when the encoding is set to 'utf8', as upstream consumers won't expect it and it breaks JSON.parse(). - if (self.encoding === 'utf8' && strings[0].length > 0 && strings[0][0] === '\uFEFF') { - strings[0] = strings[0].substring(1) - } - response.body = strings.join('') - } - - if (self._json) { - try { - response.body = JSON.parse(response.body, self._jsonReviver) - } catch (e) { - debug('invalid JSON received', self.uri.href) - } - } - debug('emitting complete', self.uri.href) - if (typeof response.body === 'undefined' && !self._json) { - response.body = self.encoding === null ? new Buffer(0) : '' - } - self.emit('complete', response, response.body) - }) -} - -Request.prototype.abort = function () { - var self = this - self._aborted = true - - if (self.req) { - self.req.abort() - } - else if (self.response) { - self.response.destroy() - } - - self.emit('abort') -} - -Request.prototype.pipeDest = function (dest) { - var self = this - var response = self.response - // Called after the response is received - if (dest.headers && !dest.headersSent) { - if (response.caseless.has('content-type')) { - var ctname = response.caseless.has('content-type') - if (dest.setHeader) { - dest.setHeader(ctname, response.headers[ctname]) - } - else { - dest.headers[ctname] = response.headers[ctname] - } - } - - if (response.caseless.has('content-length')) { - var clname = response.caseless.has('content-length') - if (dest.setHeader) { - dest.setHeader(clname, response.headers[clname]) - } else { - dest.headers[clname] = response.headers[clname] - } - } - } - if (dest.setHeader && !dest.headersSent) { - for (var i in response.headers) { - // If the response content is being decoded, the Content-Encoding header - // of the response doesn't represent the piped content, so don't pass it. - if (!self.gzip || i !== 'content-encoding') { - dest.setHeader(i, response.headers[i]) - } - } - dest.statusCode = response.statusCode - } - if (self.pipefilter) { - self.pipefilter(response, dest) - } -} - -Request.prototype.qs = function (q, clobber) { - var self = this - var base - if (!clobber && self.uri.query) { - base = self._qs.parse(self.uri.query) - } else { - base = {} - } - - for (var i in q) { - base[i] = q[i] - } - - var qs = self._qs.stringify(base) - - if (qs === '') { - return self - } - - self.uri = url.parse(self.uri.href.split('?')[0] + '?' + qs) - self.url = self.uri - self.path = self.uri.path - - if (self.uri.host === 'unix') { - self.enableUnixSocket() - } - - return self -} -Request.prototype.form = function (form) { - var self = this - if (form) { - if (!/^application\/x-www-form-urlencoded\b/.test(self.getHeader('content-type'))) { - self.setHeader('content-type', 'application/x-www-form-urlencoded') - } - self.body = (typeof form === 'string') - ? self._qs.rfc3986(form.toString('utf8')) - : self._qs.stringify(form).toString('utf8') - return self - } - // create form-data object - self._form = new FormData() - self._form.on('error', function(err) { - err.message = 'form-data: ' + err.message - self.emit('error', err) - self.abort() - }) - return self._form -} -Request.prototype.multipart = function (multipart) { - var self = this - - self._multipart.onRequest(multipart) - - if (!self._multipart.chunked) { - self.body = self._multipart.body - } - - return self -} -Request.prototype.json = function (val) { - var self = this - - if (!self.hasHeader('accept')) { - self.setHeader('accept', 'application/json') - } - - if (typeof self.jsonReplacer === 'function') { - self._jsonReplacer = self.jsonReplacer - } - - self._json = true - if (typeof val === 'boolean') { - if (self.body !== undefined) { - if (!/^application\/x-www-form-urlencoded\b/.test(self.getHeader('content-type'))) { - self.body = safeStringify(self.body, self._jsonReplacer) - } else { - self.body = self._qs.rfc3986(self.body) - } - if (!self.hasHeader('content-type')) { - self.setHeader('content-type', 'application/json') - } - } - } else { - self.body = safeStringify(val, self._jsonReplacer) - if (!self.hasHeader('content-type')) { - self.setHeader('content-type', 'application/json') - } - } - - if (typeof self.jsonReviver === 'function') { - self._jsonReviver = self.jsonReviver - } - - return self -} -Request.prototype.getHeader = function (name, headers) { - var self = this - var result, re, match - if (!headers) { - headers = self.headers - } - Object.keys(headers).forEach(function (key) { - if (key.length !== name.length) { - return - } - re = new RegExp(name, 'i') - match = key.match(re) - if (match) { - result = headers[key] - } - }) - return result -} -Request.prototype.enableUnixSocket = function () { - // Get the socket & request paths from the URL - var unixParts = this.uri.path.split(':') - , host = unixParts[0] - , path = unixParts[1] - // Apply unix properties to request - this.socketPath = host - this.uri.pathname = path - this.uri.path = path - this.uri.host = host - this.uri.hostname = host - this.uri.isUnix = true -} - - -Request.prototype.auth = function (user, pass, sendImmediately, bearer) { - var self = this - - self._auth.onRequest(user, pass, sendImmediately, bearer) - - return self -} -Request.prototype.aws = function (opts, now) { - var self = this - - if (!now) { - self._aws = opts - return self - } - - if (opts.sign_version == 4 || opts.sign_version == '4') { - var aws4 = require('aws4') - // use aws4 - var options = { - host: self.uri.host, - path: self.uri.path, - method: self.method, - headers: { - 'content-type': self.getHeader('content-type') || '' - }, - body: self.body - } - var signRes = aws4.sign(options, { - accessKeyId: opts.key, - secretAccessKey: opts.secret - }) - self.setHeader('authorization', signRes.headers.Authorization) - self.setHeader('x-amz-date', signRes.headers['X-Amz-Date']) - } - else { - // default: use aws-sign2 - var date = new Date() - self.setHeader('date', date.toUTCString()) - var auth = - { key: opts.key - , secret: opts.secret - , verb: self.method.toUpperCase() - , date: date - , contentType: self.getHeader('content-type') || '' - , md5: self.getHeader('content-md5') || '' - , amazonHeaders: aws2.canonicalizeHeaders(self.headers) - } - var path = self.uri.path - if (opts.bucket && path) { - auth.resource = '/' + opts.bucket + path - } else if (opts.bucket && !path) { - auth.resource = '/' + opts.bucket - } else if (!opts.bucket && path) { - auth.resource = path - } else if (!opts.bucket && !path) { - auth.resource = '/' - } - auth.resource = aws2.canonicalizeResource(auth.resource) - self.setHeader('authorization', aws2.authorization(auth)) - } - - return self -} -Request.prototype.httpSignature = function (opts) { - var self = this - httpSignature.signRequest({ - getHeader: function(header) { - return self.getHeader(header, self.headers) - }, - setHeader: function(header, value) { - self.setHeader(header, value) - }, - method: self.method, - path: self.path - }, opts) - debug('httpSignature authorization', self.getHeader('authorization')) - - return self -} -Request.prototype.hawk = function (opts) { - var self = this - self.setHeader('Authorization', hawk.client.header(self.uri, self.method, opts).field) -} -Request.prototype.oauth = function (_oauth) { - var self = this - - self._oauth.onRequest(_oauth) - - return self -} - -Request.prototype.jar = function (jar) { - var self = this - var cookies - - if (self._redirect.redirectsFollowed === 0) { - self.originalCookieHeader = self.getHeader('cookie') - } - - if (!jar) { - // disable cookies - cookies = false - self._disableCookies = true - } else { - var targetCookieJar = (jar && jar.getCookieString) ? jar : globalCookieJar - var urihref = self.uri.href - //fetch cookie in the Specified host - if (targetCookieJar) { - cookies = targetCookieJar.getCookieString(urihref) - } - } - - //if need cookie and cookie is not empty - if (cookies && cookies.length) { - if (self.originalCookieHeader) { - // Don't overwrite existing Cookie header - self.setHeader('cookie', self.originalCookieHeader + '; ' + cookies) - } else { - self.setHeader('cookie', cookies) - } - } - self._jar = jar - return self -} - - -// Stream API -Request.prototype.pipe = function (dest, opts) { - var self = this - - if (self.response) { - if (self._destdata) { - self.emit('error', new Error('You cannot pipe after data has been emitted from the response.')) - } else if (self._ended) { - self.emit('error', new Error('You cannot pipe after the response has been ended.')) - } else { - stream.Stream.prototype.pipe.call(self, dest, opts) - self.pipeDest(dest) - return dest - } - } else { - self.dests.push(dest) - stream.Stream.prototype.pipe.call(self, dest, opts) - return dest - } -} -Request.prototype.write = function () { - var self = this - if (self._aborted) {return} - - if (!self._started) { - self.start() - } - if (self.req) { - return self.req.write.apply(self.req, arguments) - } -} -Request.prototype.end = function (chunk) { - var self = this - if (self._aborted) {return} - - if (chunk) { - self.write(chunk) - } - if (!self._started) { - self.start() - } - if (self.req) { - self.req.end() - } -} -Request.prototype.pause = function () { - var self = this - if (!self.responseContent) { - self._paused = true - } else { - self.responseContent.pause.apply(self.responseContent, arguments) - } -} -Request.prototype.resume = function () { - var self = this - if (!self.responseContent) { - self._paused = false - } else { - self.responseContent.resume.apply(self.responseContent, arguments) - } -} -Request.prototype.destroy = function () { - var self = this - if (!self._ended) { - self.end() - } else if (self.response) { - self.response.destroy() - } -} - -Request.defaultProxyHeaderWhiteList = - Tunnel.defaultProxyHeaderWhiteList.slice() - -Request.defaultProxyHeaderExclusiveList = - Tunnel.defaultProxyHeaderExclusiveList.slice() - -// Exports - -Request.prototype.toJSON = requestToJSON -module.exports = Request diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..a1465c87 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4368 @@ +{ + "name": "com.homewizard", + "version": "3.0.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "com.homewizard", + "version": "3.0.2", + "license": "GPL-3.0", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^2.7.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "@tsconfig/node12": "^12.1.7", + "@types/homey": "npm:homey-apps-sdk-v3-types@^0.3.3", + "@types/node": "^25.0.1", + "eslint": "^7.32.0", + "eslint-config-athom": "^3.1.5" + }, + "optionalDependencies": { + "canvas": "^2.11.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/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==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/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==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/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==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/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==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "12.1.7", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-12.1.7.tgz", + "integrity": "sha512-xV9L85NANUIgJotIT7VJMSh8HaQPJF4cgr72bzgHS2xRcRBp9RCGyZvk7MOefH+lVnm+/lH7Vc6AefeF5IuOAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/homey": { + "name": "homey-apps-sdk-v3-types", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/homey-apps-sdk-v3-types/-/homey-apps-sdk-v3-types-0.3.12.tgz", + "integrity": "sha512-xr275VoF7FAiTi6PzElqHWpuGgSBY9hZFZsKeCTA4rluopf8sVk6zUPa+giSRSTiTrN3N3QjmHD5lUoNVwYd4g==", + "dev": true, + "dependencies": { + "@types/node": "^14.14.20" + } + }, + "node_modules/@types/homey/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.1.tgz", + "integrity": "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/color-convert": { + "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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/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 + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "devOptional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-athom": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-athom/-/eslint-config-athom-3.1.5.tgz", + "integrity": "sha512-QPseQu6fwQph9BtRwints6+3d+e7T7SvD26ColkAh/N0NQEJfamMd4gg7qm8gHQFDQy85hKQi0JRwJD/wKpYUA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-homey-app": "^1.0.2", + "eslint-plugin-import": "^2.24.2", + "eslint-plugin-mocha": "^6.3.0", + "eslint-plugin-node": "^11.1.0", + "typescript": "^4.4.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.5.0" + } + }, + "node_modules/eslint-config-athom/node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-athom/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-athom/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-config-athom/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-athom/node_modules/eslint-config-airbnb-base": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", + "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", + "eslint-plugin-import": "^2.22.1" + } + }, + "node_modules/eslint-config-athom/node_modules/eslint-plugin-homey-app": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-homey-app/-/eslint-plugin-homey-app-1.0.2.tgz", + "integrity": "sha512-uO09MpI0GaRfxWd8jKf6ei71zCCx3C4/8m1vm/GqYv1y/TEi8i2GdIlBCqyN67IXd4fwT+BNd+BoGxKh+8WC8A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": "^7.32.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-mocha": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-6.3.0.tgz", + "integrity": "sha512-Cd2roo8caAyG21oKaaNTj7cqeYRWW1I2B5SfpKRp0Ip1gkfwoR1Ow0IGlPWnNjzywdF4n+kHL8/9vM6zCJUxdg==", + "dev": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "ramda": "^0.27.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "eslint": ">= 4.0.0" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true + }, + "node_modules/nan": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "license": "MIT", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ramda": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz", + "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==", + "dev": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "devOptional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..e2d51d01 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "com.homewizard", + "version": "3.0.2", + "description": "Homewizard app for Homey", + "main": "app.js", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^2.7.0", + "ws": "^8.18.3" + }, + "scripts": { + "test": "athom app run", + "test-unit": "node test/xadi-provider.test.js", + "lint": "eslint . --ext js,ts,tsx --report-unused-disable-directives --max-warnings 0 --fix", + "lint-check": "eslint . --ext js,ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jtebbens/com.homewizard.git" + }, + "author": "Jeroen Tebbens", + "license": "GPL-3.0", + "bugs": { + "url": "https://github.com/jtebbens/com.homewizard/issues" + }, + "homepage": "https://github.com/jtebbens/com.homewizard#readme", + "devDependencies": { + "@tsconfig/node12": "^12.1.7", + "@types/homey": "npm:homey-apps-sdk-v3-types@^0.3.3", + "@types/node": "^25.0.1", + "eslint": "^7.32.0", + "eslint-config-athom": "^3.1.5" + } +} diff --git a/settings/index.html b/settings/index.html new file mode 100644 index 00000000..e9507e5c --- /dev/null +++ b/settings/index.html @@ -0,0 +1,1854 @@ + + + + Homewizard Settings + + + + + + + + + + + + +

+ + +

+ +
+
+
+
Policy
+
Fetch
+
Discovery
+
WebSocket
+
Legacy
+
+ + +
+

Sluipverbruik grafiek

+
+
Huidig sluipverbruik
+
Laden...
+
Berekening wordt uitgevoerd...
+
+ +

Samples van geselecteerde nacht

+ + + +
+ + +
+

Device Fetch Stats

+ + + + + + + + + + + + + + + + + + +
DeviceUptimeMisluktTimeoutsTotaalGem. (ms)WiFi (gem/min)mDNSLaatste foutSinds
Laden…
+ + +

Fetch Logs

+
Loading…
+ +
+ + +
+

Discovery debug

+
Loading…
+
+ + +
+

WebSocket Diagnostics

+

+ Events and stats snapshots are persisted to Homey settings and survive app crashes (CPU/memory kills). + After a crash, reload this page to see the last state before the kill. +

+ + +

Device Snapshots

+
+ +

Event Journal (last 50 events)

+
Loading…
+ +
+ +
+ + + +
+ +
+ + +
+

+

+
+
+
+ +
+
+ + +
+

Battery Policy Explainability

+
+ +
+

⚡ Batterij Economie

+
Gemiddelde kostprijs:
+
Break-even prijs:
+
Huidige prijs:
+
Arbitrage:
+
RTE:
+
+ +

Redenen

+
    +

    Waarschuwingen

    +
      +

      ML Decision Scores

      +
      +
      + Toon raw scores (debug) +
      
      +    
      +

      Tijdstip

      +
      +
      + + +
      +

      Legacy Fetch debug

      +
      Loading…
      + +
      + + + + + \ No newline at end of file diff --git a/test/ws-manager.test.js b/test/ws-manager.test.js new file mode 100644 index 00000000..462598af --- /dev/null +++ b/test/ws-manager.test.js @@ -0,0 +1,321 @@ +/** + * Standalone test for WebSocketManager (includes/v2/Ws.js) + * + * Run: node test/ws-manager.test.js + * + * Creates a local mock WS server that mimics the HomeWizard device + * protocol (authorize → subscribe → stream messages) and verifies + * the manager handles each phase correctly. + * + * No test framework needed. Exits 0 on success, 1 on failure. + */ + +'use strict'; + +const http = require('http'); +const { WebSocketServer, WebSocket } = require('ws'); + +// ─── Helpers ─────────────────────────────────────────── + +let passed = 0; +let failed = 0; + +function assert(condition, label) { + if (condition) { + passed++; + console.log(` ✅ ${label}`); + } else { + failed++; + console.error(` ❌ ${label}`); + } +} + +function sleep(ms) { + return new Promise(r => setTimeout(r, ms)); +} + +// ─── Mock device server ──────────────────────────────── + +function createMockServer() { + const server = http.createServer((req, res) => { + // Preflight: GET /api/system + if (req.url === '/api/system') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + product_name: 'HWE-BAT', + product_type: 'HWE-BAT', + serial: 'test-serial-001', + firmware_version: '6.02', + api_version: 'v2', + cloud_enabled: true, + wifi_ssid: 'TestNetwork', + wifi_strength: -42, + })); + return; + } + res.writeHead(404); + res.end(); + }); + + const wss = new WebSocketServer({ server }); + const state = { + authorized: false, + subscriptions: new Set(), + measurementInterval: null, + clients: [], + }; + + wss.on('connection', (ws) => { + state.clients.push(ws); + + ws.on('message', (msg) => { + let data; + try { data = JSON.parse(msg.toString()); } catch { return; } + + if (data.type === 'authorization') { + state.authorized = true; + ws.send(JSON.stringify({ type: 'authorized' })); + } + else if (data.type === 'subscribe') { + state.subscriptions.add(data.data); + + // Once subscribed to measurement, start streaming + if (data.data === 'measurement' && !state.measurementInterval) { + let tick = 0; + state.measurementInterval = setInterval(() => { + tick++; + if (ws.readyState !== WebSocket.OPEN) return; + + // Measurement every 1s (simulates energy_v2) + ws.send(JSON.stringify({ + type: 'measurement', + data: { + power_w: 100 + tick, + energy_import_kwh: 1234.5 + tick * 0.001, + energy_export_kwh: 567.8, + } + })); + + // System every 3s + if (tick % 3 === 0) { + ws.send(JSON.stringify({ + type: 'system', + data: { wifi_strength: -40 - (tick % 10) } + })); + } + + // Batteries every 2s + if (tick % 2 === 0) { + ws.send(JSON.stringify({ + type: 'batteries', + data: { + mode: 'zero', + permissions: ['charge_allowed', 'discharge_allowed'], + state_of_charge_pct: 45 + tick * 0.1, + power_w: 200, + } + })); + } + }, 1000); + } + } + else if (data.type === 'batteries' && data.data?.mode) { + // Battery mode change command + ws.send(JSON.stringify({ + type: 'batteries', + data: { mode: data.data.mode, permissions: data.data.permissions || [] } + })); + } + }); + + ws.on('close', () => { + if (state.measurementInterval) { + clearInterval(state.measurementInterval); + state.measurementInterval = null; + } + state.authorized = false; + state.subscriptions.clear(); + }); + }); + + return { server, wss, state }; +} + +// ─── Test suite ──────────────────────────────────────── + +async function runTests() { + const { server, wss, state } = createMockServer(); + + // Start server on random port + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const port = server.address().port; + const baseUrl = `http://127.0.0.1:${port}`; + + console.log(`\n🧪 Mock server on port ${port}\n`); + + // Patch fetchQueue → use plain node-fetch for tests + // We need to bypass the fetchQueue since it has module-level state + const originalRequire = require('../../includes/utils/fetchQueue'); + + // Load WebSocketManager (fetchQueue will be loaded as a module) + const WebSocketManager = require('../includes/v2/Ws'); + + // ─── Collected data from callbacks ─── + const collected = { + measurements: [], + systems: [], + batteries: [], + available: false, + }; + + // ─── Create manager ─── + const mgr = new WebSocketManager({ + device: { + getData: () => ({ id: 'test-device-001' }), + _handleBatteries: (data) => { + // Optimistic update from setBatteryMode + collected.batteries.push({ ...data, optimistic: true }); + }, + }, + url: baseUrl, + token: 'test-token-xyz', + log: (...args) => console.log(' [LOG]', ...args), + error: (...args) => console.error(' [ERR]', ...args), + setAvailable: async () => { collected.available = true; }, + getSetting: (key) => { + if (key === 'url') return baseUrl; + if (key === 'update_interval') return 2000; + return null; + }, + handleMeasurement: (data) => collected.measurements.push(data), + handleSystem: (data) => collected.systems.push(data), + handleBatteries: (data) => collected.batteries.push(data), + }); + + // ═══════ TEST 1: getStats before start ═══════ + console.log('── Test 1: Initial state ──'); + const stats0 = mgr.getStats(); + assert(!stats0.connected, 'Not connected before start'); + assert(stats0.stopped === false, 'Not stopped initially'); + assert(stats0.counters.messagesReceived === 0, 'Zero messages initially'); + + // ═══════ TEST 2: Connection + Auth + Subscribe ═══════ + console.log('\n── Test 2: Connect / Auth / Subscribe ──'); + mgr.setDebug(true); + await mgr.start(); + await sleep(2000); // wait for open + authorize + subscribe + a few messages + + const stats1 = mgr.getStats(); + assert(stats1.connected, 'Connected after start'); + assert(stats1.wsActive, 'wsActive is true'); + assert(stats1.counters.messagesReceived > 0, `Received ${stats1.counters.messagesReceived} messages`); + assert(state.authorized, 'Server saw authorization'); + assert(state.subscriptions.has('measurement'), 'Subscribed to measurement'); + assert(state.subscriptions.has('system'), 'Subscribed to system'); + assert(state.subscriptions.has('batteries'), 'Subscribed to batteries'); + assert(collected.available, 'setAvailable was called'); + + // ═══════ TEST 3: Throttling ═══════ + console.log('\n── Test 3: Throttling ──'); + await sleep(6000); // let 6s of messages flow + + const stats2 = mgr.getStats(); + // ~6 measurement messages sent (1/s), but only ~3 should be processed (2s throttle) + assert(stats2.counters.measurementsProcessed >= 2, `Measurements processed: ${stats2.counters.measurementsProcessed} (expected ~3)`); + assert(stats2.counters.measurementsDropped >= 1, `Measurements dropped/deferred: ${stats2.counters.measurementsDropped}`); + // System: ~2 sent (every 3s), only 1 should pass (10s throttle) + assert(stats2.counters.systemProcessed >= 1, `System processed: ${stats2.counters.systemProcessed}`); + // Batteries: ~3 sent (every 2s), limited by 5s throttle + assert(stats2.counters.batteriesProcessed >= 1, `Batteries processed: ${stats2.counters.batteriesProcessed}`); + + console.log(` Totals: msgs=${stats2.counters.messagesReceived}, meas=${stats2.counters.measurementsProcessed}/${stats2.counters.measurementsDropped}, sys=${stats2.counters.systemProcessed}/${stats2.counters.systemDropped}, bat=${stats2.counters.batteriesProcessed}/${stats2.counters.batteriesDeferred}`); + + // ═══════ TEST 4: setBatteryMode ═══════ + console.log('\n── Test 4: setBatteryMode ──'); + const prevBatLen = collected.batteries.length; + mgr.setBatteryMode('standby'); + await sleep(500); + assert(collected.batteries.length > prevBatLen, 'setBatteryMode triggered callback'); + const lastBat = collected.batteries[collected.batteries.length - 1]; + assert(lastBat.optimistic === true, 'Optimistic local update fired'); + assert(lastBat.mode === 'standby', `Mode is standby (got ${lastBat.mode})`); + + mgr.setBatteryMode('zero_charge_only'); + await sleep(500); + const lastBat2 = collected.batteries[collected.batteries.length - 1]; + assert(lastBat2.mode === 'zero', `zero_charge_only maps to mode=zero`); + + // ═══════ TEST 5: setBatteryMode all modes ═══════ + console.log('\n── Test 5: All battery modes ──'); + const modes = ['standby', 'zero', 'zero_charge_only', 'zero_discharge_only', 'to_full']; + for (const mode of modes) { + try { + mgr.setBatteryMode(mode); + assert(true, `setBatteryMode("${mode}") — OK`); + } catch (e) { + assert(false, `setBatteryMode("${mode}") threw: ${e.message}`); + } + } + + try { + mgr.setBatteryMode('invalid_mode'); + assert(false, 'Should have thrown for invalid mode'); + } catch (e) { + assert(true, `setBatteryMode("invalid_mode") throws: ${e.message}`); + } + + // ═══════ TEST 6: Stop / Resume ═══════ + console.log('\n── Test 6: Stop / Resume ──'); + mgr.stop(); + await sleep(500); + const stats3 = mgr.getStats(); + assert(!stats3.connected, 'Disconnected after stop'); + assert(stats3.stopped, 'Stopped flag set'); + assert(stats3.timersActive === 0, 'All timers cleared'); + + try { + mgr.setBatteryMode('zero'); + assert(false, 'Should throw when stopped'); + } catch (e) { + assert(true, 'setBatteryMode throws when stopped'); + } + + await mgr.resume(); + await sleep(2000); + const stats4 = mgr.getStats(); + assert(stats4.connected, 'Reconnected after resume'); + assert(!stats4.stopped, 'Stopped flag cleared'); + + // ═══════ TEST 7: getStats snapshot ═══════ + console.log('\n── Test 7: getStats completeness ──'); + const snap = mgr.getStats(); + assert('connected' in snap, 'Has connected'); + assert('throttle' in snap, 'Has throttle section'); + assert('counters' in snap, 'Has counters section'); + assert(typeof snap.idleMs === 'number', 'idleMs is number'); + assert(typeof snap.timersActive === 'number', 'timersActive is number'); + assert(snap.counters.lastConnectedAt !== null, 'lastConnectedAt tracked'); + + // ═══════ TEST 8: Debug toggle ═══════ + console.log('\n── Test 8: Debug toggle ──'); + mgr.setDebug(false); + assert(!mgr._debug, 'Debug turned off'); + mgr.setDebug(true); + assert(mgr._debug, 'Debug turned on'); + + // ═══════ Cleanup ═══════ + mgr.stop(); + wss.close(); + server.close(); + + console.log(`\n${'═'.repeat(40)}`); + console.log(` ${passed} passed, ${failed} failed`); + console.log(`${'═'.repeat(40)}\n`); + + process.exit(failed > 0 ? 1 : 0); +} + +runTests().catch(err => { + console.error('💥 Test crashed:', err); + process.exit(1); +}); diff --git a/test/xadi-provider.test.js b/test/xadi-provider.test.js new file mode 100644 index 00000000..e8d311b6 --- /dev/null +++ b/test/xadi-provider.test.js @@ -0,0 +1,41 @@ +const assert = require('assert'); +const XadiProvider = require('../lib/xadi-provider'); + +// Minimal homey stub +const homey = { + log: (...args) => {}, + error: (...args) => {} +}; + +(async () => { + const provider = new XadiProvider(homey); + + // Build a synthetic cache with two hourly windows: one covering 'now' and one next hour + const now = new Date(); + const startThisHour = new Date(now.getTime() - 30 * 60 * 1000); // started 30 minutes ago + const startNextHour = new Date(startThisHour.getTime() + 60 * 60 * 1000); + + provider.cache = [ + { + timestamp: startThisHour, + price: 0.1111, + priceMwh: 111.1, + hour: startThisHour.getHours(), + originalPrice: 0.1111 + }, + { + timestamp: startNextHour, + price: 0.2222, + priceMwh: 222.2, + hour: startNextHour.getHours(), + originalPrice: 0.2222 + } + ]; + + const price = provider.getCurrentPrice(); + console.log('Provider current price returned:', price); + + assert.strictEqual(price, 0.1111, 'Expected provider to return the price for the current timestamp window'); + + console.log('XadiProvider timestamp matching test: PASSED'); +})(); diff --git a/xadi_analysis.ipynb b/xadi_analysis.ipynb new file mode 100644 index 00000000..d5c50d29 --- /dev/null +++ b/xadi_analysis.ipynb @@ -0,0 +1,283 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "1b62adb8", + "metadata": {}, + "outputs": [], + "source": [ + "import urllib.request, json, datetime, math\n", + "from collections import defaultdict\n", + "\n", + "# Fetch the data\n", + "url = \"https://dap.xadi.eu/api/nl/today?markup=0.11&vat=0.21\"\n", + "print(f\"Fetching: {url}\")\n", + "req = urllib.request.Request(url, headers={\"User-Agent\": \"Mozilla/5.0\"})\n", + "with urllib.request.urlopen(req, timeout=15) as response:\n", + " raw_text = response.read().decode('utf-8')\n", + "raw_data = json.loads(raw_text)\n", + "print(f\"Status: {raw_data.get('status')}, Entries: {len(raw_data.get('data', []))}\")\n", + "\n", + "# Show raw JSON structure\n", + "print(\"\\n=== RAW JSON STRUCTURE ===\")\n", + "print(f\"Top-level keys: {list(raw_data.keys())}\")\n", + "print(\"\\nFirst 2 entries:\")\n", + "for i, entry in enumerate(raw_data.get('data', [])[:2]):\n", + " print(json.dumps(entry, indent=2, default=str))\n", + "print(f\"\\nLast entry:\")\n", + "print(json.dumps(raw_data['data'][-1], indent=2, default=str))\n", + "print(f\"\\n=== FULL RAW JSON ===\")\n", + "full_json = json.dumps(raw_data, indent=2, default=str)\n", + "print(full_json[:8000])\n", + "if len(full_json) > 8000:\n", + " print(f\"... [{len(full_json)-8000} more chars]\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28702546", + "metadata": {}, + "outputs": [], + "source": [ + "# Parse 15-minute intervals\n", + "data_entries = raw_data.get('data', [])\n", + "intervals = []\n", + "for entry in data_entries:\n", + " ts_str = entry.get('time', '')\n", + " try:\n", + " ts = datetime.datetime.fromisoformat(ts_str.replace('Z', '+00:00'))\n", + " except:\n", + " ts = datetime.datetime.strptime(ts_str[:19], '%Y-%m-%dT%H:%M:%S')\n", + " price = entry.get('price', entry.get('totalPrice', 0))\n", + " hour_str = entry.get('hour', '0:00')\n", + " markup_info = entry.get('markup', {})\n", + " original_price = markup_info.get('originalPrice', price)\n", + " intervals.append({\n", + " 'timestamp': ts, 'hour_str': hour_str,\n", + " 'hour': int(hour_str.split(':')[0]) if ':' in str(hour_str) else int(hour_str),\n", + " 'minute': int(hour_str.split(':')[1]) if ':' in str(hour_str) and len(hour_str.split(':')) > 1 else 0,\n", + " 'price': price, 'original_price': original_price,\n", + " 'price_mwh': entry.get('priceMwh', price * 1000),\n", + " })\n", + "intervals.sort(key=lambda x: x['timestamp'])\n", + "print(f\"Parsed {len(intervals)} intervals\")\n", + "print(f\"First: {intervals[0]['timestamp']} - {intervals[0]['hour']:02d}:{intervals[0]['minute']:02d} - €{intervals[0]['price']:.4f}/kWh\")\n", + "print(f\"Last: {intervals[-1]['timestamp']} - {intervals[-1]['hour']:02d}:{intervals[-1]['minute']:02d} - €{intervals[-1]['price']:.4f}/kWh\")\n", + "\n", + "# Calculate hourly averages\n", + "hourly_data = defaultdict(list)\n", + "for iv in intervals:\n", + " hourly_data[iv['hour']].append(iv['price'])\n", + "hourly_avg = {h: sum(p)/len(p) for h, p in sorted(hourly_data.items())}\n", + "\n", + "print(f\"\\n{'='*65}\")\n", + "print(f\"{'Hour (CET)':>12} | {'Avg Price (€/kWh)':>18} | {'# Intervals':>12} | {'Price (ct/kWh)':>14}\")\n", + "print(f\"{'-'*65}\")\n", + "for hour in range(24):\n", + " if hour in hourly_avg:\n", + " print(f\" {hour:02d}:00 | €{hourly_avg[hour]:>10.4f} | {len(hourly_data[hour]):>4} | {hourly_avg[hour]*100:>7.2f} ct\")\n", + "\n", + "# Cheapest and most expensive\n", + "sorted_hours = sorted(hourly_avg.items(), key=lambda x: x[1])\n", + "print(f\"\\n{'='*50}\")\n", + "print(\"TOP 5 CHEAPEST HOURS\")\n", + "print(f\"{'='*50}\")\n", + "for rank, (hour, price) in enumerate(sorted_hours[:5], 1):\n", + " neg = \" ⚡ NEGATIVE!\" if price < 0 else \"\"\n", + " print(f\" #{rank}: {hour:02d}:00 → €{price:.4f}/kWh ({price*100:.2f} ct){neg}\")\n", + "print(f\"\\n{'='*50}\")\n", + "print(\"TOP 5 MOST EXPENSIVE HOURS\")\n", + "print(f\"{'='*50}\")\n", + "for rank, (hour, price) in enumerate(sorted_hours[-5:][::-1], 1):\n", + " print(f\" #{rank}: {hour:02d}:00 → €{price:.4f}/kWh ({price*100:.2f} ct)\")\n", + "\n", + "# Statistics\n", + "all_prices = [iv['price'] for iv in intervals]\n", + "hourly_prices_list = list(hourly_avg.values())\n", + "min_hourly = min(hourly_prices_list)\n", + "max_hourly = max(hourly_prices_list)\n", + "spread = max_hourly - min_hourly\n", + "mean_price = sum(all_prices) / len(all_prices)\n", + "sorted_prices = sorted(all_prices)\n", + "median_price = sorted_prices[len(sorted_prices) // 2]\n", + "std_dev = (sum((p - mean_price) ** 2 for p in all_prices) / len(all_prices)) ** 0.5\n", + "neg_hours = sum(1 for h, p in hourly_avg.items() if p < 0)\n", + "neg_intervals = sum(1 for iv in intervals if iv['price'] < 0)\n", + "\n", + "print(f\"\\n{'='*55}\")\n", + "print(\"PRICE STATISTICS\")\n", + "print(f\"{'='*55}\")\n", + "print(f\" Min hourly avg: €{min_hourly:.4f}/kWh ({min_hourly*100:.2f} ct)\")\n", + "print(f\" Max hourly avg: €{max_hourly:.4f}/kWh ({max_hourly*100:.2f} ct)\")\n", + "print(f\" Price spread: €{spread:.4f}/kWh ({spread*100:.2f} ct)\")\n", + "print(f\" Mean (all intervals): €{mean_price:.4f}/kWh ({mean_price*100:.2f} ct)\")\n", + "print(f\" Median: €{median_price:.4f}/kWh ({median_price*100:.2f} ct)\")\n", + "print(f\" Std deviation: €{std_dev:.4f}/kWh ({std_dev*100:.2f} ct)\")\n", + "print(f\" Negative price hours: {neg_hours}\")\n", + "print(f\" Negative 15-min intervals: {neg_intervals}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8139361f", + "metadata": {}, + "outputs": [], + "source": [ + "# Battery specs - 1 unit\n", + "charge_power = 0.8 # kW\n", + "discharge_power = 0.8 # kW\n", + "capacity = 2.7 # kWh\n", + "efficiency = 0.75\n", + "charge_time = capacity / charge_power # 3.375 hours\n", + "usable_discharge = capacity * efficiency # 2.025 kWh\n", + "discharge_time = usable_discharge / discharge_power # 2.53125 hours\n", + "\n", + "print(f\"{'='*55}\")\n", + "print(\"BATTERY SPECS — 1 UNIT\")\n", + "print(f\"{'='*55}\")\n", + "print(f\" Charge power: {charge_power*1000:.0f} W ({charge_power} kW)\")\n", + "print(f\" Battery capacity: {capacity} kWh\")\n", + "print(f\" Round-trip efficiency: {efficiency*100:.0f}%\")\n", + "print(f\" Charge time (full): {charge_time:.3f}h ({charge_time*60:.0f} min)\")\n", + "print(f\" Usable discharge: {usable_discharge:.3f} kWh\")\n", + "print(f\" Discharge time: {discharge_time:.3f}h ({discharge_time*60:.0f} min)\")\n", + "print(f\" Break-even: discharge > charge_price / {efficiency}\")\n", + "\n", + "# Optimal strategy: find best non-overlapping charge/discharge windows\n", + "hours_needed_charge = math.ceil(charge_time) # 4\n", + "hours_needed_discharge = math.ceil(discharge_time) # 3\n", + "\n", + "best_profit = float('-inf')\n", + "best_combo = None\n", + "\n", + "for cs in range(24 - hours_needed_charge + 1):\n", + " ch = list(range(cs, cs + hours_needed_charge))\n", + " if not all(h in hourly_avg for h in ch):\n", + " continue\n", + " # Calculate charge cost\n", + " c_cost = 0; e_rem = capacity\n", + " for h in ch:\n", + " e = min(charge_power, e_rem)\n", + " c_cost += hourly_avg[h] * e\n", + " e_rem -= e\n", + " if e_rem <= 0: break\n", + " \n", + " for ds in range(24 - hours_needed_discharge + 1):\n", + " dh = list(range(ds, ds + hours_needed_discharge))\n", + " if not all(h in hourly_avg for h in dh):\n", + " continue\n", + " if set(ch) & set(dh):\n", + " continue\n", + " # Calculate discharge revenue\n", + " d_rev = 0; e_rem = usable_discharge\n", + " for h in dh:\n", + " e = min(discharge_power, e_rem)\n", + " d_rev += hourly_avg[h] * e\n", + " e_rem -= e\n", + " if e_rem <= 0: break\n", + " profit = d_rev - c_cost\n", + " if profit > best_profit:\n", + " best_profit = profit\n", + " best_combo = (ch, dh, c_cost, d_rev)\n", + "\n", + "charge_hours, discharge_hours, total_charge_cost, total_discharge_revenue = best_combo\n", + "net_profit = total_discharge_revenue - total_charge_cost\n", + "\n", + "# Assign labels\n", + "hour_labels = {}\n", + "for h in range(24):\n", + " if h in charge_hours: hour_labels[h] = 'CHARGE'\n", + " elif h in discharge_hours: hour_labels[h] = 'DISCHARGE'\n", + " else: hour_labels[h] = 'idle'\n", + "\n", + "print(f\"\\n{'='*60}\")\n", + "print(\"OPTIMAL STRATEGY (1 UNIT)\")\n", + "print(f\"{'='*60}\")\n", + "print(f\"\\n📥 CHARGE: {charge_hours[0]:02d}:00 – {charge_hours[-1]+1:02d}:00\")\n", + "e_rem = capacity\n", + "for h in charge_hours:\n", + " e = min(charge_power, e_rem)\n", + " if e <= 0: break\n", + " print(f\" {h:02d}:00: {e:.3f} kWh × €{hourly_avg[h]:.4f} = €{hourly_avg[h]*e:.4f}\")\n", + " e_rem -= e\n", + "\n", + "print(f\"\\n📤 DISCHARGE: {discharge_hours[0]:02d}:00 – {discharge_hours[-1]+1:02d}:00\")\n", + "e_rem = usable_discharge\n", + "for h in discharge_hours:\n", + " e = min(discharge_power, e_rem)\n", + " if e <= 0: break\n", + " print(f\" {h:02d}:00: {e:.3f} kWh × €{hourly_avg[h]:.4f} = €{hourly_avg[h]*e:.4f}\")\n", + " e_rem -= e\n", + "\n", + "energy_charged = capacity\n", + "energy_discharged = usable_discharge\n", + "avg_charge = total_charge_cost / energy_charged\n", + "avg_discharge = total_discharge_revenue / energy_discharged\n", + "breakeven_threshold = avg_charge / efficiency\n", + "\n", + "print(f\"\\n{'='*60}\")\n", + "print(\"PROFIT BREAKDOWN\")\n", + "print(f\"{'='*60}\")\n", + "print(f\" Total charge cost: €{total_charge_cost:.4f}\")\n", + "print(f\" Total discharge revenue: €{total_discharge_revenue:.4f}\")\n", + "print(f\" Energy charged: {energy_charged:.3f} kWh\")\n", + "print(f\" Energy discharged: {energy_discharged:.3f} kWh\")\n", + "print(f\" Energy lost (25%): {energy_charged - energy_discharged:.3f} kWh\")\n", + "print(f\" Avg charge price: €{avg_charge:.4f}/kWh\")\n", + "print(f\" Avg discharge price: €{avg_discharge:.4f}/kWh\")\n", + "print(f\" Break-even threshold: €{breakeven_threshold:.4f}/kWh\")\n", + "print(f\" 💰 NET PROFIT: €{net_profit:.4f}/cycle\")\n", + "if net_profit > 0:\n", + " print(f\" ✅ PROFITABLE — Annual est: €{net_profit*365:.2f}\")\n", + "else:\n", + " print(f\" ⚠ NOT PROFITABLE — spread too small for 75% efficiency\")\n", + "\n", + "# 4-unit comparison\n", + "scale = 4\n", + "print(f\"\\n{'='*65}\")\n", + "print(\"4-UNIT COMPARISON\")\n", + "print(f\"{'='*65}\")\n", + "print(f\"\\n {'Parameter':<28} {'1 Unit':>12} {'4 Units':>12}\")\n", + "print(f\" {'-'*55}\")\n", + "print(f\" {'Charge power':<28} {charge_power*1000:>10.0f} W {charge_power*scale*1000:>10.0f} W\")\n", + "print(f\" {'Battery capacity':<28} {capacity:>10.1f} kWh{capacity*scale:>10.1f} kWh\")\n", + "print(f\" {'Usable discharge':<28} {usable_discharge:>10.3f} kWh{usable_discharge*scale:>10.3f} kWh\")\n", + "print(f\" {'Charge time':<28} {charge_time:>10.3f} h {charge_time:>10.3f} h\")\n", + "print(f\" {'Charging cost':<28} €{total_charge_cost:>10.4f} €{total_charge_cost*scale:>10.4f}\")\n", + "print(f\" {'Discharge revenue':<28} €{total_discharge_revenue:>10.4f} €{total_discharge_revenue*scale:>10.4f}\")\n", + "print(f\" {'NET PROFIT / cycle':<28} €{net_profit:>10.4f} €{net_profit*scale:>10.4f}\")\n", + "print(f\" {'Annual (365 cycles)':<28} €{net_profit*365:>10.2f} €{net_profit*scale*365:>10.2f}\")\n", + "print(f\"\\n Same hours, everything scales 4× linearly.\")\n", + "\n", + "# Full schedule\n", + "print(f\"\\n{'='*80}\")\n", + "print(\"FULL HOURLY SCHEDULE\")\n", + "print(f\"{'='*80}\")\n", + "for h in range(24):\n", + " if h in hourly_avg:\n", + " label = hour_labels.get(h, 'idle')\n", + " m = \"🟢\" if label == 'CHARGE' else (\"🔴\" if label == 'DISCHARGE' else \"⚪\")\n", + " bar_w = 40\n", + " if max_hourly != min_hourly:\n", + " bar_len = int((hourly_avg[h] - min_hourly) / (max_hourly - min_hourly) * bar_w)\n", + " else:\n", + " bar_len = bar_w // 2\n", + " bar = '█' * bar_len\n", + " print(f\" {m} {h:02d}:00 |{bar:<{bar_w}}| €{hourly_avg[h]:.4f} {'← '+label if label != 'idle' else ''}\")\n", + "\n", + "print(f\"\\n Legend: 🟢 = Charging | 🔴 = Discharging | ⚪ = Idle\")\n", + "print(f\" Spread: €{spread:.4f} | Profit/cycle (1u): €{net_profit:.4f} | (4u): €{net_profit*4:.4f}\")\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}