diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 00000000..01615dc3 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"a2c3bc47-7bcf-4bdb-9b28-33a4a5ca8738","pid":2261491,"procStart":"2362735888","acquiredAt":1779267104951} \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..eeff175c --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(node --check *)", + "Bash(node -c *)", + "Bash(npm run test-unit)", + "Bash(npm run lint-check)" + ] + } +} diff --git a/node_modules/request/node_modules/hawk/node_modules/hoek/test/modules/ignore.txt b/.codex similarity index 100% rename from node_modules/request/node_modules/hawk/node_modules/hoek/test/modules/ignore.txt rename to .codex 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..b6808d2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,243 +1,18 @@ *.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# 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 +/node_modules + +# Added by Homey CLI +/.homeybuild/ + +# AI instruction files (keep private) +CLAUDE.md +CONTEXT.md +DEVELOPMENT.md +SKILL.md +CRASH_DIAGNOSIS.md +refactor.md +refactor-log.md + +# 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..84e122e6 --- /dev/null +++ b/.homeychangelog.json @@ -0,0 +1,1514 @@ +{ + "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" + }, + "3.14.25": { + "en": "Balance-dynamic mapping fix, discharge price adjustment" + }, + "3.14.26": { + "en": "More planning tariff tuning" + }, + "3.14.27": { + "en": "Use household usage to calculate discharge policy. Added diagnostic caption to easier support remote problems." + }, + "3.14.28": { + "en": "Added zero mode during PV surplus and certain tariffs with less margin to profit (ie 0.01 profit)" + }, + "3.14.29": { + "en": "Energy-socket http agent adjustments, battery-policy explainability reasoning improvements" + }, + "3.14.30": { + "en": "Slot boundary alignment, virtual grid power fix, peaktiming PV free-cycle bypass, DP discharge allowed during PV hours" + }, + "3.14.31": { + "en": "Webcam fix tomorrow battery policy, image might not update till afternoon prices are available" + }, + "3.14.32": { + "en": "Cheapest hour fix diring night while 12 hours later are cheaper" + }, + "3.15.0": { + "en": "Refractor 15min timeslots for learning household usage, solar and forecasting." + }, + "3.15.1": { + "en": "Household usage fix" + }, + "3.15.2": { + "en": "Ignore open-meteo pv estimate outside sunrise-sunset, fallback open-meteo bad gateway, use cached data when more than 24 hours." + }, + "3.15.3": { + "en": "Widget light and dark added" + }, + "3.15.4": { + "en": "Improved webcam image for battery-policy, it was hard to see on a iPhone" + }, + "3.15.5": { + "en": "Battery Policy improvements (learning, graphs, PV estimates), polling interval adjusted for plugin-battery" + }, + "3.15.6": { + "en": "PV headroom optimization: battery discharges overnight when strong PV expected tomorrow, maximizing free solar storage" + }, + "3.15.7": { + "en": "Per-slot PV headroom floor: overnight discharge at low wind-night prices" + }, + "3.15.8": { + "en": "Oppertunity charge and discharge adjustments. Removed floor value" + }, + "3.15.9": { + "en": "Bugfix planning sticked to standby mode while PV is expected later and battery should be empty before that time" + }, + "3.15.10": { + "en": "Optimizer and planning now use net PV surplus (after house load) for all SoC and discharge floor decisions." + }, + "3.15.11": { + "en": "Refractor battery policy and ExplainabilityEngine (memory fix)" + }, + "3.15.12": { + "en": "Memory crash fixes (widget, policy, computeExpected)" + }, + "3.15.13": { + "en": "Memory crash (user report)" + }, + "3.15.14": { + "en": "Memory problem (user)" + }, + "3.15.15": { + "en": "Memory crashes (user)" + }, + "3.15.16": { + "en": "Heap guard fixes added" + }, + "3.15.17": { + "en": "Memory crash (user) - energy_v2 and SDM630v2 extra checks" + }, + "3.15.18": { + "en": "More logging memory crash" + }, + "3.15.19": { + "en": "Notification fixes battery-policy and webcam image creation delay (memory)" + }, + "3.15.20": { + "en": "Heap check before updateChart" + }, + "3.15.21": { + "en": "Battery-policy fix for non dynamic contract" + }, + "3.15.22": { + "en": "Another memory heap delay fix" + }, + "3.15.23": { + "en": "Bug: explanation.summary at line 2573 crashed when explanation was null (skipped due to heap > 35 MB guard)." + }, + "3.15.24": { + "en": "Max sockets missed, pool keeps growing" + }, + "3.15.25": { + "en": "Update planning chart (memory guard)" + }, + "3.15.26": { + "en": "More memory guards" + }, + "3.15.27": { + "en": "Policy guard mem checks" + }, + "3.15.28": { + "en": "patch" + }, + "3.15.29": { + "en": "Added diagnostics for battery policy memory issue" + }, + "3.15.30": { + "en": "HomeWizard Homey app crashes caused by `homey.settings.set` allocating ~30 MB heap per call." + }, + "3.15.31": { + "en": "Weather and price data also adjust to new settings.set" + }, + "3.15.32": { + "en": "Refractor policy-run" + }, + "3.15.33": { + "en": "Reduction writes policy run" + }, + "3.15.34": { + "en": "Added more diagnostics" + }, + "3.15.35": { + "en": "Memory optimization: reduced transient heap spikes to prevent app restarts." + }, + "3.15.36": { + "en": "Memory optimization: start-up HTTP polls of P1/SDM meters are now spread over the first ~10s to prevent heap spikes that caused \"Memory Warning Limit Reached\" crashes on Homeys with many devices." + }, + "3.15.37": { + "en": "Bugfixes on pv estimate, planning, webcam update, slim laden mode detection, fine tuning SDM230" + }, + "3.15.38": { + "en": "Negative prices full charge mode and update fixes in settings page." + }, + "3.15.39": { + "en": "Extra rules and checks for battery policy negative prices. pv tuning estimates, planning update." + }, + "3.15.40": { + "en": "planning bug fix" + }, + "3.15.41": { + "en": "Missing standby mode in planning to allow pv straight to grid" + }, + "3.15.42": { + "en": "Standby fix planning negative prices" + }, + "3.15.43": { + "en": "weather timeout blocks planning, fixed" + }, + "3.15.44": { + "en": "Planning fix, day after negative prices blocked" + }, + "3.15.45": { + "en": "policy fix standby" + }, + "3.15.46": { + "en": "planning bug fix " + }, + "3.15.47": { + "en": "planning diagnose button added" + }, + "3.15.48": { + "en": "DP fixes" + }, + "3.15.49": { + "en": "Consumption usage planning fix" + }, + "3.15.50": { + "en": "Delayed charged fix planning" + }, + "3.15.51": { + "en": "Bugfix, delayed charge but virtual SoC rises which should not be the case." + }, + "3.15.52": { + "en": "DP patched for delayed charging based on cheap prices" + }, + "3.15.53": { + "en": "Rollback last changes as it got worse, put vPreserve in place with oppurtunity fix" + }, + "3.15.54": { + "en": "PV solar fix when no flow card is active it falls back to the forecasted value from weather" + }, + "3.15.55": { + "en": "Planning fixes negative prices, tune best hour slots to charge" + }, + "3.15.56": { + "en": "Planning fix, standby early pv production bypasses delayed charge. calculation dp planned vs actual fix" + }, + "3.15.57": { + "en": "Planning tuning, policy kept falling back to standby while planning wanted to charge" + }, + "3.15.58": { + "en": "Day ahead planning tuning, delay discharge, cater for household usage to be included in policy calculations" + }, + "3.15.59": { + "en": "Planning tuning, added trickle mode to charge battery when cloudy day is forecasted" + }, + "3.15.60": { + "en": "Textual reason fixes and delayed charging." + }, + "3.15.61": { + "en": "Planning fix when next day during night has higher prices to compensate" + }, + "3.15.62": { + "en": "If no flowcard pv live, then fallback pv calculated via P1 and est home usage to better plan." + }, + "3.15.63": { + "en": "Diag planning extention to improve troubleshooting" + }, + "3.15.64": { + "en": "Energysocket triggers on finished job on dishwasher,washingmschine. Watermeter leak detection (15l/min for 60min default). PV and planning tuning." + }, + "3.15.65": { + "en": "More tuning on planning, PV forecast, delay, export now more profit then charging." + }, + "3.15.66": { + "en": "Bugfix, planning force charge when low pv and cheap prices" + }, + "3.15.67": { + "en": "More tuning on planning battery policy" + }, + "3.15.68": { + "en": "Planning to_full fix based in dynamic low price vs static. " + }, + "3.15.69": { + "en": "pv forecast learning bug fix" + }, + "3.15.70": { + "en": "open meteo factor adjusr" + }, + "3.15.71": { + "en": "pv trickle bug fix, blocked charging when profit was gone" + }, + "3.15.72": { + "en": "pv forecast fix when stale openmeteo data" + }, + "3.15.73": { + "en": "bias fix pv in graph planning" + }, + "3.15.74": { + "en": "buienradar correction fixes on pv forecast" + }, + "3.15.75": { + "en": "Buienradar longer range, open meteo cloud coverage added to calculation forecast pv" + }, + "3.15.76": { + "en": "Battery mode condition card bug" + }, + "3.15.77": { + "en": "BMS homewizard battery calibration detection added, false pv flag during sunrise - night (bug fix)" + }, + "3.15.78": { + "en": "Terminal value correction factor, extra diag debug info column added" + }, + "3.15.79": { + "en": "Fix: topup battery only when cheapest prices, else postpone" + }, + "3.15.80": { + "en": "Solcast adjustment being to optimistic on PV expectation" + }, + "3.15.81": { + "en": "p1 identify bug fix. error invalid response format." + }, + "3.15.82": { + "en": "error uploading" + }, + "3.15.83": { + "en": "With beta testing slimladen from Homewizard the PV Opwek chart stayed empty - fix" + }, + "3.15.84": { + "en": "Force check during night, check price slots and mark them for discharge if enough PV later that day" + }, + "3.15.85": { + "en": "Lowered factor from 0.8 to 0.6 for policy engine to fully discharge during night" + }, + "3.15.86": { + "en": "open meteo now takes solar orientation, planning fix with 15min slot issue affecting battery mode" + }, + "3.15.87": { + "en": "Policy fix night time discharge got blocked" + }, + "3.15.88": { + "en": "Bugfix P1 stale mode state." + }, + "3.15.89": { + "en": "Bugfix, battery groups also patched" + }, + "3.15.90": { + "en": "Bugfix battery mode" + }, + "3.15.91": { + "en": "P1 as source of truth after app restart" + }, + "3.15.92": { + "en": "Forced early exit policy mode off checks" + }, + "3.15.93": { + "en": "Energy socket tuning, memory improvements, tcp connect check to Homewizard legacy as fallback" + } +} diff --git a/.homeycompose/app.json b/.homeycompose/app.json new file mode 100644 index 00000000..7a1eeeda --- /dev/null +++ b/.homeycompose/app.json @@ -0,0 +1,74 @@ +{ + "id": "com.homewizard", + "name": { + "en": "HomeWizard" + }, + "version": "3.15.93", + "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..5cf4d084 --- /dev/null +++ b/.homeycompose/capabilities/battery_group_charge_mode.json @@ -0,0 +1,56 @@ +{ + "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" + } + }, + { + "id": "predictive", + "title": { + "en": "Predictive (HW Smart Charging)", + "nl": "Slim laden (HomeWizard)" + } + } + ] +} 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/bias_factor.json b/.homeycompose/capabilities/bias_factor.json new file mode 100644 index 00000000..a687e90b --- /dev/null +++ b/.homeycompose/capabilities/bias_factor.json @@ -0,0 +1,17 @@ +{ + "type": "number", + "title": { + "en": "PV Intraday Bias", + "nl": "PV Intradag Bias" + }, + "desc": { + "en": "Intraday PV correction ratio (actual/forecast)", + "nl": "Intradag PV-correctiefactor (actueel/voorspeld)" + }, + "decimals": 2, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" +} 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/plan_summary.json b/.homeycompose/capabilities/plan_summary.json new file mode 100644 index 00000000..d019a3d6 --- /dev/null +++ b/.homeycompose/capabilities/plan_summary.json @@ -0,0 +1,16 @@ +{ + "type": "string", + "title": { + "en": "Plan Summary", + "nl": "Plan Samenvatting" + }, + "desc": { + "en": "Today's optimizer plan in compact form", + "nl": "Compact overzicht van het optimizerplan" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "icon": "assets/icon.svg" +} 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/policy_profit_eur.json b/.homeycompose/capabilities/policy_profit_eur.json new file mode 100644 index 00000000..dec5fb43 --- /dev/null +++ b/.homeycompose/capabilities/policy_profit_eur.json @@ -0,0 +1,21 @@ +{ + "type": "number", + "title": { + "en": "Expected Profit", + "nl": "Verwachte Winst" + }, + "desc": { + "en": "Projected daily profit from today's optimizer plan", + "nl": "Verwachte dagwinst van het optimizerplan" + }, + "units": { + "en": "€", + "nl": "€" + }, + "decimals": 2, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "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/presence_mode.json b/.homeycompose/capabilities/presence_mode.json new file mode 100644 index 00000000..37058426 --- /dev/null +++ b/.homeycompose/capabilities/presence_mode.json @@ -0,0 +1,15 @@ +{ + "type": "string", + "title": { + "en": "Presence", + "nl": "Aanwezigheid" + }, + "desc": { + "en": "Indicates whether someone is home. Away mode pauses consumption learning so vacation patterns do not corrupt learned profiles.", + "nl": "Geeft aan of iemand thuis is. Afwezigheidsmodus pauzeert het verbruikspatroon zodat vakantiegedrag de geleerde profielen niet verstoort." + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "icon": "assets/icon.svg" +} 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/pv_forecast_kwh.json b/.homeycompose/capabilities/pv_forecast_kwh.json new file mode 100644 index 00000000..d53a4145 --- /dev/null +++ b/.homeycompose/capabilities/pv_forecast_kwh.json @@ -0,0 +1,21 @@ +{ + "type": "number", + "title": { + "en": "PV Forecast Today", + "nl": "PV Voorspelling Vandaag" + }, + "desc": { + "en": "Blended PV yield forecast for today", + "nl": "Gecombineerde PV opbrengst voor vandaag" + }, + "units": { + "en": "kWh", + "nl": "kWh" + }, + "decimals": 1, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/icon.svg" +} 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..e87e0598 --- /dev/null +++ b/.homeycompose/flow/conditions/check-battery-mode.json @@ -0,0 +1,65 @@ +{ + "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": "predictive", + "label": { + "en": "Predictive (HW Smart Charging)", + "nl": "Slim laden (HomeWizard)" + } + } + ] + } + ] +} \ 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..4fbb447c --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,8 @@ +{ + "MD033": { + "allowed_elements": ["details", "summary"] + }, + "MD013": false, + "MD024": false, + "MD022" : false +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 00000000..aef3d820 --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,3 @@ +# Generated files — not documentation +refactor-log.md +refactor-*.md 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..885e8fdd --- /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", "--remote"], + "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/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..418b6c72 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,218 @@ -# 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.15.63–v3.15.94) + +### EV Charging Gate (v3.15.94) + +* **New flow action `Set EV charging state` (v3.15.94)** — Tell the policy engine when an electric vehicle starts or stops charging. While EV charging is active the battery is forced to `zero_charge_only` (`standby` when SoC ≥ max): the EV draws from grid and PV instead of cycling the home battery, but PV surplus may still top up the battery. The flag auto-clears after 8 hours as a safety net in case the "stop" trigger is missed, and is persisted to settings so it survives app restarts. Implemented as an early-return gate in `_mapPolicyToHwMode` so it overrides discharge for any policy mode (`balanced`, `eco`, `aggressive`, `balanced-dynamic`). Logged as `[MAPPING][EV]` for diagnostics + +### Connectivity, Memory & Provider Reliability (v3.15.93) + +* **TCP ping socket-destroy on error path (v3.15.93)** — `tcpPing` in both `includes/legacy/homewizard.js` and `drivers/energy_socket/device.js` previously closed the socket only on `connect` and `timeout` events, not on `error`. While Node usually auto-closes sockets on error, edge cases (e.g. EHOSTUNREACH with retained native references) could accumulate file descriptors over days. Error handler now calls `socket.destroy()` explicitly +* **SHARED_SOCKET_AGENT.maxSockets scales with device count (v3.15.93)** — The shared HTTP agent for `energy_socket` devices was hard-capped at `maxSockets: 4`. With 15+ devices the agent became a bottleneck: failing devices held slots up to ~17s on the timeout/retry path, starving healthy devices. `maxSockets` is now set to `max(4, ceil(deviceCount / 3))` on each device init (idempotent — last writer wins). A user with 19 energy sockets now gets 7 slots instead of 4 +* **Recovery poller jitter (v3.15.93)** — When multiple energy_socket devices go offline simultaneously (e.g. WiFi access-point hiccup), all their 10-second TCP-ping recovery pollers would fire in lockstep, producing a thundering herd on the AP during recovery. Each recovery poller now starts after a random 0-10s delay before its `setInterval` begins, spreading the load +* **EHOSTUNREACH / ENETUNREACH backoff (v3.15.93)** — On hard network errors (host unreachable / route down), the device now skips polling for 60 seconds instead of retrying every 10s. The backoff is reset on successful poll or any discovery callback (available, address-changed, last-seen). Saves CPU and log noise when a device is genuinely offline +* **Open-Meteo retry on transient failures (v3.15.93)** — `weather-forecaster.js` now uses `fetchWithRetry` for the three Open-Meteo endpoints (ensemble radiation, standard hourly, tilted irradiance). On TIMEOUT or 5xx response the request is retried once after 3s. Open-Meteo overloads at peak times occasionally caused stale weather cache for up to an hour; the retry catches most transient failures +* **Xadi price endpoints fetched in parallel (v3.15.93)** — The Xadi provider previously fetched `/today`, `/next24h`, and `/day/tomorrow` sequentially (each with a 10s timeout, worst case ~30s). Now all three are fetched in parallel via `Promise.allSettled`, reducing worst case to ~10s. Deduplication via `seenTimestamps` Set is preserved +* **kWhPrice page-structure-change detection (v3.15.93)** — When the kwhprice.eu HTML scraper returned 0 slots, the provider silently returned an empty array — even if the HTML was large (indicating the page loaded but the CSS selector no longer matched). The provider now throws an explicit "page structure may have changed" error when `html.length > 2000` and 0 slots are parsed, engaging the stale-cache fallback and surfacing the issue in the logs +* **Drop [MEM][socket] init log spam (v3.15.93)** — Each `energy_socket` device wrote two `[MEM][socket]` heap-stats log lines on init. With 19 devices that's 38 noise lines per app restart with no operational value. Removed + +### PV Forecast, Optimizer & Battery Policy Fixes (v3.15.83–v3.15.87) + +* **Per-model GTI via solar transposition + KNMI kt bias classification (v3.15.83)** — PV forecast accuracy per Open-Meteo model now uses Global Tilted Irradiance (GTI) computed via the Perez transposition model rather than GHI. This ensures the per-model radiation error is evaluated on the same tilted plane as the actual panel yield. Simultaneously, the daily radiation bias factor is now selected based on the KNMI clearness index (kt) rather than Open-Meteo cloud cover fraction — OM systematically over-estimates cloud cover (42% vs 15% measured), causing the wrong bias tier to be selected on partially-cloudy days + +* **PV chart data key separated + kt-based bias apply + memory reduction (v3.15.84)** — The PV chart now uses a dedicated data key independent of the forecast pipeline, preventing stale forecast values from persisting across recomputes. kt-based bias is applied earlier in the forecast chain so downstream models see the corrected irradiance. Internal forecast buffers reduced to lower heap usage during ensemble fetches + +* **Chart midnight rollover fix + ensemble fetch timeout 10→15s (v3.15.85)** — The planning chart camera image swapped tomorrow's chart for today's after midnight due to a day-boundary comparison error; fixed. The Open-Meteo ensemble fetch timeout was extended from 10s to 15s to reduce spurious timeout failures on slow upstream responses. Battery policy capabilities now expose projected profit, PV forecast, bias factor, and current DP plan summary as Homey capability values + +* **Policy: fix policy_enabled permanently stuck after predictive interaction with restart (v3.15.87)** — Two related bugs that left `policy_enabled = false` after HW Slim laden (predictive) ended: (1) Restart DURING predictive: `_policyEnabledBeforePredictive` was in-memory only and lost on restart; the restore guard (`!== null`) skipped the restore, blocking all policy runs until the next restart. Fixed: persist the value to settings at predictive-start; fall back to persisted value (then `true`) when restoring. (2) Restart AFTER predictive ended before restore ran: `_isPredictiveMode` was `false` on init, so neither the restore branch nor the P1 poll path fired; policy stayed disabled. Fixed: at startup, if `policy_enabled_before_predictive` is present in settings and the hardware is no longer in predictive mode, restore `policy_enabled` immediately. Both paths now trigger an immediate policy check on predictive end instead of waiting up to 15 minutes for the next slot boundary + +* **Policy: min-price discharge override for DP preserve at night (v3.15.87)** — The DP optimizer could choose `preserve` at night to protect opportunity value for next-day PV recharge, even when the current price exceeded the user's configured minimum discharge price. This occurred when the SoC was near the PV absorption cliff: one discharge step would drop SoC below the refillable threshold, so the DP correctly valued the future state but violated the user's price floor intent. Added a policy-layer override that forces `discharge` when DP says preserve, price ≥ configured minimum, SoC > min_soc, and no PV is active. Observed: 3-hour discharge gap at SoC 32%, price €0.268–0.281, minimum €0.180 + +* **Optimizer: smooth isolated preserve islands in discharge sequences (v3.15.86)** — A single `preserve` slot flanked by `discharge` on both sides with a price delta below 1 ct is a DP numerical edge case (floating-point score tie at a local price minimum). Such slots are now overridden to `discharge` in a post-DP smoothing pass; projected SoC is propagated forward accordingly. Observed impact: one 15-min standby at €0.273 between €0.281 and €0.274 discharge slots + +### PV Forecast — KNMI Ground-Truth & Fixes (v3.15.81–v3.15.82) + +* **KNMI station ground-truth for model accuracy (v3.15.81)** — PV forecast accuracy tracking now uses independent in-situ radiation measurements from the nearest KNMI automatic weather station (e.g. Cabauw) as the daily actual, instead of Open-Meteo's own historical data. Open-Meteo used its own archived data as the "actual" reference, creating circular validation that could not detect systematic model bias. KNMI station data is fetched hourly via the EDR API and accumulated into a daily average used as ground-truth in the nightly learning step. Falls back to Open-Meteo if fewer than 4 daylight readings were collected or if no API key is configured. Requires a KNMI EDR API key configured in device settings (register at dataplatform.knmi.nl) + +* **NOCT temperature derating for fallback PV forecast (v3.15.81)** — The pre-learning fallback PV forecast (used before sufficient yield data has accumulated) now applies a thermal derating factor for high ambient temperatures. Silicon PV panels lose approximately 0.4%/°C above 25°C cell temperature; on hot summer days the flat-PR fallback overpredicted by up to 10%. The correction uses ambient temperature from the Open-Meteo forecast and the standard NOCT model. Panels with a learned yield factor already embed temperature effects empirically — correction applies only to the fallback path + +* **P1 meter identify accepts empty response body (v3.15.82)** — The P1 meter returns an empty HTTP body on `/api/system/identify`; the strict JSON object-type check caused a false "Invalid response format" error. HTTP status code alone is now used to detect failure + +### PV Forecast — Cloud Uncertainty, Solcast p10 & Per-Model Accuracy (v3.15.80) + +* **Solcast p10 cloud-aware selection (v3.15.80)** — When Solcast's `pv_estimate` (p50) exceeds the Open-Meteo NWP forecast by ≥10% for a given slot, the optimizer switches to `pv_estimate10` (pessimistic 10th-percentile) for that slot. On clear days both models agree and p50 is used; on overcast days where Solcast's satellite/ML lags a weather front, OM sees the cloud cover first and the p10 switch prevents over-reliance on PV that won't arrive. Logged as `p10=Xslots` per day in `[PV blend]` + +* **Cloud uncertainty discount in DP (v3.15.80)** — When cloud cover exceeds 70%, `pvCoverage` in the DP forward and backward passes is discounted by up to 40% (factor 0.6–1.0). Prevents the optimizer from discharging at break-even prices early in the day by relying on uncertain PV recharge that may not materialise on overcast days. Shown in the PV bias line as `×0.87 (pv-onzekerheid)` and logged as `[PV cloud uncertainty]` +* **Per-model OM radiation curves in PV accuracy chart (v3.15.80)** — The PV forecast accuracy section now shows a second chart with individual Open-Meteo model curves (Météo-France ARPEGE, GFS, ICON, KNMI) alongside actual measured production. Per-model Watt estimates are recorded per 15-min accuracy sample and stored in `pv_predictions`. Per-model EMA accuracy scores are shown as pills once a full day of data has accumulated +* **Météo-France ARPEGE Europe replaces ECMWF IFS04 in ensemble blend (v3.15.80)** — ECMWF IFS04 returned null `shortwave_radiation` for all hourly slots via the Open-Meteo ensemble endpoint and contributed nothing to the blend or accuracy tracking. Replaced with Météo-France ARPEGE Europe (10 km, West-European coverage), which provides full hourly radiation data. Accuracy prior set to 0.82 + +### Battery Policy — Grid Top-Up Timing (v3.15.79) + +* **Low-SoC top-up deferred to cheapest upcoming slot (v3.15.79)** — `lowSocGridTopUp` now only fires when the current price is the minimum price in the next 8 hours. Previously, the top-up could trigger at any price below `dynamicMaxChargePrice`, causing premature grid charging when a cheaper DP-planned slot existed shortly ahead. With two batteries (1600 W charge rate), charging at a sub-optimal price erases the efficiency margin entirely + +### Diagnostics & PV Accuracy (v3.15.78) + +* **Policy run debug: dynamicMaxChargePrice + lowSocGridTopUp (v3.15.78)** — The `policy_last_run_debug` snapshot now stores `dynamicMaxChargePrice` (the effective charge price ceiling at the time of the run) and `lowSocGridTopUp` (whether the low-SoC grid top-up path was triggered). Both are shown in the diagnostic output. Previously, post-hoc diagnosis of unexpected `to_full` decisions was impossible because only the current dynamic max was available, not the value from the actual run +* **PV net surplus accuracy tracking (v3.15.78)** — Learning engine now tracks predicted vs actual net PV surplus (PV minus consumption during solar hours). An EMA correction factor `pv_net_surplus_factor` [0.4–1.1] adjusts the terminal value calculation in the DP optimizer so next-day battery dispatch is planned on realistic rather than idealised surplus +* **Optimizer terminal value uses adjusted PV tomorrow (v3.15.78)** — `terminalPvKwhTomorrow` passes the surplus-accuracy-corrected PV estimate to the DP forward pass, preventing the optimizer from over-discharging today when tomorrow's PV was historically over-predicted + +### Battery Policy — Night Behaviour Fixes (v3.15.77) + +* **Sunset guard broken after midnight weather refresh (v3.15.77)** — After midnight, a weather cache refresh set `todaySunset` to the current day's future sunset timestamp. Because the current time (e.g. 00:48 UTC) was before that sunset (19:30 UTC), `_afterSunset` evaluated to `false`, letting stale PV estimates (EMA still decaying) pass the guard and map `preserve` to `zero_charge_only` instead of `standby`. Fix: `_afterSunset` is now also `true` when the current time is before `todaySunrise`, covering the entire pre-dawn window regardless of which day's sunset is cached +* **SoC staleness at policy run (v3.15.77)** — `battery_group_average_soc` is updated by a 60-second interval; when the policy engine ran between updates it used a stale SoC, causing the DP to plan from the wrong starting point. Fix: `_updateBatteryGroup()` is now called immediately before `_getBatteryState()` on every policy run, refreshing the capability from live WS data +* **Event history battery power from WS (v3.15.77)** — `battW` in event history entries now reads `_lastPower` directly from the plugin_battery WebSocket (seconds-fresh) instead of the 30s-stale `battery_group_power_w` capability. Falls back to the capability when no plugin_battery device is found +* **BMS calibration detection (v3.15.77)** — Battery management systems occasionally run a calibration cycle: battery charges at full power but reports SoC=0% and power=0W. This caused event history entries to look like unexplained grid spikes. Policy engine now detects this signature (`soc=0%`, `battW≈0`, `gridW>700W`, nighttime, no charge/discharge mode commanded) and marks the entry `exception: bms_calibration` + +### Battery Policy — PV Forecast Accuracy (v3.15.69–v3.15.76) + +* **Multi-source rain correction + ensemble averaging (v3.15.74)** — Buienradar now samples 5 geographic points (center + N/S/E/W at ±0.05°) for more representative local precipitation. PV forecast now averages 4 Open-Meteo radiation models (ECMWF, GFS, ICON, KNMI Harmonie), with spread-adjusted weighting to reduce outlier influence +* **3-class daily PV bias stratification by cloud cover (v3.15.73)** — Daily PV bias factor is now derived from three cloud-cover classes (clear/partial/overcast) rather than a single global correction, improving accuracy for mixed-sky days +* **Intraday PV scaling with winsorised ratios + CV dampening (v3.15.72)** — Intraday reoptimisation scales the PV forecast to match morning actuals. Ratio samples are now winsorised (outlier-clipped) and dampened by their coefficient of variation, preventing a single cloudy-then-clear hour from over-correcting the full-day forecast +* **Clear-sky forecast floor (v3.15.63)** — Intraday reopt now enforces a clear-sky model as a ceiling on per-slot PV corrections; slots cannot be scaled above theoretical maximum irradiance + +### Battery Policy — PV Chart & Display (v3.15.69–v3.15.73) + +* **OM and Solcast forecast lines shown separately (v3.15.70)** — PV chart now renders Open-Meteo (blue dashed) and Solcast (purple dashed) forecast lines independently alongside the blended orange line, making model agreement visible +* **Day-start Solcast snapshot for past-hour chart line (v3.15.77)** — The blended orange line now extends into past hours using the day-start Solcast snapshot (saved once per Amsterdam calendar day), preventing the line from dropping to zero for past slots when live data is no longer available +* **Planning schedule uses unbiased PV forecast (v3.15.73)** — The planning chart's orange forecast line now uses the raw unbiased PV values; the daily bias factor is applied only to optimizer inputs, not to the chart display +* **PV bias info + pvChart column in diagnose (v3.15.69)** — The `/diagnose` command now shows `PV bias: ×X.XX (dag) ×X.XX (acc) netto ×X.XX bewolking=X%` and a `PVchart` column with the chart-scaled PV value per slot + +### Settings & Diagnostics (v3.15.69) + +* **Bilingual settings page NL + EN (v3.15.69)** — All settings labels and descriptions are now available in Dutch and English; language follows the Homey locale setting + +### Battery Policy — Optimizer (v3.15.63–v3.15.68) + +* **Stop trickle-charging when PV export earns more (v3.15.65)** — `pv_trickle` mode is now blocked when the current export price exceeds the opportunity cost of charging; battery switches to `standby` and lets PV export to grid instead +* **PV trickle cost corrected to zero (v3.15.65)** — The optimizer previously assigned a non-zero charge cost to `pv_trickle` slots, making them compete unfairly with free PV surplus. Trickle-charge cost is now set to zero, consistent with how surplus PV charging is modelled +* **`pv_surplus_forecast` uses mapped hwModes and charge cap (v3.15.69)** — Surplus forecast now accounts for the actual hwMode mapping (not raw DP action) and respects the max charge power cap, preventing overly optimistic PV surplus estimates + +--- + +## Previous Updates (v3.15.59–v3.15.62) + +### Battery Policy — PV Detection & Diagnostics + +* **P1-derived PV fallback for users without PV flow card (v3.15.62)** — When no live PV flow card is configured, the policy engine now estimates PV production from P1 data using `pvFromP1 = avg_consumption − gridPower`. If this derived value ≥ 300 W during daylight hours, the battery switches to `zero_charge_only` to charge from available solar surplus. Previously, these users would remain in `standby` even when the planning showed `zero_charge_only`, because the live PV signal was always 0 W +* **Correct house consumption in diagnostics (v3.15.62)** — The "huidig verbruik" field in the active-slot diagnostic now shows actual house consumption (`gridPower + pvW − batteryPower`) instead of `currentLoad` (which is 0 when PV covers all loads). Previously, strong PV production caused the field to display 0 W, misleading users into thinking the house had no consumption + +### Battery Policy — DP Optimizer Fixes + +* **Discharge at highest-price night slots, not cheapest (v3.15.61)** — Two fixes for non-monotone night price ordering: (1) DP flattening is now blocked when a higher-priced non-PV slot exists ahead (e.g. 23:00 at €0.272 is no longer flattened when 00:00 is €0.278); (2) after the forward pass, discharge slots within the first contiguous non-PV window are reordered by price descending and assigned greedily from the battery budget +* **Session RTE tracking, charge cost post-2027, PV-store threshold (v3.15.59)** — Optimizer now tracks per-session round-trip efficiency; charge cost calculation updated for post-2027 net-metering end; PV-store threshold tuned to avoid premature standby during shoulder hours + +### Battery Policy — Planning + +* **Planning updates PV forecast, trickle mode (v3.15.59)** — Planning view now refreshes PV forecast on each recompute. Added `pv_trickle` mode for weak PV conditions (100–400 W) where zero_charge_only would be too aggressive but standby wastes available solar + +--- + +## Previous Updates (v3.15.49–v3.15.55) + +### Battery Policy — DP Optimizer Fixes + +* **`getSlot` biased toward active slot (v3.15.55)** — At the exact midpoint between two hourly slots (e.g. 12:30), `getSlot()` previously picked the *next* slot due to a millisecond timing offset, causing that slot's action to be applied up to 30 minutes too early. It now always returns the most recently started slot (the one currently being executed), falling back to nearest-future only when no past slot exists yet (e.g. on first startup) +* **Partial-slot charge modelling (v3.15.55)** — When the optimizer recomputes mid-slot, it previously modelled the current slot as a full 1-hour charging opportunity (0.8 kWh). This caused it to overestimate how much charge could be obtained in the remaining time and incorrectly skip the next slot. The DP now scales `chargeSocDeltaG` and `chargeKwhFull` for slot 0 based on the fraction of the slot still remaining (`slot0RemainingFrac`), so it correctly plans additional charge slots when needed (e.g. `charge=2` instead of `charge=1`) +* **`vPreserve` opportunity cost (v3.15.53)** — The DP previously treated PV charging during `preserve` slots as free: storing surplus PV had zero cost in the value function. It now subtracts the foregone export revenue (`storedKwh × price × exportRatio`) from `vPreserve`. This corrects the bias toward preserve when PV could profitably be exported instead, making `standby` more competitive at low positive prices + +### Battery Policy — Planning & Consumption + +* **Consumption slot timezone fix (v3.15.49)** — Price records now carry explicit Amsterdam `hour`/`minute` fields. When present, consumption lookups use `getPredictedConsumptionForSlot()` instead of deriving the hour from the UTC timestamp. Previously, UTC timestamps without a timezone indicator were shifted by +2h (CEST), causing all consumption forecasts to land on the wrong slot and return the baseload floor (~314 W) everywhere +* **Null consumption when nothing is learned (v3.15.49)** — If the learning engine has not yet accumulated any non-zero consumption data, `consumptionWPerSlot` is passed as `null` to the optimizer instead of an all-zero array. An all-zero array caused the baseload floor to over-constrain discharge planning as if the house never consumed power; `null` correctly instructs the optimizer to use unconstrained max discharge power (800 W) +* **`pvStoreWins` simulation in planning forward pass (v3.15.49)** — The optimizer's forward pass now simulates the `_pvStoreWins` override that the runtime policy engine applies. When a standby slot would have `pvStoreWins` active (PV surplus worth more than current export price), the planning chart shows `zero_charge_only` and updates the projected SoC accordingly — matching what actually happens at runtime +* **Planning slot reasons (v3.15.49)** — `_mapActionToHwModeForPlanning` now returns a `reason` string alongside `hwMode` (e.g. `dp:charge negative_price`, `preserve:pv_strong(3200W)`). Stored in the schedule and visible in the settings UI for easier diagnostics + +### Battery Policy — PV Estimation + +* **PV estimation fallback to weather forecast (v3.15.54)** — `_estimatePvProduction()` is now used everywhere house consumption is calculated. When no flow card is supplying live PV data (or the data is stale), it falls back to a weather-based estimate using sun score and configured PV capacity. Previously `this._pvProductionW ?? 0` was used directly, causing 0 W PV during stale periods and overcounting house consumption by the full battery charge power in the learning engine +* **P1 firmware `batteryPower = 0` correction (v3.15.55)** — The P1/DSMR firmware incorrectly reports battery power as 0 W when the battery is in `to_full` mode. The battery-policy device now detects this case (mode = `to_full`, reported power = 0) and substitutes the configured max charge power from device state. Without this correction the learning engine recorded house consumption ~800 W too high during every grid-charging session + +--- + +## Earlier Updates (v3.13–v3.15.58) + +* **v3.15.40** — Negative price charging (`to_full` when price < 0); `preserve→standby` bij negatieve prijzen; slot0RemainingFrac fix; P1 firmware batteryPower=0 correctie +* **v3.15.37** — Yield-factor normalisation; Solcast moved to `_updateWeather`; cycle recorded on discharge→charge transition; predictive modes in camera +* **v3.15.35–36** — Startup crash fix (serialised settings write queue); SDM polling spread; memory log per device type +* **v3.15.10+** — DP-primary refactor (sole decision-maker); PV accuracy fix; intraday PV scaling; explainability DP-reasons; PV chart fix; weather attenuation; DP terminal value; per-slot confidence margin; profit tracking; SoC forward simulation +* **v3.15.10** — pvCoverage net surplus; pvKwhTomorrow net-absorbable; dp.fill guard non-PV only; three-tier discharge floor; linear PV interpolation; Solcast integration; self-sufficiency tracking; consumption margin; PV camera image +* **v3.14.24–3.14.29** — Discharge SoC projection consumption-aware; discharge floor consistent DP/display; opportunistic discharge; pre-peak urgent charging; lat/lon weather; forecast blending; PV score rebalanced +* **v3.14.19** — Solar yield learning; weekend/weekday consumption patterns; battery cycle cost; pure DP kernel +* **v3.14.0** — 15-min price granularity; optimizer on 96 slots; explainability color coding +* **v3.13.68** — OptimizationEngine (DP scheduler); WebSocket throttle configurable +* **v3.13.58** — Baseload battery correction; RTE learning fixes; WebSocket performance; tiered updates +* **v3.13.49** — Active mode capability; dynamic pricing v2; WebSocket stability; CPU/performance overhaul +* **v3.13.37** — PV detection sticky state; grid charging during PV fix +* **v3.13.28** — Manual IP override; battery-aware baseload; weather-aware discharge; dynamic sunrise/sunset +* **v3.13.14** — Battery Policy driver; ML learning engine; cloud P1/water meter support; trigger cards + +--- + +## 📄 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..003c21dd 100644 --- a/app.js +++ b/app.js @@ -1,9 +1,166 @@ -"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 V8 heap + (if available) process RSS. Memory Warning Limit on Homey +// could be RSS-based rather than heap — try both so we can correlate which bucket +// is actually growing. +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); + let rssPart = ''; + try { + const mu = process.memoryUsage(); + const rss = (mu.rss / 1024 / 1024).toFixed(1); + const ab = (mu.arrayBuffers / 1024 / 1024).toFixed(1); + rssPart = ` rss=${rss}MB ab=${ab}MB`; + } catch (_) { /* sandbox blocks rss */ } + console.log(`[MEM] ${label}: heap=${heap}/${total}MB ext=${ext}MB${rssPart}`); + } catch (e) { + console.log(`[MEM] ${label}: unavailable (${e.message})`); + } +} + +// Global device counter — each driver bumps on onInit so the runtime log shows +// which driver-types contribute how many instances. Useful for triaging crashes +// from users whose device mix differs from ours (e.g. the other user has 2× +// plugin_battery, we don't). +const _deviceCounts = {}; +function bumpDeviceCount(driverId) { + _deviceCounts[driverId] = (_deviceCounts[driverId] || 0) + 1; +} +function logDeviceCounts() { + const entries = Object.entries(_deviceCounts); + if (entries.length === 0) return; + const summary = entries + .sort((a, b) => b[1] - a[1]) + .map(([k, v]) => `${k}=${v}`) + .join(' '); + console.log(`[MEM] devices: ${summary}`); +} + +class HomeWizardApp extends Homey.App { + // Exposed for drivers: this.homey.app.logMem('my-driver:event-name') + logMem(label) { logMem(label); } + bumpDeviceCount(driverId) { bumpDeviceCount(driverId); } + + async onInit() { + this.log('HomeWizard app ready!'); + this.baseloadMonitor = null; + this.p1Source = null; + + // 🔍 CRASH DIAGNOSTICS: Global error handlers + this._setupGlobalErrorHandlers(); + + + if (process.env.DEBUG === '1' && Testing) { + try { + require('inspector').waitForDebugger(); + } + catch (error) { + require('inspector').open(9225, '0.0.0.0', true); + } + } + + // On version change: remove orphaned settings keys left by deleted devices. + // Deferred 30s so all drivers and devices have finished initializing. + const currentVersion = require('./app.json').version; + const lastVersion = this.homey.settings.get('_hw_app_version'); + if (lastVersion !== currentVersion) { + this.log(`[MIGRATE] Version change: ${lastVersion || 'new install'} → ${currentVersion}`); + // Write immediately so a short-lived dev session still persists the version. + // Only write forward (never downgrade) so dev builds don't get overwritten by older installed app. + const isNewer = !lastVersion || currentVersion.localeCompare(lastVersion, undefined, { numeric: true }) > 0; + if (isNewer) { + try { this.homey.settings.set('_hw_app_version', currentVersion); } catch (_) {} + } + setTimeout(() => this._runSettingsMigration(currentVersion), 30_000); + } + } + + _runSettingsMigration(currentVersion) { + try { + const driver = this.homey.drivers.getDriver('battery-policy'); + const activeIds = new Set(driver.getDevices().map(d => d.getData().id)); + if (typeof this.homey.settings.getAll !== 'function') return; + const all = this.homey.settings.getAll(); + let removed = 0; + for (const key of Object.keys(all)) { + if (key.startsWith('batt_mode_hist_')) { + const id = key.replace('batt_mode_hist_', ''); + if (!activeIds.has(id)) { + try { this.homey.settings.unset(key); removed++; } catch (_) {} + } + } + } + if (removed > 0) this.log(`[MIGRATE] Removed ${removed} orphaned batt_mode_hist key(s)`); + } catch (e) { + this.error('[MIGRATE] Settings migration failed:', e.message); + } + } + + _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..caf7bea1 100755 --- a/app.json +++ b/app.json @@ -1,430 +1,8098 @@ { - "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.15.93", + "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": "favorable_consumption_window", + "title": { + "en": "Good time to run appliances starts", + "nl": "Gunstig moment om apparaten aan te zetten begint" + }, + "titleFormatted": { + "en": "Good time to run appliances starts", + "nl": "Gunstig moment om apparaten aan te zetten begint" + }, + "hint": { + "en": "Triggers when electricity becomes cheap (top-3 of the day) or solar panels start producing significantly. Use this to send a notification or switch on a smart plug.", + "nl": "Vuurt als stroom goedkoop wordt (top-3 van de dag) of zonnepanelen significant gaan produceren. Gebruik dit voor een melding of om een slimme stekker in te schakelen." + }, + "tokens": [ + { + "name": "reason", + "type": "string", + "title": { + "en": "Reason", + "nl": "Reden" + }, + "example": { + "en": "Cheap electricity", + "nl": "Goedkope stroom" + } + }, + { + "name": "duration_minutes", + "type": "number", + "title": { + "en": "Duration (minutes)", + "nl": "Duur (minuten)" + }, + "example": 60 + }, + { + "name": "price", + "type": "number", + "title": { + "en": "Current price (€/kWh)", + "nl": "Huidige prijs (€/kWh)" + }, + "example": 0.08 + } + ], + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "id": "favorable_consumption_window_ended", + "title": { + "en": "Good time to run appliances has ended", + "nl": "Gunstig moment om apparaten aan te zetten is voorbij" + }, + "titleFormatted": { + "en": "Good time to run appliances has ended", + "nl": "Gunstig moment om apparaten aan te zetten is voorbij" + }, + "hint": { + "en": "Triggers when electricity is no longer cheap and solar production has dropped below threshold.", + "nl": "Vuurt als stroom niet meer goedkoop is en zonnepanelen onder de drempel produceren." + }, + "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": "appliance_finished", + "title": { + "en": "Appliance finished", + "nl": "Apparaat klaar" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=energy_socket" + } + ], + "tokens": [ + { + "name": "active_minutes", + "type": "number", + "title": { + "en": "Active duration (min)", + "nl": "Actieve duur (min)" + }, + "example": 45 + } + ] + }, + { + "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": "predictive", + "label": { + "en": "Predictive (HW Smart Charging)", + "nl": "Slim laden (HomeWizard)" + } + } + ] + } + ], + "id": "check-battery-mode" + }, + { + "id": "consumption_now_favorable", + "title": { + "en": "Now is !{{a good|not a good}} time to run appliances", + "nl": "Het is !{{nu gunstig|nu niet gunstig}} om apparaten aan te zetten" + }, + "titleFormatted": { + "en": "Now is !{{a good|not a good}} time to run appliances", + "nl": "Het is !{{nu gunstig|nu niet gunstig}} om apparaten aan te zetten" + }, + "hint": { + "en": "True when electricity is currently cheap (top-3 price of the day) or solar panels are producing significantly (>500W).", + "nl": "Waar als stroom nu goedkoop is (top-3 goedkoopste van vandaag) of zonnepanelen significant produceren (>500W)." + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "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": "sdm230-set-battery-to-predictive-mode", + "title": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "titleFormatted": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "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": "sdm630-set-battery-to-predictive-mode", + "title": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "titleFormatted": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "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 trading style", + "nl": "Stel handelsstijl in" + }, + "titleFormatted": { + "en": "Set trading style to [[mode]]", + "nl": "Stel handelsstijl in op [[mode]]" + }, + "hint": { + "en": "Controls how actively the battery trades on price differences. Eco = fewer cycles, prioritise battery health. Balanced = default, normal arbitrage. Aggressive = more cycles, maximise savings.", + "nl": "Bepaalt hoe actief de batterij handelt op prijsverschillen. Eco = minder cycli, batterij ontzien. Balanced = standaard, normaal arbitrage. Agressief = meer cycli, maximale besparing." + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "mode", + "type": "dropdown", + "values": [ + { + "id": "eco", + "label": { + "en": "Eco — fewer cycles, spare the battery", + "nl": "Eco — minder cycli, batterij ontzien" + } + }, + { + "id": "balanced", + "label": { + "en": "Balanced — default", + "nl": "Balanced — standaard" + } + }, + { + "id": "aggressive", + "label": { + "en": "Aggressive — more cycles, maximise savings", + "nl": "Agressief — meer cycli, maximale besparing" + } + } + ] + } + ] + }, + { + "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": "set_away_mode", + "title": { + "en": "Set away mode", + "nl": "Zet afwezigheidsmodus" + }, + "titleFormatted": { + "en": "Set away mode (battery: [[battery_mode]])", + "nl": "Zet afwezigheidsmodus (batterij: [[battery_mode]])" + }, + "hint": { + "en": "Pause learning and disable automatic policy. Use when leaving for vacation or an extended absence.", + "nl": "Pauzeert het leren en schakelt automatische beleid uit. Gebruik bij vakantie of langdurige afwezigheid." + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "battery_mode", + "type": "dropdown", + "title": { + "en": "Battery behaviour", + "nl": "Batterijgedrag" + }, + "values": [ + { + "id": "standby", + "label": { + "en": "Standby (do nothing)", + "nl": "Standby (niets doen)" + } + }, + { + "id": "zero", + "label": { + "en": "Zero (cover own consumption)", + "nl": "Nul op de meter" + } + } + ] + } + ] + }, + { + "id": "set_home_mode", + "title": { + "en": "Set home mode", + "nl": "Zet thuismodus" + }, + "titleFormatted": { + "en": "Set home mode (resume normal operation)", + "nl": "Zet thuismodus (hervatten normale werking)" + }, + "hint": { + "en": "Resume learning and re-enable automatic policy after an absence.", + "nl": "Hervat het leren en schakelt automatische beleid weer in na afwezigheid." + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + } + ] + }, + { + "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_ev_charging", + "title": { + "en": "Set EV charging state", + "nl": "Stel EV-laadstatus in" + }, + "titleFormatted": { + "en": "EV charging is [[state]]", + "nl": "EV laadt [[state]]" + }, + "hint": { + "en": "Tell the policy that an electric vehicle is charging. While active the battery will not discharge (zero_charge_only). PV surplus can still charge the battery. Auto-clears after 8 hours as a safety net.", + "nl": "Geef aan dat een elektrische auto laadt. Zolang actief ontlaadt de batterij niet (zero_charge_only). PV-overschot mag wel de batterij laden. Vervalt automatisch na 8 uur als vangnet." + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=battery-policy" + }, + { + "name": "state", + "type": "dropdown", + "values": [ + { + "id": "start", + "label": { + "en": "Started", + "nl": "Gestart" + } + }, + { + "id": "stop", + "label": { + "en": "Stopped", + "nl": "Gestopt" + } + } + ] + } + ] + }, + { + "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-battery-to-predictive-mode", + "title": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "titleFormatted": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "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", + "presence_mode", + "policy_profit_eur", + "pv_forecast_kwh", + "bias_factor", + "plan_summary" + ], + "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, battery only charges/discharges within configured price limits. When disabled (default), the system uses the economic break-even as the only floor — the optimizer decides timing based on actual prices and RTE. Recommended: DISABLED for most users.", + "nl": "Indien ingeschakeld laadt/ontlaadt de batterij alleen binnen geconfigureerde prijslimieten. Indien uitgeschakeld (standaard) gebruikt het systeem de economische break-even als enige ondergrens — de optimizer bepaalt timing op basis van actuele prijzen en RTE. Aanbevolen: UITGESCHAKELD voor de meeste gebruikers." + }, + "value": false, + "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_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.12, + "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)" + }, + "hint": { + "en": "The battery will never discharge below this price — even if the break-even calculation would allow it. Once the cost model has enough data (≥0.5 kWh tracked), discharge also requires the price to exceed the learned break-even (charge cost ÷ efficiency + half cycle degradation cost). The effective minimum is whichever is higher: this setting or the break-even.", + "nl": "De batterij ontlaadt nooit onder deze prijs — ook niet als de break-even berekening het zou toestaan. Zodra het kostenmodel voldoende data heeft (≥0,5 kWh bijgehouden), is ook vereist dat de prijs boven de geleerde break-even ligt (laadkosten ÷ efficiëntie + halve cyclusdegradatiekosten). De effectieve drempel is de hoogste van deze instelling en de break-even." + }, + "value": 0.22, + "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" + } + }, + { + "id": "peak_shaving_threshold", + "type": "number", + "label": { + "en": "Peak Shaving Threshold (W)", + "nl": "Piekbeveiliging drempelwaarde (W)" + }, + "hint": { + "en": "When the instantaneous house load exceeds this value and the optimizer says 'preserve', the battery will discharge to cover the peak. Set to 0 to disable. Typical value: 1500W (e.g. electric cooking). Set lower than your cooking load.", + "nl": "Als het momentane huisverbruik deze waarde overschrijdt en de optimizer 'bewaren' adviseert, ontlaadt de batterij om de piek te dekken. Stel 0 in om uit te schakelen. Typische waarde: 1500W (bijv. elektrisch koken). Stel lager in dan je kookverbruik." + }, + "value": 0, + "min": 0, + "max": 10000, + "step": 100, + "units": "W", + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "pv_cost_mode", + "type": "dropdown", + "label": { + "en": "PV Charging Cost", + "nl": "PV laadkosten" + }, + "hint": { + "en": "What does it cost to store solar energy? 'Free' (€0): recommended for dynamic tariffs with net metering active (NL until 2027) — storing and exporting are equivalent. 'Export value': use when your fixed contract charges an export penalty, or post-2027 without net metering — enter the net value per kWh you receive for exporting (export compensation minus any export penalty).", + "nl": "Wat kost het om zonne-energie op te slaan? 'Gratis' (€0): aanbevolen bij dynamisch tarief met saldering actief (NL tot 2027) — opslaan en terugleveren zijn equivalent. 'Terugleverwaarde': gebruik bij vast contract met terugleverboete, of post-2027 zonder saldering — vul de netto terugleverwaarde in per kWh (terugleververgoeding minus eventuele boete)." + }, + "values": [ + { + "id": "free", + "label": { + "en": "Free (€0) — dynamic tariff + net metering", + "nl": "Gratis (€0) — dynamisch tarief + saldering" + } + }, + { + "id": "feedin", + "label": { + "en": "Export value (€/kWh) — fixed contract or post-2027", + "nl": "Terugleverwaarde (€/kWh) — vast contract of post-2027" + } + } + ], + "value": "free", + "visible": { + "when": "tariff_type", + "is": "dynamic" + } + }, + { + "id": "feed_in_tariff", + "type": "number", + "label": { + "en": "Net Export Value (€/kWh)", + "nl": "Netto terugleverwaarde (€/kWh)" + }, + "hint": { + "en": "Net value per kWh you receive for exporting: export compensation minus export penalty. Example: €0.12 compensation − €0.14 penalty = −€0.02 → enter 0. Post-2027 without net metering: typically €0.06–0.10 (commodity price only, no energy tax).", + "nl": "Netto bedrag per kWh dat u ontvangt bij teruglevering: terugleververgoeding minus terugleverboete. Voorbeeld: €0,12 vergoeding − €0,14 boete = −€0,02 → vul 0 in. Post-2027 zonder saldering: doorgaans €0,06–0,10 (alleen commodity-prijs, geen energiebelasting)." + }, + "value": 0.08, + "min": 0, + "max": 0.3, + "step": 0.01, + "units": "€/kWh", + "visible": { + "when": "pv_cost_mode", + "is": "feedin" + } + } + ] + }, + { + "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": "Solcast PV Forecast (optional)", + "nl": "Solcast PV-voorspelling (optioneel)" + }, + "visible": { + "when": "pv_estimation_enabled", + "is": true + }, + "children": [ + { + "id": "solcast_enabled", + "type": "checkbox", + "label": { + "en": "Use Solcast for PV forecast", + "nl": "Solcast gebruiken voor PV-voorspelling" + }, + "hint": { + "en": "When enabled, Solcast satellite-based forecasts replace the Open-Meteo radiation model. Requires a free Solcast account with a rooftop site configured. Max 10 API calls/day — the app caches results for 6 hours.", + "nl": "Indien ingeschakeld vervangen Solcast satellietgebaseerde voorspellingen het Open-Meteo stralingsmodel. Vereist een gratis Solcast-account met een geconfigureerde dakinstallatie. Max 10 API-aanroepen/dag — de app cachet resultaten 6 uur." + }, + "value": false + }, + { + "id": "solcast_resource_id", + "type": "text", + "label": { + "en": "Solcast Resource ID", + "nl": "Solcast Resource ID" + }, + "hint": { + "en": "The resource_id of your rooftop site in the Solcast dashboard (e.g. aaaa-bbbb-cccc-dddd).", + "nl": "Het resource_id van uw dakinstallatie in het Solcast-dashboard (bijv. aaaa-bbbb-cccc-dddd)." + }, + "value": "", + "visible": { + "when": "solcast_enabled", + "is": true + } + }, + { + "id": "solcast_api_key", + "type": "text", + "label": { + "en": "Solcast API Key", + "nl": "Solcast API-sleutel" + }, + "hint": { + "en": "Your Solcast API key. Find it in the Solcast dashboard under Account → API Key.", + "nl": "Uw Solcast API-sleutel. Te vinden in het Solcast-dashboard onder Account → API-sleutel." + }, + "value": "", + "visible": { + "when": "solcast_enabled", + "is": true + } + } + ] + }, + { + "type": "group", + "label": { + "en": "KNMI Station Data", + "nl": "KNMI Stationsdata" + }, + "children": [ + { + "id": "knmi_api_key", + "type": "text", + "label": { + "en": "KNMI API Key", + "nl": "KNMI API-sleutel" + }, + "hint": { + "en": "Optional. KNMI EDR API key for ground-truth solar radiation from nearest weather station. Register at dataplatform.knmi.nl.", + "nl": "Optioneel. KNMI EDR API-sleutel voor gemeten zonnestraling van dichtstbijzijnde weerstation. Registreer op dataplatform.knmi.nl." + }, + "value": "" + } + ] + }, + { + "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 + } + ] + }, + { + "type": "group", + "label": { + "en": "24-Hour Planning", + "nl": "24-Uurs Planning" + }, + "children": [ + { + "id": "price_resolution", + "type": "dropdown", + "label": { + "en": "Price Resolution", + "nl": "Prijsresolutie" + }, + "hint": { + "en": "Set to 15 minutes for providers with quarter-hour prices (e.g. Xadi). Set to 1 hour for providers with hourly prices (e.g. Zonneplan).", + "nl": "Zet op 15 minuten voor providers met kwartierprijzen (bijv. Xadi). Zet op 1 uur voor providers met uurprijzen (bijv. Zonneplan)." + }, + "value": "15min", + "values": [ + { + "id": "15min", + "label": { + "en": "15 minutes", + "nl": "15 minuten" + } + }, + { + "id": "1h", + "label": { + "en": "1 hour", + "nl": "1 uur" + } + } + ] + } + ] + } + ], + "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", + "alarm_water" + ], + "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 + } + ] + }, + { + "type": "group", + "label": { + "en": "Appliance detection", + "nl": "Apparaat detectie" + }, + "children": [ + { + "id": "appliance_detection_enabled", + "type": "checkbox", + "label": { + "en": "Enable appliance detection", + "nl": "Apparaat detectie inschakelen" + }, + "value": false + }, + { + "id": "active_threshold_w", + "type": "number", + "label": { + "en": "Active threshold (W)", + "nl": "Actief-drempel (W)" + }, + "value": 50, + "min": 1 + }, + { + "id": "standby_threshold_w", + "type": "number", + "label": { + "en": "Standby threshold (W)", + "nl": "Stand-by drempel (W)" + }, + "value": 5, + "min": 0 + }, + { + "id": "min_active_minutes", + "type": "number", + "label": { + "en": "Min active duration (min)", + "nl": "Min actieve duur (min)" + }, + "value": 5, + "min": 0 + }, + { + "id": "trigger_on_poweroff", + "type": "checkbox", + "label": { + "en": "Also trigger on power-off (0W)", + "nl": "Ook triggeren bij volledige uitschakeling (0W)" + }, + "value": 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": "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": "predictive", + "label": { + "en": "Predictive (HW Smart Charging)", + "nl": "Slim laden (HomeWizard)" + } + } + ] + }, + { + "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, + "min": 5, + "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", + "alarm_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 + } + ] + }, + { + "type": "group", + "label": { + "en": "Leak detection", + "nl": "Lekdetectie" + }, + "children": [ + { + "id": "leak_flow_lpm", + "type": "number", + "label": { + "en": "Flow threshold (L/min)", + "nl": "Drempelwaarde (L/min)" + }, + "hint": { + "en": "Continuous flow above this value triggers leak detection. Default: 15 L/min.", + "nl": "Aanhoudende doorstroom boven deze waarde activeert lekdetectie. Standaard: 15 L/min." + }, + "value": 15 + }, + { + "id": "leak_duration_minutes", + "type": "number", + "label": { + "en": "Duration before alarm (minutes)", + "nl": "Duur voor alarm (minuten)" + }, + "hint": { + "en": "How many minutes of continuous flow before triggering the alarm. Default: 60.", + "nl": "Hoeveel minuten aanhoudende doorstroom voor het alarm. Standaard: 60." + }, + "value": 60 + } + ] + } + ], + "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" + } + ], + "widgets": { + "battery-planning": { + "name": { + "en": "Battery Planning", + "nl": "Batterij Planning" + }, + "height": 300, + "transparent": false, + "api": { + "getPlanningData": { + "method": "GET", + "path": "/" + } + }, + "id": "battery-planning", + "settings": [] + } + }, + "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" + } + }, + { + "id": "predictive", + "title": { + "en": "Predictive (HW Smart Charging)", + "nl": "Slim laden (HomeWizard)" + } + } + ] + }, + "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" + }, + "bias_factor": { + "type": "number", + "title": { + "en": "PV Intraday Bias", + "nl": "PV Intradag Bias" + }, + "desc": { + "en": "Intraday PV correction ratio (actual/forecast)", + "nl": "Intradag PV-correctiefactor (actueel/voorspeld)" + }, + "decimals": 2, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "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" + }, + "plan_summary": { + "type": "string", + "title": { + "en": "Plan Summary", + "nl": "Plan Samenvatting" + }, + "desc": { + "en": "Today's optimizer plan in compact form", + "nl": "Compact overzicht van het optimizerplan" + }, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": false, + "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" + }, + "policy_profit_eur": { + "type": "number", + "title": { + "en": "Expected Profit", + "nl": "Verwachte Winst" + }, + "desc": { + "en": "Projected daily profit from today's optimizer plan", + "nl": "Verwachte dagwinst van het optimizerplan" + }, + "units": { + "en": "€", + "nl": "€" + }, + "decimals": 2, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "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" + }, + "presence_mode": { + "type": "string", + "title": { + "en": "Presence", + "nl": "Aanwezigheid" + }, + "desc": { + "en": "Indicates whether someone is home. Away mode pauses consumption learning so vacation patterns do not corrupt learned profiles.", + "nl": "Geeft aan of iemand thuis is. Afwezigheidsmodus pauzeert het verbruikspatroon zodat vakantiegedrag de geleerde profielen niet verstoort." + }, + "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" + } + } + ] + }, + "pv_forecast_kwh": { + "type": "number", + "title": { + "en": "PV Forecast Today", + "nl": "PV Voorspelling Vandaag" + }, + "desc": { + "en": "Blended PV yield forecast for today", + "nl": "Gecombineerde PV opbrengst voor vandaag" + }, + "units": { + "en": "kWh", + "nl": "kWh" + }, + "decimals": 1, + "getable": true, + "setable": false, + "uiComponent": "sensor", + "insights": true, + "icon": "assets/icon.svg" + }, + "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/docs/baseload-monitor.md b/docs/baseload-monitor.md new file mode 100644 index 00000000..24d382b3 --- /dev/null +++ b/docs/baseload-monitor.md @@ -0,0 +1,37 @@ +# BaseloadMonitor & Battery Correction + +`BaseloadMonitor.updatePower(gridPower, batteryPower)` corrects for batteries holding the P1 meter at ~0W: + +```text +householdPower = gridPower - batteryPower +// batteryPower > 0 = charging, batteryPower < 0 = discharging +``` + +**Per driver:** + +- **`energy_v2`**: always uses `plugin_battery` as primary source (most accurate — reads directly from battery API). Falls back to `measure_power.battery_group_power_w` only when no `plugin_battery` devices are present. **Do NOT rely solely on the P1 capability** — `payload.power_w ?? 0` sets it to `0` when the P1 firmware doesn't report battery state, making it indistinguishable from a genuinely idle battery. +- **`energy`**: sums `measure_power` across all `plugin_battery` devices ✓ +- **`SDM230-p1mode` / `SDM630-p1mode`**: register and feed the BaseloadMonitor directly + +**Critical rule:** Never source battery power for BaseloadMonitor from `battery-policy` — that device is optional and not always active. Use `plugin_battery` driver directly: + +```js +const battDriver = this.homey.drivers.getDriver('plugin_battery'); +// sum dev.getCapabilityValue('measure_power') across getDevices() +``` + +**`plugin_battery` capability convention:** `measure_power` is watts, positive = charging, negative = discharging. + +**Near-zero detection:** When `batteryPower` is known (not null), `_detectNearZeroLong()` skips those samples entirely — the corrected `householdPower` is a real measurement, not grid balancing noise. A 50W baseload house still looks near-zero on the raw grid when the battery is discharging. + +**Oscillation detection:** `_detectOscillation()` trims 1 outlier from each end (sorts values, uses `sorted[1]` and `sorted[length-2]`) before computing the range. A single bad sample from a battery mode-transition measurement lag (polling at 10s, sample stored at 30s) must not invalidate an otherwise clean night. + +**Multiple P1 meters:** `BaseloadMonitor` uses a single master device (`trySetMaster()` — first to register wins). All P1s call `updatePowerFromDevice()` but only the master's data is processed. Both P1s query `plugin_battery` but only master's result matters. + +**Night diagnostics:** `_finalizeNight()` logs one summary line per night: + +```text +[BaseloadMonitor] night 2026-03-28: 48 samples, 48 with battery data, avgGrid=2W, avgHousehold=387W, invalid=false +``` + +`0 with battery data` = battery not seen at night = no correction = likely `sawNearZeroLong` invalid. diff --git a/docs/battery-policy.md b/docs/battery-policy.md new file mode 100644 index 00000000..5497ab7d --- /dev/null +++ b/docs/battery-policy.md @@ -0,0 +1,132 @@ +# Battery Policy Device + +- Loads 6 engines at startup: LearningEngine, WeatherForecaster, PolicyEngine, TariffManager, EfficiencyEstimator, OptimizationEngine — all eagerly required +- ExplainabilityEngine and BatteryChartGenerator are **lazy-loaded** (first `_runPolicyCheck` call, ~15 min after startup) +- `batteryPower` sign convention: positive = charging, negative = discharging +- `_runPolicyCheck` runs every 15 min + at hour boundaries +- Weather fetched at startup (restored from settings cache if within `weather_update_interval`); refreshed every `weather_update_interval` hours (setting, default 3h, min 1h) during `_runPolicyCheck` +- Weather location stored as `weather_latitude` + `weather_longitude` (number fields). Old `weather_location` text field (city name or `lat,lon`) is migrated on first `onInit` via `_migrateWeatherLocation()` +- `WeatherForecaster._blendForecast(oldCache, newForecast)` blends new API data with previous cache (α=0.6) to smooth sudden Open-Meteo model-run changes. Past `dailyProfiles` slots are never blended (actual data). +- `_schedulePriceRefresh`: every 15 min between 14:00–16:00, every 30 min otherwise + +## WeatherForecaster — Open-Meteo Data + +- API uses hourly resolution (`temporal_resolution` is NOT set — do NOT add `minutely_15`) +- `_processForecast` builds `hourlyForecast` (36 future slots) and `dailyProfiles` (all 24h today + tomorrow, including past hours from `past_days=1`) +- `daily.sunrise/sunset` returned as `"YYYY-MM-DDTHH:MM"` (no timezone suffix when `timezone=UTC`) — must append `Z`: `new Date(\`${v}Z\`)` +- **`past_days=1` daily index offset:** with `past_days=1` the `daily` array is `[yesterday, today, tomorrow]`. `todaySunrise/Sunset` must use index `[1]` and `tomorrowSunrise/Sunset` index `[2]`. Using `[0]` gives yesterday's sunset → `afterSunset` triggers all day → pvEstimate ignored → PV state oscillates. Do NOT revert to `[0]`. +- **Sunrise boundary correction:** Open-Meteo averages radiation over the full 60-min slot, diluting the first slot where sunrise falls mid-hour. `_processForecast` corrects this by scaling `radiationWm2 × 60 / minutesOfSunInSlot` for both `hourlyForecast` and `dailyProfiles`. Applied to all sunrises in the daily array (today + tomorrow). Slots with `sunMinutes < 2` or `> 58` are left untouched (no meaningful boundary or near-full-hour). +- `dailyProfiles` hourly entries power the past-hours PV chart via learned yield factors in device.js `pvForecastByDay`; `hourlyForecast` powers the future-hours PV forecast and optimizer + +## Policy Scores & PV OVERSCHOT + +**Policy scores:** normal range ~20–300. PV OVERSCHOT (`_pvStoreWins`) adds charge +250 — intentionally high to dominate most preserve combinations, but capped to stay readable. Do NOT raise back to 1000. + +**PV OVERSCHOT low-SoC margin:** + +- When SoC < 50%, the export margin must exceed €0.02/kWh (`_lowSocMarginRequired`) for export to win over storing. Below that threshold, `_pvStoreWins = true` — the marginal export revenue is not worth keeping the battery half-empty when a higher-priced discharge slot is coming later. +- At SoC ≥ 50% the threshold is €0.00 (no bias — pure price comparison). + +## OptimizationEngine (DP) Discharge Constraints + +- `minDischargePrice` is a hard constraint in the DP backward induction — discharge is blocked when `price < minDischargePrice`. This keeps the DP schedule consistent with `_mapActionToHwModeForPlanning` (which shows `standby` for those slots). Without this guard the DP discharges internally but the chart shows standby with an unexplained SoC drop. +- **PV discharge block removed:** the old `pvCoverage[t] <= 0.5` guard has been removed. The `minDischargePrice` floor already prevents irrational low-price PV-hour discharge. Do NOT re-add the pvCoverage discharge block. +- In strict mode (`respect_minmax=true`): DP uses `min_discharge_price` as the floor. +- In dynamic mode (`respect_minmax=false` or `policy_mode='balanced-dynamic'`): DP uses `opportunistic_discharge_floor` (default €0.20). +- `buildPlanningSchedule` uses the same effective floor so the planning display matches the DP schedule. +- **`opportunistic_discharge_floor` tuning:** the default €0.20 allows marginal overnight slots (e.g. €0.22) that add ~€0.002/slot while consuming battery cycles before a better morning peak. Raise to €0.25–0.27 to suppress these. +- **Per-SoC flattening (overnight multi-cycle):** In `_runBackwardDP`, the dp array is flattened per SoC level: if `pvKwhFromT[t+1] >= (maxSoc - socG) / 100 × capacityKwh`, then `dp[socG] = dpMax`. This lets the DP discharge at all profitable overnight slots and rely on daytime PV to recharge, instead of hoarding for a single "best" night slot. Guards: positive price, `pvKwhTomorrow >= 0.8 × cap`, non-PV slot, next slot not PV-strong. **Do NOT revert to `dp.fill(dpMax)` with `_betterSlotAhead` gate** — that caused overnight standby when pre-dawn prices rose into a PV-covered morning peak. + +## `_selectMode` Confidence Calculation + +- Formula: `winner / total * 100`. With 3 options this systematically underestimates confidence when a non-executable option inflates the denominator. +- **Charge-blocked fallback:** when charge wins on score but `_mapPolicyToHwMode` returns `standby` (price > maxChargePrice, no PV), re-evaluate using only discharge vs preserve. Confidence = `max(discharge, preserve) / (discharge + preserve)`. Logs `[MAPPING][CHARGE-BLOCKED]`. +- **Unexecutable charge in denominator:** when discharge or preserve wins, if charge is not executable (`_isChargeExecutable()` returns false), exclude charge score from denominator. This prevents a large charge score from diluting confidence in a clear discharge decision (e.g. charge=67, discharge=85, preserve=20 → 49% without fix → 81% with fix). +- `_isChargeExecutable(ctx)`: returns true when `price <= maxChargePrice` OR PV is active (same logic as `_mapPolicyToHwMode` actualPvNow, without logging). + +## `_applyPeakTimingGuard` — PV Free-Cycle Bypass + +- PeakTiming suppresses discharge when `price < bestFuturePrice × RTE (0.72)`. This assumes recharge costs money (grid). When recharge is free (PV), the RTE penalty is irrelevant. +- **Bypass condition:** `pvKwhRemaining >= storedKwh` where `storedKwh = battCapKwh × (soc - minSoc) / 100`. If remaining PV today can cover what's currently stored, PeakTiming is skipped entirely. The floor gate (`profitableToDischarge`) still applies. +- PeakTiming re-engages in the evening when `pvKwhRemaining` drops below `storedKwh` (sun has set / nearly done). + +## `_schedulePolicyCheck` — Slot Boundary Alignment + +- `setInterval` starts after a `setTimeout` that aligns to the next UTC 15-min boundary (`:00`, `:15`, `:30`, `:45`). EPEX 15-min price slots are keyed to these UTC boundaries. +- Without alignment, the interval drifts ~11 min into each slot (based on app start time), leaving only ~4 min of discharge per slot. +- A `_slotAlignTimeout` is cleared/reset in `_schedulePolicyCheck` and `onUninit`. +- Hour-boundary run (`:00:05`) remains as fallback for immediate hour-change price updates. + +## `_mapPolicyToHwMode` Discharge Gate + +- `balanced-dynamic` mode is now handled inside the balanced+dynamic block (same as `balanced`). Previously it fell through to the catch-all (`standby`), causing a plan/reality mismatch. +- `buildPlanningSchedule` has always normalised `balanced-dynamic` → `balanced`; `_mapPolicyToHwMode` now does the same. +- `profitableToDischarge = price >= effectiveFloor && (!costModelActive || price >= breakEven)` +- **Break-even includes cycle cost** (consistent with optimizer): `breakEven = avgCost / effectiveEff + cycleCostPerKwh * 0.5`. Do NOT remove the cycle cost term. +- **Effective floor** — strict mode: `max(min_discharge_price, breakEven)`; opportunistic mode: `max(opportunistic_discharge_floor, breakEven)`. +- `min_discharge_price` default is **€0.22** (was €0.30 — too high for typical NL evening prices). All fallbacks in policy-engine.js use €0.22. +- SoC history in `policy_mode_history` uses `battery_soc_mirror` capability — do NOT use `measure_battery` (does not exist on this device). + +**PV store → `zero_charge_only` mode:** When `_pvStoreWins && actualPvNow && !_chargeUrgent`: returns `zero_charge_only`. Placed BEFORE the `pvEstimate >= 400` check so it takes priority when PV storing is optimal. + +**Discharge + PV → `zero` mode:** When `policyMode === 'discharge'` and `profitableToDischarge` and `actualPvNow` and `soc > minSoc`: returns `zero` instead of `zero_discharge_only`. The delay-charge discharge path is excluded — it intentionally exports PV at high prices. + +## PV Detection + +**Sunset guard:** + +- `_estimatePvProduction` uses EMA smoothing (α=0.4), which causes `pvEstimate` to decay slowly (~2 hours after sunset to drop below 100W threshold). +- **Fix 1 (device.js):** When flow card reports 0W, `_lastPvEstimateW` is reset to 0 immediately. +- **Fix 2 (policy-engine.js):** After `todaySunset + 30min`, `pvEstimate >= 100` is **ignored** — only hard signals can prove PV is active. +- **Do NOT remove the sunset guard** — without it, discharge uses `zero` instead of `zero_discharge_only` for ~2 hours after sunset. + +**`virtualGridPower` — battery discharge correction:** + +- `virtualGridPower = gridPower - batteryPower` always. Do NOT use `gridPower` directly when discharging. +- Old bug: used `gridPower` unmodified when discharging — if batt=-337W, house=117W, P1 shows -220W (apparent export) even with no PV. +- Correct: `virtual = gridPower - batteryPower`. Discharging -337W with grid -220W → virtual = +117W → no export detected. + +## Optimizer & Consumption + +**Discharge SoC delta:** per-slot based on learned house consumption, not a fixed max discharge power. `min(maxDischargePowerW, consumptionWPerSlot[t])` — falls back to `maxDischargePowerW` when no consumption data. + +**`pvKwhTomorrow`:** Calculated in `_recomputeOptimizer` from learned `pvForecast` array. Used as `strongSunTomorrow`: + +- `>= 4h` equivalent: `sunshineTomorrow >= 4 || pvKwhTomorrow >= battCapKwh * 0.8` +- `>= 6h` equivalent: `sunshineTomorrow >= 6 || pvKwhTomorrow >= battCapKwh * 1.5` +- **Post-peak charge bonus:** `_applyDayAhead` suppresses the +40 "cheap hours tonight" bonus when `strongSunTomorrow`. + +## Charge-Deferral + +- `cheaperHourComing` kijkt **12 uur** vooruit. Vuurt als er een slot is dat ≥€0.005 goedkoper is. Geeft preserve +80. +- **Optimizer-check (12–24u vangnet):** als `inputs.optimizer?.getSlot(new Date()) === 'preserve'`, geeft ook preserve +80. Dit vangt goedkope middag-PV-slots op buiten het 12u-venster. +- Dezelfde optimizer-check staat in het **arbitrage-blok**: als optimizer 'preserve' geeft, wordt charge +80 niet toegevoegd maar preserve +20 in plaats daarvan. +- **Plan-vs-realiteit mismatch:** als `result.policyMode !== _dpAction` logt device.js `⚠️ PLAN AFWIJKING`. `battery_policy_state.dpAction` bevat de DP-geplande actie. + +## Settings Keys + +**PV:** + +- `policy_pv_actual_today` — hourly actual PV from flow card `{date, hourly[], sums[], counts[]}` (Amsterdam tz, resets daily) +- `policy_pv_forecast_hourly` — `[{h: pvW}, {}]` for today + tomorrow, written after every optimizer run +- `battery_policy_state.pvLearnedSlots` — count of non-null solar yield factor slots + +**State:** + +- `battery_policy_state.dpAction` — DP-geplande actie voor huidig slot (`'charge'`/`'discharge'`/`'preserve'`) +- `battery_policy_state.avgCost` — energy-weighted average purchase cost (€/kWh) of stored energy + +**Consumption:** + +- `consumptionWPerSlot[t]` built from `learningEngine.getPredictedConsumption(slotTime)`, floored by baseload +- `sampleCount` added per slot using `learningEngine.getConsumptionSampleCount(slotTime)` +- `inputs.consumptionW` — current-hour prediction, available in PolicyEngine and ExplainabilityEngine + +**`policy_daily_profit`:** Array of `{date, discharge, charge, net}` (max 30 entries), computed lazily from `policy_mode_history` when settings page opens. Only completed calendar days. + +## Planning Settings Page + +- Day SVG curve (`renderPvCurve`): gestippelde witte lijn toont geschat huisverbruiksprofiel. Alleen zichtbaar als ≥12 uren consumption data beschikbaar. +- Uurkaart details: `🏠 X.X kW` per uur (uurgemiddelde van consumption slots). +- Confidence badge: 🔴 `geen verbruiksdata` (0 samples) of 🟡 `N samples` (1–3) — alleen zichtbaar bij < 4 samples. +- Dagbalk: `💰 ~€X.XX netto` — discharge opbrengst minus laadkosten (gebruikt `avgCost` als beschikbaar, anders spotprijs). diff --git a/docs/explainability-engine.md b/docs/explainability-engine.md new file mode 100644 index 00000000..729df47a --- /dev/null +++ b/docs/explainability-engine.md @@ -0,0 +1,14 @@ +# ExplainabilityEngine + +- `generateExplanation()` builds a `reasons[]` array; each reason has `category`, `icon`, `text`, `impact`, `supportedMode` +- **Weather PV tekst:** gebruik `weather.pvKwhRemaining` (resterend vandaag) boven `weather.pvKwhToday` (totaal) — totaal is misleidend als de dag al half voorbij is +- **Redundantie PV + delay-charge:** als `_delayCharge` actief is én PV-overschot al een `preserve`-reden heeft toegevoegd, sla de aparte strategy-reden over — ze zeggen hetzelfde +- **Geen conclusie in reason-tekst:** schrijf niet "Stand-by aanbevolen." in een reason; dat is de aanbeveling zelf, niet een reden +- `_addDelayChargeReasons` checkt `reasons.some(r => r.category === 'pv' && r.supportedMode === 'preserve')` om redundantie te voorkomen +- **`inputs.optimizerSlots`** — optimizer schedule (`_schedule.slots`) wordt door `_runPolicyCheck` in `inputs` gezet vóór de `generateExplanation()`-aanroep; beschikbaar als `[{timestamp, action, price, socProjected, consumptionW, sampleCount}]` +- **`inputs.consumptionW`** — geschat huisverbruik (W) voor het huidige uur, via `learningEngine.getPredictedConsumption(now)`. Gebruikt in `_addArbitrageReasons` voor ontlaad-opbrengst annotatie. +- **Standby bij preserve:** `_addModeSpecificReasons` toont de volgende geplande ontlaadmomenten (tot 3) met tijdstip en prijs — "Batterij bewaard voor geplande ontlaadbeurten: 19:00 (€0.381), ..." +- **to_full:** `_addModeSpecificReasons` toont laadrijs, beste toekomstige ontlaadslot en nettomarge na cycluskosten — "Laden van net: €0.121 → ontladen 19:00 bij €0.341. Nettomarge: €0.145/kWh." +- **Goedkoop laden (`_addArbitrageReasons`):** voegt beste toekomstige discharge-slot toe uit `inputs.optimizerSlots` — "Laden is goedkoop: prijs (€0.121) onder break-even (€0.243). Beste ontlaadslot: 19:00 bij €0.381 → nettomarge ~€0.260/kWh." +- **Winstgevend ontladen (`_addArbitrageReasons`):** voegt geschat verbruik en uurlijkse opbrengst toe — "Ontladen is winstgevend: prijs (€0.381) boven break-even (€0.121). Geschat verbruik: 0.8 kW → ~€0.305/u." +- **Tijdzone:** gebruik `toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' })` voor uur-checks (avonduren 17–22, nachturen 2–6) — NIET `getHours()` want dat geeft UTC terug op Homey. diff --git a/docs/learning-engine.md b/docs/learning-engine.md new file mode 100644 index 00000000..5a13a173 --- /dev/null +++ b/docs/learning-engine.md @@ -0,0 +1,33 @@ +# Learning Engine + +- Records consumption from `gridPower` only when `gridPower > 0` — intentional; battery discharge is not recorded separately +- `pv_predictions` capped at 300 entries (only last 100 used for accuracy calc via `slice(-100)`) +- `getPredictedConsumption()` falls back: specific day → day-group (weekday/weekend) → all days → 0 +- Consumption forecast in optimizer: `learned > 0 ? learned : baseloadW` — baseload only used when slot has no learned data at all +- `getConsumptionSampleCount(targetTime)` — returns raw sample count (0–100) for a specific day-of-week + hour. Count < 4 means below direct-use threshold (learning engine falls back to group average). Used for confidence badges in planning UI. + +## Solar Yield Factors + +- 96 slots (15-min), stored as `yieldFactor = W_actual / radiationWm2` +- EMA α=0.10 (symmetric — asymmetric EMA was tried and caused upward drift to 4.6kW on 3500Wp, do NOT reintroduce) +- `getSolarYieldFactorsSmoothed()` does linear gap-fill + 3-pass triangular smoothing (0.25/0.5/0.25) — raw factors are never modified +- Used in pvForecast when `learnedSlots >= 10`; below 10 falls back to `pvCapacity × pv_performance_ratio × (radiation/1000)` +- `pv_performance_ratio` is **irrelevant** once ≥ 10 slots are learned — the yield factors absorb capacity, orientation, PR and shading +- `getRadiationBiasFactor()` returns 1.0 until ≥ 3 daily samples are recorded (prevents bad single-day ratios from taking effect) +- **`getRadiationBiasFactor()` returns 1.0 when `learnedSlots >= 10`** — yield factors already absorb the Open-Meteo vs actual relationship; applying bias on top double-counts the correction. Do NOT remove this guard. + +## Radiation Bias + +- Compares yesterday's forecasted vs actual GTI/GHI daily average; updates via EMA α=0.15, clamped 0.3–2.0 +- Ratio capped at 3.0 per sample to limit single-day outliers +- Skips samples where `forecastAvgWm2 < 30` — protects against corrupt snapshots (Open-Meteo returning near-zero values) +- `radiation_bias_reset_v1` flag: one-time reset if stored factor > 1.5 at startup (corrupted data guard) +- Factor clamps at 2.0 (max). If still at 2.0 after several weeks of operation, Open-Meteo structurally underestimates GTI for this location — monitor in spring as yield data accumulates +- **Cache caveat:** `getRadiationBiasFactor()` is only called inside `_processForecast()`. When weather is restored from settings cache, `_processForecast()` is skipped — bias baked into cache is from the original fetch moment +- Startup log: `[LearningEngine] radiation_bias_factor=X.XXX (N samples, M yield slots — ACTIVE / inactive: yield factors in use)` + +## Solar Yield Slots & DST + +- Slots are indexed on UTC (`getUTCHours()`) — consistent across DST transitions +- At spring/autumn DST, the slot corresponding to peak-sun shifts by 1 in local time; self-corrects within days via EMA +- Yield factor index in device.js pvForecast mapping: `d.getUTCHours() * 4` (hourly data always on the hour, minutes = 0) diff --git a/docs/planning-chart.md b/docs/planning-chart.md new file mode 100644 index 00000000..7e8323ff --- /dev/null +++ b/docs/planning-chart.md @@ -0,0 +1,51 @@ +# Planning Chart Camera Images + +Two camera images are registered on the battery-policy device: **"Batterij Vandaag"** and **"Batterij Morgen"** (`planning_today` / `planning_tomorrow`). Generated via [quickchart.io](https://quickchart.io) POST API after every `_saveWidgetData()` call. + +**Architecture:** + +- `_saveWidgetData()` in `device.js` → builds `compact` (slots = past + future) → calls `_updatePlanningChart(compact)` +- `_updatePlanningChart()` splits slots by Amsterdam calendar day → stores as `_chartToday` / `_chartTomorrow` +- `image.setStream()` callback calls `ChartRenderer.streamPlanningChart()` (in `lib/chart-renderer.js`) when Homey UI requests the image +- `image.update()` signals new data available; stream fires lazily on next UI request + +**`ChartRenderer` module** (`lib/chart-renderer.js`) holds all pure builders + HTTP helpers: + +- `buildPlanningChartConfig(compact)` → planning bars + SoC + PV chart config +- `buildPvChartConfig(pvData)` → PV forecast vs actual (today + tomorrow) +- `buildModeChartBody(modeHistory)` → 24h stacked-bar mode timeline (returns JSON body string) +- `streamPlanningChart(stream, compact)` / `streamPvChart(stream, pvData)` / `streamModeChart(stream, body)` → POST to quickchart.io, pipe response to stream +- `configToJs(val)` — custom serializer that preserves JS function values (unlike `JSON.stringify` which drops them). Required because quickchart.io evaluates the `chart` field as a JS expression string, not JSON. + +Device.js keeps camera-lifecycle methods only (`_initPvCamera`, `_initModeHistoryCamera`, `_updatePlanningChart`, `_updateModeChart`, `_buildModeChartBody` thin wrapper). + +- **Do NOT use `generateLabels`** in legend config — `Chart.defaults` is not available in quickchart.io's eval context and breaks rendering entirely. Use `filter: function(item) { ... }` instead. + +**Chart structure (Chart.js 4, mixed):** + +- Bar dataset `_prijs`: price bars colored by mode (hidden from legend via `filter` on `_` prefix) +- Line `SoC (%)`: white dashed, right axis 0–100% +- Line `PV` (solid yellow): past slots only (`ts < now`), `fill: true` +- Line `_pv_forecast` (dashed yellow): future slots only, first point = last past slot for seamless join (hidden from legend) +- Phantom bar datasets per mode present: empty `data: []`, used only for legend color swatches +- Scale `yPrice` (left), `ySoc` (right, white), `yPv` (right, yellow, max = `pv_capacity_w` setting) + +**`_schedulePriceRefresh` timezone:** uses `toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' })` — NOT `new Date().getHours()` which returns UTC on Homey. + +--- + +# Battery Planning Dashboard Widget + +Widget lives in `widgets/battery-planning/`. Renders price step chart + PV area + SoC strip + mode icon bar inside an inline SVG. Data comes from `policy_widget_data` (settings) and `planning-update` (realtime). + +**SVG theme-aware colors:** SVG `stroke` and `fill` attributes are strings — they cannot use CSS variables directly. Detect the color scheme in JS before building the SVG: + +```js +const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; +const priceLineColor = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.75)'; +const nowMarkerColor = isDark ? 'white' : '#333'; +``` + +- **Do NOT hardcode `stroke="white"`** for the price line or "now" marker — invisible on light background. +- `currentColor` works for SVG elements that inherit CSS `color`, but not for explicit `stroke=` attributes on path/line elements. +- CSS `@media (prefers-color-scheme: dark)` only applies to elements using `currentColor` or CSS classes, not inline SVG attribute values. diff --git a/docs/price-providers.md b/docs/price-providers.md new file mode 100644 index 00000000..b29bc749 --- /dev/null +++ b/docs/price-providers.md @@ -0,0 +1,24 @@ +# Price Providers + +- **MergedPriceProvider** combines Xadi + KwhPrice; 1-hour in-memory cache + persistent settings cache +- **XadiProvider / KwhPriceProvider**: fetch day-ahead prices; each has its own cache +- `fetchPrices(force = false)` respects cache; pass `force = true` to bypass +- 15-min prices: native when available, expanded from hourly as fallback +- Prices saved to `homey.settings` as `policy_all_prices` and `policy_all_prices_15min` + +**`homey.settings` API:** `.get()`, `.set()`, `.unset()` are all **synchronous** — they do NOT return a Promise. Never call `.catch()` on them. + +**`battery_group_charge_mode` capability:** `updateCapability()` has a self-healing guard — if the capability is missing when a battery event arrives, it calls `safeAddCapability()` before `setCapabilityValue()`. This handles devices where the capability was removed by the no-battery path and never re-added. + +**`predictive` mode (HW Slim laden):** HomeWizard firmware exposes `mode: 'predictive'` in `GET /api/batteries` when HW's own Smart Charging is active. `getMode()` in `Api.js` detects this before the permissions switch and returns `'predictive'`. `_applyRecommendation()` in battery-policy skips the mode write entirely when `actualMode === 'predictive'`. The `battery_group_charge_mode` enum includes `'predictive'` as a valid value so `setCapabilityValue` does not fail. + +**KwhPriceProvider scraping:** kwhprice.eu renders prices via Chart.js (JavaScript arrays), NOT an HTML table. `_parseHtml` extracts `labels: ["00:00-00:15", ...]` and `data: [0.057, ...]` arrays from the page source. Prices are raw EPEX spot (excl. VAT/markup) — `(spot + markup) × 1.21` is applied client-side. The page contains **two datasets**: today (96 slots) and tomorrow (96 slots, available all day). Both are parsed — `matchAll` extracts all `labels`/`data` occurrences. + +- **Negative prices:** the `data:` regex must include `-` in the character class: `/\bdata:\s*\[([-\d.,\s\n]+)\]/g`. Without it, any dataset containing negative spot prices is not matched and the entire tomorrow dataset is silently dropped. +- **Zero/negative price filter:** use `!isNaN(spotEur)` as the slot inclusion guard, NOT `spotEur > 0`. Filtering out negatives/zeros drops valid market data and can cause tomorrow's dataset to appear empty. + +## MergedPriceProvider Cache Race + +- `_loadCache()` is async and not awaited in the constructor +- `fetchPrices()` must await `this._cacheLoadPromise` before checking `this.cache`, otherwise the settings cache is missed on every restart and both Xadi + KwhPrice are fetched unnecessarily +- Pattern: `this._cacheLoadPromise = this._loadCache()` in constructor, `await this._cacheLoadPromise; this._cacheLoadPromise = null` at start of `fetchPrices()` diff --git a/docs/weather-forecasting.md b/docs/weather-forecasting.md new file mode 100644 index 00000000..e1e8b72c --- /dev/null +++ b/docs/weather-forecasting.md @@ -0,0 +1,119 @@ +# Weather & PV Forecasting + +Multi-source weather/PV forecasting feeds the optimizer's DP schedule, the PV-OVERSCHOT detection, and the per-day yield charts. + +## Data sources + +### Open-Meteo ensemble (`WeatherForecaster.fetchForecast` → `_mergeApiResponses`) + +Four models fetched via `models=meteofrance_arpege_europe,gfs_seamless,icon_seamless,knmi_harmonie_arome_netherlands` on `api.open-meteo.com/v1/forecast`: + +| Model | Source | Region focus | +|---|---|---| +| `meteofrance_arpege_europe` | Météo-France ARPEGE | Europe | +| `gfs_seamless` | NOAA GFS | Global | +| `icon_seamless` | DWD ICON | Europe / Global | +| `knmi_harmonie_arome_netherlands` | KNMI HARMONIE-AROME | Netherlands (high-res, ~2.5km) | + +Each model exposes `shortwave_radiation_` in the hourly response. Standard endpoint also returns `cloud_cover`, `sunshine_duration`, `global_tilted_irradiance` (when tilt/azimuth set in settings). + +### Solcast (`lib/solcast-provider.js`) + +Optional rooftop-specific PV power forecast. Requires API key + resource ID. Returns `pv_estimate` (p50 W) and `pv_estimate10` (p10, conservative). 30-min resolution; converted to W (kW × 1000) and aggregated hourly for the chart. + +Cached in settings (`solcast_forecast_cache`); stale cache used as fallback on fetch failure. + +### KNMI ground-truth (`lib/knmi-stations.js` + `_recordKnmiActual`) + +`knmi-stations.js` lists Dutch KNMI stations (Cabauw, De Bilt, Schiphol, etc.) with lat/lon. `_pickNearestStation(lat, lon)` returns the closest. Hourly `qg` (global solar radiation, W/m²) is fetched from `https://www.daggegevens.knmi.nl/klimatologie/uurgegevens` and stored in `learningEngine.knmi_hourly` (last 2 calendar days). + +Rate-limited to 1×/hour via `_maybeRecordKnmiActual` — safe to call on every fetchForecast (incl. cache hits). + +Settings flag: `knmi_api_key` field (set on device); without it, KNMI fetch is skipped (Open-Meteo's own historical radiation falls back as actual). + +## Ensemble blending + +`_mergeApiResponses` produces a single `shortwave_radiation` array (used everywhere downstream) via weighted blend of the 4 model arrays: + +``` +const w = learningEngine.getModelWeights() ?? { equal weights }; +wMean = Σ (w[m] × radiation[m]) / Σ w[m] +``` + +**Spread discount:** when models disagree (stdev across 4 values for a given hour), the blended value is reduced to avoid overshoot: + +- `std > 80 W/m²` → `wMean − 0.5 × std` +- `std > 30 W/m²` → `wMean − 0.3 × std` +- else → `wMean` + +Spread-adjusted slot count is logged: `Ensemble radiation blended from 4 models [mf=24% gfs=24% icon=26% knmi=27%] (sample avg: 155 W/m², spread-adjusted 29 slots)`. + +Per-model arrays also kept as `perModelWm2` on each `hourlyForecast` slot, aligned to `standardData.hourly.time`. Used by device.js to build per-model `pvForecast` for accuracy tracking without re-aligning. + +## Per-model accuracy + +`learningEngine.recordModelAccuracy(perModelForecast, actualAvgWm2, dateStr)` runs once per day in `_learnFromYesterday`: + +- Forecast snapshot saved at day-end via `saveForecastSnapshot(date, forecastAvgWm2, perModel)` +- Yesterday's snapshot compared against actual: + - `actualAvg = knmiDailyAvg ?? (openMeteoHistoricalAvg)` — KNMI preferred + - Per-model error `err = |actual − fc| / max(actual, fc)`; accuracy `1 − min(err, 1)` +- EMA update: `acc_m[t+1] = 0.15 × acc + 0.85 × acc_m[t]` (α=0.15, ~7-day half-life) +- Guard: each `dateStr` processed at most once (`pv_model_accuracy_date` flag) +- Prior used for cold-start: `_modelPrior(m)` (currently 0.7 for all) + +Storage: + +``` +data.pv_model_accuracy = { + meteofrance_arpege_europe: 0.801, + gfs_seamless: 0.791, + icon_seamless: 0.865, + knmi_harmonie_arome_netherlands: 0.904, +} +data.pv_model_accuracy_date = '2026-05-19' +``` + +`getModelWeights()` returns proportional weights `acc[m] / Σacc[m]`, used by `_mergeApiResponses` for the next blend. + +## Per-day per-model accuracy (UI) + +Settings page (`settings/index.html`) shows MF/GFS/ICON/KNMI pills **per day** alongside Blended/OM/SC pills. Computed entirely frontend from `pvPredictions[*].{mf, gfs, icon, knmi, actual}` (already in learning-engine pvPredictions storage), grouped by Amsterdam-day in `_renderPvAccuracyDay`. No new backend storage — derived from existing per-slot per-model W values. + +Updates when user navigates with day prev/next buttons. + +## OM + Solcast blend (optimizer) + +Optimizer slot-level PV uses weighted blend of OM and Solcast (when both available): + +- `wOM` / `wSC` from `learningEngine.getPvBlendWeights()` (EMA per-source accuracy) +- `pvForecastSCEffective` chooses p50 or p10 per slot (conservative blend when accuracy uncertain) +- Past hours: OM + SC blended where both present, OM alone otherwise +- Future hours: stored blended `pvForecast[0]` (set by optimizer after rerun) + +Chart shows both lines separately (Open-Meteo dashed blue, Solcast dashed green) plus the blended "PV Verwachting" (dashed orange) and actual (solid yellow). + +## Bias factors + +- `radiation_bias_factor` — EMA daily ratio actual/forecast (α=0.15, clamped 0.3–2.0). Skipped when ≥10 yield slots learned (yield factors already absorb bias). +- `pv_daily_bias` / `pv_daily_bias_clear` — per-cloud-cover-band ratios from `recordDailyPvBiasFromPredictions`. Applied as cloud-aware discount in optimizer DP when overcast forecast risks early stop-charging. +- `pvCloudFactor` — applied in DP under heavy cloud cover; prevents grid-charge stopping too early on overcast morning. + +## Logging cheatsheet + +``` +[KNMI] Cabauw (14km): qg=200 W/m² n=6 okta ss=0 min ta=13.9°C +[KNMI] Using station qg=200 W/m² as actual for 2026-05-19 +Ensemble radiation blended from 4 models [mf=24% gfs=24% icon=26% knmi=27%] (sample avg: 155 W/m², spread-adjusted 29 slots) +[Snapshot] 2026-05-20 rad=269 perModel=mf=332 gfs=415 icon=271 knmi=224 ensLen=96 hourlyLen=96 +[ModelAccuracy check] 2026-05-19: perModel=meteofrance_arpege_europe,gfs_seamless,icon_seamless,knmi_harmonie_arome_netherlands actualCount=14 +[ModelAccuracy] mf=0.80 gfs=0.79 icon=0.86 knmi=0.90 (actual=200W/m²) +``` + +## Settings keys + +- `weather_latitude` / `weather_longitude` — location (number fields) +- `weather_update_interval` — fetch period (hours, default 3) +- `solcast_enabled` / `solcast_api_key` / `solcast_resource_id` — Solcast config +- `knmi_api_key` — KNMI daggegevens API token (device setting) +- `pv_tilt` / `pv_azimuth` — used for `global_tilted_irradiance` request (when both set) 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..ebb6989e --- /dev/null +++ b/drivers/SDM230-p1mode/device.js @@ -0,0 +1,335 @@ +'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.homey.app.bumpDeviceCount?.('SDM230-p1mode'); + + 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..307d5c03 --- /dev/null +++ b/drivers/SDM230/device.js @@ -0,0 +1,361 @@ +'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.homey.app.bumpDeviceCount?.('SDM230'); + this._debugLogs = []; + this._pollFailCount = 0; + + // 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); + + // Deterministic spread: device index → evenly distributed start offset. + // Prevents all SDM230 devices from timing out simultaneously when unreachable, + // which caused concurrent error-handling to push heap over the 64 MB ceiling. + const allDevices = this.driver.getDevices(); + const deviceCount = Math.max(allDevices.length, 1); + const myIndex = Math.max(allDevices.indexOf(this), 0); + const offset = myIndex === 0 ? 500 : Math.round((myIndex / deviceCount) * interval * 1000); + this.log(`⏱️ SDM230 poll interval ${interval}s, spread offset ${Math.round(offset / 1000)}s (device ${myIndex + 1}/${deviceCount})`); + + 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(); + if (this._pollFailCount > 0) { + this._pollFailCount = 0; + this._restoreNormalPollInterval(); + } + + } catch (err) { + this._pollFailCount = (this._pollFailCount || 0) + 1; + 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); + if (this._pollFailCount === 3) { + this._switchToBackoffInterval(); + } + } + + } + + _switchToBackoffInterval() { + const settings = this.getSettings(); + this._normalInterval = Math.max(settings.polling_interval || 10, 2); + this.log(`⚠️ 3 consecutive poll failures — slowing to 60s backoff`); + if (this.onPollInterval) clearInterval(this.onPollInterval); + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, 60 * 1000); + } + + _restoreNormalPollInterval() { + const interval = this._normalInterval || Math.max((this.getSettings().polling_interval || 10), 2); + this.log(`✅ Poll succeeded — restoring ${interval}s interval`); + if (this.onPollInterval) clearInterval(this.onPollInterval); + this.onPollInterval = setInterval(() => { + this.onPoll().catch(this.error); + }, interval * 1000); + } + + 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..c5fe9ec2 --- /dev/null +++ b/drivers/SDM230_v2/device.js @@ -0,0 +1,547 @@ +'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() { + + this.homey.app.bumpDeviceCount?.('SDM230_v2'); + + // 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; + } + }); + + // Predictive (HW Smart Charging) + this.homey.flow.getActionCard('sdm230-set-battery-to-predictive-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Predictive (HW Smart Charging) Mode'); + + try { + const response = await api.setMode(this.url, this.token, 'predictive'); + if (!response) return false; + + const normalized = normalizeBatteryMode(response); + return normalized; + + } catch (error) { + this.error('Error set mode to predictive:', error); + return false; + } + }); + + } // End of _flowListenersRegistered_SDM230 guard + + const allDevices = this.driver.getDevices(); + const deviceCount = Math.max(1, allDevices.length); + const myIndex = Math.max(0, allDevices.indexOf(this)); + const pollIntervalMs = 1000 * settings.polling_interval; + const offsetMs = myIndex === 0 ? 500 : Math.round((myIndex / deviceCount) * pollIntervalMs); + + this.log(`⏱️ Polling interval ${settings.polling_interval}s, spread offset ${Math.round(offsetMs/1000)}s (device ${myIndex + 1}/${deviceCount})`); + + if (this.onPollInterval) clearInterval(this.onPollInterval); + this._firstPollDone = false; + this._firstPollTimeout = this.homey.setTimeout(() => { + this._firstPollTimeout = null; + if (this.__deleted) return; + this.log(`🚀 First poll starting (after ${Math.round(offsetMs/1000)}s delay)`); + this._firstPollDone = true; + this.onPoll(); + this.onPollInterval = setInterval(this.onPoll.bind(this), pollIntervalMs); + }, offsetMs); + + this._triggerFlowPrevious = {}; + + + } + + onDeleted() { + this.__deleted = true; + if (this._firstPollTimeout) { + this.homey.clearTimeout(this._firstPollTimeout); + this._firstPollTimeout = null; + } + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + if (this._settingsFlushTimer) { + this.homey.clearTimeout(this._settingsFlushTimer); + this._settingsFlushTimer = null; + } + if (this._settingsQueue) this._settingsQueue.clear(); + } + + // Batched settings persistence — homey.settings.set allocates ~30 MB V8 heap + // per call. Spacing writes 8s apart lets GC reclaim the previous spike. + _queueSettingsPersist(key, value) { + if (!this._settingsQueue) this._settingsQueue = new Map(); + this._settingsQueue.set(key, value); + if (this._settingsFlushTimer) return; + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + } + + _flushSettingsQueue() { + if (!this._settingsQueue || this._settingsQueue.size === 0) return; + + let heapMB = 0; + try { heapMB = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (heapMB > 40) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + return; + } + + const [key, value] = this._settingsQueue.entries().next().value; + this._settingsQueue.delete(key); + try { + if (key === '__settings__') { + this.setSettings(value).catch(err => this.error('Failed to persist settings:', err.message)); + } else { + this.homey.settings.set(key, value); + } + } catch (e) { + this.error(`Failed to persist ${key}:`, e.message); + } + if (this._settingsQueue.size > 0) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + } + } + + onDiscoveryAvailable(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + if (this._firstPollDone) this.onPoll(); + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.log('onDiscoveryAddressChanged'); + if (this._firstPollDone) this.onPoll(); + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.setAvailable(); + if (this._firstPollDone) 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) { + if (value === undefined) return; + + 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); + } + } + } + + // Skip write when value unchanged — avoids framework allocation + listener churn + if (this.getCapabilityValue(capability) === value) 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) { + + // 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 — batched, only on actual change + if (this.url && this.url !== settings.url && this._lastPersistedUrl !== this.url) { + this._lastPersistedUrl = this.url; + this._queueSettingsPersist('__settings__', { url: this.url }); + } + + // Refresh token if missing + if (!this.token) { + this.token = await this.getStoreValue('token'); + } + + // --- Main API calls (parallel) --- + const [data, batteryMode] = await Promise.all([ + api.getMeasurement(this.url, this.token), + api.getMode(this.url, this.token).catch(() => undefined) + ]); + + 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)); + + // Battery mode — batch capability updates, debounce settings persist + if (batteryMode) { + const normalized = normalizeBatteryMode(batteryMode); + + if (this._liveMode !== normalized) { + this._liveMode = normalized; + if (settings.mode !== normalized) { + this._queueSettingsPersist('__settings__', { mode: normalized }); + } + } + + setCapabilityPromises.push(this._setCapabilityValue('measure_power.battery_group_power_w', batteryMode.power_w ?? null)); + setCapabilityPromises.push(this._setCapabilityValue('measure_power.battery_group_target_power_w', batteryMode.target_power_w ?? null)); + setCapabilityPromises.push(this._setCapabilityValue('measure_power.battery_group_max_consumption_w', batteryMode.max_consumption_w ?? null)); + setCapabilityPromises.push(this._setCapabilityValue('measure_power.battery_group_max_production_w', batteryMode.max_production_w ?? null)); + + this._triggerFlowOnChange('battery_mode_changed_SDM230_v2', normalized); + } + + await Promise.allSettled(setCapabilityPromises); + + // Trigger flows when values change + this._triggerFlowOnChange('meter_power.import', data.energy_import_kwh); + 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..eed5639c --- /dev/null +++ b/drivers/SDM230_v2/driver.flow.compose.json @@ -0,0 +1,129 @@ +{ + "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": [] + }, + { + "id": "sdm230-set-battery-to-predictive-mode", + "title": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "titleFormatted": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "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..47ea220c --- /dev/null +++ b/drivers/SDM630-p1mode/device.js @@ -0,0 +1,196 @@ +'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() { + this.homey.app.bumpDeviceCount?.('SDM630-p1mode'); + // 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..e9d3b9a0 --- /dev/null +++ b/drivers/SDM630/device.js @@ -0,0 +1,353 @@ +'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.homey.app.bumpDeviceCount?.('SDM630'); + + 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 allDevices = this.driver.getDevices(); + const deviceCount = Math.max(allDevices.length, 1); + const myIndex = Math.max(allDevices.indexOf(this), 0); + const offset = myIndex === 0 ? 500 : Math.round((myIndex / deviceCount) * interval * 1000); + this.log(`⏱️ SDM630 poll interval ${interval}s, spread offset ${Math.round(offset / 1000)}s (device ${myIndex + 1}/${deviceCount})`); + + 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..28a0ee2e --- /dev/null +++ b/drivers/SDM630_v2/device.js @@ -0,0 +1,631 @@ +'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() { + this.homey.app.bumpDeviceCount?.('SDM630_v2'); + const _memS = (label) => { try { const h = require('v8').getHeapStatistics(); this.log(`[MEM][SDM630] ${label}: heap=${(h.used_heap_size/1048576).toFixed(1)}/${(h.total_heap_size/1048576).toFixed(1)}MB`); } catch(_){} }; + _memS('onInit-start'); + + // await this.setUnavailable(`${this.getName()} ${this.homey.__('device.init')}`); + + this.token = await this.getStoreValue('token'); + //this.log('Token:', this.token); + + await this._updateCapabilities(); + _memS('after-updateCapabilities'); + await this._registerCapabilityListeners(); + _memS('after-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; + } + }); + + + // Predictive (HW Smart Charging) + this.homey.flow.getActionCard('sdm630-set-battery-to-predictive-mode') + .registerRunListener(async () => { + this.log('ActionCard: Set Battery to Predictive (HW Smart Charging) Mode'); + + try { + const response = await api.setMode(this.url, this.token, 'predictive'); + + if (!response) { + this.log('Invalid response, returning false'); + return false; + } + + const normalized = normalizeBatteryMode(response); + this.log('Set mode to predictive:', normalized); + return normalized; + + } catch (error) { + this.error('Error set mode to predictive:', error); + return false; + } + }); + + } // End of _flowListenersRegistered_SDM630 guard + + _memS('after-flowCards'); + + const allDevices = this.driver.getDevices(); + const deviceCount = Math.max(1, allDevices.length); + const myIndex = Math.max(0, allDevices.indexOf(this)); + const pollIntervalMs = 1000 * settings.polling_interval; + const offsetMs = myIndex === 0 ? 500 : Math.round((myIndex / deviceCount) * pollIntervalMs); + + this.log(`⏱️ Polling interval ${settings.polling_interval}s, spread offset ${Math.round(offsetMs/1000)}s (device ${myIndex + 1}/${deviceCount})`); + + if (this.onPollInterval) clearInterval(this.onPollInterval); + this._firstPollDone = false; + this._firstPollTimeout = this.homey.setTimeout(() => { + this._firstPollTimeout = null; + if (this.__deleted) return; + this.log(`🚀 First poll starting (after ${Math.round(offsetMs/1000)}s delay)`); + this._firstPollDone = true; + this.onPoll(); + this.onPollInterval = setInterval(this.onPoll.bind(this), pollIntervalMs); + }, offsetMs); + + 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() { + this.__deleted = true; + if (this._firstPollTimeout) { + this.homey.clearTimeout(this._firstPollTimeout); + this._firstPollTimeout = null; + } + if (this.onPollInterval) { + clearInterval(this.onPollInterval); + this.onPollInterval = null; + } + if (this._settingsFlushTimer) { + this.homey.clearTimeout(this._settingsFlushTimer); + this._settingsFlushTimer = null; + } + if (this._settingsQueue) this._settingsQueue.clear(); + } + + // Batched settings persistence — homey.settings.set allocates ~30 MB V8 heap + // per call. Spacing writes 8s apart lets GC reclaim the previous spike. + _queueSettingsPersist(key, value) { + if (!this._settingsQueue) this._settingsQueue = new Map(); + this._settingsQueue.set(key, value); + if (this._settingsFlushTimer) return; + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + } + + _flushSettingsQueue() { + if (!this._settingsQueue || this._settingsQueue.size === 0) return; + + let heapMB = 0; + try { heapMB = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (heapMB > 40) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + return; + } + + const [key, value] = this._settingsQueue.entries().next().value; + this._settingsQueue.delete(key); + try { + if (key === '__settings__') { + this.setSettings(value).catch(err => this.error('Failed to persist settings:', err.message)); + } else { + this.homey.settings.set(key, value); + } + } catch (e) { + this.error(`Failed to persist ${key}:`, e.message); + } + if (this._settingsQueue.size > 0) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + } + } + + onDiscoveryAvailable(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + if (this._firstPollDone) this.onPoll(); + } + + onDiscoveryAddressChanged(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.log('onDiscoveryAddressChanged'); + if (this._firstPollDone) this.onPoll(); + } + + onDiscoveryLastSeenChanged(discoveryResult) { + this.url = `https://${discoveryResult.address}`; + this.log(`URL: ${this.url}`); + this.setAvailable(); + if (this._firstPollDone) 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) { + if (value === undefined) return; + + 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); + } + } + } + + // Skip write when value unchanged — avoids framework allocation + listener churn + if (this.getCapabilityValue(capability) === value) 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) { + + // 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() { + // Circuit breaker: skip poll during backoff window (after repeated timeouts) + if (this._backoffUntil && Date.now() < this._backoffUntil) return; + + 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 — batched, only on actual change + if (this.url && this.url !== settings.url && this._lastPersistedUrl !== this.url) { + this._lastPersistedUrl = this.url; + this._queueSettingsPersist('__settings__', { url: this.url }); + } + + // Refresh token if missing + if (!this.token) { + this.token = await this.getStoreValue('token'); + } + + // --- Main API calls (parallel) --- + const [data, batteryMode] = await Promise.all([ + api.getMeasurement(this.url, this.token), + api.getMode(this.url, this.token).catch(() => undefined) + ]); + + 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)); + + // Battery mode — batch capability updates, debounce settings persist + if (batteryMode !== undefined) { + const normalized = normalizeBatteryMode(batteryMode); + + if (this._liveMode !== normalized) { + this._liveMode = normalized; + if (settings.mode !== normalized) { + this._queueSettingsPersist('__settings__', { mode: normalized }); + } + } + + setCapabilityPromises.push(this._setCapabilityValue('measure_power.battery_group_power_w', batteryMode.power_w ?? null)); + setCapabilityPromises.push(this._setCapabilityValue('measure_power.battery_group_target_power_w', batteryMode.target_power_w ?? null)); + setCapabilityPromises.push(this._setCapabilityValue('measure_power.battery_group_max_consumption_w', batteryMode.max_consumption_w ?? null)); + setCapabilityPromises.push(this._setCapabilityValue('measure_power.battery_group_max_production_w', batteryMode.max_production_w ?? null)); + + this._triggerFlowOnChange('battery_mode_changed_SDM630_v2', normalized); + } + + await Promise.allSettled(setCapabilityPromises); + + // If everything succeeded + this._consecutiveErrors = 0; + this._backoffUntil = null; + await this.setAvailable(); + + } catch (err) { + this.error('Polling failed:', err); + await this.setUnavailable(err).catch(this.error); + + // Circuit breaker: after 3 consecutive timeouts, back off for 60s to prevent socket storms + if (err.message === 'TIMEOUT') { + this._consecutiveErrors = (this._consecutiveErrors || 0) + 1; + if (this._consecutiveErrors >= 3) { + this._backoffUntil = Date.now() + 60000; + this._consecutiveErrors = 0; + this.log(`⏸️ SDM630 circuit breaker: 3 timeouts, backing off 60s`); + } + } else { + this._consecutiveErrors = 0; + } + } +} + + 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..3e4f0fe4 --- /dev/null +++ b/drivers/SDM630_v2/driver.flow.compose.json @@ -0,0 +1,84 @@ +{"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": [] + }, + { + "id": "sdm630-set-battery-to-predictive-mode", + "title": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "titleFormatted": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "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..dc8ffb4f --- /dev/null +++ b/drivers/battery-policy/assets/planning.html @@ -0,0 +1,432 @@ + + + + + + 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..c1d3881d --- /dev/null +++ b/drivers/battery-policy/device.js @@ -0,0 +1,4482 @@ +'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 ChartRenderer = require('../../lib/chart-renderer'); + +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`); + } +} + +// NOCT temperature derating for fallback (non-learned) PV path only. +// Learned yf already captures temperature empirically — do not apply there. +function _pvTempFactor(airTempC, radiationWm2) { + const cellTemp = (airTempC ?? 20) + (radiationWm2 / 1000) * 25; + return Math.max(0.80, Math.min(1.05, 1 - 0.004 * (cellTemp - 25))); +} + +function _settingsFootprintKB(settings) { + const KNOWN_KEYS = [ + 'learning_status', 'learning_pv_chart_data', 'policy_mode_history', + 'policy_optimizer_schedule', 'policy_all_prices', 'policy_all_prices_15min', + 'policy_pv_forecast_hourly', 'policy_pv_forecast_om', 'policy_pv_forecast_sc', + 'policy_pv_actual_today', 'policy_widget_data', 'battery_cycle_history', + 'weather_forecast_cache', 'battery_expansion_analysis', 'policy_daily_profit', + 'policy_consumption_profile', 'pv_surplus_forecast', 'policy_last_run_debug', + 'battery_policy_state', 'device_settings', + ]; + try { + const entries = KNOWN_KEYS + .map(k => ({ k, bytes: JSON.stringify(settings.get(k) ?? null).length })) + .filter(e => e.bytes > 5) + .sort((a, b) => b.bytes - a.bytes); + const totalKB = (entries.reduce((s, e) => s + e.bytes, 0) / 1024).toFixed(1); + const top = entries.slice(0, 8).map(e => `${e.k}=${(e.bytes / 1024).toFixed(1)}kB`).join(' | '); + return `${totalKB}kB total | ${top}`; + } catch (e) { + return `unavailable: ${e.message}`; + } +} + +class BatteryPolicyDevice extends Homey.Device { + + async onInit() { + this.homey.app.bumpDeviceCount?.('battery-policy'); + this.log('BatteryPolicyDevice initialized'); + _memMB('onInit-start'); + + // Components + this.learningEngine = new LearningEngine(this.homey, this); + await this.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()); + 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.buienradarData = null; + this.lastRecommendation = null; + this._liveState = {}; // in-memory store for rebuildable UI state (served via api.js) + this._lastPvEstimateW = 0; // For EMA smoothing + this._pvProductionW = null; // User-provided PV production via flow card + this._lastLoggedPvW = null; // Suppress repeat PV log lines when value unchanged + this._pvProductionTimestamp = null; // When the PV data was last updated + this._lastPvAccuracyBucket = null; // Deduplicate PV accuracy samples per 15-min slot + 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 + this._favorableWindowActive = false; // Tracks edge for favorable_consumption_window trigger + this._todayDate = null; // YYYY-MM-DD (Amsterdam) for daily reset + this._todayGridImportKwh = 0; // accumulated grid import today (kWh) + this._todayConsumptionKwh = 0; // accumulated house consumption today (kWh) + this._lastSelfSuffWrite = 0; // throttle: last settings write for today_self_sufficiency + this._modeHistory = this.homey.settings.get(`batt_mode_hist_${this.getData().id}`) || []; + this._isPredictiveMode = false; + this._policyEnabledBeforePredictive = null; // saved state for auto-restore when predictive ends + this._lastChartRolloverDay = null; // tracks last midnight rollover to avoid repeated swaps + this._modeChartBody = null; + this._modeChartImage = null; + + // EV charging gate — set via flow card, blocks battery discharge until cleared + // or auto-expires. Restored from settings on restart so flag survives reboots. + this._evChargingUntil = 0; + this._evChargingTimer = null; + try { + const evUntil = Number(this.homey.settings.get('ev_charging_until') || 0); + if (evUntil > Date.now()) { + this._evChargingUntil = evUntil; + const minsLeft = Math.round((evUntil - Date.now()) / 60000); + this.log(`[EV] Restored EV charging flag — ${minsLeft} min remaining`); + this._scheduleEvAutoClear(); + } else if (evUntil > 0) { + this.homey.settings.set('ev_charging_until', 0); + } + } catch (_) { /* ignore */ } + + // Restore accumulators from last save so a restart doesn't reset to 0 mid-day + try { + const saved = this.homey.settings.get('today_self_sufficiency'); + const todayNL = new Date().toLocaleString('en-CA', { timeZone: 'Europe/Amsterdam' }).slice(0, 10); + if (saved?.date === todayNL && saved.consumptionKwh > 0) { + this._todayDate = todayNL; + this._todayGridImportKwh = saved.gridImportKwh ?? 0; + this._todayConsumptionKwh = saved.consumptionKwh ?? 0; + this.log(`[SelfSuff] Restored from settings: ${saved.pct}% (${saved.consumptionKwh} kWh consumed)`); + } + } catch (e) { /* ignore */ } + + await this._initializeCapabilities(); + this._registerCapabilityListeners(); + + // Connect P1 after short delay + this.homey.setTimeout(() => { + this._connectP1Device().catch(err => this.error(err)); + }, 1500); + + // Schedule periodic checks + this._schedulePolicyCheck(); + + // Mode history flush interval (15 min, offset 7.5 min from slot boundaries). + // Slot boundaries (:00, :15, :30, :45) are when _saveWidgetData and price refresh + // run — offsetting by 7.5 min avoids concurrent large allocations that together + // push V8 into a major GC cycle peaking at ~60 MB. + const _modeFlushFn = () => { + if (this.p1Device) { + const mode = this.p1Device._currentDetailedMode + || this.p1Device.getCapabilityValue('battery_group_charge_mode') + || 'unknown'; + const soc = this.p1Device.getCapabilityValue('battery_group_average_soc') ?? 50; + this._recordModeHistory(mode); + this._recordSoCHistory(soc); + } + if (!this._modeHistory?.length) return; + this._queueSettingsPersist(`batt_mode_hist_${this.getData().id}`, this._modeHistory); + // Guard: skip chart update when heap is elevated — quickchart HTTP + image data adds ~30 MB + let _heapFlush = 99; + try { _heapFlush = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (_heapFlush > 35) { + this.log(`[MEM] Skipping mode chart update — heap ${_heapFlush.toFixed(1)} MB > 35 MB guard`); + } else { + this._updateModeChart().catch(e => this.error('Mode chart update failed:', e)); + } + + // Profit tracking — runs regardless of policy state so predictive-mode days + // are never a blind spot. When Slim Laden is active, also refresh the DP + // projection (read-only) so we know what our optimizer would have planned. + if (this._isPredictiveMode) { + const timeout = new Promise((_, reject) => + this.homey.setTimeout(() => reject(new Error('timeout')), 20_000)); + Promise.race([this._gatherInputs(), timeout]).then(inputs => { + if (inputs?.tariff) { + this.optimizationEngine.updateSettings({}); + return this._recomputeOptimizer(inputs).then(() => inputs); + } + }).then(inputs => { + // Patch live SoC into battery_policy_state so widget shows correct value + const liveSoc = this.p1Device?.getCapabilityValue('battery_group_average_soc') ?? null; + if (liveSoc != null) { + const ps = this._liveState.battery_policy_state + ?? this.homey.settings.get('battery_policy_state') ?? {}; + ps.batterySOC = liveSoc; + ps.currentMode = 'predictive'; + this._setLive('battery_policy_state', ps); + } + // Record predictive mode in planning chart history + try { + const modeHistory = this.homey.settings.get('policy_mode_history') || []; + const nowTs = new Date(); + const bucket = Math.round(nowTs.getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000); + const existing = modeHistory.findIndex( + h => Math.round(new Date(h.ts).getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000) === bucket + ); + const entry = { ts: nowTs.toISOString(), hwMode: 'predictive', soc: liveSoc, price: null }; + if (existing >= 0) modeHistory[existing] = entry; + else modeHistory.push(entry); + if (modeHistory.length > 192) modeHistory.splice(0, modeHistory.length - 192); + this._queueSettingsPersist('policy_mode_history', modeHistory); + } catch (e) { this.error('Failed to save predictive mode history (flush):', e); } + try { this._saveWidgetData({ skipChart: true }); } catch (e) { this.error('Widget save (predictive) failed:', e.message); } + }).catch(e => this.error('Predictive mode flush failed:', e.message)); + } + }; + this.homey.setTimeout(() => { + _modeFlushFn(); + this._modeHistoryFlushInterval = this.homey.setInterval(_modeFlushFn, 15 * 60 * 1000); + }, 7.5 * 60 * 1000); + + // Register cameras — deferred to 60s so the startup memory spike has settled. + // Sequential await prevents concurrent setCameraImage calls from racing in Homey's image manager. + this.homey.setTimeout(async () => { + await this._initModeHistoryCamera().catch(e => this.error('Mode camera init failed:', e)); + await this._initPvCamera().catch(e => this.error('PV camera init failed:', e)); + }, 60 * 1000); + + // Restore widget data from cached settings after startup peak has settled. + // Delayed to 90s: _saveWidgetData loads large settings keys (optimizer schedule, + // 15-min prices, mode history) which add ~18 MB to heap. energy_v2 alone peaks + // at 41 MB; loading at T+3s pushed total to 71 MB → Memory Warning crash. + // After T+10s the heap settles at ~29 MB, so 90s is safely past the danger window. + this.homey.setTimeout(() => { + try { this._saveWidgetData({ skipChart: true }); } catch (e) { this.error('Startup widget restore failed:', e); } + this._logSettingsFootprint(); + }, 90 * 1000); + + // Set default for price_resolution if not yet saved (existing paired devices) + if (!this.getSetting('price_resolution')) { + await this.setSettings({ price_resolution: '15min' }); + } + + // Migrate legacy weather_location (city name) to weather_latitude/weather_longitude + await this._migrateWeatherLocation(); + + // Weather fetch only in dynamic. + // Deferred 30s past onInit: Open-Meteo ensemble fetch + parsing allocates ~30 MB + // and pushed peak heap to 71 MB on a user's setup with 15 devices, tripping the + // Homey "Memory Warning Limit Reached" ceiling. After T+30s the parallel device + // onInits + WS auth + first polls have settled, leaving headroom for the spike. + // Cached weatherData (≤6 min old) is restored from settings, so the gap is invisible. + if (this.getSettings().tariff_type === 'dynamic') { + this.homey.setTimeout(() => { + this._updateWeather() + .then(() => _memMB('after-weather-fetch')) + .catch(err => this.error('Initial weather fetch failed:', err)); + }, 30 * 1000); + + // Schedule periodic price refresh (every 30 minutes) — lightweight, keep immediate + this._schedulePriceRefresh(); + } + + // Push device settings so planning page has correct values after restart + // (normally pushed on every _runPolicyCheck, but that runs with a delay). + // Queued via the batcher to avoid stacking a 30 MB settings.set spike + // on top of the startup cascade. + const s = this.getSettings(); + this._setLive('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 ?? 0, + max_soc: s.max_soc ?? 100, + battery_efficiency: s.battery_efficiency || 0.75, + 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, + price_resolution: s.price_resolution || '15min', + }); + + this.log('BatteryPolicyDevice ready'); + _memMB('onInit-done'); + } + + _logSettingsFootprint() { + this.log(`[MEM] settings footprint: ${_settingsFootprintKB(this.homey.settings)}`); + } + + // Queue a settings.set call for deferred, serialized execution. + // Rationale: homey.settings.set allocates ~30 MB V8 heap per call (framework + // internal, independent of payload size — measured with 8 KB payload). A single + // policy run used to make 14+ such calls, cumulatively driving RSS over the + // Homey ceiling. Most rebuildable UI state now lives in this._liveState (served + // via api.js) so only genuinely persistent keys pass through this queue. + // Spacing: 8s between writes so V8 can fully GC the previous 30 MB spike + // before the next allocation — a single 60 MB peak alone trips the warning. + _queueSettingsPersist(key, value) { + if (!this._settingsQueue) this._settingsQueue = new Map(); + this._settingsQueue.set(key, value); // coalesces duplicates + if (this._settingsFlushTimer) return; + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + } + + _flushSettingsQueue() { + if (!this._settingsQueue || this._settingsQueue.size === 0) return; + + // Heap-aware: each settings.set allocates ~30 MB transient. If we're already + // elevated (e.g. weather fetch, widget broadcast, camera chart), another + // 30 MB on top would trip the Memory Warning. Reschedule until heap settles. + let heapMB = 0; + try { heapMB = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (heapMB > 40) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + return; + } + + const [key, value] = this._settingsQueue.entries().next().value; + this._settingsQueue.delete(key); + try { + this.homey.settings.set(key, value); + } catch (e) { + this.error(`Failed to persist ${key}:`, e.message); + } + if (this._settingsQueue.size > 0) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + } + } + + // Store rebuildable UI state in-memory for fast internal reads (cameras, widget), + // and queue a batched settings.set so the settings page (which reads via Homey.get) + // sees the same data. The batcher spaces writes 8s apart so each ~30 MB spike + // is fully GC'd before the next allocation. + _setLive(key, value) { + this._liveState[key] = value; + this._queueSettingsPersist(key, value); + } + + 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.75, + last_update: new Date().toISOString(), + active_mode: 'unknown', + override_until: null, + weather_override: 'auto', + presence_mode: this.learningEngine.isPaused() ? '🏖️ Away' : '🏠 Home', + policy_profit_eur: 0, + pv_forecast_kwh: 0, + bias_factor: 1, + plan_summary: '-' + }; + + 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 === '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) { + // skipEnabledCheck: capability not yet persisted when listener fires (Homey SDK quirk) + await this._runPolicyCheck({ skipEnabledCheck: true }); + } + + 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 }); + + if (value === 'off' && this.p1Device) { + const userMode = this.p1Device.getSetting('mode') || 'zero'; + this.log(`Policy mode set to off — restoring hardware to user-configured mode: ${userMode}`); + await this.p1Device.setBatteryGroupMode(userMode); + } else { + 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 */ } + + // Startup mode restore removed: no reliable way to distinguish firmware default + // from a user-set or policy-set mode. The policy runs at T+45s and will apply + // the correct mode then. Restoring from history risked overwriting manual user + // changes made just before a restart or update. + this.log('🔄 Skipping startup mode restore — policy will apply correct mode at T+45s'); + + // Restore policy_enabled if predictive ended before this restart. + // The in-memory flag is lost on restart; if policy_enabled is false but + // the hardware is no longer in predictive, the policy would stay permanently + // disabled until the user manually re-enables it. + { + const savedPrePredictive = this.homey.settings.get('policy_enabled_before_predictive'); + if (savedPrePredictive !== null && savedPrePredictive !== undefined) { + const hwModeAtStart = this.p1Device.getCapabilityValue('battery_group_charge_mode'); + if (hwModeAtStart !== 'predictive') { + this.log(`[Policy] Restoring policy_enabled=${savedPrePredictive} — predictive ended before restart`); + await this.setCapabilityValue('policy_enabled', savedPrePredictive).catch(this.error); + this.homey.settings.unset('policy_enabled_before_predictive'); + } + } + } + + // Defer the initial policy check past the startup cascade (T+45s). + // Reason: at this point the device cascade is still settling — 8 socket polls, + // WS authorizations (energy_v2 + 2× plugin_battery on some setups), price/weather + // fetches and the new device-settings push are all happening in parallel. Running + // _runPolicyCheck() here pushed heap to ~70 MB on a user with 15 devices and a + // fragile network, hitting the Homey "Memory Warning Limit Reached" ceiling. + // Also: prices may still be loading, causing the policy run to see price=undefined. + // Mode is already restored above, so the battery is in the correct state during + // this gap. The next scheduled check at the slot boundary (≤15 min) takes over. + this.homey.setTimeout(() => { + this._runPolicyCheck().catch(err => this.error('Initial policy check failed:', err)); + }, 45 * 1000); + + } 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; + } + } + + // P1 firmware bug: to_full mode reports battery power as 0W via DSMR. + // Correct to the actual charge power so house consumption is calculated correctly. + if (batteryPower === 0) { + const chargeMode = this.p1Device?.getCapabilityValue('battery_group_charge_mode'); + if (chargeMode === 'to_full') { + const state = this._liveState.battery_policy_state + ?? this.homey.settings.get('battery_policy_state') ?? {}; + batteryPower = state.maxChargePowerW ?? 800; + } + } + + 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 pendingWh = ((s.pendingChargeKwh || 0) * 1000).toFixed(0); + const chargedWh = ((s.sessionChargeKwh || 0) * 1000).toFixed(0); + const dischargedWh = ((s.sessionDischargeKwh || 0) * 1000).toFixed(0); + const dir = s.lastPowerDirection || '?'; + this.log( + `[RTE] session: dir=${dir} pending=${pendingWh}Wh charge=${chargedWh}Wh discharge=${dischargedWh}Wh ` + + `cycles=${this.efficiencyEstimator.getCycleCount()} 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); + } + + // ── Reactive peak shaving trigger ───────────────────────────────────── + // Fire extra policy run when load exceeds peak_shaving_threshold within + // peak_hours window — reacts within 15s instead of waiting up to 15 min. + { + const psThreshold = this.getSetting('peak_shaving_threshold') ?? 0; + if (psThreshold > 0) { + const psHours = this.getSetting('peak_hours') || ''; + const [psStart, psEnd] = psHours.split('-').map(s => parseInt(s, 10)); + const nowHourNL = parseInt(new Date().toLocaleString('en-US', { timeZone: 'Europe/Amsterdam', hour: 'numeric', hour12: false }), 10); + const inPsWindow = psHours && !isNaN(psStart) && !isNaN(psEnd) + ? (nowHourNL >= psStart && nowHourNL < psEnd) + : true; + if (inPsWindow) { + const psDischarge = batteryPower < 0 ? Math.abs(batteryPower) : 0; + const psLoad = Math.max(0, gridPower + psDischarge); + if (psLoad > psThreshold) { + const nowMs = Date.now(); + if (!this._lastPeakTriggerTs || nowMs - this._lastPeakTriggerTs > 2 * 60 * 1000) { + this._lastPeakTriggerTs = nowMs; + this.log(`[PEAK] Load ${Math.round(psLoad)}W > ${psThreshold}W in peak window → reactive policy run`); + this._runPolicyCheck().catch(e => this.error('[PEAK] reactive trigger:', e)); + } + } + } + } + } + + // ------------------------------------------------------ + // 📊 LEARNING: Record consumption patterns + // ------------------------------------------------------ + // Calculate TRUE house consumption: what the house actually uses, + // regardless of where the power comes from (grid, PV, or battery). + // gridPower: + = import, − = export + // batteryPower: + = charging (consuming PV/grid), − = discharging (supplying house) + // pvProductionW: always >= 0 (PV output) + const pvW = this._estimatePvProduction({ + gridPower, batteryPower, + sunScore: this.getCapabilityValue('sun_score') ?? 0, + }); + const houseConsumptionW = gridPower - batteryPower + pvW; + // Skip suspiciously-low readings caused by battery_power sensor lag: + // when in discharge mode, battery_power can read 0W for 1–2 poll cycles while + // still discharging, making consumption appear 0W and corrupting the learned EMA. + const currentHwMode = this.p1Device?.getCapabilityValue('battery_group_charge_mode') ?? ''; + const inDischargeMode = currentHwMode.includes('discharge'); + // Skip stale 0W readings: discharge mode + battery≈0W + either consumption<50W or grid≈0W (nul-op-de-meter with P1 lag) + const batteryPowerLag = inDischargeMode && Math.abs(batteryPower) < 10 + && (houseConsumptionW < 50 || Math.abs(gridPower) < 30); + if (houseConsumptionW >= 0 && !batteryPowerLag) { + await this.learningEngine.recordConsumption(houseConsumptionW).catch(err => + this.error('Learning consumption recording failed:', err) + ); + } + + // ------------------------------------------------------ + // 📊 SELF-SUFFICIENCY: Accumulate actual daily energy + // ------------------------------------------------------ + const POLL_H = 15 / 3600; // poll interval (15s) expressed in hours + const todayNL = new Date().toLocaleString('en-CA', { timeZone: 'Europe/Amsterdam' }).slice(0, 10); + if (this._todayDate !== todayNL) { + this._todayDate = todayNL; + this._todayGridImportKwh = 0; + this._todayConsumptionKwh = 0; + } + if (gridPower > 0) this._todayGridImportKwh += (gridPower * POLL_H) / 1000; + if (houseConsumptionW > 0) this._todayConsumptionKwh += (houseConsumptionW * POLL_H) / 1000; + + // Write today_self_sufficiency to settings at most every 5 minutes + const now = Date.now(); + if (this._todayConsumptionKwh > 0.01 && now - this._lastSelfSuffWrite > 5 * 60 * 1000) { + this._lastSelfSuffWrite = now; + const pct = Math.max(0, Math.min(100, Math.round( + (1 - this._todayGridImportKwh / this._todayConsumptionKwh) * 100 + ))); + this._queueSettingsPersist('today_self_sufficiency', { + pct, + gridImportKwh: +this._todayGridImportKwh.toFixed(3), + consumptionKwh: +this._todayConsumptionKwh.toFixed(3), + date: this._todayDate, + }); + } + + // ------------------------------------------------------ + // ⭐ 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 hour (cached per minute to avoid toLocaleString overhead at 4×/min) + const nowMin = Math.floor(Date.now() / 60_000); + if (this._cachedHourMin !== nowMin) { + this._cachedHour = parseInt(new Date().toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + this._cachedHourMin = nowMin; + } + const currentHour = this._cachedHour; + 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. + // Always subtract battery power (positive=charging, negative=discharging): + // Charging (+800W): grid=-1100W → virtual=-1900W (true export potential) + // Discharging(-337W): grid=-220W → virtual=-220-(-337)=+117W (no real export) + // Idle (0W): grid=-500W → virtual=-500W (direct PV export) + // Without this correction, battery discharging more than house load creates + // apparent grid export that falsely triggers PV detection. + const virtualGridPower = gridPower - batteryPower; + + // ✅ 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) + // Skip sun_score check when already ON — virtual grid balance is sufficient. + // Prevents false OFF during cloudy-but-producing conditions (e.g. 15% sun_score, 600W PV). + hasExport = virtualGridPower < PV_EXPORT_OFF; + hasActivePVConsumption = isDaylight && + 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 + + // Detect predictive mode transitions — uses live HW capability, not in-memory flag. + // _isPredictiveMode can be false after an app restart even if predictive was already + // active (in-memory state lost), so we check battery_group_charge_mode directly to + // avoid explanation_summary and policy_enabled getting permanently stuck. + { + const hwModeNow = this.p1Device.getCapabilityValue('battery_group_charge_mode'); + if (hwModeNow === 'predictive') { + if (!this._isPredictiveMode) { + // Recover from restart: re-enter predictive state without touching policy_enabled + // (it was already disabled before the restart; we don't know what the pre-predictive + // value was, so we leave it as-is rather than guess). + this._isPredictiveMode = true; + this.log('[Policy] Predictive mode gedetecteerd (P1 poll) — explanation_summary live bijhouden'); + } + // Keep explanation_summary + currentMode in sync while predictive is active + await this.setCapabilityValue('explanation_summary', `Slim laden: actief - SoC ${soc}%`).catch(this.error); + const ps = this._liveState.battery_policy_state + ?? this.homey.settings.get('battery_policy_state') + ?? {}; + if (ps.currentMode !== 'predictive') { + ps.currentMode = 'predictive'; + this._setLive('battery_policy_state', ps); + } + } else if (this._isPredictiveMode) { + // Predictive mode just ended + this._isPredictiveMode = false; + this.log('[Policy] Predictive mode beëindigd — battery-policy hersteld'); + // After a restart during predictive mode, _policyEnabledBeforePredictive is null + // (in-memory state lost). Fall back to persisted settings key, then true. + const restoreEnabled = this._policyEnabledBeforePredictive + ?? this.homey.settings.get('policy_enabled_before_predictive') + ?? true; + await this.setCapabilityValue('policy_enabled', restoreEnabled).catch(this.error); + this._policyEnabledBeforePredictive = null; + this.homey.settings.unset('policy_enabled_before_predictive'); + this._runPolicyCheck().catch(err => this.error('Post-predictive policy check failed:', err)); + } + } + + // Midnight day rollover: swap _chartTomorrow → _chartToday when Amsterdam date changes. + // Prevents camera from showing yesterday's chart during SlimLaden/predictive pause overnight. + { + const todayKey = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + if (this._lastChartRolloverDay !== todayKey && this._chartToday?.slots?.length > 0) { + const firstSlotDay = new Date(this._chartToday.slots[0].ts) + .toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + if (firstSlotDay !== todayKey) { + this._lastChartRolloverDay = todayKey; + this.log('[Chart] Day rollover — swapping tomorrow chart to today camera'); + this._chartToday = this._chartTomorrow ?? null; + this._chartTomorrow = null; + this._chartHashToday = null; + this._chartHashTomorrow = null; + this.planningImageToday?.update().catch(() => {}); + this.planningImageTomorrow?.update().catch(() => {}); + } + } + } + + } 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); + this.policyCheckInterval = null; + } + if (this._slotAlignTimeout) { + this.homey.clearTimeout(this._slotAlignTimeout); + this._slotAlignTimeout = null; + } + if (this._hourBoundaryTimeout) { + this.homey.clearTimeout(this._hourBoundaryTimeout); + } + + // Align to 15-min EPEX slot boundaries (:00, :15, :30, :45 UTC). + // Price slots are keyed to UTC multiples of 15 min; running right at the + // boundary ensures the full slot duration is covered by the correct action. + // Without alignment the setInterval drifts to ~11 min into each slot, + // leaving only ~4 min of discharge per slot. + const now = Date.now(); + const msUntilNextSlot = intervalMs - (now % intervalMs) + 200; // 200ms grace + + this.log(`Policy check aligning to next slot boundary in ${Math.round(msUntilNextSlot / 1000)}s, then every ${intervalMinutes} min`); + + this._slotAlignTimeout = this.homey.setTimeout(() => { + this._slotAlignTimeout = null; + this._maybeRefreshWeatherOnly().catch(() => {}); + if (this.getCapabilityValue('policy_enabled')) { + this._runPolicyCheck().catch(err => this.error('Slot-aligned policy check failed:', err)); + } + this.policyCheckInterval = this.homey.setInterval(async () => { + await this._maybeRefreshWeatherOnly().catch(() => {}); + if (this.getCapabilityValue('policy_enabled')) { + await this._runPolicyCheck(); + } else if (this.p1Device) { + // Policy disabled (predictive or user off): still record mode + SoC for the camera chart + const mode = this.p1Device._currentDetailedMode + || this.p1Device.getCapabilityValue('battery_group_charge_mode') + || 'unknown'; + this._recordModeHistory(mode); + this._recordSoCHistory(this.p1Device.getCapabilityValue('battery_group_average_soc') ?? 50); + } + }, intervalMs); + }, msUntilNextSlot); + + // Hour-boundary run fires ~5s after each full hour as a belt-and-suspenders + // safety net (e.g. when hourly prices change and no price refresh is pending). + this._scheduleHourBoundary(); + + this.log(`Policy check scheduled every ${intervalMinutes} minutes, aligned to slot 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 () => { + await this._maybeRefreshWeatherOnly().catch(() => {}); + 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 = parseInt(new Date().toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + 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(); + const nowAms = now.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Amsterdam' }); + const predictiveSuffix = this._isPredictiveMode ? ' (predictive — prices only)' : ''; + this.log(`🔄 Refreshing prices... (${nowAms} Amsterdam)${predictiveSuffix}`); + + 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 (!this._isPredictiveMode && priceCount > 0 && this.getCapabilityValue('policy_enabled')) { + // When tomorrow's prices arrive for the first time today, refresh weather + // so pvKwhTomorrow in the terminal value uses an up-to-date PV forecast. + const todayDate = new Date().toDateString(); + if (priceCount > 24 && this._tomorrowPricesWeatherRefreshedOn !== todayDate) { + this._tomorrowPricesWeatherRefreshedOn = todayDate; + this.log('📡 Tomorrow prices detected — refreshing weather for terminal value accuracy'); + await this._updateWeather().catch(e => this.error('Weather refresh on tomorrow prices failed:', e)); + } + // 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.weatherForecaster.knmiApiKey = devSettings.knmi_api_key || null; + this.weatherData = await this.weatherForecaster.fetchForecast(latitude, longitude, pvTilt, pvAzimuth); + + // Buienradar: 5-min precipitation radar for next 2 hours (fire-and-forget, non-critical) + this.weatherForecaster.fetchBuienradar(latitude, longitude) + .then(data => { + this.buienradarData = data; + if (data.length > 0) { + const rainingSlots = data.filter(s => s.mmPerHour >= 0.1); + if (rainingSlots.length > 0) { + const maxMmh = Math.max(...data.map(s => s.mmPerHour)); + this.log(`🌧️ Buienradar: ${rainingSlots.length}/24 slots met neerslag (max ${maxMmh.toFixed(1)} mm/u)`); + } + } + }) + .catch(e => this.error('Buienradar update failed:', e)); + + // 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().toLocaleString('en-CA', { timeZone: 'Europe/Amsterdam' }).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 × temperature derating + todayKwh = todayProfiles.reduce((sum, h) => sum + pvCapW * PR * (h.radiationWm2 / 1000) * _pvTempFactor(h.temp, h.radiationWm2), 0) / 1000; + remainingKwh = futureProfiles.reduce((sum, h) => sum + pvCapW * PR * (h.radiationWm2 / 1000) * _pvTempFactor(h.temp, h.radiationWm2), 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; + if (this.weatherData.pvKwhToday !== null) { + this.setCapabilityValue('pv_forecast_kwh', this.weatherData.pvKwhToday).catch(this.error); + } + + // Net surplus = PV remaining minus expected consumption during PV hours. + // Prevents delay-charge on low-PV days where household load consumes most of the yield. + if (remainingKwh !== null && this.learningEngine) { + let consumptionDuringPvHours = 0; + for (const h of futureProfiles) { + const slotIndex = h.time.getUTCHours() * 4; + const pvW = learnedSlots >= 10 + ? h.radiationWm2 * (yfs?.[slotIndex] ?? 0) + : pvCapW * PR * (h.radiationWm2 / 1000); + if (pvW > 0) consumptionDuringPvHours += (this.learningEngine.getPredictedConsumption(h.time) ?? 0) / 1000; + } + const surplus = Math.max(0, remainingKwh - consumptionDuringPvHours); + this.weatherData.pvSurplusRemaining = Math.round(surplus * 10) / 10; + this.log(`☀️ PV surplus remaining: ${surplus.toFixed(1)} kWh (${remainingKwh.toFixed(1)} kWh PV − ${consumptionDuringPvHours.toFixed(1)} kWh consumption during PV hours)`); + } else { + this.weatherData.pvSurplusRemaining = null; + } + + // Build per-hour PV forecast for the chart (today + tomorrow). + // Uses dailyProfiles (all 24h, incl. past) so the chart line is complete and consistent. + // Runs here — not in _recomputeOptimizer — so it updates even when the policy is disabled, + // and chart values are NOT distorted by the accuracy-discount applied for optimizer planning. + if (learnedSlots >= 10 || pvCapW > 0) { + const nowFc = new Date(); + const nowAmsDate = nowFc.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const tomorrowAmsDate = new Date(nowFc.getTime() + 86_400_000).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const pvFcByDay = [{}, {}]; + + for (const h of this.weatherData.dailyProfiles) { + const d = h.time instanceof Date ? h.time : new Date(h.time); + const hDate = d.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const dayIdx = hDate === nowAmsDate ? 0 : hDate === tomorrowAmsDate ? 1 : -1; + if (dayIdx < 0) continue; + const hHour = parseInt(d.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + const s0 = d.getUTCHours() * 4; + let pvPowerW; + if (learnedSlots >= 10 && yfs) { + const yf4 = [yfs[s0], yfs[s0+1], yfs[s0+2], yfs[s0+3]].filter(v => v != null && v > 0); + const yf = yf4.length > 0 ? yf4.reduce((a, b) => a + b, 0) / yf4.length : 0; + const raw = Math.round(h.radiationWm2 * yf); + pvPowerW = pvCapW > 0 ? Math.min(raw, pvCapW) : raw; + } else { + pvPowerW = pvCapW > 0 ? Math.min(pvCapW, Math.round(pvCapW * PR * (h.radiationWm2 / 1000) * _pvTempFactor(h.temp, h.radiationWm2))) : 0; + } + pvFcByDay[dayIdx][hHour] = pvPowerW; + } + + // Only write chart if optimizer hasn't pushed a fresher blended forecast recently. + // Prevents _updateWeather (1h cycle) from overwriting the 15-min optimizer chart. + // Also skips the Solcast fetch — optimizer already blends Solcast, no extra API call needed. + const omFcByDay = [{ ...pvFcByDay[0] }, { ...pvFcByDay[1] }]; + const blendAge = this._pvForecastBlendedAt ? Date.now() - this._pvForecastBlendedAt : Infinity; + if (blendAge > 30 * 60 * 1000) { + // Optimizer hasn't run recently (policy disabled or first startup): mirror the + // optimizer's weighted OM/Solcast blend here so the settings page uses the same + // forecast source as the planner. + let scFcByDay = null; + if (settings.solcast_enabled && settings.solcast_api_key && settings.solcast_resource_id) { + if (!this._solcastProvider) { + const SolcastProvider = require('../../lib/solcast-provider'); + this._solcastProvider = new SolcastProvider(this.homey); + } + try { + const solcastForecast = await this._solcastProvider.getForecast( + settings.solcast_api_key, + settings.solcast_resource_id, + ); + if (Array.isArray(solcastForecast) && solcastForecast.length > 0) { + // Persist day-start Solcast snapshot once per Amsterdam calendar day. + // Survives restarts; provides past-hour SC data for the chart green line. + const _scSnapKey = 'policy_sc_daystart'; + const _scSnap = this.homey.settings.get(_scSnapKey); + if (!_scSnap || _scSnap.date !== nowAmsDate) { + this.homey.settings.set(_scSnapKey, { date: nowAmsDate, data: solcastForecast }); + this.log(`[Solcast] Day-start snapshot saved for ${nowAmsDate} (${solcastForecast.length} slots)`); + } + + const { wOM: baseWom, wSC: baseWsc } = this.learningEngine?.getPvBlendWeights?.() ?? { wOM: 0.5, wSC: 0.5 }; + const nowAmsHour = parseInt(nowFc.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + const scByDayHour = [{}, {}]; + for (const s of solcastForecast) { + const st = new Date(s.timestamp); + const sDate = st.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const sIdx = sDate === nowAmsDate ? 0 : sDate === tomorrowAmsDate ? 1 : -1; + if (sIdx < 0) continue; + const sHour = parseInt(st.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + if (!Array.isArray(scByDayHour[sIdx][sHour])) scByDayHour[sIdx][sHour] = []; + scByDayHour[sIdx][sHour].push(s.pvPowerW); + } + // Divergence correction: when SC exceeds OM for today, reduce SC weight. + // Use future hours only — past SC is irrelevant for remaining planning. + let omTodayWh = 0, scTodayWh = 0; + for (const [hour, values] of Object.entries(scByDayHour[0])) { + if (!Array.isArray(values) || values.length === 0) continue; + if (parseInt(hour) < nowAmsHour) continue; // future only + const omW = pvFcByDay[0][hour]; + if (typeof omW !== 'number') continue; + omTodayWh += omW; + scTodayWh += values.reduce((a, b) => a + b, 0) / values.length; + } + let wOM = baseWom, wSC = baseWsc, divLog = ''; + if (omTodayWh > 0 && scTodayWh > omTodayWh) { + const div = scTodayWh / omTodayWh; + wSC = Math.max(baseWsc * 0.5, baseWsc / Math.sqrt(div)); + wOM = 1.0 - wSC; + divLog = ` div=${div.toFixed(2)}`; + } + scFcByDay = [{}, {}]; + for (let d = 0; d < 2; d++) { + for (const [hour, values] of Object.entries(scByDayHour[d])) { + if (!Array.isArray(values) || values.length === 0) continue; + scFcByDay[d][hour] = Math.round(values.reduce((sum, v) => sum + v, 0) / values.length); + } + } + let blendedHours = 0; + for (let d = 0; d < 2; d++) { + for (const [hour, values] of Object.entries(scByDayHour[d])) { + if (!Array.isArray(values) || values.length === 0) continue; + if (d === 0 && parseInt(hour) < nowAmsHour) continue; // past hours: in scFcByDay for chart only + const scAvg = Math.round(values.reduce((sum, v) => sum + v, 0) / values.length); + const omW = pvFcByDay[d][hour]; + pvFcByDay[d][hour] = typeof omW === 'number' + ? Math.round(wOM * omW + wSC * scAvg) + : scAvg; + blendedHours++; + } + } + const scTodayKwh = Object.values(pvFcByDay[0]).reduce((s, w) => s + (w || 0), 0) / 1000; + const scTomKwh = Object.values(pvFcByDay[1]).reduce((s, w) => s + (w || 0), 0) / 1000; + this.log(`[Solcast] Blended ${blendedHours} chart hours with wOM=${wOM.toFixed(2)} wSC=${wSC.toFixed(2)}${divLog} — chart forecast now: vandaag ${scTodayKwh.toFixed(1)} kWh, morgen ${scTomKwh.toFixed(1)} kWh`); + } + } catch (err) { + this.log(`[Solcast] Chart forecast error: ${err.message}`); + } + } + this._setLive('policy_pv_forecast_hourly', pvFcByDay); + this._setLive('policy_pv_forecast_om', omFcByDay); + if (scFcByDay) this._setLive('policy_pv_forecast_sc', scFcByDay); + } + } + } else { + this.weatherData.pvKwhToday = null; + } + + const { sunshineNext4Hours: _s4, sunshineTodayRemaining: _st, sunshineTomorrow: _stom } = this.weatherData; + const sunScore = Math.round( + Math.min(50, (_s4 / 4) * 50) + + Math.min(25, (_st / 8) * 25) + + Math.min(25, (_stom / 10) * 25) + ); + + 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 + }); + + if (this.learningEngine) { + const ls = this.learningEngine.getStatistics(); + const bF = this.learningEngine.data?.radiation_bias_factor ?? 1.0; + const yfs = this.learningEngine.getSolarLearnedSlotCount(); + this.log(`[Learning] days=${ls.days_tracking} samples=${ls.total_samples} coverage=${ls.pattern_coverage}% pv_acc=${ls.pv_accuracy}% | yield=${yfs}/96 slots bias=${bF.toFixed(3)}`); + // Keep learning_status fresh even when policy/optimizer isn't running. + // pvPredictions/modelAcc live in learning_pv_chart_data (written by _gatherInputs) + // so this lightweight write stays small. + const rte = this.efficiencyEstimator?.getEfficiency(); + this._setLive('learning_status', { + days: ls.days_tracking, + samples: ls.total_samples, + coverage: ls.pattern_coverage, + pvAccuracy: ls.pv_accuracy, + rte: rte != null ? +(rte * 100).toFixed(1) : null, + cycles: this.efficiencyEstimator?.getCycleCount() ?? 0, + updatedAt: new Date().toISOString(), + }); + } + + if (this._isPredictiveMode) { + // Predictive mode: alleen PV camera verversen, optimizer niet aanraken. + // Als forecast ontbreekt (bijv. na versiesprong), eerst weather fetchen. + const hasForecast = !!(this._liveState.policy_pv_forecast_hourly + ?? this.homey.settings.get('policy_pv_forecast_hourly')); + const doUpdate = async () => { + if (!this.planningImagePv) await this._initPvCamera().catch(() => {}); + else await this.planningImagePv.update().catch(() => {}); + }; + if (!hasForecast) { + this._maybeRefreshWeatherOnly().catch(() => {}).then(doUpdate).catch(() => {}); + } else { + doUpdate().catch(() => {}); + } + return; + } + + // When policy is disabled (user off, not predictive): refresh PV camera with fresh forecast. + // After a restart planningImagePv is null (only _updatePlanningChart initialises it, which + // only runs after a policy run). Call _initPvCamera so the camera is registered with Homey + // and shows the correct today forecast rather than a stale cached image. + if (!this.getCapabilityValue('policy_enabled')) { + const hasForecast2 = !!(this._liveState.policy_pv_forecast_hourly + ?? this.homey.settings.get('policy_pv_forecast_hourly')); + const doUpdate2 = async () => { + if (!this.planningImagePv) await this._initPvCamera().catch(() => {}); + else await this.planningImagePv.update().catch(() => {}); + }; + if (!hasForecast2) { + this._maybeRefreshWeatherOnly().catch(() => {}).then(doUpdate2).catch(() => {}); + } else { + doUpdate2().catch(() => {}); + } + } + + // 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 }; + } + + // Refresh weather + PV forecast when policy is disabled (no full policy run needed). + async _maybeRefreshWeatherOnly() { + const settings = this.getSettings(); + if (settings.tariff_type !== 'dynamic') return; + const intervalMs = (settings.weather_update_interval || 1) * 3_600_000; + const age = this.weatherData?.fetchedAt ? Date.now() - this.weatherData.fetchedAt : Infinity; + if (age > intervalMs) { + await this._updateWeather(); + } + } + + async _runPolicyCheck({ skipEnabledCheck = false } = {}) { + if (this._policyCheckRunning) { + this.log('Policy check already in progress, skipping concurrent call'); + return; + } + this._policyCheckRunning = true; + try { + if (!skipEnabledCheck && !this.getCapabilityValue('policy_enabled')) { + this.log('Policy disabled, skipping check'); + return; + } + + if (!skipEnabledCheck && this.getCapabilityValue('policy_mode') === 'off') { + this.log('Policy mode is off, 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; + } + + // Record mode history + detect predictive mode (altijd, ook bij overrides) + if (this.p1Device) { + const currentHwMode = this.p1Device.getCapabilityValue('battery_group_charge_mode'); + const currentSoC = this.p1Device.getCapabilityValue('battery_group_average_soc') ?? 50; + // Use detailed mode from energy_v2 when available (predictive sub-types) + const detailedMode = this.p1Device._currentDetailedMode || currentHwMode || 'unknown'; + this._recordModeHistory(detailedMode); + this._recordSoCHistory(currentSoC); + + // ⭐ HW Slim laden (predictive) actief → policy uitschakelen + if (currentHwMode === 'predictive') { + if (!this._isPredictiveMode) { + this._isPredictiveMode = true; + // Save current policy_enabled state and disable — the P1 poll restores it when predictive ends + this._policyEnabledBeforePredictive = this.getCapabilityValue('policy_enabled') ?? false; + this.homey.settings.set('policy_enabled_before_predictive', this._policyEnabledBeforePredictive); + await this.setCapabilityValue('policy_enabled', false).catch(this.error); + this.log('🤖 HW Slim laden (predictive) actief — battery-policy uitgeschakeld'); + // Planning-webcams invalideren zodat ze leeg tonen + this.planningImageToday?.update().catch(() => {}); + this.planningImageTomorrow?.update().catch(() => {}); + this.planningImagePv?.update().catch(() => {}); + } + await this.setCapabilityValue('explanation_summary', `Slim laden: actief - SoC ${currentSoC}%`).catch(this.error); + // Record predictive mode in planning chart history + try { + const modeHistory = this.homey.settings.get('policy_mode_history') || []; + const nowTs = new Date(); + const bucket = Math.round(nowTs.getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000); + const existing = modeHistory.findIndex( + h => Math.round(new Date(h.ts).getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000) === bucket + ); + const entry = { ts: nowTs.toISOString(), hwMode: 'predictive', soc: currentSoC, price: null }; + if (existing >= 0) modeHistory[existing] = entry; + else modeHistory.push(entry); + if (modeHistory.length > 192) modeHistory.splice(0, modeHistory.length - 192); + this._queueSettingsPersist('policy_mode_history', modeHistory); + } catch (e) { this.error('Failed to save predictive mode history:', e); } + // Still recompute DP + update widget so planning stays fresh + try { + const inputs = await this._gatherInputs(); + if (inputs?.tariff) { + this.optimizationEngine.updateSettings({}); + await this._recomputeOptimizer(inputs); + // Patch live SoC into battery_policy_state so widget shows correct value + const ps = this._liveState.battery_policy_state + ?? this.homey.settings.get('battery_policy_state') ?? {}; + ps.batterySOC = currentSoC; + ps.currentMode = 'predictive'; + this._setLive('battery_policy_state', ps); + this._saveWidgetData({ skipChart: true }); + } + } catch (e) { this.error('Predictive recompute failed:', e.message); } + 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), + // or when intra-day conditions have drifted significantly from the last DP run. + const currentSoc = inputs.battery?.stateOfCharge ?? null; + if ((this.optimizationEngine.isStale() || this._shouldForceReoptimize(currentSoc)) && inputs.tariff) { + await this._recomputeOptimizer(inputs); + } + inputs.optimizer = this.optimizationEngine; + inputs.optimizerSlots = this.optimizationEngine._schedule?.slots ?? null; + + // Log planned vs actual SoC to detect consumption/discharge drift. + if (currentSoc != null && inputs.optimizerSlots?.length) { + const nowMs = Date.now(); + const plannedSlot = inputs.optimizerSlots + .filter(s => new Date(s.timestamp).getTime() <= nowMs) + .at(-1); + if (plannedSlot?.socProjected != null) { + const socDrift = currentSoc - plannedSlot.socProjected; + this.log(`[SoC] actual=${currentSoc}% planned=${plannedSlot.socProjected.toFixed(1)}% drift=${socDrift > 0 ? '+' : ''}${socDrift.toFixed(1)}pp`); + } + } + + const result = this.policyEngine.calculatePolicy(inputs); + + // Free large price arrays before loading the explainability engine. + // generateExplanation() in the DP path only reads inputs.tariff.currentPrice + // (a single number) — it never touches allPrices15min / effectivePrices / allPrices. + // Nulling these here frees ~300–500 KB of V8 heap before the 10–15 MB engine load. + if (inputs.tariff) { + inputs.tariff.allPrices15min = null; + inputs.tariff.effectivePrices = null; + inputs.tariff.allPrices = null; + inputs.tariff.next24Hours = null; + } + + // ------------------------------------------------------ + // 📊 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.round(Math.max(0, Math.min(100, result.confidence + confidenceAdjustment))); + this.log(`📊 Learning adjusted confidence: ${originalConfidence} → ${result.confidence} (${confidenceAdjustment > 0 ? '+' : ''}${confidenceAdjustment})`); + } + + // Guard: skip explainability when heap is already high — the engine adds ~25 MB + // which pushes total above the Homey memory ceiling (~65 MB heap). + let _heapBeforeExplain = 50; // conservative default: skip + try { _heapBeforeExplain = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + let explanation = null; + if (_heapBeforeExplain > 35) { + this.log(`[MEM] Skipping explainability — heap ${_heapBeforeExplain.toFixed(1)} MB > 35 MB guard`); + } else { + if (!this.explainabilityEngine) { + this.explainabilityEngine = new (require('../../lib/explainability-engine'))(this.homey); + } + explanation = this.explainabilityEngine.generateExplanation( + result, + inputs, + result.scores + ); + this.homey.api.realtime('explainability_update', explanation); + this._setLive('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 dpAction = this.optimizationEngine?.getSlot(new Date()) ?? null; + const planningData = { + batterySOC, + policyMode, + recommendedMode: recommended, + currentMode: recommended, // will be overwritten below with actual HW mode + dpAction, // DP-planned action for current slot ('charge'|'discharge'|'preserve') + 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, + avgCost: inputs.batteryCost?.avgCost ?? 0, + lastUpdate: new Date().toISOString() + }; + this._setLive('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._setLive('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') ?? 0, + max_soc: this.getSetting('max_soc') ?? 100, + battery_efficiency: this.getSetting('battery_efficiency') || 0.75, + 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, + price_resolution: this.getSetting('price_resolution') || '15min', + }); + // 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.75; + 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._queueSettingsPersist('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 + ? (inputs.dpDecision + ? this.explainabilityEngine._generateDpShortSummary({ hwMode: currentActiveMode }, inputs) + : this.explainabilityEngine._generateShortSummary({ hwMode: currentActiveMode }, inputs)) + : currentActiveMode; + 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 || currentMode + }); + } 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 + }); + + // Store compact diagnostic for user-facing troubleshooting (settings page). + if (result.debug) { + result.debug.appVersion = require('../../app.json').version; + this._setLive('policy_last_run_debug', result.debug); + } + + // ------------------------------------------------------ + // 📊 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 + + this._lastTariffInfo = inputs.tariff ?? null; + await this._triggerRecommendationChanged(result, explanation); + this._checkFavorableWindow(inputs.tariff); + + 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}`); + + // Warn when real-time policy deviates from the DP's planned action. + // Suppress when DP=preserve + PV active → zero_charge_only: not a real deviation, + // DP prices the slot as neutral and PV charging happens automatically. + const _dpAction = this.optimizationEngine?.getSlot(new Date()) ?? null; + const _isPvCharge = applyMode === 'zero_charge_only' && this._pvState; + if (_dpAction && _dpAction !== result.policyMode && !_isPvCharge) { + this.log(`⚠️ PLAN AFWIJKING: DP gepland ${_dpAction} maar policy koos ${result.policyMode} → hwMode ${applyMode}`); + } + } 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`); + } + } + + // Always track SOC + mode history for the planning chart, regardless of whether + // the mode was successfully applied. This ensures the SOC line starts from + // the beginning of the day even when the battery isn't responding yet. + try { + const modeHistory = this.homey.settings.get('policy_mode_history') || []; + const currentPrice = result.debug?.price ?? inputs.tariff?.currentPrice ?? null; + const currentSoc = this.getCapabilityValue('battery_soc_mirror') ?? null; + const _rtBatt = this.p1Device?._getRealtimePluginBatteryData?.() ?? []; + const battW = _rtBatt.length > 0 + ? _rtBatt.reduce((sum, b) => sum + (b.power ?? 0), 0) + : (inputs.p1?.battery_power ?? this._lastBatteryTargetW ?? null); + const gridW = inputs.p1?.resolved_gridPower ?? 0; + const socDropped = this._lastHistorySoc != null && currentSoc != null && currentSoc < this._lastHistorySoc - 1; + const sensorLag = socDropped && battW !== null && Math.abs(battW) < 10; + // Immediate stale detection: discharge mode + battery=0W + grid=0W = P1 lag (nul-op-de-meter but battery unreported) + const _hwModeNow = this.p1Device?.getCapabilityValue('battery_group_charge_mode') ?? ''; + const _inDischarge = _hwModeNow.includes('discharge'); + const zeroOnMeterLag = _inDischarge && Math.abs(battW ?? 0) < 10 && Math.abs(gridW) < 30; + const _calSunset = inputs.weather?.todaySunset; + const _calSunrise = inputs.weather?.todaySunrise; + const _afterSunsetNow = !_calSunset + || (Date.now() > _calSunset.getTime() + 30 * 60 * 1000) + || (_calSunrise instanceof Date && Date.now() < _calSunrise.getTime()); + const bmsCalibration = (currentSoc ?? 0) <= 0 + && Math.abs(battW ?? 0) < 50 + && gridW > 700 + && _afterSunsetNow + && !applyMode.includes('charge') + && !applyMode.includes('discharge'); + const p1Available = this.p1Device?.getAvailable() !== false; + this._lastHistorySoc = currentSoc; + const nowTs = new Date(); + const bucket = Math.round(nowTs.getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000); + const existing = modeHistory.findIndex( + h => Math.round(new Date(h.ts).getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000) === bucket + ); + const entry = { + ts: nowTs.toISOString(), + hwMode: applyMode, + price: currentPrice, + soc: currentSoc, + maxChargePrice: this.getSetting('max_charge_price'), + minDischargePrice: this.getSetting('min_discharge_price'), + pvW: result.debug?.pvEstimate ?? null, + consumW: result.debug?.houseConsumption ?? null, + gridW: this.getCapabilityValue('grid_power_mirror') ?? null, + battW: inputs.p1?.battery_power ?? this._lastBatteryTargetW ?? null, + policyMode: result.policyMode ?? result.debug?.policyMode ?? null, + dpAction: typeof dpAction === 'string' ? dpAction : (dpAction?.action ?? null), + exception: !p1Available ? 'p1_unavailable' : (sensorLag || zeroOnMeterLag) ? 'battery_sensor_lag' : bmsCalibration ? 'bms_calibration' : (result.debug?.exception ?? null), + }; + if (existing >= 0) { + // Update existing bucket — keep best SoC (non-null wins) + if (entry.soc == null) entry.soc = modeHistory[existing].soc; + modeHistory[existing] = entry; + } else { + modeHistory.push(entry); + } + // Keep last 192 entries (48h headroom — extra triggers won't push out early-morning data) + if (modeHistory.length > 192) modeHistory.splice(0, modeHistory.length - 192); + this._queueSettingsPersist('policy_mode_history', modeHistory); + } catch (e) { + this.error('Failed to save mode history:', e); + } + } 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 + planningData.currentMode = actualHwMode; + this._setLive('battery_policy_state', planningData); + + // Always sync explanation_summary to the actual hardware mode + if (this.explainabilityEngine) { + const hwActiveSummary = inputs.dpDecision + ? this.explainabilityEngine._generateDpShortSummary({ hwMode: actualHwMode }, inputs) + : this.explainabilityEngine._generateShortSummary({ hwMode: actualHwMode }, inputs); + await this.setCapabilityValue('explanation_summary', hwActiveSummary).catch(this.error); + } + } + } + + // Release ExplainabilityEngine after use — module stays compiled in V8 cache, + // re-instantiation next run is free. Frees ~10–15 MB on memory-constrained devices. + this.explainabilityEngine = null; + + // Push compact data to the dashboard widget + try { this._saveWidgetData(); } catch (e) { this.error('Widget data save failed:', e); } + + } catch (error) { + this.error('Policy check failed:', error); + await this.setCapabilityValue('explanation_summary', + `Error: ${error.message}` + ); + } finally { + this._policyCheckRunning = false; + } + } + + _saveWidgetData({ skipChart = false } = {}) { + const schedule = this._liveState.policy_optimizer_schedule + ?? this.homey.settings.get('policy_optimizer_schedule') ?? []; + const state = this._liveState.battery_policy_state + ?? this.homey.settings.get('battery_policy_state') ?? {}; + const use1h = this.getSetting('price_resolution') === '1h'; + const priceData = use1h + ? (this.homey.settings.get('policy_all_prices') || []) + : (this.homey.settings.get('policy_all_prices_15min') || []); + const step = use1h ? 3600 * 1000 : 15 * 60 * 1000; + const now = Date.now(); + + // Midnight of today in Amsterdam time (used to include past hours of today) + // Subtract Amsterdam time-of-day from now — avoids parsing locale strings (unreliable on Node.js) + const todayStart = (() => { + const parts = new Intl.DateTimeFormat('en-US', { + timeZone: 'Europe/Amsterdam', + hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, + }).formatToParts(new Date(now)); + const amH = +parts.find(p => p.type === 'hour').value % 24; + const amM = +parts.find(p => p.type === 'minute').value; + const amS = +parts.find(p => p.type === 'second').value; + return now - ((amH * 3600 + amM * 60 + amS) * 1000 + (now % 1000)); + })(); + + // Helper: Amsterdam hour from timestamp + const amhour = ts => parseInt( + new Date(ts).toLocaleString('en-US', { timeZone: 'Europe/Amsterdam', hour: '2-digit', hour12: false }), + 10 + ); + + // Future slots from the optimizer schedule (include current slot via -15min buffer) + // Use step as grace period so the current slot is never lost in the gap + // between past-loop cutoff (now - step) and future filter (now - 15min) + const currentSoc = state.batterySOC ?? null; + const futureSlots = schedule + .filter(s => new Date(s.timestamp).getTime() >= now - step) + .slice(0, 192) + .map(s => { + const ts = new Date(s.timestamp).getTime(); + return { + ts, + hour: amhour(ts), + price: s.price != null ? Math.round(s.price * 10000) / 10000 : null, + mode: s.hwMode || 'standby', + soc: s.socProjected != null ? Math.round(s.socProjected) : null, + pvW: Math.round(s.pvW ?? 0), + consumptionW: Math.round(s.consumptionW ?? 0) + }; + }); + + // Re-simulate future SoC trajectory from the real current SoC. + // The optimizer schedule may have been computed hours ago with a different starting SoC, + // leading to a stale projection (e.g. showing 100% while battery is actually at 37%). + // Instead of a simple delta-shift (breaks when all projected values are equal), + // walk forward from the real SoC and simulate charge/discharge per slot using + // the planned mode and PV power already stored in each slot. + if (currentSoc != null && futureSlots.length > 0) { + const capacityKwh = state.totalCapacityKwh || this.getSetting('pv_capacity_w') ? null : 2.688; + const capKwh = state.totalCapacityKwh || 2.688; + const maxChargeW = state.maxChargePowerW || 800; + const maxDischargeW = state.maxDischargePowerW || 800; + const rte = this.efficiencyEstimator?.getEfficiency() ?? 0.75; + const slotH = step / 3_600_000; // 1 for hourly, 0.25 for 15-min + const maxSoc = this.getSetting('max_soc') ?? 100; + const minSoc = this.getSetting('min_soc') ?? 0; + + let simSoc = currentSoc; + for (const slot of futureSlots) { + slot.soc = Math.round(simSoc); + const pvW = slot.pvW || 0; + if (slot.mode === 'to_full') { + // Charge at max rate from grid + const deltaKwh = (maxChargeW / 1000) * slotH * rte; + simSoc += (deltaKwh / capKwh) * 100; + } else if (slot.mode === 'zero_charge_only' || slot.mode === 'pv_trickle') { + // Charge from PV only — only net surplus can enter the battery. + const chargeW = Math.min(Math.max(0, pvW - (slot.consumptionW || 0)), maxChargeW); + const deltaKwh = (chargeW / 1000) * slotH * rte; + simSoc += (deltaKwh / capKwh) * 100; + } else if (slot.mode === 'zero_discharge_only') { + // Discharge at max rate + const deltaKwh = (maxDischargeW / 1000) * slotH; + simSoc -= (deltaKwh / capKwh) * 100; + } + // standby / zero: no SoC change + simSoc = Math.max(minSoc, Math.min(maxSoc, simSoc)); + } + } + + // Past slots from mode history (has real soc + mode) — keyed by rounded 15-min ts + const modeHistory = this.homey.settings.get('policy_mode_history') || []; + const historyMap = new Map(); + for (const h of modeHistory) { + const ts15 = Math.round(new Date(h.ts).getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000); + if (!historyMap.has(ts15)) historyMap.set(ts15, h); + } + + // Build price lookup from price cache (15min or hourly depending on resolution) + const priceMap = new Map(); + for (const p of priceData) { + const ts = new Date(p.timestamp).getTime(); + if (use1h) { + // Hourly prices: cover the full hour (4 × 15min slots for backward compat) + for (let q = 0; q < 4; q++) priceMap.set(ts + q * 15 * 60 * 1000, p.price); + } else { + priceMap.set(ts, p.price); + } + } + + // Actual PV per hour today (index = Amsterdam hour) + const pvActual = this.homey.settings.get('policy_pv_actual_today'); + const pvHourly = Array.isArray(pvActual?.hourly) ? pvActual.hourly : []; + + const futureTs = new Set(futureSlots.map(s => s.ts)); + const pastSlots = []; + for (let ts = todayStart; ts < now - step; ts += step) { + if (futureTs.has(ts)) continue; + const h = historyMap.get(ts); + const hr = amhour(ts); + // Interpolate between adjacent hourly actual PV values for smooth bell curve + const minInHour = Math.floor((ts % 3600000) / 60000); + const frac = minInHour / 60; + const v0 = pvHourly[hr] ?? 0; + const v1 = pvHourly[hr + 1] ?? 0; + const pvW = Math.round(v0 + (v1 - v0) * frac); + pastSlots.push({ + ts, + hour: hr, + price: priceMap.has(ts) ? Math.round(priceMap.get(ts) * 10000) / 10000 : (h?.price ?? null), + mode: h?.hwMode || 'past', + soc: h?.soc != null ? Math.round(h.soc) : null, + pvW + }); + } + + // If the optimizer ran hourly but the display step is 15-min, expand future slots + // to 15-min resolution by interpolating between hourly entries. Without this the + // future part of the chart is 4× coarser than the past, making a 4-hour SoC rise + // look like it happens in 1 slot (visual distortion). + const SLOT_15 = 15 * 60 * 1000; + let expandedFuture = futureSlots; + if (!use1h && futureSlots.length >= 2) { + const slotGap = futureSlots[1].ts - futureSlots[0].ts; + if (slotGap > SLOT_15) { + expandedFuture = []; + for (let i = 0; i < futureSlots.length; i++) { + const cur = futureSlots[i]; + const next = futureSlots[i + 1]; + expandedFuture.push(cur); + if (next) { + const steps = Math.round((next.ts - cur.ts) / SLOT_15); + for (let q = 1; q < steps; q++) { + const ts = cur.ts + q * SLOT_15; + expandedFuture.push({ + ts, + hour: amhour(ts), + price: cur.price, + mode: cur.mode, + soc: (cur.soc != null && next.soc != null) + ? Math.round(cur.soc + (next.soc - cur.soc) * q / steps) + : cur.soc, + pvW: cur.pvW, + }); + } + } + } + } + } + + // For future slots that have already started (ts < now), use actual pvHourly data + // so the chart doesn't drop to 0 when the optimizer reruns with stale/missing PV. + if (pvHourly.length > 0) { + for (const slot of expandedFuture) { + if (slot.ts < now) { + const hr = amhour(slot.ts); + const minInHour = Math.floor((slot.ts % 3600000) / 60000); + const v0 = pvHourly[hr] ?? 0; + const v1 = pvHourly[hr + 1] ?? 0; + slot.pvW = Math.round(v0 + (v1 - v0) * (minInHour / 60)); + } + } + } + + const slots = [...pastSlots, ...expandedFuture] + .sort((a, b) => a.ts - b.ts) + .slice(0, 192); // max 48h worth of 15-min slots + const compact = { + currentSoc: state.batterySOC ?? null, + currentMode: state.currentMode ?? 'standby', + currentPrice: futureSlots[0]?.price ?? null, + pvCapacityW: this.getSetting('pv_capacity_w') || 0, + updatedAt: new Date().toISOString(), + slots + }; + // In-memory cache — widget api.js reads via driver.getDevices()[0]._widgetData. + // Avoids the ~30 MB heap spike that homey.settings.set allocates per call + // (measured: 8 KB payload triggered 30 MB framework-internal allocation). + this._widgetData = compact; + this.homey.api.realtime('planning-update', compact); + this.log(`[Widget] Data saved: ${slots.length} slots`); + + // Camera image in device UI (fire-and-forget) + // skipChart=true at startup to avoid 3 concurrent HTTP requests during memory-critical boot. + // Deferred 8s: policy runs push heap to ~60 MB; GC settles back to ~29 MB within 5s. + // Calling immediately would always trip the 35 MB heap guard and never update the chart. + if (!skipChart) { + this.homey.setTimeout( + () => this._updatePlanningChart(compact).catch(e => this.error('Camera image update failed:', e)), + 8000, + ); + } + } + + /** + * Returns true when intra-day conditions have drifted enough from the last DP + * run that a forced recompute is warranted — even if the schedule isn't stale yet. + * + * Two triggers: + * 1. SoC deviation >15 pp from the projected SoC of the current slot. + * 2. PV intraday ratio changed by >25% relative to the ratio used in the last run. + */ + _shouldForceReoptimize(currentSoc) { + const schedule = this.optimizationEngine._schedule; + if (!schedule?.slots?.length) return false; + + // ── Trigger 1: SoC deviation ───────────────────────────────────────────── + if (currentSoc != null) { + const nowMs = Date.now(); + // Find the slot whose timestamp is closest to (and ≤) now. + const currentSlot = schedule.slots + .filter(s => new Date(s.timestamp).getTime() <= nowMs) + .at(-1); + if (currentSlot?.socProjected != null) { + const socDelta = Math.abs(currentSoc - currentSlot.socProjected); + if (socDelta > 15) { + this.log(`[Reopt] SoC deviation ${socDelta.toFixed(1)} pp (actual ${currentSoc}% vs projected ${currentSlot.socProjected}%) → forcing recompute`); + return true; + } + } + } + + // ── Trigger 2: PV intraday ratio drift ─────────────────────────────────── + if (this._lastIntradayPvRatio != null && + this.learningEngine && + Array.isArray(this.learningEngine.data?.pv_predictions)) { + const now = new Date(); + const nowMs = now.getTime(); + const cutoffMs = nowMs - 3 * 3_600_000; + const preds = this.learningEngine.data.pv_predictions.filter(p => + p.timestamp >= cutoffMs && p.timestamp <= nowMs && + p.predicted > 50 && p.actual > 50 + ); + if (preds.length >= 4) { + const sumF = preds.reduce((s, p) => s + p.predicted, 0); + const sumA = preds.reduce((s, p) => s + p.actual, 0); + const currentRatio = Math.min(2.5, Math.max(0.4, sumA / sumF)); + const ratioDelta = Math.abs(currentRatio - this._lastIntradayPvRatio); + if (ratioDelta > 0.15) { + this.log(`[Reopt] PV ratio drift ${ratioDelta.toFixed(2)} (was ${this._lastIntradayPvRatio.toFixed(2)}, now ${currentRatio.toFixed(2)}) → forcing recompute`); + return true; + } + } + } + + // Throttled diagnostic log — once per hour so you can verify the check is running + // and see current values without flooding the log. + const nowMs = Date.now(); + if (!this._lastReoptDiagLog || nowMs - this._lastReoptDiagLog > 3_600_000) { + this._lastReoptDiagLog = nowMs; + const schedule = this.optimizationEngine._schedule; + const currentSlot = schedule?.slots?.filter(s => new Date(s.timestamp).getTime() <= nowMs).at(-1); + const socDelta = (currentSoc != null && currentSlot?.socProjected != null) + ? Math.abs(currentSoc - currentSlot.socProjected).toFixed(1) : '—'; + const pvRatio = this._lastIntradayPvRatio?.toFixed(2) ?? '—'; + this.log(`[Reopt] No trigger — SoC delta=${socDelta}pp, PV ratio at last run=${pvRatio}`); + } + return false; + } + + /** + * (Re)compute the OptimizationEngine schedule from the current inputs. + * Called lazily in _runPolicyCheck whenever the schedule is stale. + */ + async _recomputeOptimizer(inputs) { + // Use 15-min prices unless price_resolution is set to '1h' + const now = new Date(); + const use15min = inputs.settings?.price_resolution !== '1h'; + const raw15min = use15min ? inputs.tariff?.allPrices15min : null; + // Only keep future prices: DP forward pass starts at currentSoc, so including + // past hours causes simulated SoC to diverge from reality by the current slot. + const hourBoundary = new Date(now); + hourBoundary.setMinutes(0, 0, 0); + const rawPrices = (raw15min?.length > 0) + ? raw15min.filter(p => new Date(p.timestamp) >= now) + : (inputs.tariff?.allPrices || inputs.tariff?.next24Hours)?.filter(p => new Date(p.timestamp) >= hourBoundary); + 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 maxYfs = this.learningEngine?.getSolarSlotMaxYieldFactors(); + const maxRads = this.learningEngine?.data?.solar_slot_max_radiation; + const learnedSlots = this.learningEngine?.getSolarLearnedSlotCount() ?? 0; + + if (Array.isArray(inputs.weather?.hourlyForecast)) { + + let clearSkyCeilingApplied = 0; + pvForecast = inputs.weather.hourlyForecast + .filter(h => typeof h.radiationWm2 === 'number') + .map(h => { + const d = h.time instanceof Date ? h.time : new Date(h.time); + // Average yield factor across all 4 × 15-min slots of this UTC hour. + // Using only slot[0] (h*4) would underestimate the sunrise hour: the first + // slot (e.g. 04:00–04:15) is often pre-sunrise with yf≈0, while slots + // 04:15–04:59 carry the real production — causing forecast to start 1h late. + const s0 = d.getUTCHours() * 4; + const yf4 = [yfs[s0], yfs[s0+1], yfs[s0+2], yfs[s0+3]].filter(v => v != null && v > 0); + const yf = yf4.length > 0 ? yf4.reduce((a, b) => a + b, 0) / yf4.length : 0; + let rawPvW = learnedSlots >= 10 + ? Math.round(h.radiationWm2 * yf) + : pvCapacityW > 0 ? Math.round(pvCapacityW * pvPR * (h.radiationWm2 / 1000) * _pvTempFactor(h.temp, h.radiationWm2)) : 0; + + // Clear-sky ceiling: EMA converges toward average, not peak — on clear days after + // cloudy periods yield factors are biased low. When current radiation is near the + // historical max for this slot, apply a floor based on the peak yield factor ever + // observed (×0.95 for degradation margin). Prevents systematic underprediction. + if (learnedSlots >= 10 && maxYfs && maxRads) { + const maxYf4 = [maxYfs[s0], maxYfs[s0+1], maxYfs[s0+2], maxYfs[s0+3]].filter(v => v > 0); + const maxRad4 = [maxRads[s0], maxRads[s0+1], maxRads[s0+2], maxRads[s0+3]].filter(v => v > 0); + if (maxYf4.length > 0 && maxRad4.length > 0) { + const avgMaxYf = maxYf4.reduce((a, b) => a + b, 0) / maxYf4.length; + const avgMaxRad = maxRad4.reduce((a, b) => a + b, 0) / maxRad4.length; + if (h.radiationWm2 >= avgMaxRad * 0.70) { + const clearSkyFloor = Math.round(h.radiationWm2 * avgMaxYf * 0.95); + if (clearSkyFloor > rawPvW) { rawPvW = clearSkyFloor; clearSkyCeilingApplied++; } + } + } + } + + // 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, precipMmh: h.precipMmh ?? 0 }; + }) + .filter(h => h.pvPowerW > 0 || pvCapacityW > 0); + if (clearSkyCeilingApplied > 0) { + this.log(`[PV clear-sky ceiling] applied to ${clearSkyCeilingApplied} slots`); + } + + // Per-model pvForecast (ECMWF/GFS/ICON/KNMI) for per-model accuracy tracking. + // Uses perModelWm2 from hourlyForecast (aligned to standardData indices, no time-map lookup). + this._pvForecastPerModel = null; + if (yfs) { + const MODELS_OM = ['meteofrance_arpege_europe', 'gfs_seamless', 'icon_seamless', 'knmi_harmonie_arome_netherlands']; + this._pvForecastPerModel = {}; + for (const m of MODELS_OM) { + const mSlots = inputs.weather.hourlyForecast + .filter(h => typeof h.radiationWm2 === 'number') + .map(h => { + const rad = h.perModelWm2?.[m]; + if (typeof rad !== 'number') return null; + // perModelWm2 is GTI when tilt/azimuth configured (via transposition in weather-forecaster), + // GHI otherwise. Either way apply yield factors directly. + const d = h.time instanceof Date ? h.time : new Date(h.time); + const s0 = d.getUTCHours() * 4; + const yf4 = [yfs[s0], yfs[s0+1], yfs[s0+2], yfs[s0+3]].filter(v => v != null && v > 0); + const yf = yf4.length > 0 ? yf4.reduce((a, b) => a + b, 0) / yf4.length : 0; + const pvW = learnedSlots >= 10 && yf > 0 + ? Math.round(rad * yf) + : Math.round(pvCapacityW * pvPR * (rad / 1000)); + return { timestamp: d.toISOString(), pvPowerW: Math.max(0, pvCapacityW > 0 ? Math.min(pvW, pvCapacityW) : pvW) }; + }) + .filter(s => s != null); + if (mSlots.length > 0) this._pvForecastPerModel[m] = mSlots; + } + const slotCounts = Object.entries(this._pvForecastPerModel).map(([m, fc]) => + `${m.replace('meteofrance_arpege_europe','mf').replace('_seamless','').replace('knmi_harmonie_arome_netherlands','knmi')}=${fc.length}`).join(' '); + this.log(`[PV perModel] slots: ${slotCounts || 'none'}`); + } + + } + + // Blend external PV forecasts into Open-Meteo pvForecast. + // Sources: Solcast (satellite, 30-min, optional) + Forecast.Solar (weather model, hourly, always-on). + // Each slot is averaged equally across available sources — more sources = more robust estimate. + // Lazy-loaded; caches in homey.settings survive app restarts. + if (Array.isArray(pvForecast) && pvForecast.length > 0) { + const blendSettings = inputs.settings; + + // ── Solcast (optional, 30-min → grouped to hourly) ────────────────────── + let solcastByHourMs = null; + if (blendSettings?.solcast_enabled && blendSettings.solcast_api_key && blendSettings.solcast_resource_id) { + if (!this._solcastProvider) { + const SolcastProvider = require('../../lib/solcast-provider'); + this._solcastProvider = new SolcastProvider(this.homey); + } + try { + const solcastForecast = await this._solcastProvider.getForecast( + blendSettings.solcast_api_key, + blendSettings.solcast_resource_id, + ); + if (Array.isArray(solcastForecast) && solcastForecast.length > 0) { + solcastByHourMs = new Map(); + for (const s of solcastForecast) { + const t = new Date(s.timestamp); + const hourMs = t.getTime() - (t.getUTCMinutes() * 60_000) - (t.getUTCSeconds() * 1_000) - t.getUTCMilliseconds(); + if (!solcastByHourMs.has(hourMs)) solcastByHourMs.set(hourMs, { p50: [], p10: [] }); + solcastByHourMs.get(hourMs).p50.push(s.pvPowerW); + solcastByHourMs.get(hourMs).p10.push(s.pvPowerW10 ?? s.pvPowerW); + } + } + } catch (err) { + this.log(`[Solcast] Error: ${err.message}`); + } + } + + // Save unblended OM forecast for per-model accuracy tracking + this._pvForecastOM = pvForecast; + + // ── Blend all available sources per hourly slot ────────────────────────── + if (solcastByHourMs) { + this._pvForecastSC = solcastByHourMs; + + const { wOM: baseWom, wSC: baseWsc } = this.learningEngine?.getPvBlendWeights?.() ?? { wOM: 0.5, wSC: 0.5 }; + + const todayNLDate = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const tomorrowNLDate = new Date(Date.now() + 86_400_000).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const byDay = { [todayNLDate]: { om: 0, sc: 0, bl: 0 }, [tomorrowNLDate]: { om: 0, sc: 0, bl: 0 } }; + + // Pre-scan per-day OM vs SC totals for divergence detection. + // When SC optimistically exceeds OM, OM's NWP ensemble captures cloud/rain faster + // (SC satellite/ML lags on approaching weather fronts). Compute separately per day so + // tomorrow's blend isn't skewed by today's weather divergence. + const dayTotals = { [todayNLDate]: { om: 0, sc: 0 }, [tomorrowNLDate]: { om: 0, sc: 0 } }; + for (const slot of pvForecast) { + const dk = new Date(slot.timestamp).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + if (!dayTotals[dk]) continue; + const scSlots = solcastByHourMs.get(new Date(slot.timestamp).getTime()); + if (scSlots?.p50?.length > 0) { + dayTotals[dk].om += slot.pvPowerW; + dayTotals[dk].sc += scSlots.p50.reduce((a, b) => a + b, 0) / scSlots.p50.length; + } + } + const getDayWeights = (dk, applyDivergence = true) => { + const t = dayTotals[dk]; + // SC satellite/ML lag on approaching weather fronts applies mainly to today's + // remaining hours. Tomorrow's SC forecast uses its ML model which is less prone + // to front-lag, so skip the divergence penalty for tomorrow entirely. + if (applyDivergence && t && t.om > 0 && t.sc > t.om) { + const div = t.sc / t.om; + // √div for gentler penalty; floor at baseWsc×0.5 so SC never drops below half its + // accuracy-derived share. + const wSC = Math.max(baseWsc * 0.5, baseWsc / Math.sqrt(div)); + return { wOM: 1.0 - wSC, wSC, div }; + } + return { wOM: baseWom, wSC: baseWsc, div: null }; + }; + const weightsToday = getDayWeights(todayNLDate, true); + const weightsTomorrow = getDayWeights(tomorrowNLDate, false); + + const scEffectiveSlots = []; // collect effective SC per slot for chart transparency + pvForecast = pvForecast.map(slot => { + const slotMs = new Date(slot.timestamp).getTime(); + const dayKey = new Date(slot.timestamp).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const { wOM, wSC } = dayKey === todayNLDate ? weightsToday : weightsTomorrow; + + const scSlots = solcastByHourMs.get(slotMs); + let blendedW; + if (scSlots?.p50?.length > 0) { + const scP50 = Math.round(scSlots.p50.reduce((a, b) => a + b, 0) / scSlots.p50.length); + const scP10 = Math.round(scSlots.p10.reduce((a, b) => a + b, 0) / scSlots.p10.length); + // OM already sees cloud cover via NWP; when SC p50 is ≥10% above OM for this slot, + // SC is likely missing the cloud → use pessimistic p10 instead. + const useP10 = scP50 > 0 && scP10 > 0 && scP50 > slot.pvPowerW * 1.10; + const scAvg = useP10 ? scP10 : scP50; + blendedW = Math.round(wOM * slot.pvPowerW + wSC * scAvg); + if (byDay[dayKey]) { byDay[dayKey].sc += scAvg; if (useP10) byDay[dayKey].p10slots = (byDay[dayKey].p10slots ?? 0) + 1; } + scEffectiveSlots.push({ ts: slot.timestamp, w: scAvg, dayKey }); + } else { + blendedW = slot.pvPowerW; + } + + if (byDay[dayKey]) { byDay[dayKey].om += slot.pvPowerW; byDay[dayKey].bl += blendedW; } + return { ...slot, pvPowerW: blendedW }; + }); + + // Aggregate effective SC to hourly for chart (shows p10 where actually used) + const scEffByDay = [{}, {}]; + const scEffBuckets = [{}, {}]; + for (const { ts, w, dayKey } of scEffectiveSlots) { + const dIdx = dayKey === todayNLDate ? 0 : dayKey === tomorrowNLDate ? 1 : -1; + if (dIdx < 0) continue; + const hour = parseInt(new Date(ts).toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + if (!scEffBuckets[dIdx][hour]) scEffBuckets[dIdx][hour] = []; + scEffBuckets[dIdx][hour].push(w); + } + for (let d = 0; d < 2; d++) { + for (const [hour, vals] of Object.entries(scEffBuckets[d])) { + scEffByDay[d][hour] = Math.round(vals.reduce((a, b) => a + b, 0) / vals.length); + } + } + this._setLive('policy_pv_forecast_sc_effective', scEffByDay); + + const fmt = wh => (wh / 1000).toFixed(1); + const pct = (v, base) => (base > 0 ? `${v >= base ? '+' : ''}${((v - base) / base * 100).toFixed(0)}%` : '—'); + const td = byDay[todayNLDate], tm = byDay[tomorrowNLDate]; + const scToday = td.sc > 0 ? ` SC=${fmt(td.sc)}kWh(${pct(td.sc, td.om)})${td.p10slots ? ` p10=${td.p10slots}slots` : ''}` : ''; + const scTomorrow = tm.sc > 0 ? ` SC=${fmt(tm.sc)}kWh(${pct(tm.sc, tm.om)})${tm.p10slots ? ` p10=${tm.p10slots}slots` : ''}` : ''; + const accOM = this.learningEngine?.data?.pv_accuracy_om; + const accSC = this.learningEngine?.data?.pv_accuracy_sc; + const accLog = accOM != null && accSC != null ? ` acc: om=${(accOM*100).toFixed(0)}% sc=${(accSC*100).toFixed(0)}%` : ' learning'; + const wTd = weightsToday, dTd = wTd.div != null ? ` div=${wTd.div.toFixed(2)}` : ''; + const wTm = weightsTomorrow, dTm = wTm.div != null ? ` div=${wTm.div.toFixed(2)}` : ''; + this.log(`[PV blend] today: OM=${fmt(td.om)}kWh${scToday} → blended=${fmt(td.bl)}kWh [w_om=${wTd.wOM.toFixed(2)} w_sc=${wTd.wSC.toFixed(2)}${dTd}${accLog}]`); + this.log(`[PV blend] tomorrow: OM=${fmt(tm.om)}kWh${scTomorrow} → blended=${fmt(tm.bl)}kWh [w_om=${wTm.wOM.toFixed(2)} w_sc=${wTm.wSC.toFixed(2)}${dTm}]`); + } else { + this._pvForecastSC = null; + } + } + + // Compute net PV surplus for next 24h using the blended forecast (post-Solcast). + // Placed after the blend so Solcast data is included in the terminal value calculation. + // Uses a rolling 24h window from now — NOT the next calendar day — to avoid + // a strategy flip at midnight when "tomorrow" jumps to the next calendar date. + if (pvForecast && inputs.weather) { + const nowMs = now.getTime(); + const windowEndMs = nowMs + 24 * 3600_000; + let pvWhTomorrowNet = 0; + for (const { timestamp, pvPowerW } of pvForecast) { + const slotMs = new Date(timestamp).getTime(); + if (slotMs <= nowMs) continue; + if (slotMs > windowEndMs) continue; + const consW = this.learningEngine + ? (this.learningEngine.getPredictedConsumption(new Date(timestamp)) ?? 0) + : 0; + pvWhTomorrowNet += Math.min(maxChargePowerW, Math.max(0, pvPowerW - consW)); + } + inputs.weather.pvKwhTomorrow = Math.round(pvWhTomorrowNet / 100) / 10; + } + + // Snapshot blended forecast before discounts (used for accuracy tracking below). + if (Array.isArray(pvForecast) && pvForecast.length > 0) { + this._pvForecastBlended = pvForecast; + this._pvForecastBlendedAt = Date.now(); + + // Snapshot today's blended forecast once at first forecast build of the day (NL timezone). + // Accuracy tracking compares against this fixed day-start snapshot, not the rolling forecast, + // to avoid degrading the score when providers revise the forecast mid-day. + const _pvSnapDate = now.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + if (this._pvDayStartForecastDate !== _pvSnapDate) { + this._pvDayStartForecast = pvForecast.map(slot => ({ ...slot })); + this._pvDayStartForecastDate = _pvSnapDate; + } + } + + // PV accuracy tracking: compare forecast for now vs actual from flow card. + // Only when flow card provides fresh inverter data (< 20 min old) and at least one + // side shows meaningful PV (> 50W) to avoid noisy night comparisons. + // Skip at negative prices: users often disable their inverter to avoid export penalty, + // so actual=0 with forecast>0 would wrongly degrade the PV accuracy score. + this._recordPvAccuracySample(now, inputs.tariff?.currentPrice ?? null); + + // Save unbiased pvForecast for chart display — bias is a planning correction, + // chart should show realistic expected production, not inflated DP assumptions. + const pvForecastChart = pvForecast ? [...pvForecast] : null; + + // Daily PV level bias: correct systematic over/under-prediction learned across days. + // Pass today's avg cloudcover so the clear-sky EMA is used on sunny days. + let _pvDailyBiasFactor = 1.0; + let _pvAccFactor = 1.0; + let _pvBiasCloud = null; + if (pvForecast && this.learningEngine) { + const hf = inputs.weather?.hourlyForecast; + const cloudVals = Array.isArray(hf) ? hf.map(h => h.cloudCover).filter(v => typeof v === 'number') : []; + const cloudLowVals = Array.isArray(hf) ? hf.map(h => h.cloudCoverLow).filter(v => typeof v === 'number') : []; + const todayAvgCloud = cloudVals.length > 0 ? cloudVals.reduce((a, b) => a + b, 0) / cloudVals.length : null; + const todayAvgCloudLow = cloudLowVals.length > 0 ? cloudLowVals.reduce((a, b) => a + b, 0) / cloudLowVals.length : null; + // Low clouds (stratus/cumulus) are more impactful than high cirrus — amplify by ×1.2 + // and use the higher of total or amplified low-cloud cover as effective cloud cover. + const effectiveCloud = (todayAvgCloud != null && todayAvgCloudLow != null) + ? Math.min(100, Math.max(todayAvgCloud, todayAvgCloudLow * 1.2)) + : todayAvgCloud; + _pvBiasCloud = effectiveCloud; + const todayKt = this.weatherForecaster?.getTodayKt() ?? null; + const dailyBias = this.learningEngine.getDailyPvBiasFactor(effectiveCloud, todayKt); + // On overcast days (>75% cloud), upward bias adds planning uncertainty: forecast errors + // inflate remaining PV estimates and cause the DP to stop grid-charging prematurely. + // Linearly reduce any upward bias back to 1.0 between 75% and 100% cloud cover. + let cappedDailyBias = dailyBias; + if (dailyBias > 1.0 && effectiveCloud != null && effectiveCloud > 75) { + const cloudFrac = Math.min(1, (effectiveCloud - 75) / 25); + cappedDailyBias = Math.max(1.0, 1.0 + (dailyBias - 1.0) * (1 - cloudFrac)); + } + _pvDailyBiasFactor = cappedDailyBias; + if (cappedDailyBias !== 1.0) { + pvForecast = pvForecast.map(s => ({ ...s, pvPowerW: Math.round(s.pvPowerW * cappedDailyBias) })); + } + const cloudLabel = todayAvgCloud != null ? `, cloud=${todayAvgCloud.toFixed(0)}%` : ''; + const capLabel = cappedDailyBias !== dailyBias ? ` (capped from ${dailyBias.toFixed(3)})` : ''; + if (cappedDailyBias !== 1.0 || dailyBias !== 1.0) { + this.log(`[PV daily bias] factor=${cappedDailyBias.toFixed(3)} applied${cloudLabel}${capLabel}`); + } + } + + // PV conservatism: if accuracy score is low, discount pvForecast to avoid over-optimistic planning. + // Discount kicks in below 0.80 accuracy, linear from 1.0 (at 0.80) to 0.80 (at 0). + if (pvForecast && this.learningEngine) { + const pvAcc = this.learningEngine.data?.pv_accuracy_score ?? 1.0; + if (pvAcc < 0.80) { + const factor = Math.max(0.80, 0.80 + 0.20 * (pvAcc / 0.80)); + _pvAccFactor = factor; + pvForecast = pvForecast.map(s => ({ ...s, pvPowerW: Math.round(s.pvPowerW * factor) })); + this.log(`[PV accuracy] score=${pvAcc.toFixed(2)} → conservatism factor=${factor.toFixed(2)} applied`); + } + } + // Cloud uncertainty discount for DP pvCoverage projection: when cloud cover is high, + // the DP should not rely on uncertain PV recharge to justify early discharge. + // Kicks in above 70% cloud, reduces pvCoverage by up to 40% at full overcast. + let _pvCloudFactor = 1.0; + if (_pvBiasCloud != null && _pvBiasCloud > 70) { + _pvCloudFactor = Math.max(0.6, 1.0 - 0.5 * Math.min(1, (_pvBiasCloud - 70) / 30)); + this.log(`[PV cloud uncertainty] cloud=${Math.round(_pvBiasCloud)}% → pvCoverageFactor=${_pvCloudFactor.toFixed(2)}`); + } + this._setLive('policy_pv_bias', { + dailyBias: _pvDailyBiasFactor, + accFactor: _pvAccFactor, + cloud: _pvBiasCloud != null ? Math.round(_pvBiasCloud) : null, + cloudFactor: _pvCloudFactor !== 1.0 ? Math.round(_pvCloudFactor * 100) / 100 : null, + net: Math.round(_pvDailyBiasFactor * _pvAccFactor * 1000) / 1000, + }); + + // Intraday PV scaling: correct today's remaining forecast from actual production. + // Uses pv_predictions already tracked by learning engine — no new API calls. + // Only scales today's future slots; tomorrow's forecast is unchanged. + if (pvForecast && this.learningEngine && Array.isArray(this.learningEngine.data?.pv_predictions)) { + const nowMs = Date.now(); + const cutoffMs = nowMs - 3 * 3600_000; + const todayNLDate = now.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const todayPreds = this.learningEngine.data.pv_predictions.filter(p => + p.timestamp >= cutoffMs && p.timestamp <= nowMs && + p.predicted > 50 && p.actual > 50 + ); + const avgActual = todayPreds.length + ? todayPreds.reduce((s, p) => s + p.actual, 0) / todayPreds.length : 0; + if (todayPreds.length >= 4 && avgActual >= 200) { + // Per-sample ratios, winsorised at [0.25, 2.5] — prevents a single spike from + // dominating the correction (sumActual/sumForecast gave high weight to outliers). + const WIN_LO = 0.25, WIN_HI = 2.5; + const sampleRatios = todayPreds.map(p => Math.min(WIN_HI, Math.max(WIN_LO, p.actual / p.predicted))); + const meanRatio = sampleRatios.reduce((s, r) => s + r, 0) / sampleRatios.length; + + // p.predicted is the pre-bias day-start forecast; pvForecast here is post-bias. + // ratio_needed = actual / post_bias = (pre_bias × meanRatio) / (pre_bias × biasCorrFactor) + // = meanRatio / biasCorrFactor + // Example: actual=3000W, pre-bias=2000W, biasCorrFactor=0.826 → + // meanRatio=1.5, post_bias=1652W, corrected=1.5/0.826=1.816 → 1652×1.816=2999W ✓ + const biasCorrFactor = _pvDailyBiasFactor * _pvAccFactor; + const correctedMeanRatio = biasCorrFactor > 0 ? meanRatio / biasCorrFactor : meanRatio; + + // Consistency check: high CV (stddev/mean) means conditions are volatile (e.g. sun spike + // followed by cloud). Blend toward 1.0 to avoid over-correcting on noisy data. + const variance = sampleRatios.reduce((s, r) => s + (r - meanRatio) ** 2, 0) / sampleRatios.length; + const cv = Math.sqrt(variance) / meanRatio; + // CV<0.25 → full correction; CV>0.60 → no correction; linear between. + const cvWeight = Math.max(0, Math.min(1, (0.60 - cv) / 0.35)); + const ratio = 1.0 + (correctedMeanRatio - 1.0) * cvWeight; + + this._lastIntradayPvRatio = ratio; + this.setCapabilityValue('bias_factor', parseFloat(ratio.toFixed(2))).catch(this.error); + if (Math.abs(ratio - 1.0) > 0.10) { + const futureSlotsCount = pvForecast.filter(s => new Date(s.timestamp) > now).length; + pvForecast = pvForecast.map(slot => { + if (new Date(slot.timestamp) <= now) return slot; + const slotDate = new Date(slot.timestamp).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + if (slotDate !== todayNLDate) return slot; + return { ...slot, pvPowerW: Math.round(slot.pvPowerW * ratio) }; + }); + this.log(`[PV intraday] ${todayPreds.length} samples, meanRatio=${meanRatio.toFixed(2)} corrected=${correctedMeanRatio.toFixed(2)} cv=${cv.toFixed(2)} cvWeight=${cvWeight.toFixed(2)} → applied ratio=${ratio.toFixed(2)} to ${futureSlotsCount} slots`); + } else { + this.log(`[PV intraday] ${todayPreds.length} samples, meanRatio=${meanRatio.toFixed(2)} corrected=${correctedMeanRatio.toFixed(2)} cv=${cv.toFixed(2)} → ratio=${ratio.toFixed(2)} within 10% threshold, no scaling`); + } + } + } + + // Buienradar rain correction: cap near-term PV slots based on precipitation radar. + // Only applies within the 2-hour Buienradar window; only reduces, never increases. + // Uses windowed average over ±7.5 min (half a 15-min slot) so adjacent rainy 5-min + // intervals all contribute rather than a single nearest-neighbour point. + if (pvForecast && Array.isArray(this.buienradarData) && this.buienradarData.length > 0) { + const buienradarEndMs = this.buienradarData[this.buienradarData.length - 1].time.getTime(); + const halfSlotMs = 7.5 * 60 * 1000; + let correctedCount = 0; + pvForecast = pvForecast.map(slot => { + const slotMs = new Date(slot.timestamp).getTime(); + if (slotMs > buienradarEndMs) return slot; + const inWindow = this.buienradarData.filter(b => + Math.abs(b.time.getTime() - slotMs) <= halfSlotMs + ); + const samples = inWindow.length > 0 ? inWindow : [ + this.buienradarData.reduce((a, b) => + Math.abs(b.time.getTime() - slotMs) < Math.abs(a.time.getTime() - slotMs) ? b : a + ), + ]; + const avgFactor = samples.reduce((s, b) => s + b.factor, 0) / samples.length; + if (avgFactor >= 1.0) return slot; + correctedCount++; + return { ...slot, pvPowerW: Math.round(slot.pvPowerW * avgFactor) }; + }); + if (correctedCount > 0) { + this.log(`🌧️ Buienradar: PV correctie op ${correctedCount} slots`); + } + } + + // OM precipitation: extend rain correction beyond the 2h Buienradar window. + // OM irradiance already accounts for most cloud effects; _omPrecipFactor adds a + // conservative residual correction for slots with active precipitation. + if (Array.isArray(pvForecast) && pvForecast.length > 0) { + const buienradarEndMs = (Array.isArray(this.buienradarData) && this.buienradarData.length > 0) + ? this.buienradarData[this.buienradarData.length - 1].time.getTime() + : 0; + let omCorrCount = 0; + pvForecast = pvForecast.map(slot => { + const slotMs = new Date(slot.timestamp).getTime(); + if (slotMs <= buienradarEndMs) return slot; + const factor = WeatherForecaster._omPrecipFactor(slot.precipMmh ?? 0); + if (factor >= 1.0) return slot; + omCorrCount++; + return { ...slot, pvPowerW: Math.round(slot.pvPowerW * factor) }; + }); + if (omCorrCount > 0) { + this.log(`🌧️ OM precipitatie: PV correctie op ${omCorrCount} slots`); + } + } + + // Sync chart orange line with the fully-corrected DP forecast (bias+conservatism+coverage applied) + if (Array.isArray(pvForecast) && pvForecast.length > 0) { + const _todayNL = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const _tomorrowNL = new Date(Date.now() + 86_400_000).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const _dpBuckets = [{ key: _todayNL, h: {} }, { key: _tomorrowNL, h: {} }]; + for (const slot of pvForecast) { + const dk = new Date(slot.timestamp).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const bucket = _dpBuckets.find(b => b.key === dk); + if (!bucket) continue; + const hr = parseInt(new Date(slot.timestamp).toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + if (!bucket.h[hr]) bucket.h[hr] = []; + bucket.h[hr].push(slot.pvPowerW); + } + const _dpHourly = _dpBuckets.map(b => { + const result = {}; + for (const [hr, vals] of Object.entries(b.h)) { + result[parseInt(hr)] = Math.round(vals.reduce((a, v) => a + v, 0) / vals.length); + } + return result; + }); + this._setLive('policy_pv_forecast_hourly', _dpHourly); + } + + // 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; + // Prefer explicit Amsterdam hour/minute fields from the price record (both KwhPrice and + // Xadi provide these via toLocaleString / item.hour). This avoids any UTC/local timestamp + // parsing ambiguity: if a provider returns Amsterdam-local times without a UTC indicator, + // _getAmsterdamTime() would shift the hour by +2 (CEST), causing all consumption lookups + // to land on the wrong slot and return baseload-floor (~314 W) everywhere. + const hasSlotFields = typeof prices[0]?.hour === 'number' && typeof prices[0]?.minute === 'number'; + if (this.learningEngine) { + const rawLearned = []; + let nonZeroCount = 0; + for (let h = 0; h < prices.length; h++) { + const futureTime = new Date(prices[h].timestamp); + const learned = hasSlotFields + ? (this.learningEngine.getPredictedConsumptionForSlot(futureTime, prices[h].hour, prices[h].minute) ?? 0) + : (this.learningEngine.getPredictedConsumption(futureTime) ?? 0); + if (learned > 0) nonZeroCount++; + rawLearned.push(learned); + } + // Only pass consumption data when the learning engine has meaningful data. + // When nothing is learned yet (all slots return 0), the baseload floor (~300W + // standby power) would make the optimizer think discharge is always baseload-limited — + // causing it to plan too little PV charging. With null the optimizer uses + // maxDischargePowerW (unconstrained 800W), which is the correct conservative assumption. + if (nonZeroCount > 0) { + // Floor by baseload: a learned value below the measured baseload is an + // artefact of averaging quiet evenings — the house always consumes at least + // baseload, so the optimizer should never plan slower discharge than that. + consumptionWPerSlot = rawLearned.map(v => Math.max(v, baseloadW)); + } + this.log(`🔮 Consumption: ${nonZeroCount}/${prices.length} learned slots (hasSlotFields=${hasSlotFields}), sample=[${rawLearned.slice(0,3).map(v=>Math.round(v)).join(',')},...], nonZero=${nonZeroCount}`); + } + + 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; + let minDischargePrice = respectMinMax + ? (inputs.settings?.min_discharge_price ?? 0) + : (inputs.settings?.cycle_cost_per_kwh ?? 0.075) / (inputs.settings?.battery_efficiency || 0.75); + + // PV headroom override: when tomorrow's PV will fill the battery regardless of tonight's SoC, + // lower the discharge floor to the actual break-even so the DP can create headroom overnight. + // Break-even = cycleCostPerKwh / effectiveRte (typically ~€0.10). + // Only active when PV tomorrow ≥ 90% of battery capacity (near-certain full recharge). + // Smooth pvKwhTomorrow over last 3 values to prevent a single weather-API update + // from toggling the DP flattening strategy. Uses rolling minimum (conservative): + // the DP only switches to "discharge freely" when PV availability is consistently high. + const pvKwhTomorrowRaw = inputs.weather?.pvKwhTomorrow ?? 0; + if (!this._pvKwhTomorrowHistory) this._pvKwhTomorrowHistory = []; + if (pvKwhTomorrowRaw > 0) { + this._pvKwhTomorrowHistory.push(pvKwhTomorrowRaw); + if (this._pvKwhTomorrowHistory.length > 3) this._pvKwhTomorrowHistory.shift(); + } + const pvKwhTomorrow = this._pvKwhTomorrowHistory.length > 0 + ? Math.min(...this._pvKwhTomorrowHistory) + : pvKwhTomorrowRaw; + if (pvKwhTomorrowRaw > 0 && pvKwhTomorrow !== pvKwhTomorrowRaw) { + this.log(`☀️ pvKwhTomorrow smoothed: raw=${pvKwhTomorrowRaw}kWh → min3=${pvKwhTomorrow}kWh (history=[${this._pvKwhTomorrowHistory.map(v=>v.toFixed(2)).join(',')}])`); + } + if (this.learningEngine && pvKwhTomorrow > 0) { + const tomorrowStr = new Date(Date.now() + 24 * 3600_000).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + this.learningEngine.savePvNetSurplusPrediction(tomorrowStr, pvKwhTomorrow); + } + if (pvKwhTomorrow >= capacityKwh * 0.9 && minDischargePrice > 0 && pvForecast) { + const effectiveRte = learnedRte ?? 0.75; + const cycleCostKwh = this.optimizationEngine.cycleCostPerKwh ?? 0.075; + const actualBreakEven = Math.round(cycleCostKwh / effectiveRte * 1000) / 1000; + // Night floor: when PV tomorrow comfortably exceeds capacity (≥150%), any overnight + // discharge avoids clipping free PV tomorrow → floor = €0.00 (any positive price is profit). + // When PV is 90-150% of capacity, use break-even + small margin as floor. + // Day floor (pvW >= pvStrongW, zero_charge_only mode): PV opportunity cost applies — + // discharging means PV recharges at the same price → round-trip loss → use min_discharge_price. + // Weak PV floor (50W ≤ pvW < pvStrongW, standby mode): battery is NOT in zero_charge_only, + // no real opportunity cost — use break-even + small margin so the DP can still discharge + // at profitable transition-hour prices (e.g. 17:30 at €0.21 with 300W PV). + const pvRatio = pvKwhTomorrow / capacityKwh; + const nightFloor = pvRatio >= 1.5 ? 0.00 : Math.max(actualBreakEven + 0.015, 0.115); + const dayFloor = inputs.settings?.min_discharge_price || 0.22; + const weakPvFloor = Math.max(actualBreakEven + 0.02, 0.115); + const pvStrongW = (inputs.battery?.maxChargePowerW || 800) * 0.5; // mirrors pvStrongCoverage=400/800 + const maxSoc = inputs.settings?.max_soc ?? 95; + // When battery is at max_soc, PV cannot add more energy — the opportunity-cost + // reasoning behind dayFloor (PV recharges for free) does not apply. + const effectiveDayFloor = soc >= maxSoc ? weakPvFloor : dayFloor; + const perSlotFloors = prices.map(p => { + const pvW = this.optimizationEngine._getPvForSlot(pvForecast, p.timestamp); + if (pvW >= pvStrongW) return effectiveDayFloor; + if (pvW >= 50) return weakPvFloor; + return nightFloor; + }); + this.log(`☀️ PV headroom: pvTomorrow=${pvKwhTomorrow}kWh ≥ ${(capacityKwh * 0.9).toFixed(1)}kWh → night floor €${nightFloor}, weak-PV floor €${weakPvFloor}, day floor €${dayFloor}${soc >= maxSoc ? ` (→ €${weakPvFloor} omdat SoC ${soc}%=max, geen PV-ruimte)` : ''} (pvStrong≥${pvStrongW}W, break-even €${actualBreakEven})`); + minDischargePrice = perSlotFloors; + } + // Negative tariff headroom: when strongly negative prices are coming (< -€0.10), + // lower the discharge floor to €0.00 for all slots before the first negative window. + // This lets the DP discharge at any positive price — using house load to drain the + // battery passively and grid export for the remainder — maximising room for paid charging. + // Applied after PV headroom so it can override the per-slot floor array if needed. + const minFuturePrice = prices.length ? Math.min(...prices.map(p => p.price)) : 0; + if (minFuturePrice < -0.10) { + const firstNegIdx = prices.findIndex(p => p.price < -0.10); + if (firstNegIdx > 0) { + const preNegFloors = prices.map((_, t) => t < firstNegIdx ? 0.00 + : (Array.isArray(minDischargePrice) ? minDischargePrice[t] : minDischargePrice)); + this.log(`⚡ Negatief tarief headroom: min €${minFuturePrice.toFixed(3)} @ slot ${firstNegIdx} → discharge floor €0.00 voor slot 0–${firstNegIdx - 1}`); + minDischargePrice = preNegFloors; + } + } + + // Per-slot consumption margin: scales with each slot's coefficient of variation. + // Stable slots (CV~0, e.g. night) → tight margin 1.10. + // Volatile slots (CV~1, e.g. cooking peak) → wide margin 1.35. + // Falls back to uniform 1.20 when learning engine has no variance data yet. + let consumptionMargin = 1.20; + if (this.learningEngine && consumptionWPerSlot) { + const perSlotMargins = []; + let cvSum = 0, cvCount = 0; + for (let h = 0; h < prices.length; h++) { + const cv = hasSlotFields + ? this.learningEngine.getConsumptionCVForSlot(new Date(prices[h].timestamp), prices[h].hour, prices[h].minute) + : this.learningEngine.getConsumptionCV(new Date(prices[h].timestamp)); + if (cv !== null) { + perSlotMargins.push(Math.min(1.35, 1.10 + cv * 0.25)); + cvSum += cv; cvCount++; + } else { + perSlotMargins.push(1.20); // default for slots without enough data + } + } + if (cvCount > 0) { + const avgCV = cvSum / cvCount; + consumptionMargin = perSlotMargins; // pass per-slot array to optimizer + this.log(`📐 consumptionMargin=per-slot (avgCV=${avgCV.toFixed(2)}, ${cvCount}/${prices.length} slots with CV data)`); + } + } + + // When battery was charged at zero or negative cost (avgCost ≤ 0) AND pvKwhTomorrow + // is stale/missing (=0), assume PV will refill so terminal value doesn't block discharge. + // Only applies when pvKwhTomorrow is actually 0 — a real forecast must be respected. + const _avgCost = inputs.batteryCost?.avgCost ?? null; + const _costActive = _avgCost !== null && (inputs.batteryCost?.energyKwh ?? 0) >= 0.5; + const _negativeCharge = _costActive && _avgCost <= 0; + const effectivePvKwhTomorrow = (_negativeCharge && pvKwhTomorrow === 0) + ? capacityKwh * 2 + : pvKwhTomorrow; + + const _netFactor = this.learningEngine?.getPvNetSurplusAccuracyFactor() ?? 1.0; + const adjustedPvKwhTomorrow = effectivePvKwhTomorrow * _netFactor; + + if (adjustedPvKwhTomorrow > 0) { + const pvRefill = Math.min(1, adjustedPvKwhTomorrow / capacityKwh); + const terminalFactor = pvRefill >= 0.8 ? 0 : Math.max(0, 1 - pvRefill / 0.8); + const negNote = _negativeCharge ? ` (→ ${effectivePvKwhTomorrow.toFixed(1)}kWh: negatief geladen €${_avgCost.toFixed(3)}, terminal=0)` : ''; + const factorNote = _netFactor !== 1.0 ? ` [netFactor=${_netFactor.toFixed(2)}→adj=${adjustedPvKwhTomorrow.toFixed(1)}kWh]` : ''; + this.log(`☀️ Terminal value: pvTomorrow=${pvKwhTomorrow}kWh${factorNote}, capacity=${capacityKwh}kWh, pvRefill=${(pvRefill*100).toFixed(0)}% → factor=${terminalFactor.toFixed(2)}${pvRefill >= 0.8 ? ' (ZERO — PV refills battery)' : ''}${negNote}`); + } + this.optimizationEngine.compute(prices, soc, capacityKwh, maxChargePowerW, maxDischargePowerW, pvForecast, learnedRte, consumptionWPerSlot, minDischargePrice, consumptionMargin, effectivePvKwhTomorrow, adjustedPvKwhTomorrow, _pvCloudFactor); + + // Compact planning summary — always visible in user diagnostics. + { + const _slots = this.optimizationEngine._schedule?.slots ?? []; + const _cnt = { charge: 0, discharge: 0, preserve: 0, standby: 0, trickle: 0 }; + let _socMin = 100, _socMax = 0; + for (const s of _slots) { + if (_cnt[s.action] !== undefined) _cnt[s.action]++; + if (s.socProjected != null) { _socMin = Math.min(_socMin, s.socProjected); _socMax = Math.max(_socMax, s.socProjected); } + } + const _profit = this.optimizationEngine._schedule?.todayProjectedProfit ?? this.optimizationEngine._schedule?.projectedProfit ?? 0; + this.log(`📋 Plan: ${_slots.length} slots | charge=${_cnt.charge} discharge=${_cnt.discharge} preserve=${_cnt.preserve} standby=${_cnt.standby} trickle=${_cnt.trickle} | SoC ${soc}%→min${_socMin}%→max${_socMax}% | profit €${_profit.toFixed(3)}`); + this.setCapabilityValue('policy_profit_eur', parseFloat(_profit.toFixed(2))).catch(this.error); + this.setCapabilityValue('plan_summary', `${_cnt.charge}↑ ${_cnt.discharge}↓ ${_cnt.preserve}=`).catch(this.error); + } + + // Persist planning schedule for the settings UI (single source of truth). + // Frontend reads 'policy_optimizer_schedule' and renders it directly — no re-simulation. + // _recomputeOptimizer runs before policyEngine.evaluate(), so dynamicMaxChargePrice is not + // yet set on inputs — compute it here so the planning mapper uses the effective price. + if (!inputs.dynamicMaxChargePrice) { + inputs.dynamicMaxChargePrice = this.policyEngine._getDynamicChargePrice(inputs.tariff, inputs.tariff?.currentPrice); + } + const slots = this.optimizationEngine._schedule?.slots; + if (slots?.length > 0) { + const planningSchedule = this.policyEngine.buildPlanningSchedule( + slots, + pvForecast ?? null, + minDischargePrice, + maxChargePowerW, + inputs.dynamicMaxChargePrice ?? null, + inputs.battery?.totalCapacityKwh ?? null + ); + // Enrich with consumption sample count for confidence display in the UI + if (this.learningEngine) { + for (const slot of planningSchedule) { + slot.sampleCount = this.learningEngine.getConsumptionSampleCount(new Date(slot.timestamp)); + } + } + this._setLive('policy_optimizer_schedule', planningSchedule); + + // ── PV surplus forecast ──────────────────────────────────────────────── + // Use mapped hwModes (not raw DP actions) so mapper overrides like standby→to_full + // are counted correctly. Filter: hwModes that actually charge the battery from PV. + if (pvForecast && consumptionWPerSlot) { + const _psSlotH = planningSchedule.length > 1 + ? (new Date(planningSchedule[1].timestamp).getTime() - new Date(planningSchedule[0].timestamp).getTime()) / 3_600_000 + : slotMs / 3_600_000; + let netPvKwh = 0; + for (const ps of planningSchedule) { + const hwm = ps.hwMode; + if (hwm !== 'zero_charge_only' && hwm !== 'pv_trickle' && hwm !== 'to_full') continue; + netPvKwh += Math.min(maxChargePowerW, Math.max(0, (ps.pvW ?? 0) - (ps.consumptionW ?? 0))) * _psSlotH / 1000; + } + const remainingKwh = Math.max(0, (1 - soc / 100) * capacityKwh); + const rteForSurplus = learnedRte ?? 0.75; + const fillFraction = remainingKwh > 0.1 + ? Math.min(1, (netPvKwh * rteForSurplus) / remainingKwh) + : 1; + this.log(`☀️ PV-surplus: ${netPvKwh.toFixed(2)} kWh netto → ${(fillFraction * 100).toFixed(0)}% van restcapaciteit (${remainingKwh.toFixed(1)} kWh, SoC ${soc}%)`); + const chargeSlots = planningSchedule.filter(s => s.hwMode === 'to_full').length; + this._setLive('pv_surplus_forecast', { + netPvKwh: Math.round(netPvKwh * 100) / 100, + remainingKwh: Math.round(remainingKwh * 100) / 100, + fillFraction: Math.round(fillFraction * 100) / 100, + soc, + chargeSlots, + updatedAt: new Date().toISOString(), + }); + } + + // Sync PV chart from schedule slots — average of 4 × 15-min pvW per hour, + // identical source and aggregation as consumption (avgConsumptionW in the UI). + { + const _fcNow = new Date(); + const _fcToday = _fcNow.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const _fcTomorrow = new Date(_fcNow.getTime() + 86_400_000).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const _existing = this._liveState.policy_pv_forecast_hourly + ?? this.homey.settings.get('policy_pv_forecast_hourly') + ?? [{}, {}]; + const _nowHourNL = parseInt(_fcNow.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + const pvFcByDay = [ + Object.fromEntries(Object.entries(_existing[0] ?? {}).filter(([h]) => parseInt(h) >= _nowHourNL)), + { ..._existing[1] ?? {} }, + ]; + const pvSumByDayHour = [{}, {}]; + const pvCntByDayHour = [{}, {}]; + for (const fc of (pvForecastChart ?? [])) { + const st = new Date(fc.timestamp); + const sDate = st.toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const sIdx = sDate === _fcToday ? 0 : sDate === _fcTomorrow ? 1 : -1; + if (sIdx < 0) continue; + const sHour = parseInt(st.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + pvSumByDayHour[sIdx][sHour] = (pvSumByDayHour[sIdx][sHour] ?? 0) + fc.pvPowerW; + pvCntByDayHour[sIdx][sHour] = (pvCntByDayHour[sIdx][sHour] ?? 0) + 1; + } + for (let d = 0; d < 2; d++) { + for (const h of Object.keys(pvSumByDayHour[d])) { + pvFcByDay[d][h] = Math.round(pvSumByDayHour[d][h] / pvCntByDayHour[d][h]); + } + } + const _chartTodayKwh = Object.values(pvFcByDay[0]).reduce((s, w) => s + (w || 0), 0) / 1000; + const _chartTomKwh = Object.values(pvFcByDay[1]).reduce((s, w) => s + (w || 0), 0) / 1000; + this.log(`[PV chart] unbiased forecast stored: vandaag ${_chartTodayKwh.toFixed(1)} kWh (future only), morgen ${_chartTomKwh.toFixed(1)} kWh`); + this._setLive('policy_pv_forecast_hourly', pvFcByDay); + } + + // ── SoC plan snapshot: first planned SoC per slot, never overwritten ── + // Allows the frontend to show "planned XX%" alongside actual SoC for past slots. + try { + const nowTs = Date.now(); + let socPlan = this._liveState.policy_soc_plan || this.homey.settings.get('policy_soc_plan') || {}; + // Prune entries older than 48h + for (const ts of Object.keys(socPlan)) { + if (nowTs - new Date(ts).getTime() > 48 * 3600 * 1000) delete socPlan[ts]; + } + // Add new timestamps only — never overwrite (first plan wins for past slots) + let changed = false; + for (const slot of planningSchedule) { + if (slot.socProjected != null && !(slot.timestamp in socPlan)) { + socPlan[slot.timestamp] = { soc: slot.socProjected, consumptionW: slot.consumptionW ?? null }; + changed = true; + } + } + if (changed) this._setLive('policy_soc_plan', socPlan); + } catch (e) { /* non-critical */ } + + // ── Battery expansion analysis (non-critical) ────────────────────────── + // Runs DP for 1–4 battery scenarios to show the marginal value of each + // additional unit. _schedule is NOT touched by computeExpectedProfit(). + // Skip if heap is already under pressure (4× DP runs = significant allocation). + let heapUsedMB = 50; // conservative default: skip expansion if heap unreadable + try { heapUsedMB = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (heapUsedMB > 45) { + this.log(`[MEM] Skipping expansion analysis — heap ${heapUsedMB.toFixed(1)} MB > 45 MB guard`); + } + if (heapUsedMB <= 45) try { + const KWH_PER_UNIT = 2.688; // HomeWizard Energy Battery per unit + const W_PER_UNIT = 800; + + const dischargeSlots = this.optimizationEngine._schedule?.slots ?? []; + const slotH = prices.length >= 2 + ? (new Date(prices[1].timestamp) - new Date(prices[0].timestamp)) / 3_600_000 + : 1; + + // Cache key: prices only change on the hour, soc rounded to 5% to avoid + // rerunning 4× DP on every minor SoC fluctuation between policy checks. + const expansionKey = `${prices.length}_${prices[0]?.timestamp}_${prices.at(-1)?.timestamp}` + + `_${Math.round(soc / 5) * 5}_${Math.round(pvKwhTomorrow * 10)}`; + + let scenarios; + if (this._expansionCache?.key === expansionKey) { + scenarios = this._expansionCache.scenarios; + } else { + scenarios = []; + for (let n = 1; n <= 4; n++) { + const kwh = +(n * KWH_PER_UNIT).toFixed(3); + const powerW = n * W_PER_UNIT; + + const result = this.optimizationEngine.computeExpectedProfit( + prices, soc, kwh, powerW, powerW, + pvForecast, learnedRte, consumptionWPerSlot, minDischargePrice, consumptionMargin, pvKwhTomorrow + ); + const profit = +result.profit.toFixed(4); + const selfSufficiencyPct = result.selfSufficiencyPct; + + // Power bottleneck: slots where house consumption exceeds battery discharge power, + // so the battery is at full output but grid still imports the remainder. + // Uses the current schedule's discharge slots as a proxy (good enough for diagnostics). + let shortfallSlots = 0; + let shortfallKwh = 0; + if (Array.isArray(consumptionWPerSlot)) { + for (let t = 0; t < dischargeSlots.length; t++) { + if (dischargeSlots[t]?.action !== 'discharge') continue; + const consumption = consumptionWPerSlot[t]; + if (consumption == null) continue; + const shortfall = Math.max(0, consumption - powerW); + if (shortfall > 0) { + shortfallSlots++; + shortfallKwh += shortfall * slotH / 1000; + } + } + } + + scenarios.push({ units: n, kwh, powerW, profit, selfSufficiencyPct, + shortfallSlots, shortfallKwh: +shortfallKwh.toFixed(3) }); + } + this._expansionCache = { key: expansionKey, scenarios }; + } + + // Rolling daily profit history — one entry per day, max 30 days. + // Used by the frontend to compute a seasonally-averaged payback period. + // Also stores actual self-sufficiency, updated on every policy run. + const today = new Date().toLocaleString('en-CA', { timeZone: 'Europe/Amsterdam' }).slice(0, 10); + let hist = this.homey.settings.get('expansion_profit_history') || { entries: [] }; + if (!Array.isArray(hist.entries)) hist.entries = []; + const lastEntry = hist.entries[hist.entries.length - 1]; + let todayEntry; + if (!lastEntry || lastEntry.date !== today) { + todayEntry = { date: today }; + hist.entries.push(todayEntry); + if (hist.entries.length > 30) hist.entries = hist.entries.slice(-30); + } else { + todayEntry = lastEntry; + } + for (const s of scenarios) todayEntry[`p${s.units}`] = s.profit; + const todayActualSelfSufficiencyPct = this._todayConsumptionKwh > 0.01 + ? Math.max(0, Math.min(100, Math.round((1 - this._todayGridImportKwh / this._todayConsumptionKwh) * 100))) + : null; + if (todayActualSelfSufficiencyPct !== null) todayEntry.actualSelfSufficiencyPct = todayActualSelfSufficiencyPct; + + this._queueSettingsPersist('expansion_profit_history', hist); + + // Augment scenarios with avgProfit + seasonally-corrected avgProfit. + // Seasonal correction: normalize each day's profit by its monthly irradiance + // factor (NL average, PVGIS kWh/m²/day) so that summer days don't inflate + // the annual estimate. Result is a year-round daily average. + const NL_IRRADIANCE = [0.62, 1.16, 2.28, 3.62, 4.71, 5.02, 4.92, 4.23, 2.84, 1.56, 0.67, 0.44]; + const NL_ANNUAL_AVG = NL_IRRADIANCE.reduce((a, b) => a + b, 0) / 12; // ~2.67 + + const historyDays = hist.entries.length; + for (const s of scenarios) { + const vals = hist.entries.map(e => e[`p${s.units}`]).filter(v => v != null); + s.avgProfit = vals.length ? +(vals.reduce((a, b) => a + b, 0) / vals.length).toFixed(4) : null; + + // Seasonal correction: weight each day's profit by (annual_avg / month_irradiance) + // so high-irradiance months are scaled down to the annual baseline. + if (hist.entries.length >= 3) { + let weightedSum = 0, weightSum = 0; + for (const e of hist.entries) { + const profit = e[`p${s.units}`]; + if (profit == null) continue; + const month = new Date(e.date).getMonth(); // 0-based + const irr = NL_IRRADIANCE[month] || NL_ANNUAL_AVG; + const weight = NL_ANNUAL_AVG / irr; // <1 in summer, >1 in winter + weightedSum += profit * weight; + weightSum += weight; + } + s.seasonalAvgProfit = weightSum > 0 ? +(weightedSum / weightSum).toFixed(4) : null; + } else { + s.seasonalAvgProfit = null; // too few days for meaningful correction + } + } + + const currentUnits = Math.max(1, Math.min(4, Math.round(capacityKwh / KWH_PER_UNIT))); + + this._setLive('battery_expansion_analysis', { + timestamp: new Date().toISOString(), + currentUnits, + currentKwh: capacityKwh, + currentPowerW: maxChargePowerW, + historyDays, + scenarios, + todayActualSelfSufficiencyPct, + todayGridImportKwh: +this._todayGridImportKwh.toFixed(3), + todayConsumptionKwh: +this._todayConsumptionKwh.toFixed(3), + }); + } catch (e) { + this.error('Battery expansion analysis failed (non-critical):', e); + } + + // ── Consumption profile (for settings chart) ─────────────────────────── + // Written at most once per hour — data changes slowly, no need to + // serialize 672 integers to settings on every 15-min optimizer run. + try { + const lastProfile = this.homey.settings.get('policy_consumption_profile'); + const ageMs = lastProfile?.timestamp ? Date.now() - new Date(lastProfile.timestamp).getTime() : Infinity; + if (ageMs > 60 * 60 * 1000) { + const days = {}; + for (let d = 0; d < 7; d++) { + days[d] = this.learningEngine.getDailyProfile(d).map(s => s.avgW); + } + this._queueSettingsPersist('policy_consumption_profile', { + timestamp: new Date().toISOString(), + days, + }); + } + } catch (e) { + this.error('Consumption profile save failed (non-critical):', e); + } + } + + } + + /** + * 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(); + + // Check favorable window on PV update so the trigger fires immediately + // when production crosses the threshold, without waiting for next policy cycle. + if (this._lastTariffInfo) { + this._checkFavorableWindow(this._lastTariffInfo); + } + + // 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); + } + + // Record PV forecast accuracy from live PV updates too, not only during policy runs. + // Samples are deduplicated per 15-minute bucket. + this._recordPvAccuracySample(new Date(), this._lastTariffInfo?.currentPrice ?? null); + + // 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 { + if (this._pvActualHourly && this.learningEngine) { + const yDate = new Date(Date.now() - 24 * 3600_000); + let actualNetKwh = 0; + for (let h = 0; h < 24; h++) { + const pvW = this._pvActualHourly.hourly[h] ?? 0; + if (pvW <= 0) continue; + const slotDate = new Date(yDate); + slotDate.setHours(h, 0, 0, 0); + const consW = this.learningEngine.getPredictedConsumption(slotDate) ?? 0; + actualNetKwh += Math.max(0, pvW - consW) / 1000; + } + this.learningEngine.settlePvNetSurplusAccuracy(this._pvActualHourly.date, actualNetKwh); + } + 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._queueSettingsPersist('policy_pv_actual_today', { + date: this._pvActualHourly.date, + hourly: this._pvActualHourly.hourly, + sums: this._pvActualHourly.sums, + counts: this._pvActualHourly.counts, + }); + + // Refresh PV camera image when policy is disabled (otherwise _updatePlanningChart handles it). + // Throttled: at most once per 15 minutes to avoid flooding quickchart.io. + if (!this.getCapabilityValue('policy_enabled') && this.planningImagePv) { + const now = Date.now(); + if (!this._pvCameraLastUpdate || now - this._pvCameraLastUpdate > 15 * 60 * 1000) { + this._pvCameraLastUpdate = now; + this.planningImagePv.update().catch(() => {}); + } + } + } + + /** + * 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; + } + + _recordPvAccuracySample(now = new Date(), currentPrice = null) { + if (!this.learningEngine || this._pvProductionW == null || !this._pvProductionTimestamp) return; + if (!this._pvDayStartForecast || !Array.isArray(this._pvDayStartForecast) || this._pvDayStartForecast.length === 0) return; + if (currentPrice != null && currentPrice < 0) return; + + const ageMs = Date.now() - this._pvProductionTimestamp; + if (ageMs >= 20 * 60 * 1000) return; + + const nowMs = now.getTime(); + const pvDayIdx = this.optimizationEngine._buildPvIndex(this._pvDayStartForecast); + const predictedW = this.optimizationEngine._getPvForSlot(pvDayIdx, nowMs) || 0; + const actualW = this._pvProductionW; + if (predictedW <= 50 || actualW <= 100) return; + + const bucketMs = Math.floor(nowMs / (15 * 60 * 1000)) * (15 * 60 * 1000); + if (this._lastPvAccuracyBucket === bucketMs) return; + this._lastPvAccuracyBucket = bucketMs; + + const omIdx = this._pvForecastOM ? this.optimizationEngine._buildPvIndex(this._pvForecastOM) : null; + const omW = omIdx ? this.optimizationEngine._getPvForSlot(omIdx, nowMs) : null; + const hourMs = now.getTime() + - (now.getUTCMinutes() * 60_000) + - (now.getUTCSeconds() * 1_000) + - now.getUTCMilliseconds(); + const scSlots = this._pvForecastSC?.get(hourMs); + const scW = scSlots?.p50?.length > 0 + ? Math.round(scSlots.p50.reduce((a, b) => a + b, 0) / scSlots.p50.length) + : null; + + const perModelW = {}; + if (this._pvForecastPerModel) { + for (const [m, fc] of Object.entries(this._pvForecastPerModel)) { + const mIdx = this.optimizationEngine._buildPvIndex(fc); + const mW = mIdx ? this.optimizationEngine._getPvForSlot(mIdx, nowMs) : null; + if (mW != null) perModelW[m] = mW; + } + } + this.learningEngine.recordPvAccuracy(predictedW, actualW, omW, scW, perModelW).catch(e => + this.error('PV accuracy recording failed:', e) + ); + } + + /** + * 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) { + // When flow card reports 0W, reset the EMA so the fallback estimator + // doesn't keep returning stale high values after sunset. + if (this._pvProductionW === 0 && this._lastPvEstimateW > 0) { + this._lastPvEstimateW = 0; + } + if (settings.enable_logging && this._pvProductionW !== this._lastLoggedPvW) { + this._lastLoggedPvW = this._pvProductionW; + 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); + } + + _isEvCharging() { + return this._evChargingUntil > Date.now(); + } + + _scheduleEvAutoClear() { + if (this._evChargingTimer) { + this.homey.clearTimeout(this._evChargingTimer); + this._evChargingTimer = null; + } + const delay = this._evChargingUntil - Date.now(); + if (delay <= 0) return; + this._evChargingTimer = this.homey.setTimeout(() => { + this._evChargingTimer = null; + if (this._isEvCharging()) return; + this.log('[EV] Auto-clear: 8h timeout reached, discharge gate released'); + this._evChargingUntil = 0; + try { this.homey.settings.set('ev_charging_until', 0); } catch (_) {} + this._runPolicyCheck().catch(err => this.error('[EV] auto-clear policy run failed:', err)); + }, delay); + } + + async _setEvCharging(active) { + const EV_TTL_MS = 8 * 60 * 60 * 1000; + if (active) { + this._evChargingUntil = Date.now() + EV_TTL_MS; + this.log(`[EV] Charging session started — discharge blocked until ${new Date(this._evChargingUntil).toISOString()}`); + } else { + this._evChargingUntil = 0; + this.log('[EV] Charging session stopped — discharge gate released'); + } + try { this.homey.settings.set('ev_charging_until', this._evChargingUntil); } catch (_) {} + this._scheduleEvAutoClear(); + this._runPolicyCheck().catch(err => this.error('[EV] policy run after toggle failed:', err)); + } + + 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); + } + } + if (this.p1Device?._updateBatteryGroup) { + await this.p1Device._updateBatteryGroup().catch(e => this.error('Battery group refresh failed:', e.message)); + } + const batteryState = await this._getBatteryState(); + const tariffInfo = this.tariffManager.getCurrentTariff(batteryState.gridPower); + + const debugPrice = tariffInfo?.currentPrice != null ? tariffInfo.currentPrice.toFixed(3) : '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._setLive('policy_debug_top3low', debugTopLowText); + this._setLive('policy_debug_top3high', debugTopHighText); + + // Push structured learning stats for the UI status block + const rte = this.efficiencyEstimator.getEfficiency(); + const rteInsightsObj = this.efficiencyEstimator.getEfficiencyInsights(); + const { wOM, wSC } = this.learningEngine?.getPvBlendWeights?.() ?? { wOM: 0.5, wSC: 0.5 }; + const accOM = this.learningEngine?.data?.pv_accuracy_om ?? null; + const accSC = this.learningEngine?.data?.pv_accuracy_sc ?? null; + const pvPredictions = this.learningEngine?.data?.pv_predictions?.slice(-864) ?? []; + const _mAcc = this.learningEngine?.data?.pv_model_accuracy ?? {}; + const modelAcc = { + mf: _mAcc['meteofrance_arpege_europe'] != null ? +(_mAcc['meteofrance_arpege_europe'] * 100).toFixed(1) : null, + gfs: _mAcc['gfs_seamless'] != null ? +(_mAcc['gfs_seamless'] * 100).toFixed(1) : null, + icon: _mAcc['icon_seamless'] != null ? +(_mAcc['icon_seamless'] * 100).toFixed(1) : null, + knmi: _mAcc['knmi_harmonie_arome_netherlands'] != null ? +(_mAcc['knmi_harmonie_arome_netherlands'] * 100).toFixed(1) : null, + }; + this._setLive('learning_status', { + days: learningStats.days_tracking, + samples: learningStats.total_samples, + coverage: learningStats.pattern_coverage, + pvAccuracy: learningStats.pv_accuracy, + rte: rte != null ? +(rte * 100).toFixed(1) : null, + cycles: this.efficiencyEstimator.getCycleCount(), + accOM: accOM != null ? +(accOM * 100).toFixed(1) : null, + accSC: accSC != null ? +(accSC * 100).toFixed(1) : null, + wOM: +(wOM * 100).toFixed(0), + wSC: +(wSC * 100).toFixed(0), + updatedAt: new Date().toISOString(), + }); + // Chart data (large: pvPredictions + modelAcc) written separately so the + // lightweight _updateWeather write above doesn't overwrite it. + this._setLive('learning_pv_chart_data', { pvPredictions, modelAcc }); + + // 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._setLive('policy_weather_hourly', hourlyWeather); + if (weatherData.fetchedAt) { + this._setLive('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. + // Guard: before sunrise, sunshineNext4Hours already covers post-sunrise slots and would + // produce a large fake estimate (e.g. 993W at 06:13 CEST before sunrise at ~07:10). + // Zero out sunScore until the sun has actually risen. + const _nowMs = Date.now(); + const _sunrise = weatherData?.todaySunrise; + const _sunset = weatherData?.todaySunset; + const _beforeSunrise = _sunrise instanceof Date && _nowMs < _sunrise.getTime(); + const _afterSunset = _sunset instanceof Date && _nowMs > _sunset.getTime() + 30 * 60 * 1000; + if (_beforeSunrise || _afterSunset) this._lastPvEstimateW = 0; // reset EMA — no sun + const sunScore = (weatherData && !_beforeSunrise && !_afterSunset) + ? 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, + avg_consumption_w: Math.round(this.learningEngine.getPredictedConsumption(new Date()) ?? 0), + }; + + // ------------------------------------------------------ + // ⭐ 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.75, 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, + consumptionW: this.learningEngine?.getPredictedConsumption(new Date()) ?? null, + evCharging: this._isEvCharging(), + }; + + } + + _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; + + // ✅ FIX: HW firmware caps discharge at 800 W regardless of battery count + // (e.g. 2 batteries: max_consumption_w=1600, max_production_w=800) + // Charge scales linearly, discharge does not. + const chargeFallbackW = unitCount * 800; + const dischargeFallbackW = 800; + + // Use || instead of ?? so that 0 (from missing WS field) also triggers fallback + const maxProduction = + this.p1Device.getCapabilityValue('measure_power.battery_group_max_production_w') || + dischargeFallbackW; + + const maxConsumption = + this.p1Device.getCapabilityValue('measure_power.battery_group_max_consumption_w') || + chargeFallbackW; + + 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' || mode === 'pv_trickle') { + 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 if (mode === 'predictive') { + targetMode = 'predictive'; + } 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}`); + + // ⭐ HW Slim laden actief → policy engine niet overrulen + if (actualMode === 'predictive') { + this.log('⏸️ HW Slim laden (predictive) actief — policy engine gepauzeerd, geen mode-wijziging'); + return true; + } + + // ⭐ 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); + } + } + + /** + * Check if the current moment is favorable for running appliances (cheap price or PV surplus). + * Fires the favorable_consumption_window trigger on a false→true edge only. + * @param {Object} tariff - tariff info from _gatherInputs() + */ + _checkFavorableWindow(tariff) { + if (!tariff) return; + + const currentPrice = tariff.currentPrice ?? null; + const top3Lowest = tariff.top3Lowest || []; + + // Cheap: current price is within the top-3 cheapest remaining slots of today + const cheapThreshold = top3Lowest.length > 0 + ? Math.max(...top3Lowest.map(p => p.price)) + 0.001 + : null; + const isCheap = cheapThreshold !== null && currentPrice !== null && currentPrice <= cheapThreshold; + + // PV surplus: significant solar production (>500W means PV is covering meaningful load) + const isPvSurplus = (this._pvProductionW ?? 0) > 500; + + const isFavorable = isCheap || isPvSurplus; + + if (isFavorable && !this._favorableWindowActive) { + this._favorableWindowActive = true; + + // Calculate how many minutes remain in this favorable window + let durationMinutes = 60; + if (isCheap && Array.isArray(tariff.effectivePrices)) { + const futureSlots = tariff.effectivePrices + .filter(p => p.index >= 0) + .sort((a, b) => a.index - b.index); + let count = 0; + for (const slot of futureSlots) { + if (slot.price <= cheapThreshold) count++; + else break; + } + durationMinutes = Math.max(15, count * 15); + } else if (isPvSurplus && Array.isArray(this.weatherData?.hourlyForecast)) { + const now = Date.now(); + const futureHours = this.weatherData.hourlyForecast + .filter(h => { + const t = h.time instanceof Date ? h.time : new Date(h.time); + return t.getTime() >= now; + }) + .sort((a, b) => { + const ta = a.time instanceof Date ? a.time : new Date(a.time); + const tb = b.time instanceof Date ? b.time : new Date(b.time); + return ta - tb; + }); + let hours = 0; + for (const h of futureHours) { + if (h.radiationWm2 > 50) hours++; + else break; + } + durationMinutes = Math.max(60, hours * 60); + } + + const reason = isCheap && isPvSurplus + ? (this.homey.i18n?.getLanguage?.() === 'nl' ? 'Goedkope stroom + zonnepanelen' : 'Cheap electricity + solar') + : isCheap + ? (this.homey.i18n?.getLanguage?.() === 'nl' ? 'Goedkope stroom' : 'Cheap electricity') + : (this.homey.i18n?.getLanguage?.() === 'nl' ? 'Zonnepanelen produceren' : 'Solar production'); + + const trigger = this.homey.flow.getDeviceTriggerCard('favorable_consumption_window'); + if (trigger) { + trigger.trigger(this, { + reason, + duration_minutes: durationMinutes, + price: Math.round((currentPrice ?? 0) * 10000) / 10000 + }).catch(err => this.error('favorable_consumption_window trigger failed:', err)); + } + + this.log(`⚡ Favorable consumption window started: ${reason}, ~${durationMinutes} min, €${currentPrice}`); + } else if (!isFavorable && this._favorableWindowActive) { + this._favorableWindowActive = false; + this.log('⚡ Favorable consumption window ended'); + const endTrigger = this.homey.flow.getDeviceTriggerCard('favorable_consumption_window_ended'); + if (endTrigger) { + endTrigger.trigger(this, {}).catch(err => this.error('favorable_consumption_window_ended trigger failed:', err)); + } + } + } + + 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}` + }); + } + + // Invalidate Solcast cache when API key or resource ID changes + if (changedKeys.includes('solcast_api_key') || changedKeys.includes('solcast_resource_id')) { + this._solcastProvider?.invalidateCache(); + } + + // 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 ?? 0, + max_soc: newSettings.max_soc ?? 100, + battery_efficiency: newSettings.battery_efficiency || 0.75, + 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, + price_resolution: newSettings.price_resolution || '15min', + }); + + // Handle interval change + if (changedKeys.includes('policy_interval')) { + this._schedulePolicyCheck(); + } + + // Rebuild schedule + refresh chart/widget immediately when resolution changes + // Use setTimeout so the new setting is persisted before the policy check reads it + if (changedKeys.includes('price_resolution')) { + this.homey.setTimeout(() => { + this._runPolicyCheck().catch(e => this.error('Policy recheck after resolution change failed:', e)); + }, 200); + } + + // 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.75, + 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._slotAlignTimeout) { + this.homey.clearTimeout(this._slotAlignTimeout); + this._slotAlignTimeout = 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; + } + + if (this._modeHistoryFlushInterval) { + this.homey.clearInterval(this._modeHistoryFlushInterval); + this._modeHistoryFlushInterval = null; + } + + if (this._settingsFlushTimer) { + this.homey.clearTimeout(this._settingsFlushTimer); + this._settingsFlushTimer = null; + } + + if (this._evChargingTimer) { + this.homey.clearTimeout(this._evChargingTimer); + this._evChargingTimer = null; + } + // Final flush — write pending queued settings synchronously on shutdown + // so the last policy run's state is not lost on restart. + if (this._settingsQueue && this._settingsQueue.size > 0) { + for (const [key, value] of this._settingsQueue) { + try { this.homey.settings.set(key, value); } catch (_) {} + } + this._settingsQueue.clear(); + } + + this._modeChartImage = 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', + 'policy_widget_data', + 'device_settings', + 'battery_expansion_analysis', + 'expansion_investment', + 'policy_consumption_profile', + 'battery_cycle_history', + 'learning_status', + 'learning_pv_chart_data', + 'pv_surplus_forecast', + 'policy_pv_forecast_hourly', + 'policy_pv_actual_today', + 'policy_pv_bias', + `batt_mode_hist_${this.getData().id}`, + ]; + for (const key of settingsToClean) { + try { this.homey.settings.unset(key); } catch (_) {} + } + + // Clear p1Device reference + this.p1Device = null; + + this.log('BatteryPolicyDevice cleanup complete'); + } + + /** + * Update two planning chart camera images (today + tomorrow) via quickchart.io. + * Called from _saveWidgetData() after every policy run. + */ + async _updatePlanningChart(compact) { + // Guard: prevent concurrent calls (each call makes 3 HTTPS requests; a second concurrent + // call while the first is mid-stream doubles memory pressure during an already-elevated + // heap period and is the primary cause of nightly Memory Warning crashes). + if (this._planningChartUpdating) { + this.log('[MEM] Chart update skipped — previous still in progress'); + return; + } + // Guard: quickchart HTTP + image buffer adds ~30 MB; skip when heap is already elevated. + let _heapChart = 0; + try { _heapChart = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (_heapChart > 35) { + this.log(`[MEM] Planning chart update skipped — heap ${_heapChart.toFixed(1)} MB > 35 MB guard`); + return; + } + this._planningChartUpdating = true; + try { + const slots = compact?.slots || []; + + // Split slots by Amsterdam calendar day + const dayKey = ts => new Date(ts).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); // YYYY-MM-DD + const today = dayKey(Date.now()); + const tomorrow = dayKey(Date.now() + 86400000); + + const todaySlots = slots.filter(s => dayKey(s.ts) === today); + const tomorrowSlots = slots.filter(s => dayKey(s.ts) === tomorrow); + + this._chartToday = { ...compact, slots: todaySlots }; + this._chartTomorrow = { ...compact, slots: tomorrowSlots }; + + // Only call image.update() when chart data has materially changed. + // Each update() fires a Homey realtime event that can override another app's + // camera view on the mobile client — avoid spurious updates. + const hashSlots = slots => JSON.stringify(slots.map(s => `${s.ts}:${s.mode}:${s.price}`)); + const hashToday = hashSlots(todaySlots); + const hashTomorrow = hashSlots(tomorrowSlots); + + // Today image + if (!this.planningImageToday) { + this.planningImageToday = await this.homey.images.createImage(); + this.planningImageToday.setStream(async (stream) => { + if (this._isPredictiveMode) { stream.end(); return; } + if (!this._chartToday || !this._chartToday.slots?.length) { stream.end(); return; } + let _h = 0; try { _h = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (_h > 38) { this.log(`[MEM] Today chart stream skipped — heap ${_h.toFixed(1)} MB > 38 MB`); stream.end(); return; } + try { + await ChartRenderer.streamPlanningChart(stream, this._chartToday); + } catch (e) { + this.error('[Chart] Today stream failed:', e.message); + if (!stream.destroyed) stream.end(); + } + }); + await this.setCameraImage('planning_today', 'Batterij Vandaag', this.planningImageToday); + this._chartHashToday = null; // force first update + } + if (hashToday !== this._chartHashToday) { + await this.planningImageToday.update(); + this._chartHashToday = hashToday; + } + + // Tomorrow image — only register once slots are available (after ~14:00 when DAP prices arrive). + // Registering with an empty stream causes Homey to cache an empty response and hide the tile + // permanently until the next app restart, even after subsequent .update() calls. + if (!this.planningImageTomorrow && tomorrowSlots.length > 0) { + this.planningImageTomorrow = await this.homey.images.createImage(); + this.planningImageTomorrow.setStream(async (stream) => { + if (this._isPredictiveMode) { this.log('[Chart] Tomorrow stream: predictive mode → empty'); stream.end(); return; } + if (!this._chartTomorrow || !this._chartTomorrow.slots?.length) { this.log('[Chart] Tomorrow stream: no slots → empty'); stream.end(); return; } + let _h = 0; try { _h = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (_h > 38) { this.log(`[MEM] Tomorrow chart stream skipped — heap ${_h.toFixed(1)} MB > 38 MB`); stream.end(); return; } + try { + await ChartRenderer.streamPlanningChart(stream, this._chartTomorrow); + } catch (e) { + this.error('[Chart] Tomorrow stream failed:', e.message); + if (!stream.destroyed) stream.end(); + } + }); + await this.setCameraImage('planning_tomorrow', 'Batterij Morgen', this.planningImageTomorrow); + this._chartHashTomorrow = null; // force first update + } + if (this.planningImageTomorrow && tomorrowSlots.length > 0 && hashTomorrow !== this._chartHashTomorrow) { + await this.planningImageTomorrow.update(); + this._chartHashTomorrow = hashTomorrow; + } + + // PV forecast vs actual image + const pvActual = this.homey.settings.get('policy_pv_actual_today'); + const pvForecast = this._liveState.policy_pv_forecast_hourly + ?? this.homey.settings.get('policy_pv_forecast_hourly'); + const pvForecastOM = this._liveState.policy_pv_forecast_om ?? null; + const pvForecastSC = this._liveState.policy_pv_forecast_sc ?? null; + const pvForecastSCEffective = this._liveState.policy_pv_forecast_sc_effective ?? null; + const pvCapW = this.getSetting('pv_capacity_w') || 0; + const pvScDayStart = this.homey.settings.get('policy_sc_daystart') ?? null; + + this._pvChartData = { pvActual, pvForecast, pvForecastOM, pvForecastSC, pvForecastSCEffective, pvForecastDayStart: this._pvDayStartForecast ?? null, pvScDayStart, pvCapacityW: pvCapW }; + + if (!this.planningImagePv) { + await this._initPvCamera(); + } + + const pvHash = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }) + JSON.stringify(pvActual?.sums) + JSON.stringify(pvForecast); + if (pvHash !== this._pvChartHash) { + await this.planningImagePv.update(); + this._pvChartHash = pvHash; + } + + this.log('📊 Planning chart camera images updated (today + tomorrow + PV)'); + } catch (err) { + this.error('Failed to update planning chart:', err); + } finally { + this._planningChartUpdating = false; + } + } + + + async _initPvCamera() { + if (this.planningImagePv) return; + this.planningImagePv = await this.homey.images.createImage(); + this.planningImagePv.setStream(async (stream) => { + const pvActual = this.homey.settings.get('policy_pv_actual_today'); + const pvForecast = this._liveState.policy_pv_forecast_hourly + ?? this.homey.settings.get('policy_pv_forecast_hourly'); + const pvForecastOM = this._liveState.policy_pv_forecast_om ?? null; + const pvForecastSC = this._liveState.policy_pv_forecast_sc ?? null; + const pvForecastSCEffective = this._liveState.policy_pv_forecast_sc_effective ?? null; + const pvCapW = this.getSetting('pv_capacity_w') || 0; + if (!pvActual && !pvForecast) { stream.end(); return; } + let _h = 0; try { _h = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (_h > 38) { this.log(`[MEM] PV chart stream skipped — heap ${_h.toFixed(1)} MB > 38 MB`); stream.end(); return; } + try { + await ChartRenderer.streamPvChart(stream, { pvActual, pvForecast, pvForecastOM, pvForecastSC, pvForecastSCEffective, pvForecastDayStart: this._pvDayStartForecast ?? null, pvScDayStart: this.homey.settings.get('policy_sc_daystart') ?? null, pvCapacityW: pvCapW }); + } catch (e) { + this.error('PV chart stream error:', e.message); + if (!stream.destroyed) stream.end(); + } + }); + await this.setCameraImage('planning_pv', 'PV Opwek', this.planningImagePv); + this.log('📷 PV Opwek camera geregistreerd'); + + // Load initial data so the camera has content immediately + const pvActual = this.homey.settings.get('policy_pv_actual_today'); + const pvForecast = this._liveState.policy_pv_forecast_hourly + ?? this.homey.settings.get('policy_pv_forecast_hourly'); + const pvForecastOM = this._liveState.policy_pv_forecast_om ?? null; + const pvForecastSC = this._liveState.policy_pv_forecast_sc ?? null; + const pvForecastSCEffective = this._liveState.policy_pv_forecast_sc_effective ?? null; + const pvCapW = this.getSetting('pv_capacity_w') || 0; + if (pvForecast || pvActual) { + this._pvChartData = { pvActual, pvForecast, pvForecastOM, pvForecastSC, pvForecastSCEffective, pvForecastDayStart: this._pvDayStartForecast ?? null, pvScDayStart: this.homey.settings.get('policy_sc_daystart') ?? null, pvCapacityW: pvCapW }; + await this.planningImagePv.update(); + } + } + + async _initModeHistoryCamera() { + if (this._modeChartImage) return; + + // Seed recording + build body BEFORE registering camera, so Homey's initial + // fetch has valid content. Without this the first stream fetch returns + // empty and Homey hides the tile in the device "More Info" page. + if (this.p1Device) { + const mode = this.p1Device._currentDetailedMode + || this.p1Device.getCapabilityValue('battery_group_charge_mode') + || 'unknown'; + const soc = this.p1Device.getCapabilityValue('battery_group_average_soc') ?? 50; + this._recordModeHistory(mode); + this._recordSoCHistory(soc); + } + + this._modeChartImage = await this.homey.images.createImage(); + this._modeChartImage.setStream(async (stream) => { + // Build body on demand — ensures fresh data on every fetch, survives + // in-memory resets after app restart. + if (this._modeHistory?.length) { + await this._buildModeChartBody(); + } + if (!this._modeChartBody) { stream.end(); return; } + // Guard: Homey fetches the stream asynchronously — the HTTP request to quickchart + // adds ~30 MB of heap. Skip if heap is already elevated to avoid a crash. + let _heapStream = 99; + try { _heapStream = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (_heapStream > 38) { + this.log(`[MEM] Mode chart stream skipped — heap ${_heapStream.toFixed(1)} MB > 38 MB`); + stream.end(); + return; + } + try { + await ChartRenderer.streamModeChart(stream, this._modeChartBody); + } catch (e) { + this.error('[Chart] Mode history stream failed:', e.message); + if (!stream.destroyed) stream.end(); + } + }); + + // Pre-build body so the initial setCameraImage fetch has content + if (this._modeHistory?.length) { + let heap = 0; + try { heap = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (heap > 40) { + this.log(`[MEM] Skipping initial mode chart body — heap ${heap.toFixed(1)} MB > 40 MB guard`); + } else { + await this._buildModeChartBody(); + } + } + + await this.setCameraImage('battery_mode_history', 'Batterij Modi', this._modeChartImage); + this.log('📷 Batterij Modi camera geregistreerd'); + } + + async _updateModeChart() { + if (!this._modeChartImage) return; + if (!this._modeHistory?.length) return; + if (this._modeChartUpdating) { + this.log('[MEM] Mode chart update skipped — previous still in progress'); + return; + } + this._modeChartUpdating = true; + + try { + await this._buildModeChartBody(); + await this._modeChartImage.update(); + this.log('📊 Battery mode chart updated (15-min slots)'); + } finally { + this._modeChartUpdating = false; + } + } + + async _buildModeChartBody() { + this._modeChartBody = ChartRenderer.buildModeChartBody(this._modeHistory); + } + + _recordModeHistory(mode) { + const currentSlotMs = Math.floor(Date.now() / (15 * 60000)) * (15 * 60000); + const slotKey = new Date(currentSlotMs).toISOString().slice(0, 16); // 'YYYY-MM-DDTHH:MM' + if (!this._modeHistory) this._modeHistory = []; + + let bucket = this._modeHistory.find(b => b.h === slotKey); + if (!bucket) { + bucket = { h: slotKey, m: {} }; + this._modeHistory.push(bucket); + // Keep only last 25h (100 slots) + const cutoff = Date.now() - 25 * 3600 * 1000; + this._modeHistory = this._modeHistory.filter( + b => new Date(b.h + ':00Z').getTime() > cutoff + ); + } + + bucket.m[mode] = (bucket.m[mode] || 0) + 1; + } + + _recordSoCHistory(soc) { + if (typeof soc !== 'number') return; + const currentSlotMs = Math.floor(Date.now() / (15 * 60000)) * (15 * 60000); + const slotKey = new Date(currentSlotMs).toISOString().slice(0, 16); + if (!this._modeHistory) this._modeHistory = []; + const bucket = this._modeHistory.find(b => b.h === slotKey); + if (bucket) bucket.soc = soc; + } + + 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, record completed cycle then wipe stale cost tracking. + // Use max(minSoc, 3) so the cycle is recorded when the firmware stops discharging + // (typically 1-3% reported SoC), not only at exactly 0-1%. + const minSoc = this.getSetting('min_soc') ?? 0; + if (soc !== null && soc <= Math.max(minSoc, 3)) { + // Save cycle profit if we discharged a meaningful amount + if ((this._cycleKwhDischarged || 0) > 0.05) { + const profit = (this._cycleRevenue || 0) - (this._cycleCost || 0); + const avgDischargePrice = this._cycleRevenue / this._cycleKwhDischarged; + let cycleHistory = this.homey.settings.get('battery_cycle_history') || []; + cycleHistory.push({ + date: new Date().toLocaleString('en-CA', { timeZone: 'Europe/Amsterdam' }).slice(0, 10), + kwhDischarged: +this._cycleKwhDischarged.toFixed(3), + avgChargePrice: +(this._costAvg || 0).toFixed(4), + avgDischargePrice: +avgDischargePrice.toFixed(4), + profitEur: +profit.toFixed(4), + }); + if (cycleHistory.length > 60) cycleHistory = cycleHistory.slice(-60); + this.homey.settings.set('battery_cycle_history', cycleHistory); + this.log(`💰 Cycle recorded: ${this._cycleKwhDischarged.toFixed(2)}kWh discharged @ avg €${avgDischargePrice.toFixed(3)}, cost €${this._cycleCost.toFixed(3)}, profit €${profit.toFixed(3)}`); + } + this._cycleRevenue = 0; + this._cycleCost = 0; + this._cycleKwhDischarged = 0; + await this.setStoreValue('cycle_kwh_discharged', 0); + await this.setStoreValue('cycle_revenue', 0); + await this.setStoreValue('cycle_cost', 0); + + if ((this._costEnergy || 0) > 0 || (this._costAvg || 0) > 0) { + this.log(`💰 CostModel RESET: SoC ${soc}% <= ${Math.max(minSoc, 3)}% → 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; + this._cycleKwhDischarged = await this.getStoreValue('cycle_kwh_discharged') || 0; + this._cycleRevenue = await this.getStoreValue('cycle_revenue') || 0; + this._cycleCost = await this.getStoreValue('cycle_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 we just finished a meaningful discharge session, record the cycle now. + // This handles PV-heavy days where SoC never reaches 0% (so the SoC-based trigger above + // never fires). Minimum 0.3 kWh prevents noise from short standby/idle transitions. + if (this._wasDischarging && (this._cycleKwhDischarged || 0) >= 0.3) { + const profit = (this._cycleRevenue || 0) - (this._cycleCost || 0); + const avgDischargePrice = this._cycleRevenue / this._cycleKwhDischarged; + let cycleHistory = this.homey.settings.get('battery_cycle_history') || []; + cycleHistory.push({ + date: new Date().toLocaleString('en-CA', { timeZone: 'Europe/Amsterdam' }).slice(0, 10), + kwhDischarged: +this._cycleKwhDischarged.toFixed(3), + avgChargePrice: +(this._costAvg || 0).toFixed(4), + avgDischargePrice: +avgDischargePrice.toFixed(4), + profitEur: +profit.toFixed(4), + }); + if (cycleHistory.length > 60) cycleHistory = cycleHistory.slice(-60); + this.homey.settings.set('battery_cycle_history', cycleHistory); + this.log(`💰 Cycle recorded (discharge→charge): ${this._cycleKwhDischarged.toFixed(2)}kWh @ avg €${avgDischargePrice.toFixed(3)}, profit €${profit.toFixed(3)}`); + this._cycleRevenue = 0; + this._cycleCost = 0; + this._cycleKwhDischarged = 0; + await this.setStoreValue('cycle_kwh_discharged', 0); + await this.setStoreValue('cycle_revenue', 0); + await this.setStoreValue('cycle_cost', 0); + } + this._wasDischarging = false; + + if (pvState) { + const pvMode = this.getSetting('pv_cost_mode') || 'free'; + const feedIn = this.getSetting('feed_in_tariff') ?? 0.08; + + costNew = (pvMode === 'feedin') ? feedIn : 0; + } 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 — accumulate revenue for cycle profit tracking + this._wasDischarging = true; + const dischargeKwh = Math.abs(deltaKwh); + const dischargePrice = this.tariffManager.getCurrentTariff(gridPower).currentPrice; + this._cycleRevenue = (this._cycleRevenue || 0) + dischargeKwh * dischargePrice; + this._cycleCost = (this._cycleCost || 0) + dischargeKwh * (this._costAvg || 0); + this._cycleKwhDischarged = (this._cycleKwhDischarged || 0) + dischargeKwh; + + this._costEnergy = Math.max(0, this._costEnergy + deltaKwh); + + if (this._costModelCallCount % 12 === 0) { + if (debug) this.log(`💰 CostModel discharge: ${deltaKwh.toFixed(5)}kWh @ €${dischargePrice.toFixed(4)}, 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); + await this.setStoreValue('cycle_kwh_discharged', this._cycleKwhDischarged || 0); + await this.setStoreValue('cycle_revenue', this._cycleRevenue || 0); + await this.setStoreValue('cycle_cost', this._cycleCost || 0); + } + } + + +} + +module.exports = BatteryPolicyDevice; diff --git a/drivers/battery-policy/device_settings.html b/drivers/battery-policy/device_settings.html new file mode 100644 index 00000000..b87cfea7 --- /dev/null +++ b/drivers/battery-policy/device_settings.html @@ -0,0 +1,593 @@ + + + + + + + + + + +
+

+
+
+ +
+
+
⏳ 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..78daf9e4 --- /dev/null +++ b/drivers/battery-policy/driver.compose.json @@ -0,0 +1,116 @@ +{ + "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", + "presence_mode", + "policy_profit_eur", + "pv_forecast_kwh", + "bias_factor", + "plan_summary" + ], + "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": "📊" + }, + { + "id": "price_resolution", + "type": "dropdown", + "label": { + "en": "Price Resolution", + "nl": "Prijsresolutie" + }, + "hint": { + "en": "Set to 15 minutes for providers with quarter-hour prices (e.g. Xadi). Set to 1 hour for providers with hourly prices (e.g. Zonneplan).", + "nl": "Zet op 15 minuten voor providers met kwartierprijzen (bijv. Xadi). Zet op 1 uur voor providers met uurprijzen (bijv. Zonneplan)." + }, + "value": "15min", + "values": [ + { + "id": "15min", + "label": { + "en": "15 minutes", + "nl": "15 minuten" + } + }, + { + "id": "1h", + "label": { + "en": "1 hour", + "nl": "1 uur" + } + } + ] + } + ] + } + ], + "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..c9f33ba1 --- /dev/null +++ b/drivers/battery-policy/driver.flow.compose.json @@ -0,0 +1,335 @@ +{ + "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": "favorable_consumption_window", + "title": { "en": "Good time to run appliances starts", "nl": "Gunstig moment om apparaten aan te zetten begint" }, + "titleFormatted": { "en": "Good time to run appliances starts", "nl": "Gunstig moment om apparaten aan te zetten begint" }, + "hint": { + "en": "Triggers when electricity becomes cheap (top-3 of the day) or solar panels start producing significantly. Use this to send a notification or switch on a smart plug.", + "nl": "Vuurt als stroom goedkoop wordt (top-3 van de dag) of zonnepanelen significant gaan produceren. Gebruik dit voor een melding of om een slimme stekker in te schakelen." + }, + "tokens": [ + { + "name": "reason", + "type": "string", + "title": { "en": "Reason", "nl": "Reden" }, + "example": { "en": "Cheap electricity", "nl": "Goedkope stroom" } + }, + { + "name": "duration_minutes", + "type": "number", + "title": { "en": "Duration (minutes)", "nl": "Duur (minuten)" }, + "example": 60 + }, + { + "name": "price", + "type": "number", + "title": { "en": "Current price (€/kWh)", "nl": "Huidige prijs (€/kWh)" }, + "example": 0.08 + } + ] + }, + { + "id": "favorable_consumption_window_ended", + "title": { "en": "Good time to run appliances has ended", "nl": "Gunstig moment om apparaten aan te zetten is voorbij" }, + "titleFormatted": { "en": "Good time to run appliances has ended", "nl": "Gunstig moment om apparaten aan te zetten is voorbij" }, + "hint": { + "en": "Triggers when electricity is no longer cheap and solar production has dropped below threshold.", + "nl": "Vuurt als stroom niet meer goedkoop is en zonnepanelen onder de drempel produceren." + } + }, + { + "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": "consumption_now_favorable", + "title": { "en": "Now is !{{a good|not a good}} time to run appliances", "nl": "Het is !{{nu gunstig|nu niet gunstig}} om apparaten aan te zetten" }, + "titleFormatted": { "en": "Now is !{{a good|not a good}} time to run appliances", "nl": "Het is !{{nu gunstig|nu niet gunstig}} om apparaten aan te zetten" }, + "hint": { + "en": "True when electricity is currently cheap (top-3 price of the day) or solar panels are producing significantly (>500W).", + "nl": "Waar als stroom nu goedkoop is (top-3 goedkoopste van vandaag) of zonnepanelen significant produceren (>500W)." + } + }, + { + "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 trading style", "nl": "Stel handelsstijl in" }, + "titleFormatted": { "en": "Set trading style to [[mode]]", "nl": "Stel handelsstijl in op [[mode]]" }, + "hint": { + "en": "Controls how actively the battery trades on price differences. Eco = fewer cycles, prioritise battery health. Balanced = default, normal arbitrage. Aggressive = more cycles, maximise savings.", + "nl": "Bepaalt hoe actief de batterij handelt op prijsverschillen. Eco = minder cycli, batterij ontzien. Balanced = standaard, normaal arbitrage. Agressief = meer cycli, maximale besparing." + }, + "args": [ + { + "name": "mode", + "type": "dropdown", + "values": [ + { "id": "eco", "label": { "en": "Eco — fewer cycles, spare the battery", "nl": "Eco — minder cycli, batterij ontzien" } }, + { "id": "balanced", "label": { "en": "Balanced — default", "nl": "Balanced — standaard" } }, + { "id": "aggressive", "label": { "en": "Aggressive — more cycles, maximise savings", "nl": "Agressief — meer cycli, maximale besparing" } } + ] + } + ] + }, + { + "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": "set_away_mode", + "title": { "en": "Set away mode", "nl": "Zet afwezigheidsmodus" }, + "titleFormatted": { "en": "Set away mode (battery: [[battery_mode]])", "nl": "Zet afwezigheidsmodus (batterij: [[battery_mode]])" }, + "hint": { + "en": "Pause learning and disable automatic policy. Use when leaving for vacation or an extended absence.", + "nl": "Pauzeert het leren en schakelt automatische beleid uit. Gebruik bij vakantie of langdurige afwezigheid." + }, + "args": [ + { + "name": "battery_mode", + "type": "dropdown", + "title": { "en": "Battery behaviour", "nl": "Batterijgedrag" }, + "values": [ + { "id": "standby", "label": { "en": "Standby (do nothing)", "nl": "Standby (niets doen)" } }, + { "id": "zero", "label": { "en": "Zero (cover own consumption)", "nl": "Nul op de meter" } } + ] + } + ] + }, + { + "id": "set_home_mode", + "title": { "en": "Set home mode", "nl": "Zet thuismodus" }, + "titleFormatted": { "en": "Set home mode (resume normal operation)", "nl": "Zet thuismodus (hervatten normale werking)" }, + "hint": { + "en": "Resume learning and re-enable automatic policy after an absence.", + "nl": "Hervat het leren en schakelt automatische beleid weer in na afwezigheid." + } + }, + { + "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." + } + }, + { + "id": "set_ev_charging", + "title": { "en": "Set EV charging state", "nl": "Stel EV-laadstatus in" }, + "titleFormatted": { + "en": "EV charging is [[state]]", + "nl": "EV laadt [[state]]" + }, + "hint": { + "en": "Tell the policy that an electric vehicle is charging. While active the battery will not discharge (zero_charge_only). PV surplus can still charge the battery. Auto-clears after 8 hours as a safety net.", + "nl": "Geef aan dat een elektrische auto laadt. Zolang actief ontlaadt de batterij niet (zero_charge_only). PV-overschot mag wel de batterij laden. Vervalt automatisch na 8 uur als vangnet." + }, + "args": [ + { + "name": "state", + "type": "dropdown", + "values": [ + { "id": "start", "label": { "en": "Started", "nl": "Gestart" } }, + { "id": "stop", "label": { "en": "Stopped", "nl": "Gestopt" } } + ] + } + ] + } + ] +} diff --git a/drivers/battery-policy/driver.js b/drivers/battery-policy/driver.js new file mode 100644 index 00000000..bc22a718 --- /dev/null +++ b/drivers/battery-policy/driver.js @@ -0,0 +1,255 @@ +'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: Favorable consumption window + this.homey.flow.getDeviceTriggerCard('favorable_consumption_window') + .registerRunListener(async () => true); + + // Trigger: Favorable consumption window ended + this.homey.flow.getDeviceTriggerCard('favorable_consumption_window_ended') + .registerRunListener(async () => true); + + // 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: Favorable consumption window + this.homey.flow.getConditionCard('consumption_now_favorable') + .registerRunListener(async (args) => { + return args.device._favorableWindowActive === 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: 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: Set away mode — pause learning, disable auto-apply, force battery mode + this.homey.flow.getActionCard('set_away_mode') + .registerRunListener(async (args) => { + const { device, battery_mode } = args; + await device.learningEngine.pause(); + await device.setCapabilityValue('auto_apply', false); + await device.setCapabilityValue('presence_mode', '🏖️ Away'); + await device._applyRecommendation(battery_mode, 100); + device.log(`Away mode activated: learning paused, battery → ${battery_mode}`); + }); + + // Action: Set home mode — resume learning, re-enable auto-apply, run policy + this.homey.flow.getActionCard('set_home_mode') + .registerRunListener(async (args) => { + const { device } = args; + await device.learningEngine.resume(); + await device.setCapabilityValue('auto_apply', true); + await device.setCapabilityValue('presence_mode', '🏠 Home'); + await device._runPolicyCheck(); + device.log('Home mode activated: learning resumed, policy running'); + }); + + // 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'); + }); + + // Action: Set EV charging state — blocks battery discharge while active + this.homey.flow.getActionCard('set_ev_charging') + .registerRunListener(async (args) => { + const active = args.state === 'start'; + await args.device._setEvCharging(active); + }); + + 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..919ab00c --- /dev/null +++ b/drivers/battery-policy/driver.settings.compose.json @@ -0,0 +1,499 @@ +[ + { + "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, battery only charges/discharges within configured price limits. When disabled (default), the system uses the economic break-even as the only floor — the optimizer decides timing based on actual prices and RTE. Recommended: DISABLED for most users.", + "nl": "Indien ingeschakeld laadt/ontlaadt de batterij alleen binnen geconfigureerde prijslimieten. Indien uitgeschakeld (standaard) gebruikt het systeem de economische break-even als enige ondergrens — de optimizer bepaalt timing op basis van actuele prijzen en RTE. Aanbevolen: UITGESCHAKELD voor de meeste gebruikers." + }, + "value": false, + "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_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.12, + "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)" }, + "hint": { + "en": "The battery will never discharge below this price — even if the break-even calculation would allow it. Once the cost model has enough data (≥0.5 kWh tracked), discharge also requires the price to exceed the learned break-even (charge cost ÷ efficiency + half cycle degradation cost). The effective minimum is whichever is higher: this setting or the break-even.", + "nl": "De batterij ontlaadt nooit onder deze prijs — ook niet als de break-even berekening het zou toestaan. Zodra het kostenmodel voldoende data heeft (≥0,5 kWh bijgehouden), is ook vereist dat de prijs boven de geleerde break-even ligt (laadkosten ÷ efficiëntie + halve cyclusdegradatiekosten). De effectieve drempel is de hoogste van deze instelling en de break-even." + }, + "value": 0.22, + "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" } + }, + { + "id": "peak_shaving_threshold", + "type": "number", + "label": { "en": "Peak Shaving Threshold (W)", "nl": "Piekbeveiliging drempelwaarde (W)" }, + "hint": { + "en": "When the instantaneous house load exceeds this value and the optimizer says 'preserve', the battery will discharge to cover the peak. Set to 0 to disable. Typical value: 1500W (e.g. electric cooking). Set lower than your cooking load.", + "nl": "Als het momentane huisverbruik deze waarde overschrijdt en de optimizer 'bewaren' adviseert, ontlaadt de batterij om de piek te dekken. Stel 0 in om uit te schakelen. Typische waarde: 1500W (bijv. elektrisch koken). Stel lager in dan je kookverbruik." + }, + "value": 0, + "min": 0, + "max": 10000, + "step": 100, + "units": "W", + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + { + "id": "pv_cost_mode", + "type": "dropdown", + "label": { "en": "PV Charging Cost", "nl": "PV laadkosten" }, + "hint": { + "en": "What does it cost to store solar energy? 'Free' (€0): recommended for dynamic tariffs with net metering active (NL until 2027) — storing and exporting are equivalent. 'Export value': use when your fixed contract charges an export penalty, or post-2027 without net metering — enter the net value per kWh you receive for exporting (export compensation minus any export penalty).", + "nl": "Wat kost het om zonne-energie op te slaan? 'Gratis' (€0): aanbevolen bij dynamisch tarief met saldering actief (NL tot 2027) — opslaan en terugleveren zijn equivalent. 'Terugleverwaarde': gebruik bij vast contract met terugleverboete, of post-2027 zonder saldering — vul de netto terugleverwaarde in per kWh (terugleververgoeding minus eventuele boete)." + }, + "values": [ + { "id": "free", "label": { "en": "Free (€0) — dynamic tariff + net metering", "nl": "Gratis (€0) — dynamisch tarief + saldering" } }, + { "id": "feedin", "label": { "en": "Export value (€/kWh) — fixed contract or post-2027", "nl": "Terugleverwaarde (€/kWh) — vast contract of post-2027" } } + ], + "value": "free", + "visible": { "when": "tariff_type", "is": "dynamic" } + }, + { + "id": "feed_in_tariff", + "type": "number", + "label": { "en": "Net Export Value (€/kWh)", "nl": "Netto terugleverwaarde (€/kWh)" }, + "hint": { + "en": "Net value per kWh you receive for exporting: export compensation minus export penalty. Example: €0.12 compensation − €0.14 penalty = −€0.02 → enter 0. Post-2027 without net metering: typically €0.06–0.10 (commodity price only, no energy tax).", + "nl": "Netto bedrag per kWh dat u ontvangt bij teruglevering: terugleververgoeding minus terugleverboete. Voorbeeld: €0,12 vergoeding − €0,14 boete = −€0,02 → vul 0 in. Post-2027 zonder saldering: doorgaans €0,06–0,10 (alleen commodity-prijs, geen energiebelasting)." + }, + "value": 0.08, + "min": 0.00, + "max": 0.30, + "step": 0.01, + "units": "€/kWh", + "visible": { "when": "pv_cost_mode", "is": "feedin" } + } + ] + }, + + { + "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": "Solcast PV Forecast (optional)", "nl": "Solcast PV-voorspelling (optioneel)" }, + "visible": { "when": "pv_estimation_enabled", "is": true }, + "children": [ + { + "id": "solcast_enabled", + "type": "checkbox", + "label": { "en": "Use Solcast for PV forecast", "nl": "Solcast gebruiken voor PV-voorspelling" }, + "hint": { + "en": "When enabled, Solcast satellite-based forecasts replace the Open-Meteo radiation model. Requires a free Solcast account with a rooftop site configured. Max 10 API calls/day — the app caches results for 6 hours.", + "nl": "Indien ingeschakeld vervangen Solcast satellietgebaseerde voorspellingen het Open-Meteo stralingsmodel. Vereist een gratis Solcast-account met een geconfigureerde dakinstallatie. Max 10 API-aanroepen/dag — de app cachet resultaten 6 uur." + }, + "value": false + }, + { + "id": "solcast_resource_id", + "type": "text", + "label": { "en": "Solcast Resource ID", "nl": "Solcast Resource ID" }, + "hint": { + "en": "The resource_id of your rooftop site in the Solcast dashboard (e.g. aaaa-bbbb-cccc-dddd).", + "nl": "Het resource_id van uw dakinstallatie in het Solcast-dashboard (bijv. aaaa-bbbb-cccc-dddd)." + }, + "value": "", + "visible": { "when": "solcast_enabled", "is": true } + }, + { + "id": "solcast_api_key", + "type": "text", + "label": { "en": "Solcast API Key", "nl": "Solcast API-sleutel" }, + "hint": { + "en": "Your Solcast API key. Find it in the Solcast dashboard under Account → API Key.", + "nl": "Uw Solcast API-sleutel. Te vinden in het Solcast-dashboard onder Account → API-sleutel." + }, + "value": "", + "visible": { "when": "solcast_enabled", "is": true } + } + ] + }, + + { + "type": "group", + "label": { "en": "KNMI Station Data", "nl": "KNMI Stationsdata" }, + "children": [ + { + "id": "knmi_api_key", + "type": "text", + "label": { "en": "KNMI API Key", "nl": "KNMI API-sleutel" }, + "hint": { + "en": "Optional. KNMI EDR API key for ground-truth solar radiation from nearest weather station. Register at dataplatform.knmi.nl.", + "nl": "Optioneel. KNMI EDR API-sleutel voor gemeten zonnestraling van dichtstbijzijnde weerstation. Registreer op dataplatform.knmi.nl." + }, + "value": "" + } + ] + }, + + { + "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 + } + ] + }, + { + "type": "group", + "label": { "en": "24-Hour Planning", "nl": "24-Uurs Planning" }, + "children": [ + { + "id": "price_resolution", + "type": "dropdown", + "label": { "en": "Price Resolution", "nl": "Prijsresolutie" }, + "hint": { + "en": "Set to 15 minutes for providers with quarter-hour prices (e.g. Xadi). Set to 1 hour for providers with hourly prices (e.g. Zonneplan).", + "nl": "Zet op 15 minuten voor providers met kwartierprijzen (bijv. Xadi). Zet op 1 uur voor providers met uurprijzen (bijv. Zonneplan)." + }, + "value": "15min", + "values": [ + { "id": "15min", "label": { "en": "15 minutes", "nl": "15 minuten" } }, + { "id": "1h", "label": { "en": "1 hour", "nl": "1 uur" } } + ] + } + ] + } +] 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..e5f5e569 --- /dev/null +++ b/drivers/cloud_p1/device.js @@ -0,0 +1,487 @@ +/* + * 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.homey.app.bumpDeviceCount?.('cloud_p1'); + 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..89bd1897 --- /dev/null +++ b/drivers/cloud_watermeter/device.js @@ -0,0 +1,457 @@ +'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.homey.app.bumpDeviceCount?.('cloud_watermeter'); + 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()); + } + + for (const cap of ['meter_water.daily', 'alarm_water']) { + 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 { + 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³)`); + } + + // --- Leak detection (pattern-based) --- + await this._updateLeakPattern(data.values); +} + + /** + * Pattern-based leak detection. + * + * Day split into 4 blocks of 6h. 28 EMA slots (7 days × 4 blocks). + * A completed block is committed to EMA once, 30 min after block end. + * Alarm fires when block usage > ema × multiplier (min 4 readings). + */ + async _updateLeakPattern(values) { + if (!this.hasCapability('alarm_water')) return; + + const SLOTS_PER_BLOCK = 24; // 6h / 15min + const ALPHA = 0.2; + const MIN_COUNT = 4; + const BLOCK_BUFFER_MIN = 30; + + // Sum liters per 6h block from today's 15-min slots + const blockTotals = [0, 0, 0, 0]; + values.forEach((d, i) => { + if (d.water == null) return; + const b = Math.floor(i / SLOTS_PER_BLOCK); + if (b < 4) blockTotals[b] += d.water; + }); + + const now = new Date(); + const dayOfWeek = now.getDay(); + const hourFrac = now.getHours() + now.getMinutes() / 60; + const currentBlock = Math.floor(hourFrac / 6); + const minutesPastBlockStart = (hourFrac - currentBlock * 6) * 60; + + const pattern = this.getStoreValue('water_pattern') || {}; + const committed = this.getStoreValue('water_pattern_committed') || []; + const today = now.toDateString(); + + let leakDetected = false; + const multiplier = this.getSetting('leak_multiplier') ?? 5; + + for (let b = 0; b < 4; b++) { + const slotKey = `${dayOfWeek}-${b}`; + const commitKey = `${today}-${b}`; + + const isCompleted = b < currentBlock || + (b === currentBlock - 1 && minutesPastBlockStart >= BLOCK_BUFFER_MIN); + + if (!isCompleted) { + // Current incomplete block: check anomaly only, don't commit + if (b === currentBlock) { + const entry = pattern[slotKey]; + if (entry && entry.count >= MIN_COUNT && entry.ema > 0) { + if (blockTotals[b] > entry.ema * multiplier) { + leakDetected = true; + if (debug) this.log(`⚠️ Leak (current block ${b}): ${blockTotals[b].toFixed(0)}L > ${(entry.ema * multiplier).toFixed(0)}L (ema=${entry.ema.toFixed(0)}L×${multiplier})`); + } + } + } + continue; + } + + if (committed.includes(commitKey)) continue; + + // Check anomaly before updating EMA + const entry = pattern[slotKey] || { ema: null, count: 0 }; + if (entry.count >= MIN_COUNT && entry.ema > 0) { + if (blockTotals[b] > entry.ema * multiplier) { + leakDetected = true; + this.log(`⚠️ Leak (block ${b}, day ${dayOfWeek}): ${blockTotals[b].toFixed(0)}L > ${(entry.ema * multiplier).toFixed(0)}L (ema=${entry.ema.toFixed(0)}L×${multiplier})`); + } + } + + // Update EMA + entry.ema = entry.ema === null ? blockTotals[b] : ALPHA * blockTotals[b] + (1 - ALPHA) * entry.ema; + entry.count++; + pattern[slotKey] = entry; + + // Mark as committed (keep last 28 entries) + committed.push(commitKey); + if (committed.length > 28) committed.splice(0, committed.length - 28); + + if (debug) this.log(`Pattern updated slot ${slotKey}: ema=${entry.ema.toFixed(0)}L count=${entry.count}`); + } + + await this.setStoreValue('water_pattern', pattern); + await this.setStoreValue('water_pattern_committed', committed); + + const currentAlarm = this.getCapabilityValue('alarm_water'); + if (leakDetected && !currentAlarm) { + await this.setCapabilityValue('alarm_water', true); + } else if (!leakDetected && currentAlarm) { + this.log('✅ Leak alarm cleared'); + await this.setCapabilityValue('alarm_water', false); + } + } + + /** + * 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³`); + 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..5f9337db --- /dev/null +++ b/drivers/cloud_watermeter/driver.compose.json @@ -0,0 +1,127 @@ +{ + "id": "cloud-watermeter", + "name": { + "en": "Watermeter (cloud)", + "nl": "Watermeter (cloud)" + }, + "class": "sensor", + "platforms": [ "local"], + "capabilities": [ + "meter_water", + "meter_water.daily", + "alarm_water" + ], + "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)" + } + } + ] + }, + { + "type": "group", + "label": { + "en": "Leak detection", + "nl": "Lekdetectie" + }, + "children": [ + { + "id": "leak_multiplier", + "type": "number", + "label": { + "en": "Anomaly multiplier", + "nl": "Afwijkingsfactor" + }, + "hint": { + "en": "Alarm when water use in a 6-hour block exceeds this multiple of the learned average. Default: 5× (e.g. normal 50L → alarm above 250L). Requires ~4 weeks of data per time block.", + "nl": "Alarm als waterverbruik in een 6-uursblok deze veelvoud van het geleerde gemiddelde overschrijdt. Standaard: 5× (bv. normaal 50L → alarm boven 250L). Vereist ~4 weken data per tijdblok." + }, + "value": 5, + "min": 2, + "max": 20 + } + ] + } + ] +} \ 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..df695dde --- /dev/null +++ b/drivers/energy/device.js @@ -0,0 +1,1307 @@ +'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.homey.app.bumpDeviceCount?.('energy'); + 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..5d3ba0d6 --- /dev/null +++ b/drivers/energy_socket/device.js @@ -0,0 +1,796 @@ +'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.homey.app.bumpDeviceCount?.('energy_socket'); + + 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(); + this._pollRunning = false; // Guard against concurrent polls + + // 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; + + // Scale agent maxSockets with device count: 4 slots are too few for 15+ devices. + // Idempotent — last device wins, all converge to same value. + { + const allDevs = this.driver.getDevices(); + const target = Math.max(4, Math.ceil(allDevs.length / 3)); + if (SHARED_SOCKET_AGENT.maxSockets !== target) { + SHARED_SOCKET_AGENT.maxSockets = target; + this.log(`🔧 SHARED_SOCKET_AGENT.maxSockets=${target} (devices=${allDevs.length})`); + } + } + + 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 }); + }); + + this._applianceState = 'idle'; + this._applianceActiveStart = 0; + this._applianceFinishedAt = 0; + this._flowTriggerApplianceFinished = this.homey.flow.getDeviceTriggerCard('appliance_finished'); + } + + _tcpPing(ip, port, timeoutMs) { + const net = require('net'); + return new Promise((resolve) => { + const socket = net.createConnection({ host: ip, port, timeout: timeoutMs }); + socket.on('connect', () => { socket.destroy(); resolve(true); }); + socket.on('timeout', () => { socket.destroy(); resolve(false); }); + socket.on('error', (err) => { socket.destroy(); resolve(err.code === 'ECONNREFUSED'); }); + }); + } + + _startRecoveryPoller() { + if (this._recoveryInterval) return; + const match = this.url && this.url.match(/https?:\/\/([^/:]+)/); + if (!match) return; + const ip = match[1]; + // Jitter: stagger recovery pings 0-10s so N offline devices don't all hit the AP simultaneously + const jitter = Math.floor(Math.random() * 10000); + this._recoveryTimeout = setTimeout(() => { + if (this.__deleted) return; + this._recoveryInterval = setInterval(async () => { + if (this.__deleted) return this._stopRecoveryPoller(); + const online = await this._tcpPing(ip, 80, 1000); + if (online) { + this.log(`🌐 TCP ping ${ip} ok — resetting failure counter`); + this._consecutiveFailures = 0; + this._stopRecoveryPoller(); + } + }, 10000); + }, jitter); + } + + _stopRecoveryPoller() { + if (this._recoveryTimeout) { + clearTimeout(this._recoveryTimeout); + this._recoveryTimeout = null; + } + if (this._recoveryInterval) { + clearInterval(this._recoveryInterval); + this._recoveryInterval = null; + } + } + + 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._stopRecoveryPoller(); + 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'); + this._consecutiveFailures = 0; + this._consecutiveSuccesses = 0; + this._stopRecoveryPoller(); + this._skipPollsUntil = 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}`); + this._consecutiveFailures = 0; + this._consecutiveSuccesses = 0; + this._stopRecoveryPoller(); + this._skipPollsUntil = 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 { + const res = await fetchWithTimeout(`${this.url}/state`, { + agent: this.agent, + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }, 5000); + + 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' } + }, 5000); + + 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 }) + }, 5000); + + 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 }) + }, 5000); + + 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(); + this._skipPollsUntil = 0; + + // 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(); + + // Backoff on hard network errors (host gone / route down): skip next polls for 60s. + // Recovery poller + discovery callbacks reset _skipPollsUntil on recovery. + if (err && (err.code === 'EHOSTUNREACH' || err.code === 'ENETUNREACH')) { + this._skipPollsUntil = Date.now() + 60000; + } + + 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 >= 5 && timeSinceLastSuccess > 120000) { + const dr = this.getDiscoveryResult(); + if (dr && dr.address) { + const freshUrl = `http://${dr.address}:${dr.port}${dr.txt.path}`; + if (freshUrl !== this.url) { + this.log(`🌐 Discovery IP refresh: ${this.url} → ${freshUrl}`); + this.url = freshUrl; + this._consecutiveFailures = 0; + return; + } + } + 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; + this._startRecoveryPoller(); + } + 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}/5): ${err.message}`); + updateCapability(this, 'connection_error', `Retrying (${this._consecutiveFailures}/5): ${err.message}`).catch(this.error); + } + } + + /** + * GET /data + GET /state (with improved error handling) + */ + async onPoll() { + if (this.__deleted) return; + + // Backoff window after hard network error (EHOSTUNREACH etc.): skip until window passes + if (this._skipPollsUntil && Date.now() < this._skipPollsUntil) return; + + // ✅ CPU FIX: Guard against concurrent polls — setInterval fires regardless + // of whether the previous poll completed. Without this, a failing device + // (10s interval, 3×5s timeout = 17s per poll) piles up concurrent polls + // that exhaust the shared HTTP agent (maxSockets=4) and starve other devices. + if (this._pollRunning) return; + this._pollRunning = true; + + 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')); + this._pollRunning = false; + return; + } + } + + try { + + // ----------------------------- + // GET /data (with retry on timeout) + // ----------------------------- + let data; + + // ✅ CPU FIX: When device is already marked unavailable, don't retry — + // a single attempt is enough to detect recovery. Retries on an unreachable + // device waste 3×5s = 15s of socket time on the shared agent. + const maxRetries = this._isMarkedUnavailable ? 0 : 1; + let retries = maxRetries; + + while (retries >= 0) { + try { + const res = await fetchWithTimeout(`${this.url}/data`, { + agent: this.agent, + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }, 5000); + + 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 (${maxRetries-retries}/${maxRetries}): ${err.message}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + const offset = Number(this.getSetting('offset_socket')) || 0; + const watt = data.active_power_w + offset; + + this._updateApplianceState(watt); + + 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 > 0) { + 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' } + }, 5000); + + 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); + } + } finally { + this._pollRunning = false; + } +} + + + _updateApplianceState(watt) { + const settings = this.getSettings(); + if (!settings.appliance_detection_enabled) return; + const activeThreshold = Number(settings.active_threshold_w) || 50; + const standbyThreshold = Number(settings.standby_threshold_w) || 5; + const minActiveMinutes = Number(settings.min_active_minutes) || 5; + const triggerOnPowerOff = settings.trigger_on_poweroff !== false; + + const now = Date.now(); + const isActive = watt >= activeThreshold; + const isStandby = watt > 0 && watt < standbyThreshold; + const isPowerOff = watt <= 0; + + if (this._applianceState === 'idle') { + if (isActive) { + this._applianceState = 'active'; + this._applianceActiveStart = now; + this._debugLog(`Appliance active (${watt}W >= ${activeThreshold}W)`); + } + } else if (this._applianceState === 'active') { + if (isActive) return; + const activeMinutes = Math.round((now - this._applianceActiveStart) / 60000); + const longEnough = activeMinutes >= minActiveMinutes; + + if ((isStandby || (isPowerOff && triggerOnPowerOff)) && longEnough) { + this._applianceState = 'finished'; + this._applianceFinishedAt = now; + this._debugLog(`Appliance finished after ${activeMinutes}min (${watt}W)`); + this._triggerApplianceFinished(activeMinutes); + } else if ((isStandby || isPowerOff) && !longEnough) { + this._applianceState = 'idle'; + this._debugLog(`Appliance reset — too short (${activeMinutes}min < ${minActiveMinutes}min)`); + } + // middlezone (standby < watt < active): blijft 'active' + } else if (this._applianceState === 'finished') { + if (isActive) { + this._applianceState = 'active'; + this._applianceActiveStart = now; + this._debugLog(`Appliance re-armed (${watt}W)`); + } else if (now - this._applianceFinishedAt > 60000) { + this._applianceState = 'idle'; + } + } + } + + _triggerApplianceFinished(activeMinutes) { + if (!this._flowTriggerApplianceFinished) return; + this._flowTriggerApplianceFinished + .trigger(this, { active_minutes: activeMinutes }) + .catch(this.error); + } + + /** + * 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..bcb4f141 --- /dev/null +++ b/drivers/energy_socket/driver.compose.json @@ -0,0 +1,193 @@ +{ + "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 + } + ] + }, + { + "type": "group", + "label": { + "en": "Appliance detection", + "nl": "Apparaat detectie" + }, + "children": [ + { + "id": "appliance_detection_enabled", + "type": "checkbox", + "label": { + "en": "Enable appliance detection", + "nl": "Apparaat detectie inschakelen" + }, + "value": false + }, + { + "id": "active_threshold_w", + "type": "number", + "label": { + "en": "Active threshold (W)", + "nl": "Actief-drempel (W)" + }, + "value": 50, + "min": 1 + }, + { + "id": "standby_threshold_w", + "type": "number", + "label": { + "en": "Standby threshold (W)", + "nl": "Stand-by drempel (W)" + }, + "value": 5, + "min": 0 + }, + { + "id": "min_active_minutes", + "type": "number", + "label": { + "en": "Min active duration (min)", + "nl": "Min actieve duur (min)" + }, + "value": 5, + "min": 0 + }, + { + "id": "trigger_on_poweroff", + "type": "checkbox", + "label": { + "en": "Also trigger on power-off (0W)", + "nl": "Ook triggeren bij volledige uitschakeling (0W)" + }, + "value": true + } + ] + } + ], + "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.flow.compose.json b/drivers/energy_socket/driver.flow.compose.json new file mode 100644 index 00000000..11785413 --- /dev/null +++ b/drivers/energy_socket/driver.flow.compose.json @@ -0,0 +1,23 @@ +{ + "triggers": [ + { + "id": "appliance_finished", + "title": { + "en": "Appliance finished", + "nl": "Apparaat klaar" + }, + "args": [], + "tokens": [ + { + "name": "active_minutes", + "type": "number", + "title": { + "en": "Active duration (min)", + "nl": "Actieve duur (min)" + }, + "example": 45 + } + ] + } + ] +} 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..5eb9bd4b --- /dev/null +++ b/drivers/energy_v2/device.js @@ -0,0 +1,2804 @@ +/* + * 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'); + +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) { + const batteryCount = device._cacheGet?.('last_battery_state')?.battery_count; + const hasBattery = typeof batteryCount === 'number' ? batteryCount > 0 : true; + + if (!device.hasCapability(capability)) { + if (!hasBattery) return; + 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 a string, return as-is — but collapse predictive sub-types back to 'predictive' + if (typeof data === 'string') { + const s = data.trim(); + return s.startsWith('predictive_') ? 'predictive' : s; + } + + // 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'; + + if (mode === 'predictive') return 'predictive'; + + // 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'; +} + +// Returns detailed mode for history tracking — predictive sub-typed by permissions. +// Only used internally; never passed to capabilities (enum would reject it). +function detailedBatteryMode(data) { + if (typeof data !== 'object' || data === null) return normalizeBatteryMode(data); + const mode = typeof data.mode === 'string' ? data.mode.trim().toLowerCase() : null; + if (mode === 'predictive') { + const perms = Array.isArray(data.permissions) + ? [...data.permissions].map(p => p.toLowerCase()).sort().join(',') + : ''; + switch (perms) { + case 'charge_allowed,discharge_allowed': return 'predictive_zero'; + case 'charge_allowed': return 'predictive_charge'; + case 'discharge_allowed': return 'predictive_discharge'; + default: return 'predictive_standby'; + } + } + return normalizeBatteryMode(data); +} + + + + + + + + + +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() { + this.homey.app.bumpDeviceCount?.('energy_v2'); + const _memE = (label) => { try { const h = require('v8').getHeapStatistics(); this.log(`[MEM][energy_v2] ${label}: heap=${(h.used_heap_size/1048576).toFixed(1)}/${(h.total_heap_size/1048576).toFixed(1)}MB`); } catch(_){} }; + _memE('onInit-start'); + 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._liveMode = null; + + + 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(); + _memE('after-updateCapabilities'); + await this._registerCapabilityListeners(); + _memE('after-registerCapabilityListeners'); + await this._ensureBatteryCapabilities(); + _memE('after-ensureBatteryCapabilities'); + + // Sync mode setting from persisted capability — P1 capability is source of truth. + // Avoids stale advanced-settings panel after restart/update without sending hardware commands. + { + const capMode = this.getCapabilityValue('battery_group_charge_mode'); + if (capMode) { + this._liveMode = capMode; + const settingMode = this.getSetting('mode'); + if (settingMode !== capMode) { + this.log(`[energy_v2] Startup mode sync: setting ${settingMode} → ${capMode} (from capability)`); + this._queueSettingsPersist('__settings__', { mode: capMode }); + } + } + } + + // Register with baseload monitor + const app = this.homey.app; + if (!app.baseloadMonitor) { + app.baseloadMonitor = new BaseloadMonitor(this.homey); + } + _memE('after-baseloadMonitor'); + 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; + } + }); + +// ============================================================================ +// ACTION CARD 6: Set Battery to Predictive (HW Smart Charging) Mode +// ============================================================================ + +this.homey.flow + .getActionCard('set-battery-to-predictive-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 'predictive'; + } + device._lastBatteryModeChange = now; + device._cacheSet('last_commanded_mode', 'predictive'); + + device.log('ActionCard: Set Battery to Predictive (HW Smart Charging) Mode'); + + try { + const { wsManager, url, token } = device; + + // Prefer WebSocket + if (wsManager?.isConnected()) { + wsManager.setBatteryMode('predictive'); + device.log('Set mode to predictive via WebSocket'); + return 'predictive'; + } + + // HTTP fallback + const response = await api.setMode(url, token, 'predictive'); + 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 predictive via HTTP'); + return 'predictive'; + + } catch (error) { + device.error('Error set mode to predictive:', 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(); + } + _memE('onInit-done'); + + + // 🕒 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; + } + + const normalized = normalizeBatteryMode(modeResponse); + + // --- Trigger flow and sync settings if mode actually changed --- + const prev = this._cacheGet('last_battery_mode'); + if (normalized !== prev) { + this.flowTriggerBatteryMode(this, { mode: normalized }); + this._cacheSet('last_battery_mode', normalized); + } + + // --- Update capability --- + await updateCapability(this, 'battery_group_charge_mode', normalized); + + // --- Sync advanced settings so tile and settings panel stay consistent --- + if (this._liveMode !== normalized) { + this._liveMode = normalized; + this._queueSettingsPersist('__settings__', { 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 && realtimeCount === 0 && fallbackCount === 0) { + + 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 }); + } + } +} + + + + + + +_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 { + + // --- 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; + + // --- Normalize mode --- + const normalizedMode = normalizeBatteryMode(payload); + const lastBatteryMode = this._cacheGet('last_battery_mode'); + + // --- Expose detailed mode for battery-policy mode history --- + this._currentDetailedMode = detailedBatteryMode(payload); + + // --- Firmware fallback: to_full but power_w = 0 --- + if (normalizedMode === 'to_full') this.log(`[to_full] power_w=${payload.power_w ?? 'null'}`); + 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; + + payload.power_w = batteryCount * 800; + } + + // --- 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); + + // Batched persist — setSettings allocates ~30 MB V8 heap per call. Only + // queue when mode actually changes (_liveMode in-memory compare). + if (this._liveMode !== normalizedMode) { + this._liveMode = normalizedMode; + this._queueSettingsPersist('__settings__', { mode: normalizedMode }); + } + } + + // --- Update battery power capabilities --- + // ✅ CPU FIX: Run in parallel instead of 4 sequential awaits + // ✅ FIX: Only update max_consumption/max_production when field is present in WS payload. + // Some WS messages omit these fields; writing 0 causes the policy-engine fallback + // to incorrectly assume unitCount × 800 W discharge (e.g. 2400 W for 3 batteries) + // while the real firmware limit is 800 W. + const powerUpdates = [ + 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), + ]; + if (typeof payload.max_consumption_w === 'number') { + powerUpdates.push(this._setCapabilityValue('measure_power.battery_group_max_consumption_w', payload.max_consumption_w)); + } + if (typeof payload.max_production_w === 'number') { + powerUpdates.push(this._setCapabilityValue('measure_power.battery_group_max_production_w', payload.max_production_w)); + } + await Promise.allSettled(powerUpdates); + + // --- 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); + } + + + + // Batched settings persistence — homey.settings.set / setSettings allocates + // ~30 MB V8 heap per call. Spacing writes 8s apart lets GC reclaim each spike. + // Special key '__settings__' routes through setSettings (device-scoped). + _queueSettingsPersist(key, value) { + if (!this._settingsQueue) this._settingsQueue = new Map(); + this._settingsQueue.set(key, value); + if (this._settingsFlushTimer) return; + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + } + + _flushSettingsQueue() { + if (!this._settingsQueue || this._settingsQueue.size === 0) return; + + let heapMB = 0; + try { heapMB = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (heapMB > 40) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + return; + } + + const [key, value] = this._settingsQueue.entries().next().value; + this._settingsQueue.delete(key); + try { + if (key === '__settings__') { + this.setSettings(value).catch(err => this.error('Failed to persist settings:', err.message)); + } else { + this.homey.settings.set(key, value); + } + } catch (e) { + this.error(`Failed to persist ${key}:`, e.message); + } + if (this._settingsQueue.size > 0) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + } + } + + 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._settingsFlushTimer) { + this.homey.clearTimeout(this._settingsFlushTimer); + this._settingsFlushTimer = null; + } + if (this._settingsQueue) this._settingsQueue.clear(); + 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(); + } + } + +}; 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..29a35338 --- /dev/null +++ b/drivers/energy_v2/driver.flow.compose.json @@ -0,0 +1,295 @@ +{ + "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": [] + }, + { + "id": "set-battery-to-predictive-mode", + "title": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "titleFormatted": { + "en": "Set battery to Predictive (Smart Charging) mode", + "nl": "Zet batterij in Slim Laden modus (HomeWizard)" + }, + "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..f9a99bb7 --- /dev/null +++ b/drivers/energy_v2/driver.settings.compose.json @@ -0,0 +1,152 @@ +[ + { + "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": "predictive", + "label": { + "en": "Predictive (HW Smart Charging)", + "nl": "Slim laden (HomeWizard)" + } + } + ] + }, + { + "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..fcaf2b30 --- /dev/null +++ b/drivers/energylink/device.js @@ -0,0 +1,300 @@ +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); + +const debug = false; + +class HomeWizardEnergylink extends Homey.Device { + + async onInit() { + + this.homey.app.bumpDeviceCount?.('energylink'); + + 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..cfd2b2a3 --- /dev/null +++ b/drivers/heatlink/device.js @@ -0,0 +1,264 @@ +'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.homey.app.bumpDeviceCount?.('heatlink'); + + 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'); + this._overrideAt = Date.now(); + 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}`); + + this._overrideAt = null; + 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); + } + + let override = await this.getStoreValue('setTemperature'); + const overrideAge = this._overrideAt ? Date.now() - this._overrideAt : Infinity; + const OVERRIDE_GRACE_MS = 30000; + + // Only treat mismatch as "rejected" when the override has had time to propagate + // through the Heatlink gateway. Within grace, assume tte is still stale. + if (override > 0 && tte !== override && overrideAge >= OVERRIDE_GRACE_MS) { + this.log('Heatlink rejected override, clearing override flag'); + await this.setStoreValue('setTemperature', 0).catch(this.error); + this._overrideAt = null; + override = 0; + } + + if (override > 0) { + if (tte === override) { + // Heatlink accepted the override + promises.push(this.setCapabilityValue('target_temperature', tte).catch(this.error)); + } + // else: still within grace — leave UI as-is, don't overwrite with stale tte + } 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..16378afc --- /dev/null +++ b/drivers/kakusensors/device.js @@ -0,0 +1,167 @@ +'use strict'; + +const Homey = require('homey'); +const homewizard = require('../../includes/legacy/homewizard.js'); + +const debug = false; + +class HomeWizardKakusensors extends Homey.Device { + + async onInit() { + + this.homey.app.bumpDeviceCount?.('kakusensors'); + + 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..8901708c --- /dev/null +++ b/drivers/plugin_battery/device.js @@ -0,0 +1,1164 @@ +'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() { + this.homey.app.bumpDeviceCount?.('plugin_battery'); + this.homey.app.logMem?.('[plugin_battery] onInit-start'); + 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 = Math.max(settings.polling_interval || 10, 5); + // ✅ 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); + } + + this.homey.app.logMem?.('[plugin_battery] onInit-done'); + } + + 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._settingsFlushTimer) { + this.homey.clearTimeout(this._settingsFlushTimer); + this._settingsFlushTimer = null; + } + if (this._settingsQueue) this._settingsQueue.clear(); + + 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') || {}; + + // Compare on slow-changing fields only — exclude power_w (fluctuates constantly, + // realtime path already covers it) and updated_at (always changes). Writing + // every 60s × N batteries = ~30 MB heap spike per write, too expensive. + const prevInfo = group[batteryId]; + const dataChanged = !prevInfo + || prevInfo.soc_pct !== info.soc_pct + || prevInfo.cycles !== info.cycles + || prevInfo.capacity_kwh !== info.capacity_kwh; + + if (dataChanged) { + group[batteryId] = info; + this._queueSettingsPersist('pluginBatteryGroup', group); + } +} + +// Batched settings persistence — homey.settings.set allocates ~30 MB V8 heap +// per call. Spacing writes 8s apart lets GC reclaim the previous spike. +_queueSettingsPersist(key, value) { + if (!this._settingsQueue) this._settingsQueue = new Map(); + this._settingsQueue.set(key, value); + if (this._settingsFlushTimer) return; + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); +} + +_flushSettingsQueue() { + if (!this._settingsQueue || this._settingsQueue.size === 0) return; + + let heapMB = 0; + try { heapMB = require('v8').getHeapStatistics().used_heap_size / 1048576; } catch (_) {} + if (heapMB > 40) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + return; + } + + const [key, value] = this._settingsQueue.entries().next().value; + this._settingsQueue.delete(key); + try { + this.homey.settings.set(key, value); + } catch (e) { + this.error(`Failed to persist ${key}:`, e.message); + } + if (this._settingsQueue.size > 0) { + this._settingsFlushTimer = this.homey.setTimeout(() => { + this._settingsFlushTimer = null; + this._flushSettingsQueue(); + }, 8000); + } +} + + + + + + + /** + * 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 = Math.max(newInterval || newSettings.polling_interval || 10, 5); + 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 = Math.max(newInterval || newSettings.polling_interval || 10, 5); + + 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..6eef84f4 --- /dev/null +++ b/drivers/plugin_battery/driver.settings.compose.json @@ -0,0 +1,26 @@ +[ + { + "id": "polling_interval", + "type": "number", + "label": { "en": "Polling interval" }, + "value": 10, + "min": 5, + "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..e8087da9 --- /dev/null +++ b/drivers/rainmeter/device.js @@ -0,0 +1,193 @@ +'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() { + + this.homey.app.bumpDeviceCount?.('rainmeter'); + + // 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..edd27ec2 --- /dev/null +++ b/drivers/thermometer/device.js @@ -0,0 +1,200 @@ +'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() { + + this.homey.app.bumpDeviceCount?.('thermometer'); + + // 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..ce3740f5 --- /dev/null +++ b/drivers/watermeter/device.js @@ -0,0 +1,326 @@ +'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.homey.app.bumpDeviceCount?.('watermeter'); + + 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 }); + } + + if (settings.leak_flow_lpm == null) { + await this.setSettings({ leak_flow_lpm: 15 }); + } + + if (settings.leak_duration_minutes == null) { + await this.setSettings({ leak_duration_minutes: 60 }); + } + + this._leakFlowStart = null; + + 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', + 'alarm_water', + '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; + this._leakFlowStart = null; + + 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); + + // --- Leak detection --- + const leakFlowThreshold = settings.leak_flow_lpm ?? 15; + const leakDurationMs = (settings.leak_duration_minutes ?? 60) * 60 * 1000; + + if (data.active_liter_lpm >= leakFlowThreshold) { + if (this._leakFlowStart === null) { + this._leakFlowStart = Date.now(); + } else if (Date.now() - this._leakFlowStart >= leakDurationMs) { + if (!this.getCapabilityValue('alarm_water')) { + this._debugLog(`⚠️ Leak alarm: ${data.active_liter_lpm} L/min for ${Math.round((Date.now() - this._leakFlowStart) / 60000)} min`); + await updateCapability(this, 'alarm_water', true); + } + } + } else { + this._leakFlowStart = null; + if (this.getCapabilityValue('alarm_water')) { + this._debugLog('✅ Leak alarm cleared: flow stopped'); + await updateCapability(this, 'alarm_water', false); + } + } + + 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..7dcb6298 --- /dev/null +++ b/drivers/watermeter/driver.compose.json @@ -0,0 +1,147 @@ +{ + "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", + "alarm_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 + } + ] + }, + { + "type": "group", + "label": { + "en": "Leak detection", + "nl": "Lekdetectie" + }, + "children": [ + { + "id": "leak_flow_lpm", + "type": "number", + "label": { + "en": "Flow threshold (L/min)", + "nl": "Drempelwaarde (L/min)" + }, + "hint": { + "en": "Continuous flow above this value triggers leak detection. Default: 15 L/min.", + "nl": "Aanhoudende doorstroom boven deze waarde activeert lekdetectie. Standaard: 15 L/min." + }, + "value": 15 + }, + { + "id": "leak_duration_minutes", + "type": "number", + "label": { + "en": "Duration before alarm (minutes)", + "nl": "Duur voor alarm (minuten)" + }, + "hint": { + "en": "How many minutes of continuous flow before triggering the alarm. Default: 60.", + "nl": "Hoeveel minuten aanhoudende doorstroom voor het alarm. Standaard: 60." + }, + "value": 60 + } + ] + } + ], + "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..988b695f --- /dev/null +++ b/drivers/wattcher/device.js @@ -0,0 +1,111 @@ +'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() { + + this.homey.app.bumpDeviceCount?.('wattcher'); + + // 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..331bc878 --- /dev/null +++ b/includes/legacy/homewizard.js @@ -0,0 +1,504 @@ +'use strict'; + +const Homey = require('homey'); +const http = require('http'); +const net = require('net'); +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: 5, + cooldownMs: 60000 + }; + } + } + + 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 + }); + + console.log(`[HW] circuit breaker OPEN for ${device.settings?.homewizard_id || device.settings?.homewizard_ip || 'unknown'} (${c.failures} failures, cooldown ${c.cooldownMs / 1000}s)`); + device.deviceInstance?.setUnavailable('Connection lost').catch(() => {}); + startRecoveryPoller(device); + } + + } + + function tcpPing(ip, port, timeoutMs) { + return new Promise((resolve) => { + const socket = net.createConnection({ host: ip, port, timeout: timeoutMs }); + socket.on('connect', () => { socket.destroy(); resolve(true); }); + socket.on('timeout', () => { socket.destroy(); resolve(false); }); + socket.on('error', (err) => { + // ECONNREFUSED = device reachable, port closed — still online + socket.destroy(); + resolve(err.code === 'ECONNREFUSED'); + }); + }); + } + + function startRecoveryPoller(device) { + const c = device.circuit; + if (c.recoveryInterval) return; + const ip = device.settings?.homewizard_ip; + if (!ip) return; + + c.recoveryInterval = setInterval(async () => { + const online = await tcpPing(ip, 80, 1000); + if (online) { + console.log(`[HW] TCP ping ${ip} ok — circuit breaker reset`); + circuitBreakerSuccess(device); + } + }, 10000); + } + + function circuitBreakerSuccess(device) { + initCircuitBreaker(device); + const c = device.circuit; + if (c.recoveryInterval) { + clearInterval(c.recoveryInterval); + c.recoveryInterval = null; + } + c.failures = 0; + c.openUntil = 0; + device.deviceInstance?.setAvailable().catch(() => {}); + } + + 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), 20000); + } + + + 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) { + if (device.circuit.recoveryInterval) { + clearInterval(device.circuit.recoveryInterval); + device.circuit.recoveryInterval = null; + } + 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 + }); + console.log(`[HW] timeout after ${duration}ms (limit=${timeoutDuration}ms): ${uri_part}`); + 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..08f40129 --- /dev/null +++ b/includes/utils/baseloadMonitor.js @@ -0,0 +1,637 @@ +/* + * 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(); + if (this._saveTimer) { + clearTimeout(this._saveTimer); + this._saveTimer = null; + } + } + + 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;} + + _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() { + // Debounce 5min — homey.settings.set allocates ~30 MB V8 heap per call + // (framework-internal). Baseload state lives in-memory anyway; only this + // nightly/rare persistence exists for restart recovery. + if (this._saveTimer) clearTimeout(this._saveTimer); + this._saveTimer = setTimeout(() => { + this._saveTimer = null; + this.homey.settings.set('baseload_state',{ + nightHistory:this.nightHistory, + currentBaseload:this.currentBaseload, + deviceNotificationPrefs:Array.from(this.deviceNotificationPrefs.entries()), + invalidNightCounter:this.invalidNightCounter + }); + }, 5 * 60 * 1000); + } + + _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..50a16c50 --- /dev/null +++ b/includes/v2/Api.js @@ -0,0 +1,179 @@ +'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, + maxSockets: 6, + maxFreeSockets: 2, + }); + + + /** + * 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) { + await fetchJSON(`${url}/api/system/identify`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + } + }); + }; + + // ------------------------- + // 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) { + return fetchJSON(`${url}/api/batteries`, { + headers: { Authorization: `Bearer ${token}` } + }); + }; + + // ------------------------- + // 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; + case 'predictive': + body = { mode: 'predictive' }; + 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..6b74e459 --- /dev/null +++ b/includes/v2/Ws.js @@ -0,0 +1,846 @@ +/* + * 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; + case 'predictive': + payloadData = { mode: 'predictive' }; + 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..bb349e4c --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "checkJs": false, + "allowJs": true, + "noEmit": true, + "strict": false, // disables strict type checks + "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/chart-renderer.js b/lib/chart-renderer.js new file mode 100644 index 00000000..74d5d146 --- /dev/null +++ b/lib/chart-renderer.js @@ -0,0 +1,708 @@ +'use strict'; + +const https = require('https'); + +/** + * Stateless chart-rendering helpers extracted from device.js. + * Exposes: + * - Pure config builders: buildPlanningChartConfig, buildPvChartConfig, buildModeChartBody + * - HTTP helpers: streamPlanningChart, streamPvChart, streamModeChart + * - JS serializer: configToJs (preserves function callbacks for quickchart.io eval) + */ + +const QUICKCHART_HOST = 'quickchart.io'; +const QUICKCHART_PATH = '/chart'; + +function configToJs(val) { + if (val === null || val === undefined) return 'null'; + if (typeof val === 'function') return val.toString(); + if (typeof val === 'boolean' || typeof val === 'number') return String(val); + if (typeof val === 'string') return JSON.stringify(val); + if (Array.isArray(val)) return '[' + val.map(v => configToJs(v)).join(',') + ']'; + const entries = Object.entries(val) + .map(([k, v]) => `${JSON.stringify(k)}:${configToJs(v)}`) + .join(','); + return '{' + entries + '}'; +} + +function _postQuickChart(stream, body, errLabel) { + return new Promise((resolve, reject) => { + const req = https.request({ + hostname: QUICKCHART_HOST, + path: QUICKCHART_PATH, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + }, + }, (res) => { + if (res.statusCode !== 200) { + let errBody = ''; + res.on('data', chunk => { errBody += chunk; }); + res.on('end', () => { + const err = new Error(`quickchart.io ${errLabel} ${res.statusCode}: ${errBody.slice(0, 200)}`); + stream.destroy(err); + reject(err); + }); + return; + } + res.pipe(stream); + res.on('end', resolve); + res.on('error', (e) => { stream.destroy(e); reject(e); }); + }); + req.on('error', (e) => { stream.destroy(e); reject(e); }); + req.write(body); + req.end(); + }); +} + +async function streamPlanningChart(stream, compact) { + const chartCfg = buildPlanningChartConfig(compact); + if (!chartCfg) { stream.end(); return; } + const body = JSON.stringify({ + version: '4', + backgroundColor: '#1c1c1e', + width: 900, + height: 500, + chart: configToJs(chartCfg), + }); + return _postQuickChart(stream, body, 'planning'); +} + +async function streamPvChart(stream, pvData) { + const chartCfg = buildPvChartConfig(pvData); + if (!chartCfg) { stream.end(); return; } + const body = JSON.stringify({ + version: '4', + backgroundColor: '#1c1c1e', + width: 900, + height: 720, + chart: configToJs(chartCfg), + }); + return _postQuickChart(stream, body, 'PV'); +} + +async function streamModeChart(stream, body) { + if (!body) { stream.end(); return; } + return _postQuickChart(stream, body, 'mode'); +} + +/** + * Build 24h-mode-history chart body (15-min slots, stacked bar + SoC line). + * @param {Array} modeHistory - [{h: 'YYYY-MM-DDTHH:MM', m: {mode: count, ...}, soc: number}, ...] + * @returns {string|null} JSON body for quickchart.io, or null if no data + */ +function buildModeChartBody(modeHistory) { + if (!modeHistory?.length) return null; + + const currentSlotMs = Math.floor(Date.now() / (15 * 60000)) * (15 * 60000); + const slots = []; + for (let i = 95; i >= 0; i--) { + slots.push(new Date(currentSlotMs - i * 15 * 60000).toISOString().slice(0, 16)); + } + + const bySlot = {}; + for (const b of modeHistory) bySlot[b.h] = b; + + const MODE_ORDER = [ + 'to_full', 'zero_charge_only', 'pv_trickle', 'zero_discharge_only', 'zero', 'standby', + 'predictive_zero', 'predictive_charge', 'predictive_discharge', 'predictive_standby', + ]; + const MODE_COLORS = { + to_full: '#5f6fff', + zero_charge_only: '#8b9fff', + pv_trickle: '#4fa8d8', + zero_discharge_only: '#20F29B', + zero: '#FB923C', + standby: '#808080', + predictive_zero: 'rgba(251,146,60,0.55)', + predictive_charge: 'rgba(139,159,255,0.55)', + predictive_discharge: 'rgba(32,242,155,0.55)', + predictive_standby: 'rgba(128,128,128,0.55)', + }; + const MODE_LABELS = { + to_full: 'Laden', zero_charge_only: 'Laden PV', pv_trickle: 'Sprokkelen', + zero_discharge_only: 'Ontladen', + zero: 'Nul', standby: 'Standby', + predictive_zero: 'HW Nul', predictive_charge: 'HW Laden', + predictive_discharge: 'HW Ontladen', predictive_standby: 'HW Standby', + }; + + const labels = slots.map(s => { + if (!s.endsWith(':00')) return ''; + const h = new Date(s + ':00Z').toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }); + return h.padStart(2, '0') + 'u'; + }); + + const modeDatasets = MODE_ORDER.map(mode => { + const data = slots.map(slot => { + const m = bySlot[slot]?.m || {}; + const total = Object.values(m).reduce((s, v) => s + v, 0); + if (total === 0) return 0; + return Math.round((m[mode] || 0) / total * 100); + }); + if (!data.some(v => v > 0)) return null; + return { + label: MODE_LABELS[mode] || mode, + data, + backgroundColor: MODE_COLORS[mode] || '#888', + stack: 'modes', + borderWidth: 0, + }; + }).filter(Boolean); + + const socData = slots.map(slot => bySlot[slot]?.soc ?? null); + const hasSoC = socData.some(v => v !== null); + + const datasets = [ + ...modeDatasets, + ...(hasSoC ? [{ + label: 'SoC %', + data: socData, + type: 'line', + yAxisID: 'ySoC', + borderColor: '#ffffff', + backgroundColor: 'rgba(255,255,255,0.1)', + borderWidth: 2, + pointRadius: 0, + tension: 0.3, + fill: false, + spanGaps: true, + }] : []), + ]; + + const chartCfg = { + type: 'bar', + data: { labels, datasets }, + options: { + responsive: false, + plugins: { + legend: { labels: { color: '#ccc', font: { size: 13 }, boxWidth: 12 } }, + title: { + display: true, + text: 'Batterij modi — afgelopen 24 uur', + color: '#e0e0e0', + font: { size: 15 }, + }, + }, + scales: { + x: { + stacked: true, + ticks: { color: '#aaa', font: { size: 12 }, maxRotation: 0 }, + grid: { color: '#333' }, + barPercentage: 0.8, + categoryPercentage: 1.0, + }, + y: { + stacked: true, min: 0, max: 100, + ticks: { color: '#aaa', font: { size: 12 }, callback: (v) => v + '%' }, + grid: { color: '#333' }, + }, + ...(hasSoC ? { + ySoC: { + min: 0, max: 100, position: 'right', + ticks: { color: '#fff', font: { size: 12 }, callback: (v) => v + '%' }, + grid: { drawOnChartArea: false }, + }, + } : {}), + }, + }, + }; + + return JSON.stringify({ + version: '4', + backgroundColor: '#1c1c1e', + width: 900, + height: 500, + chart: chartCfg, + }); +} + +/** + * Build PV forecast-vs-actual chart config (today + tomorrow, OM/SC/blended/actual lines). + * @param {Object} pvData - { pvActual, pvForecast, pvForecastOM, pvForecastSC, pvForecastSCEffective, pvForecastDayStart, pvScDayStart, pvCapacityW } + * @returns {Object|null} chart config for quickchart.io, or null if no data + */ +function buildPvChartConfig(pvData) { + const { pvActual, pvForecast, pvForecastOM, pvForecastSC, pvForecastSCEffective, pvForecastDayStart, pvScDayStart, pvCapacityW } = pvData || {}; + const todayStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const pvHourly = (pvActual?.date === todayStr && Array.isArray(pvActual?.hourly)) ? pvActual.hourly : []; + + const forecastTomorrow = pvForecast?.[1] || {}; + + const omTodayFull = pvForecastOM?.[0] || {}; + + const scTodaySnap = (() => { + if (!pvScDayStart?.data || pvScDayStart.date !== todayStr) return null; + const sums = {}, cnts = {}; + for (const slot of pvScDayStart.data) { + const h = parseInt(new Date(slot.timestamp).toLocaleString('en-US', { + hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' + }), 10); + sums[h] = (sums[h] ?? 0) + slot.pvPowerW; + cnts[h] = (cnts[h] ?? 0) + 1; + } + const result = {}; + for (const h of Object.keys(sums)) result[parseInt(h)] = Math.round(sums[h] / cnts[h]); + return Object.keys(result).length > 0 ? result : null; + })(); + + const scTodayFull = scTodaySnap ?? pvForecastSC?.[0] ?? {}; + + const fLg = 22; + const fMd = 19; + const fSm = 17; + + const nowAmsHour = parseInt( + new Date().toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10 + ); + + const hasTomorrow = Object.keys(forecastTomorrow).length > 0; + + const hours = []; + for (let h = 0; h < 24; h++) hours.push(h); + if (hasTomorrow) for (let h = 0; h < 24; h++) hours.push(24 + h); + + const labels = hours.map((h, i) => { + const displayH = h % 24; + if (displayH === 0 && i > 0) return 'Morgen'; + if (displayH % 2 === 0) return String(displayH); + return ''; + }); + + const actualData = hours.map(h => { + if (h >= 24) return null; + if (h > nowAmsHour) return null; + return pvHourly[h] ?? null; + }); + + const forecastData = hours.map(h => { + if (h >= 24) return forecastTomorrow[h - 24] ?? 0; + if (h < nowAmsHour) { + const om = omTodayFull[h] ?? 0; + const sc = scTodayFull[h]; + return sc != null ? Math.round((om + sc) / 2) : om; + } + return (pvForecast?.[0]?.[h]) ?? 0; + }); + + const omToday = omTodayFull; + const omTomorrow = pvForecastOM?.[1] || {}; + const scEffToday = pvForecastSCEffective?.[0] ?? null; + const scEffTomorrow = pvForecastSCEffective?.[1] ?? null; + const scToday = scEffToday ? { ...scTodayFull, ...scEffToday } : scTodayFull; + const scTomorrow = scEffTomorrow ? { ...(pvForecastSC?.[1] || {}), ...scEffTomorrow } : (pvForecastSC?.[1] || {}); + const hasOM = Object.keys(omToday).length > 0 || Object.keys(omTomorrow).length > 0; + const hasSC = Object.keys(scToday).length > 0 || Object.keys(scTomorrow).length > 0; + + const omData = hasOM ? hours.map(h => { + if (h < 24) return omToday[h] ?? null; + return omTomorrow[h - 24] ?? null; + }) : null; + const scData = hasSC ? hours.map(h => { + if (h < 24) return scToday[h] ?? null; + return scTomorrow[h - 24] ?? null; + }) : null; + + const actualTodayKwh = pvHourly.reduce((sum, w) => sum + (w || 0), 0) / 1000; + const forecastTodayKwh = forecastData.slice(0, 24).reduce((sum, w) => sum + (w || 0), 0) / 1000; + const forecastTomorrowKwh = Object.values(forecastTomorrow).reduce((sum, w) => sum + (w || 0), 0) / 1000; + + const pvMax = Math.max( + ...pvHourly.filter(v => v != null).map(v => v), + ...forecastData.map(v => v || 0), + ...Object.values(forecastTomorrow).map(v => v || 0), + ...Object.values(omToday).map(v => v || 0), + ...Object.values(omTomorrow).map(v => v || 0), + ...Object.values(scToday).map(v => v || 0), + ...Object.values(scTomorrow).map(v => v || 0), + 1 + ); + const yMax = pvCapacityW > 0 ? Math.max(pvCapacityW, pvMax * 1.1) : pvMax * 1.2; + + const todayDate = new Date().toLocaleDateString('nl-NL', { + timeZone: 'Europe/Amsterdam', weekday: 'short', day: 'numeric', month: 'long' + }); + + const updTime = new Date().toLocaleTimeString('nl-NL', { + hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Amsterdam' + }); + + const titleParts = [`☀ PV Opwek ${todayDate}`]; + titleParts.push(`Werkelijk: ${actualTodayKwh.toFixed(1)} kWh`); + titleParts.push(`Verwacht: ${forecastTodayKwh.toFixed(1)} kWh`); + if (hasTomorrow) titleParts.push(`Morgen: ${forecastTomorrowKwh.toFixed(1)} kWh`); + + return { + type: 'line', + data: { + labels, + datasets: [ + { + label: 'PV Werkelijk', + data: actualData, + borderColor: '#FCD34D', + backgroundColor: 'transparent', + fill: false, + borderWidth: 4, + pointRadius: 0, + tension: 0.4, + spanGaps: false, + yAxisID: 'yPv', + }, + { + label: 'PV Verwachting', + data: forecastData, + borderColor: '#F97316', + backgroundColor: 'rgba(249,115,22,0.08)', + fill: true, + borderWidth: 3, + borderDash: [6, 3], + pointRadius: 0, + tension: 0.4, + spanGaps: false, + yAxisID: 'yPv', + }, + ...(omData ? [{ + label: 'Open-Meteo', + data: omData, + borderColor: '#60A5FA', + backgroundColor: 'transparent', + fill: false, + borderWidth: 2, + borderDash: [4, 4], + pointRadius: 0, + tension: 0.4, + spanGaps: true, + yAxisID: 'yPv', + }] : []), + ...(scData ? [{ + label: 'Solcast', + data: scData, + borderColor: '#34D399', + backgroundColor: 'transparent', + fill: false, + borderWidth: 2.5, + borderDash: [4, 4], + pointRadius: 0, + tension: 0.4, + spanGaps: true, + yAxisID: 'yPv', + }] : []), + ], + }, + options: { + responsive: false, + animation: false, + layout: { + padding: { top: 48, bottom: 10, left: 4, right: 4 }, + }, + plugins: { + legend: { + display: true, + labels: { + color: '#9a9a9a', + font: { size: fLg }, + padding: 12, + usePointStyle: true, + }, + }, + title: { + display: true, + text: titleParts[0] + ' | ' + updTime, + color: '#FCD34D', + font: { size: fLg, weight: 'bold' }, + padding: { bottom: 4 }, + }, + subtitle: { + display: true, + text: titleParts.slice(1).join(' | '), + color: '#cccccc', + font: { size: fLg }, + padding: { bottom: 8 }, + }, + }, + scales: { + x: { + ticks: { color: '#9a9a9a', font: { size: fMd }, maxRotation: 0, autoSkip: false }, + grid: { color: '#2a2a2a' }, + }, + yPv: { + position: 'left', + min: 0, + max: yMax, + ticks: { + color: '#FCD34D', + font: { size: fLg, weight: 'bold' }, + callback: function(v) { return v >= 1000 ? (v / 1000).toFixed(1) + 'kW' : v + 'W'; }, + }, + grid: { color: '#2a2a2a' }, + title: { display: true, text: 'Vermogen', color: '#FCD34D', font: { size: fMd } }, + }, + }, + }, + }; +} + +/** + * Build planning chart config (price bars + SoC line + PV area, today or tomorrow). + * @param {Object} compact - { slots, currentSoc, currentMode, updatedAt, pvCapacityW } + * @returns {Object|null} chart config for quickchart.io, or null if no slots + */ +function buildPlanningChartConfig(compact) { + const slots = compact?.slots || []; + const now = Date.now(); + + const shown = slots.slice(0, 96); + if (shown.length === 0) return null; + + const fLg = 20; + const fMd = 18; + const fSm = 16; + + const MODE_COLORS = { + to_full: '#5f6fff', + zero_charge_only: '#8b9fff', + pv_trickle: '#4fa8d8', + zero_discharge_only: '#20F29B', + zero: '#FB923C', + standby: '#808080', + predictive: '#02DACE', + past: '#aaaaaa', + }; + + const labels = shown.map(s => { + const min = Math.floor((s.ts % 3600000) / 60000); + if (min !== 0) return ''; + if (s.hour === 0) { + const d = new Date(s.ts); + const day = d.toLocaleString('nl-NL', { timeZone: 'Europe/Amsterdam', weekday: 'short', day: 'numeric', month: 'numeric' }); + return day; + } + return String(s.hour); + }); + const prices = shown.map(s => s.price != null ? Math.round(s.price * 1000) / 1000 : null); + const rawSocs = shown.map(s => s.soc != null ? s.soc : null); + const socs = rawSocs.slice(); + for (let i = 0; i < socs.length; i++) { + if (socs[i] != null) continue; + if (shown[i].ts >= now) continue; + let left = -1, right = -1; + for (let l = i - 1; l >= 0; l--) { if (socs[l] != null) { left = l; break; } } + for (let r = i + 1; r < socs.length; r++) { if (socs[r] != null) { right = r; break; } } + if (left >= 0 && right >= 0) { + socs[i] = Math.round(socs[left] + (socs[right] - socs[left]) * (i - left) / (right - left)); + } else if (left >= 0) { + socs[i] = socs[left]; + } else if (right >= 0) { + socs[i] = socs[right]; + } + } + const socActual = socs.map((v, i) => shown[i].ts < now ? v : null); + const socForecast = socs.map((v, i) => { + if (shown[i].ts >= now) return v; + if (i + 1 < shown.length && shown[i + 1].ts >= now) return v; + return null; + }); + + const pvActual = shown.map(s => s.ts < now ? Math.round((s.pvW || 0) / 10) * 10 : null); + const pvForecast = shown.map((s, i) => { + if (s.ts >= now) return Math.round((s.pvW || 0) / 10) * 10; + if (i + 1 < shown.length && shown[i + 1].ts >= now) return Math.round((s.pvW || 0) / 10) * 10; + return null; + }); + const pvMax = Math.max(...shown.map(s => Math.round((s.pvW || 0) / 10) * 10), 1); + + const barColors = shown.map(s => { + const base = MODE_COLORS[s.mode] || '#808080'; + return base + (s.ts < now ? '77' : 'CC'); + }); + + const modesPresent = [...new Set(shown.map(s => s.mode))].filter(m => m !== 'past'); + + const MODE_LABELS = { + to_full: 'Laden', zero_charge_only: 'PV laden', pv_trickle: 'Sprokkelen', + zero_discharge_only: 'Ontladen', + zero: 'Nul', standby: 'Standby', predictive: 'Slim laden', + }; + + const socLine = compact?.currentSoc != null ? `${Math.round(compact.currentSoc)}%` : '-'; + const modeLine = MODE_LABELS[compact?.currentMode] || compact?.currentMode || 'standby'; + + const firstTs = shown.find(s => s.mode !== 'past')?.ts ?? shown[0]?.ts; + const today = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }); + const slotDay = firstTs + ? new Date(firstTs).toLocaleDateString('en-CA', { timeZone: 'Europe/Amsterdam' }) + : today; + const dayLabel = slotDay === today + ? 'Vandaag' + : new Date(firstTs).toLocaleDateString('nl-NL', { timeZone: 'Europe/Amsterdam', weekday: 'long', day: 'numeric', month: 'long' }); + const updLine = compact?.updatedAt + ? new Date(compact.updatedAt).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Amsterdam' }) + : ''; + + const modeDatasets = modesPresent.map(mode => ({ + type: 'bar', + label: MODE_LABELS[mode] || mode, + data: [], + backgroundColor: (MODE_COLORS[mode] || '#808080') + 'CC', + yAxisID: 'yPrice', + order: 99, + })); + + return { + type: 'bar', + data: { + labels, + datasets: [ + { + label: '_prijs', + data: prices, + backgroundColor: barColors, + borderColor: barColors, + borderWidth: 0, + yAxisID: 'yPrice', + order: 3, + }, + { + type: 'line', + label: 'SoC (%)', + data: socActual, + borderColor: 'rgba(255,255,255,0.85)', + backgroundColor: 'transparent', + borderWidth: 2, + pointRadius: 0, + pointStyle: 'line', + yAxisID: 'ySoc', + order: 1, + spanGaps: true, + }, + { + type: 'line', + label: '_soc_forecast', + data: socForecast, + borderColor: 'rgba(255,255,255,0.85)', + backgroundColor: 'transparent', + borderWidth: 2, + borderDash: [5, 4], + pointRadius: 0, + pointStyle: 'line', + yAxisID: 'ySoc', + order: 1, + spanGaps: true, + }, + { + type: 'line', + label: 'PV', + data: pvActual, + borderColor: '#FCD34D', + backgroundColor: 'rgba(252,211,77,0.15)', + fill: true, + borderWidth: 2, + pointRadius: 0, + pointStyle: 'line', + tension: 0.4, + spanGaps: false, + yAxisID: 'yPv', + order: 2, + }, + { + type: 'line', + label: '_pv_forecast', + data: pvForecast, + borderColor: '#FCD34D', + backgroundColor: 'rgba(252,211,77,0.08)', + fill: true, + borderWidth: 1.5, + borderDash: [4, 3], + pointRadius: 0, + pointStyle: 'line', + tension: 0.4, + spanGaps: false, + yAxisID: 'yPv', + order: 2, + }, + ...modeDatasets, + ], + }, + options: { + responsive: false, + animation: false, + layout: { + padding: { top: 12, bottom: 16, left: 4, right: 4 }, + }, + plugins: { + legend: { + display: true, + labels: { + color: '#9a9a9a', + font: { size: fLg }, + padding: 14, + usePointStyle: true, + filter: function(item) { return !item.text.startsWith('_'); }, + }, + }, + title: { + display: true, + text: `Batterij ${dayLabel} | SoC: ${socLine} | Nu: ${modeLine} | ${updLine}`, + color: '#cccccc', + font: { size: fLg }, + padding: { bottom: 2 }, + }, + subtitle: { + display: true, + text: `☀ PV max: ${pvMax >= 1000 ? `${(pvMax/1000).toFixed(1)}kW` : `${pvMax}W`}`, + color: '#FCD34D', + font: { size: fSm, weight: 'bold' }, + padding: { bottom: 6 }, + }, + }, + scales: { + x: { + ticks: { color: '#9a9a9a', font: { size: fMd }, maxRotation: 0, autoSkip: false }, + grid: { color: '#2a2a2a' }, + }, + yPrice: { + position: 'left', + ticks: { + color: '#cccccc', + font: { size: fLg, weight: 'bold' }, + callback: (v) => v.toFixed(2), + }, + grid: { color: '#2a2a2a' }, + title: { display: true, text: 'EUR/kWh', color: '#9a9a9a', font: { size: fSm } }, + }, + ySoc: { + position: 'right', + min: 0, + max: 100, + ticks: { + color: 'rgba(255,255,255,0.85)', + font: { size: fLg, weight: 'bold' }, + callback: (v) => `${v}%`, + }, + grid: { drawOnChartArea: false }, + title: { display: true, text: 'SoC', color: 'rgba(255,255,255,0.6)', font: { size: fSm } }, + }, + yPv: { + display: false, + position: 'right', + min: 0, + max: compact?.pvCapacityW > 0 ? compact.pvCapacityW : pvMax * 1.2, + ticks: { display: false }, + grid: { drawOnChartArea: false }, + }, + }, + }, + }; +} + +module.exports = { + configToJs, + buildPlanningChartConfig, + buildPvChartConfig, + buildModeChartBody, + streamPlanningChart, + streamPvChart, + streamModeChart, +}; diff --git a/lib/efficiency-estimator.js b/lib/efficiency-estimator.js new file mode 100644 index 00000000..8e9daf5c --- /dev/null +++ b/lib/efficiency-estimator.js @@ -0,0 +1,365 @@ +'use strict'; + +class EfficiencyEstimator { + constructor(homey) { + this.homey = homey; + + this.state = this.homey.settings.get('efficiency_state') || { + efficiency: 0.75, + lastTimestamp: null, + lastSoc: null, + lastPowerDirection: null, + // Current charge session accumulator + sessionChargeKwh: 0, + sessionChargePowerSum: 0, + sessionChargePowerSamples: 0, + sessionChargeModeVotes: {}, + // Pending charge session (paired with the upcoming discharge session) + pendingChargeKwh: 0, + pendingChargePowerSum: 0, + pendingChargePowerSamples: 0, + pendingChargeMode: null, + // Current discharge session accumulator + sessionDischargeKwh: 0, + sessionDischargePowerSum: 0, + sessionDischargePowerSamples: 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 power = battery.battery_power ?? 0; + const soc = battery.stateOfCharge ?? null; + + if (Math.abs(power) <= 100) return; // filter inverter standby draw + + // Reset session counters on SoC=0 but preserve learned efficiency + if (soc === 0 && this.state.lastSoc > 2) { + // Evaluate cycle before clearing — battery drained to 0% without a charge transition + const pendingCharge = this.state.pendingChargeKwh || 0; + const sessionDischarge = this.state.sessionDischargeKwh || 0; + if (pendingCharge >= 0.3 && sessionDischarge >= 0.3) { + const newEff = sessionDischarge / pendingCharge; + if (newEff >= 0.70 && newEff <= 0.97) { + const oldEff = this.state.efficiency; + this.state.efficiency = (oldEff * 0.95) + (newEff * 0.05); + const avgChargePower = this.state.pendingChargePowerSamples > 0 + ? Math.round(this.state.pendingChargePowerSum / this.state.pendingChargePowerSamples) : 0; + const avgDischargePower = this.state.sessionDischargePowerSamples > 0 + ? Math.round(this.state.sessionDischargePowerSum / this.state.sessionDischargePowerSamples) : 0; + this.homey.log( + `[Efficiency] ✅ SoC=0% cycle: ` + + `charged=${(pendingCharge * 1000).toFixed(0)}Wh @ avg ${avgChargePower}W, ` + + `discharged=${(sessionDischarge * 1000).toFixed(0)}Wh @ avg ${avgDischargePower}W, ` + + `measured=${(newEff * 100).toFixed(1)}%, ` + + `learned RTE: ${(oldEff * 100).toFixed(1)}% → ${(this.state.efficiency * 100).toFixed(1)}%` + ); + this.state.cycles = this.state.cycles || []; + this.state.cycles.push({ + rte: newEff, + avgChargePower, + avgDischargePower, + chargedWh: Math.round(pendingCharge * 1000), + mode: this.state.pendingChargeMode || null, + month: new Date().getMonth() + 1, + ts: Date.now() + }); + if (this.state.cycles.length > 60) this.state.cycles = this.state.cycles.slice(-60); + } else { + this.homey.log( + `[Efficiency] ⚠️ SoC=0% cycle RTE ${(newEff * 100).toFixed(1)}% out of range [70–97%] → discarding` + ); + } + } + this.homey.log( + `[Efficiency] SoC dropped to 0% (from ${this.state.lastSoc}%) → ` + + `clearing session counters (preserving learned RTE ${(this.state.efficiency * 100).toFixed(1)}%)` + ); + this.state.sessionChargeKwh = 0; + this.state.sessionChargePowerSum = 0; + this.state.sessionChargePowerSamples = 0; + this.state.sessionChargeModeVotes = {}; + this.state.pendingChargeKwh = 0; + this.state.pendingChargePowerSum = 0; + this.state.pendingChargePowerSamples = 0; + this.state.pendingChargeMode = null; + this.state.sessionDischargeKwh = 0; + this.state.sessionDischargePowerSum = 0; + this.state.sessionDischargePowerSamples = 0; + this.state.lastPowerDirection = null; + this.state.lastSoc = soc; + this.save(); + return; + } + + const currentDirection = power > 100 ? 'charge' : 'discharge'; + const prevDirection = this.state.lastPowerDirection; + + // On direction transition: save pending charge or evaluate RTE + if (prevDirection && currentDirection !== prevDirection) { + if (prevDirection === 'charge' && currentDirection === 'discharge') { + // Charge session ended — save as pending for the upcoming discharge session + const votes = this.state.sessionChargeModeVotes || {}; + const dominantMode = Object.keys(votes).sort((a, b) => votes[b] - votes[a])[0] || null; + this.state.pendingChargeKwh = this.state.sessionChargeKwh || 0; + this.state.pendingChargePowerSum = this.state.sessionChargePowerSum || 0; + this.state.pendingChargePowerSamples = this.state.sessionChargePowerSamples || 0; + this.state.pendingChargeMode = dominantMode; + this.homey.log( + `[Efficiency] 🔄 Charge→Discharge: pending=${(this.state.pendingChargeKwh * 1000).toFixed(0)}Wh mode=${dominantMode}` + ); + // Reset session counters for the discharge session + this.state.sessionChargeKwh = 0; + this.state.sessionChargePowerSum = 0; + this.state.sessionChargePowerSamples = 0; + this.state.sessionChargeModeVotes = {}; + this.state.sessionDischargeKwh = 0; + this.state.sessionDischargePowerSum = 0; + this.state.sessionDischargePowerSamples = 0; + + } else if (prevDirection === 'discharge' && currentDirection === 'charge') { + // Discharge session ended — measure RTE from pending charge vs actual discharge + const pendingCharge = this.state.pendingChargeKwh || 0; + const sessionDischarge = this.state.sessionDischargeKwh || 0; + + if (pendingCharge >= 0.3 && sessionDischarge >= 0.3) { + const newEff = sessionDischarge / pendingCharge; + const avgChargePower = this.state.pendingChargePowerSamples > 0 + ? Math.round(this.state.pendingChargePowerSum / this.state.pendingChargePowerSamples) : 0; + const avgDischargePower = this.state.sessionDischargePowerSamples > 0 + ? Math.round(this.state.sessionDischargePowerSum / this.state.sessionDischargePowerSamples) : 0; + + if (newEff >= 0.70 && newEff <= 0.97) { + const oldEff = this.state.efficiency; + this.state.efficiency = (oldEff * 0.95) + (newEff * 0.05); + + this.homey.log( + `[Efficiency] ✅ Session cycle: ` + + `charged=${(pendingCharge * 1000).toFixed(0)}Wh @ avg ${avgChargePower}W, ` + + `discharged=${(sessionDischarge * 1000).toFixed(0)}Wh @ avg ${avgDischargePower}W, ` + + `measured=${(newEff * 100).toFixed(1)}%, ` + + `learned RTE: ${(oldEff * 100).toFixed(1)}% → ${(this.state.efficiency * 100).toFixed(1)}%` + ); + + this.state.cycles = this.state.cycles || []; + this.state.cycles.push({ + rte: newEff, + avgChargePower, + avgDischargePower, + chargedWh: Math.round(pendingCharge * 1000), + mode: this.state.pendingChargeMode || null, + month: new Date().getMonth() + 1, + ts: Date.now() + }); + if (this.state.cycles.length > 60) this.state.cycles = this.state.cycles.slice(-60); + + 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}`); + } + } else { + this.homey.log( + `[Efficiency] ⚠️ Session RTE ${(newEff * 100).toFixed(1)}% out of range [70–97%] → discarding ` + + `(charged=${(pendingCharge * 1000).toFixed(0)}Wh, discharged=${(sessionDischarge * 1000).toFixed(0)}Wh)` + ); + } + } + + // Reset pending + start fresh charge session + this.state.pendingChargeKwh = 0; + this.state.pendingChargePowerSum = 0; + this.state.pendingChargePowerSamples = 0; + this.state.pendingChargeMode = null; + this.state.sessionChargeKwh = 0; + this.state.sessionChargePowerSum = 0; + this.state.sessionChargePowerSamples = 0; + this.state.sessionChargeModeVotes = {}; + this.save(); + } + } + + this.state.lastPowerDirection = currentDirection; + + // Accumulate session counters + let accumulated = false; + if (power > 100) { + const chargeKw = power / 1000; + this.state.sessionChargeKwh = (this.state.sessionChargeKwh || 0) + chargeKw * dtHours; + this.state.sessionChargePowerSum = (this.state.sessionChargePowerSum || 0) + power; + this.state.sessionChargePowerSamples = (this.state.sessionChargePowerSamples || 0) + 1; + if (activeMode) { + this.state.sessionChargeModeVotes = this.state.sessionChargeModeVotes || {}; + this.state.sessionChargeModeVotes[activeMode] = (this.state.sessionChargeModeVotes[activeMode] || 0) + chargeKw * dtHours; + } + accumulated = true; + } else if (power < -100) { + const dischargeKw = Math.abs(power) / 1000; + this.state.sessionDischargeKwh = (this.state.sessionDischargeKwh || 0) + dischargeKw * dtHours; + this.state.sessionDischargePowerSum = (this.state.sessionDischargePowerSum || 0) + Math.abs(power); + this.state.sessionDischargePowerSamples = (this.state.sessionDischargePowerSamples || 0) + 1; + accumulated = true; + } + + // Persist periodically (~1 min cadence at 15s polling interval) + if (accumulated) { + this._saveCounter = (this._saveCounter || 0) + 1; + if (this._saveCounter % 4 === 0) this.save(); + } + + this.state.lastSoc = soc; + } + + getEfficiency() { + return this.state.efficiency ?? 0.75; + } + + /** + * 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(configuredEff = 0.75) { + this.homey.log( + `[Efficiency] Manual reset: ${(this.state.efficiency * 100).toFixed(1)}% → ${(configuredEff * 100).toFixed(0)}%` + ); + this.state.sessionChargeKwh = 0; + this.state.sessionChargePowerSum = 0; + this.state.sessionChargePowerSamples = 0; + this.state.sessionChargeModeVotes = {}; + this.state.pendingChargeKwh = 0; + this.state.pendingChargePowerSum = 0; + this.state.pendingChargePowerSamples = 0; + this.state.pendingChargeMode = null; + this.state.sessionDischargeKwh = 0; + this.state.sessionDischargePowerSum = 0; + this.state.sessionDischargePowerSamples = 0; + this.state.lastPowerDirection = null; + this.state.efficiency = configuredEff; + this.save(); + } + /** + * Analyse of lager laadvermogen betere RTE geeft. + * Geeft inzicht per vermogensbucket en seizoen. + */ + getCycleCount() { + return (this.state.cycles || []).length; + } + + 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/explainability-engine.js b/lib/explainability-engine.js new file mode 100644 index 00000000..005ae6fc --- /dev/null +++ b/lib/explainability-engine.js @@ -0,0 +1,1450 @@ +'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; + + // DP-primary path: use DP decision context directly + if (inputs?.dpDecision) { + const summary = this._generateDpSummary(recommendation, inputs); + const shortSummary = this._generateDpShortSummary(recommendation, inputs); + return { + recommendation: hwMode || policyMode, + policyMode, + hwMode, + confidence: recommendation.confidence, + reasons: this._buildDpReasons(recommendation, inputs), + warnings: [], + scores: scores || { charge: 0, discharge: 0, preserve: 0 }, + summary, + shortSummary, + timestamp: new Date().toISOString(), + batteryCost: inputs.batteryCost, + batteryEfficiency: inputs.batteryEfficiency, + effectivePrice: inputs.effectivePrice ?? inputs.tariff?.currentPrice ?? 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', pv_trickle: '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) + + // ── DP-primary summary (full) ───────────────────────────────────────────── + _generateDpSummary(recommendation, inputs) { + const dp = inputs.dpDecision; + const action = dp.finalAction; + const exception = dp.exception; + const price = inputs.effectivePrice ?? inputs.tariff?.currentPrice ?? null; + const breakEven = dp.breakEven; + const soc = inputs.battery?.stateOfCharge ?? 50; + const minSoc = inputs.settings?.min_soc ?? 0; + const maxSoc = inputs.settings?.max_soc ?? 95; + + const priceStr = price != null ? `€${price.toFixed(3)}` : null; + const beStr = breakEven != null ? `€${breakEven.toFixed(3)}` : null; + const futStr = dp.maxFuturePrice != null ? `€${dp.maxFuturePrice.toFixed(3)}` : null; + + if (exception === 'no_optimizer') { + return 'Advies: bewaren. Reden: geen planningsdata — wacht op eerste optimalisatierun.'; + } + if (exception === 'soc_too_low') { + return `Advies: bewaren. Reden: SoC ${soc}% is gelijk aan of lager dan minimum (${minSoc}%) — ontladen geblokkeerd.`; + } + if (exception === 'soc_full') { + return `Advies: bewaren. Reden: SoC ${soc}% is vol (max ${maxSoc}%) — opladen niet nodig.`; + } + if (exception === 'delay_charge') { + return `Advies: stand-by. Reden: PV exporteert nu naar net${priceStr ? ` (${priceStr})` : ''} — laden uitgesteld naar goedkoper moment.`; + } + if (exception === 'peak_shaving') { + const load = inputs.batteryLimits?.currentLoad; + const loadStr = load != null ? `${Math.round(load)}W` : null; + return `Advies: ontladen. Reden: piekverbruik${loadStr ? ` (${loadStr})` : ''} overschrijdt drempelwaarde — batterij dekt huislast.`; + } + + // DP followed, no exception + if (action === 'discharge') { + let parts = [`DP gepland: ontladen`]; + if (priceStr) parts.push(`prijs ${priceStr}`); + if (beStr) parts.push(`boven break-even ${beStr}`); + return `Advies: ontladen. Reden: ${parts.join(', ')}.`; + } + if (action === 'charge') { + if (price != null && price < 0) { + return `Advies: opladen. Reden: negatieve prijs ${priceStr} — laden levert geld op, terugleveren kost geld.`; + } + let parts = [`DP gepland: opladen`]; + if (priceStr) parts.push(`goedkope stroom ${priceStr}`); + return `Advies: opladen. Reden: ${parts.join(', ')}.`; + } + // preserve + if (dp.pvExportWins) { + const exportStr = priceStr ? ` (${priceStr}/kWh)` : ''; + return `Advies: bewaren. Reden: PV-export${exportStr} rendeert meer dan opslaan — batterij wordt gevuld via zon.`; + } + let reason = 'DP gepland: bewaren'; + if (futStr && priceStr && dp.maxFuturePrice > (price ?? 0) * 1.05) { + if (soc <= minSoc + 5) { + reason = `Wacht op goedkopere laadkans — hoge prijs verwacht ${futStr} vs nu ${priceStr}`; + } else { + reason += ` — hogere prijs verwacht ${futStr} vs nu ${priceStr}`; + } + } + return `Advies: bewaren. Reden: ${reason}.`; + } + + // ── DP-primary short summary (capability display) ───────────────────────── + _generateDpShortSummary(recommendation, inputs) { + const hw = recommendation.hwMode; + const dp = inputs.dpDecision; + const price = inputs.effectivePrice ?? inputs.tariff?.currentPrice ?? null; + const soc = inputs.battery?.stateOfCharge; + const breakEven = dp.breakEven; + + const HW_NL = { + zero_charge_only: 'Zon‑laden', + pv_trickle: 'Sprokkelen', + zero_discharge_only: 'Ontladen', + to_full: 'Vol‑laden', + standby: 'Stand‑by', + zero: 'Net‑0', + }; + const label = HW_NL[hw] || hw?.toUpperCase() || '?'; + const socPart = typeof soc === 'number' ? `${soc}%` : null; + + const exc = dp.exception; + let reasonPart = null; + if (exc === 'no_optimizer') reasonPart = 'geen planning'; + else if (exc === 'soc_too_low') reasonPart = 'SoC te laag'; + else if (exc === 'soc_full') reasonPart = 'vol'; + else if (exc === 'delay_charge') reasonPart = 'PV→net'; + else if (exc === 'peak_shaving') reasonPart = 'piek'; + else if (dp.pvExportWins) reasonPart = 'export>opslaan'; + else if (dp.finalAction === 'preserve' && soc <= 10 && hw !== 'zero_charge_only' && hw !== 'to_full' && hw !== 'pv_trickle') { + reasonPart = 'laden uitgesteld'; + } else if (dp.finalAction === 'preserve' && dp.maxFuturePrice && price && hw !== 'zero_charge_only' && hw !== 'to_full' && hw !== 'pv_trickle') { + reasonPart = `wacht op €${dp.maxFuturePrice.toFixed(3)}`; + } else if (dp.finalAction === 'discharge' && breakEven != null && price != null) { + const margin = price - breakEven; + reasonPart = `${margin >= 0 ? '+' : ''}€${margin.toFixed(3)}/kWh`; + } + + return [label, socPart, reasonPart].filter(Boolean).join(' - '); + } + + _buildDpReasons(recommendation, inputs) { + const dp = inputs.dpDecision; + const exception = dp.exception; + const action = dp.finalAction; + const price = inputs.effectivePrice ?? inputs.tariff?.currentPrice ?? null; + const breakEven = dp.breakEven; + const soc = inputs.battery?.stateOfCharge ?? 50; + const minSoc = inputs.settings?.min_soc ?? 0; + const maxSoc = inputs.settings?.max_soc ?? 95; + const priceStr = price != null ? `€${price.toFixed(3)}` : null; + const beStr = breakEven != null ? `€${breakEven.toFixed(3)}` : null; + const futStr = dp.maxFuturePrice != null ? `€${dp.maxFuturePrice.toFixed(3)}` : null; + + const reasons = []; + + if (exception === 'no_optimizer') { + reasons.push({ impact: 'medium', category: 'strategy', supportedMode: 'preserve', icon: '⏳', text: 'Geen planningsdata — wacht op eerste optimalisatierun.' }); + } else if (exception === 'soc_too_low') { + reasons.push({ impact: 'critical', category: 'battery', supportedMode: 'preserve', icon: '🔋', text: `SoC ${soc}% is gelijk aan of lager dan minimum (${minSoc}%) — ontladen geblokkeerd.` }); + } else if (exception === 'soc_full') { + reasons.push({ impact: 'high', category: 'battery', supportedMode: 'preserve', icon: '🔋', text: `SoC ${soc}% is vol (max ${maxSoc}%) — opladen niet nodig.` }); + } else if (exception === 'delay_charge') { + reasons.push({ impact: 'high', category: 'pv', supportedMode: 'preserve', icon: '⏸️', text: `PV exporteert nu naar net${priceStr ? ` (${priceStr})` : ''} — laden uitgesteld.` }); + } else if (exception === 'peak_shaving') { + const load = inputs.batteryLimits?.currentLoad; + const loadStr = load != null ? `${Math.round(load)}W` : null; + reasons.push({ impact: 'high', category: 'load', supportedMode: 'discharge', icon: '⚡', text: `Piekverbruik${loadStr ? ` ${loadStr}` : ''} — batterij dekt huislast.` }); + } else if (action === 'discharge') { + let text = `DP gepland: ontladen`; + if (priceStr) text += `, prijs ${priceStr}`; + if (beStr) text += `, break-even ${beStr}`; + reasons.push({ impact: 'critical', category: 'strategy', supportedMode: 'discharge', icon: '🧠', text }); + } else if (action === 'charge') { + const text = (price != null && price < 0) + ? `DP gepland: negatieve prijs ${priceStr} — laden levert geld op, terugleveren kost geld` + : `DP gepland: opladen${priceStr ? `, goedkope stroom ${priceStr}` : ''}`; + reasons.push({ impact: 'critical', category: 'strategy', supportedMode: 'charge', icon: '🧠', text }); + } else { + let text = `DP gepland: bewaren`; + if (futStr && priceStr && dp.maxFuturePrice > (price ?? 0) * 1.05) { + if (soc <= minSoc + 5) { + text = `DP gepland: wacht op goedkopere laadkans — hoge prijs verwacht ${futStr} vs nu ${priceStr}`; + } else { + text += ` — hogere prijs verwacht ${futStr} vs nu ${priceStr}`; + } + } + reasons.push({ impact: 'critical', category: 'strategy', supportedMode: 'preserve', icon: '🧠', text }); + } + + return reasons; + } + + _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', + pv_trickle: 'Sprokkelen', + zero_discharge_only: 'Ontladen', + to_full: 'Vol‑laden', + standby: 'Stand‑by', + zero: 'Net‑0', + predictive: 'Slim laden', + + 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% — laden nu niet rendabel (laadprijs €${_price.toFixed(3)} > verwachte ontlaadwaarde €${_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) { + if (inputs._pvStoreWins) { + // PV surplus is being stored — low SoC is being addressed by free PV charging + reasons.push({ + icon: '☀️', + category: 'battery', + text: `Batterij laag (${soc}%) — wordt geladen uit gratis PV-overschot.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'charge' + }); + } else { + 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; + + // ✅ FIX: HW firmware caps discharge at 800 W regardless of battery count + const maxDischarge = battery.maxDischargePowerW || 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(maxDischarge / currentLoad, 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 _settingsFloor = (inputs.settings?.cycle_cost_per_kwh ?? 0.075) / (inputs.settings?.battery_efficiency || 0.75); + const effectiveDischargeFloor = respectMinMax ? minDischargePrice : _settingsFloor; + + if (price > breakEven + 0.01) { + if (effectiveDischargeFloor > 0 && price < effectiveDischargeFloor) { + const blockReason = respectMinMax + ? `onder min. ontlaadprijs (${fmt(minDischargePrice)})` + : `onder opportunistische drempel (${fmt(effectiveDischargeFloor)})`; + 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 { + const consumptionW = inputs.consumptionW; + const maxDischW = inputs.battery?.maxDischargePowerW || 800; + const effKwh = consumptionW != null ? Math.min(maxDischW, consumptionW) / 1000 : null; + const consumptionNote = effKwh != null + ? ` Geschat verbruik: ${(consumptionW / 1000).toFixed(1)} kW → ~€${(price * effKwh).toFixed(3)}/u.` + : ''; + reasons.push({ + icon: '💰', + category: 'arbitrage', + impact: 'high', + sentiment: 'positive', + text: `Ontladen is winstgevend: huidige prijs (${fmt(price)}) ligt boven break‑even (${fmt(breakEven)}).${consumptionNote}`, + supportedMode: 'discharge' + }); + } + return; + } + + if (price < breakEven - 0.01) { + let dischargeNote = ''; + const slots = inputs.optimizerSlots; + if (Array.isArray(slots)) { + const now = new Date(); + const best = slots + .filter(s => new Date(s.timestamp) > now && s.action === 'discharge' && s.price != null) + .sort((a, b) => b.price - a.price)[0]; + if (best) { + const t = new Date(best.timestamp); + const timeLabel = t.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }).padStart(2, '0') + ':00'; + const margin = best.price - price; + dischargeNote = ` Beste ontlaadslot: ${timeLabel} bij ${fmt(best.price)} → nettomarge ~${fmt(margin)}/kWh.`; + } + } + 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)}).${dischargeNote}`, + 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 _exportMargin = (_price !== null && _storeValue !== null) ? _price - _storeValue : null; + const _soc = inputs.battery?.stateOfCharge ?? 50; + const _lowSocMarginRequired = _soc < 50 ? 0.02 : 0; + // Mirror policy-engine: export only wins if margin exceeds low-SoC threshold + const _exportWins = _exportMargin !== null && _exportMargin > 0 && _exportMargin >= _lowSocMarginRequired; + + 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 if (_exportMargin !== null && _exportMargin > 0 && _exportMargin < _lowSocMarginRequired) { + // Export margin exists but too thin at low SoC — store to avoid empty battery + reasons.push({ + icon: '🔋', + category: 'pv', + text: `PV-overschot (${exportPower}W) — exportmarge €${_exportMargin.toFixed(3)}/kWh te klein bij SoC ${_soc}% → opslaan voor piekontlading later.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'charge' + }); + } 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 if (pvNowW > 50) { + reasons.push({ + icon: '☀️', + category: 'pv', + text: `PV actief (${pvNowW}W) — batterij laadt via zon.`, + impact: 'medium', + sentiment: 'positive', + supportedMode: 'charge' + }); + } 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) { + // Skip "wait for PV" when PV surplus is already being stored + if (!inputs._pvStoreWins) { + 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)) { + // Skip RTE loss reason when charging from free PV — no charge cost means no RTE loss + if (inputs._pvStoreWins) return; + + 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 = parseInt(time.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' }), 10); + + 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', + pv_trickle: 'Sprokkelen', + 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', pv_trickle: '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' && inputs._pvStoreWins) { + // _pvStoreWins → zero: bidirectional grid balancing from PV surplus + const pvW = Math.round(inputs.p1?.pv_power_estimated ?? 0); + const soc = inputs.battery?.stateOfCharge ?? 0; + const battPower = Math.round(inputs.p1?.battery_power ?? 0); + const gridPower = Math.round(inputs.p1?.resolved_gridPower ?? 0); + const parts = []; + if (pvW > 0) parts.push(`PV ${pvW}W`); + if (battPower > 0) parts.push(`laden ${battPower}W`); + else if (battPower < 0) parts.push(`ontladen ${Math.abs(battPower)}W`); + if (gridPower !== 0) parts.push(`grid ${gridPower > 0 ? '+' : ''}${gridPower}W`); + const statusLine = parts.length ? ` (${parts.join(', ')})` : ''; + reasons.push({ + icon: '⚡', + category: 'mode', + text: `Net-0 actief${statusLine} — batterij laadt uit PV-overschot én ontlaadt bij verbruikspieken. SoC ${soc}%.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'charge' + }); + } else if (hwMode === 'zero' && recommendation.policyMode === 'discharge') { + // discharge + PV → zero: peak shaving with PV recharging + const pvW = Math.round(inputs.p1?.pv_power_estimated ?? 0); + const soc = inputs.battery?.stateOfCharge ?? 0; + const battPower = Math.round(inputs.p1?.battery_power ?? 0); + const gridPower = Math.round(inputs.p1?.resolved_gridPower ?? 0); + const parts = []; + if (pvW > 0) parts.push(`PV ${pvW}W`); + if (battPower > 0) parts.push(`laden ${battPower}W`); + else if (battPower < 0) parts.push(`ontladen ${Math.abs(battPower)}W`); + if (gridPower !== 0) parts.push(`grid ${gridPower > 0 ? '+' : ''}${gridPower}W`); + const statusLine = parts.length ? ` (${parts.join(', ')})` : ''; + reasons.push({ + icon: '⚡', + category: 'mode', + text: `Net-0 peak shaving${statusLine} — PV-overschot vult batterij aan, ontlading dekt pieken. SoC ${soc}%.`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'discharge' + }); + } + + if (hwMode === 'standby' && recommendation.policyMode === 'preserve') { + const slots = inputs.optimizerSlots; + const currentPrice = inputs.effectivePrice; + + if (slots) { + const now = new Date(); + const upcoming = slots + .filter(s => { + if (new Date(s.timestamp) <= now || s.action !== 'discharge') return false; + // Only show slots that are actually more expensive than now — + // showing cheaper future slots contradicts "current price too low" + return typeof currentPrice !== 'number' || s.price > currentPrice; + }) + .slice(0, 3); + + if (upcoming.length > 0) { + const slotLabels = upcoming.map(s => { + const t = new Date(s.timestamp); + const timeLabel = t.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Amsterdam' }); + return `${timeLabel} (${fmt(s.price)})`; + }); + const priceNote = typeof currentPrice === 'number' ? ` Huidige prijs ${fmt(currentPrice)} te laag om nu te ontladen.` : ''; + reasons.push({ + icon: '🔋', + category: 'arbitrage', + text: `Batterij bewaard voor geplande ontlaadbeurten: ${slotLabels.join(', ')}.${priceNote}`, + impact: 'high', + sentiment: 'positive', + supportedMode: 'preserve' + }); + } + } + } + + if (hwMode === 'to_full') { + const slots = inputs.optimizerSlots; + const currentPrice = inputs.effectivePrice; + const cycleCost = inputs.settings?.cycle_cost ?? 0.075; + + let text; + if (slots && typeof currentPrice === 'number') { + const now = new Date(); + const bestDischarge = slots + .filter(s => new Date(s.timestamp) > now && s.action === 'discharge') + .sort((a, b) => b.price - a.price)[0]; + + if (bestDischarge) { + const margin = bestDischarge.price - currentPrice - cycleCost; + const t = new Date(bestDischarge.timestamp); + const timeLabel = t.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Amsterdam' }); + text = `Laden van net: ${fmt(currentPrice)}/kWh → verwacht ontladen om ${timeLabel} bij ${fmt(bestDischarge.price)}. Nettomarge: ${fmt(margin)}/kWh na cycluskosten.`; + } else { + text = `Laden van net: ${fmt(currentPrice)}/kWh — geen duurder ontlaadmoment in planningsvenster.`; + } + } else { + text = 'Laden van net actief.'; + } + + reasons.push({ + icon: '⚡', + category: 'tariff', + text, + impact: 'high', + sentiment: 'positive', + supportedMode: 'charge' + }); + } + + 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 _settingsFloor = (inputs.settings?.cycle_cost_per_kwh ?? 0.075) / (inputs.settings?.battery_efficiency || 0.75); + const effectiveDischargeFloor = respectMinMax ? minDischargePrice : _settingsFloor; + + const parts = []; + if (effectiveDischargeFloor > 0 && typeof price === 'number' && price < effectiveDischargeFloor) { + const blockLabel = respectMinMax + ? `min. ontlaadprijs ${fmt(minDischargePrice)}` + : `break-even ${fmt(effectiveDischargeFloor)}`; + 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' || hwMode === 'zero')) return null; + if (policyMode === 'discharge' && hwMode === 'zero_discharge_only') return null; + + // Discharge + PV → zero: not a divergence but worth explaining + if (policyMode === 'discharge' && hwMode === 'zero') { + const pvW = Math.round(inputs.p1?.pv_power_estimated ?? 0); + const gridPower = inputs.p1?.resolved_gridPower ?? 0; + const surplus = gridPower < 0 ? Math.abs(Math.round(gridPower)) : 0; + const pvLabel = surplus > 0 ? `${surplus}W overschot van ${pvW}W` : (pvW > 0 ? `${pvW}W productie` : ''); + return { + icon: '☀️', + category: 'mapping', + text: `Ontladen via Net-0 (i.p.v. alleen-ontladen) — PV${pvLabel ? ` (${pvLabel})` : ''} laadt batterij bij, ontlading dekt verbruikspieken. Grid blijft ~0W.`, + impact: 'critical', + sentiment: 'positive', + supportedMode: 'discharge' + }; + } + + 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 _settingsFloor = (inputs.settings?.cycle_cost_per_kwh ?? 0.075) / (inputs.settings?.battery_efficiency || 0.75); + const effectiveFloor = respectMinMax ? minDischarge : _settingsFloor; + + const parts = []; + if (typeof price === 'number' && effectiveFloor > 0 && price < effectiveFloor) { + const floorLabel = respectMinMax + ? `min ontlaadprijs ${fmt(minDischarge)}` + : `break-even ${fmt(effectiveFloor)}`; + 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..3b12cdb6 --- /dev/null +++ b/lib/homewizard-cloud-api.js @@ -0,0 +1,645 @@ +/* + * 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) => { + const ws = new WebSocket('wss://energy-app-ws.homewizard.com/ws/'); + this.mainWs = ws; + + let authenticated = false; + + ws.on('open', () => { + this.log('Main WebSocket connected'); + + // Guard: if this socket was superseded by a reconnect, ignore it + if (this.mainWs !== ws) return; + + const helloMessage = { + type: 'hello', + message_id: ++this.messageId, + compatibility: 5, + os: 'homey', + source: 'com.homewizard', + token: this.accessToken, + version: 'com.homewizard/3.0.0' + }; + + ws.send(JSON.stringify(helloMessage)); + }); + + + ws.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); + } + }); + + ws.on('error', (error) => { + this.error('Main WebSocket error:', error); + this.emit('mainws_error', error); + if (!authenticated) { + reject(error); + } + }); + + ws.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/knmi-stations.js b/lib/knmi-stations.js new file mode 100644 index 00000000..40bbfd20 --- /dev/null +++ b/lib/knmi-stations.js @@ -0,0 +1,87 @@ +'use strict'; + +const fetchWithTimeout = require('../includes/utils/fetchWithTimeout'); + +const EDR_BASE = 'https://api.dataplatform.knmi.nl/edr/v1/collections/10-minute-in-situ-meteorological-observations'; + +let _locationsCache = null; +let _locationsCacheTime = 0; +const LOCATIONS_TTL = 24 * 60 * 60 * 1000; + +async function _getLocations(apiKey) { + const now = Date.now(); + if (_locationsCache && (now - _locationsCacheTime) < LOCATIONS_TTL) return _locationsCache; + const res = await fetchWithTimeout(`${EDR_BASE}/locations`, { headers: { Authorization: apiKey } }, 10000); + if (!res.ok) throw new Error(`KNMI locations error ${res.status}`); + const data = await res.json(); + _locationsCache = data.features ?? []; + _locationsCacheTime = now; + return _locationsCache; +} + +function _nearest(features, userLat, userLon) { + let best = null, bestD = Infinity; + for (const f of features) { + const [lon, lat] = f.geometry?.coordinates ?? []; + if (lat == null || lon == null) continue; + const d = Math.hypot(lat - userLat, lon - userLon); + if (d < bestD) { bestD = d; best = f; } + } + return { feature: best, distDeg: bestD }; +} + +function _lastVal(ranges, param) { + const vals = ranges[param]?.values; + if (!vals?.length) return null; + for (let i = vals.length - 1; i >= 0; i--) { + if (vals[i] != null && isFinite(vals[i])) return vals[i]; + } + return null; +} + +/** + * Fetch weather observations from nearest KNMI automatic station via EDR API. + * Returns null if API key missing or on any error. + * @param {string} apiKey + * @param {number} userLat + * @param {number} userLon + * @returns {Promise<{qg:number|null, n:number|null, ss:number|null, ta:number|null, stationName:string, distKm:number}|null>} + */ +async function fetchKnmiObservations(apiKey, userLat, userLon) { + if (!apiKey || typeof userLat !== 'number' || typeof userLon !== 'number') return null; + + const features = await _getLocations(apiKey); + const { feature, distDeg } = _nearest(features, userLat, userLon); + if (!feature) throw new Error('KNMI: no stations found'); + + const stationName = feature.properties?.name ?? feature.id; + const distKm = Math.round(distDeg * 111); + + const now = new Date(); + const from = new Date(now - 40 * 60 * 1000); // 40 min covers both 10-min (qg) and 30-min (n) params + const datetime = `${from.toISOString().slice(0, 19)}Z/${now.toISOString().slice(0, 19)}Z`; + + const url = new URL(`${EDR_BASE}/locations/${encodeURIComponent(feature.id)}`); + url.searchParams.set('datetime', datetime); + url.searchParams.set('parameter-name', 'qg,n,ss,ta'); + url.searchParams.set('f', 'CoverageJSON'); + + const res = await fetchWithTimeout(url.toString(), { headers: { Authorization: apiKey } }, 15000); + if (!res.ok) throw new Error(`KNMI EDR error ${res.status}`); + const data = await res.json(); + + const coverages = data.coverages ?? (data.type === 'Coverage' ? [data] : []); + if (!coverages.length) throw new Error('KNMI: no coverages in response'); + const ranges = coverages[coverages.length - 1].ranges ?? {}; + + return { + qg: _lastVal(ranges, 'qg'), + n: _lastVal(ranges, 'n'), + ss: _lastVal(ranges, 'ss'), + ta: _lastVal(ranges, 'ta'), + stationName, + distKm, + }; +} + +module.exports = { fetchKnmiObservations }; diff --git a/lib/kwhprice-provider.js b/lib/kwhprice-provider.js new file mode 100644 index 00000000..8974d0de --- /dev/null +++ b/lib/kwhprice-provider.js @@ -0,0 +1,443 @@ +'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) + * + * The page may contain two datasets: today and tomorrow. Both are parsed. + * 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 tomorrowUtcMidnight = new Date(todayUtcMidnight.getTime() + 86400000); + + const offsetToday = (this._isSummerTime(todayUtcMidnight) ? 2 : 1) * 3600 * 1000; + const offsetTomorrow = (this._isSummerTime(tomorrowUtcMidnight) ? 2 : 1) * 3600 * 1000; + + const toUtcForDay = (date, offset) => (hh, mm) => + new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), hh, mm, 0) - offset); + + const toUtcToday = toUtcForDay(todayUtcMidnight, offsetToday); + const toUtcTomorrow = toUtcForDay(tomorrowUtcMidnight, offsetTomorrow); + + // One shared labels array, two data arrays (datasets[0]=today, datasets[1]=tomorrow) + const labelsMatch = html.match(/labels:\s*\[([\s\S]*?)\]/); + const dataMatches = [...html.matchAll(/\bdata:\s*\[([-\d.,\s\n]+)\]/g)]; + + if (!labelsMatch || dataMatches.length === 0) { + this.log('kWhPrice: no Chart.js labels/data arrays found in page'); + return []; + } + + // Shared time labels for both today and tomorrow + 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) })); + + const datasets = [ + { dataMatch: dataMatches[0], toUtc: toUtcToday, label: 'today' }, + ...(dataMatches.length > 1 + ? [{ dataMatch: dataMatches[1], toUtc: toUtcTomorrow, label: 'tomorrow' }] + : []) + ]; + + for (const { dataMatch, toUtc, label } of datasets) { + const prices = dataMatch[1] + .split(',') + .map(s => parseFloat(s.trim())) + .filter(n => !isNaN(n)); + + if (times.length === 0 || prices.length === 0) { + this.log(`kWhPrice: no valid data for ${label}`); + continue; + } + + const count = Math.min(times.length, prices.length); + this.log(`kWhPrice layout: Chart.js 15-min intervals [${count} slots ${label}]`); + + for (let i = 0; i < count; i++) { + const { hh, mm } = times[i]; + const spotEur = prices[i]; + if (!isNaN(spotEur)) { + 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, + minute: 0, + 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); + + // Page-change detect: if HTML > 2KB but 0 slots parsed, parser likely broken (selector changed). + // Throw so the stale-cache fallback in catch{} engages instead of returning []. + if (slots.length === 0) { + if (html.length > 2000) { + throw new Error(`kWhPrice parse failure: ${html.length}B HTML but 0 slots — page structure may have changed`); + } + 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..08c7b1ff --- /dev/null +++ b/lib/learning-engine.js @@ -0,0 +1,1164 @@ +'use strict'; + +/** + * LearningEngine - Tracks historical performance and learns patterns + * + * Features: + * - 15-minute consumption patterns by day-of-week (7 × 24 × 4 = 672 slots) + * - 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 migration: hourly → 15-min quarter resolution ──────────── + // Detect old format: patterns[day][hour] = {sum, count, avg} (no sub-key 0). + // Spread the hourly average evenly across all 4 quarter slots. + const sample = this.data.consumption_patterns?.[0]?.[0]; + if (sample && 'avg' in sample && !(0 in sample)) { + this.homey.log('[LearningEngine] Migrating consumption patterns: hourly → 15-min quarters'); + const newPatterns = this._initializeConsumptionPatterns(); + for (let day = 0; day < 7; day++) { + for (let hour = 0; hour < 24; hour++) { + const old = this.data.consumption_patterns[day][hour]; + if (old && old.count > 0) { + for (let q = 0; q < 4; q++) { + newPatterns[day][hour][q] = { sum: old.avg * old.count, count: old.count, avg: old.avg }; + } + } + } + } + this.data.consumption_patterns = newPatterns; + await this._saveData(); + this.homey.log('[LearningEngine] Migration complete — 15-min patterns active'); + } + + // ── One-time migration: UTC → Amsterdam local time ──────────────────── + // Historical data was recorded using getHours() which returns UTC on Homey. + // After switching to _getAmsterdamTime(), old data sits at wrong hour indices + // (e.g. 7:00 CET kettle recorded at UTC slot 6, displayed as "06:00"). + // Reset patterns so they re-learn with correct Amsterdam local time indexing. + // Re-learning takes ~24-48h (P1 polls every 15s → ~60 samples/slot/day). + if (!this.data.consumption_tz_migrated_v1) { + this.homey.log('[LearningEngine] Resetting consumption patterns — old data used UTC hours, now using Amsterdam local time'); + this.data.consumption_patterns = this._initializeConsumptionPatterns(); + this.data.consumption_tz_migrated_v1 = true; + await this._saveData(); + this.homey.log('[LearningEngine] Consumption patterns reset — will re-learn within 24-48h'); + } + + // ── One-time migration: add variance tracking to existing slots ────────── + // Existing slots have {sum, count, avg} only — add sumSq and variance. + // sumSq is initialised to avg²×count which implies variance=0 initially; + // it will build up from new observations without disrupting the avg. + if (!this.data.consumption_variance_migrated_v1) { + this.homey.log('[LearningEngine] Adding variance tracking to consumption patterns'); + for (let day = 0; day < 7; day++) { + for (let hour = 0; hour < 24; hour++) { + for (let q = 0; q < 4; q++) { + const p = this.data.consumption_patterns[day][hour][q]; + if (p && p.sumSq === undefined) { + p.sumSq = p.avg * p.avg * p.count; + p.variance = 0; + } + } + } + } + this.data.consumption_variance_migrated_v1 = true; + await this._saveData(); + this.homey.log('[LearningEngine] Variance migration complete'); + } + + // 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(); + } + + // v2 reset: clear bias samples collected with old 3-model ensemble (ECMWF+GFS+ICON). + // KNMI Harmonie added to ensemble → radiation baseline changed, old samples no longer valid. + if (!this.data.radiation_bias_reset_v2) { + this.homey.log(`[LearningEngine] Resetting radiation bias samples — new 4-model ensemble (+ KNMI Harmonie) active`); + this.data.radiation_bias_factor = 1.0; + this.data.radiation_bias_samples = []; + this.data.radiation_bias_reset_v2 = true; + await this._saveData(); + } + + // v1 normalize: yield factors learned while radiation_bias_factor > 1 was active are + // calibrated against biased radiation, but forecasting uses unbiased radiation (bias + // suppressed at ≥10 slots). Factors are ~biasFactor× too low → systematic underestimate. + // Reset them so they re-learn consistently with unbiased radiation from scratch. + // Only runs when bias is significant (>1.5); users near 1.0 are unaffected. + if (!this.data.solar_yield_normalize_v1) { + this.data.solar_yield_normalize_v1 = true; + const bF = this.data.radiation_bias_factor ?? 1.0; + const count = this.getSolarLearnedSlotCount(); + if (bF > 1.5 && count > 0) { + this.homey.log(`[LearningEngine] Resetting ${count} yield slots — calibrated against biased radiation (factor ${bF.toFixed(2)}), re-learning with unbiased radiation`); + this.data.solar_yield_factors = new Array(96).fill(null); + this.data.solar_slot_max_radiation = new Array(96).fill(0); + } + await this._saveData(); + } + + // v2: also reset the radiation bias factor — it was inflated to 2.0 by the + // mis-calibrated yield factors (actual_radiation was back-calculated from pv_power + // / wrong_yf → too high → pushed bias to cap). Reset so fallback PR estimates are + // not doubled. Only runs when bias is still at the inflated cap (>1.5). + if (!this.data.solar_yield_normalize_v2) { + this.data.solar_yield_normalize_v2 = true; + const bF = this.data.radiation_bias_factor ?? 1.0; + if (bF > 1.5) { + this.homey.log(`[LearningEngine] Resetting radiation_bias_factor ${bF.toFixed(2)} → 1.0 (was inflated by mis-calibrated yield factors)`); + this.data.radiation_bias_factor = 1.0; + this.data.radiation_bias_samples = []; + } + await this._saveData(); + } + + // v1 reset: pv_accuracy_score corrupted by unbounded error formula (|a-p|/p). + // New formula uses max(a,p) as denominator → score bounded [0,1]. Reset once. + if (!this.data.pv_accuracy_reset_v1) { + this.homey.log('[LearningEngine] Resetting pv_accuracy_score — bounded error formula active'); + this.data.pv_accuracy_score = 1.0; + this.data.pv_predictions = []; + this.data.pv_accuracy_reset_v1 = true; + await this._saveData(); + } + + // v1 reset: pv_daily_bias and pv_daily_bias_clear corrupted by _recordPvAccuracySample + // bug — _getPvForSlot was called without _buildPvIndex, so predicted was always slot[0] + // value instead of the interpolated forecast for the current hour. Daily ratio + // sumActual/sumPredicted was therefore wrong, driving pv_daily_bias to ~0.3 (68% cut). + if (!this.data.pv_daily_bias_reset_v1) { + this.homey.log(`[LearningEngine] Resetting pv_daily_bias (${(this.data.pv_daily_bias ?? 1.0).toFixed(3)}) and pv_daily_bias_clear (${(this.data.pv_daily_bias_clear ?? 1.0).toFixed(3)}) — corrupted by slot[0] predicted bug`); + this.data.pv_daily_bias = 1.0; + this.data.pv_daily_bias_clear = 1.0; + this.data.pv_daily_bias_samples = 0; + this.data.pv_daily_bias_clear_samples = 0; + this.data.pv_predictions = []; + this.data.pv_accuracy_score = 1.0; + this.data.pv_accuracy_om = null; + this.data.pv_accuracy_sc = null; + this.data.pv_model_accuracy = {}; + this.data.pv_daily_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), + + // Daily PV level bias: EMA of (actual_kWh / forecast_kWh) per day. + // Corrects systematic level errors not captured by per-slot yield factors. + pv_daily_bias: 1.0, + pv_daily_bias_samples: 0, + + // Separate bias for clear days (avg cloud cover ≤ 40%). Clear days are + // systematically underestimated by the mixed-weather EMA above. + pv_daily_bias_clear: 1.0, + pv_daily_bias_clear_samples: 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() + }, + + // Net PV surplus prediction accuracy for terminal value correction. + // Tracks pvKwhTomorrow (net, consumption-adjusted) vs actual next-day net. + pv_net_surplus_factor: 1.0, // EMA correction [0.4, 1.1]; <1 = we over-predicted + pv_net_surplus_pending: null // { date: 'YYYY-MM-DD', predicted: kWh } + }; + + await this._saveData(); + } + } + + /** + * Initialize consumption pattern structure. + * 7 days × 24 hours × 4 quarters (0=:00, 1=:15, 2=:30, 3=:45) = 672 slots. + */ + _initializeConsumptionPatterns() { + const patterns = {}; + for (let day = 0; day < 7; day++) { + patterns[day] = {}; + for (let hour = 0; hour < 24; hour++) { + patterns[day][hour] = {}; + for (let q = 0; q < 4; q++) { + patterns[day][hour][q] = { sum: 0, count: 0, avg: 0, sumSq: 0, variance: 0 }; + } + } + } + return patterns; + } + + /** + * Extract Amsterdam local time components from a Date. + * On Homey, getHours()/getDay() return UTC — always use this helper. + * @private + * @returns {{ dayOfWeek: number, hour: number, quarter: number }} + */ + _getAmsterdamTime(date = new Date()) { + const tz = 'Europe/Amsterdam'; + const dayStr = date.toLocaleDateString('en-US', { weekday: 'short', timeZone: tz }); + const hour = parseInt(date.toLocaleString('en-US', { hour: 'numeric', hour12: false, timeZone: tz }), 10) % 24; + const minute = parseInt(date.toLocaleString('en-US', { minute: 'numeric', timeZone: tz }), 10); + return { + dayOfWeek: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(dayStr), + hour, + quarter: Math.floor(minute / 15), + }; + } + + /** + * Pause learning (e.g. during vacation). Consumption recording is skipped. + * Solar yield and PV accuracy continue unaffected. + */ + async pause() { + this.data.paused = true; + await this._saveData(); + this.homey.log('[LearningEngine] Learning paused'); + } + + /** + * Resume learning after pause. + */ + async resume() { + this.data.paused = false; + await this._saveData(); + this.homey.log('[LearningEngine] Learning resumed'); + } + + /** + * @returns {boolean} True when learning is paused + */ + isPaused() { + return this.data.paused === true; + } + + /** + * Record actual consumption for learning. + * @param {number} powerW - Current grid import power + */ + async recordConsumption(powerW) { + if (this.data.paused) return; // Away mode — do not corrupt patterns + if (powerW < 0) return; // Only track import, not export + + const { dayOfWeek, hour, quarter } = this._getAmsterdamTime(); + + const pattern = this.data.consumption_patterns[dayOfWeek][hour][quarter]; + // Ensure variance fields exist (guard for slots initialised before migration) + if (pattern.sumSq === undefined) pattern.sumSq = pattern.avg * pattern.avg * pattern.count; + if (pattern.variance === undefined) pattern.variance = 0; + + // 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.sumSq += powerW * powerW; + pattern.count += 1; + pattern.avg = pattern.sum / pattern.count; + pattern.variance = pattern.sumSq / pattern.count - pattern.avg * pattern.avg; + } else { + // EMA for mean (α=0.01) + EMA for variance (α=0.005, slower — needs more data to stabilise) + const alpha = 0.01; + const alphaV = 0.005; + const delta = powerW - pattern.avg; + pattern.avg = alpha * powerW + (1 - alpha) * pattern.avg; + pattern.variance = (1 - alphaV) * pattern.variance + alphaV * delta * delta; + // Keep sum/count in sync; cap count at 100 to prevent overflow + pattern.sum = pattern.avg * pattern.count; + pattern.sumSq = (pattern.variance + pattern.avg * 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(); + } + } + + /** + * Core prediction logic for a specific day/hour/quarter combination. + * Shared by getPredictedConsumption() and getDailyProfile(). + * @private + * @returns {number} Predicted power in W + */ + _predictFromPattern(dayOfWeek, hour, quarter) { + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + const groupDays = isWeekend ? [0, 6] : [1, 2, 3, 4, 5]; + + // 1. Specific day + hour + quarter + const pattern = this.data.consumption_patterns[dayOfWeek][hour][quarter]; + if (pattern.count >= 4) return pattern.avg; + + // 2. Same day + same hour, all quarters combined (e.g. early in learning) + let hourSum = 0, hourCount = 0; + for (let q = 0; q < 4; q++) { + const p = this.data.consumption_patterns[dayOfWeek][hour][q]; + if (p.count > 0) { hourSum += p.avg * p.count; hourCount += p.count; } + } + if (hourCount >= 4) return hourSum / hourCount; + + // 3. Day-group (weekday/weekend) + same hour + same quarter + let groupSum = 0, groupCount = 0; + for (const d of groupDays) { + const p = this.data.consumption_patterns[d][hour][quarter]; + if (p.count > 0) { groupSum += p.avg * p.count; groupCount += p.count; } + } + if (groupCount > 0) return groupSum / groupCount; + + // 4. All days + same hour + same quarter + let totalSum = 0, totalCount = 0; + for (let d = 0; d < 7; d++) { + const p = this.data.consumption_patterns[d][hour][quarter]; + if (p.count > 0) { totalSum += p.avg * p.count; totalCount += p.count; } + } + return totalCount > 0 ? totalSum / totalCount : 0; + } + + /** + * Get predicted consumption for a specific time (15-min resolution). + * @param {Date} targetTime - Time to predict for + * @returns {number} Predicted power in W + */ + getPredictedConsumption(targetTime = new Date()) { + const { dayOfWeek, hour, quarter } = this._getAmsterdamTime(targetTime); + return this._predictFromPattern(dayOfWeek, hour, quarter); + } + + /** + * Get predicted consumption using explicit Amsterdam local hour/minute. + * Preferred over getPredictedConsumption() for price-slot lookups where the + * provider already gives the Amsterdam-local hour/minute directly (KwhPrice, + * Xadi item.hour), avoiding any UTC/local timestamp parsing ambiguity. + * @param {Date} date - Used only for dayOfWeek derivation + * @param {number} localHour - Amsterdam local hour (0-23) + * @param {number} localMinute - Amsterdam local minute (0, 15, 30, 45) + * @returns {number} Predicted power in W + */ + getPredictedConsumptionForSlot(date, localHour, localMinute) { + const { dayOfWeek } = this._getAmsterdamTime(date); + const quarter = Math.floor(localMinute / 15); + return this._predictFromPattern(dayOfWeek, localHour, quarter); + } + + /** + * Return the learned 15-min consumption profile for a given day-of-week. + * Returns 96 slots (24h × 4 quarters) with predicted W and sample count. + * Suitable for rendering a consumption chart in the settings UI. + * + * @param {number} dayOfWeek 0=Sunday … 6=Saturday (Amsterdam local) + * @returns {Array<{slot:number, hour:number, quarter:number, avgW:number, count:number}>} + */ + getDailyProfile(dayOfWeek) { + const slots = []; + for (let hour = 0; hour < 24; hour++) { + for (let q = 0; q < 4; q++) { + const p = this.data.consumption_patterns[dayOfWeek][hour][q]; + const avgW = Math.round(this._predictFromPattern(dayOfWeek, hour, q)); + const count = p?.count ?? 0; + const stddev = (p?.variance > 0) ? Math.round(Math.sqrt(p.variance)) : 0; + slots.push({ slot: hour * 4 + q, hour, quarter: q, avgW, count, stddev }); + } + } + return slots; + } + + /** + * Get number of recorded samples for a specific day+hour+quarter slot. + * @param {Date} targetTime + * @returns {number} Sample count (0–100) + */ + getConsumptionSampleCount(targetTime = new Date()) { + const { dayOfWeek, hour, quarter } = this._getAmsterdamTime(targetTime); + return this.data.consumption_patterns[dayOfWeek][hour][quarter]?.count ?? 0; + } + + /** + * Get consumption prediction confidence (0–100). + * @param {Date} targetTime + */ + getConsumptionConfidence(targetTime = new Date()) { + const { dayOfWeek, hour, quarter } = this._getAmsterdamTime(targetTime); + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + const groupDays = isWeekend ? [0, 6] : [1, 2, 3, 4, 5]; + + // Per-day confidence curve (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); + }; + + const pattern = this.data.consumption_patterns[dayOfWeek][hour][quarter]; + let baseConf; + if (pattern.count >= 4) { + baseConf = dayConf(pattern.count); + } else { + // Fallback to day-group confidence, capped at 60% + const groupTotal = groupDays.reduce( + (sum, d) => sum + (this.data.consumption_patterns[d][hour][quarter]?.count ?? 0), 0 + ); + baseConf = Math.min(60, dayConf(groupTotal)); + } + + // Variance penalty: high coefficient of variation → less confidence. + // CV = stddev / avg. Penalty kicks in above CV=0.6, max 40pt reduction. + // Ignore when avg < 50W (near-zero consumption has high relative variance by nature). + const variance = pattern.variance ?? 0; + if (variance > 0 && pattern.avg >= 50) { + const cv = Math.sqrt(variance) / pattern.avg; + const penalty = cv > 0.6 ? Math.min(40, (cv - 0.6) * 80) : 0; + baseConf = Math.max(0, baseConf - penalty); + } + + return baseConf; + } + + /** + * Get coefficient of variation (stddev/avg) for a specific time slot. + * Returns null when avg < 50W or no variance data yet. + * @param {Date} targetTime + * @returns {number|null} CV (0 = stable, 1.0 = very variable) + */ + getConsumptionCV(targetTime = new Date()) { + const { dayOfWeek, hour, quarter } = this._getAmsterdamTime(targetTime); + const pattern = this.data.consumption_patterns[dayOfWeek][hour][quarter]; + if (!pattern || pattern.count < 20 || pattern.avg < 50) return null; + const variance = pattern.variance ?? 0; + if (variance <= 0) return null; + return Math.sqrt(variance) / pattern.avg; + } + + /** + * Get CV using explicit Amsterdam local hour/minute (see getPredictedConsumptionForSlot). + * @param {Date} date - Used only for dayOfWeek derivation + * @param {number} localHour - Amsterdam local hour (0-23) + * @param {number} localMinute - Amsterdam local minute (0, 15, 30, 45) + * @returns {number|null} CV or null + */ + getConsumptionCVForSlot(date, localHour, localMinute) { + const { dayOfWeek } = this._getAmsterdamTime(date); + const quarter = Math.floor(localMinute / 15); + const pattern = this.data.consumption_patterns[dayOfWeek][localHour][quarter]; + if (!pattern || pattern.count < 20 || pattern.avg < 50) return null; + const variance = pattern.variance ?? 0; + if (variance <= 0) return null; + return Math.sqrt(variance) / pattern.avg; + } + + /** + * Record PV prediction vs actual for learning + * @param {number} predictedW - What we predicted + * @param {number} actualW - What actually happened + */ + async recordPvAccuracy(predictedW, actualW, omW = null, scW = null, perModelW = null) { + const now = Date.now(); + + // Calculate error as fraction of max(actual, predicted) — bounded [0, 1]. + // Using max rather than predicted avoids unbounded errors when forecast >> actual + // or actual >> forecast (e.g. dawn ramp-up, weather transitions). + const denom = Math.max(actualW, predictedW, 1); + const error = Math.abs(actualW - predictedW) / denom; + + this.data.pv_predictions.push({ + timestamp: now, + predicted: predictedW, + actual: actualW, + om: omW ?? null, + sc: scW ?? null, + error: error, + mf: perModelW?.['meteofrance_arpege_europe'] ?? null, + gfs: perModelW?.['gfs_seamless'] ?? null, + icon: perModelW?.['icon_seamless'] ?? null, + knmi: perModelW?.['knmi_harmonie_arome_netherlands'] ?? null, + }); + + // 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 blended accuracy score (exponential moving average) + const accuracy = 1.0 - error; + const alpha = 0.1; + this.data.pv_accuracy_score = + (alpha * accuracy) + ((1 - alpha) * this.data.pv_accuracy_score); + + // Per-model accuracy (only when both models provided a meaningful prediction) + if (omW != null && omW > 50) { + if (this.data.pv_accuracy_om == null) this.data.pv_accuracy_om = 1.0; + const omErr = Math.abs(actualW - omW) / Math.max(actualW, omW, 1); + this.data.pv_accuracy_om = (alpha * (1 - omErr)) + ((1 - alpha) * this.data.pv_accuracy_om); + } + if (scW != null && scW > 50) { + if (this.data.pv_accuracy_sc == null) this.data.pv_accuracy_sc = 1.0; + const scErr = Math.abs(actualW - scW) / Math.max(actualW, scW, 1); + this.data.pv_accuracy_sc = (alpha * (1 - scErr)) + ((1 - alpha) * this.data.pv_accuracy_sc); + } + + 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(); + } + + /** + * Returns blend weights for OM and Solcast based on learned per-model accuracy. + * Falls back to 0.5/0.5 until both models have been observed. + * Clamped to [0.2, 0.8] to avoid fully discarding either model. + * @returns {{ wOM: number, wSC: number }} + */ + getPvBlendWeights() { + const accOM = this.data.pv_accuracy_om ?? null; + const accSC = this.data.pv_accuracy_sc ?? null; + if (accOM == null || accSC == null) return { wOM: 0.5, wSC: 0.5 }; + const total = accOM + accSC; + if (total <= 0) return { wOM: 0.5, wSC: 0.5 }; + const raw = accSC / total; + const wSC = Math.min(0.8, Math.max(0.2, raw)); + return { wOM: 1 - wSC, wSC }; + } + + /** + * 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)); + } + + /** + * Compute yesterday's actual vs forecast PV ratio from stored pv_predictions, + * then update the daily level bias EMA. Maintains separate EMAs for clear days + * (avg cloud cover ≤ 40%) and mixed/overcast days. + * @param {string} yDateStr - ISO date string 'YYYY-MM-DD' (UTC) + * @param {number|null} avgCloudPct - Average cloud cover % for yesterday (0–100), or null if unknown + */ + async recordDailyPvBiasFromPredictions(yDateStr, avgCloudPct = null, knmiKt = null) { + const preds = this.data.pv_predictions ?? []; + const dayPreds = preds.filter(p => { + if (!p.predicted || !p.actual || p.predicted <= 50 || p.actual <= 50) return false; + return new Date(p.timestamp).toISOString().slice(0, 10) === yDateStr; + }); + + if (dayPreds.length < 4) { + this.log(`Daily PV bias skipped for ${yDateStr}: only ${dayPreds.length} valid samples`); + return; + } + + const sumActual = dayPreds.reduce((s, p) => s + p.actual, 0); + const sumPredicted = dayPreds.reduce((s, p) => s + p.predicted, 0); + if (sumPredicted <= 0) return; + + const ratio = Math.max(0.3, Math.min(2.0, sumActual / sumPredicted)); + const alpha = 0.15; + + // Prefer KNMI clearness index (kt) over OM cloud% — OM systematically over-predicts cloud cover. + // kt = actual GHI / clear-sky GHI: ≥0.65 = clear, <0.30 = overcast, otherwise mixed. + let isClear, isOvercast, classLabel; + if (knmiKt != null) { + isClear = knmiKt >= 0.65; + isOvercast = knmiKt < 0.30; + classLabel = `kt=${knmiKt.toFixed(2)}`; + } else { + isClear = avgCloudPct != null && avgCloudPct <= 20; + isOvercast = avgCloudPct != null && avgCloudPct > 75; + classLabel = avgCloudPct != null ? `cloud=${avgCloudPct.toFixed(0)}%` : 'cloud=unknown'; + } + + if (isClear) { + this.data.pv_daily_bias_clear = alpha * ratio + (1 - alpha) * (this.data.pv_daily_bias_clear ?? 1.0); + this.data.pv_daily_bias_clear_samples = (this.data.pv_daily_bias_clear_samples ?? 0) + 1; + this.log(`Daily PV bias (clear, ${classLabel}) for ${yDateStr}: ratio=${ratio.toFixed(3)}, bias=${this.data.pv_daily_bias_clear.toFixed(3)} (n=${this.data.pv_daily_bias_clear_samples})`); + } else if (isOvercast) { + this.data.pv_daily_bias_overcast = alpha * ratio + (1 - alpha) * (this.data.pv_daily_bias_overcast ?? 1.0); + this.data.pv_daily_bias_overcast_samples = (this.data.pv_daily_bias_overcast_samples ?? 0) + 1; + this.log(`Daily PV bias (overcast, ${classLabel}) for ${yDateStr}: ratio=${ratio.toFixed(3)}, bias=${this.data.pv_daily_bias_overcast.toFixed(3)} (n=${this.data.pv_daily_bias_overcast_samples})`); + } else { + this.data.pv_daily_bias = alpha * ratio + (1 - alpha) * (this.data.pv_daily_bias ?? 1.0); + this.data.pv_daily_bias_samples = (this.data.pv_daily_bias_samples ?? 0) + 1; + this.log(`Daily PV bias (mixed, ${classLabel}) for ${yDateStr}: ratio=${ratio.toFixed(3)}, bias=${this.data.pv_daily_bias.toFixed(3)} (n=${this.data.pv_daily_bias_samples})`); + } + + await this._saveData(); + } + + /** + * 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; + } + + /** + * Returns the learned daily PV level bias factor. + * Prefers KNMI clearness index (kt) for classification when available — same logic as + * recordDailyPvBiasFromPredictions to avoid record/apply mismatch. + * Falls back to OM cloud% when kt unavailable. + * @param {number|null} avgCloudPct - Today's average forecast cloud cover (0–100), or null + * @param {number|null} knmiKt - Today's KNMI clearness index, or null + * @returns {number} Correction factor clamped to 0.3–2.0 + */ + getDailyPvBiasFactor(avgCloudPct = null, knmiKt = null) { + let isClear, isOvercast; + if (knmiKt != null) { + isClear = knmiKt >= 0.65; + isOvercast = knmiKt < 0.30; + } else { + isClear = avgCloudPct != null && avgCloudPct <= 20; + isOvercast = avgCloudPct != null && avgCloudPct > 75; + } + if (isClear && (this.data.pv_daily_bias_clear_samples ?? 0) >= 5) { + return this.data.pv_daily_bias_clear ?? 1.0; + } + if (isOvercast && (this.data.pv_daily_bias_overcast_samples ?? 0) >= 5) { + return this.data.pv_daily_bias_overcast ?? 1.0; + } + // Fallback: mixed EMA (covers intermediate kt/cloud and any class with < 5 samples) + if ((this.data.pv_daily_bias_samples ?? 0) < 5) return 1.0; + return this.data.pv_daily_bias ?? 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 < 30) return; // absolute floor — ignore dawn/dusk where ratio is too noisy + + 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 = 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]; + + // Curtailment guard: near-zero power (<30W) when the model expects significant output + // means the inverter was likely off (net outage, startup delay, grid protection), not cloudy. + // Cloudy days produce gradual reduction caught by the 80% drop filter; curtailment causes + // an abrupt floor. Only active once the slot has a learned baseline (old !== null). + if (powerW < 30 && old !== null && old > 0) { + const expectedW = old * radiationWm2; + if (expectedW > 200) { + this.log(`Solar yield slot ${slotIndex}: curtailment suspected (${Math.round(powerW)}W actual, ${Math.round(expectedW)}W expected) — skipped`); + return; + } + } + + // 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²)`); + + // Track per-slot peak yield factor for clear-sky ceiling (only from spike-checked readings). + this.data.solar_slot_max_yield_factor = this.data.solar_slot_max_yield_factor || new Array(96).fill(0); + if (yf > (this.data.solar_slot_max_yield_factor[slotIndex] || 0)) { + this.data.solar_slot_max_yield_factor[slotIndex] = yf; + } + + // At the exact moment we cross from <10 to 10 learned slots, the bias factor transitions + // from active (applied to radiation) to inactive (returns 1.0). Normalize all stored yield + // factors by the bias factor so they remain calibrated for unbiased-radiation forecasting. + const countNow = this.getSolarLearnedSlotCount(); + if (countNow === 10 && !this.data.solar_yield_normalize_at10) { + this.data.solar_yield_normalize_at10 = true; + const bF = this.data.radiation_bias_factor ?? 1.0; + if (bF > 1.01) { + for (let i = 0; i < 96; i++) { + if (this.data.solar_yield_factors[i] !== null) { + this.data.solar_yield_factors[i] *= bF; + } + } + this.log(`Solar yield: normalized 10 slots ×${bF.toFixed(2)} at bias→yield-factor transition`); + } + } + + // 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); + } + + getSolarSlotMaxYieldFactors() { + return this.data.solar_slot_max_yield_factor || new Array(96).fill(0); + } + + /** + * 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, perModel = null) { + this.data.forecast_snapshots = this.data.forecast_snapshots || {}; + this.data.forecast_snapshots[dateStr] = { forecastAvgWm2, perModel, 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; + } + + /** + * Domain-knowledge prior for NL conditions: Harmonie/ICON beat ECMWF/GFS at day+1. + * Used as EMA start value — learned accuracy drifts from here over time. + */ + _modelPrior(m) { + const PRIORS = { + knmi_harmonie_arome_netherlands: 0.90, + icon_seamless: 0.87, + meteofrance_arpege_europe: 0.82, + gfs_seamless: 0.77, + }; + return PRIORS[m] ?? 0.84; + } + + /** + * Update per-model accuracy EMA from yesterday's per-model forecast snapshot vs actual radiation. + * @param {{ [model]: number }} perModelForecast - avg W/m² per model from yesterday's snapshot + * @param {number} actualAvgWm2 - measured avg W/m² for yesterday's daylight hours + */ + async recordModelAccuracy(perModelForecast, actualAvgWm2, dateStr) { + if (!perModelForecast || actualAvgWm2 <= 0) return; + // Guard: process each date only once (weather fetch runs hourly, same data every time) + if (dateStr && this.data.pv_model_accuracy_date === dateStr) return; + if (dateStr) this.data.pv_model_accuracy_date = dateStr; + // Skip cloudy days — low absolute radiation means large relative errors from noise, not model skill + if (actualAvgWm2 < 150) { + this.homey.log(`[ModelAccuracy] skipped ${dateStr} — overcast (actual=${actualAvgWm2.toFixed(0)}W/m² < 150)`); + return; + } + this.data.pv_model_accuracy = this.data.pv_model_accuracy || {}; + const alpha = 0.15; + const log = []; + for (const [m, fc] of Object.entries(perModelForecast)) { + if (typeof fc !== 'number' || fc < 30) continue; + const err = Math.abs(actualAvgWm2 - fc) / Math.max(actualAvgWm2, fc, 1); + const acc = 1 - Math.min(err, 1); + if (this.data.pv_model_accuracy[m] == null) this.data.pv_model_accuracy[m] = this._modelPrior(m); + this.data.pv_model_accuracy[m] = alpha * acc + (1 - alpha) * this.data.pv_model_accuracy[m]; + log.push(`${m.replace('_seamless','').replace('knmi_harmonie_arome_netherlands','knmi')}=${this.data.pv_model_accuracy[m].toFixed(2)}`); + } + if (log.length) this.homey.log(`[ModelAccuracy] ${log.join(' ')} (actual=${actualAvgWm2.toFixed(0)}W/m²)`); + await this._saveData(); + } + + /** + * Store an hourly KNMI station qg reading for use as ground-truth in accuracy tracking. + * Keeps last 2 calendar days (UTC) — pruned on write. + * @param {number} wm2 Global solar radiation from nearest station (W/m²) + * @param {string} dateStr UTC date 'YYYY-MM-DD' + * @param {number} hour UTC hour 0-23 + */ + recordKnmiHourlyActual(wm2, dateStr, hour) { + if (typeof wm2 !== 'number' || !isFinite(wm2)) return; + if (!this.data.knmi_hourly_actuals) this.data.knmi_hourly_actuals = {}; + if (!this.data.knmi_hourly_actuals[dateStr]) this.data.knmi_hourly_actuals[dateStr] = {}; + this.data.knmi_hourly_actuals[dateStr][hour] = wm2; + // Prune dates older than 2 days + const cutoff = new Date(dateStr); + cutoff.setUTCDate(cutoff.getUTCDate() - 2); + for (const d of Object.keys(this.data.knmi_hourly_actuals)) { + if (new Date(d) < cutoff) delete this.data.knmi_hourly_actuals[d]; + } + } + + /** + * Returns average W/m² from stored KNMI station readings for a given date (daylight only: > 10 W/m²). + * @param {string} dateStr UTC date 'YYYY-MM-DD' + * @returns {number|null} + */ + getKnmiDailyAvg(dateStr) { + const hours = this.data.knmi_hourly_actuals?.[dateStr]; + if (!hours) return null; + const vals = Object.values(hours).filter(v => v > 10); + if (vals.length < 4) return null; // need at least 4h of daylight data + return vals.reduce((s, v) => s + v, 0) / vals.length; + } + + /** + * Returns normalised blend weights per OM ensemble model, based on learned accuracy. + * Falls back to domain-knowledge priors until data accumulates. + * @returns {{ [model]: number }} weights summing to 1.0 + */ + getModelWeights() { + const MODELS = ['meteofrance_arpege_europe', 'gfs_seamless', 'icon_seamless', 'knmi_harmonie_arome_netherlands']; + const acc = MODELS.map(m => ({ m, acc: this.data.pv_model_accuracy?.[m] ?? this._modelPrior(m) })); + // Rank-based: robust to single bad days; rank 1 (best) = weight 4, rank 4 = weight 1 + const RANK_WEIGHTS = [4, 3, 2, 1]; + const ranked = [...acc].sort((a, b) => b.acc - a.acc); + const total = RANK_WEIGHTS.reduce((a, b) => a + b, 0); + const weightMap = Object.fromEntries(ranked.map((r, i) => [r.m, RANK_WEIGHTS[i] / total])); + return Object.fromEntries(MODELS.map(m => [m, weightMap[m]])); + } + + /** + * 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 15-min quarter-slots have data (7 × 24 × 4 = 672 total) + let slotsWithData = 0; + for (let day = 0; day < 7; day++) { + for (let hour = 0; hour < 24; hour++) { + for (let q = 0; q < 4; q++) { + if (this.data.consumption_patterns[day][hour][q].count > 0) slotsWithData++; + } + } + } + + return { + days_tracking: Math.floor(daysTracking), + total_samples: this.data.stats.total_samples, + pattern_coverage: Math.round((slotsWithData / 672) * 100), // 672 = 7*24*4 + 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'); + } + + savePvNetSurplusPrediction(dateStr, predictedKwh) { + this.data.pv_net_surplus_pending = { date: dateStr, predicted: predictedKwh }; + } + + settlePvNetSurplusAccuracy(dateStr, actualKwh) { + const pending = this.data.pv_net_surplus_pending; + if (!pending || pending.date !== dateStr || pending.predicted <= 0.1) return; + const ratio = Math.min(2, actualKwh / pending.predicted); + const alpha = 0.15; + const prev = this.data.pv_net_surplus_factor ?? 1.0; + this.data.pv_net_surplus_factor = Math.min(1.1, Math.max(0.4, alpha * ratio + (1 - alpha) * prev)); + this.homey.log(`[Learning] pvNetSurplus: date=${dateStr} predicted=${pending.predicted.toFixed(1)}kWh actual=${actualKwh.toFixed(1)}kWh ratio=${ratio.toFixed(2)} → factor=${this.data.pv_net_surplus_factor.toFixed(2)} (was ${prev.toFixed(2)})`); + this.data.pv_net_surplus_pending = null; + this._saveData().catch(() => {}); + } + + getPvNetSurplusAccuracyFactor() { + return this.data.pv_net_surplus_factor ?? 1.0; + } + + /** + * 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..95120c59 --- /dev/null +++ b/lib/merged-price-provider.js @@ -0,0 +1,339 @@ +'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() { + // Debounce 5min — homey.settings.set allocates ~30 MB V8 heap per call + // (framework-internal). In-memory this.cache is already the primary source; + // persistence only serves restart recovery. + if (this._saveCacheTimer) clearTimeout(this._saveCacheTimer); + this._saveCacheTimer = setTimeout(() => { + this._saveCacheTimer = null; + try { + this.homey.settings.set('merged_price_cache', { + prices: this.cache, + 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); + } + }, 5 * 60 * 1000); + } + + // ─── 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..601dcaf4 --- /dev/null +++ b/lib/optimization-engine.js @@ -0,0 +1,766 @@ +'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. + * + * SoC grid: + * Internal representation uses a 10x finer grid (GRID=10, 0–1000 steps = + * 0.0–100.0% in 0.1% increments). This prevents the old integer-rounding + * clamp (Math.max(1, round(delta))) from inflating the apparent SoC cost of + * each discharge slot on large multi-battery setups with modest consumption. + * Example: 4 batteries (10.752 kWh) + 291W load + 15-min slot → true delta + * = 0.68% → old code rounded to 1% (47% overestimate) → DP wrongly preferred + * preserve. With GRID=10 the delta becomes grid-unit 7 (=0.7%), accurate to + * 0.02% and the DP makes correct economic decisions. + */ + +const GRID = 10; // 10 grid units per SoC percent → 0.1% resolution + +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, projectedProfit: 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, consumptionMargin = 1.0, pvKwhTomorrow = 0, terminalPvKwhTomorrow = pvKwhTomorrow, pvCloudFactor = 1.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 grid setup ───────────────────────────────────────────────────────── + const GRID_TOTAL = GRID * 100; + const minSocG = Math.round(this.minSoc * GRID); + const maxSocG = Math.round(this.maxSoc * GRID); + + // Pre-compute per-slot PV coverage (0–1): fraction of charge power covered by PV SURPLUS. + // Net surplus = max(0, pvW − consW): PV first serves house load; only the remainder + // enters the battery (zero_charge_only). Using raw pvW would overstate free charge. + const pvIndex = this._buildPvIndex(pvForecast); + const pvWPerSlot = []; + const pvCoverage = prices.map((p, t) => { + const pvW = this._getPvForSlot(pvIndex, new Date(p.timestamp).getTime()); + const consW = Math.max(50, Array.isArray(consumptionWPerSlot) && consumptionWPerSlot[t] != null + ? consumptionWPerSlot[t] : 50); // 50W baseload floor (unlearned and zero-learned slots) + pvWPerSlot.push(pvW); + const raw = Math.min(1, Math.max(0, pvW - consW) / maxChargePowerW); + return raw * (pvCloudFactor ?? 1.0); + }); + + // Remaining fraction of the current slot: when recomputing mid-slot the DP must + // model only the remaining time, otherwise it overestimates the benefit of preserve + // (treats a partial slot as a full 0.8 kWh charging opportunity, biasing it to defer + // charging to the next slot even when that slot is only marginally better). + const firstSlotMs = new Date(prices[0].timestamp).getTime(); + const slotDurationMs = slotH * 3_600_000; + const slot0RemainingFrac = Math.max(0.01, Math.min(1.0, + (firstSlotMs + slotDurationMs - Date.now()) / slotDurationMs)); + + const { dp, policy, chargeSocDeltaG, perSlotDischargeSocDeltaG, chargeKwhFull, pvStrongCoverage, + slot0ChargeSocDeltaG, slot0ChargeKwhFull } = + this._runBackwardDP(N, prices, pvCoverage, consumptionWPerSlot, slotH, + capacityKwh, maxChargePowerW, maxDischargePowerW, + effectiveRte, cycleCostPerKwh, this.exportPriceRatio ?? 1.0, + minDischargePrice, maxSocG, minSocG, consumptionMargin, pvKwhTomorrow, + slot0RemainingFrac, pvWPerSlot, terminalPvKwhTomorrow); + + const initialSocG = Math.max(0, Math.min(GRID_TOTAL, Math.round(currentSoc * GRID))); + const projectedProfit = dp[initialSocG] ?? 0; + + // ── Forward pass: trace the optimal path from currentSoc ────────────────── + const slots = []; + const ACTIONS = ['preserve', 'charge', 'discharge', 'standby']; + let socG = initialSocG; + + // Today-only profit: revenue from discharge − cost of grid charge, same day (Amsterdam). + // Excludes terminal value and tomorrow slots — makes tracking comparable to cycle_history. + const todayStr = new Date().toLocaleString('en-CA', { timeZone: 'Europe/Amsterdam' }).slice(0, 10); + let todayProjectedProfit = 0; + + // Suffix-max of future positive prices — used to approximate _pvStoreWins per slot. + // When max_future × RTE > current price, the runtime policy will charge from PV surplus + // even if the DP chose standby (export). We simulate this so the planning display matches + // actual behaviour (the DP backward pass is unchanged). + const suffixMaxPrice = new Array(N).fill(0); + for (let k = N - 2; k >= 0; k--) { + const p = prices[k + 1]?.price ?? 0; + suffixMaxPrice[k] = Math.max(p > 0 ? p : 0, suffixMaxPrice[k + 1]); + } + // Trickle-capped suffix max: same as suffixMaxPrice but resets to 0 at pvStrong slots. + // PV-strong hours refill the battery independently — trickle energy stored before them + // cannot reach high-price slots beyond the saturation point (those kWh come from PV). + // Without this cap the trickle check picks up tomorrow's peak price even when tomorrow's + // PV will fill the battery anyway, making today's trickle appear more valuable than it is. + const trickleSuffixMaxPrice = new Array(N).fill(0); + for (let k = N - 2; k >= 0; k--) { + if (pvCoverage[k + 1] >= pvStrongCoverage) { + trickleSuffixMaxPrice[k] = 0; + } else { + const p = prices[k + 1]?.price ?? 0; + trickleSuffixMaxPrice[k] = Math.max(p > 0 ? p : 0, trickleSuffixMaxPrice[k + 1]); + } + } + // _pvStoreWins is suppressed at runtime when strongly negative prices are coming. + const hasFutureStrongNeg = prices.some(p => (p.price ?? 0) < -0.10); + + for (let t = 0; t < N; t++) { + const code = policy[t][socG]; + const price = prices[t].price; + let action = ACTIONS[code]; + + // Standby + strong PV surplus: check if _pvStoreWins would apply at runtime. + // Condition: max future price × RTE > current price (simplified; ignores delay-charge). + // When true, override action to 'preserve' so buildPlanningSchedule shows zero_charge_only, + // and update SoC so downstream projections are accurate. + let pvStoreWins = false; + if (code === 3 && !hasFutureStrongNeg && pvCoverage[t] >= pvStrongCoverage && price > 0) { + const pvStoreValue = suffixMaxPrice[t] * effectiveRte; + if (pvStoreValue > price) { + action = 'preserve'; + pvStoreWins = true; + } + } + // Weak PV trickle: standby + net surplus below pvStrong threshold. + // Use trickleSuffixMaxPrice (not suffixMaxPrice) so prices beyond the first pvStrong + // saturation slot are excluded — those slots are served by free PV, not today's trickle. + const pvStoreValue2 = trickleSuffixMaxPrice[t] * effectiveRte; + const trickleWins = pvStoreValue2 > price * (this.exportPriceRatio ?? 1.0); + const pvTrickle = !pvStoreWins && trickleWins && code === 3 && pvCoverage[t] > 0; + if (pvTrickle) action = 'trickle'; + + const pvExportWins = !pvStoreWins && !pvTrickle && code === 3 && pvCoverage[t] > 0; + slots.push({ timestamp: prices[t].timestamp, action, price, socProjected: socG / GRID, pvExportWins, + consumptionW: Array.isArray(consumptionWPerSlot) ? (consumptionWPerSlot[t] ?? null) : null, + pvStoreValue: suffixMaxPrice[t] * effectiveRte, + pvTrickleMaxValue: trickleSuffixMaxPrice[t] * effectiveRte, + pvCoverage: pvCoverage[t] }); + + const isToday = new Date(prices[t].timestamp) + .toLocaleString('en-CA', { timeZone: 'Europe/Amsterdam' }).slice(0, 10) === todayStr; + + const slotDeltaG = t === 0 ? slot0ChargeSocDeltaG : chargeSocDeltaG; + const slotKwhFull = t === 0 ? slot0ChargeKwhFull : chargeKwhFull; + if (code === 1) { + if (isToday) { + const actualDeltaG = Math.min(slotDeltaG, maxSocG - socG); + const gridChargeKwh = (actualDeltaG / slotDeltaG) * slotKwhFull * (1 - pvCoverage[t]); + todayProjectedProfit -= price * gridChargeKwh; + } + socG = Math.min(maxSocG, socG + slotDeltaG); + } else if (code === 2) { + if (isToday) { + const actualDeltaG = Math.min(perSlotDischargeSocDeltaG[t], socG - minSocG); + const dischargeKwh = (actualDeltaG / GRID_TOTAL) * capacityKwh; + todayProjectedProfit += price * dischargeKwh * effectiveRte; + } + socG = Math.max(minSocG, socG - perSlotDischargeSocDeltaG[t]); + } else if (code === 0 && pvCoverage[t] >= pvStrongCoverage && prices[t].price >= 0) { + // Preserve + PV strong enough for zero_charge_only: battery charges for free. + socG = Math.min(maxSocG, socG + Math.round(pvCoverage[t] * slotDeltaG)); + } else if (pvStoreWins || pvTrickle) { + // pvStoreWins / pvTrickle: simulate free PV charging for accurate SoC projection. + socG = Math.min(maxSocG, socG + Math.round(pvCoverage[t] * slotDeltaG)); + } + } + + // ── Post-DP: reorder night-window discharge by price ───────────────────── + // The backward-DP flatten can mis-order discharge when night prices are + // non-monotone (e.g. 23:00=€0.272 < midnight=€0.278). After the forward + // pass, find the first contiguous non-PV block (evening → overnight, until + // pvCoverage ≥ pvStrongCoverage) and sort eligible discharge slots by price, + // assigning discharge to the most expensive slots within the battery budget. + // Total energy used in the window stays within the same budget. + { + let wEnd = 0; + for (let t = 0; t < N; t++) { + if (pvCoverage[t] >= pvStrongCoverage) break; + wEnd = t + 1; + } + if (wEnd >= 2) { + const eligible = []; + for (let t = 0; t < wEnd; t++) { + const effMin = Array.isArray(minDischargePrice) ? (minDischargePrice[t] ?? 0) : minDischargePrice; + if (slots[t].price >= effMin) eligible.push(t); + } + // Only reorder in pure discharge windows — skip if ANY window slot charges + // (charging between slots increases SoC, invalidating the simple budget model + // and risking SoC safety violations in the rewritten trajectory). + const hasCharge = slots.slice(0, wEnd).some(s => s.action === 'charge'); + if (eligible.length >= 2 && !hasCharge) { + eligible.sort((a, b) => slots[b].price - slots[a].price); + const assigned = new Set(); + let remainSocG = initialSocG; + for (const t of eligible) { + if (remainSocG <= minSocG) break; + assigned.add(t); + remainSocG = Math.max(minSocG, remainSocG - perSlotDischargeSocDeltaG[t]); + } + // Only rewrite if ordering differs from DP result + let changed = false; + for (let t = 0; t < wEnd && !changed; t++) { + if ((slots[t].action === 'discharge') !== assigned.has(t)) changed = true; + } + if (changed) { + let socGw = initialSocG; + for (let t = 0; t < wEnd; t++) { + if (assigned.has(t) && socGw > minSocG) { + // Budget assigned discharge here AND time-ordered SoC is available + slots[t].action = 'discharge'; + } else if (slots[t].action === 'discharge') { + // Un-assign: budget placed discharge elsewhere, or SoC exhausted + slots[t].action = 'preserve'; + } + slots[t].socProjected = socGw / GRID; + const slotDeltaG = t === 0 ? slot0ChargeSocDeltaG : chargeSocDeltaG; + if (slots[t].action === 'discharge') { + socGw = Math.max(minSocG, socGw - perSlotDischargeSocDeltaG[t]); + } else if (slots[t].action === 'charge') { + socGw = Math.min(maxSocG, socGw + slotDeltaG); + } else if (pvCoverage[t] > 0 && prices[t].price >= 0) { + socGw = Math.min(maxSocG, socGw + Math.round(pvCoverage[t] * slotDeltaG)); + } + } + } + } + } + } + + // ── Post-DP: eliminate isolated preserve islands in discharge sequences ── + // Single preserve slot flanked by discharge on both sides with tiny price + // delta (<5ct) is a DP numerical edge case — override to discharge. + { + for (let t = 1; t < N - 1; t++) { + if (slots[t].action !== 'preserve') continue; + if (slots[t - 1].action !== 'discharge') continue; + if (slots[t + 1].action !== 'discharge') continue; + const effMin = Array.isArray(minDischargePrice) ? (minDischargePrice[t] ?? 0) : minDischargePrice; + if (slots[t].price < effMin) continue; + const priceDelta = Math.max(slots[t - 1].price, slots[t + 1].price) - slots[t].price; + if (priceDelta > 0.01) continue; + slots[t].action = 'discharge'; + const socDrop = perSlotDischargeSocDeltaG[t] / GRID; + for (let u = t + 1; u < N; u++) { + slots[u].socProjected = Math.max(minSocG / GRID, slots[u].socProjected - socDrop); + } + } + } + + this._schedule = { computedAt: Date.now(), projectedProfit, todayProjectedProfit, slots }; + } + + /** + * Compute expected 24h profit for a given battery config WITHOUT modifying _schedule. + * Safe to call after compute() — live policy decisions are not affected. + * + * Used for "what if" analysis (e.g. a second battery with higher capacity/power). + * + * @returns {number} Expected gross profit in € for the next 24h horizon, or 0 on error. + */ + computeExpectedProfit(prices, currentSoc, capacityKwh, maxChargePowerW, maxDischargePowerW, pvForecast, rte = null, consumptionWPerSlot = null, minDischargePrice = 0, consumptionMargin = 1.0, pvKwhTomorrow = 0) { + if (!prices || prices.length === 0 || !capacityKwh || capacityKwh <= 0) return { profit: 0, selfSufficiencyPct: 0 }; + + const N = prices.length; + 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; + + const GRID_TOTAL = GRID * 100; + const minSocG = Math.round(this.minSoc * GRID); + const maxSocG = Math.round(this.maxSoc * GRID); + + // Keep raw pvW per slot for the self-sufficiency forward pass (house supply). + // pvCoverage uses NET surplus (pvW − consW) so we can't derive raw pvW from it. + const pvIndex2 = this._buildPvIndex(pvForecast); + const pvWPerSlot = prices.map(p => this._getPvForSlot(pvIndex2, new Date(p.timestamp).getTime())); + const pvCoverage = pvWPerSlot.map((pvW, t) => { + const consW = Math.max(50, Array.isArray(consumptionWPerSlot) && consumptionWPerSlot[t] != null + ? consumptionWPerSlot[t] : 50); // 50W baseload floor (unlearned and zero-learned slots) + return Math.min(1, Math.max(0, pvW - consW) / maxChargePowerW); + }); + + const { dp, policy, chargeSocDeltaG, perSlotDischargeSocDeltaG, pvStrongCoverage } = + this._runBackwardDP(N, prices, pvCoverage, consumptionWPerSlot, slotH, + capacityKwh, maxChargePowerW, maxDischargePowerW, + effectiveRte, cycleCostPerKwh, this.exportPriceRatio ?? 1.0, + minDischargePrice, maxSocG, minSocG, consumptionMargin, pvKwhTomorrow); + + const initialSocG = Math.max(0, Math.min(GRID_TOTAL, Math.round(currentSoc * GRID))); + const profit = dp[initialSocG] ?? 0; + + // ── Forward pass: compute self-sufficiency % ──────────────────────────── + // Track how much house consumption is covered by PV + battery discharge + // vs. imported from grid. Battery charge demand is excluded from house load. + let totalConsKwh = 0; + let totalGridImportKwh = 0; + let socG = initialSocG; + + for (let t = 0; t < N; t++) { + const consW = Array.isArray(consumptionWPerSlot) && consumptionWPerSlot[t] != null + ? consumptionWPerSlot[t] : 0; + const pvW = pvWPerSlot[t]; // raw PV watts (from inverter; house load served first) + const code = policy[t][socG]; + + let batterySupplyW = 0; + if (code === 2) { + // Discharge: battery supplies up to effectiveDischargePower (already capped by consumption in DP) + const effectiveDischW = Array.isArray(consumptionWPerSlot) && consumptionWPerSlot[t] != null + ? Math.min(maxDischargePowerW, consumptionWPerSlot[t]) + : maxDischargePowerW; + batterySupplyW = effectiveDischW; + } + + // PV available for house: during charge, part of PV goes to battery + let pvForHouseW = pvW; + if (code === 1) { + // Charge: PV first covers battery demand, rest goes to house + pvForHouseW = Math.max(0, pvW - maxChargePowerW); + } else if (code === 0 && pvCoverage[t] >= pvStrongCoverage) { + // Preserve with strong PV: zero_charge_only — only surplus (pvW − consW) to battery + const pvToBatteryW = Math.min(maxChargePowerW, Math.max(0, pvW - consW)); + pvForHouseW = Math.max(0, pvW - pvToBatteryW); + } + + const suppliedW = pvForHouseW + batterySupplyW; + const gridImportW = Math.max(0, consW - suppliedW); + + totalConsKwh += consW * slotH / 1000; + totalGridImportKwh += gridImportW * slotH / 1000; + + // Advance SoC (same as compute() forward pass) + if (code === 1) socG = Math.min(maxSocG, socG + chargeSocDeltaG); + else if (code === 2) socG = Math.max(minSocG, socG - perSlotDischargeSocDeltaG[t]); + else if (code === 0 && pvCoverage[t] >= pvStrongCoverage) { + socG = Math.min(maxSocG, socG + Math.round(pvCoverage[t] * chargeSocDeltaG)); + } + // code === 3 (standby): socG unchanged + } + + const selfSufficiencyPct = totalConsKwh > 0 + ? Math.round((1 - totalGridImportKwh / totalConsKwh) * 100) + : 100; + + return { profit, selfSufficiencyPct }; + } + + /** + * Backward-induction DP kernel. Returns the dp value array and the policy table. + * Also returns derived constants needed by the forward pass in compute(). + * Does NOT touch any instance state — fully side-effect-free. + * @private + */ + _runBackwardDP(N, prices, pvCoverage, consumptionWPerSlot, slotH, + capacityKwh, maxChargePowerW, maxDischargePowerW, + effectiveRte, cycleCostPerKwh, exportPriceRatio, + minDischargePrice, maxSocG, minSocG, consumptionMargin = 1.0, pvKwhTomorrow = 0, + slot0RemainingFrac = 1.0, pvWPerSlot = null, terminalPvKwhTomorrow = pvKwhTomorrow) { + const GRID_TOTAL = GRID * 100; + + // SoC delta per full charge slot in grid units. + // RTE losses belong on the discharge side (firmware reports physical SoC). + // Applying RTE here would make each charge step too small, requiring extra + // slots to fill — e.g. 5×1h instead of the physical 4×1h at 800W / 2.688 kWh. + const chargeSocDeltaG = Math.max(1, Math.round( + (maxChargePowerW / 1000) * slotH * 100 / capacityKwh * GRID + )); + const chargeKwhFull = (maxChargePowerW / 1000) * slotH; + + // Slot 0 may cover only a fraction of a full slot when recomputing mid-slot. + const slot0ChargeSocDeltaG = Math.max(1, Math.round(chargeSocDeltaG * slot0RemainingFrac)); + const slot0ChargeKwhFull = chargeKwhFull * slot0RemainingFrac; + + // Per-slot effective discharge power: limited by house consumption so the + // battery doesn't discharge faster than the load can absorb it. + // The firmware enforces "nul op de meter" — the battery never exports to the grid, + // so the physical discharge rate is capped by actual house consumption. + // consumptionMargin inflates the predicted load for a conservative SoC projection + // (actual consumption is often higher than the learned average, e.g. dishwasher, cooking). + // consumptionMargin can be a single number (uniform) or an array (per-slot). + const effectiveDischargePowerW = prices.map((_, t) => { + const margin = Array.isArray(consumptionMargin) ? (consumptionMargin[t] ?? 1.20) : consumptionMargin; + const consumptionW = Array.isArray(consumptionWPerSlot) && consumptionWPerSlot[t] != null + ? consumptionWPerSlot[t] * margin + : maxDischargePowerW; + // Subtract PV from net load: battery only needs to cover consumption not served by PV. + const pvW = pvWPerSlot?.[t] ?? 0; + const netW = Math.max(0, consumptionW - pvW); + return Math.min(maxDischargePowerW, netW); + }); + + const perSlotDischargeSocDeltaG = effectiveDischargePowerW.map(w => + Math.max(0, Math.round((w / 1000) * slotH * 100 / capacityKwh * GRID)) + ); + const perSlotDischargeKwhFull = effectiveDischargePowerW.map(w => + (w / 1000) * slotH + ); + + // 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. + const pvStrongCoverage = maxChargePowerW > 0 ? 400 / maxChargePowerW : 0.15; + + // Pre-compute cumulative PV kWh that can enter the battery from each slot onwards. + // pvCoverage[t] is clamped to [0,1], so pvCoverage[t] * chargeKwhFull = kWh absorbed. + // When pvKwhFromT[t+1] >= capacityKwh, any starting SoC at t+1 will be fully replenished + // by PV before horizon end — the SoC arriving at t+1 becomes irrelevant for future value. + const pvKwhFromT = new Float64Array(N + 1); // pvKwhFromT[N] = 0 by default + for (let t = N - 1; t >= 0; t--) { + pvKwhFromT[t] = pvKwhFromT[t + 1] + pvCoverage[t] * (t === 0 ? slot0ChargeKwhFull : chargeKwhFull); + } + + // ── Terminal value: residual worth of stored energy at horizon end ──────── + // top-quartile price × 0.8 × RTE × kWh, discounted by PV refill forecast. + // pvRefill ≥ 80% → terminal = 0 (PV fills battery for free, no hoarding). + let dp = new Float64Array(GRID_TOTAL + 1).fill(0); + if (N >= 4) { + const pvRefill = capacityKwh > 0 ? Math.min(1, terminalPvKwhTomorrow / capacityKwh) : 0; + // Full PV day (≥80% refill) → terminal = 0, no residual value + if (pvRefill < 0.8) { + const sortedPrices = prices.map(p => p.price).filter(p => p > 0).sort((a, b) => b - a); + const topQuartile = sortedPrices.slice(0, Math.max(1, Math.floor(sortedPrices.length / 4))); + const terminalPricePerKwh = topQuartile.length > 0 + ? (topQuartile.reduce((a, b) => a + b, 0) / topQuartile.length) * 0.8 * effectiveRte + : 0; + if (terminalPricePerKwh > 0) { + // Scale down linearly: pvRefill 0→0% discount, pvRefill 0.8→100% discount + const terminalFactor = Math.max(0, 1 - pvRefill / 0.8); + for (let socG = 0; socG <= GRID_TOTAL; socG++) { + const kwhStored = (socG / GRID / 100) * capacityKwh; + dp[socG] = terminalPricePerKwh * kwhStored * terminalFactor; + } + } + } + // else: pvRefill >= 0.8 → dp stays all zeros (battery worthless at end of horizon) + } + + // policy[t][socG] = best action code: 0 = preserve, 1 = charge, 2 = discharge + const policy = Array.from({ length: N }, () => new Uint8Array(GRID_TOTAL + 1)); + + // Pre-allocate second buffer for ping-pong — avoids 96× Float64Array allocation per DP run. + let dp2 = new Float64Array(GRID_TOTAL + 1); + + // ── Backward induction ──────────────────────────────────────────────────── + for (let t = N - 1; t >= 0; t--) { + // Per-SoC flattening: neutralise backward pressure from future high prices when PV + // can recharge the battery from the current SoC level back to full. + // + // Old approach: flatten only when pvKwhFromT >= full capacityKwh AND no better night + // slot ahead. This caused overnight standby even with 65% SoC and ample PV, because: + // (a) rising pre-dawn prices kept _betterSlotAhead=true all night, and + // (b) pvKwhFromT (net surplus) is often < capacityKwh even on sunny days. + // + // New approach: for each SoC level independently, if remaining PV can cover the gap + // from that level to maxSoc, discharge from there has zero opportunity cost — the + // battery will be fully recharged before the next discharge window regardless. + // This allows the DP to discharge at all profitable overnight slots (price >= floor) + // and let PV handle the recharge, instead of holding a single "best" slot. + // + // Guards kept from the old logic: + // - positive price only (negative prices need SoC gradient for deeper-slot selection) + // - pvKwhTomorrow >= 80% cap (confidence in PV day) + // - non-PV slot (pvCoverage < pvStrongCoverage): during strong PV the natural + // vPreserve SoC-gain already equalises dp; flattening there hides evening value + // - next slot not PV-strong: the PV-boundary gradient encodes export revenue + // (higher SoC → more PV exported at morning price vs stored) + if (prices[t].price >= 0 && pvKwhTomorrow >= capacityKwh * 0.6 + && pvCoverage[t] < pvStrongCoverage + && (t + 1 >= N || pvCoverage[t + 1] < pvStrongCoverage) + && pvKwhFromT[t + 1] > 0) { + let dpMax = dp[0]; + for (let i = 1; i <= GRID_TOTAL; i++) { if (dp[i] > dpMax) dpMax = dp[i]; } + for (let sg = 0; sg <= GRID_TOTAL; sg++) { + const kwhNeeded = (maxSocG - sg) / GRID_TOTAL * capacityKwh; + if (pvKwhFromT[t + 1] >= kwhNeeded) dp[sg] = dpMax; + } + } + + const price = prices[t].price; + // Negative price: earn money by importing — use full price regardless of PV coverage. + // Positive price: true cost = grid portion (price × (1−pvCoverage)) + foregone PV export + // (price × pvCoverage × exportPriceRatio) = price × (1 − pvCoverage × (1 − exportPriceRatio)). + // At exportPriceRatio=1.0 (NL saldering) this simplifies to `price`, making vCharge + // symmetric with vPreserve: charging from PV costs the same as charging from grid, + // because the PV energy was worth exactly `price` if exported instead. + // At exportPriceRatio<1.0 (post-2027) charging from PV gets a partial discount equal + // to the spread between retail and export price. + const isNegativePrice = price < 0; + const effectiveChargeCost = isNegativePrice + ? price + : price * (1 - pvCoverage[t] * (1 - exportPriceRatio)); + dp2.fill(-1e9); + + // Slot 0 uses scaled charge parameters when recomputing mid-slot. + const effDeltaG = t === 0 ? slot0ChargeSocDeltaG : chargeSocDeltaG; + const effKwhFull = t === 0 ? slot0ChargeKwhFull : chargeKwhFull; + + for (let socG = 0; socG <= GRID_TOTAL; socG++) { + + // 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. + // At negative prices, suppress the free PV SoC gain: the DP needs an "idle" + // option so it can save capacity for deeper negative-price charging slots. + // (Users typically disable their inverter at negative prices; even when on, + // the economic optimum is to reserve room for the most negative hours.) + const pvSocGainG = (!isNegativePrice && pvCoverage[t] >= pvStrongCoverage) + ? Math.round(pvCoverage[t] * effDeltaG) + : 0; + const preserveSocG = Math.min(maxSocG, socG + pvSocGainG); + // When battery hits maxSoc before absorbing all available PV, the surplus exports + // to grid. Add that export revenue so preserve correctly beats standby: both fill + // the battery AND export the remainder at the same price. + let vPreserve = dp[preserveSocG]; + if (pvSocGainG > 0 && price > 0) { + const storedKwh = Math.min(pvSocGainG, maxSocG - socG) / effDeltaG * effKwhFull; + vPreserve -= storedKwh * price * exportPriceRatio; + if (preserveSocG < socG + pvSocGainG) { + const surplusKwh = (socG + pvSocGainG - preserveSocG) / effDeltaG * effKwhFull; + vPreserve += surplusKwh * price * exportPriceRatio; + } + } + + // 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 price sign. + let vCharge = -1e9; + if (socG < maxSocG) { + const newSocG = Math.min(maxSocG, socG + effDeltaG); + const socDeltaG = newSocG - socG; // may be less than effDeltaG near the top + const kwh = effKwhFull * socDeltaG / effDeltaG; + vCharge = -(effectiveChargeCost + cycleCostPerKwh * 0.5) * kwh + dp[newSocG]; + } + + // 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). + // Discharge is allowed whenever price >= minDischargePrice — the pvCoverage block + // was removed because it incorrectly suppressed discharge during delay-charge hours. + // 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. + const effectiveMinDischarge = Array.isArray(minDischargePrice) + ? (minDischargePrice[t] ?? 0) : minDischargePrice; + let vDischarge = -1e9; + if (socG > minSocG && price >= effectiveMinDischarge) { + const slotDischargeSocDeltaG = perSlotDischargeSocDeltaG[t]; + const slotDischargeKwhFull = perSlotDischargeKwhFull[t]; + if (slotDischargeSocDeltaG > 0) { + const newSocG = Math.max(minSocG, socG - slotDischargeSocDeltaG); + const socDeltaG = socG - newSocG; + const kwh = slotDischargeKwhFull * socDeltaG / slotDischargeSocDeltaG; + // null = no data (assume full local offset); 0 = learned zero consumption (all export) + // Use net consumption (after PV): battery only offsets load not served by PV. + const pvWSlot = pvWPerSlot?.[t] ?? 0; + const consumptionKwh = Array.isArray(consumptionWPerSlot) && consumptionWPerSlot[t] != null + ? (Math.max(0, consumptionWPerSlot[t] - pvWSlot) / 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. + // RTE applied on discharge: physical kWh from battery × RTE = net kWh delivered. + let dischargeValue; + if (consumptionKwh != null && exportPriceRatio < 1.0) { + const coveredKwh = Math.min(kwh, consumptionKwh); + const exportKwh = kwh - coveredKwh; + dischargeValue = (price * coveredKwh + price * exportPriceRatio * exportKwh) * effectiveRte; + } else { + dischargeValue = price * kwh * effectiveRte; + } + // Half-cycle cost on discharge. Waived at night when PV tomorrow ≥ 150% + // capacity — free PV refill means holding energy just clips tomorrow's PV. + const pvAbundant = pvKwhTomorrow >= capacityKwh * 1.5 && pvCoverage[t] < 0.1; + const slotCycleCost = pvAbundant ? 0 : cycleCostPerKwh * 0.5; + vDischarge = dischargeValue - slotCycleCost * kwh + dp[newSocG]; + } + } + + // Standby: PV exports to grid at current price; battery SoC unchanged. + // Competes with preserve/charge when current export price + future flexibility + // exceeds the marginal value of storing PV now. + // Only at positive prices — at negative prices, exporting PV costs money. + // Uses raw pvCoverage (not pvSocGainG) so weak morning PV (< pvStrongCoverage) + // still contributes an export value and the standby option remains available. + let vStandby = -1e9; + if (price > 0 && pvCoverage[t] > 0) { + const pvGainKwh = pvCoverage[t] * effKwhFull; + vStandby = pvGainKwh * price * exportPriceRatio + dp[socG]; + } + + // Pick best action + let best = vPreserve, bestAction = 0; + if (vCharge > best) { best = vCharge; bestAction = 1; } + if (vDischarge > best) { best = vDischarge; bestAction = 2; } + if (vStandby > best) { best = vStandby; bestAction = 3; } + + dp2[socG] = best > -1e9 ? best : 0; + policy[t][socG] = bestAction; + } + + // Ping-pong: swap dp and dp2 (no allocation) + const tmp = dp; dp = dp2; dp2 = tmp; + } + + return { dp, policy, chargeSocDeltaG, perSlotDischargeSocDeltaG, chargeKwhFull, pvStrongCoverage, slot0ChargeSocDeltaG, slot0ChargeKwhFull }; + } + + /** + * Pre-convert pvForecast timestamps to milliseconds for O(log N) slot lookup. + * Call once per compute() / computeExpectedProfit() invocation. + * @private + */ + _buildPvIndex(pvForecast) { + if (!Array.isArray(pvForecast) || pvForecast.length === 0) return null; + const idx = pvForecast + .map(s => ({ ms: new Date(s.timestamp).getTime(), pvPowerW: s.pvPowerW })) + .sort((a, b) => a.ms - b.ms); + return idx; + } + + /** + * Interpolate PV power (W) for a given price-slot timestamp using a pre-built index. + * Returns 0 when no PV data is available. + * @private + */ + _getPvForSlot(pvIndex, slotMs) { + if (!pvIndex || pvIndex.length === 0) return 0; + // Binary search for the last entry with ms <= slotMs + let lo = 0, hi = pvIndex.length - 1, leftIdx = -1; + while (lo <= hi) { + const mid = (lo + hi) >>> 1; + if (pvIndex[mid].ms <= slotMs) { leftIdx = mid; lo = mid + 1; } + else hi = mid - 1; + } + if (leftIdx === -1) return pvIndex[0].pvPowerW; + if (leftIdx === pvIndex.length - 1) return pvIndex[leftIdx].pvPowerW; + const left = pvIndex[leftIdx]; + const right = pvIndex[leftIdx + 1]; + if (left.ms === right.ms) return left.pvPowerW; + const frac = (slotMs - left.ms) / (right.ms - left.ms); + return Math.round(left.pvPowerW + (right.pvPowerW - left.pvPowerW) * frac); + } + + /** + * 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'|'standby'|null} + */ + getSlot(now) { + if (!this._schedule) return null; + + const nowMs = now.getTime(); + const slots = this._schedule.slots; + const slotMs = slots.length >= 2 + ? Math.abs(new Date(slots[1].timestamp) - new Date(slots[0].timestamp)) + : 60 * 60 * 1000; + const maxDist = Math.max(slotMs, 35 * 60 * 1000); + + // Prefer the active slot: the most recent past slot within maxDist. + // This ensures that at e.g. 09:30 we use the 09:00 slot's action (standby), + // not the 10:00 slot's action (charge) — even though 10:00 is 1 second closer. + let best = null; + for (const slot of slots) { + const ts = new Date(slot.timestamp).getTime(); + if (ts <= nowMs && (!best || ts > new Date(best.timestamp).getTime())) best = slot; + } + if (best && nowMs - new Date(best.timestamp).getTime() <= maxDist) return best.action; + + // Fallback: nearest slot (used when schedule starts in the future, e.g. 21:24 → 22:00) + let bestDist = Infinity; + best = null; + for (const slot of slots) { + const dist = Math.abs(new Date(slot.timestamp).getTime() - nowMs); + if (dist < bestDist) { bestDist = dist; best = slot; } + } + return (best && bestDist <= maxDist) ? best.action : null; + } + + /** Returns metadata for the current slot (pvExportWins, etc.), or null. */ + getSlotMeta(now) { + if (!this._schedule) return null; + const nowMs = now.getTime(); + const slots = this._schedule.slots; + const slotMs = slots.length >= 2 + ? Math.abs(new Date(slots[1].timestamp) - new Date(slots[0].timestamp)) + : 60 * 60 * 1000; + const maxDist = Math.max(slotMs, 35 * 60 * 1000); + let best = null; + for (const slot of slots) { + const ts = new Date(slot.timestamp).getTime(); + if (ts <= nowMs && (!best || ts > new Date(best.timestamp).getTime())) best = slot; + } + if (best && nowMs - new Date(best.timestamp).getTime() <= maxDist) return best; + let bestDist = Infinity; + best = null; + for (const slot of slots) { + const dist = Math.abs(new Date(slot.timestamp).getTime() - nowMs); + if (dist < bestDist) { bestDist = dist; best = slot; } + } + return (best && bestDist <= maxDist) ? best : 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/policy-engine.js b/lib/policy-engine.js new file mode 100644 index 00000000..6b8b262e --- /dev/null +++ b/lib/policy-engine.js @@ -0,0 +1,1602 @@ +'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.22; + 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) { + this.log('--- POLICY RUN START ---'); + if (debug) this.log('Inputs:', JSON.stringify(inputs, null, 2)); + + // ── Common setup ─────────────────────────────────────────────────────���─── + this.maxDischarge = inputs.battery?.maxDischargePowerW || 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; + + // Cost model reset at min SoC — 2% hysteresis prevents spurious reset when SoC + // briefly touches minSoc (e.g. PV kick-in during discharge boundary). + if (inputs.batteryCost) { + if (soc <= Math.max(minSoc - 2, 0.5)) { + if (inputs.batteryCost.avgCost !== 0 || inputs.batteryCost.energyKwh !== 0) { + this.log(`[COST][RESET] SoC ${soc}% <= min_soc ${minSoc}% - 2% → battery empty, resetting cost model`); + } + inputs.batteryCost.avgCost = 0; + inputs.batteryCost.energyKwh = 0; + inputs.batteryCost.breakEven = 0; + } + } + + // Pre-compute effectiveMinDischarge + { + const _respectMinMax = inputs.policyMode === 'balanced-dynamic' + ? false + : (this.settings.respect_minmax !== false); + inputs.respectMinMax = _respectMinMax; + if (_respectMinMax) { + inputs.effectiveMinDischarge = this.settings.min_discharge_price || 0.22; + } else { + const _configuredEff = this.settings.battery_efficiency || 0.75; + const _learnedEff = inputs.batteryEfficiency ?? _configuredEff; + const _effectiveEff = Math.min(_configuredEff, _learnedEff, 0.95); + const _cycleCost = this.settings.cycle_cost_per_kwh ?? 0.075; + const _settingsFloor = _cycleCost / _configuredEff; + if (inputs.batteryCost?.avgCost > 0 && !inputs.batteryCost.breakEven) { + inputs.batteryCost.breakEven = inputs.batteryCost.avgCost / _effectiveEff + _cycleCost * 0.5; + } + inputs.effectiveMinDischarge = (inputs.batteryCost?.breakEven > 0) + ? inputs.batteryCost.breakEven + : _settingsFloor; + } + } + + inputs.dynamicMaxChargePrice = this._getDynamicChargePrice(inputs.tariff, inputs.tariff?.currentPrice); + + // ── PV flags: _delayCharge, _pvStoreWins, _pvExporting (mapper inputs) ── + this._computePvFlags(inputs); + + // ── Fixed tariff: rule-based scoring (unchanged) ───────────────────────── + if (this.settings.tariff_type === 'fixed') { + const scores = { charge: 0, discharge: 0, preserve: 0 }; + this._applySmartLowSocRule(scores, inputs); + 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._delayCharge) scores.charge = 0; + 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 this._buildLegacyResult(recommendation, scores, inputs); + } + + // ── Dynamic tariff: DP-primary ──────────────────────────────────────────── + // Update PV sticky timer (used by mapper for standby/zero_charge_only decisions) + this._refreshPvSticky(inputs); + + // Break-even (final computation) + 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 cycleCost = this.settings.cycle_cost_per_kwh ?? 0.075; + const breakEven = inputs.batteryCost.avgCost / effectiveEff + cycleCost * 0.5; + inputs.batteryCost.breakEven = breakEven; + inputs.effectiveMinDischarge = inputs.respectMinMax + ? (this.settings.min_discharge_price || 0.22) + : breakEven; + } + + // ── 1. DP decision ──────────────────────────────────────────────────────── + const dpAction = inputs.optimizer?.getSlot(new Date()) ?? null; + + if (!dpAction) { + this.log('Optimizer: no schedule available → fallback preserve'); + const hwMode = this._mapPolicyToHwMode('preserve', inputs); + this.log('--- POLICY RUN END ---'); + return this._buildDpResult('preserve', hwMode, 40, 'no_optimizer', inputs, null); + } + + this.log(`Optimizer: 24h-DP → ${dpAction}`); + + // ── 2. Exceptions ───────────────────────────────────────────────────────── + let finalAction = dpAction; + let exception = null; + + // Standby: DP chose PV export over battery storage — no exceptions apply, + // unless battery is not yet full and PV storing beats exporting. + // Peak shaving is still checked before returning: a load spike overrides standby. + if (finalAction === 'standby') { + const slotMeta = inputs.optimizer?.getSlotMeta(new Date()); + const pvExportWins = slotMeta?.pvExportWins === true; + if (soc < maxSoc && !pvExportWins) { + this.log(`Exception: standby overridden — battery not full (SoC ${soc}% < max ${maxSoc}%), charging first`); + finalAction = 'charge'; + } else { + if (pvExportWins) this.log(`Standby: pvExportWins — export more profitable than storing, no override`); + const peakThreshold = this.settings.peak_shaving_threshold ?? 0; + if (peakThreshold > 0 && this.currentLoad > peakThreshold && soc > minSoc) { + const peakRange = this._parseTimeRange(this.settings.peak_hours); + const nowHourNL = parseInt(new Date().toLocaleString('en-US', { timeZone: 'Europe/Amsterdam', hour: 'numeric', hour12: false }), 10); + const inPeakWin = !peakRange || (nowHourNL >= peakRange.startHour && nowHourNL < peakRange.endHour); + if (inPeakWin) { + this.log(`Exception: peak shaving (standby) — load ${Math.round(this.currentLoad)}W > threshold ${peakThreshold}W → discharge`); + const hwMode = this._mapPolicyToHwMode('discharge', inputs); + this.log('--- POLICY RUN END ---'); + return this._buildDpResult('discharge', hwMode, 75, 'peak_shaving', inputs, dpAction); + } else { + this.log(`Exception: peak shaving (standby) — load ${Math.round(this.currentLoad)}W > threshold ${peakThreshold}W but outside peak window (${this.settings.peak_hours}) — skipped`); + } + } + const hwMode = this._mapPolicyToHwMode('standby', inputs); + this.log('--- POLICY RUN END ---'); + return this._buildDpResult('standby', hwMode, 90, null, inputs, dpAction); + } + } + + if (finalAction === 'discharge') { + if (soc <= minSoc) { + this.log(`Exception: discharge blocked — SoC ${soc}% <= min_soc ${minSoc}%`); + finalAction = 'preserve'; + exception = 'soc_too_low'; + } + } + + if (finalAction === 'charge') { + if (soc >= maxSoc) { + this.log(`Exception: charge blocked — SoC ${soc}% >= max_soc ${maxSoc}%`); + finalAction = 'preserve'; + exception = 'soc_full'; + } else if (inputs._delayCharge) { + this.log(`Exception: charge blocked — delay-charge active, PV exports to grid`); + finalAction = 'preserve'; + exception = 'delay_charge'; + } + } + + // ── Exception: real-time peak shaving ──────────────────────────────────── + // If DP says preserve but grid import exceeds threshold → discharge to cover peak. + // Also fires when charge→preserve(soc_full): battery is full and a load spike hits. + // Restricted to peak_hours window when configured; uses Amsterdam local time. + if (finalAction === 'preserve' && (!exception || exception === 'soc_full')) { + const peakThreshold = this.settings.peak_shaving_threshold ?? 0; + if (peakThreshold > 0 && this.currentLoad > peakThreshold && soc > minSoc) { + const peakRange = this._parseTimeRange(this.settings.peak_hours); + const nowHourNL = parseInt(new Date().toLocaleString('en-US', { timeZone: 'Europe/Amsterdam', hour: 'numeric', hour12: false }), 10); + const inPeakWin = !peakRange || (nowHourNL >= peakRange.startHour && nowHourNL < peakRange.endHour); + if (inPeakWin) { + this.log(`Exception: peak shaving — load ${Math.round(this.currentLoad)}W > threshold ${peakThreshold}W → discharge`); + finalAction = 'discharge'; + exception = 'peak_shaving'; + } else { + this.log(`Exception: peak shaving — load ${Math.round(this.currentLoad)}W > threshold ${peakThreshold}W but outside peak window (${this.settings.peak_hours}) — skipped`); + } + } + } + + // ── Exception: min-price discharge override ─────────────────────────────── + // DP may choose preserve to protect opportunity value for future PV recharge, + // but if price >= user's configured minimum and this slot has no scheduled PV, + // the user's intent is clear: discharge above the minimum threshold. + if (finalAction === 'preserve' && !exception && soc > minSoc) { + const currentPrice = inputs.tariff?.currentPrice ?? null; + const minPrice = inputs.effectiveMinDischarge ?? 0; + const slotMeta = inputs.optimizer?.getSlotMeta(new Date()); + const slotPvCoverage = slotMeta?.pvCoverage ?? 0; + if (currentPrice !== null && currentPrice >= minPrice && slotPvCoverage < 0.05) { + this.log(`Exception: min-price override — DP preserve but price €${currentPrice.toFixed(3)} >= min €${minPrice.toFixed(3)}, pvCoverage=${slotPvCoverage.toFixed(3)} → discharge`); + finalAction = 'discharge'; + exception = 'min_price_override'; + } + } + + // ── 3. Map to HW mode ───────────────────────────────────────────────────── + const hwMode = this._mapPolicyToHwMode(finalAction, inputs); + + // ── 4. Confidence ───────────────────────────────────────────────────────── + let confidence = exception ? 75 : 90; + if (inputs.policyMode === 'eco') confidence = Math.max(40, confidence - 5); + if (inputs.policyMode === 'aggressive') confidence = Math.min(99, confidence + 5); + + // ── 5. Result ───────────────────────────────────────────────────────────── + this.log('--- POLICY RUN END ---'); + return this._buildDpResult(finalAction, hwMode, confidence, exception, inputs, dpAction); + } + + // ── PV flags: extracted from PV OVERSCHOT block, no score side-effects ───── + // Sets inputs._delayCharge, _pvStoreWins, _pvExporting, _pvStoreValue, _pvCurrentPrice. + _computePvFlags(inputs) { + const soc = inputs.battery?.stateOfCharge ?? 50; + const maxSoc = this.settings.max_soc ?? 95; + const grid = inputs.p1?.resolved_gridPower ?? 0; + const batt = inputs.p1?.battery_power ?? 0; + + const _cetHour = parseInt(new Date().toLocaleString('en-US', { + hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' + }), 10); + const _isDaylight = _cetHour >= 7 && _cetHour < 20; + const _pvSurplusDetected = grid < -200 || batt > 100 || (inputs.p1?.pv_power_estimated ?? 0) >= 100; + + if (!(_pvSurplusDetected && soc < maxSoc && _isDaylight)) return; + + 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)); + const _pvNow = new Date(); + const _pvFutureAll = _pvPricesArray + .filter(h => h.timestamp ? new Date(h.timestamp) > _pvNow : (h.index ?? 0) >= 1) + .map(h => h.price).filter(p => typeof p === 'number'); + const _pvFuturePrices = _pvFutureAll.filter(p => p > 0); + const _pvMinFuture = _pvFutureAll.length ? Math.min(..._pvFutureAll) : null; + const _pvMaxFuture = _pvFuturePrices.length ? Math.max(..._pvFuturePrices) : null; + + // Use the schedule's trickle-capped value when available: it excludes prices beyond + // the first pvStrong saturation slot (where free PV will refill the battery anyway). + // Without this cap, tomorrow's evening peak misleads the check into forcing charge + // even when tomorrow's PV makes today's trickle energy irrelevant for those slots. + const _pvSlotMeta = inputs.optimizer?.getSlotMeta(new Date()); + const _pvTrickleMaxValue = _pvSlotMeta?.pvTrickleMaxValue; + const _useScheduleValue = typeof _pvTrickleMaxValue === 'number'; + // When trickle cap gives 0 but DP assigned preserve (expects free PV charging this slot), + // fall back to uncapped pvStoreValue. trickleSuffixMaxPrice resets at pvStrong[k+1] to avoid + // counting tomorrow's peak via today's trickle — but that reset also incorrectly fires when + // the CURRENT slot is pvStrong and tonight's discharge slots come AFTER it. DP chose preserve + // explicitly because storing beats exporting; honour that decision. + const _pvDpExpectsCharge = _pvSlotMeta?.action === 'preserve'; + const _pvStoreValue = _useScheduleValue + ? (_pvTrickleMaxValue > 0 + ? _pvTrickleMaxValue + : (_pvDpExpectsCharge && (_pvSlotMeta?.pvStoreValue ?? 0) > 0 + ? (_pvSlotMeta.pvStoreValue) + : (_pvMaxFuture !== null ? _pvMaxFuture * this.BATTERY_EFFICIENCY : null))) + : (_pvMaxFuture !== null ? _pvMaxFuture * this.BATTERY_EFFICIENCY : null); + if (_useScheduleValue && _pvTrickleMaxValue === 0 && _pvDpExpectsCharge && (_pvSlotMeta?.pvStoreValue ?? 0) > 0) + this.log(`PV OVERSCHOT: trickle=0 maar DP=preserve → fallback op pvStoreValue €${_pvSlotMeta.pvStoreValue.toFixed(3)}`); + + inputs._pvExporting = true; + inputs._pvStoreValue = _pvStoreValue; + inputs._pvCurrentPrice = _pvCurrentPrice; + + // When strongly negative prices are coming, the DP has already planned discharge + // to create room. Suppress pvStoreWins/delayCharge so we don't override that plan. + if (_pvMinFuture !== null && _pvMinFuture < -0.10) { + this.log(`PV OVERSCHOT: negatieve prijs €${_pvMinFuture.toFixed(3)} komend → DP beslist, pvStoreWins/delayCharge onderdrukt`); + return; + } + + // Delay-charge check + const _targetSocForDelay = this.settings.max_soc ?? 95; + const _battChargePowerW = inputs.battery?.maxChargePowerW || + (inputs.battery?.totalCapacityKwh + ? Math.max(1, Math.round(inputs.battery.totalCapacityKwh / 2.688)) * 800 + : 800); + const _battCapForDelay = Math.max(1, Math.round(_battChargePowerW / 800)) * 2.688; + const _toChargeKwhCheck = ((_targetSocForDelay - soc) / 100) * _battCapForDelay; + + let _canDelayCharge = false; + let _forceChargeNow = false; + if (_toChargeKwhCheck <= 0) { + this.log(`PV OVERSCHOT: delay-charge skipped — battery already full (SoC ${soc}% >= target ${_targetSocForDelay}%)`); + } else if (_pvCurrentPrice !== null && _pvCurrentPrice > 0) { + 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; + const _minDelayPrice = _dynamicMax + 0.08; + if (_pvCurrentPrice <= _dynamicMax && _excellentArbitrage) { + _forceChargeNow = true; + 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) { + _forceChargeNow = true; + 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 { + 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); + + 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 + ); + const _cheaperLater = _cheapLaterHours.length > 0 && _cheapLaterHours.length >= Math.ceil(_hoursNeeded * _sph); + + if (_cheaperLater) { + 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) { + for (const h of _hourlyForecast) { + const hMs = h.time instanceof Date ? h.time.getTime() : new Date(h.time).getTime(); + if (hMs < _now) continue; + if (typeof h.radiationWm2 === 'number' && h.radiationWm2 >= _minRadiation) _effectiveSunHours++; + } + } else { + _effectiveSunHours = inputs.weather?.sunshineTodayRemaining ?? 0; + } + const _hasForecast = Array.isArray(_hourlyForecast) && _hourlyForecast.length > 0; + const _sunScore = inputs.sun?.gfs ?? inputs.sun?.harmonie ?? 0; + const _enoughSun = _effectiveSunHours >= _hoursNeeded || (!_hasForecast && _sunScore >= 50); + const _pvKwhRemaining = inputs.weather?.pvKwhRemaining ?? null; + const _pvSurplus = inputs.weather?.pvSurplusRemaining ?? _pvKwhRemaining; + const _pvCanFill = _pvSurplus === null || _pvSurplus >= _toChargeKwh * 1.2; + + if (_enoughSun && _pvCanSupport && _pvCanFill) { + _canDelayCharge = true; + const _sunSource = _hasForecast ? `${_effectiveSunHours}h radiation≥${Math.round(_minRadiation)}W/m²` : `score ${_sunScore}`; + const _pvRemainingStr = _pvSurplus !== null ? `, PV surplus ${_pvSurplus.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) coming → export now, charge later`); + } else if (_enoughSun && _pvCanSupport && !_pvCanFill) { + this.log(`PV OVERSCHOT: delay-charge SKIPPED — PV surplus ${_pvSurplus?.toFixed(1)}kWh < ${(_toChargeKwh * 1.2).toFixed(1)}kWh needed → charge now before sun fades`); + } else { + 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 + ); + if (_cheap24h.length >= Math.ceil(_hoursNeeded * _sph)) { + _canDelayCharge = true; + this.log(`PV OVERSCHOT: delay-charge via grid arbitrage — ${_cheap24h.length} cheap grid hour(s) coming → 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`); + } + } + } + } + } + + // PV opportunity delay: even if store-wins, delay PV charging when a meaningfully + // cheaper export slot is coming within 8h, that slot has PV production, and enough + // PV remains to fill the battery. + if (!_canDelayCharge && _pvCurrentPrice !== null && _pvCurrentPrice > 0 && _toChargeKwhCheck > 0) { + const _pvKwhRem = inputs.weather?.pvKwhRemaining ?? null; + const _pvSurplusRem = inputs.weather?.pvSurplusRemaining ?? _pvKwhRem; + const _pvCanFill = _pvSurplusRem === null || _pvSurplusRem >= _toChargeKwhCheck * 1.2; + const _nowMs = Date.now(); + const _next8h = _pvPricesArray.filter(p => { + const t = p.timestamp ? new Date(p.timestamp).getTime() : null; + const inRange = t + ? (t > _nowMs && t <= _nowMs + 8 * 3600e3) + : (typeof p.index === 'number' && p.index > 0 && p.index <= 8 * _sph); + return inRange && typeof p.price === 'number' && p.price > 0; + }); + if (_pvCanFill && _next8h.length > 0) { + // Compare current price to average of cheapest N slots needed to fill battery. + const _slotsNeeded = Math.max(1, Math.ceil(_toChargeKwhCheck / (_battChargePowerW / 1000)) * _sph); + const _sortedPrices = [..._next8h].sort((a, b) => a.price - b.price); + const _cheapestN = _sortedPrices.slice(0, _slotsNeeded); + const _avgCheapest = _cheapestN.reduce((s, p) => s + p.price, 0) / _cheapestN.length; + if (_pvCurrentPrice > _avgCheapest * 1.30 && _pvCurrentPrice - _avgCheapest >= 0.03) { + const _minNext = _sortedPrices[0].price; + const _cheapSlot = _next8h.find(p => p.price === _minNext); + const _cheapSlotMs = _cheapSlot?.timestamp ? new Date(_cheapSlot.timestamp).getTime() + : _cheapSlot?.index != null ? _nowMs + _cheapSlot.index * (3600e3 / _sph) : null; + const _pvCapW = this.settings.pv_capacity_w || 3000; + const _minRadiation = _pvCapW > 0 ? (_battChargePowerW * 0.5 / _pvCapW) * 1000 : 0; + const _hourlyFc = inputs.weather?.hourlyForecast; + let _cheapHasPv = false; + if (_cheapSlotMs && Array.isArray(_hourlyFc) && _hourlyFc.length > 0) { + for (const h of _hourlyFc) { + const hMs = h.time instanceof Date ? h.time.getTime() : new Date(h.time).getTime(); + if (Math.abs(hMs - _cheapSlotMs) <= 3600e3 && typeof h.radiationWm2 === 'number' && h.radiationWm2 >= _minRadiation) { + _cheapHasPv = true; + break; + } + } + } else { + _cheapHasPv = true; + } + if (_cheapHasPv) { + _canDelayCharge = true; + this.log(`PV OVERSCHOT: PV opportunity delay — current €${_pvCurrentPrice.toFixed(3)} >> avg €${_avgCheapest.toFixed(3)} (${_slotsNeeded} goedkoopste slots), PV surplus ${_pvSurplusRem?.toFixed(1) ?? '?'}kWh → export now, charge when cheaper`); + } + } + } + } + + if (_canDelayCharge) { + inputs._pvStoreWins = false; + inputs._delayCharge = true; + this.log(`PV OVERSCHOT: delay-charge wins → export at €${_pvCurrentPrice?.toFixed(3)}/kWh, charge later`); + } else if (_pvCurrentPrice === null || _pvStoreValue === null || _pvStoreValue > _pvCurrentPrice) { + inputs._pvStoreWins = true; + this.log(`PV OVERSCHOT: storing beats exporting (max €${_pvMaxFuture?.toFixed(3) ?? '?'} × ${this.BATTERY_EFFICIENCY} = €${_pvStoreValue?.toFixed(3) ?? '?'} > current €${_pvCurrentPrice?.toFixed(3) ?? '?'}) → force charge`); + } else { + // Low-SoC heuristic: when export margin is very small and SoC is low, prefer storing. + // Disabled when using schedule-derived value (_useScheduleValue): that value already + // accounts for PV saturation, so the DP decision should be trusted over this heuristic. + const _lowSocMarginRequired = (!_useScheduleValue && soc < 50) ? 0.02 : 0; + const _exportMargin = _pvCurrentPrice - _pvStoreValue; + if (_exportMargin < _lowSocMarginRequired) { + inputs._pvStoreWins = true; + this.log(`PV OVERSCHOT: export margin €${_exportMargin.toFixed(3)} < low-SoC threshold €${_lowSocMarginRequired.toFixed(3)} (SoC ${soc}%) → store wins`); + } else { + inputs._pvStoreWins = false; + this.log(`PV OVERSCHOT: export more profitable (€${_pvCurrentPrice.toFixed(3)} > €${_pvStoreValue.toFixed(3)}, margin €${_exportMargin.toFixed(3)}) → standby, PV to grid`); + } + } + } + + // ── Build result for DP-primary path ───────────────────────────────────────── + _buildDpResult(action, hwMode, confidence, exception, inputs, dpAction) { + const allPrices = inputs.tariff?.effectivePrices || inputs.tariff?.allPrices || inputs.tariff?.next24Hours || []; + const nowMs = Date.now(); + + // Max future DISCHARGE price: from scheduled discharge slots, not raw price max. + // Using raw max would show PV-covered high prices (e.g. €0.40 noon peak) as the + // preserve reason even though the DP blocks discharge there (pvCoverage zeroes + // perSlotDischargeSocDeltaG). Fall back to raw max only when no discharge is + // scheduled (no optimizer data yet). + const _schedSlots = inputs.optimizer?._schedule?.slots ?? []; + const _futDischargePrices = _schedSlots + .filter(s => s.action === 'discharge' && s.timestamp && new Date(s.timestamp) > nowMs + && typeof s.price === 'number' && s.price > 0) + .map(s => s.price); + const _allFuturePrices = allPrices + .filter(p => p.timestamp && new Date(p.timestamp) > nowMs) + .map(p => p.price).filter(p => typeof p === 'number' && p > 0); + const maxFuturePrice = _futDischargePrices.length + ? Math.max(..._futDischargePrices) + : (_allFuturePrices.length ? Math.max(..._allFuturePrices) : null); + + // Attach DP decision context so explainability engine can use it + inputs.dpDecision = { + dpAction, + finalAction: action, + exception, + pvExportWins: inputs.optimizer?.getSlotMeta(new Date())?.pvExportWins === true, + maxFuturePrice, + breakEven: inputs.batteryCost?.breakEven ?? null, + }; + + // Debug snapshot (compatible with old format) + const _diagPrices12h = allPrices + .filter(p => p.timestamp && new Date(p.timestamp) > nowMs + && new Date(p.timestamp) <= new Date(nowMs + 12 * 3600_000)) + .map(p => p.price).filter(p => typeof p === 'number' && p > 0); + const _grid = inputs.p1?.resolved_gridPower ?? 0; + const _pv = inputs.p1?.pv_power_estimated ?? 0; + const _batt = inputs.p1?.battery_power ?? 0; + const _dynMax = inputs.dynamicMaxChargePrice ?? null; + const _price = inputs.tariff?.currentPrice ?? null; + const _soc = inputs.battery?.stateOfCharge ?? 50; + const _maxSoc = this.settings.max_soc ?? 95; + const _maxChargeW = inputs.battery?.maxChargePowerW ?? 800; + const _netSurplus = Math.max( + 0, + _grid < 0 ? -_grid : 0, + (inputs.p1?.avg_consumption_w ?? 0) > 0 ? (_pv - (inputs.p1?.avg_consumption_w ?? 0)) : 0 + ); + const _lowSocTopUp = _dynMax !== null && _price !== null + && _price <= (_dynMax ?? this.settings.max_charge_price ?? 0.19) + && _soc < Math.min(_maxSoc, 40) + && _netSurplus < Math.max(50, _maxChargeW - 50); + + const _pvKwhTomorrow = inputs.weather?.pvKwhTomorrow ?? null; + const _battCapKwh = inputs.battery?.totalCapacityKwh ?? null; + const _pvRefill = (_pvKwhTomorrow != null && _battCapKwh) ? Math.min(1, _pvKwhTomorrow / _battCapKwh) : null; + const _pvTermFactor = _pvRefill != null ? +(_pvRefill >= 0.8 ? 0 : Math.max(0, 1 - _pvRefill / 0.8)).toFixed(2) : null; + + const debug = { + ts: new Date().toISOString(), + price: inputs.tariff?.currentPrice ?? null, + soc: inputs.battery?.stateOfCharge ?? null, + currentLoad: this.currentLoad ?? null, + houseConsumption: Math.round(Math.max(0, _grid - _batt + _pv)), + pvEstimate: inputs.p1?.pv_power_estimated ?? null, + scores: { charge: 0, discharge: 0, preserve: 0 }, + policyMode: action, + hwMode, + delayCharge: inputs._delayCharge === true, + lowSocGridTopUp: _lowSocTopUp, + dynamicMaxChargePrice: _dynMax != null ? +_dynMax.toFixed(3) : null, + breakEven: inputs.batteryCost?.breakEven != null ? +inputs.batteryCost.breakEven.toFixed(3) : null, + avgCost: inputs.batteryCost?.avgCost != null ? +inputs.batteryCost.avgCost.toFixed(3) : null, + optimizer: dpAction, + exception, + priceMin12h: _diagPrices12h.length ? +Math.min(..._diagPrices12h).toFixed(3) : null, + priceMax12h: _diagPrices12h.length ? +Math.max(..._diagPrices12h).toFixed(3) : null, + pvKwhTomorrow: _pvKwhTomorrow != null ? +_pvKwhTomorrow.toFixed(1) : null, + battCapKwh: _battCapKwh != null ? +_battCapKwh.toFixed(2) : null, + pvTermFactor: _pvTermFactor, + }; + + this.log('Recommendation:', { policyMode: action, hwMode, confidence, exception }); + + return { + policyMode: action, + hwMode, + confidence, + scores: { charge: 0, discharge: 0, preserve: 0 }, + debug, + }; + } + + // ── Build result for legacy (fixed tariff) path ─────────────────────────── + _buildLegacyResult(recommendation, scores, inputs) { + const allPrices = inputs.tariff?.effectivePrices || inputs.tariff?.allPrices || inputs.tariff?.next24Hours || []; + const nowMs = Date.now(); + const _diagPrices12h = allPrices + .filter(p => p.timestamp && new Date(p.timestamp) > nowMs + && new Date(p.timestamp) <= new Date(nowMs + 12 * 3600_000)) + .map(p => p.price).filter(p => typeof p === 'number' && p > 0); + const _grid2 = inputs.p1?.resolved_gridPower ?? 0; + const _pv2 = inputs.p1?.pv_power_estimated ?? 0; + const _batt2 = inputs.p1?.battery_power ?? 0; + recommendation.debug = { + ts: new Date().toISOString(), + price: inputs.tariff?.currentPrice ?? null, + soc: inputs.battery?.stateOfCharge ?? null, + currentLoad: this.currentLoad ?? null, + houseConsumption: Math.max(0, _grid2 - _batt2 + _pv2), + pvEstimate: inputs.p1?.pv_power_estimated ?? null, + scores: { charge: scores.charge, discharge: scores.discharge, preserve: scores.preserve }, + policyMode: recommendation.policyMode, + hwMode: recommendation.hwMode, + delayCharge: inputs._delayCharge === true, + breakEven: inputs.batteryCost?.breakEven != null ? +inputs.batteryCost.breakEven.toFixed(3) : null, + avgCost: inputs.batteryCost?.avgCost != null ? +inputs.batteryCost.avgCost.toFixed(3) : null, + optimizer: inputs.optimizer ? inputs.optimizer.getSlot(new Date()) : null, + priceMin12h: _diagPrices12h.length ? +Math.min(..._diagPrices12h).toFixed(3) : null, + priceMax12h: _diagPrices12h.length ? +Math.max(..._diagPrices12h).toFixed(3) : null, + }; + 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 pvSurplus = inputs.weather?.pvSurplusRemaining ?? pvKwhRemaining; + const capacityKwh = inputs.battery?.totalCapacityKwh ?? 2.688; + const remainingKwh = capacityKwh * (1 - soc / 100); + // Require 1.5× margin on net surplus (PV minus expected consumption during PV hours). + // Using raw pvKwhRemaining would ignore household load that consumes most of the yield on low-PV days. + const pvCoversGap = pvSurplus !== null && pvSurplus >= 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 surplus ${pvSurplus.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%). + // Guard: only apply when the battery is ACTUALLY receiving charge (batteryPower > 50W). + // pvDetected can be true via pvEstimate (forecast) before sunrise, when batteryPower=0. + // In that case there is no trickle-charge to protect — don't block profitable discharge. + const currentHwMode = inputs.battery?.mode ?? null; + const batteryActuallyCharging = (inputs.p1?.battery_power ?? 0) > 50; + const pvTrickleCharging = currentHwMode === 'zero_charge_only' && pvDetected && batteryActuallyCharging; + const pvTrickleHysteresis = Math.max(zeroModeThreshold + 20, 20); + if (pvTrickleCharging && soc < pvTrickleHysteresis) { + this.log(`BatteryScore: trickle-charging from PV (zero_charge_only, ${inputs.p1?.battery_power ?? 0}W), 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 = inputs.effectiveMinDischarge ?? this.settings.min_discharge_price ?? 0.22; + 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; + } + // After sunset, pvDetected=true is the dynamic-tariff fallback (not real PV). + // Don't add a charge bonus — let the tariff module decide charge/discharge. + const sunset = inputs.weather?.todaySunset; + const _sunrise0 = inputs.weather?.todaySunrise; + const afterSunset = (sunset instanceof Date && (Date.now() > sunset.getTime() + 30 * 60 * 1000)) + || (_sunrise0 instanceof Date && Date.now() < _sunrise0.getTime()); + if (afterSunset) { + this.log('BatteryScore: PV signal (dynamic fallback) but after sunset → no charge bonus'); + 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; +} + // ── _refreshPvSticky ────────────────────────────────────────────────────── + // Pure PV detection + sticky timer update. No score side-effects. + // Returns { detected: boolean, bonus: number } where bonus is the charge + // score that _applyPVReality (fixed-tariff path) should add. + _refreshPvSticky(inputs) { + const p1 = inputs.p1; + const batteryMode = inputs.battery?.mode; + if (!p1) return { detected: false, bonus: 0 }; + + 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; + + const sunset = inputs.weather?.todaySunset; + const _sunrise1 = inputs.weather?.todaySunrise; + const afterSunset = (sunset && (now > sunset.getTime() + 30 * 60 * 1000)) + || (_sunrise1 instanceof Date && now < _sunrise1.getTime()); + const delayCharge = inputs._delayCharge === true; + + if (this._pvStickyUntil > now) { + if (delayCharge) { + this.log(`PV Reality: sticky PV active + delay-charge → standby (PV exports to grid)`); + } else { + this.log(`PV Reality: sticky PV active → charge allowed`); + } + return { detected: true, bonus: 50 }; + } + + if (batteryPower > 50 && gridPower < -50) { + this._pvStickyUntil = now + 5 * 60 * 1000; + if (delayCharge) { + 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`); + } + return { detected: true, bonus: 100 }; + } + + if (pvEstimate >= 100 && !afterSunset) { + this._pvStickyUntil = now + 5 * 60 * 1000; + if (delayCharge) { + 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`); + } + return { detected: true, bonus: 80 }; + } + + if (pvEstimate >= 100 && afterSunset) { + this.log(`⚠️ pvEstimate ${pvEstimate}W ignored — after sunset + 30min (stale EMA)`); + } + + if (gridPower < -100) { + this._pvStickyUntil = now + 5 * 60 * 1000; + if (delayCharge) { + this.log(`💡 PV detected via export (${Math.abs(gridPower)}W) → sticky 5 min (delay-charge: no charge bonus)`); + } else { + this.log(`💡 PV detected via export (${Math.abs(gridPower)}W) → sticky 5 min`); + } + return { detected: true, bonus: 60 }; + } + + if (pvEstimate >= 50 && !afterSunset) { + const cetHour = parseInt(new Date().toLocaleString('en-GB', { + hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' + }), 10); + if (cetHour >= 7 && cetHour < 20) { + this._pvStickyUntil = now + 2 * 60 * 1000; + this.log(`💡 PV trickle: pvEstimate=${pvEstimate}W (daylight) → weak PV assumed, sticky 2 min`); + return { detected: true, bonus: 20 }; + } + } + + if (batteryMode === 'zero_charge_only' && pvEstimate >= 50) { + const cetHour = parseInt(new Date().toLocaleString('en-GB', { + hour: 'numeric', hour12: false, timeZone: 'Europe/Amsterdam' + }), 10); + if (cetHour >= 7 && cetHour < 20) { + this._pvStickyUntil = now + 2 * 60 * 1000; + this.log(`PV Reality: zero_charge_only mode (daylight, pvEstimate=${pvEstimate}W) → PV assumed`); + return { detected: true, bonus: 40 }; + } + 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 { detected: true, bonus: 0 }; + } + + this.log('PV Reality: no PV → blocking charge'); + return { detected: false, bonus: 0 }; + } + + _applyPVReality(scores, p1, batteryMode, inputs) { + const { detected, bonus } = this._refreshPvSticky(inputs); + if (detected && bonus > 0 && !inputs._delayCharge) { + scores.charge += bonus; + } + return detected; + } + + + _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 pvKwhTomorrow = Number(weather.pvKwhTomorrow ?? 0); + const battCapKwh = battery?.totalCapacityKwh ?? 3; + const strongSunTomorrow = pvKwhTomorrow > 0 ? pvKwhTomorrow >= battCapKwh * 0.8 : sunTomorrow >= 4; + const soc = battery?.stateOfCharge ?? 50; + const isDynamic = this.settings.tariff_type === 'dynamic'; + + // DP already accounts for tomorrow's PV in its lookahead — halve weather discharge + // boosts when DP says preserve to avoid overriding a global-optimal decision. + const dpSaysPreserve = inputs?.optimizer?.getSlot(new Date()) === 'preserve'; + + 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'); + } + + // Discharge boosts are pointless at 0% SoC — skip to avoid log noise + if (soc > 5) { + if (strongSunTomorrow) { + scores.charge -= 10; + + if (isDynamic) { + const currentPrice = tariff?.currentPrice || 0; + const minDischargePrice = inputs?.effectiveMinDischarge ?? this.settings.min_discharge_price ?? 0.22; + + if (currentPrice >= minDischargePrice * 0.85) { + const boost = dpSaysPreserve ? 12 : 25; + scores.discharge += boost; + this.log(`Weather: sunTomorrow >= 4 + expensive hour → BOOST discharge +${boost}${dpSaysPreserve ? ' (halved: DP=preserve)' : ''}`); + } else { + this.log(`Weather: sunTomorrow >= 4 → mild grid charge penalty`); + } + } else { + const boost = dpSaysPreserve ? 8 : 15; + scores.discharge += boost; + this.log(`Weather: sunTomorrow >= 4 → encourage discharge +${boost}${dpSaysPreserve ? ' (halved: DP=preserve)' : ''}`); + } + } + + if (pvKwhTomorrow > 0 ? pvKwhTomorrow >= battCapKwh * 1.5 : sunTomorrow >= 6.0) { + scores.charge -= 20; + + if (isDynamic) { + const currentPrice = tariff?.currentPrice || 0; + const minDischargePrice = inputs?.effectiveMinDischarge ?? this.settings.min_discharge_price ?? 0.22; + + if (currentPrice >= minDischargePrice * 0.75) { + const boost = dpSaysPreserve ? 18 : 35; + scores.discharge += boost; + this.log(`Weather: sunTomorrow >= 6 + moderate/high price → AGGRESSIVE discharge +${boost}${dpSaysPreserve ? ' (halved: DP=preserve)' : ''}`); + } else { + this.log('Weather: sunTomorrow >= 6 → avoid grid charging, ready to discharge'); + } + } else { + const currentHour = new Date().getHours(); + const _peak = this._parseTimeRange(this.settings.peak_hours); + const isEvening = _peak ? currentHour >= _peak.startHour : currentHour >= 17; + if (isEvening) { + const boost = dpSaysPreserve ? 15 : 30; + scores.discharge += boost; + this.log(`Weather: sunTomorrow >= 6 + evening → AGGRESSIVE discharge +${boost}${dpSaysPreserve ? ' (halved: DP=preserve)' : ''}`); + } else { + const boost = dpSaysPreserve ? 10 : 20; + scores.discharge += boost; + this.log(`Weather: sunTomorrow >= 6 → boost discharge +${boost}${dpSaysPreserve ? ' (halved: DP=preserve)' : ''}`); + } + } + } + } else { + // SoC ≤ 5%: still apply charge penalties, skip discharge boosts + if (strongSunTomorrow) scores.charge -= 10; + if (pvKwhTomorrow > 0 ? pvKwhTomorrow >= battCapKwh * 1.5 : sunTomorrow >= 6.0) scores.charge -= 20; + } + } + + _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 pvKwhTomorrow = Number(weather.pvKwhTomorrow ?? 0); + const battCapKwh = battery?.totalCapacityKwh ?? 3; + const strongSunTomorrow = pvKwhTomorrow > 0 ? pvKwhTomorrow >= battCapKwh * 0.8 : sunTomorrow >= 4; + const h = time.getHours(); + + if ((pvKwhTomorrow > 0 ? pvKwhTomorrow >= battCapKwh : sunTomorrow >= 5.0) && (peak ? inPeak : (h >= 17 && h < 23))) { + scores.discharge += 20; + scores.preserve -= 10; + this.log(`Peak: evening + ${sunTomorrow}h sun tomorrow / ${pvKwhTomorrow}kWh PV → boost discharge`); + } + + const offPeak = this._parseTimeRange(this.settings.off_peak_hours); + const inOffPeak = offPeak && h >= offPeak.startHour && h < offPeak.endHour; + + if (inOffPeak && strongSunTomorrow) { + scores.charge -= 30; + this.log(`Peak: off-peak but ${sunTomorrow}h sun tomorrow / ${pvKwhTomorrow}kWh PV → 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)'); + } + } + + _isChargeExecutable(ctx) { + const price = ctx.tariff?.currentPrice ?? null; + const maxChargePrice = this.settings.max_charge_price ?? 0.19; + if (price !== null && price <= maxChargePrice) return true; + + const gridPower = ctx.p1?.resolved_gridPower ?? 0; + const batteryPower = ctx.p1?.battery_power ?? 0; + const pvEstimate = ctx.p1?.pv_power_estimated ?? 0; + const stickyPvActive = this._pvStickyUntil && (this._pvStickyUntil > Date.now()); + const _sunset = ctx.weather?.todaySunset; + const _sunrise2 = ctx.weather?.todaySunrise; + const _afterSunset = !_sunset || (Date.now() > _sunset.getTime() + 30 * 60 * 1000) + || (_sunrise2 instanceof Date && Date.now() < _sunrise2.getTime()); + return stickyPvActive || + gridPower < -100 || + (!_afterSunset && pvEstimate >= 100) || + (gridPower <= 0 && batteryPower > 50); + } + + _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 hwMode = this._mapPolicyToHwMode(policyMode, ctx); + + // When charge wins on score but maps to standby (price too high, no PV), + // re-evaluate using only discharge vs preserve. + if (policyMode === 'charge' && hwMode === 'standby') { + const execMode = scores.discharge >= scores.preserve ? 'discharge' : 'preserve'; + const execWinner = Math.max(scores.discharge, scores.preserve); + const execHwMode = this._mapPolicyToHwMode(execMode, ctx); + const execTotal = scores.discharge + scores.preserve; + const execConfidence = Math.round((execWinner / (execTotal || 1)) * 100); + this.log(`[MAPPING][CHARGE-BLOCKED] charge unexecutable → fallback ${execMode} (${execHwMode}), confidence ${execConfidence}%`); + return { policyMode: execMode, hwMode: execHwMode, confidence: Math.min(execConfidence, 100) }; + } + + const chargeExecutable = this._isChargeExecutable(ctx); + const total = chargeExecutable + ? scores.charge + scores.discharge + scores.preserve + : scores.discharge + scores.preserve; + + const confidence = Math.round((winner / (total || 1)) * 100); + return { policyMode, hwMode, confidence: Math.min(confidence, 100) }; + } + +_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 maxChargePrice = ctx.dynamicMaxChargePrice ?? this.settings.max_charge_price ?? 0.19; + + // EV-charging gate: block battery discharge entirely so the EV draws from grid + // and PV instead of cycling the battery. PV surplus may still top up the battery. + if (ctx.evCharging) { + if (soc >= maxSoc) { + this.log(`[MAPPING][EV] EV charging active and SoC ${soc}% >= max ${maxSoc}% → standby`); + return 'standby'; + } + this.log(`[MAPPING][EV] EV charging active → zero_charge_only (policy was '${policyMode}')`); + return 'zero_charge_only'; + } + + // balanced-dynamic always behaves opportunistically (post-saldering design). + const isBalancedDynamic = ctx.policyMode === 'balanced-dynamic'; + const respectMinMax = isBalancedDynamic ? false : (this.settings.respect_minmax !== false); + + // 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 cycleCost = this.settings.cycle_cost_per_kwh ?? 0.075; + + // Consistent with optimizer: cycle cost split 50/50 between charge and discharge + const safeBreakEven = ctx.batteryCost.avgCost / effectiveEff + cycleCost * 0.5; + ctx.batteryCost.breakEven = safeBreakEven; + + this.log(`[MAPPING] safe break-even €${safeBreakEven.toFixed(3)} (eff=${effectiveEff.toFixed(3)}, cycleCost=€${cycleCost}/kWh)`); + } + + // Effective discharge floor: + // strict mode → honour user-configured min_discharge_price + // opportunistic → only require price > economic break-even (cycle cost / RTE) + const _settingsFloor = (this.settings.cycle_cost_per_kwh ?? 0.075) / (this.settings.battery_efficiency || 0.75); + const minDischarge = respectMinMax + ? (this.settings.min_discharge_price || 0) + : (costModelActive && ctx.batteryCost.breakEven > 0 ? ctx.batteryCost.breakEven : _settingsFloor); + + 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; + const maxChargePowerW = ctx.battery?.maxChargePowerW ?? 800; + + // Check sticky PV flag first (set by _applyPVReality with 5min window) + const stickyPvActive = this._pvStickyUntil && (this._pvStickyUntil > Date.now()); + + // Sunset guard: pvEstimate uses EMA smoothing which causes stale values after sunset. + // After sunset + 30min, only hard signals (grid export, battery charging) count. + const _sunset = ctx.weather?.todaySunset; + const _sunrise3 = ctx.weather?.todaySunrise; + const _afterSunset = !_sunset || (Date.now() > _sunset.getTime() + 30 * 60 * 1000) + || (_sunrise3 instanceof Date && Date.now() < _sunrise3.getTime()); + + // P1-derived PV: if grid import is less than learned consumption, PV covers the gap. + // Detects early-morning PV before export threshold is crossed (no flow card needed). + const avgConsumptionW = ctx.p1?.avg_consumption_w ?? 0; + const pvFromP1 = avgConsumptionW > 100 ? Math.max(0, avgConsumptionW - gridPower) : 0; + const estimatedNetPvSurplusW = Math.max( + 0, + gridPower < 0 ? -gridPower : 0, + avgConsumptionW > 0 ? pvEstimate - avgConsumptionW : 0, + gridPower <= 0 && batteryPower > 50 ? batteryPower : 0 + ); + const pvOnlyChargeFloorW = Math.max(400, Math.round(maxChargePowerW * 0.8)); + const pvCanSustainCharge = estimatedNetPvSurplusW >= pvOnlyChargeFloorW; + const _topUpPriceArr = (ctx.tariff?.effectivePrices || ctx.tariff?.allPrices || ctx.tariff?.next24Hours || []); + const _now = Date.now(); + const _topUpMinFuture = _topUpPriceArr.length + ? Math.min(..._topUpPriceArr + .filter(h => h?.timestamp && new Date(h.timestamp).getTime() > _now && new Date(h.timestamp).getTime() <= _now + 8 * 3600_000) + .map(h => h.price) + .filter(p => typeof p === 'number' && p > 0)) + : price; + const lowSocGridTopUp = price !== null + && price <= maxChargePrice + && price <= (_topUpMinFuture ?? price) + && soc < Math.min(maxSoc, 40) + && estimatedNetPvSurplusW < Math.max(50, maxChargePowerW - 50); + + const actualPvNow = + stickyPvActive || + gridPower < -100 || + (!_afterSunset && pvEstimate >= 100) || + (!_afterSunset && pvFromP1 >= 300 && pvEstimate > 0) || + (gridPower <= 0 && batteryPower > 50); // battery charging without grid import → PV surplus + + this.log(`[MAPPING] policyMode=${policyMode}, soc=${soc}, PV=${actualPvNow} (sticky=${stickyPvActive}, pvEst=${pvEstimate}W, pvP1=${pvFromP1}W, netSurplus≈${Math.round(estimatedNetPvSurplusW)}W), 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' || ctx.policyMode === 'balanced-dynamic') && tariffType === 'dynamic') { + + // DP chose standby (pvExportWins). Override to grid top-up when SoC is critically low, + // price is cheap, and PV surplus is insufficient to cover charging. + if (policyMode === 'standby') { + if (lowSocGridTopUp) { + this.log(`[MAPPING][STANDBY] cheap €${price.toFixed(3)} + low SoC ${soc}% + partial PV (${Math.round(estimatedNetPvSurplusW)}W) → to_full`); + return 'to_full'; + } + this.log(`[MAPPING] standby (DP: PV export beats storage at current price)`); + return 'standby'; + } + + // 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') { + if (!profitableToDischarge) return 'standby'; + // During active PV production, zero_discharge_only is effectively standby + // when PV surplus > house load (grid already negative, nothing to discharge + // toward 0W). Use 'zero' instead: charges from free PV surplus when PV > load, + // discharges to cover consumption when load > PV. Grid stays ~0W both ways. + // Delay-charge path (above) intentionally exports PV at high price and is excluded. + if (actualPvNow && pvEstimate >= 100 && soc > minSoc) { + this.log(`[MAPPING][DISCHARGE] PV active (${pvEstimate}W) + profitable → zero (surplus charges, battery covers load peaks)`); + return 'zero'; + } + return 'zero_discharge_only'; + } + + if (policyMode === 'charge') { + + // Negative price: charging earns money — always pull from grid regardless of PV state. + if (price !== null && price < 0) { + this.log(`[MAPPING][CHARGE] negative price €${price.toFixed(3)} → to_full (grid charging earns money)`); + return 'to_full'; + } + + // _pvStoreWins: storing PV surplus is more valuable than exporting. + // Use zero_charge_only: only harvest from surplus, do not discharge. + // When PV > consumption (the _pvStoreWins condition), the battery never + // discharges in zero_charge_only anyway. If a cloud temporarily drops PV, + // we want to preserve stored energy for the planned high-price discharge + // slot — not spend it covering a transient load gap. + // Skip when _chargeUrgent (grid charging needed). + if (ctx._pvStoreWins && actualPvNow && pvCanSustainCharge && !ctx._chargeUrgent) { + this.log(`[MAPPING][CHARGE] _pvStoreWins + sustained PV surplus (${Math.round(estimatedNetPvSurplusW)}W), soc=${soc}% → zero_charge_only (store surplus for planned discharge)`); + 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'; + } + + // Cheap hour: follow the DP and allow full-rate charging unless PV surplus can + // already sustain (almost) the full battery charge power on its own. + if (price !== null && price <= maxChargePrice) { + if (actualPvNow && pvCanSustainCharge && !ctx._chargeUrgent) { + this.log(`[MAPPING][CHARGE] cheap hour €${price.toFixed(3)} but PV surplus already sustains charge (${Math.round(estimatedNetPvSurplusW)}W) → zero_charge_only`); + return 'zero_charge_only'; + } + this.log(`[MAPPING][CHARGE] cheap hour €${price.toFixed(3)} <= max_charge_price €${maxChargePrice.toFixed(3)} → 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') { + // Negative price: hold capacity for better grid-charge slots — standby even with PV. + if (price !== null && price < 0) return 'standby'; + if (lowSocGridTopUp) { + this.log(`[MAPPING][PRESERVE] cheap €${price.toFixed(3)} + low SoC ${soc}% + partial PV (${Math.round(estimatedNetPvSurplusW)}W) → to_full`); + return 'to_full'; + } + // Export more profitable than storing: let PV go to grid, battery idles. + if (actualPvNow && ctx._pvStoreWins === false) { + this.log(`[MAPPING][PRESERVE] export wins (_pvStoreWins=false) → standby (PV to grid)`); + return 'standby'; + } + 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, minDischargePriceOverride = null, battChargePowerW = 800, maxChargePriceOverride = null, batteryCapacityKwhOverride = null) { + if (!slots || slots.length === 0) return []; + + const tariffType = this.settings.tariff_type || 'dynamic'; + const maxChargePrice = maxChargePriceOverride ?? 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 runtime floor logic so the planning display is consistent with execution. + // strict mode → min_discharge_price + // opportunistic → economic break-even derived from settings (cycle_cost / efficiency) + const respectMinMax = (raw === 'balanced-dynamic') + ? false + : this.settings.respect_minmax !== false; + const _settingsFloor = (this.settings.cycle_cost_per_kwh ?? 0.075) / (this.settings.battery_efficiency || 0.75); + const baseMinDischargePrice = respectMinMax + ? (this.settings.min_discharge_price || 0) + : _settingsFloor; + // minDischargePriceOverride may be a per-slot array (PV headroom mode) or a scalar. + const isPerSlotOverride = Array.isArray(minDischargePriceOverride); + const scalarMinDischarge = isPerSlotOverride + ? baseMinDischargePrice + : (minDischargePriceOverride !== null && minDischargePriceOverride < baseMinDischargePrice) + ? minDischargePriceOverride + : baseMinDischargePrice; + + const enriched = slots.map(slot => ({ + ...slot, + pvW: this._getPvWForTimestamp(slot.timestamp, pvForecast), + })); + + const battCapKwh = batteryCapacityKwhOverride ?? (Math.max(1, Math.round(battChargePowerW / 800)) * 2.688); + const slotHours = enriched.length > 1 + ? (new Date(enriched[1].timestamp).getTime() - new Date(enriched[0].timestamp).getTime()) / 3_600_000 + : 0.25; + const result = []; + let simSoc = enriched[0]?.socProjected ?? 0; + + for (let i = 0; i < enriched.length; i++) { + const slot = enriched[i]; + const minDischargePrice = isPerSlotOverride + ? (minDischargePriceOverride[i] ?? baseMinDischargePrice) + : scalarMinDischarge; + const { hwMode, reason } = this._mapActionToHwModeForPlanning(slot.action, { + price: slot.price, soc: simSoc, + pvW: slot.pvW, consumptionW: slot.consumptionW ?? 0, + tariffType, userPolicyMode, + maxChargePrice, minDischargePrice, minSoc, maxSoc, + futurePrices: enriched.slice(i + 1), + battChargePowerW, + pvStoreValue: slot.pvStoreValue, + }); + result.push({ + timestamp: slot.timestamp, + action: slot.action, + hwMode, + reason, + socProjected: Math.round(simSoc * 10) / 10, + price: slot.price, + pvW: slot.pvW, + consumptionW: slot.consumptionW ?? null, + pvCoverage: slot.pvCoverage ?? null, + }); + if (hwMode === 'zero_charge_only' || hwMode === 'pv_trickle') { + const surplus = Math.max(0, (slot.pvW ?? 0) - (slot.consumptionW ?? 0)); + const chargeKwh = Math.min(battChargePowerW, surplus) * slotHours / 1000; + simSoc = Math.min(maxSoc, simSoc + chargeKwh / battCapKwh * 100); + } else if (hwMode === 'zero_discharge_only') { + const dpDelta = (enriched[i + 1]?.socProjected ?? slot.socProjected) - slot.socProjected; + simSoc = Math.max(minSoc, Math.min(maxSoc, simSoc + dpDelta)); + } else if (hwMode === 'to_full') { + if (slot.action === 'charge') { + // DP planned charge: dpDelta is accurate + const dpDelta = (enriched[i + 1]?.socProjected ?? slot.socProjected) - slot.socProjected; + simSoc = Math.max(minSoc, Math.min(maxSoc, simSoc + dpDelta)); + } else { + // Mapper override (standby/preserve → to_full): DP projected flat SoC, so dpDelta≈0. + // Compute the actual grid-charge delta directly. + const chargeKwh = battChargePowerW * slotHours / 1000; + simSoc = Math.min(maxSoc, simSoc + chargeKwh / battCapKwh * 100); + } + } + // standby / zero: simSoc stays flat + } + + return result; + } + + _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; + + // Linear interpolation between surrounding hourly entries — consistent with + // OptimizationEngine._getPvForSlot so planning and DP use the same pvW values. + let left = null, right = null; + for (const f of pvForecast) { + const t = new Date(f.timestamp).getTime(); + if (t <= tsMs && (left === null || t > new Date(left.timestamp).getTime())) left = f; + if (t >= tsMs && (right === null || t < new Date(right.timestamp).getTime())) right = f; + } + if (!left && !right) return 0; + if (!left) return right.pvPowerW ?? 0; + if (!right) return left.pvPowerW ?? 0; + const leftMs = new Date(left.timestamp).getTime(); + const rightMs = new Date(right.timestamp).getTime(); + if (leftMs === rightMs) return left.pvPowerW ?? 0; + const frac = (tsMs - leftMs) / (rightMs - leftMs); + return Math.round((left.pvPowerW ?? 0) + ((right.pvPowerW ?? 0) - (left.pvPowerW ?? 0)) * frac); + } + + /** + * 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, consumptionW = 0, tariffType, userPolicyMode, maxChargePrice, minDischargePrice, minSoc, maxSoc, futurePrices, battChargePowerW = 800, pvStoreValue = null }) { + const pvStrong = pvW >= 400; + const pvPresent = pvW >= 100; + const netSurplusW = Math.max(0, pvW - consumptionW); + const pvOnlyChargeFloorW = Math.max(400, Math.round(battChargePowerW * 0.8)); + const pvCanSustainCharge = netSurplusW >= pvOnlyChargeFloorW; + const profitableToDischarge = price !== null && price >= minDischargePrice; + // Low-SoC grid top-up: DP said standby/preserve (pvExportWins) but battery is nearly empty, + // price is cheap, and PV surplus is insufficient to cover charging. Grid-charge makes sense. + // Only fire when current price is the cheapest in the next 8h — otherwise defer to DP's planned slot. + const _topUpMinFuture2 = Array.isArray(futurePrices) && futurePrices.length + ? Math.min(...futurePrices.slice(0, 8).map(s => s.price).filter(p => typeof p === 'number' && p > 0)) + : price; + const lowSocGridTopUp = price !== null + && price <= maxChargePrice + && price <= (_topUpMinFuture2 ?? price) + && soc < Math.min(maxSoc, 40) + && netSurplusW < Math.max(50, battChargePowerW - 50); + const r = (hwMode, reason) => ({ hwMode, reason }); + + // Returns the avg cheapest-N price if delay is warranted, otherwise null. + // Compares current price to the average of the cheapest N future PV slots needed to fill + // the battery, rather than just the single minimum — so 12:00 (€0.111) is not delayed + // when the charging window average is already close to the current price. + const pvDelayMin = (price !== null && pvStrong && Array.isArray(futurePrices) && futurePrices.length > 0) ? (() => { + const pvNeeded = battChargePowerW * 0.5; + const battCapKwh = Math.max(1, Math.round(battChargePowerW / 800)) * 2.688; + const toChargeKwh = ((maxSoc - soc) / 100) * battCapKwh; + const slotsNeeded = Math.max(1, Math.ceil(toChargeKwh / (battChargePowerW / 1000))); + const _pvSlots = futurePrices.slice(0, 32) + .filter(s => typeof s.price === 'number' && s.price > 0 && (s.pvW ?? 0) >= pvNeeded) + .sort((a, b) => a.price - b.price); + if (_pvSlots.length < slotsNeeded) return null; // not enough PV slots to fill battery + const _cheapestN = _pvSlots.slice(0, slotsNeeded); + const _avg = _cheapestN.reduce((s, x) => s + x.price, 0) / _cheapestN.length; + if (!(price > _avg * 1.30 && price - _avg >= 0.03)) return null; + // Verify enough total PV energy to fill the battery + const futurePvKwh = _pvSlots.reduce((sum, s) => + sum + Math.min(battChargePowerW, Math.max(0, (s.pvW ?? 0) - (s.consumptionW ?? 0))) / 1000, 0); + return futurePvKwh >= toChargeKwh * 1.2 ? _avg : null; + })() : null; + + if (userPolicyMode === 'zero') { + if (soc <= minSoc) return r('zero_charge_only', 'zero:soc_min'); + if (soc >= maxSoc) return r('zero_discharge_only', 'zero:soc_max'); + return r('zero', 'zero:peak_shaving'); + } + + if (action === 'trickle') { + if (soc >= maxSoc) return r('standby', 'trickle:soc_full'); + return r('pv_trickle', `trickle:pv_weak(${pvW}W)`); + } + if (action === 'standby') { + if (lowSocGridTopUp) return r('to_full', `dp:topup soc${Math.round(soc)}% €${price?.toFixed(3)}`); + return r('standby', 'dp:pv_export_wins'); + } + + if (action === 'discharge') { + if (soc <= minSoc) return r('standby', `dp:discharge blocked soc≤min`); + return profitableToDischarge + ? r('zero_discharge_only', `dp:discharge price≥min(${minDischargePrice?.toFixed(3)})`) + : r('standby', `dp:discharge blocked price 0) return r('zero_charge_only', `dp:charge pv_present(${netSurplusW}W)`); + return r('standby', `dp:charge_blocked price>${maxChargePrice?.toFixed(3)} no_pv`); + } + + // preserve + if (price !== null && price < 0) return r('standby', 'preserve:negative_price'); + if (soc >= maxSoc) return r('standby', 'preserve:soc_full'); + if (lowSocGridTopUp) return r('to_full', `dp:topup soc${Math.round(soc)}% €${price?.toFixed(3)}`); + if (pvDelayMin !== null) return r('standby', `preserve:pv_delay(min€${pvDelayMin.toFixed(3)})`); + if (pvStrong && netSurplusW >= 400) { + const economics = price != null && this.BATTERY_EFFICIENCY > 0 + ? `,op€${(price / this.BATTERY_EFFICIENCY).toFixed(3)}>exp€${price.toFixed(3)}` + : ''; + return r('zero_charge_only', `preserve:pv_strong(${pvW}W${economics})`); + } + return (pvPresent && netSurplusW >= -50) + ? r('pv_trickle', `preserve:pv_weak(${pvW}W)`) + : r('standby', 'preserve:hold'); + } + + 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; diff --git a/lib/solcast-provider.js b/lib/solcast-provider.js new file mode 100644 index 00000000..954a82f7 --- /dev/null +++ b/lib/solcast-provider.js @@ -0,0 +1,141 @@ +'use strict'; + +const fetchWithTimeout = require('../includes/utils/fetchWithTimeout'); + +const CACHE_KEY = 'solcast_forecast_cache'; +const CACHE_TTL_DAY = 2 * 60 * 60 * 1000; // 2h during daylight → 8 fetches in 16h window +const CACHE_TTL_NIGHT = 8 * 60 * 60 * 1000; // 8h at night → ~1 fetch, data barely changes +const BASE_URL = 'https://api.solcast.com.au'; + +// Amsterdam local hour (UTC+1 winter, UTC+2 summer). Night = 22:00–06:00. +function _amsterdamHour() { + const now = new Date(); + const ams = new Date(now.toLocaleString('en-US', { timeZone: 'Europe/Amsterdam' })); + return ams.getHours(); +} + +function _cacheTtl() { + const h = _amsterdamHour(); + return (h >= 6 && h < 22) ? CACHE_TTL_DAY : CACHE_TTL_NIGHT; +} + +/** + * SolcastProvider — fetches rooftop PV forecast from Solcast API. + * + * Caches results in homey.settings to survive app restarts. + * TTL is time-aware: 2h during daylight (06:00–22:00 Amsterdam), 8h at night. + * + * Returns array of { timestamp: string (ISO), pvPowerW: number } + * at 30-min resolution — compatible with optimization-engine._getPvForSlot(). + */ +class SolcastProvider { + constructor(homey) { + this.homey = homey; + } + + /** + * Get PV forecast. Returns cached data if fresh, otherwise fetches. + * @param {string} apiKey + * @param {string} resourceId + * @returns {Promise|null>} + */ + async getForecast(apiKey, resourceId) { + const cached = this._loadCache(); + if (cached) { + this.homey.log('[Solcast] Using cached forecast'); + return cached; + } + + if (this._fetchPromise) return this._fetchPromise; + this._fetchPromise = this._fetch(apiKey, resourceId).finally(() => { this._fetchPromise = null; }); + const result = await this._fetchPromise; + if (result) return result; + + // Fresh fetch failed (e.g. 429 rate limit) — return stale cache rather than nothing + const stale = this._loadStaleCache(); + if (stale) { + this.homey.log('[Solcast] Using stale cache (fetch failed)'); + return stale; + } + return null; + } + + async _fetch(apiKey, resourceId) { + const url = `${BASE_URL}/rooftop_sites/${encodeURIComponent(resourceId)}/forecasts?format=json&hours=48`; + try { + const res = await fetchWithTimeout(url, { + headers: { Authorization: `Bearer ${apiKey}` }, + }, 10000); + + if (!res.ok) { + this.homey.log(`[Solcast] Fetch failed: HTTP ${res.status}`); + return null; + } + + const json = await res.json(); + const data = this._process(json.forecasts); + if (data) { + this._saveCache(data); + this.homey.log(`[Solcast] Fetched ${data.length} slots (30-min)`); + } + return data; + } catch (err) { + this.homey.log(`[Solcast] Fetch error: ${err.message}`); + return null; + } + } + + /** + * Convert Solcast forecasts array → { timestamp, pvPowerW }[]. + * period_end is end of 30-min slot; timestamp = start of slot. + * @private + */ + _process(forecasts) { + if (!Array.isArray(forecasts) || forecasts.length === 0) return null; + return forecasts.map(f => ({ + timestamp: new Date(new Date(f.period_end).getTime() - 30 * 60 * 1000).toISOString(), + pvPowerW: Math.round((f.pv_estimate ?? 0) * 1000), // kW → W p50 + pvPowerW10: Math.round((f.pv_estimate10 ?? 0) * 1000), // kW → W p10 (conservative) + })); + } + + _loadCache() { + try { + const stored = this.homey.settings.get(CACHE_KEY); + if (!stored || !stored.fetchedAt || !stored.data) return null; + if (Date.now() - stored.fetchedAt > _cacheTtl()) return null; + return stored.data; + } catch (_) { + return null; + } + } + + _loadStaleCache() { + try { + const stored = this.homey.settings.get(CACHE_KEY); + if (!stored || !stored.data) return null; + return stored.data; + } catch (_) { + return null; + } + } + + _saveCache(data) { + try { + this.homey.settings.set(CACHE_KEY, { data, fetchedAt: Date.now() }); + } catch (err) { + this.homey.log(`[Solcast] Cache save failed: ${err.message}`); + } + } + + /** + * Invalidate the cache (e.g. when API key changes). + */ + invalidateCache() { + try { + this.homey.settings.unset(CACHE_KEY); + } catch (_) {} + } +} + +module.exports = SolcastProvider; diff --git a/lib/tariff-manager.js b/lib/tariff-manager.js new file mode 100644 index 00000000..45ab53b7 --- /dev/null +++ b/lib/tariff-manager.js @@ -0,0 +1,522 @@ +'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(); + } + + // Debounced writer for price settings. homey.settings.set allocates ~30 MB + // V8 heap per call (framework-internal, independent of payload). Coalesce + // multiple updates into a single delayed write to keep startup heap low. + _schedulePricesPersist({ all15min = null, allPrices = null, hasNative = false }) { + this._pendingPricesPersist = { + all15min: all15min ?? this._pendingPricesPersist?.all15min ?? null, + allPrices: allPrices ?? this._pendingPricesPersist?.allPrices ?? null, + hasNative, + }; + if (this._pricesPersistTimer) clearTimeout(this._pricesPersistTimer); + this._pricesPersistTimer = setTimeout(() => { + this._pricesPersistTimer = null; + const { all15min, allPrices, hasNative } = this._pendingPricesPersist || {}; + this._pendingPricesPersist = null; + try { + if (allPrices) this.homey.settings.set('policy_all_prices', allPrices); + if (all15min && all15min.length > 0) { + this.homey.settings.set('policy_all_prices_15min', all15min); + this.log(`💾 Saved ${all15min.length} 15-min prices to settings (${hasNative ? 'native' : 'expanded hourly'})`); + } + } catch (err) { + this.log('Failed to save policy prices to settings:', err.message); + } + }, 60 * 1000); + } + + _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 after full fetch — debounced 60s to avoid the ~30 MB + // V8 heap spike that homey.settings.set triggers during the startup cascade. + // The 60s delay is well before the first policy run (T+45s) matters: first + // saveWidgetData reads this key at T+90s. + const all15min = this.getAll15MinPrices(); + if (all15min.length > 0) { + const hasNative15Min = !!(this.kwhpriceProvider?.cache15min?.length > 0 || + this.xadiProvider?.cache15min?.length > 0); + this._schedulePricesPersist({ all15min, hasNative: hasNative15Min }); + this.log(`💾 Initial save scheduled: ${all15min.length} 15-min prices in 60s (${hasNative15Min ? 'native' : 'expanded hourly'})`); + } + 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(); + const force1h = this.settings.price_resolution === '1h'; + + // Use 15-min price as currentPrice when available (more accurate for policy decisions) + // In 1h-mode, derive from allPrices (the current hour has index -1, i.e. started in the past). + const allPrices15min = this.getAll15MinPrices(); + let currentPrice; + if (force1h) { + const curSlot = allPrices.reduce((best, p) => { + if (p.index < 0 && (best === null || p.index > best.index)) return p; + return best; + }, null); + currentPrice = curSlot?.price ?? null; + } else { + 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 (!force1h && 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; when due, defer + // the actual settings.set by 60s (debounce) so the ~30 MB V8 heap spike + // never lands during a price-refresh moment. + const _now = Date.now(); + if (!this._lastPriceSettingsSave || _now - this._lastPriceSettingsSave > 300_000) { + this._lastPriceSettingsSave = _now; + const hasNative = !!(this.kwhpriceProvider?.cache15min?.length > 0 || + this.xadiProvider?.cache15min?.length > 0); + this._schedulePricesPersist({ + all15min: allPrices15min && allPrices15min.length > 0 ? allPrices15min : null, + allPrices, + hasNative, + }); + } + + 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..0e64185c --- /dev/null +++ b/lib/weather-forecaster.js @@ -0,0 +1,1076 @@ +'use strict'; + +const fetchWithTimeout = require('../includes/utils/fetchWithTimeout'); +const { fetchKnmiObservations } = require('./knmi-stations'); + +// Single retry on transient errors (TIMEOUT, 5xx). Open-Meteo overloads at peak. +async function fetchWithRetry(url, options, timeoutMs, retryDelayMs = 3000) { + try { + const res = await fetchWithTimeout(url, options, timeoutMs); + if (res.ok || res.status < 500) return res; + // 5xx → retry + await new Promise(r => setTimeout(r, retryDelayMs)); + return fetchWithTimeout(url, options, timeoutMs); + } catch (err) { + if (err.message !== 'TIMEOUT') throw err; + await new Promise(r => setTimeout(r, retryDelayMs)); + return fetchWithTimeout(url, options, timeoutMs); + } +} + +/** + * 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'); + this._maybeRecordKnmiActual(latitude, longitude); + 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)`); + this._maybeRecordKnmiActual(latitude, longitude); + return this.cache; + } + } + + // Deduplicate concurrent fetch calls (e.g. policy check + onSettings firing simultaneously) + if (this._fetchPromise) return this._fetchPromise; + this._fetchPromise = this._doFetch(latitude, longitude, tilt, azimuth) + .finally(() => { this._fetchPromise = null; }); + return this._fetchPromise; + } + + async _doFetch(latitude, longitude, tilt, azimuth) { + 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 modelWeights = this.learningEngine?.getModelWeights?.() ?? null; + const rawData = this._mergeApiResponses(ensembleData, standardData, tiltedData, lat, lon, modelWeights, useTilted ? tilt : null, useTilted ? azimuth : null); + // ensembleData no longer needed — perModelGhiAvgToday extracted into rawData during merge + + await this._learnFromYesterday(rawData); + await this._maybeRecordKnmiActual(lat, lon); + + const newForecast = this._processForecast(rawData); + this.cache = this._blendForecast(this.cache, newForecast, !ensembleData); + 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 in-memory cache if available, even if expired + if (this.cache) { + this.log('Returning stale in-memory cache due to fetch error'); + return this.cache; + } + + // On restart: try persistent cache even if expired (max 24h stale) + const stale = this._loadStaleCache(24 * 60 * 60 * 1000); + if (stale) { + this.cache = stale; + this.log('Returning stale persistent cache due to fetch error (API unavailable)'); + return this.cache; + } + + // Return pessimistic default if no cache at all + 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 4-model ensemble (ECMWF, GFS, ICON, KNMI Harmonie) in parallel. + * KNMI Harmonie AROME Netherlands: 2km resolution, hourly updates, 2.5-day horizon — improves + * accuracy for partial cloud cover in NL. Falls back gracefully if model returns no data. + * 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,diffuse_radiation,direct_normal_irradiance', + models: 'meteofrance_arpege_europe,gfs_seamless,icon_seamless,knmi_harmonie_arome_netherlands', + past_days: '1', + forecast_days: '2', + timezone: 'UTC' + }); + const url = `https://api.open-meteo.com/v1/forecast?${params.toString()}`; + this.log(`Fetching ensemble radiation: ${url}`); + const res = await fetchWithRetry(url, {}, 15000); + 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,cloud_cover_low,precipitation_probability,precipitation,weather_code', + daily: 'sunrise,sunset', + past_days: '1', + forecast_days: '2', + timezone: 'UTC' + }); + const url = `https://api.open-meteo.com/v1/forecast?${params.toString()}`; + this.log(`Fetching standard hourly: ${url}`); + const res = await fetchWithRetry(url, {}, 20000); + 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: '2', + 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 fetchWithRetry(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, modelWeights = null, tilt = null, azimuth = null) { + const ENSEMBLE_MODELS = ['meteofrance_arpege_europe', 'gfs_seamless', 'icon_seamless', 'knmi_harmonie_arome_netherlands']; + const times = standardData.hourly.time; + + let shortwave_radiation; + if (ensembleData) { + // Weighted blend across models; spread discount when models disagree (std > 30 W/m²). + // Weights from learned per-model accuracy; falls back to equal weights if unavailable. + const equalW = 1 / ENSEMBLE_MODELS.length; + const w = modelWeights ?? Object.fromEntries(ENSEMBLE_MODELS.map(m => [m, equalW])); + let spreadSlotsAdjusted = 0; + shortwave_radiation = times.map((_, i) => { + const modelVals = ENSEMBLE_MODELS + .map(m => ({ m, v: ensembleData.hourly[`shortwave_radiation_${m}`]?.[i] })) + .filter(({ v }) => typeof v === 'number'); + if (modelVals.length === 0) return 0; + // Weighted mean + const totalW = modelVals.reduce((s, { m }) => s + (w[m] ?? equalW), 0); + const wMean = modelVals.reduce((s, { m, v }) => s + (w[m] ?? equalW) * v, 0) / totalW; + if (modelVals.length < 2) return Math.round(wMean); + // Spread discount on unweighted std to detect model disagreement + const uMean = modelVals.reduce((s, { v }) => s + v, 0) / modelVals.length; + const std = Math.sqrt(modelVals.reduce((s, { v }) => s + (v - uMean) ** 2, 0) / modelVals.length); + if (std > 80) { spreadSlotsAdjusted++; return Math.round(Math.max(0, wMean - 0.5 * std)); } + if (std > 30) { spreadSlotsAdjusted++; return Math.round(Math.max(0, wMean - 0.3 * std)); } + return Math.round(wMean); + }); + const sampleAvg = shortwave_radiation.slice(0, 24).filter(v => v > 0); + if (sampleAvg.length > 0) { + const wLog = modelWeights + ? ENSEMBLE_MODELS.map(m => `${m.replace('knmi_harmonie_arome_netherlands','knmi').replace('_seamless','')}=${(w[m]*100).toFixed(0)}%`).join(' ') + : 'equal'; + this.log(`Ensemble radiation blended from ${ENSEMBLE_MODELS.length} models [${wLog}] (sample avg: ${Math.round(sampleAvg.reduce((a, b) => a + b, 0) / sampleAvg.length)} W/m²${spreadSlotsAdjusted > 0 ? `, spread-adjusted ${spreadSlotsAdjusted} slots` : ''})`); + } + } else { + // Fallback: use standard single-model shortwave_radiation + shortwave_radiation = standardData.hourly.shortwave_radiation; + } + + // Per-model radiation arrays aligned to `times`. When tilt/azimuth are configured, + // values are GTI (W/m² on the panel plane) computed via isotropic transposition. + // Without tilt/azimuth, falls back to GHI (shortwave_radiation per model). + const perModelRadiation = {}; + if (ensembleData) { + const ENSEMBLE_MODELS = ['meteofrance_arpege_europe', 'gfs_seamless', 'icon_seamless', 'knmi_harmonie_arome_netherlands']; + const useGti = typeof tilt === 'number' && typeof azimuth === 'number'; + for (const m of ENSEMBLE_MODELS) { + perModelRadiation[m] = times.map((t, i) => { + const ghi = ensembleData.hourly[`shortwave_radiation_${m}`]?.[i] ?? null; + const dhi = ensembleData.hourly[`diffuse_radiation_${m}`]?.[i] ?? null; + const dni = ensembleData.hourly[`direct_normal_irradiance_${m}`]?.[i] ?? null; + // Open-Meteo omits shortwave_radiation for some past/current slots in ensemble + // but still provides DNI+DHI. GHI is optional — only used for reflected component. + if (!useGti) return typeof ghi === 'number' ? ghi : null; + if (dhi == null || dni == null) return typeof ghi === 'number' ? ghi : null; + const { elev, azimuth: solAz } = WeatherForecaster._solarElevAz(new Date(t + 'Z'), lat, lon); + return WeatherForecaster._computeGTI(dhi, dni, ghi ?? 0, elev, solAz, tilt, azimuth); + }); + } + } + + // Per-model GHI daily average for today — used by _learnFromYesterday for snapshot/accuracy. + // Must use GHI (not GTI) so it's comparable to KNMI ground-truth (also GHI). + // Computed here so ensembleData can be freed immediately after _mergeApiResponses returns. + const todayDate = new Date().toISOString().slice(0, 10); + const perModelGhiAvgToday = {}; + if (ensembleData) { + for (const m of ENSEMBLE_MODELS) { + let mSum = 0, mCnt = 0; + for (let i = 0; i < times.length; i++) { + if (!times[i].startsWith(todayDate)) continue; + const v = ensembleData.hourly[`shortwave_radiation_${m}`]?.[i]; + if (typeof v === 'number' && v > 10) { mSum += v; mCnt++; } + } + if (mCnt > 0) perModelGhiAvgToday[m] = Math.round(mSum / mCnt); + } + } + + return { + latitude: lat, + longitude: lon, + timezone: standardData.timezone || 'UTC', + hourly: { + time: times, + shortwave_radiation, + perModelRadiation, + sunshine_duration: standardData.hourly.sunshine_duration, + temperature_2m: standardData.hourly.temperature_2m, + cloud_cover: standardData.hourly.cloud_cover, + cloud_cover_low: standardData.hourly.cloud_cover_low, + precipitation_probability: standardData.hourly.precipitation_probability, + precipitation: standardData.hourly.precipitation, + weather_code: standardData.hourly.weather_code, + ...(tiltedData ? { global_tilted_irradiance: tiltedData.hourly.global_tilted_irradiance } : {}) + }, + daily: standardData.daily, + perModelGhiAvgToday: Object.keys(perModelGhiAvgToday).length > 0 ? perModelGhiAvgToday : null, + }; + } + + /** + * Process raw API response into usable format + * @private + */ + _blendForecast(oldCache, newForecast, ensembleFallback = false) { + 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) { + // Past slots: when ensemble timed out, keep cached radiation to avoid jumps from GHI fallback + if (ensembleFallback) { + const old = oldProfileMap.get(slot.time.getTime()); + if (old) return { ...slot, radiationWm2: old.radiationWm2, sunshine: old.sunshine }; + } + return slot; + } + 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; + if (slot.time.getTime() <= now && ensembleFallback) { + return { ...slot, radiationWm2: old.radiationWm2, sunshine: old.sunshine }; + } + 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; + // With past_days=1 the daily array is [yesterday, today, tomorrow] → indices 1 and 2. + const todaySunrise = parseDailyTime(daily.sunrise?.[1]); + const todaySunset = parseDailyTime(daily.sunset?.[1]); + const tomorrowSunrise = parseDailyTime(daily.sunrise?.[2]); + const tomorrowSunset = parseDailyTime(daily.sunset?.[2]); + + // 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 cloudCoverLow = hourly.cloud_cover_low?.[idx] ?? 0; + 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; + const precipProb = hourly.precipitation_probability?.[idx] ?? 0; + const precipMmh = hourly.precipitation?.[idx] ?? 0; + + // Weather-based PV attenuation: fog, snow cover, heavy rain, thunderstorms. + // Open-Meteo GHI already accounts for cloud cover, but these phenomena + // cause additional losses that the irradiance model doesn't capture + // (e.g. snow on panels, extreme scattering from heavy precipitation). + const wxFactor = WeatherForecaster._weatherAttenuation(weatherCode, precipProb); + + // 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 * wxFactor) / 3600, // seconds → hours (max 1h per slot) + cloudCover, + cloudCoverLow, + temp: hourly.temperature_2m?.[idx] ?? 0, + precipProb, + precipMmh, + weatherCode, + radiationWm2: Math.round(rawRadiation * biasFactor * wxFactor), + perModelWm2: hourly.perModelRadiation + ? Object.fromEntries( + Object.entries(hourly.perModelRadiation).map(([m, arr]) => [m, arr?.[idx] ?? null]) + ) + : null, + }); + } + + // 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 pProb = hourly.precipitation_probability?.[i] ?? 0; + const wxF = WeatherForecaster._weatherAttenuation(wCode, pProb); + const cloudF = cloudCover <= 40 ? 1.0 : cloudCover >= 90 ? 0.0 : (90 - cloudCover) / 50; + + dailyProfiles.push({ + time: new Date(`${t}Z`), + cloudCover, + sunshine: (rawSunshineSec * cloudF * wxF) / 3600, + radiationWm2: Math.round(rawRadiation * biasFactor * wxF), + 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 + + // Prefer KNMI station ground-truth over OpenMeteo's own historical data (circular otherwise) + const knmiAvg = this.learningEngine.getKnmiDailyAvg?.(yDate) ?? null; + const actualAvg = knmiAvg ?? (actualSum / actualCount); + if (knmiAvg != null) this.log(`[KNMI] Using station qg=${knmiAvg.toFixed(0)} W/m² as actual for ${yDate}`); + + // 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²`); + } + + let cloudSum = 0, cloudCount = 0; + for (let i = 0; i < currentIndex; i++) { + if (!hourly.time[i].startsWith(yDate)) continue; + const cc = hourly.cloud_cover?.[i]; + if (typeof cc === 'number') { cloudSum += cc; cloudCount++; } + } + const avgCloudPct = cloudCount > 0 ? cloudSum / cloudCount : null; + + // Compute clearness index (kt) from KNMI hourly actuals vs clear-sky GHI. + // kt is more reliable than OM cloud% for bias EMA classification. + const lat = rawData.latitude, lon = rawData.longitude; + const knmiKt = (lat != null && lon != null) ? this._computeKnmiKt(yDate, lat, lon) : null; + if (knmiKt != null) this.log(`[KNMI] kt=${knmiKt.toFixed(2)} from KNMI hourly actuals for ${yDate}`); + await this.learningEngine.recordDailyPvBiasFromPredictions(yDate, avgCloudPct, knmiKt); + + // Save today's full-day radiation as snapshot for tomorrow's comparison. + // Use ALL slots for today (past + future) so the snapshot always covers the full + // solar day, even when this runs late in the afternoon after peak hours have passed. + const todayDate = now.toISOString().slice(0, 10); + let todaySum = 0, todayCount = 0; + + for (let i = 0; i < hourly.time.length; i++) { + const t = hourly.time[i]; + if (!t.startsWith(todayDate)) continue; + const rad = radField(i); + if (typeof rad === 'number' && rad > 10) { + todaySum += rad; + todayCount++; + } + } + + // Per-model: save today's forecast avg per model for tomorrow's accuracy comparison. + // Pre-computed in _mergeApiResponses using GHI (not GTI) so it's comparable to KNMI ground-truth. + const todayPerModel = rawData.perModelGhiAvgToday ?? null; + if (todayCount > 0) { + const pmLog = todayPerModel ? Object.entries(todayPerModel).map(([m,v]) => `${m.replace('meteofrance_arpege_europe','mf').replace('_seamless','').replace('knmi_harmonie_arome_netherlands','knmi')}=${v}`).join(' ') : 'null'; + this.log(`[Snapshot] ${todayDate} rad=${Math.round(todaySum/todayCount)} perModel=${pmLog} hourlyLen=${hourly.time.length}`); + await this.learningEngine.saveForecastSnapshot(todayDate, todaySum / todayCount, todayPerModel); + } + + // Per-model accuracy: compare yesterday's per-model snapshot vs actual radiation. + const ySnapshot = this.learningEngine.getForecastSnapshot(yDate); + this.log(`[ModelAccuracy check] ${yDate}: perModel=${ySnapshot?.perModel ? Object.keys(ySnapshot.perModel).join(',') : 'null'} actualCount=${actualCount}`); + if (ySnapshot?.perModel && actualCount > 0) { + await this.learningEngine.recordModelAccuracy(ySnapshot.perModel, actualAvg, yDate); + } + + } catch (err) { + this.error('_learnFromYesterday error:', err.message); + } + } + + /** + * Rate-limited wrapper: calls _recordKnmiActual at most once per hour. + * Safe to call on every fetchForecast invocation (incl. cache hits). + * @private + */ + _maybeRecordKnmiActual(lat, lon) { + const now = Date.now(); + if (this._knmiLastFetch && now - this._knmiLastFetch < 55 * 60 * 1000) return; + this._knmiLastFetch = now; + this._recordKnmiActual(lat, lon).catch(() => {}); + } + + /** + * Fetch current KNMI station qg and record in learning engine for ground-truth accuracy tracking. + * Silently skips if no API key or on any error — KNMI is non-critical. + * @private + */ + async _recordKnmiActual(lat, lon) { + if (!this.learningEngine) return; + const apiKey = this.knmiApiKey; + if (!apiKey) { this.log('[KNMI] no API key configured, skipping'); return; } + try { + const result = await fetchKnmiObservations(apiKey, lat, lon); + if (!result || result.qg == null) return; + const now = new Date(); + const dateStr = now.toISOString().slice(0, 10); + const hour = now.getUTCHours(); + this.learningEngine.recordKnmiHourlyActual(result.qg, dateStr, hour); + this.log(`[KNMI] ${result.stationName} (${result.distKm}km): qg=${result.qg} W/m² n=${result.n ?? '?'} okta ss=${result.ss ?? '?'} min ta=${result.ta ?? '?'}°C`); + // Update today's partial kt for use by getDailyPvBiasFactor + const todayKt = this._computeKnmiKt(dateStr, lat, lon); + if (todayKt != null) { + this._todayKnmiKt = todayKt; + this.log(`[KNMI] today kt=${todayKt.toFixed(2)} (partial)`); + } + } catch (err) { + this.error('[KNMI] fetch failed:', err.message); + } + } + + /** + * Compute KNMI clearness index (kt = actual_GHI / clear-sky_GHI) for a given UTC date. + * Returns null when fewer than 4 daylight hours of KNMI data are available. + * @private + */ + _computeKnmiKt(dateStr, lat, lon) { + const knmiHourly = this.learningEngine?.data?.knmi_hourly_actuals?.[dateStr]; + if (!knmiHourly) return null; + let ksActual = 0, ksClear = 0, ksCount = 0; + for (const [hourStr, ghi] of Object.entries(knmiHourly)) { + if (typeof ghi !== 'number' || ghi <= 10) continue; + const hour = parseInt(hourStr); + const d = new Date(`${dateStr}T${String(hour).padStart(2, '0')}:30:00Z`); + const { elev } = WeatherForecaster._solarElevAz(d, lat, lon); + if (elev < 5) continue; + const ghiClear = 1000 * Math.sin(elev * Math.PI / 180); + if (ghiClear <= 10) continue; + ksActual += ghi; + ksClear += ghiClear; + ksCount++; + } + return ksCount >= 4 && ksClear > 0 ? ksActual / ksClear : null; + } + + /** Returns today's KNMI clearness index (partial day, updated each KNMI fetch). */ + getTodayKt() { return this._todayKnmiKt ?? null; } + + /** + * 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; + } + + /** + * 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; + if (this._saveCacheTimer) { + clearTimeout(this._saveCacheTimer); + this._saveCacheTimer = null; + } + this.homey.settings.unset('weather_forecast_cache'); + this.log('Weather cache invalidated'); + } + + _saveCache() { + // Debounce 5min — homey.settings.set allocates ~30 MB V8 heap per call + // (framework-internal, independent of payload). In-memory this.cache is + // already the primary source; persistence is only for restart recovery. + if (this._saveCacheTimer) clearTimeout(this._saveCacheTimer); + this._saveCacheTimer = setTimeout(() => { + this._saveCacheTimer = null; + 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); + } + }, 5 * 60 * 1000); + } + + // Like _loadCache but accepts stale data up to maxAgeMs old (for API-failure fallback) + _loadStaleCache(maxAgeMs) { + try { + const stored = this.homey.settings.get('weather_forecast_cache'); + if (!stored || !stored.expiry) return null; + if (stored.expiry <= Date.now() - maxAgeMs) return null; // too old + return this._reviveCache(stored.cache); + } catch (e) { + return null; + } + } + + _loadCache() { + try { + const stored = this.homey.settings.get('weather_forecast_cache'); + if (!stored || !stored.expiry || stored.expiry <= Date.now()) return null; + return { cache: this._reviveCache(stored.cache), expiry: stored.expiry }; + } catch (e) { + this.error('Failed to load weather cache from settings:', e.message); + return null; + } + } + + _reviveCache(c) { + 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 c; + } + + /** + * PV attenuation factor based on WMO weather codes. + * Returns 0–1 multiplier for radiation and sunshine. + * + * Open-Meteo GHI accounts for clouds but misses: + * - Ground-level fog (not a cloud layer) + * - Snow cover on panels (blocks PV even if sun shines) + * - Extreme precipitation scattering + * - Thunderstorm turbulence / hail + * + * WMO code groups: + * 45, 48 — Fog / rime fog → 0.12 + * 71, 73 — Light/moderate snowfall → 0.30 (partial panel coverage) + * 75, 77 — Heavy snow / snow grains → 0.05 (near-total panel blockage) + * 85, 86 — Snow showers (light/heavy) → 0.15 + * 65, 67 — Heavy rain / freezing rain → 0.50 (severe scattering) + * 95, 96, 99 — Thunderstorm (±hail) → 0.30 (extreme cloud + scattering) + * + * precipProb is used as a confidence gate: if probability < 40%, moderate the + * attenuation (blend towards 1.0) since the event may not materialise. + * + * @param {number} wmoCode - WMO weather code from Open-Meteo + * @param {number} precipProb - Precipitation probability 0–100 + * @returns {number} Attenuation factor 0–1 (1 = no attenuation) + * @static + */ + /** + * Fetch 2-hour precipitation forecast from Buienradar (5-min intervals). + * Returns [{time: Date, mmPerHour: number, factor: number}] or [] on error. + */ + async fetchBuienradar(lat, lon) { + if (this._buienradarCache && this._buienradarExpiry > Date.now()) { + return this._buienradarCache; + } + // Fetch 5 points (center + N/S/E/W ±0.05°) in parallel. + // Average factor per timestep — catches incoming rain cells better than a single point. + const D = 0.05; + const points = [ + [lat, lon ], + [lat + D, lon ], + [lat - D, lon ], + [lat, lon + D ], + [lat, lon - D ], + ]; + const fetchPoint = async ([la, lo]) => { + const url = `https://gpsgadget.buienradar.nl/data/raintext?lat=${la.toFixed(2)}&lon=${lo.toFixed(2)}`; + const response = await fetchWithTimeout(url, {}, 8000); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const text = await response.text(); + return text.trim().split('\n').map(line => { + const value = parseInt(line.split('|')[0], 10); + return isNaN(value) ? 0 : (value > 0 ? Math.pow(10, (value - 109) / 32) : 0); + }); + }; + try { + const now = Date.now(); + const results = await Promise.allSettled(points.map(p => fetchPoint(p))); + const valid = results.filter(r => r.status === 'fulfilled').map(r => r.value); + if (valid.length === 0) throw new Error('all points failed'); + const len = Math.min(...valid.map(v => v.length)); + const result = Array.from({ length: len }, (_, i) => { + const avgMmh = valid.reduce((s, v) => s + (v[i] ?? 0), 0) / valid.length; + return { time: new Date(now + i * 5 * 60000), mmPerHour: avgMmh, factor: WeatherForecaster._buienradarFactor(avgMmh) }; + }); + this._buienradarCache = result; + this._buienradarExpiry = now + 15 * 60000; + return result; + } catch (err) { + this.error('Buienradar fetch failed:', err.message); + return []; + } + } + + static _buienradarFactor(mmh) { + if (mmh < 0.1) return 1.00; + if (mmh < 0.5) return 0.90; + if (mmh < 2.0) return 0.75; + if (mmh < 5.0) return 0.55; + if (mmh < 10) return 0.35; + if (mmh < 20) return 0.20; + return 0.10; + } + + // Conservative additional factor applied to OM-based PV slots. + // OM irradiance already captures most cloud/rain attenuation; this corrects + // residual underestimation during active precipitation (optical depth effects). + static _omPrecipFactor(mmh) { + if (mmh < 0.3) return 1.00; + if (mmh < 1.0) return 0.95; + if (mmh < 5.0) return 0.90; + if (mmh < 10) return 0.82; + return 0.80; + } + + static _weatherAttenuation(wmoCode, precipProb = 100) { + let factor = 1.0; + switch (wmoCode) { + case 45: case 48: factor = 0.12; break; // fog / rime fog + case 75: case 77: factor = 0.05; break; // heavy snow / snow grains + case 71: case 73: factor = 0.30; break; // light/moderate snow + case 85: factor = 0.20; break; // light snow showers + case 86: factor = 0.10; break; // heavy snow showers + case 65: case 67: factor = 0.50; break; // heavy rain / freezing rain + case 95: case 96: case 99: factor = 0.30; break; // thunderstorm (±hail) + default: return 1.0; // no special attenuation + } + // Confidence gate: if precipitation probability is low, blend towards 1.0 + // (the severe weather may not materialise). Fog codes (45, 48) bypass this + // since precipProb doesn't apply to fog events. + if (wmoCode !== 45 && wmoCode !== 48 && precipProb < 40) { + const confidence = precipProb / 40; // 0 at 0%, 1 at 40%+ + factor = 1.0 - (1.0 - factor) * confidence; + } + return factor; + } + + /** + * Solar elevation and azimuth from UTC datetime, lat/lon. + * Azimuth convention: 0=south, negative=east, positive=west (matches Open-Meteo panel azimuth). + * Accuracy: ~0.1° — sufficient for GTI transposition. + */ + static _solarElevAz(utcDate, latDeg, lonDeg) { + const d2r = Math.PI / 180; + const n = utcDate.getTime() / 86400000 + 2440587.5 - 2451545.0; // days from J2000 + const L = ((280.460 + 0.9856474 * n) % 360 + 360) % 360; + const g = ((357.528 + 0.9856003 * n) % 360 + 360) % 360; + const gR = g * d2r; + const lam = L + 1.915 * Math.sin(gR) + 0.020 * Math.sin(2 * gR); + const eps = 23.439 - 4.0e-7 * n; + const lamR = lam * d2r, epsR = eps * d2r; + const decl = Math.asin(Math.sin(epsR) * Math.sin(lamR)); + const RA = Math.atan2(Math.cos(epsR) * Math.sin(lamR), Math.cos(lamR)) / d2r; + const EoT = (L - RA) / 15; // equation of time in hours + const UTh = utcDate.getUTCHours() + utcDate.getUTCMinutes() / 60 + utcDate.getUTCSeconds() / 3600; + const H = (UTh + lonDeg / 15 + EoT - 12) * 15; // solar hour angle (degrees) + const HR = H * d2r, latR = latDeg * d2r; + const sinElev = Math.sin(latR) * Math.sin(decl) + Math.cos(latR) * Math.cos(decl) * Math.cos(HR); + const elev = Math.asin(Math.max(-1, Math.min(1, sinElev))) / d2r; + if (elev <= 0) return { elev, azimuth: 0 }; + // atan2 gives correctly signed azimuth from south (positive=west, negative=east). + // acos+sign-flip fails: acos always returns 0–180° and the quadrant logic is wrong. + const az = Math.atan2( + Math.sin(HR), + Math.cos(HR) * Math.sin(latR) - Math.tan(decl) * Math.cos(latR) + ) / d2r; + return { elev, azimuth: az }; + } + + /** + * Isotropic sky transposition: GHI/DHI/DNI on a horizontal surface → GTI on tilted panel. + * @param {number} dhi - Diffuse horizontal irradiance (W/m²) + * @param {number} dni - Direct normal irradiance (W/m²) + * @param {number} ghi - Global horizontal irradiance (W/m²) + * @param {number} elevDeg - Solar elevation (degrees above horizon) + * @param {number} solarAzDeg - Solar azimuth (0=south, neg=east, pos=west) + * @param {number} tiltDeg - Panel tilt from horizontal (degrees) + * @param {number} panelAzDeg - Panel azimuth (0=south, neg=east, pos=west) + * @param {number} albedo - Ground reflectance (default 0.20) + */ + static _computeGTI(dhi, dni, ghi, elevDeg, solarAzDeg, tiltDeg, panelAzDeg, albedo = 0.20) { + if (elevDeg <= 0) return 0; + const d2r = Math.PI / 180; + const tiltR = tiltDeg * d2r; + const azDiff = (solarAzDeg - panelAzDeg) * d2r; + const cosTheta = Math.cos(elevDeg * d2r) * Math.sin(tiltR) * Math.cos(azDiff) + + Math.sin(elevDeg * d2r) * Math.cos(tiltR); + const direct = dni * Math.max(0, cosTheta); + const diffuse = dhi * (1 + Math.cos(tiltR)) / 2; + const reflected = ghi * albedo * (1 - Math.cos(tiltR)) / 2; + return Math.max(0, Math.round(direct + diffuse + reflected)); + } +} + +module.exports = WeatherForecaster; diff --git a/lib/xadi-provider.js b/lib/xadi-provider.js new file mode 100644 index 00000000..24e32645 --- /dev/null +++ b/lib/xadi-provider.js @@ -0,0 +1,428 @@ +'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, + minute: 0, + 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 { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrowStr = tomorrow.toISOString().split('T')[0]; + + const endpoints = [ + { label: '/today', url: `https://dap.xadi.eu/api/nl/today?${baseParams}` }, + { label: '/next24h', url: `https://dap.xadi.eu/api/nl/next24h?${baseParams}` }, + { label: `/day/${tomorrowStr}`, url: `https://dap.xadi.eu/api/nl/day/${tomorrowStr}?${baseParams}` }, + ]; + + // Parallel fetch — each endpoint independent, dedup via seenTimestamps below. + // Worst case ~10s total instead of 30s sequential. + const results = await Promise.allSettled( + endpoints.map(e => fetchWithTimeout(e.url, {}, 10000) + .then(async res => ({ endpoint: e, res, data: res.ok ? await res.json() : null }))) + ); + + for (let i = 0; i < results.length; i++) { + const r = results[i]; + const e = endpoints[i]; + if (r.status === 'rejected') { this.log(`Failed to fetch ${e.label}: ${r.reason.message}`); continue; } + const { res, data } = r.value; + if (!res.ok) { this.log(`${e.label} HTTP ${res.status}`); continue; } + if (!data || data.status !== 'success' || !data.data || data.data.length === 0) continue; + + 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 ${e.label} (${newCount} new)`); + } + + 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..01e435b6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,13 +1,124 @@ { - "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", + "tab_expansion": "Expansion", + "planning_title": "Battery Planning - 48 Hours", + "planning_subtitle": "Expected prices, battery modes and SoC projection for today and tomorrow", + "baseload_title": "Baseload chart", + "baseload_current": "Current baseload", + "baseload_samples_title": "Samples of selected night", + "baseload_show_samples": "Show samples", + "col_time": "Time", + "col_power": "Power (W)", + "col_failed": "Failed", + "col_total": "Total", + "col_avg_ms": "Avg. (ms)", + "col_last_error": "Last error", + "col_since": "Since", + "btn_clear_fetch": "Clear fetch logs", + "btn_refresh": "Refresh", + "btn_clear_journal": "Clear journal", + "learning_title": "Learning engine status", + "consumption_profile_title": "Consumption profile (learned)", + "day_mon": "Mon", "day_tue": "Tue", "day_wed": "Wed", "day_thu": "Thu", + "day_fri": "Fri", "day_sat": "Sat", "day_sun": "Sun", + "label_policy_mode_dynamic": "Dynamic Pricing", + "label_policy_mode": "Policy Mode", + "label_soc": "Current SoC", + "label_avg_price": "Avg. Price", + "label_max_charge": "Max Charge Price", + "label_min_discharge": "Min Discharge Price", + "label_resolution": "Resolution", + "today": "Today", + "tomorrow": "Tomorrow", + "weather_updated_at": "Weather updated at", + "pv_slots_learned": "PV slots learned", + "min_slots_note": "(min. 10 to learn)", + "no_tomorrow_prices": "Tomorrow's prices not yet available — planning based on today", + "cycle_plural": "🔄 {n}× cycle", + "cycle_single": "🔋 1× cycle", + "self_sufficient": "self-sufficient", + "net_suffix": "net" + }, + "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..1af4edac 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -1,13 +1,114 @@ { - "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", + "tab_expansion": "Uitbreiding", + "planning_title": "Batterij Planning - 48 Uur", + "planning_subtitle": "Verwachte prijzen, batterij modi en SoC projectie voor vandaag en morgen", + "baseload_title": "Sluipverbruik grafiek", + "baseload_current": "Huidig sluipverbruik", + "baseload_samples_title": "Samples van geselecteerde nacht", + "baseload_show_samples": "Toon samples", + "col_time": "Tijd", + "col_power": "Vermogen (W)", + "col_failed": "Mislukt", + "col_total": "Totaal", + "col_avg_ms": "Gem. (ms)", + "col_last_error": "Laatste fout", + "col_since": "Sinds", + "btn_clear_fetch": "Wis fetch-logs", + "btn_refresh": "Ververs", + "btn_clear_journal": "Wis journal", + "learning_title": "Leermotor status", + "consumption_profile_title": "Verbruiksprofiel (geleerd)", + "day_mon": "Ma", "day_tue": "Di", "day_wed": "Wo", "day_thu": "Do", + "day_fri": "Vr", "day_sat": "Za", "day_sun": "Zo", + "label_policy_mode_dynamic": "Dynamisch Tarief", + "label_policy_mode": "Policy Modus", + "label_soc": "Huidige SoC", + "label_avg_price": "Gem. Prijs", + "label_max_charge": "Max Laadprijs", + "label_min_discharge": "Min Ontlaadprijs", + "label_resolution": "Resolutie", + "today": "Vandaag", + "tomorrow": "Morgen", + "weather_updated_at": "Weerdata bijgewerkt om", + "pv_slots_learned": "PV-slots geleerd", + "min_slots_note": "(min. 10 voor leren)", + "no_tomorrow_prices": "Prijzen voor morgen nog niet beschikbaar — planning op basis van vandaag", + "cycle_plural": "🔄 {n}× cyclus", + "cycle_single": "🔋 1× cyclus", + "self_sufficient": "zelfvoorzienend", + "net_suffix": "netto" + }, + "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..4f8112d7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3929 @@ +{ + "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", + "fast-check": "^4.7.0" + } + }, + "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/@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/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/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==", + "dev": 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/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==", + "dev": 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==", + "dev": 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/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/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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": 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/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==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/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==", + "dev": 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-check": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", + "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "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.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": 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/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", + "dev": 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/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/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.", + "dev": 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==", + "dev": 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==", + "dev": 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/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/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": 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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": 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/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==", + "dev": 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==", + "dev": 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/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "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/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", + "dev": 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-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==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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-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==", + "dev": 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==", + "dev": 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/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/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/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==", + "dev": 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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..d9c6608b --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "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 && node test/weather-attenuation.test.js && node test/terminal-value.test.js && node test/optimizer-properties.test.js && node test/policy-planning.test.js && node test/chart-renderer.test.js", + "test-properties": "node test/optimizer-properties.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", + "fast-check": "^4.7.0" + } +} diff --git a/refactor-log.md b/refactor-log.md new file mode 100644 index 00000000..96b237ca --- /dev/null +++ b/refactor-log.md @@ -0,0 +1,2787 @@ +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +## ✗ 3:soc-trace-consistency +FAIL + +### Counterexample +```json +Error: Property failed after 15 tests +{ seed: 12345, path: "14:0:0:0:0:0:0:0:13:4:0:2:0:0:0:1:0:1:1:1:0:1:2:2:1:0:1:1:0:2:0:1:0:0:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":85.55},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]] +Shrunk 58 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 15 tests +{ seed: 12345, path: "14:0:0:0:0:0:0:0:13:4:0:2:0:0:0:1:0:1:1:1:0:1:2:2:1:0:1:1:0:2:0:1:0:0:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":85.55},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]] +Shrunk 58 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +## ✗ 6:rte-no-double-apply-soc +FAIL + +### Counterexample +```json +Error: Property failed after 65 tests +{ seed: 12345, path: "64:3:14:3:3:7:4:4:4:4:3:3:4:4:3:5:5:3:3:3:3:4:4:4:3:3:14:3:3:3:3:14:5:4:3:5:3:3:7:3:3:3:3:4:5:4:3:15:4:3:3:13:3:5:4:4:3:4:4:5:3:3:3:3:3:3:5:3:3:3:3:5:3:3:7:3:4:4:3:3:15:5:4:3:5:3:5:4:3:3:3:4:3:3:7:4:5:3:4:3:3:6:4:3:7:11:4:4:7:3:5:5:3:3:3:3:3:9:3:5:3:3:3:3:5:13:3:3:5:10:4:3:3:4:3:4:5:4:5:5:3:4:4:3:3:3:3:4:5:3:3:3:4:4:3:3:4:3:14:5:11:5:11:5:11:5:3:4:3:3:3:14:5:10:3:3:4:4:6:4:3:3:3:3:5:6:3:4:4:4:3:4:3:14:5:9:3:6:3:5:6:7:5:3:3:4:3:3:11:3:3:3:3:3:3:13:3:4:4:6:5:3:3:3:4:3:6:3:5:5:3:3:3:3:5:3:3:3:10:3:3:3:3:3:13:5:4:3:3:3:5:3:3:4:3:4:4:3:3:3:3:3:13:6:3:4:3:3:3:14:5:4:3:3:3:3:5:4:4:4:6:4:3:7:4:3:5:8:3:3:3:3:3:3:3:14:5:4:3:3:3:3:3:7:5:5:3:5:6:7:3:5:3:5:3:3:3:3:15:4:4:6:4:3:3:4:3:3:6:3:3:6:4:4:3:3:6:3:5:13:7:3:3:13:4:3:3:5:3:3:4:5:3:3:3:3:3:4:5:6:4:3:4:4:5:3:3:3:3:3:4:3:14:5:3:8:3:3:4:3:4:3:3:4:3:3:5:3:3:4:4:5:5:3:3:4:3:3:3:14:5:3:7:4:5:7:3:3:6:7:3:5:3:3:3:5:3:4:3:3:14:5:3:7:3:4:6:3:5:10:5:3:7:3:3:4:13:3:3:13:5:3:7:3:3:3:3:3:3:4:3:3:3:4:4:5:8:3:3:4:4:6:3:3:3:3:3:14:5:7:7:3:4:5:6:4:7:3:3:3:3:4:5:3:3:4:4:15:3:3:13:5:3:3:5:3:4:4:3:5:6:3:3:3:3:3:3:3:3:3:4:3:3:4:3:3:3:4:13:3:3:3:13:5:3:6:3:3:3:6:4:4:3:3:5:3:3:5:3:4:5:3:3:6:3:3:4:3:17:4:4:3:3:5:4:3:3:3:3:3:6:3:7:3:4:5:5:5:4:4:15:3:3:13:3:4:3:4:4:3:5:3:4:4:4:4:3:3:11:5:4:4:3:3:13:3:3:3:14:3:5:3:4:5:3:3:3:3:3:3:6:4:4:3:3:9:5:4:3:3:4:4:5:3:3:3:13:3:5:3:6:4:6:9:7:3:3:3:5:4:4:3:3:5:3:4:3:14:5:3:5:3:3:13:5:3:5:3:3:12:14:3:3:3:3:15:4:5:7:3:3:3:3:8:3:4:3:7:3:6:3:3:5:3:3:4:7:3:13:4:3:3:3:4:3:3:7:5:3:4:6:4:3:4:3:3:3:3:4:4:5:3:3:4:3:3:14:5:3:4:7:3:3:5:5:3:7:5:3:3:5:7:3:4:4:3:3:15:3:3:8:3:3:3:3:3:3:6:5:4:4:4:6:3:3:3:3:4:3:3:13:5:3:3:3:13:4:4:3:3:3:5:7:4:3:4:8:4:6:3:3:3:3:3:4:3:4:3:4:3:14:5:3:4:5:3:3:3:5:3:5:4:3:4:4:3:4:7:10:3:3:3:3:3:3:3:3:3:14:6:3:3:3:9:3:3:3:5:3:5:13:3:4:3:3:3:3:4:14:5:3:13:4:9:3:4:3:4:3:3:4:3:3:4:4:3:3:4:4:3:3:3:3:4:3:7:3:3:3:3:3:3:14:5:3:4:4:3:4:4:3:3:8:4:3:5:3:7:7:5:13:3:3:14:4:4:3:4:3:4:4:3:4:4:3:3:4:3:4:3:3:7:4:6:3:3:4:4:6:3:3:13:5:6:3:4:5:3:3:3:4:5:4:4:5:5:3:3:3:3:3:4:3:5:4:3:3:14:5:3:4:3:5:3:4:5:3:3:5:4:3:3:6:3:3:4:4:6:4:4:3:3:14:5:3:4:3:4:5:3:6:3:4:3:3:3:3:5:3:3:3:4:3:3:3:7:3:13:3:3:16:3:6:5:3:4:3:3:4:4:3:3:7:3:4:5:3:4:3:3:4:5:6:3:3:3:3:13:3:4:7:7:6:4:3:7:3:4:5:3:3:6:4:4:3:3:4:3:14:5:3:4:3:3:4:4:4:4:3:4:3:3:3:4:6:3:3:5:5:4:4:3:3:4:3:15:5:3:3:3:3:3:4:6:3:3:3:4:3:3:4:5:4:3:4:6:3:4:3:13:5:3:3:3:13:3:8:3:3:5:5:3:3:3:3:4:3:6:6:7:4:3:3:4:3:3:4:3:14:5:3:3:9:5:3:3:9:5:3:3:9:4:5:3:4:3:15:3:4:3:3:4:5:6:3:3:4:4:4:4:4:3:3:3:3:3:3:6:3:4:3:3:4:14:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:21:6:7:7:7:6:5:10:5:5:6:6:6:6:9:5:5:7:7:5:5:5:6:5:5:5:18:5:7:5:6:6:6:5:5:7:8:5:6:5:7:6:5:7:7:5:5:5:5:6:5:7:7", endOnFailure: true } +Counterexample: [[{"min_soc":0,"max_soc":91,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":2.846975088967972,"maxChargeW":1676,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.5899721161101736,0,0,0,0,-0.24999999999999759]]] +Shrunk 1245 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 65 tests +{ seed: 12345, path: "64:3:14:3:3:7:4:4:4:4:3:3:4:4:3:5:5:3:3:3:3:4:4:4:3:3:14:3:3:3:3:14:5:4:3:5:3:3:7:3:3:3:3:4:5:4:3:15:4:3:3:13:3:5:4:4:3:4:4:5:3:3:3:3:3:3:5:3:3:3:3:5:3:3:7:3:4:4:3:3:15:5:4:3:5:3:5:4:3:3:3:4:3:3:7:4:5:3:4:3:3:6:4:3:7:11:4:4:7:3:5:5:3:3:3:3:3:9:3:5:3:3:3:3:5:13:3:3:5:10:4:3:3:4:3:4:5:4:5:5:3:4:4:3:3:3:3:4:5:3:3:3:4:4:3:3:4:3:14:5:11:5:11:5:11:5:3:4:3:3:3:14:5:10:3:3:4:4:6:4:3:3:3:3:5:6:3:4:4:4:3:4:3:14:5:9:3:6:3:5:6:7:5:3:3:4:3:3:11:3:3:3:3:3:3:13:3:4:4:6:5:3:3:3:4:3:6:3:5:5:3:3:3:3:5:3:3:3:10:3:3:3:3:3:13:5:4:3:3:3:5:3:3:4:3:4:4:3:3:3:3:3:13:6:3:4:3:3:3:14:5:4:3:3:3:3:5:4:4:4:6:4:3:7:4:3:5:8:3:3:3:3:3:3:3:14:5:4:3:3:3:3:3:7:5:5:3:5:6:7:3:5:3:5:3:3:3:3:15:4:4:6:4:3:3:4:3:3:6:3:3:6:4:4:3:3:6:3:5:13:7:3:3:13:4:3:3:5:3:3:4:5:3:3:3:3:3:4:5:6:4:3:4:4:5:3:3:3:3:3:4:3:14:5:3:8:3:3:4:3:4:3:3:4:3:3:5:3:3:4:4:5:5:3:3:4:3:3:3:14:5:3:7:4:5:7:3:3:6:7:3:5:3:3:3:5:3:4:3:3:14:5:3:7:3:4:6:3:5:10:5:3:7:3:3:4:13:3:3:13:5:3:7:3:3:3:3:3:3:4:3:3:3:4:4:5:8:3:3:4:4:6:3:3:3:3:3:14:5:7:7:3:4:5:6:4:7:3:3:3:3:4:5:3:3:4:4:15:3:3:13:5:3:3:5:3:4:4:3:5:6:3:3:3:3:3:3:3:3:3:4:3:3:4:3:3:3:4:13:3:3:3:13:5:3:6:3:3:3:6:4:4:3:3:5:3:3:5:3:4:5:3:3:6:3:3:4:3:17:4:4:3:3:5:4:3:3:3:3:3:6:3:7:3:4:5:5:5:4:4:15:3:3:13:3:4:3:4:4:3:5:3:4:4:4:4:3:3:11:5:4:4:3:3:13:3:3:3:14:3:5:3:4:5:3:3:3:3:3:3:6:4:4:3:3:9:5:4:3:3:4:4:5:3:3:3:13:3:5:3:6:4:6:9:7:3:3:3:5:4:4:3:3:5:3:4:3:14:5:3:5:3:3:13:5:3:5:3:3:12:14:3:3:3:3:15:4:5:7:3:3:3:3:8:3:4:3:7:3:6:3:3:5:3:3:4:7:3:13:4:3:3:3:4:3:3:7:5:3:4:6:4:3:4:3:3:3:3:4:4:5:3:3:4:3:3:14:5:3:4:7:3:3:5:5:3:7:5:3:3:5:7:3:4:4:3:3:15:3:3:8:3:3:3:3:3:3:6:5:4:4:4:6:3:3:3:3:4:3:3:13:5:3:3:3:13:4:4:3:3:3:5:7:4:3:4:8:4:6:3:3:3:3:3:4:3:4:3:4:3:14:5:3:4:5:3:3:3:5:3:5:4:3:4:4:3:4:7:10:3:3:3:3:3:3:3:3:3:14:6:3:3:3:9:3:3:3:5:3:5:13:3:4:3:3:3:3:4:14:5:3:13:4:9:3:4:3:4:3:3:4:3:3:4:4:3:3:4:4:3:3:3:3:4:3:7:3:3:3:3:3:3:14:5:3:4:4:3:4:4:3:3:8:4:3:5:3:7:7:5:13:3:3:14:4:4:3:4:3:4:4:3:4:4:3:3:4:3:4:3:3:7:4:6:3:3:4:4:6:3:3:13:5:6:3:4:5:3:3:3:4:5:4:4:5:5:3:3:3:3:3:4:3:5:4:3:3:14:5:3:4:3:5:3:4:5:3:3:5:4:3:3:6:3:3:4:4:6:4:4:3:3:14:5:3:4:3:4:5:3:6:3:4:3:3:3:3:5:3:3:3:4:3:3:3:7:3:13:3:3:16:3:6:5:3:4:3:3:4:4:3:3:7:3:4:5:3:4:3:3:4:5:6:3:3:3:3:13:3:4:7:7:6:4:3:7:3:4:5:3:3:6:4:4:3:3:4:3:14:5:3:4:3:3:4:4:4:4:3:4:3:3:3:4:6:3:3:5:5:4:4:3:3:4:3:15:5:3:3:3:3:3:4:6:3:3:3:4:3:3:4:5:4:3:4:6:3:4:3:13:5:3:3:3:13:3:8:3:3:5:5:3:3:3:3:4:3:6:6:7:4:3:3:4:3:3:4:3:14:5:3:3:9:5:3:3:9:5:3:3:9:4:5:3:4:3:15:3:4:3:3:4:5:6:3:3:4:4:4:4:4:3:3:3:3:3:3:6:3:4:3:3:4:14:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:5:21:6:7:7:7:6:5:10:5:5:6:6:6:6:9:5:5:7:7:5:5:5:6:5:5:5:18:5:7:5:6:6:6:5:5:7:8:5:6:5:7:6:5:7:7:5:5:5:5:6:5:7:7", endOnFailure: true } +Counterexample: [[{"min_soc":0,"max_soc":91,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":2.846975088967972,"maxChargeW":1676,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.5899721161101736,0,0,0,0,-0.24999999999999759]]] +Shrunk 1245 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + + +--- +## Summary +- Passed: 4 +- Failed: 2 + +### Failed invariants +- 3:soc-trace-consistency +- 6:rte-no-double-apply-soc + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 6 +- Failed: 0 + + +--- +## Fix Iteration 1 — All invariants green + +### Invariant 3 — root cause +Test bug: `baseArb` generated `currentSoc` up to 94%, but `max_soc` can be as low as 85%. +When `currentSoc > max_soc`, `OptimizationEngine.compute()` clamps `initialSocG` to `GRID_TOTAL` +(1000) not `maxSocG` (850), so `socProjected[0]` legitimately reads 85.6% when `max_soc=85`. +The bounds check `socProjected > maxSoc + EPS` fired falsely. + +**Fix:** `.map()` transform clamps `currentSoc = Math.min(currentSoc, max_soc)` before passing +to the engine. No engine code changed. + +### Invariant 6 — root cause +Test design flaw: compared **absolute** `socProjected[i]` between two engines with different RTE. +Different RTE values produce different DP values → different actions at some earlier slot → +trajectories legitimately diverge → absolute SoC at slot i differs even when action[i] matches. + +**Fix:** compare **SoC delta** (`socProjected[i+1] − socProjected[i]`) only while the two +engines are synchronized (identical actions from slot 0). Break as soon as actions diverge. +This correctly tests the real invariant: "given same starting SoC and same action, the SoC +change is independent of RTE" — because `chargeSocDeltaG` and `perSlotDischargeSocDeltaG` +are computed from power/capacity/time only, with no RTE term. + +No engine code needed changing — RTE was never applied to SoC delta in the code. + +### Engine code findings (no bugs found, but documented) +1. **Invariant 1 (no-grid-charge-full-pv):** PASSES — but analysis reveals an asymmetry: + `vCharge` at `pvCoverage=1.0` does not subtract the foregone PV export opportunity cost, + while `vPreserve` does. At `cycleCost=0` this makes `vCharge > vPreserve`, causing the DP + to prefer 'charge' over 'preserve' when PV fully covers it. The invariant passes because + we tested it with `cycleCost` in `[0, 0.10]` — at nonzero cycle cost the vCharge penalty + tips the balance back. With `cycleCost=0` specifically this would fail. + **Recommendation:** consider subtracting `pvCoverage × price × exportRatio × kwh` from + `vCharge` to make the two actions economically symmetric at high PV coverage. + +2. **Invariant 2 (fill-to-max-negative):** PASSES — DP correctly fills to maxSoc when + enough negative-price slots exist and cycle_cost=0. + +3. **Invariant 4 (cycle-counter-monotonic):** PASSES — the simulation correctly models + monotonic accumulation across restarts. + +4. **Invariant 5 (policy-mode-history-per-slot):** PASSES — dedup-by-bucket logic correct. + +--- +## Final result: 6/6 invariants pass — 1000 scenarios each, seed=12345 +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 6 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 6 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 7 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 366 tests +{ seed: 12345, path: "365:0:1:12:1:1:1:1:3:4:2:4:2:1:2:1:2:3:1:1:4:2:1:1:2:1:1:12:1:12:7:1:5:3:4:1:2:1:2:2:4:1:1:1:1:2:1:1:2:1:1:1:12:7:1:3:2:3:1:1:1:4:2:1:1:3:2:5:1:2:1:1:2:1:12:7:1:2:3:2:2:2:1:3:2:1:4:2:7:13:1:1:1:1:12:7:1:2:1:2:2:1:1:4:2:2:1:1:1:2:3:1:1:1:1:2:1:1:2:1:1:12:7:1:1:4:1:4:1:2:2:1:2:1:1:1:6:3:1:3:1:1:1:1:12:7:1:1:2:2:1:2:2:2:2:4:2:4:1:2:2:3:12:1:1:12:7:1:1:1:3:1:1:3:4:2:1:5:3:2:1:3:1:1:2:1:1:12:7:1:1:1:1:2:1:1:1:3:1:1:1:1:1:4:2:4:1:1:5:2:1:12:6:6:6:6:6:6:3:13:1:1:1:1:12:6:5:1:4:2:3:2:1:2:2:2:1:2:1:4:1:1:2:1:1:1:12:6:4:2:3:3:2:1:1:1:6:1:1:1:1:1:5:1:1:1:2:1:12:6:4:1:1:3:1:2:1:1:1:3:3:1:3:4:1:1:1:2:1:12:1:1:12:6:3:3:2:3:3:1:1:3:1:6:2:1:2:5:1:1:1:1:1:1:12:6:3:2:1:2:2:1:1:3:4:1:8:1:1:2:2:1:2:1:1:12:6:3:1:2:2:1:2:1:1:1:1:1:2:3:1:3:2:2:1:3:1:12:1:1:1:12:6:3:1:1:1:1:6:1:1:2:5:1:2:1:2:4:2:1:2:1:12:6:2:4:1:3:2:1:1:1:1:1:1:2:1:1:1:1:2:2:1:1:2:2:1:2:1:1:12:6:2:3:1:1:2:2:1:3:1:2:2:2:1:1:4:1:1:1:2:1:1:1:12:1:12:6:2:2:2:1:1:5:4:1:1:2:1:1:2:1:1:3:1:2:2:2:1:12:6:2:2:1:1:1:1:2:1:6:2:3:1:1:1:5:3:1:1:2:1:1:12:6:2:1:3:1:1:1:2:1:1:2:2:7:1:1:3:2:4:12:1:12:6:2:1:1:8:2:1:3:1:1:1:1:1:2:4:6:2:1:12:6:2:1:1:1:5:1:1:2:3:3:1:1:1:2:3:7:2:1:12:6:1:8:1:2:2:3:1:1:4:2:2:1:2:5:12:1:12:6:1:4:3:3:1:2:1:1:2:3:2:1:1:1:4:1:1:4:1:1:1:1:1:12:6:1:3:4:1:3:1:2:1:1:1:2:1:2:2:3:1:2:2:2:2:1:12:6:1:3:1:2:8:2:3:10:3:13:1:1:1:1:12:6:1:2:4:3:1:2:2:1:2:1:2:1:3:1:2:2:2:1:1:1:1:2:1:12:6:1:2:2:2:2:1:2:2:1:3:1:1:4:1:1:1:3:3:1:2:1:1:1:12:6:1:2:1:3:1:2:5:1:1:2:11:4:1:12:1:12:6:1:2:1:1:2:1:1:2:1:3:2:1:1:1:1:8:2:2:1:1:2:1:1:12:6:1:1:4:5:3:4:1:1:1:2:2:1:3:1:3:1:2:1:1:1:12:6:1:1:3:1:3:3:1:6:2:3:4:1:1:1:1:1:1:1:1:12:1:12:6:1:1:2:2:2:5:1:1:8:1:1:1:2:1:2:1:1:1:1:2:1:1:12:6:1:1:2:1:1:2:1:5:1:6:2:1:1:1:2:2:1:4:2:1:12:6:1:1:1:3:1:11:6:1:1:1:3:1:3:1:12:1:12:6:1:1:1:2:1:1:3:1:1:1:1:3:2:3:1:2:1:1:4:1:1:1:1:1:2:1:12:6:1:1:1:1:2:1:2:1:1:1:1:2:1:3:1:1:2:1:1:2:3:1:1:2:1:2:1:1:12:6:1:1:1:1:1:1:1:1:2:1:1:1:1:10:5:6:12:1:12:5:7:1:1:1:1:1:1:1:1:1:2:1:1:1:2:1:1:1:1:1:8:2:1:12:5:5:5:5:5:5:5:4:1:2:1:1:1:12:5:5:1:3:5:2:1:2:1:1:1:1:2:2:2:1:2:1:3:12:1:12:5:4:5:1:3:2:2:5:1:2:1:2:1:4:1:3:1:1:1:1:12:5:4:2:2:4:6:1:2:3:2:1:1:3:1:2:2:2:1:12:5:4:1:3:2:2:2:1:1:2:1:3:1:2:1:1:2:2:2:1:1:1:1:12:1:12:5:4:1:1:2:1:3:1:1:1:1:2:1:1:1:1:1:1:1:2:2:1:2:2:2:2:1:12:5:3:5:1:1:1:2:4:2:1:2:5:2:4:3:2:1:12:5:3:3:1:4:1:3:1:6:3:1:3:2:2:1:1:1:12:1:1:12:5:3:2:2:3:1:1:1:1:1:3:1:1:3:8:4:1:2:1:12:5:3:2:1:1:2:1:3:2:2:1:1:1:1:1:5:1:1:1:2:1:1:1:1:2:1:1:12:5:3:1:3:1:4:1:4:2:4:1:1:1:1:2:1:4:1:1:1:12:1:12:5:3:1:2:1:1:2:1:3:1:1:2:1:2:3:2:1:1:1:1:2:2:3:1:1:1:1:12:5:3:1:1:2:1:1:2:1:2:1:1:2:3:1:2:2:2:1:1:1:1:5:1:1:1:1:1:1:12:5:2:11:5:2:12:2:2:12:1:12:5:2:4:4:2:1:2:3:1:1:4:1:1:2:2:1:4:2:1:1:12:5:2:3:4:2:2:1:2:1:2:1:3:1:2:3:2:2:2:1:2:1:12:5:2:3:1:2:7:5:2:3:1:2:6:1:12:1:1:1:12:5:2:2:4:2:2:2:1:1:1:3:2:1:2:1:2:2:1:1:2:1:1:1:2:1:12:5:2:2:2:2:1:2:2:1:1:4:1:1:3:1:1:2:5:1:2:2:1:12:5:2:2:1:2:7:2:1:1:1:1:1:1:1:4:2:1:2:2:2:12:1:12:5:2:2:1:1:1:3:2:1:1:1:2:1:1:3:4:2:1:1:1:6:1:1:1:1:1:1:1:12:5:2:1:4:2:2:1:1:4:1:1:2:1:3:1:2:3:2:2:1:2:1:12:5:2:1:3:1:1:4:2:6:1:10:3:13:1:1:1:1:12:5:2:1:2:2:1:2:1:1:6:1:3:1:3:2:1:3:5:1:1:1:1:1:1:12:5:2:1:2:1:1:1:1:1:3:1:2:1:1:2:1:5:3:2:2:4:1:1:1:1:1:12:5:2:1:1:2:5:2:1:4:1:2:1:1:3:3:2:2:1:1:12:1:1:1:12:5:2:1:1:1:4:4:1:5:2:4:1:3:2:1:4:2:1:12:5:2:1:1:1:1:3:1:1:2:1:2:5:6:1:1:1:2:1:1:1:1:1:2:1:12:5:1:7:2:3:1:1:1:5:1:2:4:2:1:2:1:1:12:1:1:1:12:5:1:5:2:1:2:1:3:4:1:2:1:1:1:1:3:4:2:2:1:1:12:5:1:4:2:5:2:1:2:4:4:1:4:1:1:2:2:2:1:12:5:1:4:1:1:3:1:1:1:1:11:5:1:3:1:1:1:12:1:12:5:1:3:3:2:1:1:4:2:3:1:1:1:1:2:1:1:1:3:1:1:1:1:2:1:1:12:5:1:3:2:1:1:2:3:2:3:1:3:1:5:2:1:6:1:1:1:1:1:1:1:12:5:1:3:1:2:1:1:2:1:3:1:2:2:2:3:1:3:1:2:2:13:1:1:1:1:12:5:1:2:9:1:1:2:2:1:2:1:6:1:3:1:1:3:1:1:1:1:12:5:1:2:3:3:4:4:2:2:1:4:1:2:2:1:3:1:2:1:12:5:1:2:2:4:1:1:1:2:2:4:2:1:4:1:1:4:1:1:1:12:1:1:12:5:1:2:2:1:2:2:1:4:2:2:2:3:1:1:1:1:2:1:1:2:2:2:1:12:5:1:2:1:4:1:1:2:4:2:1:2:7:1:1:3:2:2:1:1:12:5:1:2:1:2:1:3:6:2:2:4:4:1:1:1:2:2:1:12:1:12:5:1:2:1:1:2:2:2:1:3:3:1:3:2:2:1:1:3:1:1:1:2:2:1:12:5:1:2:1:1:1:1:1:3:3:3:2:3:2:1:3:3:6:1:1:1:1:1:1:1:12:5:1:1:5:1:1:3:1:7:4:1:1:1:2:1:1:3:13:1:1:1:1:12:5:1:1:3:8:1:3:2:2:2:1:6:3:1:3:1:1:1:1:12:5:1:1:3:1:3:6:3:1:1:4:2:4:1:1:2:1:1:2:1:1:12:5:1:1:2:5:1:1:1:1:3:2:1:3:2:3:2:1:1:1:2:1:1:1:12:1:12:5:1:1:2:2:2:2:1:1:4:1:5:1:1:3:1:2:5:1:2:1:12:5:1:1:2:1:3:1:1:1:2:2:2:1:2:2:1:1:1:1:3:1:2:2:1:1:1:2:1:12:5:1:1:2:1:1:1:3:1:3:2:2:2:2:1:1:2:1:1:3:1:2:1:1:11:1:1:11:5:1:1:1:4:2:1:1:5:6:1:2:3:1:1:1:3:2:1:2:1:12:5:1:1:1:2:2:1:1:1:2:1:2:1:1:1:1:1:2:2:2:1:3:1:3:1:1:1:2:1:12:5:1:1:1:1:6:2:1:2:1:3:1:2:2:1:1:1:1:2:2:2:1:2:1:1:1:12:5:1:1:1:1:2:3:1:2:1:2:1:1:2:3:1:1:1:1:1:2:1:2:14:1:1:1:1:1:1:11:5:1:1:1:1:1:3:2:2:1:1:2:2:3:2:1:1:1:4:1:2:1:2:1:2:1:12:4:8:3:1:3:5:2:2:1:5:2:1:2:3:1:1:1:1:12:4:7:1:2:1:1:2:3:2:1:3:1:1:2:1:2:4:1:1:1:1:2:1:12:4:6:2:1:2:1:2:1:1:1:1:1:2:2:1:1:1:3:3:2:1:12:1:1:1:1:11:4:6:1:1:1:1:1:1:1:2:1:1:2:1:1:1:1:9:4:3:1:1:1:1:12:4:5:1:3:5:2:3:1:4:1:1:2:5:2:3:1:1:1:1:12:4:5:1:1:2:1:3:2:2:1:2:3:1:1:1:1:4:1:1:1:4:1:1:1:1:1:12:4:4:4:4:4:4:4:4:4:15:1:1:1:1:1:1:1:11:4:4:3:1:2:2:2:1:1:1:1:1:2:3:1:3:2:1:4:1:3:1:1:1:1:12:4:4:2:1:1:1:1:1:3:1:1:2:1:2:1:2:1:2:4:1:2:2:1:2:1:12:4:4:1:2:4:1:8:1:3:1:2:3:1:1:2:1:1:2:1:1:1:12:4:4:1:1:4:1:1:1:3:2:1:5:1:1:1:1:1:1:2:3:2:11:1:11:4:4:1:1:1:2:1:5:1:2:4:1:2:1:1:4:1:2:1:1:1:2:1:1:1:12:4:3:4:1:2:2:1:4:1:1:3:1:1:2:2:1:1:2:2:4:1:1:1:1:1:12:4:3:3:2:1:2:3:3:2:4:1:2:1:2:1:1:2:2:1:2:1:1:1:12:4:3:3:1:1:1:1:1:1:5:1:1:1:1:1:2:5:1:2:3:1:1:11:1:1:11:4:3:2:2:4:1:1:1:2:1:1:1:1:2:4:1:1:2:1:1:1:2:1:3:1:1:1:1:12:4:3:2:1:1:2:1:2:1:3:1:2:1:1:1:2:1:2:2:1:1:1:3:1:2:1:1:1:12:4:3:1:4:3:2:4:4:1:1:1:1:1:1:1:1:2:1:1:2:1:1:1:2:1:12:4:3:1:3:1:2:1:1:1:11:4:3:1:2:2:1:11:1:11:4:3:1:2:2:1:1:3:7:4:3:1:2:2:1:1:2:1:1:2:1:1:12:4:3:1:1:2:3:1:2:2:6:1:1:1:1:3:1:1:2:5:2:1:12:4:3:1:1:1:3:2:1:1:2:1:1:1:1:1:1:1:1:3:1:4:3:1:1:1:1:2:1:12:4:3:1:1:1:1:1:5:1:1:2:4:1:2:3:1:2:4:1:1:11:1:1:1:11:4:2:6:2:1:5:1:2:1:1:1:4:2:3:1:1:2:4:1:1:1:1:1:12:4:2:4:1:6:3:1:1:1:1:1:1:4:1:1:1:1:2:1:2:2:2:1:12:4:2:3:5:2:1:1:1:1:5:1:1:6:2:2:1:1:2:2:1:12:4:2:3:2:2:2:1:2:3:1:7:1:1:2:3:1:3:1:11:1:11:4:2:3:1:2:5:2:1:4:2:2:2:2:1:2:1:3:2:1:2:1:12:4:2:2:4:1:2:1:1:3:1:2:4:1:1:1:1:1:1:1:2:1:1:1:1:1:1:2:1:12:4:2:2:2:6:4:1:1:2:1:1:1:2:2:1:3:2:4:2:1:12:4:2:2:2:1:3:1:2:2:2:2:1:1:1:10:2:13:1:1:1:1:1:11:4:2:2:1:4:1:4:5:1:1:1:5:2:2:1:1:1:1:4:1:1:1:1:1:12:4:2:2:1:1:2:2:1:1:1:3:3:3:1:5:1:1:1:1:4:1:2:1:12:4:2:2:1:1:1:1:1:1:2:2:3:3:6:1:2:1:3:2:1:1:1:2:1:12:4:2:1:4:4:1:5:3:1:1:1:2:2:8:2:11:1:11:4:2:1:3:3:4:1:2:2:2:5:1:1:1:1:1:4:5:1:1:1:1:1:1:12:4:2:1:2:3:2:3:4:2:4:1:3:1:1:2:2:2:1:2:1:1:1:12:4:2:1:2:2:1:1:2:3:1:1:2:5:1:1:2:3:6:2:1:1:12:4:2:1:2:1:1:7:1:2:4:3:2:5:2:3:11:1:1:11:4:2:1:1:6:1:3:1:3:1:1:2:4:2:1:3:1:1:1:1:1:1:1:2:1:12:4:2:1:1:2:2:3:2:6:1:1:1:9:2:1:2:2:1:1:12:4:2:1:1:2:1:1:1:4:2:5:1:3:4:1:2:1:1:1:1:1:2:1:1:1:12:4:2:1:1:1:3:1:1:1:2:1:3:3:1:1:3:2:1:1:1:2:1:3:11:1:1:11:4:2:1:1:1:1:4:3:1:1:1:1:1:4:1:1:3:2:1:7:1:2:1:12:4:1:7:4:1:7:4:1:7:3:1:1:2:1:12:4:1:6:1:2:1:1:1:1:2:3:1:4:1:1:1:1:4:4:2:1:1:12:4:1:5:2:1:1:1:4:2:1:1:1:1:2:3:9:2:11:1:11:4:1:4:5:3:3:2:1:2:2:2:2:1:3:1:1:5:2:1:12:4:1:4:1:2:4:1:5:4:2:4:1:2:1:1:2:2:2:1:12:4:1:4:1:1:1:1:8:2:2:1:1:1:1:3:1:2:1:3:1:2:1:1:12:4:1:3:4:1:1:1:2:3:1:1:5:1:2:2:2:5:2:11:1:11:4:1:3:2:4:2:3:1:1:8:4:1:3:2:1:2:2:1:12:4:1:3:1:3:3:2:4:1:1:1:1:4:3:3:1:1:3:1:2:1:12:4:1:3:1:2:1:1:3:2:3:2:1:3:1:1:1:1:2:1:2:1:1:1:1:1:1:2:1:12:4:1:3:1:1:1:9:4:1:3:1:1:1:8:2:11:1:11:4:1:2:7:1:2:1:1:4:2:2:2:2:1:5:3:1:1:2:1:12:4:1:2:3:2:2:3:2:3:3:1:1:7:1:1:6:1:1:1:1:1:1:1:12:4:1:2:3:1:1:1:2:1:1:3:2:1:5:4:1:3:1:2:1:1:1:2:1:12:4:1:2:2:2:4:2:3:3:8:3:1:1:1:1:1:12:1:1:1:1:11:4:1:2:2:1:3:3:1:3:1:4:2:1:2:1:1:2:2:1:1:1:2:1:2:1:12:4:1:2:1:4:2:1:1:1:1:4:1:3:1:1:1:1:1:1:3:2:1:1:2:2:1:12:4:1:2:1:3:1:1:1:1:2:1:1:2:1:2:1:3:1:2:1:5:1:4:1:1:1:1:1:12:4:1:2:1:2:1:3:1:5:1:1:1:1:1:4:1:3:1:2:1:1:3:11:1:11:4:1:2:1:1:4:1:2:2:2:1:1:4:2:1:3:3:2:2:1:2:2:1:12:4:1:2:1:1:1:2:1:2:1:1:1:1:1:2:1:2:3:3:1:3:1:3:1:1:2:1:12:4:1:1:9:1:2:2:2:7:3:1:1:1:1:1:1:1:1:2:1:1:1:12:4:1:1:5:2:2:2:2:1:1:1:4:1:2:1:2:2:2:1:14:1:1:1:1:1:1:11:4:1:1:4:2:3:4:2:1:1:1:1:1:6:1:1:3:2:1:1:2:1:1:1:12:4:1:1:3:2:5:3:1:2:1:1:2:2:6:2:2:2:1:2:1:12:4:1:1:3:1:3:4:1:3:2:1:1:3:3:1:1:4:1:4:1:1:1:1:1:12:4:1:1:3:1:1:1:6:1:2:2:2:1:2:1:2:4:3:1:2:11:1:11:4:1:1:2:4:1:7:2:2:3:2:4:1:3:2:1:3:1:1:1:1:12:4:1:1:2:2:1:3:1:1:1:4:2:2:1:1:1:5:5:1:1:1:1:2:1:12:4:1:1:2:1:4:1:1:1:2:2:2:1:3:1:1:3:2:1:1:3:1:1:2:1:1:1:12:4:1:1:2:1:2:1:2:1:1:3:2:1:2:5:1:1:1:1:1:4:2:11:1:1:11:4:1:1:2:1:1:2:1:1:1:1:5:1:1:2:1:3:2:1:1:1:3:1:4:2:1:12:4:1:1:1:4:2:1:1:2:1:1:1:3:1:1:2:1:3:1:1:1:1:1:1:1:1:3:1:1:1:1:12:4:1:1:1:3:2:2:1:2:1:1:1:2:1:2:1:1:2:3:2:1:1:1:1:2:2:1:1:12:4:1:1:1:3:1:1:1:1:1:1:6:2:2:2:5:1:2:1:1:3:11:1:11:4:1:1:1:2:2:3:1:1:4:1:1:1:2:1:1:2:1:1:2:1:3:2:4:1:1:1:1:1:12:4:1:1:1:2:1:1:1:2:1:1:2:1:1:3:1:1:1:1:5:3:2:2:3:1:1:1:12:4:1:1:1:1:4:1:1:1:1:1:2:2:1:1:1:3:1:1:1:1:1:4:2:1:1:1:2:1:1:12:4:1:1:1:1:2:4:1:1:1:1:1:1:2:2:1:1:1:3:1:3:1:1:1:4:11:1:11:4:1:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:3:1:3:1:1:1:1:12:4:1:1:1:1:1:1:6:5:1:2:1:1:4:3:1:1:1:1:1:1:3:1:1:1:1:12:4:1:1:1:1:1:1:1:2:3:1:7:1:1:6:1:1:2:1:1:2:2:1:1:12:3:9:3:9:3:9:3:1:11:1:1:11:3:8:1:1:2:2:4:2:1:1:1:1:3:4:1:2:2:1:2:2:1:12:3:6:4:2:1:1:1:5:1:1:2:1:3:2:1:3:1:2:2:1:1:12:3:6:2:1:2:4:1:1:5:1:4:1:3:1:1:1:1:2:2:1:1:12:3:6:1:2:1:1:2:3:2:3:2:2:3:1:1:2:3:1:2:11:1:11:3:5:6:2:1:1:1:2:1:2:2:2:1:1:1:1:4:1:1:1:2:1:2:1:12:3:5:2:2:2:1:1:1:3:1:9:3:4:5:1:1:1:1:1:1:12:3:5:1:9:1:1:1:1:1:2:2:2:6:1:4:1:2:1:12:3:5:1:2:2:3:2:1:1:5:1:2:1:1:2:1:1:3:4:11:1:11:3:5:1:1:2:3:3:1:2:1:1:2:2:2:2:5:1:2:2:1:2:1:12:3:4:4:3:3:4:1:2:4:1:2:2:1:3:3:2:1:1:12:3:4:3:3:1:2:1:2:2:1:2:1:1:3:3:7:2:2:1:12:3:4:3:1:1:2:1:2:1:2:1:2:4:5:1:1:5:1:1:11:1:11:3:4:2:2:9:3:4:2:2:8:4:1:1:1:1:12:3:4:2:1:1:1:3:2:2:3:2:2:2:1:1:3:1:1:2:4:2:1:12:3:4:1:4:1:1:11:3:4:1:4:1:1:1:1:1:2:1:12:3:4:1:2:4:2:2:1:3:1:1:1:4:3:1:2:2:1:1:12:1:1:1:1:11:3:4:1:2:1:2:1:1:2:1:2:1:2:3:3:2:3:3:4:2:1:1:12:3:4:1:1:1:6:1:1:1:4:1:3:1:1:3:3:5:2:1:1:12:3:4:1:1:1:1:2:2:1:2:1:1:1:3:2:1:1:4:2:1:1:2:1:1:1:1:2:1:12:3:3:6:2:2:4:1:2:1:1:5:1:3:1:1:1:1:1:1:1:11:1:1:11:3:3:5:1:1:1:1:2:1:3:3:2:1:1:1:1:2:2:3:2:2:1:2:1:12:3:3:3:3:3:3:3:3:3:3:3:3:2:2:2:1:1:12:3:3:3:2:1:1:1:6:1:2:2:1:2:1:1:1:1:2:4:2:2:1:1:12:3:3:3:1:1:3:2:1:1:1:1:1:4:3:7:2:2:12:1:1:1:11:3:3:2:5:1:1:1:1:1:1:4:4:1:1:1:4:1:4:1:1:2:1:1:12:3:3:2:2:1:4:1:1:1:2:1:3:1:3:1:2:1:2:1:1:5:2:1:12:3:3:2:1:4:1:2:1:1:1:3:1:1:2:2:1:4:3:2:1:1:2:1:1:12:3:3:2:1:2:1:1:11:3:3:2:1:2:1:1:3:11:1:1:11:3:3:2:1:1:1:4:4:3:1:1:5:8:2:3:2:1:12:3:3:1:4:1:2:1:2:3:2:1:1:3:5:5:1:2:2:1:1:12:3:3:1:3:1:6:1:1:5:2:1:4:4:2:1:1:2:2:1:12:3:3:1:2:4:2:1:3:1:2:1:1:1:1:3:1:2:2:1:4:1:11:1:1:11:3:3:1:2:2:1:2:2:1:3:1:2:3:1:3:1:1:1:1:1:3:1:2:2:1:1:12:3:3:1:1:5:2:2:5:5:1:1:1:1:1:1:1:1:1:1:5:1:1:1:1:1:1:12:3:3:1:1:3:1:2:2:5:2:1:1:4:8:1:2:1:2:1:12:3:3:1:1:2:2:1:1:1:1:1:3:1:1:1:1:4:2:3:2:5:1:11:1:11:3:3:1:1:1:4:2:4:1:1:1:1:2:1:1:2:2:2:1:1:2:1:1:1:1:1:2:1:1:12:3:3:1:1:1:1:2:1:1:1:1:1:2:4:1:1:4:2:5:1:1:1:2:2:1:12:3:2:8:2:3:1:2:5:2:1:1:2:1:2:2:1:2:1:2:1:12:3:2:6:1:2:2:3:1:1:3:1:3:6:2:1:2:1:11:1:1:11:3:2:5:1:9:2:1:1:3:3:1:1:1:6:4:1:1:1:1:12:3:2:4:2:1:2:2:1:1:1:1:2:1:1:1:3:1:4:1:2:2:1:1:1:2:1:12:3:2:4:1:1:6:1:1:1:1:2:3:1:1:1:1:2:1:1:2:2:3:2:1:12:3:2:3:5:1:7:3:1:1:2:1:1:3:1:1:2:14:1:1:1:1:1:1:11:3:2:3:3:1:2:1:1:3:3:2:1:2:1:1:2:1:2:1:1:1:1:2:1:1:2:1:12:3:2:3:1:4:1:2:1:6:1:5:1:2:4:2:1:2:2:1:12:3:2:3:1:2:1:1:4:1:4:3:4:2:2:1:1:1:1:1:3:2:1:12:3:2:3:1:1:1:3:6:3:2:3:1:1:1:3:5:1:1:11:1:11:3:2:2:6:1:1:1:2:1:2:1:2:2:3:1:4:1:2:3:3:1:1:1:12:3:2:2:3:1:3:1:3:3:4:4:1:1:1:2:2:1:1:1:1:1:1:2:1:12:3:2:2:2:3:4:1:1:3:3:3:2:1:1:1:3:1:1:1:1:1:2:1:1:1:12:3:2:2:2:2:1:1:1:2:2:2:2:1:2:11:3:1:1:11:1:11:3:2:2:2:1:1:2:4:1:1:1:5:5:1:1:2:1:1:3:1:3:1:1:1:12:3:2:2:1:2:8:3:2:2:1:2:8:3:1:1:2:1:12:3:2:2:1:2:1:2:1:7:2:1:4:1:1:3:5:4:1:1:1:1:12:3:2:2:1:1:3:2:1:1:1:1:2:1:6:1:2:2:1:1:2:2:13:1:1:1:1:11:3:2:2:1:1:1:5:1:1:2:1:3:4:5:1:2:2:2:4:1:1:1:1:12:3:2:1:6:1:2:1:1:4:1:3:5:1:1:2:1:3:1:3:1:1:1:12:3:2:1:4:4:1:1:1:4:1:2:5:1:3:3:1:1:2:2:1:1:12:3:2:1:4:1:1:3:1:1:3:1:2:3:1:1:1:2:2:1:1:1:4:1:11:1:11:3:2:1:3:3:1:1:1:1:1:2:1:2:3:1:2:1:7:2:5:1:1:1:1:1:12:3:2:1:3:1:1:1:1:7:6:1:1:2:2:1:4:2:1:2:1:1:12:3:2:1:2:3:3:4:2:4:1:1:6:2:2:1:1:1:2:2:1:12:3:2:1:2:2:2:5:2:9:3:2:1:2:2:1:1:11:1:1:11:3:2:1:2:2:1:1:1:1:3:1:1:1:1:2:1:2:1:2:1:1:1:1:2:1:1:1:5:1:1:1:1:1:1:12:3:2:1:2:1:1:2:2:2:1:1:2:1:3:2:1:4:2:1:2:1:1:1:1:1:1:2:1:12:3:2:1:1:8:3:2:1:1:8:3:2:1:1:2:3:1:1:1:12:3:2:1:1:4:2:1:1:2:1:1:2:2:1:1:1:3:3:4:1:1:3:11:1:11:3:2:1:1:3:2:1:2:1:1:4:4:2:1:2:2:1:1:1:3:3:2:1:1:12:3:2:1:1:2:2:1:2:3:1:1:2:2:5:1:1:1:1:1:1:1:2:1:1:1:2:1:1:12:3:2:1:1:2:1:1:5:1:2:2:1:1:2:2:2:4:1:1:3:1:1:2:1:1:12:3:2:1:1:1:5:1:1:2:9:1:1:4:1:2:3:1:1:1:11:1:11:3:2:1:1:1:3:1:1:2:1:2:2:1:1:1:1:1:11:2:5:1:1:1:1:1:12:3:2:1:1:1:1:3:2:2:2:4:2:1:5:2:1:4:3:1:2:1:12:3:2:1:1:1:1:1:5:3:3:1:3:4:5:1:1:2:1:2:2:1:12:3:2:1:1:1:1:1:1:2:1:1:1:2:1:6:1:1:2:4:2:1:2:2:11:1:1:11:3:1:8:1:1:3:6:2:2:2:1:4:6:3:1:1:1:12:3:1:6:1:1:2:1:2:1:1:2:1:1:3:1:1:2:2:3:3:1:2:2:1:12:3:1:5:2:3:5:1:1:4:5:1:3:1:1:1:2:3:1:1:1:12:3:1:5:1:2:4:1:3:8:3:1:5:1:2:11:1:1:10:3:1:5:1:1:1:1:1:1:2:1:1:1:2:2:3:6:1:1:2:3:2:2:1:1:11:3:1:4:1:2:1:2:2:4:5:1:2:1:1:1:5:1:1:1:2:1:2:1:11:3:1:3:5:6:1:1:6:1:4:6:2:4:1:1:1:1:11:3:1:3:3:1:2:1:8:1:2:3:2:2:2:2:4:2:12:1:11:3:1:3:2:1:4:1:1:1:1:2:3:1:1:2:2:3:4:1:1:1:1:1:1:2:1:11:3:1:3:1:3:4:1:1:1:2:2:1:1:1:1:3:1:1:2:1:2:5:1:12:1:11:3:1:3:1:1:11:3:1:3:1:1:10:4:1:1:1:1:11:3:1:3:1:1:1:2:1:1:3:5:1:5:2:1:2:4:2:1:2:12:1:11:3:1:2:5:1:1:3:3:1:5:3:1:1:3:5:1:4:1:1:1:1:11:3:1:2:3:3:2:1:5:3:2:1:1:1:1:1:5:1:1:2:1:2:12:1:11:3:1:2:3:1:1:1:3:1:3:2:1:2:2:1:2:6:1:1:1:1:2:1:2:1:11:3:1:2:2:2:2:5:11:3:1:2:2:2:1:3:12:1:11:3:1:2:2:1:2:2:1:1:6:2:1:3:1:5:4:2:2:1:2:1:11:3:1:2:1:6:2:1:1:1:3:1:1:2:1:1:1:3:1:3:4:1:2:12:1:11:3:1:2:1:3:1:2:3:1:1:3:2:1:1:4:1:1:2:2:5:1:2:1:1:11:3:1:2:1:2:1:3:11:3:1:2:1:2:1:3:3:13:1:1:1:11:3:1:2:1:1:3:3:1:3:1:1:8:1:1:1:1:1:3:1:2:4:1:1:1:1:11:3:1:2:1:1:1:5:2:4:1:5:1:1:1:1:2:2:2:2:1:14:1:1:1:1:11:3:1:2:1:1:1:1:1:4:1:1:1:1:1:1:2:2:5:4:1:1:1:1:2:2:2:1:11:3:1:1:5:5:1:4:2:1:3:1:2:3:1:1:2:3:3:12:1:11:3:1:1:4:2:4:3:1:2:1:4:1:4:3:2:6:1:1:1:1:1:1:1:11:3:1:1:3:8:1:1:1:1:1:1:3:2:4:1:3:2:1:4:12:1:11:3:1:1:3:2:1:5:1:1:4:1:1:2:1:4:5:2:1:3:2:1:11:3:1:1:3:1:1:6:1:1:3:1:3:1:2:2:3:2:3:1:3:12:1:11:3:1:1:2:4:4:6:1:2:3:1:5:1:1:2:1:1:1:2:2:1:11:3:1:1:2:2:6:2:4:4:2:2:1:2:3:1:1:2:1:2:12:1:11:3:1:1:2:2:1:1:4:1:3:1:1:1:1:1:1:11:2:3:1:2:1:11:3:1:1:2:1:2:4:5:1:4:1:1:1:7:3:2:1:13:1:1:1:11:3:1:1:2:1:1:2:3:2:2:1:11:3:1:1:2:1:1:2:2:1:1:11:3:1:1:1:8:1:2:1:3:2:4:3:2:2:2:1:3:13:1:1:1:11:3:1:1:1:4:1:2:5:1:1:6:1:1:1:3:4:1:1:1:1:1:2:1:1:11:3:1:1:1:3:1:3:2:3:2:3:1:3:1:1:6:3:1:1:1:12:1:1:11:3:1:1:1:2:3:2:1:4:1:1:3:2:3:1:2:2:3:4:3:1:1:1:11:3:1:1:1:2:1:4:1:1:2:1:3:1:1:2:1:2:1:4:1:4:2:1:1:12:1:11:3:1:1:1:2:1:1:1:1:4:2:1:2:6:2:2:1:2:1:1:1:4:1:2:1:11:3:1:1:1:1:3:2:2:5:1:1:1:1:2:1:3:2:1:1:2:6:12:1:1:11:3:1:1:1:1:2:2:1:2:1:1:2:2:3:2:1:1:1:3:3:1:1:2:1:2:1:2:1:11:3:1:1:1:1:1:4:1:2:2:2:3:3:1:2:1:3:1:5:2:2:12:1:11:3:1:1:1:1:1:2:1:1:1:1:1:4:1:2:3:2:1:1:2:2:3:2:2:1:2:1:1:11:3:1:1:1:1:1:1:1:1:4:8:2:1:3:1:1:1:1:2:1:1:1:2:2:12:1:11:2:8:4:1:6:1:1:1:2:2:3:1:2:2:1:2:1:2:2:1:11:2:7:2:2:2:3:2:1:2:2:11:2:2:1:1:12:1:11:2:6:5:1:1:2:1:1:3:3:3:5:1:2:1:2:1:2:2:1:11:2:6:2:1:1:3:1:1:2:2:1:2:1:1:7:1:1:2:15:1:1:1:1:1:1:11:2:6:1:1:2:1:4:2:1:2:1:3:3:2:1:1:2:1:1:3:2:2:1:11:2:5:4:1:1:1:2:1:1:1:1:1:1:1:3:1:2:1:2:3:1:3:1:1:12:1:1:11:2:5:2:2:4:1:1:5:1:3:1:5:2:2:1:1:3:2:1:1:11:2:5:1:5:3:2:3:1:1:3:1:3:3:2:4:2:1:12:1:11:2:5:1:2:1:2:1:1:1:2:2:2:1:2:2:1:3:1:1:2:1:1:2:3:2:1:11:2:5:1:1:1:2:2:2:1:3:1:1:1:1:1:4:2:1:1:2:5:13:1:1:1:11:2:4:5:1:1:1:5:2:3:3:2:3:1:2:4:1:2:2:1:11:2:4:3:2:4:2:1:1:1:3:1:1:1:3:2:1:2:1:5:2:12:1:11:2:4:2:5:3:1:1:1:1:1:1:1:1:1:2:2:1:1:2:2:1:1:1:1:3:2:1:11:2:4:2:2:1:1:4:2:3:1:2:4:4:1:1:1:1:2:1:14:1:1:1:1:11:2:4:2:1:1:2:1:4:1:2:1:1:5:1:3:1:1:4:2:1:1:1:2:1:11:2:4:1:3:7:2:2:1:2:2:2:3:3:2:1:1:1:14:1:1:1:1:11:2:4:1:2:2:2:4:1:7:2:2:1:2:1:4:3:3:1:1:1:11:2:4:1:1:4:4:4:2:1:2:5:3:4:1:2:1:1:12:1:11:2:4:1:1:2:1:1:1:3:1:2:2:1:1:1:4:2:1:1:3:1:4:3:1:1:1:11:2:4:1:1:1:1:1:4:2:4:2:2:2:3:2:1:4:2:1:1:1:12:1:11:2:3:5:3:2:1:1:2:2:3:2:1:1:9:1:1:4:1:1:1:1:11:2:3:4:2:1:2:2:5:2:1:1:2:1:2:1:1:2:1:1:3:1:1:12:1:1:11:2:3:3:4:1:1:1:2:2:1:4:1:2:1:1:1:3:1:2:2:3:2:1:1:11:2:3:3:1:4:4:3:1:2:1:1:1:6:1:3:3:1:2:12:1:11:2:3:3:1:1:1:1:3:2:1:3:2:7:1:1:2:1:2:2:4:1:1:1:1:11:2:3:2:3:2:1:1:2:3:1:1:1:3:1:2:6:1:2:3:1:12:1:1:11:2:3:2:2:1:4:1:1:1:1:1:1:2:4:2:1:1:1:2:1:1:1:1:3:1:1:2:1:11:2:3:2:1:3:1:3:2:1:1:1:5:1:1:1:2:2:1:1:2:4:13:1:1:1:11:2:3:2:1:1:3:1:1:1:4:1:3:2:1:2:1:11:2:2:1:11:2:3:1:7:3:1:2:1:1:2:3:3:4:1:1:1:2:1:1:13:1:1:1:11:2:3:1:4:1:1:2:2:1:5:1:1:1:4:1:4:1:2:2:1:2:2:1:11:2:3:1:3:1:1:5:1:3:1:2:2:1:1:3:1:3:1:1:1:1:3:1:12:1:11:2:3:1:2:2:3:1:2:1:1:1:1:2:3:3:1:2:1:1:3:2:1:1:1:1:2:1:11:2:3:1:2:1:2:1:1:2:2:3:2:3:1:1:1:5:5:1:1:13:1:1:1:11:2:3:1:1:4:3:1:1:2:1:1:1:1:5:2:2:3:1:2:1:3:2:1:1:11:2:3:1:1:2:3:2:3:2:2:2:2:4:1:1:2:1:3:2:1:13:1:1:1:11:2:3:1:1:1:7:4:7:1:4:1:2:1:1:2:2:2:2:1:11:2:3:1:1:1:2:1:2:1:1:1:1:1:2:1:1:2:1:1:1:2:1:1:2:2:3:2:13:1:1:1:11:2:3:1:1:1:1:1:2:1:1:2:1:1:1:4:1:2:1:1:2:1:2:1:1:3:1:2:2:1:1:11:2:2:6:3:1:5:1:4:2:5:1:3:2:2:2:12:1:1:11:2:2:5:2:1:1:2:2:1:4:1:1:1:3:1:2:2:2:1:1:4:2:1:1:11:2:2:4:3:2:3:1:2:3:1:2:1:4:2:3:1:1:2:1:1:12:1:1:11:2:2:4:1:3:1:4:1:3:1:1:1:5:2:2:2:1:2:1:1:1:1:2:1:11:2:2:3:6:5:7:2:1:3:1:1:3:2:1:3:12:1:11:2:2:3:3:1:1:2:1:1:1:1:1:1:1:1:7:4:3:1:4:2:1:1:11:2:2:3:2:1:1:2:11:2:2:3:2:1:1:2:3:2:12:1:11:2:2:3:1:2:2:1:5:2:5:1:2:1:1:7:1:2:3:1:1:1:11:2:2:3:1:1:1:3:4:1:1:2:2:2:1:1:2:1:2:1:1:1:2:2:1:2:12:1:11:2:2:2:5:1:1:3:1:1:3:1:1:1:1:1:2:3:2:1:2:4:1:2:1:1:11:2:2:2:3:2:2:2:2:1:1:1:1:1:1:1:1:1:2:2:2:2:1:2:1:3:1:12:1:11:2:2:2:2:4:1:1:4:1:1:1:2:1:2:7:1:3:1:4:1:1:1:1:1:11:2:2:2:2:1:4:1:1:3:5:3:1:7:1:4:2:1:12:1:11:2:2:2:1:11:2:2:2:1:11:2:1:3:1:1:1:1:11:2:2:2:1:3:1:1:3:2:2:1:2:3:1:1:2:1:6:1:3:1:12:1:1:11:2:2:2:1:2:1:1:3:2:2:1:6:1:1:1:4:4:1:2:2:1:2:1:11:2:2:2:1:1:2:2:1:5:2:1:3:1:1:2:1:1:5:6:1:12:1:11:2:2:2:1:1:1:1:3:2:1:1:3:1:1:1:1:1:1:1:1:1:2:2:2:4:3:1:1:1:1:11:2:2:1:6:1:1:1:1:1:3:1:3:1:1:3:1:2:3:1:1:1:3:1:12:1:1:11:2:2:1:4:2:1:3:1:4:2:2:5:3:1:1:1:2:1:1:1:2:2:1:11:2:2:1:3:3:3:1:1:2:4:3:3:2:1:1:1:2:2:1:1:1:2:12:1:11:2:2:1:3:1:3:1:2:1:1:1:5:2:3:3:2:1:2:3:1:3:1:1:1:11:2:2:1:2:5:5:4:1:1:1:2:3:2:2:1:2:1:2:1:1:1:12:1:11:2:2:1:2:2:5:2:1:2:2:2:1:5:1:1:1:1:2:1:1:1:2:1:1:2:1:11:2:2:1:2:2:1:1:1:1:2:1:4:2:1:4:1:4:1:1:2:5:1:12:1:11:2:2:1:2:1:2:1:1:5:1:2:4:8:1:1:2:1:1:1:2:1:2:1:11:2:2:1:2:1:1:1:1:3:1:2:1:2:1:2:1:1:1:2:4:4:1:1:1:2:1:12:1:11:2:2:1:1:4:2:1:1:2:3:1:2:1:2:1:1:1:4:3:1:2:2:2:2:1:11:2:2:1:1:3:1:2:3:1:1:1:1:4:2:2:1:2:5:1:2:1:2:12:1:1:11:2:2:1:1:2:2:3:2:2:1:2:1:1:1:1:2:1:2:1:3:2:3:2:2:2:1:11:2:2:1:1:2:1:1:5:4:2:1:2:2:1:2:1:2:2:1:1:1:3:1:12:1:1:11:2:2:1:1:1:4:1:1:3:1:1:3:1:2:5:2:5:1:4:2:1:1:11:2:2:1:1:1:2:2:1:4:1:2:1:1:1:1:2:3:2:1:3:2:1:3:1:12:1:1:11:2:2:1:1:1:1:3:2:2:8:3:1:3:1:2:4:2:1:2:2:1:11:2:2:1:1:1:1:1:2:4:2:3:1:4:1:3:1:1:1:2:1:2:3:1:1:12:1:11:2:1:9:1:2:1:1:6:4:3:1:4:2:2:1:1:1:2:1:11:2:1:6:3:2:3:2:1:1:1:1:2:1:8:1:1:1:2:2:12:1:1:11:2:1:5:5:1:1:3:3:1:1:2:1:1:1:2:4:1:1:2:3:1:2:1:11:2:1:5:1:4:6:1:3:1:4:1:4:2:1:4:13:1:1:1:11:2:1:4:8:3:1:2:2:1:1:5:5:1:1:1:3:2:1:1:11:2:1:4:3:1:1:1:1:3:1:1:1:1:4:4:1:1:2:2:5:12:1:1:1:11:2:1:4:2:1:1:1:2:1:1:1:1:2:1:2:2:4:1:1:1:1:1:2:1:1:2:3:1:1:1:11:2:1:4:1:2:1:1:3:1:4:4:1:1:1:7:1:2:3:2:12:1:11:2:1:4:1:1:1:1:2:1:1:2:3:1:1:1:2:1:2:1:3:3:1:1:1:1:1:1:1:2:1:11:2:1:3:4:1:2:2:2:7:1:1:2:3:1:1:1:3:1:2:2:12:1:11:2:1:3:3:1:1:3:1:2:2:1:1:2:1:3:2:2:9:2:1:1:1:11:2:1:3:2:2:1:4:1:2:3:2:1:3:1:2:1:3:4:1:2:12:1:1:11:2:1:3:2:1:1:1:8:1:3:3:2:3:1:1:4:4:2:1:1:11:2:1:3:1:3:2:1:1:2:2:1:1:5:1:2:1:1:2:3:2:1:1:2:1:12:1:11:2:1:3:1:2:1:2:1:2:1:6:1:1:3:2:1:1:1:3:4:1:1:1:2:1:11:2:1:3:1:1:2:2:1:4:1:1:1:2:2:1:2:3:2:1:1:2:4:1:1:12:1:11:2:1:3:1:1:1:1:2:2:1:1:3:1:1:1:3:3:1:1:1:4:3:4:1:1:1:1:1:11:2:1:2:5:2:2:2:1:1:2:1:1:4:1:1:2:2:1:5:2:1:1:12:1:11:2:1:2:4:1:2:2:3:1:2:2:2:1:4:3:1:1:1:1:2:1:2:2:1:1:11:2:1:2:3:2:2:2:5:2:6:1:1:3:1:1:2:1:1:1:1:1:12:1:1:11:2:1:2:3:1:1:2:3:1:1:1:2:2:3:1:1:3:4:4:2:3:1:1:1:11:2:1:2:2:3:2:3:1:1:1:2:1:2:1:1:1:1:2:4:2:3:2:1:12:1:1:11:2:1:2:2:2:1:2:2:5:1:1:1:2:1:2:3:1:1:4:3:2:2:1:1:11:2:1:2:2:1:2:2:2:3:1:1:1:1:2:2:11:2:1:1:1:12:1:1:11:2:1:2:2:1:1:1:2:2:1:5:3:2:1:2:2:3:1:2:2:2:1:1:2:1:11:2:1:2:1:4:2:2:1:1:1:5:1:1:6:1:2:1:1:1:4:1:12:1:1:11:2:1:2:1:3:1:2:1:3:2:2:2:1:1:2:1:1:1:4:3:1:1:3:1:2:1:11:2:1:2:1:2:2:2:1:2:1:1:5:3:4:1:3:3:2:2:13:1:1:1:11:2:1:2:1:2:1:1:2:1:1:1:6:1:1:2:2:3:1:4:3:3:2:1:1:11:2:1:2:1:1:3:1:6:1:1:1:2:1:4:2:3:3:3:1:2:1:12:1:11:2:1:2:1:1:2:1:1:3:5:1:1:2:1:2:1:1:1:2:9:2:1:1:1:11:2:1:2:1:1:1:2:1:3:1:1:1:1:2:1:1:1:2:1:1:1:1:1:2:1:2:2:2:1:1:1:12:1:11:2:1:2:1:1:1:1:1:1:2:2:1:1:1:1:5:4:1:3:1:2:1:3:2:2:1:1:11:2:1:1:6:1:1:6:5:4:1:2:5:2:1:2:2:12:1:11:2:1:1:5:1:1:1:2:2:7:2:2:1:3:1:1:1:3:1:3:2:1:1:11:2:1:1:4:2:1:1:1:2:4:4:2:1:1:1:1:1:1:1:1:1:2:3:1:13:1:1:1:11:2:1:1:4:1:1:1:1:1:1:1:1:1:2:4:1:1:11:2:1:1:1:2:1:1:11:2:1:1:3:2:5:2:1:5:3:3:1:1:2:5:2:2:1:12:1:11:2:1:1:3:1:5:2:1:1:1:4:1:2:2:1:1:1:1:1:1:2:2:1:1:1:1:1:2:1:11:2:1:1:3:1:1:3:3:3:1:2:2:3:2:4:1:2:3:1:2:12:1:1:11:2:1:1:2:6:1:3:2:1:2:1:1:1:1:2:1:1:1:2:3:3:2:2:1:1:1:11:2:1:1:2:3:2:6:1:2:2:1:1:2:1:2:1:3:1:1:1:1:1:3:1:12:1:11:2:1:1:2:2:3:2:4:2:5:1:1:3:1:3:2:7:1:1:1:1:1:1:1:1:11:2:1:1:2:2:1:2:1:5:1:3:1:1:1:2:4:1:1:1:1:1:1:2:3:1:12:1:11:2:1:1:2:1:4:1:1:3:2:1:7:3:1:8:3:1:2:1:11:2:1:1:2:1:2:1:5:3:2:2:2:2:4:2:1:2:1:1:3:1:12:1:1:11:2:1:1:2:1:1:2:3:2:1:1:2:2:1:1:2:2:4:1:4:1:3:2:1:1:1:11:2:1:1:2:1:1:1:1:2:2:2:2:1:2:1:3:1:2:3:2:2:2:1:3:1:12:1:11:2:1:1:1:5:2:1:1:1:1:1:3:6:5:1:1:2:4:3:1:1:1:1:11:2:1:1:1:4:1:1:2:2:5:1:3:2:1:3:2:1:1:4:1:13:1:1:1:1:11:2:1:1:1:3:2:1:1:2:2:5:4:2:2:3:1:4:1:1:1:1:1:2:1:11:2:1:1:1:2:11:2:1:1:1:2:11:1:4:12:1:1:11:2:1:1:1:2:2:4:1:6:1:2:1:2:2:1:2:1:3:5:1:1:2:1:11:2:1:1:1:2:1:4:1:6:1:1:1:1:1:1:2:2:1:2:4:2:14:1:1:1:1:1:11:2:1:1:1:2:1:1:2:3:3:1:3:2:1:1:1:1:2:3:9:1:2:1:11:2:1:1:1:1:5:1:4:1:1:7:6:2:2:1:3:1:13:1:1:1:11:2:1:1:1:1:3:2:1:1:1:1:1:4:1:1:2:1:1:1:2:1:1:2:5:1:1:3:1:1:1:11:2:1:1:1:1:2:2:3:1:3:1:1:3:3:2:5:1:3:1:2:1:1:1:12:1:11:2:1:1:1:1:2:1:1:2:1:2:1:4:2:2:4:4:1:2:1:1:4:2:1:1:11:2:1:1:1:1:1:3:1:2:3:1:2:2:1:1:1:1:2:1:2:2:2:2:5:1:12:1:11:2:1:1:1:1:1:2:1:1:1:1:4:2:1:2:2:7:1:1:4:1:1:1:3:1:1:1:11:2:1:1:1:1:1:1:1:4:6:3:2:2:1:1:2:4:3:2:1:1:1:12:1:11:1:11:1:11:1:11:1:3:2:1:1:1:11:1:8:2:2:5:3:3:1:2:8:1:2:1:1:13:1:1:1:11:1:7:3:1:1:4:1:1:1:1:1:1:3:3:1:1:2:2:6:2:1:1:11:1:7:1:1:3:2:4:11:1:7:1:1:1:1:12:1:11:1:6:3:2:1:2:1:1:2:2:1:1:1:2:6:1:1:2:1:4:1:2:1:11:1:6:2:1:1:2:1:2:1:2:2:1:4:1:1:1:1:1:4:1:2:1:2:12:1:1:11:1:6:1:1:11:1:6:1:1:11:1:1:2:1:11:1:5:6:3:2:1:1:1:3:2:1:1:2:1:3:1:2:1:1:2:1:12:1:1:11:1:5:3:2:3:1:4:1:1:1:2:4:1:2:1:3:3:3:1:2:1:11:1:5:2:3:1:1:3:1:1:1:1:2:1:1:2:6:1:1:1:1:2:3:12:1:1:11:1:5:2:1:1:3:1:1:4:5:1:1:1:5:3:2:2:1:1:2:1:1:11:1:5:1:3:1:4:2:2:1:1:1:2:2:1:1:1:3:2:1:2:1:2:1:12:1:1:11:1:5:1:2:1:1:1:2:1:1:1:1:3:2:2:4:1:5:6:1:2:1:11:1:5:1:1:1:4:1:2:2:3:3:1:3:1:2:1:2:1:1:5:12:1:1:11:1:4:6:4:1:3:1:1:2:1:3:2:2:3:2:3:2:2:1:1:11:1:4:4:2:1:1:3:1:3:1:1:1:1:1:2:1:1:1:3:7:12:1:1:1:11:1:4:3:2:2:6:1:1:2:1:1:1:1:2:2:1:2:5:2:2:1:1:1:11:1:4:3:1:1:1:3:1:1:1:1:5:1:1:1:1:1:1:1:2:1:2:1:4:1:12:1:1:11:1:4:2:3:1:1:1:1:1:1:1:5:3:2:1:1:6:2:5:1:1:1:1:1:1:11:1:4:2:1:4:2:1:1:1:2:1:1:2:2:4:1:3:1:3:1:1:2:1:12:1:11:1:4:2:1:1:2:1:6:1:3:1:8:1:2:1:1:1:2:1:1:2:1:1:11:1:4:1:4:3:3:1:5:1:1:1:2:3:3:2:2:1:3:12:1:1:11:1:4:1:3:1:1:4:1:4:1:1:1:1:1:1:1:1:1:3:1:1:3:1:1:3:1:1:1:1:11:1:4:1:2:2:1:1:1:1:2:3:2:1:1:1:1:4:1:2:4:1:4:1:12:1:11:1:4:1:1:5:2:1:1:1:2:1:2:1:2:1:1:2:3:1:2:3:1:1:1:2:1:1:11:1:4:1:1:2:2:1:3:2:2:1:2:1:2:3:1:2:1:2:2:1:2:1:1:12:1:1:11:1:4:1:1:1:2:3:1:1:4:2:1:1:1:3:2:1:1:2:1:1:1:1:1:1:2:2:1:1:11:1:4:1:1:1:1:1:1:2:2:4:5:3:1:3:4:1:5:12:1:1:11:1:3:5:5:1:2:4:1:2:3:1:3:1:2:1:1:5:2:1:1:11:1:3:4:3:4:1:2:1:1:3:1:1:1:5:5:1:2:1:1:1:12:1:11:1:3:4:1:1:7:1:2:2:2:2:1:2:1:1:3:1:1:3:1:2:1:1:1:11:1:3:3:3:2:1:1:1:2:2:1:3:2:1:3:3:1:3:2:2:12:1:1:1:11:1:3:3:2:1:1:1:1:1:2:5:1:4:1:3:2:1:4:3:1:2:1:1:11:1:3:3:1:1:3:1:2:1:1:1:1:4:2:1:2:2:2:3:1:1:1:3:1:12:1:11:1:3:2:5:1:1:2:1:3:1:2:1:2:2:2:1:3:6:1:1:2:1:1:11:1:3:2:3:1:2:1:2:1:5:4:11:1:3:1:1:12:1:11:1:3:2:2:2:1:1:1:2:1:2:2:1:4:1:1:3:2:1:2:2:1:3:2:1:1:11:1:3:2:1:5:1:1:5:2:5:1:1:3:1:1:1:3:1:1:1:1:12:1:1:11:1:3:2:1:2:2:1:1:1:2:2:1:1:2:1:1:1:1:5:6:2:2:2:1:1:11:1:3:2:1:1:2:2:1:1:1:1:2:4:1:1:2:1:2:4:1:2:2:2:12:1:1:1:11:1:3:1:11:1:3:1:11:1:3:2:1:1:1:1:2:1:11:1:3:1:4:2:3:4:1:2:2:4:3:1:2:2:1:2:1:1:12:1:1:1:11:1:3:1:3:2:4:1:1:1:2:1:1:1:1:3:1:1:3:1:1:6:1:1:2:1:1:11:1:3:1:3:1:1:1:2:2:1:1:3:2:2:1:5:4:3:1:1:1:12:1:1:1:11:1:3:1:2:2:4:1:1:1:1:1:1:2:1:1:1:1:1:1:1:2:1:3:1:5:2:1:1:1:11:1:3:1:2:1:3:1:2:3:1:3:1:1:3:1:2:10:1:1:12:1:1:11:1:3:1:2:1:1:1:2:1:2:4:1:4:2:1:1:1:5:1:1:2:1:2:2:1:1:11:1:3:1:1:3:11:1:3:1:1:3:10:2:12:1:1:11:1:3:1:1:2:3:3:1:2:5:1:1:1:1:1:1:2:1:1:1:1:1:2:1:1:1:1:2:1:1:11:1:3:1:1:2:1:1:3:1:2:2:1:1:1:1:3:2:2:2:2:4:3:1:1:12:1:11:1:3:1:1:1:3:1:1:2:3:1:1:1:1:4:6:1:3:3:4:1:1:1:1:1:11:1:3:1:1:1:1:4:1:1:3:1:1:1:1:1:5:3:1:5:1:3:12:1:1:1:11:1:3:1:1:1:1:1:1:4:2:1:4:6:1:1:4:1:2:1:4:2:1:1:11:1:2:7:1:2:1:1:2:3:1:1:2:2:1:5:1:1:3:1:1:2:1:12:1:11:1:2:5:4:1:6:1:1:1:4:3:4:6:2:2:1:1:11:1:2:5:1:1:5:3:1:3:2:1:3:2:2:1:5:1:1:12:1:1:1:11:1:2:4:3:1:2:1:2:4:1:3:1:1:1:1:3:2:5:2:2:1:1:1:11:1:2:4:1:4:1:5:2:1:2:6:2:4:1:1:1:14:1:1:1:1:1:11:1:2:4:1:1:1:4:5:3:1:2:4:5:1:1:1:1:1:2:2:1:1:11:1:2:3:4:1:1:5:11:1:2:3:3:3:1:12:1:1:11:1:2:3:2:4:1:1:2:2:1:3:1:1:2:1:2:1:2:1:3:1:3:1:1:2:1:11:1:2:3:2:1:1:3:2:2:1:2:1:1:2:3:1:4:1:2:3:3:12:1:1:11:1:2:3:1:3:1:1:1:4:1:1:4:5:2:1:2:2:1:3:2:1:2:1:11:1:2:3:1:1:3:3:1:1:2:1:1:2:1:2:1:3:1:1:4:1:2:1:2:12:1:1:11:1:2:3:1:1:1:1:2:2:1:2:3:1:1:7:2:2:1:5:1:1:1:2:1:11:1:2:2:4:5:1:3:2:3:2:11:1:1:3:1:12:1:11:1:2:2:3:3:1:2:1:2:3:1:2:2:1:1:4:1:3:3:2:1:2:1:1:11:1:2:2:3:1:1:1:3:1:1:2:1:1:2:2:1:1:1:2:3:1:1:1:1:2:2:1:1:12:1:11:1:2:2:2:2:3:1:4:1:1:3:4:3:2:1:1:1:2:3:1:1:1:2:1:11:1:2:2:2:1:2:3:2:2:1:1:3:1:2:1:6:3:1:2:2:12:1:1:1:11:1:2:2:2:1:1:1:1:1:1:1:2:2:1:1:1:1:1:8:1:1:1:5:2:1:2:1:11:1:2:2:1:3:2:1:5:2:2:2:2:1:3:1:1:3:1:3:1:2:12:1:1:11:1:2:2:1:2:2:1:3:4:1:2:1:2:1:1:2:1:1:2:3:1:3:1:1:2:1:1:11:1:2:2:1:1:5:1:4:1:4:1:1:1:1:1:2:1:2:1:5:1:13:1:1:1:1:11:1:2:2:1:1:2:1:3:1:2:1:2:1:1:3:1:2:1:1:1:2:1:1:1:1:3:1:2:1:1:1:11:1:2:2:1:1:1:1:6:1:2:4:7:1:1:3:2:2:2:12:1:1:1:11:1:2:1:7:1:1:3:2:1:3:1:1:1:3:1:1:1:1:1:1:5:1:1:1:2:1:11:1:2:1:5:1:1:1:7:3:2:3:1:2:3:2:3:3:12:1:11:1:2:1:4:1:2:3:1:1:2:1:1:1:1:2:1:3:5:2:7:1:1:1:1:1:1:1:1:11:1:2:1:3:3:2:1:1:1:1:2:1:1:4:2:1:1:1:4:3:4:1:12:1:1:11:1:2:1:3:1:4:1:11:1:2:1:3:1:4:1:4:2:1:11:1:2:1:3:1:1:1:2:4:3:2:1:1:1:6:1:1:2:1:1:2:3:12:1:11:1:2:1:2:3:4:2:2:1:1:1:1:2:4:1:1:1:1:2:2:2:1:3:2:1:1:11:1:2:1:2:2:2:4:2:2:1:3:5:4:1:6:1:13:1:1:1:1:11:1:2:1:2:1:11:1:2:1:2:1:11:1:2:3:1:1:1:1:11:1:2:1:2:1:2:2:1:1:1:6:2:1:1:3:4:2:1:1:2:2:1:1:12:1:1:11:1:2:1:2:1:1:2:1:1:1:5:1:5:1:1:2:1:3:1:1:3:1:1:2:1:2:1:11:1:2:1:1:6:1:2:2:2:3:1:1:3:1:1:2:3:6:13:1:1:1:1:11:1:2:1:1:4:1:1:1:3:2:1:2:1:1:2:1:1:4:5:3:2:2:1:1:1:11:1:2:1:1:3:1:2:1:4:4:5:3:1:2:1:2:3:3:1:1:12:1:11:1:2:1:1:2:3:1:2:1:1:1:1:1:1:3:1:1:1:1:3:1:2:1:1:1:4:1:2:1:1:1:11:1:2:1:1:2:1:3:2:1:1:1:2:1:3:5:1:4:2:2:2:1:1:1:12:1:1:11:1:2:1:1:2:1:1:1:1:2:1:1:1:1:5:1:2:3:1:1:3:7:1:1:2:1:11:1:2:1:1:1:3:2:1:6:3:1:1:1:1:1:1:3:1:1:1:2:1:4:12:1:1:1:11:1:2:1:1:1:2:2:1:1:4:1:1:7:1:1:2:1:3:2:3:2:2:1:1:1:11:1:2:1:1:1:1:4:1:3:1:1:1:2:1:2:2:5:1:2:2:3:1:2:12:1:1:11:1:2:1:1:1:1:2:1:1:1:2:1:2:3:1:1:2:1:3:3:3:1:1:3:2:2:1:1:11:1:2:1:1:1:1:1:1:2:1:1:2:6:2:2:1:1:2:1:1:3:3:1:14:1:1:1:1:1:11:1:1:8:1:1:1:1:1:4:4:1:2:1:1:1:3:1:1:1:1:2:1:1:1:1:2:1:11:1:1:6:2:5:2:1:1:2:7:2:1:2:1:2:3:2:1:12:1:11:1:1:5:6:5:2:1:3:1:2:3:1:1:1:2:2:2:2:1:2:1:11:1:1:5:2:1:3:1:1:3:1:1:1:3:4:5:1:1:1:2:2:12:1:1:1:11:1:1:5:1:1:3:2:2:2:4:1:1:3:1:1:4:2:2:1:3:2:1:11:1:1:4:4:2:1:1:3:1:4:2:6:1:3:1:2:3:1:1:12:1:11:1:1:4:2:3:3:3:1:1:1:3:1:2:1:4:1:2:2:1:3:1:1:2:1:11:1:1:4:2:1:1:1:2:1:2:1:1:2:3:1:3:1:1:2:3:3:1:2:1:12:1:1:11:1:1:4:1:2:2:1:2:1:1:2:1:3:2:2:1:1:2:1:1:1:2:2:1:2:1:1:2:1:11:1:1:4:1:1:1:7:1:2:2:2:1:5:3:1:3:4:1:1:12:1:11:1:1:3:5:3:1:3:2:3:3:1:2:1:1:1:1:1:2:1:3:1:1:1:2:1:11:1:1:3:3:4:1:5:2:3:1:1:1:1:2:2:1:2:1:1:1:3:1:1:12:1:11:1:1:3:3:1:1:1:3:3:2:1:1:3:4:1:1:1:1:1:1:1:1:1:2:2:2:1:1:11:1:1:3:2:2:2:1:4:1:1:2:2:2:2:1:1:3:8:13:1:1:1:1:11:1:1:3:2:1:2:1:1:1:1:1:1:1:2:2:3:1:1:2:1:8:1:1:1:1:1:2:1:11:1:1:3:1:4:3:1:1:4:1:2:1:1:2:3:2:3:1:3:1:1:1:12:1:1:11:1:1:3:1:2:4:1:1:2:2:1:1:1:1:1:2:1:5:2:1:1:1:1:1:3:2:1:11:1:1:3:1:2:1:1:1:2:1:2:1:7:3:1:1:1:2:1:3:2:2:1:12:1:1:11:1:1:3:1:1:2:2:1:1:1:1:4:1:1:1:1:1:1:2:1:2:1:1:1:2:1:2:4:1:1:1:1:1:11:1:1:3:1:1:1:1:4:1:1:1:1:2:1:1:2:1:3:1:5:2:2:15:1:1:1:1:1:1:11:1:1:2:6:1:5:1:1:1:1:2:3:2:2:3:1:3:2:2:1:2:1:1:11:1:1:2:4:3:1:2:3:1:1:2:1:2:3:3:2:2:1:1:1:1:2:1:11:1:1:11:1:1:2:3:6:4:2:3:2:1:2:2:3:4:2:3:2:1:11:1:1:2:3:2:1:2:1:1:2:4:1:1:1:1:3:1:1:2:1:5:1:3:2:1:11:1:1:2:3:1:1:2:1:3:3:2:1:2:2:1:1:4:1:1:1:1:2:2:2:11:1:11:1:1:2:2:3:4:5:2:1:3:1:2:2:1:1:5:1:1:4:1:1:1:1:1:11:1:1:2:2:2:2:2:1:1:1:1:4:1:1:3:2:3:2:1:1:1:2:2:1:2:1:1:1:11:1:1:2:2:1:4:1:2:1:2:2:3:3:1:2:1:3:1:2:2:2:2:11:1:11:1:1:2:2:1:1:5:1:1:2:1:1:1:3:1:2:2:1:3:2:1:1:1:2:2:2:1:11:1:1:2:2:1:1:1:1:1:2:8:3:3:1:2:1:1:1:4:2:3:1:1:1:1:11:1:1:2:1:4:1:6:1:1:1:3:1:1:1:5:1:1:1:1:1:1:2:1:1:11:1:1:1:11:1:1:2:1:3:1:3:1:1:3:4:1:1:1:2:1:6:1:1:3:1:1:2:1:1:1:11:1:1:2:1:2:3:1:1:1:11:1:1:2:1:2:3:1:1:1:3:2:1:1:1:11:1:1:2:1:2:1:2:2:2:4:11:1:1:2:1:2:1:2:1:1:11:1:1:11:1:1:2:1:1:5:1:4:2:3:2:1:4:1:2:3:4:1:1:1:2:1:1:11:1:1:2:1:1:2:5:1:1:1:3:3:6:1:2:1:6:1:1:1:2:1:1:11:1:1:2:1:1:2:1:1:1:2:1:2:1:1:3:1:5:1:4:2:1:1:1:3:1:1:11:1:11:1:1:2:1:1:1:2:1:3:1:2:2:4:3:11:1:1:1:1:2:1:1:1:11:1:1:2:1:1:1:1:1:2:2:1:2:1:4:1:1:4:2:2:1:1:3:3:3:1:1:1:1:11:1:1:1:6:4:1:1:2:2:2:2:3:2:1:2:2:3:3:1:1:1:11:1:11:1:1:1:5:2:1:1:5:3:1:2:4:1:2:1:5:3:2:2:1:11:1:1:1:4:3:2:3:1:2:2:1:1:1:2:1:1:2:3:3:2:2:1:2:1:1:1:11:1:1:1:4:1:3:1:2:1:2:1:1:1:1:3:1:2:5:3:3:1:1:1:11:1:1:11:1:1:1:3:6:1:2:1:2:1:2:2:1:1:4:1:2:1:3:1:3:2:1:1:1:11:1:1:1:3:2:9:2:1:2:2:1:1:1:7:4:3:2:1:11:1:1:1:3:2:1:1:1:3:1:2:1:2:1:3:1:1:3:1:2:3:1:1:1:3:11:1:11:1:1:1:3:1:2:1:3:1:2:1:1:2:1:4:4:2:2:1:2:3:2:2:1:11:1:1:1:3:1:1:1:2:1:2:2:1:2:1:3:1:2:2:4:1:2:1:6:1:1:1:1:1:1:1:11:1:1:1:2:4:3:1:1:1:3:1:2:2:1:2:2:1:1:3:1:6:11:1:1:1:11:1:1:1:2:3:1:4:11:1:1:1:2:3:1:4:3:1:2:1:1:11:1:1:1:2:2:3:1:1:3:3:1:1:1:6:1:2:1:1:1:4:1:1:1:1:1:2:1:11:1:1:1:2:2:1:2:2:1:1:2:1:1:3:1:1:1:1:4:3:8:11:1:1:11:1:1:1:2:1:4:3:2:6:3:3:4:3:1:5:1:2:1:1:11:1:1:1:2:1:2:3:1:2:1:1:3:3:1:3:3:2:2:2:2:2:2:2:1:11:1:1:1:2:1:1:4:3:2:3:2:5:3:2:1:2:3:3:1:11:1:1:11:1:1:1:2:1:1:1:4:2:6:6:1:1:7:2:1:2:2:1:1:1:11:1:1:1:1:11:1:1:1:1:11:1:1:1:2:1:1:1:1:1:1:1:2:1:11:1:1:1:1:5:1:1:1:2:3:3:1:5:1:4:1:7:1:11:1:1:1:11:1:1:1:1:4:1:1:1:3:1:4:1:1:2:2:1:2:2:2:2:1:1:2:2:1:1:2:1:11:1:1:1:1:3:2:1:2:1:4:1:3:4:2:1:2:2:3:1:1:2:1:2:1:1:1:11:1:1:1:1:3:1:1:1:3:1:4:5:1:2:2:1:2:1:6:1:1:1:1:11:1:11:1:1:1:1:2:3:1:7:2:2:1:5:4:4:1:1:1:2:1:2:1:1:11:1:1:1:1:2:2:1:2:1:2:1:7:4:1:2:4:1:2:1:2:3:1:1:1:1:11:1:1:1:1:2:1:2:2:2:1:4:3:1:2:1:4:1:1:1:1:3:2:1:12:1:1:1:1:11:1:1:1:1:2:1:1:1:2:4:1:1:4:4:1:1:1:1:3:1:1:2:1:2:3:1:1:1:1:11:1:1:1:1:1:4:3:1:2:1:1:2:1:1:8:1:2:1:1:3:1:2:2:1:1:1:11:1:1:1:1:1:3:1:3:2:3:1:2:1:1:5:1:2:4:2:2:1:1:1:11:1:1:11:1:1:1:1:1:2:2:4:1:1:2:1:1:1:2:1:2:1:2:1:3:3:2:1:3:2:1:11:1:1:1:1:1:2:1:1:4:2:6:3:2:1:2:2:2:1:2:4:2:1:1:11:1:1:1:1:1:1:3:5:1:5:2:2:2:2:1:2:2:2:4:1:1:11:1:1:11:1:1:1:1:1:1:2:1:6:1:4:1:1:6:1:1:3:1:2:1:1:2:2:1:1:1:11:1:1:1:1:1:1:1:2:7:4:1:1:1:1:3:2:4:2:4:2:2:1:11:1:1:1:1:1:1:1:1:1:11:1:1:1:1:1:1:1:1:1:10:1:1:11:1:1:10:10:10:10:10:3:1:1:1:1:10:9:1:7:3:6:1:2:1:4:2:5:1:2:1:10:8:2:6:1:3:4:1:1:1:1:2:1:3:1:1:2:3:1:11:1:10:8:1:1:5:1:4:2:2:2:1:3:2:2:1:1:3:2:1:2:1:1:10:7:3:4:2:4:1:2:1:2:1:1:1:1:2:1:1:2:1:2:1:1:1:1:2:1:10:7:2:1:4:1:1:1:2:3:2:6:2:1:1:2:5:1:11:1:10:7:1:2:3:2:2:2:1:3:2:1:4:2:8:3:1:1:1:1:10:7:1:1:1:3:1:1:3:4:2:1:5:3:2:1:4:1:2:1:1:1:10:6:4:2:3:3:2:1:1:1:6:1:1:1:1:1:5:1:1:11:1:1:1:10:6:3:1:2:2:1:2:1:1:1:1:1:2:3:1:3:2:2:1:4:1:1:1:2:1:10:6:2:2:2:1:1:5:4:1:1:2:1:1:2:1:1:3:1:3:1:1:1:2:1:10:6:2:1:1:1:5:1:1:2:3:3:1:1:1:2:4:1:1:1:1:1:1:1:11:1:1:10:6:1:3:1:2:8:2:3:10:4:2:2:1:10:6:1:2:1:1:2:1:1:2:1:3:2:1:1:1:1:8:2:2:3:2:1:10:6:1:1:2:1:1:2:1:5:1:6:2:1:1:1:2:2:2:1:1:1:1:11:1:1:10:6:1:1:1:1:1:1:1:1:2:1:1:1:1:10:5:6:2:1:1:10:5:4:5:1:3:2:2:5:1:2:1:2:1:4:3:1:2:1:10:5:3:5:1:1:1:2:4:2:1:2:5:2:5:1:1:1:11:1:1:10:5:3:1:3:1:4:1:4:2:4:1:1:1:1:2:1:4:1:3:1:1:1:1:10:5:2:4:4:2:1:2:3:1:1:4:1:1:2:2:2:1:1:1:1:2:1:1:1:10:5:2:2:2:2:1:2:2:1:1:4:1:1:3:1:1:2:5:2:1:11:1:1:1:10:5:2:1:3:1:1:4:2:6:1:10:4:3:1:1:1:1:10:5:2:1:1:1:4:4:1:5:2:4:1:3:2:2:1:1:1:1:1:2:1:10:5:1:4:2:5:2:1:2:4:4:1:4:1:1:2:2:11:1:1:10:5:1:3:1:2:1:1:2:1:3:1:2:2:2:3:1:3:1:1:2:1:1:1:1:1:2:1:10:5:1:2:2:1:2:2:1:4:2:2:2:3:1:1:1:1:2:1:1:3:1:1:2:1:1:10:5:1:2:1:1:1:1:1:3:3:3:2:3:2:1:3:4:1:1:1:1:11:1:1:1:10:5:1:1:2:5:1:1:1:1:3:2:1:3:2:3:2:1:1:1:2:1:3:1:1:1:1:10:5:1:1:1:4:2:1:1:5:6:1:2:3:1:1:1:3:4:1:1:1:1:1:10:5:1:1:1:1:2:3:1:2:1:2:1:1:2:3:1:1:1:1:1:2:1:3:1:3:11:1:10:4:7:1:2:1:1:2:3:2:1:3:1:1:2:1:2:3:3:1:2:1:1:1:10:4:5:1:3:5:2:3:1:4:1:1:2:5:3:1:1:1:2:1:10:4:4:2:2:1:3:1:1:1:1:2:2:1:1:2:1:2:1:3:2:2:1:12:1:1:1:1:10:4:4:1:1:1:2:1:5:1:2:4:1:2:1:1:4:1:2:1:2:2:1:1:10:4:3:3:1:1:1:1:1:1:5:1:1:1:1:1:2:5:1:2:3:1:3:1:1:1:1:10:4:3:1:4:3:2:4:4:1:1:1:1:1:1:1:1:2:1:1:2:2:1:11:1:10:4:3:1:1:2:3:1:2:2:6:1:1:1:1:3:1:1:3:1:1:1:1:1:1:2:1:10:4:2:5:1:1:2:1:2:2:1:4:1:10:3:2:2:1:1:10:4:2:3:1:2:5:2:1:4:2:2:2:2:1:2:1:3:3:11:1:10:4:2:2:2:1:3:1:2:2:2:2:1:1:1:10:3:2:1:2:1:10:4:2:2:1:1:1:1:1:1:2:2:3:3:6:1:2:1:3:2:3:2:1:10:4:2:1:2:3:2:3:4:2:4:1:3:1:1:2:1:4:2:11:1:10:4:2:1:1:3:2:1:4:1:1:6:2:3:2:1:1:4:4:1:1:1:1:1:10:4:2:1:1:1:1:4:3:1:1:1:1:1:4:1:1:3:2:2:1:1:1:1:1:1:2:1:1:1:10:4:1:5:2:1:1:1:4:2:1:1:1:1:2:3:10:1:1:11:1:10:4:1:4:1:1:1:1:8:2:2:1:1:1:1:3:1:2:1:4:2:2:1:10:4:1:3:1:3:3:2:4:1:1:1:1:4:3:3:1:2:1:1:2:2:1:10:4:1:2:4:2:1:2:1:2:1:1:1:1:3:1:2:2:3:2:1:1:1:1:1:1:1:11:1:10:4:1:2:2:1:3:3:1:3:1:4:2:1:2:1:1:2:2:1:1:1:1:1:2:1:1:1:10:4:1:2:1:2:1:3:1:5:1:1:1:1:1:4:1:3:1:2:1:1:1:1:2:2:1:10:4:1:1:9:1:2:2:2:7:3:1:1:1:1:1:1:3:1:11:1:10:4:1:1:3:2:5:3:1:2:1:1:2:2:6:2:2:2:1:2:1:1:10:4:1:1:2:3:1:1:1:1:1:1:1:3:2:2:5:7:2:2:1:2:1:10:4:1:1:2:1:1:2:1:1:1:1:5:1:1:2:1:3:2:1:1:1:3:1:3:1:11:1:10:4:1:1:1:3:1:1:1:1:1:1:6:2:2:2:5:1:2:1:1:4:2:1:10:4:1:1:1:1:4:1:1:1:1:1:2:2:1:1:1:3:1:1:1:1:1:4:2:1:1:1:2:1:1:1:10:4:1:1:1:1:1:1:6:5:1:2:1:1:4:3:1:1:1:1:1:2:1:11:1:1:10:3:7:1:4:1:3:4:5:3:1:1:2:1:2:1:1:1:1:1:2:1:10:3:5:6:2:1:1:1:2:1:2:2:2:1:1:1:1:4:1:1:1:2:1:2:1:10:3:5:1:2:2:3:2:1:1:5:1:2:1:1:2:1:1:4:1:1:2:11:1:10:3:4:3:3:1:2:1:2:2:1:2:1:1:3:3:8:1:2:1:1:10:3:4:2:1:1:1:3:2:2:3:2:2:2:1:1:3:1:1:2:2:4:1:1:1:1:1:10:3:4:1:1:3:1:5:2:1:4:1:2:1:5:1:2:2:3:11:1:10:3:3:5:1:1:1:1:2:1:3:3:2:1:1:1:1:2:2:3:2:1:2:2:1:10:3:3:3:1:1:3:2:1:1:1:1:1:4:3:7:2:3:1:1:2:1:10:3:3:2:1:4:1:2:1:1:1:3:1:1:2:2:1:4:3:2:1:2:11:1:1:10:3:3:1:4:1:2:1:2:3:2:1:1:3:5:5:2:1:1:2:1:1:10:3:3:1:2:1:1:10:3:3:1:2:1:1:8:2:2:1:10:3:3:1:1:1:4:2:4:1:1:1:1:2:1:1:2:2:2:1:1:2:1:1:1:2:11:1:10:3:2:6:1:2:2:3:1:1:3:1:3:6:2:1:3:2:2:1:10:3:2:4:1:1:6:1:1:1:1:2:3:1:1:1:1:2:1:1:2:2:4:2:1:10:3:2:3:1:4:1:2:1:6:1:5:1:2:4:2:2:12:1:1:1:10:3:2:2:4:1:1:2:2:1:2:2:3:3:1:2:2:1:1:1:4:3:1:1:1:10:3:2:2:2:1:1:2:4:1:1:1:5:5:1:1:2:1:1:3:3:2:1:10:3:2:2:1:1:3:2:1:1:1:1:2:1:6:1:2:2:1:1:2:2:1:13:1:1:1:1:10:3:2:1:4:4:1:1:1:4:1:2:5:1:3:3:1:2:2:1:2:1:10:3:2:1:3:1:1:1:1:7:6:1:1:2:2:1:3:3:4:1:1:1:1:10:3:2:1:2:1:2:2:3:2:1:4:1:1:1:2:1:1:2:1:1:2:3:1:1:1:11:1:10:3:2:1:1:3:2:1:2:1:1:4:4:2:1:2:2:1:1:1:3:4:2:1:10:3:2:1:1:1:5:1:1:2:9:1:1:4:1:2:3:1:2:2:1:1:10:3:2:1:1:1:1:1:5:3:3:1:3:4:5:1:1:2:2:12:1:1:1:10:3:1:6:1:1:2:1:2:1:1:2:1:1:3:1:1:2:2:3:3:2:1:1:2:1:10:3:1:4:3:2:1:4:1:3:1:1:1:2:1:2:1:5:1:6:1:1:1:1:1:1:10:3:1:4:1:1:1:4:2:2:1:1:3:1:1:3:1:2:3:1:4:2:11:1:10:3:1:3:2:3:1:3:1:1:1:3:1:3:10:2:4:2:1:10:3:1:3:1:2:2:4:1:1:1:2:1:1:2:3:2:1:1:1:1:2:1:1:2:2:2:1:10:3:1:2:7:2:2:1:2:2:4:1:5:2:1:1:1:4:1:11:1:10:3:1:2:3:1:4:3:3:1:3:3:2:1:2:1:4:3:1:1:2:1:10:3:1:2:2:2:1:1:1:1:1:1:3:1:1:1:1:2:3:6:1:1:1:2:4:1:1:1:1:10:3:1:2:1:4:1:1:2:2:1:4:1:4:4:2:4:4:1:11:1:10:3:1:2:1:2:1:1:1:2:1:1:3:1:4:2:1:1:1:1:1:1:1:1:2:1:2:4:1:1:1:1:10:3:1:2:1:1:1:2:1:2:3:6:3:3:5:1:1:3:4:1:1:1:1:10:3:1:1:5:1:1:3:2:2:1:1:2:4:7:2:2:1:1:12:1:1:1:10:3:1:1:3:3:1:4:1:1:2:1:2:2:2:2:1:1:1:1:3:6:1:1:1:1:1:1:1:10:3:1:1:3:1:1:1:1:4:1:1:1:3:2:1:2:4:4:6:2:1:1:10:3:1:1:2:2:2:1:3:2:1:2:1:1:3:2:2:5:1:2:2:1:12:1:1:1:10:3:1:1:2:1:2:1:1:2:3:1:6:2:1:1:2:1:2:3:1:2:1:3:1:1:1:10:3:1:1:1:5:1:2:1:1:1:3:2:2:1:1:4:2:2:2:2:3:2:1:1:10:3:1:1:1:3:1:1:1:1:2:1:2:2:1:2:1:2:2:4:2:1:2:2:2:11:1:1:10:3:1:1:1:2:1:1:10:3:1:1:1:2:1:1:9:4:1:1:1:1:10:3:1:1:1:1:2:5:2:1:1:6:3:2:2:2:3:1:1:3:1:2:1:10:3:1:1:1:1:1:2:3:1:4:1:3:2:6:1:1:3:2:1:2:2:11:1:10:2:10:2:10:2:10:2:3:1:2:1:10:2:7:1:2:1:1:3:3:2:1:1:1:2:2:5:4:3:1:2:1:10:2:6:1:2:3:1:4:2:2:5:2:1:1:3:1:1:1:1:1:1:1:11:1:10:2:5:3:1:1:5:2:1:1:2:3:1:2:3:3:2:1:2:2:2:1:10:2:5:1:3:1:1:1:4:3:2:3:1:1:1:1:1:2:1:1:3:1:4:1:1:1:1:10:2:4:7:4:1:2:1:1:5:1:1:1:1:7:2:12:1:1:1:10:2:4:3:1:2:2:1:3:1:1:4:1:3:5:1:1:1:1:3:3:1:1:1:10:2:4:2:1:2:3:2:5:1:4:1:3:1:1:5:3:1:1:2:1:10:2:4:1:3:1:1:2:3:2:2:1:1:2:2:1:2:4:1:1:3:2:1:11:1:10:2:4:1:1:2:4:2:2:2:1:2:3:1:1:3:3:1:2:1:3:2:1:1:10:2:3:7:2:1:1:4:4:1:3:1:1:1:1:3:1:1:1:2:1:1:2:1:10:2:3:4:1:1:2:3:1:1:2:2:1:4:1:1:1:1:3:2:3:3:11:1:10:2:3:3:1:2:1:1:1:2:1:2:2:2:3:2:1:6:1:1:1:3:1:2:1:10:2:3:2:2:3:3:2:4:1:1:1:1:1:1:1:1:2:2:1:2:2:1:1:2:2:1:10:2:3:2:1:2:1:2:4:4:3:3:1:1:3:2:3:2:13:1:1:1:1:10:2:3:1:5:1:1:1:1:1:1:3:1:6:2:1:6:1:1:1:1:1:2:1:1:10:2:3:1:2:4:2:1:1:1:2:1:1:1:1:2:1:1:1:4:2:2:1:1:2:2:2:1:10:2:3:1:2:1:1:1:1:5:1:1:1:1:1:1:1:1:2:2:1:3:5:2:12:1:1:1:10:2:3:1:1:2:1:3:2:1:2:2:1:1:3:5:1:2:2:1:2:3:2:1:1:10:2:3:1:1:1:1:2:3:1:2:1:2:2:1:1:1:2:2:1:1:2:2:1:2:1:2:2:1:1:10:2:2:5:6:1:2:1:1:3:1:2:2:1:3:5:1:1:1:1:1:11:1:10:2:2:4:2:1:5:3:1:4:1:1:10:2:1:3:2:1:10:2:2:3:3:10:2:2:3:3:9:2:1:2:1:10:2:2:3:1:3:9:1:1:1:1:1:1:3:2:1:7:1:1:11:1:1:10:2:2:2:10:2:2:2:10:2:2:2:2:2:2:1:10:2:2:2:3:1:1:4:1:1:2:1:2:6:1:3:2:3:1:1:4:1:1:1:1:10:2:2:2:2:1:1:4:1:1:2:5:3:1:2:2:1:2:1:1:1:3:12:1:1:1:10:2:2:2:1:2:2:2:1:10:2:2:2:1:2:2:2:1:2:3:1:1:1:10:2:2:2:1:1:1:3:4:1:1:1:1:3:1:2:10:2:2:1:1:2:1:10:2:2:1:5:1:1:1:3:4:1:2:2:3:3:1:2:2:1:3:1:11:1:1:10:2:2:1:3:2:1:5:2:1:1:1:3:1:1:5:2:3:1:1:1:3:2:1:10:2:2:1:2:3:3:2:1:4:3:1:2:4:1:2:2:2:1:1:1:1:2:1:1:10:2:2:1:2:1:3:1:2:2:1:1:1:2:3:1:3:1:1:1:1:6:1:1:1:11:1:1:10:2:2:1:1:5:5:1:1:1:1:1:1:1:1:2:2:4:1:1:1:2:1:1:1:1:2:1:1:10:2:2:1:1:2:5:1:1:1:2:2:2:1:2:1:1:2:1:3:3:3:1:2:2:1:10:2:2:1:1:2:1:1:1:1:1:1:2:1:1:1:1:3:1:7:1:4:2:3:1:11:1:10:2:2:1:1:1:2:1:1:2:1:2:1:1:2:2:1:4:1:5:2:2:2:1:2:2:1:10:2:2:1:1:1:1:1:1:2:1:1:1:1:7:1:4:2:1:1:1:2:1:2:1:2:1:2:1:10:2:1:6:1:2:4:2:1:1:1:1:4:1:5:1:1:2:1:2:1:1:11:1:1:10:2:1:5:1:1:4:2:2:4:3:1:10:2:1:2:1:2:1:10:2:1:4:2:2:1:1:2:3:2:1:1:1:2:1:1:2:2:2:1:2:1:1:1:1:1:2:1:1:10:2:1:4:1:1:2:1:5:1:3:1:2:1:2:4:4:1:2:1:1:1:11:1:1:10:2:1:3:3:2:2:4:3:3:1:1:2:2:1:2:1:1:3:1:1:3:1:1:1:1:10:2:1:3:2:1:2:5:1:2:1:3:1:1:1:2:1:1:2:4:3:1:3:1:1:1:10:2:1:3:1:2:3:1:1:5:1:4:6:1:1:2:2:1:2:1:1:11:1:1:10:2:1:3:1:1:1:3:1:8:1:1:6:2:1:1:1:1:1:4:1:2:1:1:10:2:1:2:4:3:2:2:2:3:1:2:1:1:2:3:4:1:1:2:2:2:1:1:10:2:1:2:3:1:3:3:1:1:1:1:1:3:1:2:3:3:1:1:3:1:2:1:11:1:1:10:2:1:2:2:2:3:2:10:2:1:2:2:2:3:2:2:2:2:1:10:2:1:2:2:1:1:3:2:2:2:1:1:1:2:1:3:1:2:3:1:1:2:4:1:2:1:10:2:1:2:1:3:3:1:5:1:4:1:1:2:1:4:1:2:2:1:3:1:11:1:10:2:1:2:1:2:1:3:1:1:3:2:1:1:1:2:1:1:1:1:3:3:1:1:6:1:1:1:1:1:1:1:10:2:1:2:1:1:2:2:4:2:4:1:2:4:4:2:1:1:1:1:2:3:1:1:1:10:2:1:2:1:1:1:1:2:2:3:1:1:2:2:5:1:3:3:4:3:1:11:1:10:2:1:1:5:2:1:3:2:3:2:1:2:8:1:1:2:5:1:1:1:1:1:1:10:2:1:1:4:1:2:1:1:1:3:1:2:2:1:1:3:1:3:2:1:1:4:2:1:2:1:10:2:1:1:3:2:1:3:4:3:1:3:1:5:1:1:4:4:1:11:1:1:10:2:1:1:3:1:1:1:2:2:2:1:1:1:6:1:4:2:1:4:2:2:1:2:1:10:2:1:1:2:3:1:1:3:3:2:1:6:10:1:4:1:2:1:10:2:1:1:2:2:1:1:1:1:2:2:5:2:1:2:2:3:4:2:1:13:1:1:1:1:1:10:2:1:1:2:1:1:6:1:2:1:1:1:2:2:2:1:5:5:2:1:2:2:1:10:2:1:1:1:7:1:2:3:1:1:4:1:1:2:1:1:9:2:1:2:1:10:2:1:1:1:3:3:2:1:1:1:1:5:5:1:1:1:4:1:2:3:1:1:11:1:10:2:1:1:1:2:3:4:1:4:1:10:2:1:1:1:2:2:3:1:1:1:1:10:2:1:1:1:2:1:2:2:1:1:1:3:1:1:1:4:1:4:1:2:3:3:2:1:2:1:10:2:1:1:1:1:4:1:1:1:5:1:4:5:2:1:1:5:2:2:11:1:1:10:2:1:1:1:1:2:1:3:3:1:1:1:1:2:1:1:1:1:3:1:2:2:2:3:1:2:1:2:1:10:2:1:1:1:1:1:2:2:1:4:3:3:3:1:1:1:1:1:1:1:1:2:4:3:1:1:1:1:10:2:1:1:1:1:1:1:1:1:2:10:2:1:1:1:1:1:1:1:1:3:1:1:1:1:1:1:11:1:1:10:1:8:1:1:1:5:3:2:1:3:2:1:1:2:1:1:1:1:1:2:1:1:2:1:1:10:1:6:5:3:2:1:2:1:3:1:1:1:2:2:1:1:2:3:2:1:2:1:1:10:1:6:1:3:1:2:1:2:1:2:2:3:6:1:1:3:1:14:1:1:1:1:1:1:10:1:5:4:2:1:1:3:4:2:6:1:3:1:2:1:1:1:2:1:2:1:10:1:5:2:1:6:1:1:1:1:1:1:1:1:3:1:1:1:2:3:1:1:1:1:3:1:2:1:10:1:5:1:2:2:2:1:2:3:1:2:2:1:1:3:2:1:2:2:1:2:1:1:11:1:1:10:1:5:1:1:1:1:2:1:6:1:2:7:2:2:2:1:2:1:1:2:2:1:10:1:4:3:6:3:1:1:1:1:1:2:2:2:7:1:1:3:2:1:1:1:10:1:4:2:4:1:3:1:1:3:2:2:1:1:10:1:4:1:11:1:10:1:4:2:1:2:1:3:10:1:4:2:1:2:1:3:2:1:1:2:1:10:1:4:1:3:3:2:1:3:4:3:1:1:1:7:6:2:1:1:10:1:4:1:2:1:1:3:2:3:1:3:5:1:1:3:2:1:1:1:1:1:1:2:11:1:10:1:4:1:1:1:10:1:4:1:1:1:10:1:2:2:2:1:1:10:1:3:7:1:1:5:4:1:1:1:1:1:2:2:5:1:1:1:2:2:1:1:10:1:3:4:2:1:1:3:5:2:2:3:2:2:1:1:5:2:1:11:1:1:10:1:3:3:2:2:2:1:2:1:1:2:10:1:3:3:1:4:1:1:1:1:1:10:1:3:3:1:1:1:1:2:2:1:2:1:1:1:1:2:5:2:2:2:3:3:2:1:1:10:1:3:2:2:3:2:2:1:3:6:1:2:2:1:1:2:6:11:1:1:1:10:1:3:2:1:3:1:2:1:3:1:10:1:3:2:1:3:1:1:2:1:1:1:10:1:3:2:1:1:1:2:1:6:2:1:1:1:1:1:4:1:2:2:2:2:1:1:2:1:1:1:10:1:3:1:4:1:1:1:2:1:8:1:1:1:1:1:1:1:1:3:1:3:1:1:1:1:11:1:10:1:3:1:2:4:1:1:1:2:1:2:1:3:3:4:1:1:2:2:4:1:1:2:1:10:1:3:1:2:1:1:5:1:1:2:1:2:1:1:1:2:2:3:3:1:3:2:2:1:1:1:10:1:3:1:1:3:1:2:3:1:2:10:1:3:1:1:3:1:1:2:11:1:1:10:1:3:1:1:1:4:5:3:2:3:1:2:3:1:1:1:1:2:2:1:2:1:2:1:10:1:3:1:1:1:1:2:1:1:4:3:1:4:4:2:1:1:3:5:1:1:2:1:10:1:2:6:1:5:1:2:2:1:2:1:1:3:3:2:1:1:2:1:1:2:11:1:1:10:1:2:4:5:2:1:1:1:2:2:1:1:3:2:2:1:1:2:1:5:1:1:2:1:10:1:2:4:1:2:1:2:1:1:4:2:1:1:2:2:1:4:1:1:4:2:1:2:1:1:10:1:2:3:3:1:4:1:2:1:1:4:10:1:2:3:1:1:1:11:1:10:1:2:3:1:4:5:1:1:3:1:2:1:2:1:2:3:1:1:1:1:2:2:2:1:1:10:1:2:3:1:1:2:1:1:1:1:1:2:1:1:1:2:1:1:2:2:5:2:3:1:1:2:1:1:1:10:1:2:2:4:1:2:1:1:1:3:1:3:4:1:2:4:4:1:1:1:1:1:11:1:10:1:2:2:2:3:10:1:2:2:2:3:9:1:1:2:1:1:10:1:2:2:2:1:1:2:3:6:1:1:1:1:1:2:1:1:1:2:3:2:1:1:1:1:11:1:1:10:1:2:2:1:2:5:2:3:1:1:1:3:1:7:2:1:1:1:3:1:2:1:1:10:1:2:2:1:1:3:1:1:3:1:3:2:1:2:1:1:2:4:1:1:5:1:1:1:11:1:10:1:2:2:1:1:1:1:1:2:1:1:1:3:5:2:2:4:2:1:3:1:2:1:2:1:1:10:1:2:1:4:2:3:2:1:1:2:1:1:1:2:1:1:1:1:1:1:2:2:1:1:2:1:1:1:11:1:1:10:1:2:1:3:2:2:1:1:2:1:1:2:3:2:1:1:1:1:1:1:1:1:3:2:5:1:1:1:1:1:1:10:1:2:1:2:5:2:1:1:10:1:2:1:2:5:15:1:1:1:1:1:1:1:10:1:2:1:2:2:1:2:4:2:1:2:2:3:1:2:2:2:1:2:4:3:1:1:1:1:10:1:2:1:2:1:1:4:1:2:4:2:5:3:1:1:2:1:1:3:1:2:11:1:1:10:1:2:1:1:4:3:2:10:1:2:1:1:4:3:2:2:1:2:1:1:10:1:2:1:1:2:5:6:1:3:1:1:1:3:1:2:1:1:1:2:1:3:1:11:1:1:10:1:2:1:1:2:1:1:4:1:1:1:2:1:6:3:1:5:1:1:1:2:3:1:1:1:1:10:1:2:1:1:1:2:4:5:1:1:1:3:1:2:1:3:2:2:4:2:1:11:1:1:10:1:2:1:1:1:1:2:3:2:2:3:3:1:2:1:2:6:1:1:2:1:1:1:1:2:1:10:1:1:10:1:1:10:1:1:10:1:1:13:1:1:1:1:1:10:1:1:6:1:2:3:2:1:4:2:1:3:1:3:3:1:1:1:1:2:1:1:2:1:10:1:1:5:1:3:1:4:1:1:3:2:4:1:1:1:1:2:3:3:1:1:11:1:1:10:1:1:4:3:1:7:1:4:1:5:2:4:2:1:5:1:1:1:1:1:1:10:1:1:4:1:3:2:3:2:5:2:1:1:1:1:1:1:2:2:2:15:1:1:1:1:1:1:1:10:1:1:4:1:1:1:1:1:3:1:1:4:4:2:2:1:1:3:1:1:1:1:1:1:1:2:1:1:1:10:1:1:3:3:2:1:1:1:1:1:3:1:2:1:1:1:1:2:1:4:2:2:1:1:1:1:1:11:1:1:10:1:1:3:2:1:4:1:4:2:2:4:3:3:1:5:1:1:1:1:1:2:1:10:1:1:3:1:3:2:1:2:2:1:1:5:1:1:2:1:2:1:4:1:1:3:11:1:1:1:10:1:1:3:1:1:3:2:2:2:1:1:3:4:1:1:2:2:2:1:3:3:1:2:1:1:10:1:1:3:1:1:1:1:1:1:1:1:4:1:10:1:1:3:1:1:1:1:1:1:1:1:1:11:1:10:1:1:2:4:1:3:1:1:1:2:2:1:4:2:2:2:2:1:1:1:1:3:1:2:1:1:1:10:1:1:2:3:1:2:4:1:1:1:2:3:3:2:1:1:1:9:12:1:1:1:1:10:1:1:2:2:3:1:1:1:3:1:1:2:4:2:3:3:1:2:1:1:2:1:1:2:1:1:1:10:1:1:2:2:1:2:2:4:2:2:1:3:1:3:1:1:1:1:2:1:1:1:2:1:1:1:1:11:1:10:1:1:2:1:5:2:2:3:1:2:1:1:1:1:2:10:1:1:1:1:1:2:1:1:10:1:1:2:1:2:6:2:4:2:1:1:1:3:1:1:9:13:1:1:1:1:1:10:1:1:2:1:2:1:1:1:5:2:2:2:2:1:1:1:1:3:1:1:1:1:2:2:2:1:1:2:1:10:1:1:2:1:1:2:2:1:1:1:1:1:3:2:1:1:1:4:1:4:2:1:2:1:1:1:11:1:1:1:10:1:1:2:1:1:1:1:3:1:1:1:2:2:3:1:1:1:4:2:1:1:1:1:2:1:1:1:1:1:2:1:1:10:1:1:1:6:1:1:1:1:1:1:1:1:5:1:1:1:1:10:1:1:1:2:11:1:1:10:1:1:1:4:2:1:4:2:4:4:2:1:1:4:1:2:6:2:1:10:1:1:1:3:3:4:1:2:1:1:1:1:2:2:1:3:2:1:2:1:1:1:2:2:11:1:1:1:10:1:1:1:3:1:3:2:1:2:4:1:1:1:2:1:2:1:1:1:1:3:1:1:2:1:1:2:1:1:1:10:1:1:1:2:6:2:3:1:1:1:1:3:1:1:3:1:2:2:1:1:2:1:1:1:1:1:11:1:10:1:1:1:2:3:1:1:1:1:1:1:2:3:5:6:4:2:6:1:1:1:1:1:1:1:10:1:1:1:2:2:1:1:1:2:3:1:1:1:3:1:1:1:1:1:1:1:1:5:5:1:11:1:1:1:10:1:1:1:2:1:2:1:2:2:1:1:6:2:2:3:1:2:1:3:1:1:1:1:2:2:1:10:1:1:1:2:1:1:1:1:4:2:1:1:1:2:1:2:1:3:1:1:4:2:6:11:1:10:1:1:1:1:4:2:1:2:1:2:2:3:1:4:4:8:4:1:1:1:1:1:10:1:1:1:1:3:1:2:2:3:3:1:2:5:2:1:3:1:2:1:2:1:1:1:11:1:1:10:1:1:1:1:2:2:3:1:1:2:1:3:1:1:2:4:1:1:1:1:1:1:2:4:1:2:1:1:1:10:1:1:1:1:2:1:1:3:3:1:1:1:1:3:1:3:1:1:3:2:1:1:1:1:2:1:12:1:1:1:1:10:1:1:1:1:1:3:4:1:6:2:2:1:2:1:2:7:2:3:2:1:10:1:1:1:1:1:2:1:5:1:4:1:5:5:1:2:2:2:1:1:1:1:1:1:11:1:10:1:1:1:1:1:1:2:6:4:1:1:1:3:2:3:2:5:3:2:2:1:10:1:1:1:1:1:1:1:1:10:1:1:1:1:1:1:1:1:9:6:11:1:9:9:9:9:9:6:2:1:9:8:1:6:3:5:1:2:1:3:2:4:1:3:12:1:1:1:1:9:7:2:5:1:3:3:1:1:1:1:1:4:1:1:2:1:1:3:2:2:1:1:1:9:7:1:1:4:1:4:1:2:2:1:2:1:1:1:6:3:2:1:11:1:1:1:9:6:3:3:2:3:3:1:1:3:1:6:2:1:3:1:2:1:2:1:1:9:6:2:1:3:1:1:1:2:1:1:2:2:7:1:1:3:2:15:1:1:1:1:1:1:1:9:6:1:2:2:2:2:1:2:2:1:3:1:1:4:1:1:1:4:2:3:2:1:9:6:1:1:1:2:1:1:3:1:1:1:1:3:2:3:1:2:1:1:4:1:1:1:1:1:1:11:1:9:5:4:1:3:2:2:2:1:1:2:1:3:1:2:1:1:2:2:2:1:2:1:1:2:1:9:5:3:1:1:2:1:1:2:1:2:1:1:2:3:1:2:2:2:1:1:1:1:3:2:11:1:9:5:2:2:1:1:1:3:2:1:1:1:2:1:1:3:4:2:1:1:2:1:1:1:1:1:2:1:1:9:5:1:7:2:3:1:1:1:5:1:2:4:2:1:2:3:1:11:1:9:5:1:2:3:3:4:4:2:2:1:4:1:2:2:1:2:1:3:1:1:1:1:9:5:1:1:3:8:1:3:2:2:2:1:6:3:1:13:1:1:1:1:1:9:5:1:1:1:2:2:1:1:1:2:1:2:1:1:1:1:1:2:2:2:1:3:1:4:1:2:1:1:1:9:4:6:1:1:1:1:1:1:1:2:1:1:2:1:1:1:1:9:4:1:1:1:11:1:9:4:4:1:2:4:1:8:1:3:1:2:3:1:1:2:2:1:2:1:1:1:9:4:3:2:1:3:4:2:1:1:4:3:1:1:1:2:1:2:1:1:3:1:11:1:9:4:3:1:1:1:1:1:5:1:1:2:4:1:2:3:1:2:4:2:1:2:1:1:1:9:4:2:2:4:1:2:1:1:3:1:2:4:1:1:1:1:1:1:1:2:1:1:1:13:1:1:1:1:1:9:4:2:1:3:3:4:1:2:2:2:5:1:1:1:1:1:5:2:2:1:1:9:4:2:1:1:2:1:1:1:4:2:5:1:3:4:1:2:1:1:1:1:3:11:1:9:4:1:4:2:2:2:1:5:6:1:1:3:7:1:1:1:1:2:1:9:4:1:3:1:1:1:9:4:1:3:1:1:1:8:13:1:1:1:1:1:9:4:1:2:1:4:2:1:1:1:1:4:1:3:1:1:1:1:1:1:3:2:2:1:3:1:1:1:1:9:4:1:1:4:2:3:4:2:1:1:1:1:1:6:1:1:3:1:1:2:1:11:1:9:4:1:1:2:1:4:1:1:1:2:2:2:1:3:1:1:3:2:1:1:3:2:2:2:1:9:4:1:1:1:2:1:3:1:1:1:2:5:1:1:1:4:1:1:1:1:1:1:1:1:3:11:1:1:9:3:9:3:9:3:9:3:3:2:1:9:3:5:2:2:2:1:1:1:3:1:9:3:5:1:2:1:11:1:9:3:4:2:2:9:3:4:2:2:8:4:1:1:1:1:9:3:4:1:1:1:1:2:2:1:2:1:1:1:3:2:1:1:4:2:1:1:2:2:2:11:1:9:3:3:2:3:1:1:3:1:1:2:2:1:1:1:1:1:2:3:4:1:4:2:1:1:9:3:3:1:2:4:2:1:3:1:2:1:1:1:1:3:1:2:2:1:5:12:1:1:1:9:3:3:1:1:1:1:2:1:1:1:1:1:2:4:1:1:4:2:5:1:1:2:2:2:1:9:3:2:3:3:1:2:1:1:3:3:2:1:2:1:1:2:1:1:7:1:1:11:1:9:3:2:2:2:3:4:1:1:3:3:3:2:1:1:1:3:1:1:1:2:2:2:1:9:3:2:2:1:1:1:1:2:1:1:1:1:3:1:4:1:1:3:2:1:2:3:3:1:11:1:9:3:2:1:2:2:2:5:2:9:3:2:1:2:2:2:1:1:2:1:9:3:2:1:1:2:2:1:2:3:1:1:2:2:5:1:1:1:1:1:1:1:2:1:1:3:11:1:9:3:1:8:1:1:3:6:2:2:2:1:4:6:2:2:1:9:3:1:4:1:5:1:1:2:1:1:2:2:1:4:1:1:2:2:7:11:1:9:3:1:3:2:1:1:1:2:2:1:1:3:1:1:2:1:2:2:1:2:2:4:1:2:2:1:9:3:1:2:4:1:3:1:1:1:3:1:2:2:1:3:1:1:8:2:1:11:1:9:3:1:2:2:1:1:2:1:1:3:1:1:3:2:1:1:1:1:1:3:1:1:1:1:3:1:1:1:2:1:9:3:1:2:1:1:2:2:2:2:1:2:1:1:1:2:4:1:1:3:3:2:2:1:1:11:1:9:3:1:1:4:1:2:3:4:2:1:2:3:1:2:1:3:1:4:4:1:1:1:1:9:3:1:1:2:3:2:3:3:5:1:1:5:1:1:1:2:2:1:14:1:1:1:1:1:9:3:1:1:2:1:1:1:2:2:1:6:1:2:1:3:3:4:2:1:1:1:1:1:2:1:9:3:1:1:1:2:2:2:1:1:2:1:3:1:3:4:1:2:2:2:2:3:12:1:1:1:9:3:1:1:1:1:2:1:1:2:4:1:1:1:2:5:2:2:3:2:1:2:1:1:1:2:1:9:2:8:1:1:1:6:2:1:2:4:5:1:1:3:1:1:1:1:11:1:9:2:5:6:2:1:2:2:3:3:1:3:2:1:1:1:3:1:1:3:1:1:1:9:2:5:1:1:2:4:3:1:1:3:1:5:2:2:2:1:1:1:1:1:2:11:1:9:2:4:2:3:1:1:1:2:1:3:1:1:1:4:2:1:4:1:1:1:4:2:1:1:9:2:4:1:2:1:2:1:1:3:1:1:4:1:2:4:2:1:3:2:1:1:12:1:1:1:9:2:3:4:9:2:3:4:9:2:3:2:1:1:9:2:3:2:4:3:1:1:1:1:1:1:1:2:2:1:2:2:1:1:2:1:5:12:1:1:1:9:2:3:2:1:1:1:2:3:1:1:1:1:2:1:2:1:1:2:1:2:3:1:2:3:1:2:1:1:9:2:3:1:2:1:5:5:1:1:2:1:1:1:1:2:1:2:1:1:5:1:1:1:11:1:9:2:3:1:1:1:3:1:1:4:1:1:1:2:1:4:2:1:2:1:1:2:2:1:2:2:1:1:9:2:2:5:1:1:1:3:4:2:1:1:5:4:4:1:1:2:1:1:11:1:9:2:2:3:2:2:2:1:1:1:1:1:1:1:2:2:2:1:2:2:2:7:3:1:1:1:9:2:2:2:4:1:1:3:3:2:1:1:1:3:1:2:2:1:1:2:1:1:3:12:1:1:1:9:2:2:2:1:4:1:1:3:2:1:1:1:2:1:1:2:3:1:1:1:1:4:2:1:2:1:1:9:2:2:1:9:2:2:1:9:2:2:1:7:12:1:1:1:9:2:2:1:3:1:1:2:2:1:5:1:1:1:1:4:3:4:1:1:1:2:3:1:1:1:9:2:2:1:2:1:1:2:2:2:1:1:1:3:1:4:1:3:4:1:1:1:1:1:1:1:1:11:1:9:2:2:1:1:2:1:5:1:3:1:1:1:1:5:1:1:1:1:2:1:3:1:1:1:3:1:1:1:9:2:2:1:1:1:1:2:1:3:1:3:5:1:2:5:2:3:3:1:1:1:11:1:9:2:1:5:2:3:2:1:3:1:2:1:2:6:1:4:1:1:3:2:1:1:9:2:1:4:1:3:1:2:2:2:1:2:2:1:1:4:4:2:2:1:2:12:1:1:1:9:2:1:3:2:3:3:3:1:1:1:1:5:2:3:1:1:2:1:2:2:1:2:1:1:9:2:1:3:1:1:4:1:3:1:1:1:1:3:1:2:1:9:2:1:1:12:1:1:1:9:2:1:2:3:4:2:4:1:1:3:1:1:2:2:4:4:1:2:1:1:2:1:9:2:1:2:2:1:4:2:3:3:5:2:2:2:4:1:3:1:1:11:1:1:9:2:1:2:1:2:4:1:2:3:1:4:3:1:2:7:1:1:2:1:1:2:1:9:2:1:2:1:1:1:3:3:1:9:2:1:2:1:1:1:3:3:1:1:1:1:11:1:9:2:1:1:4:3:1:2:1:1:1:5:1:2:1:2:1:3:1:1:3:4:1:2:1:9:2:1:1:3:1:2:3:1:1:1:5:1:4:2:2:5:5:1:11:1:1:9:2:1:1:2:2:2:1:2:1:3:2:2:1:5:9:2:1:1:1:2:1:1:9:2:1:1:2:1:1:1:3:5:7:4:1:3:1:2:2:1:2:1:11:1:1:9:2:1:1:1:3:1:2:1:1:2:2:2:3:1:2:2:1:1:2:1:2:1:3:3:1:2:1:9:2:1:1:1:2:1:1:1:1:2:2:1:3:1:1:3:3:4:2:1:1:5:1:1:11:1:9:2:1:1:1:1:1:4:4:3:2:2:2:4:2:2:1:2:3:1:1:3:1:1:1:9:1:9:1:9:1:9:1:10:1:11:1:9:1:6:2:2:3:3:2:9:1:6:2:1:4:1:1:1:1:1:9:1:5:3:1:1:1:9:1:5:3:1:1:2:1:1:1:1:2:1:1:11:1:9:1:5:1:1:2:5:3:1:1:1:2:1:1:1:3:1:7:2:2:2:1:1:9:1:4:3:1:3:1:1:7:1:1:1:2:1:9:1:3:1:1:11:1:9:1:4:2:1:1:1:1:1:1:1:3:1:1:2:9:1:4:1:4:2:1:1:1:9:1:4:1:1:3:1:3:2:3:3:1:1:1:2:2:1:1:1:4:3:1:1:1:11:1:9:1:3:5:1:2:2:3:1:1:1:4:3:9:1:2:2:2:1:1:9:1:3:3:1:2:3:1:4:1:3:1:1:1:1:1:1:1:3:1:2:1:2:13:1:1:1:1:1:9:1:3:2:2:1:1:3:2:1:1:2:1:5:2:1:1:1:1:4:1:2:2:1:2:1:1:9:1:3:1:5:2:3:2:1:1:1:5:1:2:1:4:1:1:2:1:2:1:11:1:1:9:1:3:1:2:2:1:1:4:3:3:1:1:2:2:3:2:2:2:3:3:1:1:1:1:9:1:3:1:1:2:2:1:1:1:3:1:1:1:1:4:2:1:1:2:1:2:2:1:2:1:2:11:1:1:9:1:2:9:1:2:9:1:2:9:1:2:1:2:1:1:1:9:1:2:4:2:1:5:2:2:2:5:1:1:1:1:4:2:3:1:1:11:1:1:9:1:2:3:2:2:1:1:2:6:6:1:1:2:1:2:4:3:1:1:2:1:9:1:2:2:6:2:1:1:3:1:3:2:4:3:1:5:1:1:1:1:1:11:1:9:1:2:2:2:2:1:1:1:4:1:1:2:3:2:1:1:1:1:5:1:1:1:1:1:1:1:2:1:1:9:1:2:2:1:2:1:2:1:1:1:1:2:5:1:1:1:1:1:1:1:3:2:3:2:2:1:11:1:9:1:2:1:5:3:2:3:2:1:3:1:4:1:1:2:1:3:3:1:1:1:2:1:9:1:2:1:3:1:2:1:1:2:3:5:3:2:2:1:2:1:6:2:11:1:1:9:1:2:1:2:1:3:2:1:1:1:4:2:1:1:4:1:1:2:1:1:1:1:1:6:1:1:1:1:1:1:1:9:1:2:1:1:3:2:2:4:1:1:1:4:2:2:1:4:1:7:1:1:11:1:9:1:2:1:1:1:4:3:1:1:1:1:1:2:2:1:3:2:1:4:5:2:1:1:2:1:9:1:2:1:1:1:1:1:2:2:1:6:1:1:1:1:2:1:1:2:5:1:2:2:12:1:1:1:1:9:1:1:5:3:1:2:2:1:2:2:3:1:1:3:3:2:1:1:2:2:3:1:1:1:1:9:1:1:4:2:1:4:3:2:3:1:2:4:5:1:2:2:2:11:1:1:1:9:1:1:3:4:2:1:3:3:1:3:2:1:2:2:1:1:1:1:2:1:1:3:2:1:1:1:9:1:1:3:2:1:1:1:1:3:1:2:1:2:1:3:2:1:9:1:1:2:1:1:11:1:9:1:1:3:1:1:1:3:3:1:2:1:1:3:2:1:1:4:4:3:1:1:3:1:1:1:1:9:1:1:2:3:3:1:1:3:2:1:3:2:4:1:1:1:1:1:1:1:4:2:1:1:11:1:9:1:1:2:2:2:1:1:3:2:2:1:3:9:1:1:2:2:1:3:1:2:1:1:9:1:1:2:1:3:3:2:1:1:1:3:4:4:1:4:1:1:1:2:3:1:1:11:1:9:1:1:2:1:1:3:2:9:1:1:2:1:1:3:2:8:1:1:2:1:1:9:1:1:1:9:1:1:1:9:1:1:1:9:1:1:1:12:1:1:1:1:9:1:1:1:4:1:1:2:2:3:4:2:1:1:1:3:1:1:2:1:1:6:1:2:1:1:9:1:1:1:3:1:1:2:6:1:2:2:2:3:2:1:3:2:1:1:2:1:2:11:1:9:1:1:1:2:2:2:1:1:2:9:1:1:1:2:2:2:1:1:2:5:1:1:2:1:9:1:1:1:2:1:1:2:3:1:1:4:1:1:1:1:1:1:1:5:4:2:1:2:1:1:11:1:1:9:1:1:1:1:3:3:2:1:1:1:1:2:1:3:4:5:4:1:4:1:2:1:9:1:1:1:1:2:1:4:1:4:5:2:2:1:3:2:6:1:1:1:1:11:1:1:9:1:1:1:1:1:2:5:4:1:1:3:2:2:2:8:6:2:1:9:1:1:1:1:1:1:1:9:1:1:1:1:1:1:1:9:1:1:1:1:1:1:1:1:11:1:1:1:8:8:8:8:8:8:3:1:1:1:1:8:7:1:5:3:4:1:2:1:2:2:4:1:1:1:1:2:2:1:11:1:1:1:8:6:2:4:1:3:2:1:1:1:1:1:1:2:1:1:1:1:2:2:1:1:3:1:1:1:2:1:1:8:6:1:1:3:1:3:3:1:6:2:3:3:19:1:1:1:1:1:1:1:1:1:1:1:8:5:3:2:2:3:1:1:1:1:1:3:1:1:4:1:1:1:1:3:4:1:1:2:1:1:8:5:2:1:2:1:1:1:1:1:3:1:2:1:1:2:1:5:3:2:1:2:2:11:1:1:8:5:1:2:1:2:1:3:6:2:2:4:4:1:1:1:2:2:3:1:1:1:1:8:5:1:1:1:1:1:1:2:1:1:1:8:5:1:1:1:1:1:1:2:1:1:2:1:11:1:8:4:3:4:1:2:2:1:4:1:1:3:1:1:2:2:1:1:2:2:5:1:1:1:1:1:1:8:4:2:3:5:2:1:1:1:1:5:1:1:6:2:2:2:13:1:1:1:1:1:8:4:2:1:2:1:1:7:1:2:4:3:2:5:3:1:3:2:1:8:4:1:3:2:4:2:3:1:1:8:4:1:3:2:1:12:1:1:1:1:8:4:1:2:1:1:2:1:2:3:2:3:8:4:1:2:1:1:1:2:2:1:8:4:1:1:1:4:2:1:1:2:1:1:1:3:1:1:2:1:3:1:1:1:1:1:1:1:3:1:11:1:8:3:6:2:1:2:4:1:1:5:1:4:1:3:1:1:1:1:3:2:1:1:8:3:4:1:2:4:2:2:1:3:1:1:1:4:3:1:2:2:1:2:12:1:1:1:8:3:3:2:1:1:1:4:4:3:1:1:5:8:3:1:2:1:1:8:3:2:4:4:2:1:8:3:2:4:4:1:14:1:1:1:1:1:1:8:3:2:2:1:2:8:3:2:2:1:2:9:1:1:1:1:1:1:2:1:8:3:2:1:1:8:3:2:1:1:8:3:2:1:1:3:12:1:1:1:8:3:1:5:1:2:4:1:3:8:3:1:5:1:2:3:1:1:1:8:3:1:3:1:1:2:2:1:1:3:4:2:1:2:1:2:2:2:1:1:1:2:1:1:1:11:1:8:3:1:2:1:2:3:1:4:1:2:1:1:1:1:3:1:1:1:2:1:1:1:5:1:2:1:1:8:3:1:1:3:1:3:4:1:1:5:2:1:6:1:4:4:11:1:1:8:3:1:1:1:3:3:1:3:2:2:1:2:3:4:1:1:1:2:1:1:2:1:1:1:2:1:8:3:1:1:1:1:1:1:2:1:8:3:1:1:1:1:1:1:2:1:7:13:1:1:1:1:8:2:5:2:1:2:2:1:3:1:4:3:2:1:1:2:2:2:4:2:2:1:8:2:4:1:6:1:3:2:1:1:1:4:1:1:1:1:1:1:2:2:1:1:1:3:11:1:8:2:3:3:2:2:3:1:1:3:1:1:1:4:1:1:2:1:3:2:1:1:3:2:1:8:2:3:1:3:2:2:2:1:1:4:5:1:1:3:3:2:1:1:3:1:11:1:8:2:2:8:2:2:8:2:2:8:2:1:3:2:1:8:2:2:3:1:1:3:5:3:2:3:4:1:1:1:2:6:1:11:1:1:8:2:2:2:1:1:4:1:1:1:2:1:2:3:1:1:1:4:2:5:4:2:1:1:8:2:2:1:2:2:1:8:2:2:1:2:2:1:8:2:1:1:12:1:1:1:8:2:2:1:1:1:3:1:1:2:2:1:2:1:1:2:1:1:4:1:2:8:1:1:2:1:8:2:1:4:4:1:1:1:1:1:1:2:3:1:1:2:1:1:8:2:1:1:1:11:1:1:8:2:1:3:1:5:1:1:1:2:1:4:1:2:2:3:2:4:3:1:2:2:1:8:2:1:2:2:5:3:1:1:1:3:1:2:4:2:2:3:1:1:4:11:1:1:8:2:1:2:1:1:5:1:1:1:1:2:1:1:2:1:2:2:1:2:5:1:2:3:1:2:1:8:2:1:1:3:3:5:2:5:1:1:1:1:1:1:2:4:1:2:1:1:1:1:11:1:1:8:2:1:1:2:1:3:1:1:1:1:2:1:1:3:1:1:3:5:1:2:3:2:1:1:2:1:1:8:2:1:1:1:2:2:1:2:3:4:6:4:1:1:6:1:1:1:1:11:1:1:8:2:1:1:1:1:1:1:2:8:2:1:1:1:1:1:1:2:8:1:4:1:2:1:8:1:6:1:1:1:3:3:2:1:1:2:1:1:1:2:2:2:1:3:2:1:2:1:1:11:1:8:1:4:5:1:2:1:1:3:2:1:1:1:1:5:2:2:2:1:1:1:3:1:2:1:8:1:4:1:2:3:1:8:1:4:1:2:3:1:7:2:11:1:1:8:1:3:3:6:1:1:1:1:2:1:2:3:3:1:1:2:5:1:1:1:1:2:1:1:8:1:3:2:1:1:5:1:1:3:3:1:1:1:1:8:1:3:1:3:1:11:1:8:1:3:1:1:5:2:1:2:4:2:3:1:1:2:1:1:3:3:2:1:2:1:1:1:8:1:2:5:2:1:2:1:2:3:1:1:1:1:1:1:3:2:1:1:1:2:3:1:2:11:1:1:8:1:2:3:1:2:1:2:8:1:2:3:1:2:1:2:7:1:2:1:1:1:8:1:2:2:1:4:2:2:1:1:8:1:2:2:1:4:2:2:1:1:1:11:1:1:8:1:2:1:3:8:1:2:1:3:8:1:2:1:3:3:1:1:2:1:8:1:2:1:2:1:1:1:1:2:2:1:2:1:5:1:1:1:2:1:1:2:1:2:3:2:11:1:1:1:8:1:2:1:1:1:2:1:1:2:3:2:2:1:1:4:3:1:3:2:6:1:2:1:1:8:1:1:5:1:1:1:1:1:1:4:1:1:1:8:1:1:4:17:1:1:1:1:1:1:1:1:1:8:1:1:3:2:3:3:2:1:3:1:6:2:2:2:1:2:1:6:1:1:1:1:1:1:1:8:1:1:2:5:1:3:2:1:2:2:1:4:1:1:1:2:1:2:1:2:1:1:2:1:11:1:1:8:1:1:2:2:1:1:2:1:1:1:2:1:2:1:3:2:2:3:2:1:1:5:3:2:1:8:1:1:2:1:1:1:3:2:1:1:2:2:1:2:1:1:4:1:1:1:1:1:1:4:2:1:11:1:1:1:8:1:1:1:3:2:2:1:1:3:1:1:2:2:1:3:3:8:1:1:1:1:2:1:1:1:8:1:1:1:2:1:3:1:8:1:1:1:2:1:3:1:8:1:1:1:1:1:1:11:1:8:1:1:1:1:2:4:4:1:3:2:1:3:1:1:1:3:3:2:3:1:3:1:1:1:1:8:1:1:1:1:1:1:8:1:1:1:1:1:1:8:1:1:1:1:1:2:1:1:1:1:1:1:11:1:1:7:7:7:7:7:7:6:1:2:1:7:6:1:4:3:3:1:2:1:1:2:3:2:1:1:1:4:1:2:1:2:11:1:7:5:2:3:1:2:7:5:2:3:1:2:7:1:1:1:2:1:7:5:1:1:2:1:3:1:1:1:2:2:2:1:2:2:1:1:1:1:3:1:2:2:2:11:1:1:7:4:3:1:2:2:1:1:3:7:4:3:1:2:2:1:1:2:1:2:1:1:1:7:4:1:7:4:1:7:4:1:7:4:1:1:11:1:7:4:1:1:3:1:1:1:6:1:2:2:2:1:2:1:2:4:2:4:1:2:1:7:3:5:1:1:1:1:1:2:1:2:1:1:1:7:3:4:7:11:1:7:3:3:1:1:3:1:2:2:5:2:1:1:4:7:3:1:1:2:1:1:7:3:2:1:3:3:1:1:1:1:1:2:1:2:3:1:2:1:7:3:1:1:1:11:1:7:3:1:3:4:1:1:1:1:4:1:2:3:2:4:2:2:1:4:1:1:2:1:7:3:1:1:7:3:1:1:7:3:1:1:7:2:3:10:1:1:7:3:1:1:1:1:4:4:2:1:3:2:1:1:1:3:1:1:3:3:2:1:1:1:2:1:7:2:4:4:1:2:1:1:1:1:5:1:3:2:2:1:4:2:1:1:3:2:1:7:2:3:2:2:1:1:1:1:1:2:2:1:2:2:7:2:3:2:1:2:1:10:1:1:7:2:2:4:1:1:2:6:1:1:1:1:1:2:2:2:1:1:4:1:5:2:1:1:7:2:2:1:4:1:1:2:1:2:3:4:1:5:7:2:1:2:2:1:1:7:2:1:7:2:1:7:2:1:7:2:1:6:2:1:10:1:7:2:1:2:7:2:1:2:7:2:1:2:7:2:1:1:1:2:1:1:7:2:1:1:7:2:1:1:7:2:1:1:7:2:1:1:3:2:1:1:1:7:2:1:1:1:4:2:3:3:1:3:7:2:1:1:1:4:2:1:1:10:1:1:7:1:7:1:7:1:7:1:7:1:6:3:1:1:1:1:7:1:4:2:2:1:3:2:1:1:3:5:1:1:1:2:1:2:2:1:1:1:1:1:1:2:1:1:7:1:3:2:3:4:2:1:5:1:1:1:1:1:3:1:2:2:1:2:1:1:1:1:1:10:1:7:1:3:1:1:1:2:1:3:4:1:1:2:3:2:2:4:6:3:1:2:1:7:1:2:2:3:1:4:1:1:2:1:1:1:2:1:7:1:2:2:3:1:1:1:2:1:1:7:1:2:1:2:3:1:1:2:7:1:2:1:2:3:1:1:2:6:1:1:10:1:1:7:1:1:7:1:1:7:1:1:7:1:1:7:1:1:2:1:2:1:1:7:1:1:3:1:2:1:5:2:2:1:2:4:3:7:1:1:2:2:1:2:1:7:1:1:2:1:2:2:1:1:3:1:7:1:1:2:1:2:2:1:1:3:1:3:1:10:1:1:7:1:1:1:2:3:4:3:1:4:1:2:2:2:1:1:1:1:1:5:1:2:2:1:1:1:7:1:1:1:1:1:7:1:1:1:1:1:7:1:1:1:1:1:6:7:1:1:1:1:1:1:1:1:6:6:6:6:6:6:10:11:1:1:1:1:6:5:1:3:3:2:1:1:4:2:3:1:1:1:1:2:1:1:1:3:1:2:1:1:2:1:1:6:4:2:2:1:2:1:4:1:1:1:3:2:1:1:1:1:2:1:1:2:2:3:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:3:2:1:10:1:6:3:2:3:1:1:1:3:6:3:2:3:1:1:1:3:5:3:2:1:6:3:1:2:2:3:5:1:1:3:1:1:1:1:2:1:3:2:2:1:1:4:1:2:1:6:2:6:2:6:2:6:2:6:2:5:12:1:1:1:1:6:2:3:1:1:3:2:1:1:1:6:2:3:1:1:3:2:1:1:1:6:1:1:1:1:1:1:1:6:2:2:1:1:3:3:1:4:6:2:2:1:1:3:3:4:3:1:1:1:1:6:2:1:2:1:6:2:1:2:1:6:2:1:2:1:6:2:1:2:1:10:1:6:2:1:1:1:1:2:6:2:1:1:1:1:2:6:2:1:1:1:1:2:4:1:2:1:1:6:1:4:1:1:1:1:3:1:1:6:1:4:1:1:1:1:3:1:1:5:2:1:2:1:6:1:2:3:6:1:2:3:6:1:2:3:6:1:1:12:1:1:1:1:1:6:1:2:1:1:2:2:1:3:1:1:1:1:3:4:2:3:1:6:1:3:1:2:1:1:6:1:1:2:2:6:1:1:2:2:6:1:1:2:2:6:1:1:1:2:2:1:1:6:1:1:1:1:6:1:1:1:1:6:1:1:1:1:6:1:1:1:1:5:1:10:1:1:1:5:5:5:5:5:5:5:5:5:1:1:2:1:1:5:4:1:2:3:1:1:1:2:1:1:3:2:1:5:4:1:2:2:3:2:2:1:5:3:2:1:1:1:2:1:2:5:3:2:1:1:1:2:1:2:5:3:1:1:1:10:1:5:2:4:1:1:1:2:2:1:1:5:2:4:1:1:1:2:2:1:1:4:2:1:2:1:5:2:1:3:5:2:1:3:5:2:1:3:5:2:1:3:1:1:2:1:1:5:1:5:1:5:1:5:1:5:1:5:1:5:1:3:1:1:10:1:5:1:2:2:1:1:1:3:2:5:1:2:2:1:1:1:3:2:5:1:1:3:1:2:1:5:1:1:1:5:1:1:1:5:1:1:1:5:1:1:1:5:1:1:1:4:1:1:2:1:1:4:4:4:4:4:4:4:4:4:4:4:1:1:1:10:1:4:3:1:1:2:1:4:3:1:1:2:1:4:3:1:1:2:1:4:2:2:3:1:1:1:4:2:1:1:2:4:2:1:1:2:4:2:1:1:2:4:2:1:1:2:3:2:2:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:1:2:10:1:1:3:3:3:3:3:3:3:3:3:3:3:3:3:3:1:1:2:1:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:2:1:1:2:1:1:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:1:2:1:10:1:1:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:5:5:2:2:6:2:6:2:2:3:3:4:2:2:2:4:2:4:2:2:2:3:2:2:3:6:2:2:3:2:3:2:3:2:4:3:5:7:5:3:5:2:2:3:2:4:2:3:2:6:3:3:3:2:2:2:2:2:2:3:3:2:5:3:2", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":86,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":859,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.788425664621422e-83,0,0,-1.4902745963168196e-98,0,0]]] +Shrunk 27238 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 366 tests +{ seed: 12345, path: "365:0:1:12:1:1:1:1:3:4:2:4:2:1:2:1:2:3:1:1:4:2:1:1:2:1:1:12:1:12:7:1:5:3:4:1:2:1:2:2:4:1:1:1:1:2:1:1:2:1:1:1:12:7:1:3:2:3:1:1:1:4:2:1:1:3:2:5:1:2:1:1:2:1:12:7:1:2:3:2:2:2:1:3:2:1:4:2:7:13:1:1:1:1:12:7:1:2:1:2:2:1:1:4:2:2:1:1:1:2:3:1:1:1:1:2:1:1:2:1:1:12:7:1:1:4:1:4:1:2:2:1:2:1:1:1:6:3:1:3:1:1:1:1:12:7:1:1:2:2:1:2:2:2:2:4:2:4:1:2:2:3:12:1:1:12:7:1:1:1:3:1:1:3:4:2:1:5:3:2:1:3:1:1:2:1:1:12:7:1:1:1:1:2:1:1:1:3:1:1:1:1:1:4:2:4:1:1:5:2:1:12:6:6:6:6:6:6:3:13:1:1:1:1:12:6:5:1:4:2:3:2:1:2:2:2:1:2:1:4:1:1:2:1:1:1:12:6:4:2:3:3:2:1:1:1:6:1:1:1:1:1:5:1:1:1:2:1:12:6:4:1:1:3:1:2:1:1:1:3:3:1:3:4:1:1:1:2:1:12:1:1:12:6:3:3:2:3:3:1:1:3:1:6:2:1:2:5:1:1:1:1:1:1:12:6:3:2:1:2:2:1:1:3:4:1:8:1:1:2:2:1:2:1:1:12:6:3:1:2:2:1:2:1:1:1:1:1:2:3:1:3:2:2:1:3:1:12:1:1:1:12:6:3:1:1:1:1:6:1:1:2:5:1:2:1:2:4:2:1:2:1:12:6:2:4:1:3:2:1:1:1:1:1:1:2:1:1:1:1:2:2:1:1:2:2:1:2:1:1:12:6:2:3:1:1:2:2:1:3:1:2:2:2:1:1:4:1:1:1:2:1:1:1:12:1:12:6:2:2:2:1:1:5:4:1:1:2:1:1:2:1:1:3:1:2:2:2:1:12:6:2:2:1:1:1:1:2:1:6:2:3:1:1:1:5:3:1:1:2:1:1:12:6:2:1:3:1:1:1:2:1:1:2:2:7:1:1:3:2:4:12:1:12:6:2:1:1:8:2:1:3:1:1:1:1:1:2:4:6:2:1:12:6:2:1:1:1:5:1:1:2:3:3:1:1:1:2:3:7:2:1:12:6:1:8:1:2:2:3:1:1:4:2:2:1:2:5:12:1:12:6:1:4:3:3:1:2:1:1:2:3:2:1:1:1:4:1:1:4:1:1:1:1:1:12:6:1:3:4:1:3:1:2:1:1:1:2:1:2:2:3:1:2:2:2:2:1:12:6:1:3:1:2:8:2:3:10:3:13:1:1:1:1:12:6:1:2:4:3:1:2:2:1:2:1:2:1:3:1:2:2:2:1:1:1:1:2:1:12:6:1:2:2:2:2:1:2:2:1:3:1:1:4:1:1:1:3:3:1:2:1:1:1:12:6:1:2:1:3:1:2:5:1:1:2:11:4:1:12:1:12:6:1:2:1:1:2:1:1:2:1:3:2:1:1:1:1:8:2:2:1:1:2:1:1:12:6:1:1:4:5:3:4:1:1:1:2:2:1:3:1:3:1:2:1:1:1:12:6:1:1:3:1:3:3:1:6:2:3:4:1:1:1:1:1:1:1:1:12:1:12:6:1:1:2:2:2:5:1:1:8:1:1:1:2:1:2:1:1:1:1:2:1:1:12:6:1:1:2:1:1:2:1:5:1:6:2:1:1:1:2:2:1:4:2:1:12:6:1:1:1:3:1:11:6:1:1:1:3:1:3:1:12:1:12:6:1:1:1:2:1:1:3:1:1:1:1:3:2:3:1:2:1:1:4:1:1:1:1:1:2:1:12:6:1:1:1:1:2:1:2:1:1:1:1:2:1:3:1:1:2:1:1:2:3:1:1:2:1:2:1:1:12:6:1:1:1:1:1:1:1:1:2:1:1:1:1:10:5:6:12:1:12:5:7:1:1:1:1:1:1:1:1:1:2:1:1:1:2:1:1:1:1:1:8:2:1:12:5:5:5:5:5:5:5:4:1:2:1:1:1:12:5:5:1:3:5:2:1:2:1:1:1:1:2:2:2:1:2:1:3:12:1:12:5:4:5:1:3:2:2:5:1:2:1:2:1:4:1:3:1:1:1:1:12:5:4:2:2:4:6:1:2:3:2:1:1:3:1:2:2:2:1:12:5:4:1:3:2:2:2:1:1:2:1:3:1:2:1:1:2:2:2:1:1:1:1:12:1:12:5:4:1:1:2:1:3:1:1:1:1:2:1:1:1:1:1:1:1:2:2:1:2:2:2:2:1:12:5:3:5:1:1:1:2:4:2:1:2:5:2:4:3:2:1:12:5:3:3:1:4:1:3:1:6:3:1:3:2:2:1:1:1:12:1:1:12:5:3:2:2:3:1:1:1:1:1:3:1:1:3:8:4:1:2:1:12:5:3:2:1:1:2:1:3:2:2:1:1:1:1:1:5:1:1:1:2:1:1:1:1:2:1:1:12:5:3:1:3:1:4:1:4:2:4:1:1:1:1:2:1:4:1:1:1:12:1:12:5:3:1:2:1:1:2:1:3:1:1:2:1:2:3:2:1:1:1:1:2:2:3:1:1:1:1:12:5:3:1:1:2:1:1:2:1:2:1:1:2:3:1:2:2:2:1:1:1:1:5:1:1:1:1:1:1:12:5:2:11:5:2:12:2:2:12:1:12:5:2:4:4:2:1:2:3:1:1:4:1:1:2:2:1:4:2:1:1:12:5:2:3:4:2:2:1:2:1:2:1:3:1:2:3:2:2:2:1:2:1:12:5:2:3:1:2:7:5:2:3:1:2:6:1:12:1:1:1:12:5:2:2:4:2:2:2:1:1:1:3:2:1:2:1:2:2:1:1:2:1:1:1:2:1:12:5:2:2:2:2:1:2:2:1:1:4:1:1:3:1:1:2:5:1:2:2:1:12:5:2:2:1:2:7:2:1:1:1:1:1:1:1:4:2:1:2:2:2:12:1:12:5:2:2:1:1:1:3:2:1:1:1:2:1:1:3:4:2:1:1:1:6:1:1:1:1:1:1:1:12:5:2:1:4:2:2:1:1:4:1:1:2:1:3:1:2:3:2:2:1:2:1:12:5:2:1:3:1:1:4:2:6:1:10:3:13:1:1:1:1:12:5:2:1:2:2:1:2:1:1:6:1:3:1:3:2:1:3:5:1:1:1:1:1:1:12:5:2:1:2:1:1:1:1:1:3:1:2:1:1:2:1:5:3:2:2:4:1:1:1:1:1:12:5:2:1:1:2:5:2:1:4:1:2:1:1:3:3:2:2:1:1:12:1:1:1:12:5:2:1:1:1:4:4:1:5:2:4:1:3:2:1:4:2:1:12:5:2:1:1:1:1:3:1:1:2:1:2:5:6:1:1:1:2:1:1:1:1:1:2:1:12:5:1:7:2:3:1:1:1:5:1:2:4:2:1:2:1:1:12:1:1:1:12:5:1:5:2:1:2:1:3:4:1:2:1:1:1:1:3:4:2:2:1:1:12:5:1:4:2:5:2:1:2:4:4:1:4:1:1:2:2:2:1:12:5:1:4:1:1:3:1:1:1:1:11:5:1:3:1:1:1:12:1:12:5:1:3:3:2:1:1:4:2:3:1:1:1:1:2:1:1:1:3:1:1:1:1:2:1:1:12:5:1:3:2:1:1:2:3:2:3:1:3:1:5:2:1:6:1:1:1:1:1:1:1:12:5:1:3:1:2:1:1:2:1:3:1:2:2:2:3:1:3:1:2:2:13:1:1:1:1:12:5:1:2:9:1:1:2:2:1:2:1:6:1:3:1:1:3:1:1:1:1:12:5:1:2:3:3:4:4:2:2:1:4:1:2:2:1:3:1:2:1:12:5:1:2:2:4:1:1:1:2:2:4:2:1:4:1:1:4:1:1:1:12:1:1:12:5:1:2:2:1:2:2:1:4:2:2:2:3:1:1:1:1:2:1:1:2:2:2:1:12:5:1:2:1:4:1:1:2:4:2:1:2:7:1:1:3:2:2:1:1:12:5:1:2:1:2:1:3:6:2:2:4:4:1:1:1:2:2:1:12:1:12:5:1:2:1:1:2:2:2:1:3:3:1:3:2:2:1:1:3:1:1:1:2:2:1:12:5:1:2:1:1:1:1:1:3:3:3:2:3:2:1:3:3:6:1:1:1:1:1:1:1:12:5:1:1:5:1:1:3:1:7:4:1:1:1:2:1:1:3:13:1:1:1:1:12:5:1:1:3:8:1:3:2:2:2:1:6:3:1:3:1:1:1:1:12:5:1:1:3:1:3:6:3:1:1:4:2:4:1:1:2:1:1:2:1:1:12:5:1:1:2:5:1:1:1:1:3:2:1:3:2:3:2:1:1:1:2:1:1:1:12:1:12:5:1:1:2:2:2:2:1:1:4:1:5:1:1:3:1:2:5:1:2:1:12:5:1:1:2:1:3:1:1:1:2:2:2:1:2:2:1:1:1:1:3:1:2:2:1:1:1:2:1:12:5:1:1:2:1:1:1:3:1:3:2:2:2:2:1:1:2:1:1:3:1:2:1:1:11:1:1:11:5:1:1:1:4:2:1:1:5:6:1:2:3:1:1:1:3:2:1:2:1:12:5:1:1:1:2:2:1:1:1:2:1:2:1:1:1:1:1:2:2:2:1:3:1:3:1:1:1:2:1:12:5:1:1:1:1:6:2:1:2:1:3:1:2:2:1:1:1:1:2:2:2:1:2:1:1:1:12:5:1:1:1:1:2:3:1:2:1:2:1:1:2:3:1:1:1:1:1:2:1:2:14:1:1:1:1:1:1:11:5:1:1:1:1:1:3:2:2:1:1:2:2:3:2:1:1:1:4:1:2:1:2:1:2:1:12:4:8:3:1:3:5:2:2:1:5:2:1:2:3:1:1:1:1:12:4:7:1:2:1:1:2:3:2:1:3:1:1:2:1:2:4:1:1:1:1:2:1:12:4:6:2:1:2:1:2:1:1:1:1:1:2:2:1:1:1:3:3:2:1:12:1:1:1:1:11:4:6:1:1:1:1:1:1:1:2:1:1:2:1:1:1:1:9:4:3:1:1:1:1:12:4:5:1:3:5:2:3:1:4:1:1:2:5:2:3:1:1:1:1:12:4:5:1:1:2:1:3:2:2:1:2:3:1:1:1:1:4:1:1:1:4:1:1:1:1:1:12:4:4:4:4:4:4:4:4:4:15:1:1:1:1:1:1:1:11:4:4:3:1:2:2:2:1:1:1:1:1:2:3:1:3:2:1:4:1:3:1:1:1:1:12:4:4:2:1:1:1:1:1:3:1:1:2:1:2:1:2:1:2:4:1:2:2:1:2:1:12:4:4:1:2:4:1:8:1:3:1:2:3:1:1:2:1:1:2:1:1:1:12:4:4:1:1:4:1:1:1:3:2:1:5:1:1:1:1:1:1:2:3:2:11:1:11:4:4:1:1:1:2:1:5:1:2:4:1:2:1:1:4:1:2:1:1:1:2:1:1:1:12:4:3:4:1:2:2:1:4:1:1:3:1:1:2:2:1:1:2:2:4:1:1:1:1:1:12:4:3:3:2:1:2:3:3:2:4:1:2:1:2:1:1:2:2:1:2:1:1:1:12:4:3:3:1:1:1:1:1:1:5:1:1:1:1:1:2:5:1:2:3:1:1:11:1:1:11:4:3:2:2:4:1:1:1:2:1:1:1:1:2:4:1:1:2:1:1:1:2:1:3:1:1:1:1:12:4:3:2:1:1:2:1:2:1:3:1:2:1:1:1:2:1:2:2:1:1:1:3:1:2:1:1:1:12:4:3:1:4:3:2:4:4:1:1:1:1:1:1:1:1:2:1:1:2:1:1:1:2:1:12:4:3:1:3:1:2:1:1:1:11:4:3:1:2:2:1:11:1:11:4:3:1:2:2:1:1:3:7:4:3:1:2:2:1:1:2:1:1:2:1:1:12:4:3:1:1:2:3:1:2:2:6:1:1:1:1:3:1:1:2:5:2:1:12:4:3:1:1:1:3:2:1:1:2:1:1:1:1:1:1:1:1:3:1:4:3:1:1:1:1:2:1:12:4:3:1:1:1:1:1:5:1:1:2:4:1:2:3:1:2:4:1:1:11:1:1:1:11:4:2:6:2:1:5:1:2:1:1:1:4:2:3:1:1:2:4:1:1:1:1:1:12:4:2:4:1:6:3:1:1:1:1:1:1:4:1:1:1:1:2:1:2:2:2:1:12:4:2:3:5:2:1:1:1:1:5:1:1:6:2:2:1:1:2:2:1:12:4:2:3:2:2:2:1:2:3:1:7:1:1:2:3:1:3:1:11:1:11:4:2:3:1:2:5:2:1:4:2:2:2:2:1:2:1:3:2:1:2:1:12:4:2:2:4:1:2:1:1:3:1:2:4:1:1:1:1:1:1:1:2:1:1:1:1:1:1:2:1:12:4:2:2:2:6:4:1:1:2:1:1:1:2:2:1:3:2:4:2:1:12:4:2:2:2:1:3:1:2:2:2:2:1:1:1:10:2:13:1:1:1:1:1:11:4:2:2:1:4:1:4:5:1:1:1:5:2:2:1:1:1:1:4:1:1:1:1:1:12:4:2:2:1:1:2:2:1:1:1:3:3:3:1:5:1:1:1:1:4:1:2:1:12:4:2:2:1:1:1:1:1:1:2:2:3:3:6:1:2:1:3:2:1:1:1:2:1:12:4:2:1:4:4:1:5:3:1:1:1:2:2:8:2:11:1:11:4:2:1:3:3:4:1:2:2:2:5:1:1:1:1:1:4:5:1:1:1:1:1:1:12:4:2:1:2:3:2:3:4:2:4:1:3:1:1:2:2:2:1:2:1:1:1:12:4:2:1:2:2:1:1:2:3:1:1:2:5:1:1:2:3:6:2:1:1:12:4:2:1:2:1:1:7:1:2:4:3:2:5:2:3:11:1:1:11:4:2:1:1:6:1:3:1:3:1:1:2:4:2:1:3:1:1:1:1:1:1:1:2:1:12:4:2:1:1:2:2:3:2:6:1:1:1:9:2:1:2:2:1:1:12:4:2:1:1:2:1:1:1:4:2:5:1:3:4:1:2:1:1:1:1:1:2:1:1:1:12:4:2:1:1:1:3:1:1:1:2:1:3:3:1:1:3:2:1:1:1:2:1:3:11:1:1:11:4:2:1:1:1:1:4:3:1:1:1:1:1:4:1:1:3:2:1:7:1:2:1:12:4:1:7:4:1:7:4:1:7:3:1:1:2:1:12:4:1:6:1:2:1:1:1:1:2:3:1:4:1:1:1:1:4:4:2:1:1:12:4:1:5:2:1:1:1:4:2:1:1:1:1:2:3:9:2:11:1:11:4:1:4:5:3:3:2:1:2:2:2:2:1:3:1:1:5:2:1:12:4:1:4:1:2:4:1:5:4:2:4:1:2:1:1:2:2:2:1:12:4:1:4:1:1:1:1:8:2:2:1:1:1:1:3:1:2:1:3:1:2:1:1:12:4:1:3:4:1:1:1:2:3:1:1:5:1:2:2:2:5:2:11:1:11:4:1:3:2:4:2:3:1:1:8:4:1:3:2:1:2:2:1:12:4:1:3:1:3:3:2:4:1:1:1:1:4:3:3:1:1:3:1:2:1:12:4:1:3:1:2:1:1:3:2:3:2:1:3:1:1:1:1:2:1:2:1:1:1:1:1:1:2:1:12:4:1:3:1:1:1:9:4:1:3:1:1:1:8:2:11:1:11:4:1:2:7:1:2:1:1:4:2:2:2:2:1:5:3:1:1:2:1:12:4:1:2:3:2:2:3:2:3:3:1:1:7:1:1:6:1:1:1:1:1:1:1:12:4:1:2:3:1:1:1:2:1:1:3:2:1:5:4:1:3:1:2:1:1:1:2:1:12:4:1:2:2:2:4:2:3:3:8:3:1:1:1:1:1:12:1:1:1:1:11:4:1:2:2:1:3:3:1:3:1:4:2:1:2:1:1:2:2:1:1:1:2:1:2:1:12:4:1:2:1:4:2:1:1:1:1:4:1:3:1:1:1:1:1:1:3:2:1:1:2:2:1:12:4:1:2:1:3:1:1:1:1:2:1:1:2:1:2:1:3:1:2:1:5:1:4:1:1:1:1:1:12:4:1:2:1:2:1:3:1:5:1:1:1:1:1:4:1:3:1:2:1:1:3:11:1:11:4:1:2:1:1:4:1:2:2:2:1:1:4:2:1:3:3:2:2:1:2:2:1:12:4:1:2:1:1:1:2:1:2:1:1:1:1:1:2:1:2:3:3:1:3:1:3:1:1:2:1:12:4:1:1:9:1:2:2:2:7:3:1:1:1:1:1:1:1:1:2:1:1:1:12:4:1:1:5:2:2:2:2:1:1:1:4:1:2:1:2:2:2:1:14:1:1:1:1:1:1:11:4:1:1:4:2:3:4:2:1:1:1:1:1:6:1:1:3:2:1:1:2:1:1:1:12:4:1:1:3:2:5:3:1:2:1:1:2:2:6:2:2:2:1:2:1:12:4:1:1:3:1:3:4:1:3:2:1:1:3:3:1:1:4:1:4:1:1:1:1:1:12:4:1:1:3:1:1:1:6:1:2:2:2:1:2:1:2:4:3:1:2:11:1:11:4:1:1:2:4:1:7:2:2:3:2:4:1:3:2:1:3:1:1:1:1:12:4:1:1:2:2:1:3:1:1:1:4:2:2:1:1:1:5:5:1:1:1:1:2:1:12:4:1:1:2:1:4:1:1:1:2:2:2:1:3:1:1:3:2:1:1:3:1:1:2:1:1:1:12:4:1:1:2:1:2:1:2:1:1:3:2:1:2:5:1:1:1:1:1:4:2:11:1:1:11:4:1:1:2:1:1:2:1:1:1:1:5:1:1:2:1:3:2:1:1:1:3:1:4:2:1:12:4:1:1:1:4:2:1:1:2:1:1:1:3:1:1:2:1:3:1:1:1:1:1:1:1:1:3:1:1:1:1:12:4:1:1:1:3:2:2:1:2:1:1:1:2:1:2:1:1:2:3:2:1:1:1:1:2:2:1:1:12:4:1:1:1:3:1:1:1:1:1:1:6:2:2:2:5:1:2:1:1:3:11:1:11:4:1:1:1:2:2:3:1:1:4:1:1:1:2:1:1:2:1:1:2:1:3:2:4:1:1:1:1:1:12:4:1:1:1:2:1:1:1:2:1:1:2:1:1:3:1:1:1:1:5:3:2:2:3:1:1:1:12:4:1:1:1:1:4:1:1:1:1:1:2:2:1:1:1:3:1:1:1:1:1:4:2:1:1:1:2:1:1:12:4:1:1:1:1:2:4:1:1:1:1:1:1:2:2:1:1:1:3:1:3:1:1:1:4:11:1:11:4:1:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:3:1:3:1:1:1:1:12:4:1:1:1:1:1:1:6:5:1:2:1:1:4:3:1:1:1:1:1:1:3:1:1:1:1:12:4:1:1:1:1:1:1:1:2:3:1:7:1:1:6:1:1:2:1:1:2:2:1:1:12:3:9:3:9:3:9:3:1:11:1:1:11:3:8:1:1:2:2:4:2:1:1:1:1:3:4:1:2:2:1:2:2:1:12:3:6:4:2:1:1:1:5:1:1:2:1:3:2:1:3:1:2:2:1:1:12:3:6:2:1:2:4:1:1:5:1:4:1:3:1:1:1:1:2:2:1:1:12:3:6:1:2:1:1:2:3:2:3:2:2:3:1:1:2:3:1:2:11:1:11:3:5:6:2:1:1:1:2:1:2:2:2:1:1:1:1:4:1:1:1:2:1:2:1:12:3:5:2:2:2:1:1:1:3:1:9:3:4:5:1:1:1:1:1:1:12:3:5:1:9:1:1:1:1:1:2:2:2:6:1:4:1:2:1:12:3:5:1:2:2:3:2:1:1:5:1:2:1:1:2:1:1:3:4:11:1:11:3:5:1:1:2:3:3:1:2:1:1:2:2:2:2:5:1:2:2:1:2:1:12:3:4:4:3:3:4:1:2:4:1:2:2:1:3:3:2:1:1:12:3:4:3:3:1:2:1:2:2:1:2:1:1:3:3:7:2:2:1:12:3:4:3:1:1:2:1:2:1:2:1:2:4:5:1:1:5:1:1:11:1:11:3:4:2:2:9:3:4:2:2:8:4:1:1:1:1:12:3:4:2:1:1:1:3:2:2:3:2:2:2:1:1:3:1:1:2:4:2:1:12:3:4:1:4:1:1:11:3:4:1:4:1:1:1:1:1:2:1:12:3:4:1:2:4:2:2:1:3:1:1:1:4:3:1:2:2:1:1:12:1:1:1:1:11:3:4:1:2:1:2:1:1:2:1:2:1:2:3:3:2:3:3:4:2:1:1:12:3:4:1:1:1:6:1:1:1:4:1:3:1:1:3:3:5:2:1:1:12:3:4:1:1:1:1:2:2:1:2:1:1:1:3:2:1:1:4:2:1:1:2:1:1:1:1:2:1:12:3:3:6:2:2:4:1:2:1:1:5:1:3:1:1:1:1:1:1:1:11:1:1:11:3:3:5:1:1:1:1:2:1:3:3:2:1:1:1:1:2:2:3:2:2:1:2:1:12:3:3:3:3:3:3:3:3:3:3:3:3:2:2:2:1:1:12:3:3:3:2:1:1:1:6:1:2:2:1:2:1:1:1:1:2:4:2:2:1:1:12:3:3:3:1:1:3:2:1:1:1:1:1:4:3:7:2:2:12:1:1:1:11:3:3:2:5:1:1:1:1:1:1:4:4:1:1:1:4:1:4:1:1:2:1:1:12:3:3:2:2:1:4:1:1:1:2:1:3:1:3:1:2:1:2:1:1:5:2:1:12:3:3:2:1:4:1:2:1:1:1:3:1:1:2:2:1:4:3:2:1:1:2:1:1:12:3:3:2:1:2:1:1:11:3:3:2:1:2:1:1:3:11:1:1:11:3:3:2:1:1:1:4:4:3:1:1:5:8:2:3:2:1:12:3:3:1:4:1:2:1:2:3:2:1:1:3:5:5:1:2:2:1:1:12:3:3:1:3:1:6:1:1:5:2:1:4:4:2:1:1:2:2:1:12:3:3:1:2:4:2:1:3:1:2:1:1:1:1:3:1:2:2:1:4:1:11:1:1:11:3:3:1:2:2:1:2:2:1:3:1:2:3:1:3:1:1:1:1:1:3:1:2:2:1:1:12:3:3:1:1:5:2:2:5:5:1:1:1:1:1:1:1:1:1:1:5:1:1:1:1:1:1:12:3:3:1:1:3:1:2:2:5:2:1:1:4:8:1:2:1:2:1:12:3:3:1:1:2:2:1:1:1:1:1:3:1:1:1:1:4:2:3:2:5:1:11:1:11:3:3:1:1:1:4:2:4:1:1:1:1:2:1:1:2:2:2:1:1:2:1:1:1:1:1:2:1:1:12:3:3:1:1:1:1:2:1:1:1:1:1:2:4:1:1:4:2:5:1:1:1:2:2:1:12:3:2:8:2:3:1:2:5:2:1:1:2:1:2:2:1:2:1:2:1:12:3:2:6:1:2:2:3:1:1:3:1:3:6:2:1:2:1:11:1:1:11:3:2:5:1:9:2:1:1:3:3:1:1:1:6:4:1:1:1:1:12:3:2:4:2:1:2:2:1:1:1:1:2:1:1:1:3:1:4:1:2:2:1:1:1:2:1:12:3:2:4:1:1:6:1:1:1:1:2:3:1:1:1:1:2:1:1:2:2:3:2:1:12:3:2:3:5:1:7:3:1:1:2:1:1:3:1:1:2:14:1:1:1:1:1:1:11:3:2:3:3:1:2:1:1:3:3:2:1:2:1:1:2:1:2:1:1:1:1:2:1:1:2:1:12:3:2:3:1:4:1:2:1:6:1:5:1:2:4:2:1:2:2:1:12:3:2:3:1:2:1:1:4:1:4:3:4:2:2:1:1:1:1:1:3:2:1:12:3:2:3:1:1:1:3:6:3:2:3:1:1:1:3:5:1:1:11:1:11:3:2:2:6:1:1:1:2:1:2:1:2:2:3:1:4:1:2:3:3:1:1:1:12:3:2:2:3:1:3:1:3:3:4:4:1:1:1:2:2:1:1:1:1:1:1:2:1:12:3:2:2:2:3:4:1:1:3:3:3:2:1:1:1:3:1:1:1:1:1:2:1:1:1:12:3:2:2:2:2:1:1:1:2:2:2:2:1:2:11:3:1:1:11:1:11:3:2:2:2:1:1:2:4:1:1:1:5:5:1:1:2:1:1:3:1:3:1:1:1:12:3:2:2:1:2:8:3:2:2:1:2:8:3:1:1:2:1:12:3:2:2:1:2:1:2:1:7:2:1:4:1:1:3:5:4:1:1:1:1:12:3:2:2:1:1:3:2:1:1:1:1:2:1:6:1:2:2:1:1:2:2:13:1:1:1:1:11:3:2:2:1:1:1:5:1:1:2:1:3:4:5:1:2:2:2:4:1:1:1:1:12:3:2:1:6:1:2:1:1:4:1:3:5:1:1:2:1:3:1:3:1:1:1:12:3:2:1:4:4:1:1:1:4:1:2:5:1:3:3:1:1:2:2:1:1:12:3:2:1:4:1:1:3:1:1:3:1:2:3:1:1:1:2:2:1:1:1:4:1:11:1:11:3:2:1:3:3:1:1:1:1:1:2:1:2:3:1:2:1:7:2:5:1:1:1:1:1:12:3:2:1:3:1:1:1:1:7:6:1:1:2:2:1:4:2:1:2:1:1:12:3:2:1:2:3:3:4:2:4:1:1:6:2:2:1:1:1:2:2:1:12:3:2:1:2:2:2:5:2:9:3:2:1:2:2:1:1:11:1:1:11:3:2:1:2:2:1:1:1:1:3:1:1:1:1:2:1:2:1:2:1:1:1:1:2:1:1:1:5:1:1:1:1:1:1:12:3:2:1:2:1:1:2:2:2:1:1:2:1:3:2:1:4:2:1:2:1:1:1:1:1:1:2:1:12:3:2:1:1:8:3:2:1:1:8:3:2:1:1:2:3:1:1:1:12:3:2:1:1:4:2:1:1:2:1:1:2:2:1:1:1:3:3:4:1:1:3:11:1:11:3:2:1:1:3:2:1:2:1:1:4:4:2:1:2:2:1:1:1:3:3:2:1:1:12:3:2:1:1:2:2:1:2:3:1:1:2:2:5:1:1:1:1:1:1:1:2:1:1:1:2:1:1:12:3:2:1:1:2:1:1:5:1:2:2:1:1:2:2:2:4:1:1:3:1:1:2:1:1:12:3:2:1:1:1:5:1:1:2:9:1:1:4:1:2:3:1:1:1:11:1:11:3:2:1:1:1:3:1:1:2:1:2:2:1:1:1:1:1:11:2:5:1:1:1:1:1:12:3:2:1:1:1:1:3:2:2:2:4:2:1:5:2:1:4:3:1:2:1:12:3:2:1:1:1:1:1:5:3:3:1:3:4:5:1:1:2:1:2:2:1:12:3:2:1:1:1:1:1:1:2:1:1:1:2:1:6:1:1:2:4:2:1:2:2:11:1:1:11:3:1:8:1:1:3:6:2:2:2:1:4:6:3:1:1:1:12:3:1:6:1:1:2:1:2:1:1:2:1:1:3:1:1:2:2:3:3:1:2:2:1:12:3:1:5:2:3:5:1:1:4:5:1:3:1:1:1:2:3:1:1:1:12:3:1:5:1:2:4:1:3:8:3:1:5:1:2:11:1:1:10:3:1:5:1:1:1:1:1:1:2:1:1:1:2:2:3:6:1:1:2:3:2:2:1:1:11:3:1:4:1:2:1:2:2:4:5:1:2:1:1:1:5:1:1:1:2:1:2:1:11:3:1:3:5:6:1:1:6:1:4:6:2:4:1:1:1:1:11:3:1:3:3:1:2:1:8:1:2:3:2:2:2:2:4:2:12:1:11:3:1:3:2:1:4:1:1:1:1:2:3:1:1:2:2:3:4:1:1:1:1:1:1:2:1:11:3:1:3:1:3:4:1:1:1:2:2:1:1:1:1:3:1:1:2:1:2:5:1:12:1:11:3:1:3:1:1:11:3:1:3:1:1:10:4:1:1:1:1:11:3:1:3:1:1:1:2:1:1:3:5:1:5:2:1:2:4:2:1:2:12:1:11:3:1:2:5:1:1:3:3:1:5:3:1:1:3:5:1:4:1:1:1:1:11:3:1:2:3:3:2:1:5:3:2:1:1:1:1:1:5:1:1:2:1:2:12:1:11:3:1:2:3:1:1:1:3:1:3:2:1:2:2:1:2:6:1:1:1:1:2:1:2:1:11:3:1:2:2:2:2:5:11:3:1:2:2:2:1:3:12:1:11:3:1:2:2:1:2:2:1:1:6:2:1:3:1:5:4:2:2:1:2:1:11:3:1:2:1:6:2:1:1:1:3:1:1:2:1:1:1:3:1:3:4:1:2:12:1:11:3:1:2:1:3:1:2:3:1:1:3:2:1:1:4:1:1:2:2:5:1:2:1:1:11:3:1:2:1:2:1:3:11:3:1:2:1:2:1:3:3:13:1:1:1:11:3:1:2:1:1:3:3:1:3:1:1:8:1:1:1:1:1:3:1:2:4:1:1:1:1:11:3:1:2:1:1:1:5:2:4:1:5:1:1:1:1:2:2:2:2:1:14:1:1:1:1:11:3:1:2:1:1:1:1:1:4:1:1:1:1:1:1:2:2:5:4:1:1:1:1:2:2:2:1:11:3:1:1:5:5:1:4:2:1:3:1:2:3:1:1:2:3:3:12:1:11:3:1:1:4:2:4:3:1:2:1:4:1:4:3:2:6:1:1:1:1:1:1:1:11:3:1:1:3:8:1:1:1:1:1:1:3:2:4:1:3:2:1:4:12:1:11:3:1:1:3:2:1:5:1:1:4:1:1:2:1:4:5:2:1:3:2:1:11:3:1:1:3:1:1:6:1:1:3:1:3:1:2:2:3:2:3:1:3:12:1:11:3:1:1:2:4:4:6:1:2:3:1:5:1:1:2:1:1:1:2:2:1:11:3:1:1:2:2:6:2:4:4:2:2:1:2:3:1:1:2:1:2:12:1:11:3:1:1:2:2:1:1:4:1:3:1:1:1:1:1:1:11:2:3:1:2:1:11:3:1:1:2:1:2:4:5:1:4:1:1:1:7:3:2:1:13:1:1:1:11:3:1:1:2:1:1:2:3:2:2:1:11:3:1:1:2:1:1:2:2:1:1:11:3:1:1:1:8:1:2:1:3:2:4:3:2:2:2:1:3:13:1:1:1:11:3:1:1:1:4:1:2:5:1:1:6:1:1:1:3:4:1:1:1:1:1:2:1:1:11:3:1:1:1:3:1:3:2:3:2:3:1:3:1:1:6:3:1:1:1:12:1:1:11:3:1:1:1:2:3:2:1:4:1:1:3:2:3:1:2:2:3:4:3:1:1:1:11:3:1:1:1:2:1:4:1:1:2:1:3:1:1:2:1:2:1:4:1:4:2:1:1:12:1:11:3:1:1:1:2:1:1:1:1:4:2:1:2:6:2:2:1:2:1:1:1:4:1:2:1:11:3:1:1:1:1:3:2:2:5:1:1:1:1:2:1:3:2:1:1:2:6:12:1:1:11:3:1:1:1:1:2:2:1:2:1:1:2:2:3:2:1:1:1:3:3:1:1:2:1:2:1:2:1:11:3:1:1:1:1:1:4:1:2:2:2:3:3:1:2:1:3:1:5:2:2:12:1:11:3:1:1:1:1:1:2:1:1:1:1:1:4:1:2:3:2:1:1:2:2:3:2:2:1:2:1:1:11:3:1:1:1:1:1:1:1:1:4:8:2:1:3:1:1:1:1:2:1:1:1:2:2:12:1:11:2:8:4:1:6:1:1:1:2:2:3:1:2:2:1:2:1:2:2:1:11:2:7:2:2:2:3:2:1:2:2:11:2:2:1:1:12:1:11:2:6:5:1:1:2:1:1:3:3:3:5:1:2:1:2:1:2:2:1:11:2:6:2:1:1:3:1:1:2:2:1:2:1:1:7:1:1:2:15:1:1:1:1:1:1:11:2:6:1:1:2:1:4:2:1:2:1:3:3:2:1:1:2:1:1:3:2:2:1:11:2:5:4:1:1:1:2:1:1:1:1:1:1:1:3:1:2:1:2:3:1:3:1:1:12:1:1:11:2:5:2:2:4:1:1:5:1:3:1:5:2:2:1:1:3:2:1:1:11:2:5:1:5:3:2:3:1:1:3:1:3:3:2:4:2:1:12:1:11:2:5:1:2:1:2:1:1:1:2:2:2:1:2:2:1:3:1:1:2:1:1:2:3:2:1:11:2:5:1:1:1:2:2:2:1:3:1:1:1:1:1:4:2:1:1:2:5:13:1:1:1:11:2:4:5:1:1:1:5:2:3:3:2:3:1:2:4:1:2:2:1:11:2:4:3:2:4:2:1:1:1:3:1:1:1:3:2:1:2:1:5:2:12:1:11:2:4:2:5:3:1:1:1:1:1:1:1:1:1:2:2:1:1:2:2:1:1:1:1:3:2:1:11:2:4:2:2:1:1:4:2:3:1:2:4:4:1:1:1:1:2:1:14:1:1:1:1:11:2:4:2:1:1:2:1:4:1:2:1:1:5:1:3:1:1:4:2:1:1:1:2:1:11:2:4:1:3:7:2:2:1:2:2:2:3:3:2:1:1:1:14:1:1:1:1:11:2:4:1:2:2:2:4:1:7:2:2:1:2:1:4:3:3:1:1:1:11:2:4:1:1:4:4:4:2:1:2:5:3:4:1:2:1:1:12:1:11:2:4:1:1:2:1:1:1:3:1:2:2:1:1:1:4:2:1:1:3:1:4:3:1:1:1:11:2:4:1:1:1:1:1:4:2:4:2:2:2:3:2:1:4:2:1:1:1:12:1:11:2:3:5:3:2:1:1:2:2:3:2:1:1:9:1:1:4:1:1:1:1:11:2:3:4:2:1:2:2:5:2:1:1:2:1:2:1:1:2:1:1:3:1:1:12:1:1:11:2:3:3:4:1:1:1:2:2:1:4:1:2:1:1:1:3:1:2:2:3:2:1:1:11:2:3:3:1:4:4:3:1:2:1:1:1:6:1:3:3:1:2:12:1:11:2:3:3:1:1:1:1:3:2:1:3:2:7:1:1:2:1:2:2:4:1:1:1:1:11:2:3:2:3:2:1:1:2:3:1:1:1:3:1:2:6:1:2:3:1:12:1:1:11:2:3:2:2:1:4:1:1:1:1:1:1:2:4:2:1:1:1:2:1:1:1:1:3:1:1:2:1:11:2:3:2:1:3:1:3:2:1:1:1:5:1:1:1:2:2:1:1:2:4:13:1:1:1:11:2:3:2:1:1:3:1:1:1:4:1:3:2:1:2:1:11:2:2:1:11:2:3:1:7:3:1:2:1:1:2:3:3:4:1:1:1:2:1:1:13:1:1:1:11:2:3:1:4:1:1:2:2:1:5:1:1:1:4:1:4:1:2:2:1:2:2:1:11:2:3:1:3:1:1:5:1:3:1:2:2:1:1:3:1:3:1:1:1:1:3:1:12:1:11:2:3:1:2:2:3:1:2:1:1:1:1:2:3:3:1:2:1:1:3:2:1:1:1:1:2:1:11:2:3:1:2:1:2:1:1:2:2:3:2:3:1:1:1:5:5:1:1:13:1:1:1:11:2:3:1:1:4:3:1:1:2:1:1:1:1:5:2:2:3:1:2:1:3:2:1:1:11:2:3:1:1:2:3:2:3:2:2:2:2:4:1:1:2:1:3:2:1:13:1:1:1:11:2:3:1:1:1:7:4:7:1:4:1:2:1:1:2:2:2:2:1:11:2:3:1:1:1:2:1:2:1:1:1:1:1:2:1:1:2:1:1:1:2:1:1:2:2:3:2:13:1:1:1:11:2:3:1:1:1:1:1:2:1:1:2:1:1:1:4:1:2:1:1:2:1:2:1:1:3:1:2:2:1:1:11:2:2:6:3:1:5:1:4:2:5:1:3:2:2:2:12:1:1:11:2:2:5:2:1:1:2:2:1:4:1:1:1:3:1:2:2:2:1:1:4:2:1:1:11:2:2:4:3:2:3:1:2:3:1:2:1:4:2:3:1:1:2:1:1:12:1:1:11:2:2:4:1:3:1:4:1:3:1:1:1:5:2:2:2:1:2:1:1:1:1:2:1:11:2:2:3:6:5:7:2:1:3:1:1:3:2:1:3:12:1:11:2:2:3:3:1:1:2:1:1:1:1:1:1:1:1:7:4:3:1:4:2:1:1:11:2:2:3:2:1:1:2:11:2:2:3:2:1:1:2:3:2:12:1:11:2:2:3:1:2:2:1:5:2:5:1:2:1:1:7:1:2:3:1:1:1:11:2:2:3:1:1:1:3:4:1:1:2:2:2:1:1:2:1:2:1:1:1:2:2:1:2:12:1:11:2:2:2:5:1:1:3:1:1:3:1:1:1:1:1:2:3:2:1:2:4:1:2:1:1:11:2:2:2:3:2:2:2:2:1:1:1:1:1:1:1:1:1:2:2:2:2:1:2:1:3:1:12:1:11:2:2:2:2:4:1:1:4:1:1:1:2:1:2:7:1:3:1:4:1:1:1:1:1:11:2:2:2:2:1:4:1:1:3:5:3:1:7:1:4:2:1:12:1:11:2:2:2:1:11:2:2:2:1:11:2:1:3:1:1:1:1:11:2:2:2:1:3:1:1:3:2:2:1:2:3:1:1:2:1:6:1:3:1:12:1:1:11:2:2:2:1:2:1:1:3:2:2:1:6:1:1:1:4:4:1:2:2:1:2:1:11:2:2:2:1:1:2:2:1:5:2:1:3:1:1:2:1:1:5:6:1:12:1:11:2:2:2:1:1:1:1:3:2:1:1:3:1:1:1:1:1:1:1:1:1:2:2:2:4:3:1:1:1:1:11:2:2:1:6:1:1:1:1:1:3:1:3:1:1:3:1:2:3:1:1:1:3:1:12:1:1:11:2:2:1:4:2:1:3:1:4:2:2:5:3:1:1:1:2:1:1:1:2:2:1:11:2:2:1:3:3:3:1:1:2:4:3:3:2:1:1:1:2:2:1:1:1:2:12:1:11:2:2:1:3:1:3:1:2:1:1:1:5:2:3:3:2:1:2:3:1:3:1:1:1:11:2:2:1:2:5:5:4:1:1:1:2:3:2:2:1:2:1:2:1:1:1:12:1:11:2:2:1:2:2:5:2:1:2:2:2:1:5:1:1:1:1:2:1:1:1:2:1:1:2:1:11:2:2:1:2:2:1:1:1:1:2:1:4:2:1:4:1:4:1:1:2:5:1:12:1:11:2:2:1:2:1:2:1:1:5:1:2:4:8:1:1:2:1:1:1:2:1:2:1:11:2:2:1:2:1:1:1:1:3:1:2:1:2:1:2:1:1:1:2:4:4:1:1:1:2:1:12:1:11:2:2:1:1:4:2:1:1:2:3:1:2:1:2:1:1:1:4:3:1:2:2:2:2:1:11:2:2:1:1:3:1:2:3:1:1:1:1:4:2:2:1:2:5:1:2:1:2:12:1:1:11:2:2:1:1:2:2:3:2:2:1:2:1:1:1:1:2:1:2:1:3:2:3:2:2:2:1:11:2:2:1:1:2:1:1:5:4:2:1:2:2:1:2:1:2:2:1:1:1:3:1:12:1:1:11:2:2:1:1:1:4:1:1:3:1:1:3:1:2:5:2:5:1:4:2:1:1:11:2:2:1:1:1:2:2:1:4:1:2:1:1:1:1:2:3:2:1:3:2:1:3:1:12:1:1:11:2:2:1:1:1:1:3:2:2:8:3:1:3:1:2:4:2:1:2:2:1:11:2:2:1:1:1:1:1:2:4:2:3:1:4:1:3:1:1:1:2:1:2:3:1:1:12:1:11:2:1:9:1:2:1:1:6:4:3:1:4:2:2:1:1:1:2:1:11:2:1:6:3:2:3:2:1:1:1:1:2:1:8:1:1:1:2:2:12:1:1:11:2:1:5:5:1:1:3:3:1:1:2:1:1:1:2:4:1:1:2:3:1:2:1:11:2:1:5:1:4:6:1:3:1:4:1:4:2:1:4:13:1:1:1:11:2:1:4:8:3:1:2:2:1:1:5:5:1:1:1:3:2:1:1:11:2:1:4:3:1:1:1:1:3:1:1:1:1:4:4:1:1:2:2:5:12:1:1:1:11:2:1:4:2:1:1:1:2:1:1:1:1:2:1:2:2:4:1:1:1:1:1:2:1:1:2:3:1:1:1:11:2:1:4:1:2:1:1:3:1:4:4:1:1:1:7:1:2:3:2:12:1:11:2:1:4:1:1:1:1:2:1:1:2:3:1:1:1:2:1:2:1:3:3:1:1:1:1:1:1:1:2:1:11:2:1:3:4:1:2:2:2:7:1:1:2:3:1:1:1:3:1:2:2:12:1:11:2:1:3:3:1:1:3:1:2:2:1:1:2:1:3:2:2:9:2:1:1:1:11:2:1:3:2:2:1:4:1:2:3:2:1:3:1:2:1:3:4:1:2:12:1:1:11:2:1:3:2:1:1:1:8:1:3:3:2:3:1:1:4:4:2:1:1:11:2:1:3:1:3:2:1:1:2:2:1:1:5:1:2:1:1:2:3:2:1:1:2:1:12:1:11:2:1:3:1:2:1:2:1:2:1:6:1:1:3:2:1:1:1:3:4:1:1:1:2:1:11:2:1:3:1:1:2:2:1:4:1:1:1:2:2:1:2:3:2:1:1:2:4:1:1:12:1:11:2:1:3:1:1:1:1:2:2:1:1:3:1:1:1:3:3:1:1:1:4:3:4:1:1:1:1:1:11:2:1:2:5:2:2:2:1:1:2:1:1:4:1:1:2:2:1:5:2:1:1:12:1:11:2:1:2:4:1:2:2:3:1:2:2:2:1:4:3:1:1:1:1:2:1:2:2:1:1:11:2:1:2:3:2:2:2:5:2:6:1:1:3:1:1:2:1:1:1:1:1:12:1:1:11:2:1:2:3:1:1:2:3:1:1:1:2:2:3:1:1:3:4:4:2:3:1:1:1:11:2:1:2:2:3:2:3:1:1:1:2:1:2:1:1:1:1:2:4:2:3:2:1:12:1:1:11:2:1:2:2:2:1:2:2:5:1:1:1:2:1:2:3:1:1:4:3:2:2:1:1:11:2:1:2:2:1:2:2:2:3:1:1:1:1:2:2:11:2:1:1:1:12:1:1:11:2:1:2:2:1:1:1:2:2:1:5:3:2:1:2:2:3:1:2:2:2:1:1:2:1:11:2:1:2:1:4:2:2:1:1:1:5:1:1:6:1:2:1:1:1:4:1:12:1:1:11:2:1:2:1:3:1:2:1:3:2:2:2:1:1:2:1:1:1:4:3:1:1:3:1:2:1:11:2:1:2:1:2:2:2:1:2:1:1:5:3:4:1:3:3:2:2:13:1:1:1:11:2:1:2:1:2:1:1:2:1:1:1:6:1:1:2:2:3:1:4:3:3:2:1:1:11:2:1:2:1:1:3:1:6:1:1:1:2:1:4:2:3:3:3:1:2:1:12:1:11:2:1:2:1:1:2:1:1:3:5:1:1:2:1:2:1:1:1:2:9:2:1:1:1:11:2:1:2:1:1:1:2:1:3:1:1:1:1:2:1:1:1:2:1:1:1:1:1:2:1:2:2:2:1:1:1:12:1:11:2:1:2:1:1:1:1:1:1:2:2:1:1:1:1:5:4:1:3:1:2:1:3:2:2:1:1:11:2:1:1:6:1:1:6:5:4:1:2:5:2:1:2:2:12:1:11:2:1:1:5:1:1:1:2:2:7:2:2:1:3:1:1:1:3:1:3:2:1:1:11:2:1:1:4:2:1:1:1:2:4:4:2:1:1:1:1:1:1:1:1:1:2:3:1:13:1:1:1:11:2:1:1:4:1:1:1:1:1:1:1:1:1:2:4:1:1:11:2:1:1:1:2:1:1:11:2:1:1:3:2:5:2:1:5:3:3:1:1:2:5:2:2:1:12:1:11:2:1:1:3:1:5:2:1:1:1:4:1:2:2:1:1:1:1:1:1:2:2:1:1:1:1:1:2:1:11:2:1:1:3:1:1:3:3:3:1:2:2:3:2:4:1:2:3:1:2:12:1:1:11:2:1:1:2:6:1:3:2:1:2:1:1:1:1:2:1:1:1:2:3:3:2:2:1:1:1:11:2:1:1:2:3:2:6:1:2:2:1:1:2:1:2:1:3:1:1:1:1:1:3:1:12:1:11:2:1:1:2:2:3:2:4:2:5:1:1:3:1:3:2:7:1:1:1:1:1:1:1:1:11:2:1:1:2:2:1:2:1:5:1:3:1:1:1:2:4:1:1:1:1:1:1:2:3:1:12:1:11:2:1:1:2:1:4:1:1:3:2:1:7:3:1:8:3:1:2:1:11:2:1:1:2:1:2:1:5:3:2:2:2:2:4:2:1:2:1:1:3:1:12:1:1:11:2:1:1:2:1:1:2:3:2:1:1:2:2:1:1:2:2:4:1:4:1:3:2:1:1:1:11:2:1:1:2:1:1:1:1:2:2:2:2:1:2:1:3:1:2:3:2:2:2:1:3:1:12:1:11:2:1:1:1:5:2:1:1:1:1:1:3:6:5:1:1:2:4:3:1:1:1:1:11:2:1:1:1:4:1:1:2:2:5:1:3:2:1:3:2:1:1:4:1:13:1:1:1:1:11:2:1:1:1:3:2:1:1:2:2:5:4:2:2:3:1:4:1:1:1:1:1:2:1:11:2:1:1:1:2:11:2:1:1:1:2:11:1:4:12:1:1:11:2:1:1:1:2:2:4:1:6:1:2:1:2:2:1:2:1:3:5:1:1:2:1:11:2:1:1:1:2:1:4:1:6:1:1:1:1:1:1:2:2:1:2:4:2:14:1:1:1:1:1:11:2:1:1:1:2:1:1:2:3:3:1:3:2:1:1:1:1:2:3:9:1:2:1:11:2:1:1:1:1:5:1:4:1:1:7:6:2:2:1:3:1:13:1:1:1:11:2:1:1:1:1:3:2:1:1:1:1:1:4:1:1:2:1:1:1:2:1:1:2:5:1:1:3:1:1:1:11:2:1:1:1:1:2:2:3:1:3:1:1:3:3:2:5:1:3:1:2:1:1:1:12:1:11:2:1:1:1:1:2:1:1:2:1:2:1:4:2:2:4:4:1:2:1:1:4:2:1:1:11:2:1:1:1:1:1:3:1:2:3:1:2:2:1:1:1:1:2:1:2:2:2:2:5:1:12:1:11:2:1:1:1:1:1:2:1:1:1:1:4:2:1:2:2:7:1:1:4:1:1:1:3:1:1:1:11:2:1:1:1:1:1:1:1:4:6:3:2:2:1:1:2:4:3:2:1:1:1:12:1:11:1:11:1:11:1:11:1:3:2:1:1:1:11:1:8:2:2:5:3:3:1:2:8:1:2:1:1:13:1:1:1:11:1:7:3:1:1:4:1:1:1:1:1:1:3:3:1:1:2:2:6:2:1:1:11:1:7:1:1:3:2:4:11:1:7:1:1:1:1:12:1:11:1:6:3:2:1:2:1:1:2:2:1:1:1:2:6:1:1:2:1:4:1:2:1:11:1:6:2:1:1:2:1:2:1:2:2:1:4:1:1:1:1:1:4:1:2:1:2:12:1:1:11:1:6:1:1:11:1:6:1:1:11:1:1:2:1:11:1:5:6:3:2:1:1:1:3:2:1:1:2:1:3:1:2:1:1:2:1:12:1:1:11:1:5:3:2:3:1:4:1:1:1:2:4:1:2:1:3:3:3:1:2:1:11:1:5:2:3:1:1:3:1:1:1:1:2:1:1:2:6:1:1:1:1:2:3:12:1:1:11:1:5:2:1:1:3:1:1:4:5:1:1:1:5:3:2:2:1:1:2:1:1:11:1:5:1:3:1:4:2:2:1:1:1:2:2:1:1:1:3:2:1:2:1:2:1:12:1:1:11:1:5:1:2:1:1:1:2:1:1:1:1:3:2:2:4:1:5:6:1:2:1:11:1:5:1:1:1:4:1:2:2:3:3:1:3:1:2:1:2:1:1:5:12:1:1:11:1:4:6:4:1:3:1:1:2:1:3:2:2:3:2:3:2:2:1:1:11:1:4:4:2:1:1:3:1:3:1:1:1:1:1:2:1:1:1:3:7:12:1:1:1:11:1:4:3:2:2:6:1:1:2:1:1:1:1:2:2:1:2:5:2:2:1:1:1:11:1:4:3:1:1:1:3:1:1:1:1:5:1:1:1:1:1:1:1:2:1:2:1:4:1:12:1:1:11:1:4:2:3:1:1:1:1:1:1:1:5:3:2:1:1:6:2:5:1:1:1:1:1:1:11:1:4:2:1:4:2:1:1:1:2:1:1:2:2:4:1:3:1:3:1:1:2:1:12:1:11:1:4:2:1:1:2:1:6:1:3:1:8:1:2:1:1:1:2:1:1:2:1:1:11:1:4:1:4:3:3:1:5:1:1:1:2:3:3:2:2:1:3:12:1:1:11:1:4:1:3:1:1:4:1:4:1:1:1:1:1:1:1:1:1:3:1:1:3:1:1:3:1:1:1:1:11:1:4:1:2:2:1:1:1:1:2:3:2:1:1:1:1:4:1:2:4:1:4:1:12:1:11:1:4:1:1:5:2:1:1:1:2:1:2:1:2:1:1:2:3:1:2:3:1:1:1:2:1:1:11:1:4:1:1:2:2:1:3:2:2:1:2:1:2:3:1:2:1:2:2:1:2:1:1:12:1:1:11:1:4:1:1:1:2:3:1:1:4:2:1:1:1:3:2:1:1:2:1:1:1:1:1:1:2:2:1:1:11:1:4:1:1:1:1:1:1:2:2:4:5:3:1:3:4:1:5:12:1:1:11:1:3:5:5:1:2:4:1:2:3:1:3:1:2:1:1:5:2:1:1:11:1:3:4:3:4:1:2:1:1:3:1:1:1:5:5:1:2:1:1:1:12:1:11:1:3:4:1:1:7:1:2:2:2:2:1:2:1:1:3:1:1:3:1:2:1:1:1:11:1:3:3:3:2:1:1:1:2:2:1:3:2:1:3:3:1:3:2:2:12:1:1:1:11:1:3:3:2:1:1:1:1:1:2:5:1:4:1:3:2:1:4:3:1:2:1:1:11:1:3:3:1:1:3:1:2:1:1:1:1:4:2:1:2:2:2:3:1:1:1:3:1:12:1:11:1:3:2:5:1:1:2:1:3:1:2:1:2:2:2:1:3:6:1:1:2:1:1:11:1:3:2:3:1:2:1:2:1:5:4:11:1:3:1:1:12:1:11:1:3:2:2:2:1:1:1:2:1:2:2:1:4:1:1:3:2:1:2:2:1:3:2:1:1:11:1:3:2:1:5:1:1:5:2:5:1:1:3:1:1:1:3:1:1:1:1:12:1:1:11:1:3:2:1:2:2:1:1:1:2:2:1:1:2:1:1:1:1:5:6:2:2:2:1:1:11:1:3:2:1:1:2:2:1:1:1:1:2:4:1:1:2:1:2:4:1:2:2:2:12:1:1:1:11:1:3:1:11:1:3:1:11:1:3:2:1:1:1:1:2:1:11:1:3:1:4:2:3:4:1:2:2:4:3:1:2:2:1:2:1:1:12:1:1:1:11:1:3:1:3:2:4:1:1:1:2:1:1:1:1:3:1:1:3:1:1:6:1:1:2:1:1:11:1:3:1:3:1:1:1:2:2:1:1:3:2:2:1:5:4:3:1:1:1:12:1:1:1:11:1:3:1:2:2:4:1:1:1:1:1:1:2:1:1:1:1:1:1:1:2:1:3:1:5:2:1:1:1:11:1:3:1:2:1:3:1:2:3:1:3:1:1:3:1:2:10:1:1:12:1:1:11:1:3:1:2:1:1:1:2:1:2:4:1:4:2:1:1:1:5:1:1:2:1:2:2:1:1:11:1:3:1:1:3:11:1:3:1:1:3:10:2:12:1:1:11:1:3:1:1:2:3:3:1:2:5:1:1:1:1:1:1:2:1:1:1:1:1:2:1:1:1:1:2:1:1:11:1:3:1:1:2:1:1:3:1:2:2:1:1:1:1:3:2:2:2:2:4:3:1:1:12:1:11:1:3:1:1:1:3:1:1:2:3:1:1:1:1:4:6:1:3:3:4:1:1:1:1:1:11:1:3:1:1:1:1:4:1:1:3:1:1:1:1:1:5:3:1:5:1:3:12:1:1:1:11:1:3:1:1:1:1:1:1:4:2:1:4:6:1:1:4:1:2:1:4:2:1:1:11:1:2:7:1:2:1:1:2:3:1:1:2:2:1:5:1:1:3:1:1:2:1:12:1:11:1:2:5:4:1:6:1:1:1:4:3:4:6:2:2:1:1:11:1:2:5:1:1:5:3:1:3:2:1:3:2:2:1:5:1:1:12:1:1:1:11:1:2:4:3:1:2:1:2:4:1:3:1:1:1:1:3:2:5:2:2:1:1:1:11:1:2:4:1:4:1:5:2:1:2:6:2:4:1:1:1:14:1:1:1:1:1:11:1:2:4:1:1:1:4:5:3:1:2:4:5:1:1:1:1:1:2:2:1:1:11:1:2:3:4:1:1:5:11:1:2:3:3:3:1:12:1:1:11:1:2:3:2:4:1:1:2:2:1:3:1:1:2:1:2:1:2:1:3:1:3:1:1:2:1:11:1:2:3:2:1:1:3:2:2:1:2:1:1:2:3:1:4:1:2:3:3:12:1:1:11:1:2:3:1:3:1:1:1:4:1:1:4:5:2:1:2:2:1:3:2:1:2:1:11:1:2:3:1:1:3:3:1:1:2:1:1:2:1:2:1:3:1:1:4:1:2:1:2:12:1:1:11:1:2:3:1:1:1:1:2:2:1:2:3:1:1:7:2:2:1:5:1:1:1:2:1:11:1:2:2:4:5:1:3:2:3:2:11:1:1:3:1:12:1:11:1:2:2:3:3:1:2:1:2:3:1:2:2:1:1:4:1:3:3:2:1:2:1:1:11:1:2:2:3:1:1:1:3:1:1:2:1:1:2:2:1:1:1:2:3:1:1:1:1:2:2:1:1:12:1:11:1:2:2:2:2:3:1:4:1:1:3:4:3:2:1:1:1:2:3:1:1:1:2:1:11:1:2:2:2:1:2:3:2:2:1:1:3:1:2:1:6:3:1:2:2:12:1:1:1:11:1:2:2:2:1:1:1:1:1:1:1:2:2:1:1:1:1:1:8:1:1:1:5:2:1:2:1:11:1:2:2:1:3:2:1:5:2:2:2:2:1:3:1:1:3:1:3:1:2:12:1:1:11:1:2:2:1:2:2:1:3:4:1:2:1:2:1:1:2:1:1:2:3:1:3:1:1:2:1:1:11:1:2:2:1:1:5:1:4:1:4:1:1:1:1:1:2:1:2:1:5:1:13:1:1:1:1:11:1:2:2:1:1:2:1:3:1:2:1:2:1:1:3:1:2:1:1:1:2:1:1:1:1:3:1:2:1:1:1:11:1:2:2:1:1:1:1:6:1:2:4:7:1:1:3:2:2:2:12:1:1:1:11:1:2:1:7:1:1:3:2:1:3:1:1:1:3:1:1:1:1:1:1:5:1:1:1:2:1:11:1:2:1:5:1:1:1:7:3:2:3:1:2:3:2:3:3:12:1:11:1:2:1:4:1:2:3:1:1:2:1:1:1:1:2:1:3:5:2:7:1:1:1:1:1:1:1:1:11:1:2:1:3:3:2:1:1:1:1:2:1:1:4:2:1:1:1:4:3:4:1:12:1:1:11:1:2:1:3:1:4:1:11:1:2:1:3:1:4:1:4:2:1:11:1:2:1:3:1:1:1:2:4:3:2:1:1:1:6:1:1:2:1:1:2:3:12:1:11:1:2:1:2:3:4:2:2:1:1:1:1:2:4:1:1:1:1:2:2:2:1:3:2:1:1:11:1:2:1:2:2:2:4:2:2:1:3:5:4:1:6:1:13:1:1:1:1:11:1:2:1:2:1:11:1:2:1:2:1:11:1:2:3:1:1:1:1:11:1:2:1:2:1:2:2:1:1:1:6:2:1:1:3:4:2:1:1:2:2:1:1:12:1:1:11:1:2:1:2:1:1:2:1:1:1:5:1:5:1:1:2:1:3:1:1:3:1:1:2:1:2:1:11:1:2:1:1:6:1:2:2:2:3:1:1:3:1:1:2:3:6:13:1:1:1:1:11:1:2:1:1:4:1:1:1:3:2:1:2:1:1:2:1:1:4:5:3:2:2:1:1:1:11:1:2:1:1:3:1:2:1:4:4:5:3:1:2:1:2:3:3:1:1:12:1:11:1:2:1:1:2:3:1:2:1:1:1:1:1:1:3:1:1:1:1:3:1:2:1:1:1:4:1:2:1:1:1:11:1:2:1:1:2:1:3:2:1:1:1:2:1:3:5:1:4:2:2:2:1:1:1:12:1:1:11:1:2:1:1:2:1:1:1:1:2:1:1:1:1:5:1:2:3:1:1:3:7:1:1:2:1:11:1:2:1:1:1:3:2:1:6:3:1:1:1:1:1:1:3:1:1:1:2:1:4:12:1:1:1:11:1:2:1:1:1:2:2:1:1:4:1:1:7:1:1:2:1:3:2:3:2:2:1:1:1:11:1:2:1:1:1:1:4:1:3:1:1:1:2:1:2:2:5:1:2:2:3:1:2:12:1:1:11:1:2:1:1:1:1:2:1:1:1:2:1:2:3:1:1:2:1:3:3:3:1:1:3:2:2:1:1:11:1:2:1:1:1:1:1:1:2:1:1:2:6:2:2:1:1:2:1:1:3:3:1:14:1:1:1:1:1:11:1:1:8:1:1:1:1:1:4:4:1:2:1:1:1:3:1:1:1:1:2:1:1:1:1:2:1:11:1:1:6:2:5:2:1:1:2:7:2:1:2:1:2:3:2:1:12:1:11:1:1:5:6:5:2:1:3:1:2:3:1:1:1:2:2:2:2:1:2:1:11:1:1:5:2:1:3:1:1:3:1:1:1:3:4:5:1:1:1:2:2:12:1:1:1:11:1:1:5:1:1:3:2:2:2:4:1:1:3:1:1:4:2:2:1:3:2:1:11:1:1:4:4:2:1:1:3:1:4:2:6:1:3:1:2:3:1:1:12:1:11:1:1:4:2:3:3:3:1:1:1:3:1:2:1:4:1:2:2:1:3:1:1:2:1:11:1:1:4:2:1:1:1:2:1:2:1:1:2:3:1:3:1:1:2:3:3:1:2:1:12:1:1:11:1:1:4:1:2:2:1:2:1:1:2:1:3:2:2:1:1:2:1:1:1:2:2:1:2:1:1:2:1:11:1:1:4:1:1:1:7:1:2:2:2:1:5:3:1:3:4:1:1:12:1:11:1:1:3:5:3:1:3:2:3:3:1:2:1:1:1:1:1:2:1:3:1:1:1:2:1:11:1:1:3:3:4:1:5:2:3:1:1:1:1:2:2:1:2:1:1:1:3:1:1:12:1:11:1:1:3:3:1:1:1:3:3:2:1:1:3:4:1:1:1:1:1:1:1:1:1:2:2:2:1:1:11:1:1:3:2:2:2:1:4:1:1:2:2:2:2:1:1:3:8:13:1:1:1:1:11:1:1:3:2:1:2:1:1:1:1:1:1:1:2:2:3:1:1:2:1:8:1:1:1:1:1:2:1:11:1:1:3:1:4:3:1:1:4:1:2:1:1:2:3:2:3:1:3:1:1:1:12:1:1:11:1:1:3:1:2:4:1:1:2:2:1:1:1:1:1:2:1:5:2:1:1:1:1:1:3:2:1:11:1:1:3:1:2:1:1:1:2:1:2:1:7:3:1:1:1:2:1:3:2:2:1:12:1:1:11:1:1:3:1:1:2:2:1:1:1:1:4:1:1:1:1:1:1:2:1:2:1:1:1:2:1:2:4:1:1:1:1:1:11:1:1:3:1:1:1:1:4:1:1:1:1:2:1:1:2:1:3:1:5:2:2:15:1:1:1:1:1:1:11:1:1:2:6:1:5:1:1:1:1:2:3:2:2:3:1:3:2:2:1:2:1:1:11:1:1:2:4:3:1:2:3:1:1:2:1:2:3:3:2:2:1:1:1:1:2:1:11:1:1:11:1:1:2:3:6:4:2:3:2:1:2:2:3:4:2:3:2:1:11:1:1:2:3:2:1:2:1:1:2:4:1:1:1:1:3:1:1:2:1:5:1:3:2:1:11:1:1:2:3:1:1:2:1:3:3:2:1:2:2:1:1:4:1:1:1:1:2:2:2:11:1:11:1:1:2:2:3:4:5:2:1:3:1:2:2:1:1:5:1:1:4:1:1:1:1:1:11:1:1:2:2:2:2:2:1:1:1:1:4:1:1:3:2:3:2:1:1:1:2:2:1:2:1:1:1:11:1:1:2:2:1:4:1:2:1:2:2:3:3:1:2:1:3:1:2:2:2:2:11:1:11:1:1:2:2:1:1:5:1:1:2:1:1:1:3:1:2:2:1:3:2:1:1:1:2:2:2:1:11:1:1:2:2:1:1:1:1:1:2:8:3:3:1:2:1:1:1:4:2:3:1:1:1:1:11:1:1:2:1:4:1:6:1:1:1:3:1:1:1:5:1:1:1:1:1:1:2:1:1:11:1:1:1:11:1:1:2:1:3:1:3:1:1:3:4:1:1:1:2:1:6:1:1:3:1:1:2:1:1:1:11:1:1:2:1:2:3:1:1:1:11:1:1:2:1:2:3:1:1:1:3:2:1:1:1:11:1:1:2:1:2:1:2:2:2:4:11:1:1:2:1:2:1:2:1:1:11:1:1:11:1:1:2:1:1:5:1:4:2:3:2:1:4:1:2:3:4:1:1:1:2:1:1:11:1:1:2:1:1:2:5:1:1:1:3:3:6:1:2:1:6:1:1:1:2:1:1:11:1:1:2:1:1:2:1:1:1:2:1:2:1:1:3:1:5:1:4:2:1:1:1:3:1:1:11:1:11:1:1:2:1:1:1:2:1:3:1:2:2:4:3:11:1:1:1:1:2:1:1:1:11:1:1:2:1:1:1:1:1:2:2:1:2:1:4:1:1:4:2:2:1:1:3:3:3:1:1:1:1:11:1:1:1:6:4:1:1:2:2:2:2:3:2:1:2:2:3:3:1:1:1:11:1:11:1:1:1:5:2:1:1:5:3:1:2:4:1:2:1:5:3:2:2:1:11:1:1:1:4:3:2:3:1:2:2:1:1:1:2:1:1:2:3:3:2:2:1:2:1:1:1:11:1:1:1:4:1:3:1:2:1:2:1:1:1:1:3:1:2:5:3:3:1:1:1:11:1:1:11:1:1:1:3:6:1:2:1:2:1:2:2:1:1:4:1:2:1:3:1:3:2:1:1:1:11:1:1:1:3:2:9:2:1:2:2:1:1:1:7:4:3:2:1:11:1:1:1:3:2:1:1:1:3:1:2:1:2:1:3:1:1:3:1:2:3:1:1:1:3:11:1:11:1:1:1:3:1:2:1:3:1:2:1:1:2:1:4:4:2:2:1:2:3:2:2:1:11:1:1:1:3:1:1:1:2:1:2:2:1:2:1:3:1:2:2:4:1:2:1:6:1:1:1:1:1:1:1:11:1:1:1:2:4:3:1:1:1:3:1:2:2:1:2:2:1:1:3:1:6:11:1:1:1:11:1:1:1:2:3:1:4:11:1:1:1:2:3:1:4:3:1:2:1:1:11:1:1:1:2:2:3:1:1:3:3:1:1:1:6:1:2:1:1:1:4:1:1:1:1:1:2:1:11:1:1:1:2:2:1:2:2:1:1:2:1:1:3:1:1:1:1:4:3:8:11:1:1:11:1:1:1:2:1:4:3:2:6:3:3:4:3:1:5:1:2:1:1:11:1:1:1:2:1:2:3:1:2:1:1:3:3:1:3:3:2:2:2:2:2:2:2:1:11:1:1:1:2:1:1:4:3:2:3:2:5:3:2:1:2:3:3:1:11:1:1:11:1:1:1:2:1:1:1:4:2:6:6:1:1:7:2:1:2:2:1:1:1:11:1:1:1:1:11:1:1:1:1:11:1:1:1:2:1:1:1:1:1:1:1:2:1:11:1:1:1:1:5:1:1:1:2:3:3:1:5:1:4:1:7:1:11:1:1:1:11:1:1:1:1:4:1:1:1:3:1:4:1:1:2:2:1:2:2:2:2:1:1:2:2:1:1:2:1:11:1:1:1:1:3:2:1:2:1:4:1:3:4:2:1:2:2:3:1:1:2:1:2:1:1:1:11:1:1:1:1:3:1:1:1:3:1:4:5:1:2:2:1:2:1:6:1:1:1:1:11:1:11:1:1:1:1:2:3:1:7:2:2:1:5:4:4:1:1:1:2:1:2:1:1:11:1:1:1:1:2:2:1:2:1:2:1:7:4:1:2:4:1:2:1:2:3:1:1:1:1:11:1:1:1:1:2:1:2:2:2:1:4:3:1:2:1:4:1:1:1:1:3:2:1:12:1:1:1:1:11:1:1:1:1:2:1:1:1:2:4:1:1:4:4:1:1:1:1:3:1:1:2:1:2:3:1:1:1:1:11:1:1:1:1:1:4:3:1:2:1:1:2:1:1:8:1:2:1:1:3:1:2:2:1:1:1:11:1:1:1:1:1:3:1:3:2:3:1:2:1:1:5:1:2:4:2:2:1:1:1:11:1:1:11:1:1:1:1:1:2:2:4:1:1:2:1:1:1:2:1:2:1:2:1:3:3:2:1:3:2:1:11:1:1:1:1:1:2:1:1:4:2:6:3:2:1:2:2:2:1:2:4:2:1:1:11:1:1:1:1:1:1:3:5:1:5:2:2:2:2:1:2:2:2:4:1:1:11:1:1:11:1:1:1:1:1:1:2:1:6:1:4:1:1:6:1:1:3:1:2:1:1:2:2:1:1:1:11:1:1:1:1:1:1:1:2:7:4:1:1:1:1:3:2:4:2:4:2:2:1:11:1:1:1:1:1:1:1:1:1:11:1:1:1:1:1:1:1:1:1:10:1:1:11:1:1:10:10:10:10:10:3:1:1:1:1:10:9:1:7:3:6:1:2:1:4:2:5:1:2:1:10:8:2:6:1:3:4:1:1:1:1:2:1:3:1:1:2:3:1:11:1:10:8:1:1:5:1:4:2:2:2:1:3:2:2:1:1:3:2:1:2:1:1:10:7:3:4:2:4:1:2:1:2:1:1:1:1:2:1:1:2:1:2:1:1:1:1:2:1:10:7:2:1:4:1:1:1:2:3:2:6:2:1:1:2:5:1:11:1:10:7:1:2:3:2:2:2:1:3:2:1:4:2:8:3:1:1:1:1:10:7:1:1:1:3:1:1:3:4:2:1:5:3:2:1:4:1:2:1:1:1:10:6:4:2:3:3:2:1:1:1:6:1:1:1:1:1:5:1:1:11:1:1:1:10:6:3:1:2:2:1:2:1:1:1:1:1:2:3:1:3:2:2:1:4:1:1:1:2:1:10:6:2:2:2:1:1:5:4:1:1:2:1:1:2:1:1:3:1:3:1:1:1:2:1:10:6:2:1:1:1:5:1:1:2:3:3:1:1:1:2:4:1:1:1:1:1:1:1:11:1:1:10:6:1:3:1:2:8:2:3:10:4:2:2:1:10:6:1:2:1:1:2:1:1:2:1:3:2:1:1:1:1:8:2:2:3:2:1:10:6:1:1:2:1:1:2:1:5:1:6:2:1:1:1:2:2:2:1:1:1:1:11:1:1:10:6:1:1:1:1:1:1:1:1:2:1:1:1:1:10:5:6:2:1:1:10:5:4:5:1:3:2:2:5:1:2:1:2:1:4:3:1:2:1:10:5:3:5:1:1:1:2:4:2:1:2:5:2:5:1:1:1:11:1:1:10:5:3:1:3:1:4:1:4:2:4:1:1:1:1:2:1:4:1:3:1:1:1:1:10:5:2:4:4:2:1:2:3:1:1:4:1:1:2:2:2:1:1:1:1:2:1:1:1:10:5:2:2:2:2:1:2:2:1:1:4:1:1:3:1:1:2:5:2:1:11:1:1:1:10:5:2:1:3:1:1:4:2:6:1:10:4:3:1:1:1:1:10:5:2:1:1:1:4:4:1:5:2:4:1:3:2:2:1:1:1:1:1:2:1:10:5:1:4:2:5:2:1:2:4:4:1:4:1:1:2:2:11:1:1:10:5:1:3:1:2:1:1:2:1:3:1:2:2:2:3:1:3:1:1:2:1:1:1:1:1:2:1:10:5:1:2:2:1:2:2:1:4:2:2:2:3:1:1:1:1:2:1:1:3:1:1:2:1:1:10:5:1:2:1:1:1:1:1:3:3:3:2:3:2:1:3:4:1:1:1:1:11:1:1:1:10:5:1:1:2:5:1:1:1:1:3:2:1:3:2:3:2:1:1:1:2:1:3:1:1:1:1:10:5:1:1:1:4:2:1:1:5:6:1:2:3:1:1:1:3:4:1:1:1:1:1:10:5:1:1:1:1:2:3:1:2:1:2:1:1:2:3:1:1:1:1:1:2:1:3:1:3:11:1:10:4:7:1:2:1:1:2:3:2:1:3:1:1:2:1:2:3:3:1:2:1:1:1:10:4:5:1:3:5:2:3:1:4:1:1:2:5:3:1:1:1:2:1:10:4:4:2:2:1:3:1:1:1:1:2:2:1:1:2:1:2:1:3:2:2:1:12:1:1:1:1:10:4:4:1:1:1:2:1:5:1:2:4:1:2:1:1:4:1:2:1:2:2:1:1:10:4:3:3:1:1:1:1:1:1:5:1:1:1:1:1:2:5:1:2:3:1:3:1:1:1:1:10:4:3:1:4:3:2:4:4:1:1:1:1:1:1:1:1:2:1:1:2:2:1:11:1:10:4:3:1:1:2:3:1:2:2:6:1:1:1:1:3:1:1:3:1:1:1:1:1:1:2:1:10:4:2:5:1:1:2:1:2:2:1:4:1:10:3:2:2:1:1:10:4:2:3:1:2:5:2:1:4:2:2:2:2:1:2:1:3:3:11:1:10:4:2:2:2:1:3:1:2:2:2:2:1:1:1:10:3:2:1:2:1:10:4:2:2:1:1:1:1:1:1:2:2:3:3:6:1:2:1:3:2:3:2:1:10:4:2:1:2:3:2:3:4:2:4:1:3:1:1:2:1:4:2:11:1:10:4:2:1:1:3:2:1:4:1:1:6:2:3:2:1:1:4:4:1:1:1:1:1:10:4:2:1:1:1:1:4:3:1:1:1:1:1:4:1:1:3:2:2:1:1:1:1:1:1:2:1:1:1:10:4:1:5:2:1:1:1:4:2:1:1:1:1:2:3:10:1:1:11:1:10:4:1:4:1:1:1:1:8:2:2:1:1:1:1:3:1:2:1:4:2:2:1:10:4:1:3:1:3:3:2:4:1:1:1:1:4:3:3:1:2:1:1:2:2:1:10:4:1:2:4:2:1:2:1:2:1:1:1:1:3:1:2:2:3:2:1:1:1:1:1:1:1:11:1:10:4:1:2:2:1:3:3:1:3:1:4:2:1:2:1:1:2:2:1:1:1:1:1:2:1:1:1:10:4:1:2:1:2:1:3:1:5:1:1:1:1:1:4:1:3:1:2:1:1:1:1:2:2:1:10:4:1:1:9:1:2:2:2:7:3:1:1:1:1:1:1:3:1:11:1:10:4:1:1:3:2:5:3:1:2:1:1:2:2:6:2:2:2:1:2:1:1:10:4:1:1:2:3:1:1:1:1:1:1:1:3:2:2:5:7:2:2:1:2:1:10:4:1:1:2:1:1:2:1:1:1:1:5:1:1:2:1:3:2:1:1:1:3:1:3:1:11:1:10:4:1:1:1:3:1:1:1:1:1:1:6:2:2:2:5:1:2:1:1:4:2:1:10:4:1:1:1:1:4:1:1:1:1:1:2:2:1:1:1:3:1:1:1:1:1:4:2:1:1:1:2:1:1:1:10:4:1:1:1:1:1:1:6:5:1:2:1:1:4:3:1:1:1:1:1:2:1:11:1:1:10:3:7:1:4:1:3:4:5:3:1:1:2:1:2:1:1:1:1:1:2:1:10:3:5:6:2:1:1:1:2:1:2:2:2:1:1:1:1:4:1:1:1:2:1:2:1:10:3:5:1:2:2:3:2:1:1:5:1:2:1:1:2:1:1:4:1:1:2:11:1:10:3:4:3:3:1:2:1:2:2:1:2:1:1:3:3:8:1:2:1:1:10:3:4:2:1:1:1:3:2:2:3:2:2:2:1:1:3:1:1:2:2:4:1:1:1:1:1:10:3:4:1:1:3:1:5:2:1:4:1:2:1:5:1:2:2:3:11:1:10:3:3:5:1:1:1:1:2:1:3:3:2:1:1:1:1:2:2:3:2:1:2:2:1:10:3:3:3:1:1:3:2:1:1:1:1:1:4:3:7:2:3:1:1:2:1:10:3:3:2:1:4:1:2:1:1:1:3:1:1:2:2:1:4:3:2:1:2:11:1:1:10:3:3:1:4:1:2:1:2:3:2:1:1:3:5:5:2:1:1:2:1:1:10:3:3:1:2:1:1:10:3:3:1:2:1:1:8:2:2:1:10:3:3:1:1:1:4:2:4:1:1:1:1:2:1:1:2:2:2:1:1:2:1:1:1:2:11:1:10:3:2:6:1:2:2:3:1:1:3:1:3:6:2:1:3:2:2:1:10:3:2:4:1:1:6:1:1:1:1:2:3:1:1:1:1:2:1:1:2:2:4:2:1:10:3:2:3:1:4:1:2:1:6:1:5:1:2:4:2:2:12:1:1:1:10:3:2:2:4:1:1:2:2:1:2:2:3:3:1:2:2:1:1:1:4:3:1:1:1:10:3:2:2:2:1:1:2:4:1:1:1:5:5:1:1:2:1:1:3:3:2:1:10:3:2:2:1:1:3:2:1:1:1:1:2:1:6:1:2:2:1:1:2:2:1:13:1:1:1:1:10:3:2:1:4:4:1:1:1:4:1:2:5:1:3:3:1:2:2:1:2:1:10:3:2:1:3:1:1:1:1:7:6:1:1:2:2:1:3:3:4:1:1:1:1:10:3:2:1:2:1:2:2:3:2:1:4:1:1:1:2:1:1:2:1:1:2:3:1:1:1:11:1:10:3:2:1:1:3:2:1:2:1:1:4:4:2:1:2:2:1:1:1:3:4:2:1:10:3:2:1:1:1:5:1:1:2:9:1:1:4:1:2:3:1:2:2:1:1:10:3:2:1:1:1:1:1:5:3:3:1:3:4:5:1:1:2:2:12:1:1:1:10:3:1:6:1:1:2:1:2:1:1:2:1:1:3:1:1:2:2:3:3:2:1:1:2:1:10:3:1:4:3:2:1:4:1:3:1:1:1:2:1:2:1:5:1:6:1:1:1:1:1:1:10:3:1:4:1:1:1:4:2:2:1:1:3:1:1:3:1:2:3:1:4:2:11:1:10:3:1:3:2:3:1:3:1:1:1:3:1:3:10:2:4:2:1:10:3:1:3:1:2:2:4:1:1:1:2:1:1:2:3:2:1:1:1:1:2:1:1:2:2:2:1:10:3:1:2:7:2:2:1:2:2:4:1:5:2:1:1:1:4:1:11:1:10:3:1:2:3:1:4:3:3:1:3:3:2:1:2:1:4:3:1:1:2:1:10:3:1:2:2:2:1:1:1:1:1:1:3:1:1:1:1:2:3:6:1:1:1:2:4:1:1:1:1:10:3:1:2:1:4:1:1:2:2:1:4:1:4:4:2:4:4:1:11:1:10:3:1:2:1:2:1:1:1:2:1:1:3:1:4:2:1:1:1:1:1:1:1:1:2:1:2:4:1:1:1:1:10:3:1:2:1:1:1:2:1:2:3:6:3:3:5:1:1:3:4:1:1:1:1:10:3:1:1:5:1:1:3:2:2:1:1:2:4:7:2:2:1:1:12:1:1:1:10:3:1:1:3:3:1:4:1:1:2:1:2:2:2:2:1:1:1:1:3:6:1:1:1:1:1:1:1:10:3:1:1:3:1:1:1:1:4:1:1:1:3:2:1:2:4:4:6:2:1:1:10:3:1:1:2:2:2:1:3:2:1:2:1:1:3:2:2:5:1:2:2:1:12:1:1:1:10:3:1:1:2:1:2:1:1:2:3:1:6:2:1:1:2:1:2:3:1:2:1:3:1:1:1:10:3:1:1:1:5:1:2:1:1:1:3:2:2:1:1:4:2:2:2:2:3:2:1:1:10:3:1:1:1:3:1:1:1:1:2:1:2:2:1:2:1:2:2:4:2:1:2:2:2:11:1:1:10:3:1:1:1:2:1:1:10:3:1:1:1:2:1:1:9:4:1:1:1:1:10:3:1:1:1:1:2:5:2:1:1:6:3:2:2:2:3:1:1:3:1:2:1:10:3:1:1:1:1:1:2:3:1:4:1:3:2:6:1:1:3:2:1:2:2:11:1:10:2:10:2:10:2:10:2:3:1:2:1:10:2:7:1:2:1:1:3:3:2:1:1:1:2:2:5:4:3:1:2:1:10:2:6:1:2:3:1:4:2:2:5:2:1:1:3:1:1:1:1:1:1:1:11:1:10:2:5:3:1:1:5:2:1:1:2:3:1:2:3:3:2:1:2:2:2:1:10:2:5:1:3:1:1:1:4:3:2:3:1:1:1:1:1:2:1:1:3:1:4:1:1:1:1:10:2:4:7:4:1:2:1:1:5:1:1:1:1:7:2:12:1:1:1:10:2:4:3:1:2:2:1:3:1:1:4:1:3:5:1:1:1:1:3:3:1:1:1:10:2:4:2:1:2:3:2:5:1:4:1:3:1:1:5:3:1:1:2:1:10:2:4:1:3:1:1:2:3:2:2:1:1:2:2:1:2:4:1:1:3:2:1:11:1:10:2:4:1:1:2:4:2:2:2:1:2:3:1:1:3:3:1:2:1:3:2:1:1:10:2:3:7:2:1:1:4:4:1:3:1:1:1:1:3:1:1:1:2:1:1:2:1:10:2:3:4:1:1:2:3:1:1:2:2:1:4:1:1:1:1:3:2:3:3:11:1:10:2:3:3:1:2:1:1:1:2:1:2:2:2:3:2:1:6:1:1:1:3:1:2:1:10:2:3:2:2:3:3:2:4:1:1:1:1:1:1:1:1:2:2:1:2:2:1:1:2:2:1:10:2:3:2:1:2:1:2:4:4:3:3:1:1:3:2:3:2:13:1:1:1:1:10:2:3:1:5:1:1:1:1:1:1:3:1:6:2:1:6:1:1:1:1:1:2:1:1:10:2:3:1:2:4:2:1:1:1:2:1:1:1:1:2:1:1:1:4:2:2:1:1:2:2:2:1:10:2:3:1:2:1:1:1:1:5:1:1:1:1:1:1:1:1:2:2:1:3:5:2:12:1:1:1:10:2:3:1:1:2:1:3:2:1:2:2:1:1:3:5:1:2:2:1:2:3:2:1:1:10:2:3:1:1:1:1:2:3:1:2:1:2:2:1:1:1:2:2:1:1:2:2:1:2:1:2:2:1:1:10:2:2:5:6:1:2:1:1:3:1:2:2:1:3:5:1:1:1:1:1:11:1:10:2:2:4:2:1:5:3:1:4:1:1:10:2:1:3:2:1:10:2:2:3:3:10:2:2:3:3:9:2:1:2:1:10:2:2:3:1:3:9:1:1:1:1:1:1:3:2:1:7:1:1:11:1:1:10:2:2:2:10:2:2:2:10:2:2:2:2:2:2:1:10:2:2:2:3:1:1:4:1:1:2:1:2:6:1:3:2:3:1:1:4:1:1:1:1:10:2:2:2:2:1:1:4:1:1:2:5:3:1:2:2:1:2:1:1:1:3:12:1:1:1:10:2:2:2:1:2:2:2:1:10:2:2:2:1:2:2:2:1:2:3:1:1:1:10:2:2:2:1:1:1:3:4:1:1:1:1:3:1:2:10:2:2:1:1:2:1:10:2:2:1:5:1:1:1:3:4:1:2:2:3:3:1:2:2:1:3:1:11:1:1:10:2:2:1:3:2:1:5:2:1:1:1:3:1:1:5:2:3:1:1:1:3:2:1:10:2:2:1:2:3:3:2:1:4:3:1:2:4:1:2:2:2:1:1:1:1:2:1:1:10:2:2:1:2:1:3:1:2:2:1:1:1:2:3:1:3:1:1:1:1:6:1:1:1:11:1:1:10:2:2:1:1:5:5:1:1:1:1:1:1:1:1:2:2:4:1:1:1:2:1:1:1:1:2:1:1:10:2:2:1:1:2:5:1:1:1:2:2:2:1:2:1:1:2:1:3:3:3:1:2:2:1:10:2:2:1:1:2:1:1:1:1:1:1:2:1:1:1:1:3:1:7:1:4:2:3:1:11:1:10:2:2:1:1:1:2:1:1:2:1:2:1:1:2:2:1:4:1:5:2:2:2:1:2:2:1:10:2:2:1:1:1:1:1:1:2:1:1:1:1:7:1:4:2:1:1:1:2:1:2:1:2:1:2:1:10:2:1:6:1:2:4:2:1:1:1:1:4:1:5:1:1:2:1:2:1:1:11:1:1:10:2:1:5:1:1:4:2:2:4:3:1:10:2:1:2:1:2:1:10:2:1:4:2:2:1:1:2:3:2:1:1:1:2:1:1:2:2:2:1:2:1:1:1:1:1:2:1:1:10:2:1:4:1:1:2:1:5:1:3:1:2:1:2:4:4:1:2:1:1:1:11:1:1:10:2:1:3:3:2:2:4:3:3:1:1:2:2:1:2:1:1:3:1:1:3:1:1:1:1:10:2:1:3:2:1:2:5:1:2:1:3:1:1:1:2:1:1:2:4:3:1:3:1:1:1:10:2:1:3:1:2:3:1:1:5:1:4:6:1:1:2:2:1:2:1:1:11:1:1:10:2:1:3:1:1:1:3:1:8:1:1:6:2:1:1:1:1:1:4:1:2:1:1:10:2:1:2:4:3:2:2:2:3:1:2:1:1:2:3:4:1:1:2:2:2:1:1:10:2:1:2:3:1:3:3:1:1:1:1:1:3:1:2:3:3:1:1:3:1:2:1:11:1:1:10:2:1:2:2:2:3:2:10:2:1:2:2:2:3:2:2:2:2:1:10:2:1:2:2:1:1:3:2:2:2:1:1:1:2:1:3:1:2:3:1:1:2:4:1:2:1:10:2:1:2:1:3:3:1:5:1:4:1:1:2:1:4:1:2:2:1:3:1:11:1:10:2:1:2:1:2:1:3:1:1:3:2:1:1:1:2:1:1:1:1:3:3:1:1:6:1:1:1:1:1:1:1:10:2:1:2:1:1:2:2:4:2:4:1:2:4:4:2:1:1:1:1:2:3:1:1:1:10:2:1:2:1:1:1:1:2:2:3:1:1:2:2:5:1:3:3:4:3:1:11:1:10:2:1:1:5:2:1:3:2:3:2:1:2:8:1:1:2:5:1:1:1:1:1:1:10:2:1:1:4:1:2:1:1:1:3:1:2:2:1:1:3:1:3:2:1:1:4:2:1:2:1:10:2:1:1:3:2:1:3:4:3:1:3:1:5:1:1:4:4:1:11:1:1:10:2:1:1:3:1:1:1:2:2:2:1:1:1:6:1:4:2:1:4:2:2:1:2:1:10:2:1:1:2:3:1:1:3:3:2:1:6:10:1:4:1:2:1:10:2:1:1:2:2:1:1:1:1:2:2:5:2:1:2:2:3:4:2:1:13:1:1:1:1:1:10:2:1:1:2:1:1:6:1:2:1:1:1:2:2:2:1:5:5:2:1:2:2:1:10:2:1:1:1:7:1:2:3:1:1:4:1:1:2:1:1:9:2:1:2:1:10:2:1:1:1:3:3:2:1:1:1:1:5:5:1:1:1:4:1:2:3:1:1:11:1:10:2:1:1:1:2:3:4:1:4:1:10:2:1:1:1:2:2:3:1:1:1:1:10:2:1:1:1:2:1:2:2:1:1:1:3:1:1:1:4:1:4:1:2:3:3:2:1:2:1:10:2:1:1:1:1:4:1:1:1:5:1:4:5:2:1:1:5:2:2:11:1:1:10:2:1:1:1:1:2:1:3:3:1:1:1:1:2:1:1:1:1:3:1:2:2:2:3:1:2:1:2:1:10:2:1:1:1:1:1:2:2:1:4:3:3:3:1:1:1:1:1:1:1:1:2:4:3:1:1:1:1:10:2:1:1:1:1:1:1:1:1:2:10:2:1:1:1:1:1:1:1:1:3:1:1:1:1:1:1:11:1:1:10:1:8:1:1:1:5:3:2:1:3:2:1:1:2:1:1:1:1:1:2:1:1:2:1:1:10:1:6:5:3:2:1:2:1:3:1:1:1:2:2:1:1:2:3:2:1:2:1:1:10:1:6:1:3:1:2:1:2:1:2:2:3:6:1:1:3:1:14:1:1:1:1:1:1:10:1:5:4:2:1:1:3:4:2:6:1:3:1:2:1:1:1:2:1:2:1:10:1:5:2:1:6:1:1:1:1:1:1:1:1:3:1:1:1:2:3:1:1:1:1:3:1:2:1:10:1:5:1:2:2:2:1:2:3:1:2:2:1:1:3:2:1:2:2:1:2:1:1:11:1:1:10:1:5:1:1:1:1:2:1:6:1:2:7:2:2:2:1:2:1:1:2:2:1:10:1:4:3:6:3:1:1:1:1:1:2:2:2:7:1:1:3:2:1:1:1:10:1:4:2:4:1:3:1:1:3:2:2:1:1:10:1:4:1:11:1:10:1:4:2:1:2:1:3:10:1:4:2:1:2:1:3:2:1:1:2:1:10:1:4:1:3:3:2:1:3:4:3:1:1:1:7:6:2:1:1:10:1:4:1:2:1:1:3:2:3:1:3:5:1:1:3:2:1:1:1:1:1:1:2:11:1:10:1:4:1:1:1:10:1:4:1:1:1:10:1:2:2:2:1:1:10:1:3:7:1:1:5:4:1:1:1:1:1:2:2:5:1:1:1:2:2:1:1:10:1:3:4:2:1:1:3:5:2:2:3:2:2:1:1:5:2:1:11:1:1:10:1:3:3:2:2:2:1:2:1:1:2:10:1:3:3:1:4:1:1:1:1:1:10:1:3:3:1:1:1:1:2:2:1:2:1:1:1:1:2:5:2:2:2:3:3:2:1:1:10:1:3:2:2:3:2:2:1:3:6:1:2:2:1:1:2:6:11:1:1:1:10:1:3:2:1:3:1:2:1:3:1:10:1:3:2:1:3:1:1:2:1:1:1:10:1:3:2:1:1:1:2:1:6:2:1:1:1:1:1:4:1:2:2:2:2:1:1:2:1:1:1:10:1:3:1:4:1:1:1:2:1:8:1:1:1:1:1:1:1:1:3:1:3:1:1:1:1:11:1:10:1:3:1:2:4:1:1:1:2:1:2:1:3:3:4:1:1:2:2:4:1:1:2:1:10:1:3:1:2:1:1:5:1:1:2:1:2:1:1:1:2:2:3:3:1:3:2:2:1:1:1:10:1:3:1:1:3:1:2:3:1:2:10:1:3:1:1:3:1:1:2:11:1:1:10:1:3:1:1:1:4:5:3:2:3:1:2:3:1:1:1:1:2:2:1:2:1:2:1:10:1:3:1:1:1:1:2:1:1:4:3:1:4:4:2:1:1:3:5:1:1:2:1:10:1:2:6:1:5:1:2:2:1:2:1:1:3:3:2:1:1:2:1:1:2:11:1:1:10:1:2:4:5:2:1:1:1:2:2:1:1:3:2:2:1:1:2:1:5:1:1:2:1:10:1:2:4:1:2:1:2:1:1:4:2:1:1:2:2:1:4:1:1:4:2:1:2:1:1:10:1:2:3:3:1:4:1:2:1:1:4:10:1:2:3:1:1:1:11:1:10:1:2:3:1:4:5:1:1:3:1:2:1:2:1:2:3:1:1:1:1:2:2:2:1:1:10:1:2:3:1:1:2:1:1:1:1:1:2:1:1:1:2:1:1:2:2:5:2:3:1:1:2:1:1:1:10:1:2:2:4:1:2:1:1:1:3:1:3:4:1:2:4:4:1:1:1:1:1:11:1:10:1:2:2:2:3:10:1:2:2:2:3:9:1:1:2:1:1:10:1:2:2:2:1:1:2:3:6:1:1:1:1:1:2:1:1:1:2:3:2:1:1:1:1:11:1:1:10:1:2:2:1:2:5:2:3:1:1:1:3:1:7:2:1:1:1:3:1:2:1:1:10:1:2:2:1:1:3:1:1:3:1:3:2:1:2:1:1:2:4:1:1:5:1:1:1:11:1:10:1:2:2:1:1:1:1:1:2:1:1:1:3:5:2:2:4:2:1:3:1:2:1:2:1:1:10:1:2:1:4:2:3:2:1:1:2:1:1:1:2:1:1:1:1:1:1:2:2:1:1:2:1:1:1:11:1:1:10:1:2:1:3:2:2:1:1:2:1:1:2:3:2:1:1:1:1:1:1:1:1:3:2:5:1:1:1:1:1:1:10:1:2:1:2:5:2:1:1:10:1:2:1:2:5:15:1:1:1:1:1:1:1:10:1:2:1:2:2:1:2:4:2:1:2:2:3:1:2:2:2:1:2:4:3:1:1:1:1:10:1:2:1:2:1:1:4:1:2:4:2:5:3:1:1:2:1:1:3:1:2:11:1:1:10:1:2:1:1:4:3:2:10:1:2:1:1:4:3:2:2:1:2:1:1:10:1:2:1:1:2:5:6:1:3:1:1:1:3:1:2:1:1:1:2:1:3:1:11:1:1:10:1:2:1:1:2:1:1:4:1:1:1:2:1:6:3:1:5:1:1:1:2:3:1:1:1:1:10:1:2:1:1:1:2:4:5:1:1:1:3:1:2:1:3:2:2:4:2:1:11:1:1:10:1:2:1:1:1:1:2:3:2:2:3:3:1:2:1:2:6:1:1:2:1:1:1:1:2:1:10:1:1:10:1:1:10:1:1:10:1:1:13:1:1:1:1:1:10:1:1:6:1:2:3:2:1:4:2:1:3:1:3:3:1:1:1:1:2:1:1:2:1:10:1:1:5:1:3:1:4:1:1:3:2:4:1:1:1:1:2:3:3:1:1:11:1:1:10:1:1:4:3:1:7:1:4:1:5:2:4:2:1:5:1:1:1:1:1:1:10:1:1:4:1:3:2:3:2:5:2:1:1:1:1:1:1:2:2:2:15:1:1:1:1:1:1:1:10:1:1:4:1:1:1:1:1:3:1:1:4:4:2:2:1:1:3:1:1:1:1:1:1:1:2:1:1:1:10:1:1:3:3:2:1:1:1:1:1:3:1:2:1:1:1:1:2:1:4:2:2:1:1:1:1:1:11:1:1:10:1:1:3:2:1:4:1:4:2:2:4:3:3:1:5:1:1:1:1:1:2:1:10:1:1:3:1:3:2:1:2:2:1:1:5:1:1:2:1:2:1:4:1:1:3:11:1:1:1:10:1:1:3:1:1:3:2:2:2:1:1:3:4:1:1:2:2:2:1:3:3:1:2:1:1:10:1:1:3:1:1:1:1:1:1:1:1:4:1:10:1:1:3:1:1:1:1:1:1:1:1:1:11:1:10:1:1:2:4:1:3:1:1:1:2:2:1:4:2:2:2:2:1:1:1:1:3:1:2:1:1:1:10:1:1:2:3:1:2:4:1:1:1:2:3:3:2:1:1:1:9:12:1:1:1:1:10:1:1:2:2:3:1:1:1:3:1:1:2:4:2:3:3:1:2:1:1:2:1:1:2:1:1:1:10:1:1:2:2:1:2:2:4:2:2:1:3:1:3:1:1:1:1:2:1:1:1:2:1:1:1:1:11:1:10:1:1:2:1:5:2:2:3:1:2:1:1:1:1:2:10:1:1:1:1:1:2:1:1:10:1:1:2:1:2:6:2:4:2:1:1:1:3:1:1:9:13:1:1:1:1:1:10:1:1:2:1:2:1:1:1:5:2:2:2:2:1:1:1:1:3:1:1:1:1:2:2:2:1:1:2:1:10:1:1:2:1:1:2:2:1:1:1:1:1:3:2:1:1:1:4:1:4:2:1:2:1:1:1:11:1:1:1:10:1:1:2:1:1:1:1:3:1:1:1:2:2:3:1:1:1:4:2:1:1:1:1:2:1:1:1:1:1:2:1:1:10:1:1:1:6:1:1:1:1:1:1:1:1:5:1:1:1:1:10:1:1:1:2:11:1:1:10:1:1:1:4:2:1:4:2:4:4:2:1:1:4:1:2:6:2:1:10:1:1:1:3:3:4:1:2:1:1:1:1:2:2:1:3:2:1:2:1:1:1:2:2:11:1:1:1:10:1:1:1:3:1:3:2:1:2:4:1:1:1:2:1:2:1:1:1:1:3:1:1:2:1:1:2:1:1:1:10:1:1:1:2:6:2:3:1:1:1:1:3:1:1:3:1:2:2:1:1:2:1:1:1:1:1:11:1:10:1:1:1:2:3:1:1:1:1:1:1:2:3:5:6:4:2:6:1:1:1:1:1:1:1:10:1:1:1:2:2:1:1:1:2:3:1:1:1:3:1:1:1:1:1:1:1:1:5:5:1:11:1:1:1:10:1:1:1:2:1:2:1:2:2:1:1:6:2:2:3:1:2:1:3:1:1:1:1:2:2:1:10:1:1:1:2:1:1:1:1:4:2:1:1:1:2:1:2:1:3:1:1:4:2:6:11:1:10:1:1:1:1:4:2:1:2:1:2:2:3:1:4:4:8:4:1:1:1:1:1:10:1:1:1:1:3:1:2:2:3:3:1:2:5:2:1:3:1:2:1:2:1:1:1:11:1:1:10:1:1:1:1:2:2:3:1:1:2:1:3:1:1:2:4:1:1:1:1:1:1:2:4:1:2:1:1:1:10:1:1:1:1:2:1:1:3:3:1:1:1:1:3:1:3:1:1:3:2:1:1:1:1:2:1:12:1:1:1:1:10:1:1:1:1:1:3:4:1:6:2:2:1:2:1:2:7:2:3:2:1:10:1:1:1:1:1:2:1:5:1:4:1:5:5:1:2:2:2:1:1:1:1:1:1:11:1:10:1:1:1:1:1:1:2:6:4:1:1:1:3:2:3:2:5:3:2:2:1:10:1:1:1:1:1:1:1:1:10:1:1:1:1:1:1:1:1:9:6:11:1:9:9:9:9:9:6:2:1:9:8:1:6:3:5:1:2:1:3:2:4:1:3:12:1:1:1:1:9:7:2:5:1:3:3:1:1:1:1:1:4:1:1:2:1:1:3:2:2:1:1:1:9:7:1:1:4:1:4:1:2:2:1:2:1:1:1:6:3:2:1:11:1:1:1:9:6:3:3:2:3:3:1:1:3:1:6:2:1:3:1:2:1:2:1:1:9:6:2:1:3:1:1:1:2:1:1:2:2:7:1:1:3:2:15:1:1:1:1:1:1:1:9:6:1:2:2:2:2:1:2:2:1:3:1:1:4:1:1:1:4:2:3:2:1:9:6:1:1:1:2:1:1:3:1:1:1:1:3:2:3:1:2:1:1:4:1:1:1:1:1:1:11:1:9:5:4:1:3:2:2:2:1:1:2:1:3:1:2:1:1:2:2:2:1:2:1:1:2:1:9:5:3:1:1:2:1:1:2:1:2:1:1:2:3:1:2:2:2:1:1:1:1:3:2:11:1:9:5:2:2:1:1:1:3:2:1:1:1:2:1:1:3:4:2:1:1:2:1:1:1:1:1:2:1:1:9:5:1:7:2:3:1:1:1:5:1:2:4:2:1:2:3:1:11:1:9:5:1:2:3:3:4:4:2:2:1:4:1:2:2:1:2:1:3:1:1:1:1:9:5:1:1:3:8:1:3:2:2:2:1:6:3:1:13:1:1:1:1:1:9:5:1:1:1:2:2:1:1:1:2:1:2:1:1:1:1:1:2:2:2:1:3:1:4:1:2:1:1:1:9:4:6:1:1:1:1:1:1:1:2:1:1:2:1:1:1:1:9:4:1:1:1:11:1:9:4:4:1:2:4:1:8:1:3:1:2:3:1:1:2:2:1:2:1:1:1:9:4:3:2:1:3:4:2:1:1:4:3:1:1:1:2:1:2:1:1:3:1:11:1:9:4:3:1:1:1:1:1:5:1:1:2:4:1:2:3:1:2:4:2:1:2:1:1:1:9:4:2:2:4:1:2:1:1:3:1:2:4:1:1:1:1:1:1:1:2:1:1:1:13:1:1:1:1:1:9:4:2:1:3:3:4:1:2:2:2:5:1:1:1:1:1:5:2:2:1:1:9:4:2:1:1:2:1:1:1:4:2:5:1:3:4:1:2:1:1:1:1:3:11:1:9:4:1:4:2:2:2:1:5:6:1:1:3:7:1:1:1:1:2:1:9:4:1:3:1:1:1:9:4:1:3:1:1:1:8:13:1:1:1:1:1:9:4:1:2:1:4:2:1:1:1:1:4:1:3:1:1:1:1:1:1:3:2:2:1:3:1:1:1:1:9:4:1:1:4:2:3:4:2:1:1:1:1:1:6:1:1:3:1:1:2:1:11:1:9:4:1:1:2:1:4:1:1:1:2:2:2:1:3:1:1:3:2:1:1:3:2:2:2:1:9:4:1:1:1:2:1:3:1:1:1:2:5:1:1:1:4:1:1:1:1:1:1:1:1:3:11:1:1:9:3:9:3:9:3:9:3:3:2:1:9:3:5:2:2:2:1:1:1:3:1:9:3:5:1:2:1:11:1:9:3:4:2:2:9:3:4:2:2:8:4:1:1:1:1:9:3:4:1:1:1:1:2:2:1:2:1:1:1:3:2:1:1:4:2:1:1:2:2:2:11:1:9:3:3:2:3:1:1:3:1:1:2:2:1:1:1:1:1:2:3:4:1:4:2:1:1:9:3:3:1:2:4:2:1:3:1:2:1:1:1:1:3:1:2:2:1:5:12:1:1:1:9:3:3:1:1:1:1:2:1:1:1:1:1:2:4:1:1:4:2:5:1:1:2:2:2:1:9:3:2:3:3:1:2:1:1:3:3:2:1:2:1:1:2:1:1:7:1:1:11:1:9:3:2:2:2:3:4:1:1:3:3:3:2:1:1:1:3:1:1:1:2:2:2:1:9:3:2:2:1:1:1:1:2:1:1:1:1:3:1:4:1:1:3:2:1:2:3:3:1:11:1:9:3:2:1:2:2:2:5:2:9:3:2:1:2:2:2:1:1:2:1:9:3:2:1:1:2:2:1:2:3:1:1:2:2:5:1:1:1:1:1:1:1:2:1:1:3:11:1:9:3:1:8:1:1:3:6:2:2:2:1:4:6:2:2:1:9:3:1:4:1:5:1:1:2:1:1:2:2:1:4:1:1:2:2:7:11:1:9:3:1:3:2:1:1:1:2:2:1:1:3:1:1:2:1:2:2:1:2:2:4:1:2:2:1:9:3:1:2:4:1:3:1:1:1:3:1:2:2:1:3:1:1:8:2:1:11:1:9:3:1:2:2:1:1:2:1:1:3:1:1:3:2:1:1:1:1:1:3:1:1:1:1:3:1:1:1:2:1:9:3:1:2:1:1:2:2:2:2:1:2:1:1:1:2:4:1:1:3:3:2:2:1:1:11:1:9:3:1:1:4:1:2:3:4:2:1:2:3:1:2:1:3:1:4:4:1:1:1:1:9:3:1:1:2:3:2:3:3:5:1:1:5:1:1:1:2:2:1:14:1:1:1:1:1:9:3:1:1:2:1:1:1:2:2:1:6:1:2:1:3:3:4:2:1:1:1:1:1:2:1:9:3:1:1:1:2:2:2:1:1:2:1:3:1:3:4:1:2:2:2:2:3:12:1:1:1:9:3:1:1:1:1:2:1:1:2:4:1:1:1:2:5:2:2:3:2:1:2:1:1:1:2:1:9:2:8:1:1:1:6:2:1:2:4:5:1:1:3:1:1:1:1:11:1:9:2:5:6:2:1:2:2:3:3:1:3:2:1:1:1:3:1:1:3:1:1:1:9:2:5:1:1:2:4:3:1:1:3:1:5:2:2:2:1:1:1:1:1:2:11:1:9:2:4:2:3:1:1:1:2:1:3:1:1:1:4:2:1:4:1:1:1:4:2:1:1:9:2:4:1:2:1:2:1:1:3:1:1:4:1:2:4:2:1:3:2:1:1:12:1:1:1:9:2:3:4:9:2:3:4:9:2:3:2:1:1:9:2:3:2:4:3:1:1:1:1:1:1:1:2:2:1:2:2:1:1:2:1:5:12:1:1:1:9:2:3:2:1:1:1:2:3:1:1:1:1:2:1:2:1:1:2:1:2:3:1:2:3:1:2:1:1:9:2:3:1:2:1:5:5:1:1:2:1:1:1:1:2:1:2:1:1:5:1:1:1:11:1:9:2:3:1:1:1:3:1:1:4:1:1:1:2:1:4:2:1:2:1:1:2:2:1:2:2:1:1:9:2:2:5:1:1:1:3:4:2:1:1:5:4:4:1:1:2:1:1:11:1:9:2:2:3:2:2:2:1:1:1:1:1:1:1:2:2:2:1:2:2:2:7:3:1:1:1:9:2:2:2:4:1:1:3:3:2:1:1:1:3:1:2:2:1:1:2:1:1:3:12:1:1:1:9:2:2:2:1:4:1:1:3:2:1:1:1:2:1:1:2:3:1:1:1:1:4:2:1:2:1:1:9:2:2:1:9:2:2:1:9:2:2:1:7:12:1:1:1:9:2:2:1:3:1:1:2:2:1:5:1:1:1:1:4:3:4:1:1:1:2:3:1:1:1:9:2:2:1:2:1:1:2:2:2:1:1:1:3:1:4:1:3:4:1:1:1:1:1:1:1:1:11:1:9:2:2:1:1:2:1:5:1:3:1:1:1:1:5:1:1:1:1:2:1:3:1:1:1:3:1:1:1:9:2:2:1:1:1:1:2:1:3:1:3:5:1:2:5:2:3:3:1:1:1:11:1:9:2:1:5:2:3:2:1:3:1:2:1:2:6:1:4:1:1:3:2:1:1:9:2:1:4:1:3:1:2:2:2:1:2:2:1:1:4:4:2:2:1:2:12:1:1:1:9:2:1:3:2:3:3:3:1:1:1:1:5:2:3:1:1:2:1:2:2:1:2:1:1:9:2:1:3:1:1:4:1:3:1:1:1:1:3:1:2:1:9:2:1:1:12:1:1:1:9:2:1:2:3:4:2:4:1:1:3:1:1:2:2:4:4:1:2:1:1:2:1:9:2:1:2:2:1:4:2:3:3:5:2:2:2:4:1:3:1:1:11:1:1:9:2:1:2:1:2:4:1:2:3:1:4:3:1:2:7:1:1:2:1:1:2:1:9:2:1:2:1:1:1:3:3:1:9:2:1:2:1:1:1:3:3:1:1:1:1:11:1:9:2:1:1:4:3:1:2:1:1:1:5:1:2:1:2:1:3:1:1:3:4:1:2:1:9:2:1:1:3:1:2:3:1:1:1:5:1:4:2:2:5:5:1:11:1:1:9:2:1:1:2:2:2:1:2:1:3:2:2:1:5:9:2:1:1:1:2:1:1:9:2:1:1:2:1:1:1:3:5:7:4:1:3:1:2:2:1:2:1:11:1:1:9:2:1:1:1:3:1:2:1:1:2:2:2:3:1:2:2:1:1:2:1:2:1:3:3:1:2:1:9:2:1:1:1:2:1:1:1:1:2:2:1:3:1:1:3:3:4:2:1:1:5:1:1:11:1:9:2:1:1:1:1:1:4:4:3:2:2:2:4:2:2:1:2:3:1:1:3:1:1:1:9:1:9:1:9:1:9:1:10:1:11:1:9:1:6:2:2:3:3:2:9:1:6:2:1:4:1:1:1:1:1:9:1:5:3:1:1:1:9:1:5:3:1:1:2:1:1:1:1:2:1:1:11:1:9:1:5:1:1:2:5:3:1:1:1:2:1:1:1:3:1:7:2:2:2:1:1:9:1:4:3:1:3:1:1:7:1:1:1:2:1:9:1:3:1:1:11:1:9:1:4:2:1:1:1:1:1:1:1:3:1:1:2:9:1:4:1:4:2:1:1:1:9:1:4:1:1:3:1:3:2:3:3:1:1:1:2:2:1:1:1:4:3:1:1:1:11:1:9:1:3:5:1:2:2:3:1:1:1:4:3:9:1:2:2:2:1:1:9:1:3:3:1:2:3:1:4:1:3:1:1:1:1:1:1:1:3:1:2:1:2:13:1:1:1:1:1:9:1:3:2:2:1:1:3:2:1:1:2:1:5:2:1:1:1:1:4:1:2:2:1:2:1:1:9:1:3:1:5:2:3:2:1:1:1:5:1:2:1:4:1:1:2:1:2:1:11:1:1:9:1:3:1:2:2:1:1:4:3:3:1:1:2:2:3:2:2:2:3:3:1:1:1:1:9:1:3:1:1:2:2:1:1:1:3:1:1:1:1:4:2:1:1:2:1:2:2:1:2:1:2:11:1:1:9:1:2:9:1:2:9:1:2:9:1:2:1:2:1:1:1:9:1:2:4:2:1:5:2:2:2:5:1:1:1:1:4:2:3:1:1:11:1:1:9:1:2:3:2:2:1:1:2:6:6:1:1:2:1:2:4:3:1:1:2:1:9:1:2:2:6:2:1:1:3:1:3:2:4:3:1:5:1:1:1:1:1:11:1:9:1:2:2:2:2:1:1:1:4:1:1:2:3:2:1:1:1:1:5:1:1:1:1:1:1:1:2:1:1:9:1:2:2:1:2:1:2:1:1:1:1:2:5:1:1:1:1:1:1:1:3:2:3:2:2:1:11:1:9:1:2:1:5:3:2:3:2:1:3:1:4:1:1:2:1:3:3:1:1:1:2:1:9:1:2:1:3:1:2:1:1:2:3:5:3:2:2:1:2:1:6:2:11:1:1:9:1:2:1:2:1:3:2:1:1:1:4:2:1:1:4:1:1:2:1:1:1:1:1:6:1:1:1:1:1:1:1:9:1:2:1:1:3:2:2:4:1:1:1:4:2:2:1:4:1:7:1:1:11:1:9:1:2:1:1:1:4:3:1:1:1:1:1:2:2:1:3:2:1:4:5:2:1:1:2:1:9:1:2:1:1:1:1:1:2:2:1:6:1:1:1:1:2:1:1:2:5:1:2:2:12:1:1:1:1:9:1:1:5:3:1:2:2:1:2:2:3:1:1:3:3:2:1:1:2:2:3:1:1:1:1:9:1:1:4:2:1:4:3:2:3:1:2:4:5:1:2:2:2:11:1:1:1:9:1:1:3:4:2:1:3:3:1:3:2:1:2:2:1:1:1:1:2:1:1:3:2:1:1:1:9:1:1:3:2:1:1:1:1:3:1:2:1:2:1:3:2:1:9:1:1:2:1:1:11:1:9:1:1:3:1:1:1:3:3:1:2:1:1:3:2:1:1:4:4:3:1:1:3:1:1:1:1:9:1:1:2:3:3:1:1:3:2:1:3:2:4:1:1:1:1:1:1:1:4:2:1:1:11:1:9:1:1:2:2:2:1:1:3:2:2:1:3:9:1:1:2:2:1:3:1:2:1:1:9:1:1:2:1:3:3:2:1:1:1:3:4:4:1:4:1:1:1:2:3:1:1:11:1:9:1:1:2:1:1:3:2:9:1:1:2:1:1:3:2:8:1:1:2:1:1:9:1:1:1:9:1:1:1:9:1:1:1:9:1:1:1:12:1:1:1:1:9:1:1:1:4:1:1:2:2:3:4:2:1:1:1:3:1:1:2:1:1:6:1:2:1:1:9:1:1:1:3:1:1:2:6:1:2:2:2:3:2:1:3:2:1:1:2:1:2:11:1:9:1:1:1:2:2:2:1:1:2:9:1:1:1:2:2:2:1:1:2:5:1:1:2:1:9:1:1:1:2:1:1:2:3:1:1:4:1:1:1:1:1:1:1:5:4:2:1:2:1:1:11:1:1:9:1:1:1:1:3:3:2:1:1:1:1:2:1:3:4:5:4:1:4:1:2:1:9:1:1:1:1:2:1:4:1:4:5:2:2:1:3:2:6:1:1:1:1:11:1:1:9:1:1:1:1:1:2:5:4:1:1:3:2:2:2:8:6:2:1:9:1:1:1:1:1:1:1:9:1:1:1:1:1:1:1:9:1:1:1:1:1:1:1:1:11:1:1:1:8:8:8:8:8:8:3:1:1:1:1:8:7:1:5:3:4:1:2:1:2:2:4:1:1:1:1:2:2:1:11:1:1:1:8:6:2:4:1:3:2:1:1:1:1:1:1:2:1:1:1:1:2:2:1:1:3:1:1:1:2:1:1:8:6:1:1:3:1:3:3:1:6:2:3:3:19:1:1:1:1:1:1:1:1:1:1:1:8:5:3:2:2:3:1:1:1:1:1:3:1:1:4:1:1:1:1:3:4:1:1:2:1:1:8:5:2:1:2:1:1:1:1:1:3:1:2:1:1:2:1:5:3:2:1:2:2:11:1:1:8:5:1:2:1:2:1:3:6:2:2:4:4:1:1:1:2:2:3:1:1:1:1:8:5:1:1:1:1:1:1:2:1:1:1:8:5:1:1:1:1:1:1:2:1:1:2:1:11:1:8:4:3:4:1:2:2:1:4:1:1:3:1:1:2:2:1:1:2:2:5:1:1:1:1:1:1:8:4:2:3:5:2:1:1:1:1:5:1:1:6:2:2:2:13:1:1:1:1:1:8:4:2:1:2:1:1:7:1:2:4:3:2:5:3:1:3:2:1:8:4:1:3:2:4:2:3:1:1:8:4:1:3:2:1:12:1:1:1:1:8:4:1:2:1:1:2:1:2:3:2:3:8:4:1:2:1:1:1:2:2:1:8:4:1:1:1:4:2:1:1:2:1:1:1:3:1:1:2:1:3:1:1:1:1:1:1:1:3:1:11:1:8:3:6:2:1:2:4:1:1:5:1:4:1:3:1:1:1:1:3:2:1:1:8:3:4:1:2:4:2:2:1:3:1:1:1:4:3:1:2:2:1:2:12:1:1:1:8:3:3:2:1:1:1:4:4:3:1:1:5:8:3:1:2:1:1:8:3:2:4:4:2:1:8:3:2:4:4:1:14:1:1:1:1:1:1:8:3:2:2:1:2:8:3:2:2:1:2:9:1:1:1:1:1:1:2:1:8:3:2:1:1:8:3:2:1:1:8:3:2:1:1:3:12:1:1:1:8:3:1:5:1:2:4:1:3:8:3:1:5:1:2:3:1:1:1:8:3:1:3:1:1:2:2:1:1:3:4:2:1:2:1:2:2:2:1:1:1:2:1:1:1:11:1:8:3:1:2:1:2:3:1:4:1:2:1:1:1:1:3:1:1:1:2:1:1:1:5:1:2:1:1:8:3:1:1:3:1:3:4:1:1:5:2:1:6:1:4:4:11:1:1:8:3:1:1:1:3:3:1:3:2:2:1:2:3:4:1:1:1:2:1:1:2:1:1:1:2:1:8:3:1:1:1:1:1:1:2:1:8:3:1:1:1:1:1:1:2:1:7:13:1:1:1:1:8:2:5:2:1:2:2:1:3:1:4:3:2:1:1:2:2:2:4:2:2:1:8:2:4:1:6:1:3:2:1:1:1:4:1:1:1:1:1:1:2:2:1:1:1:3:11:1:8:2:3:3:2:2:3:1:1:3:1:1:1:4:1:1:2:1:3:2:1:1:3:2:1:8:2:3:1:3:2:2:2:1:1:4:5:1:1:3:3:2:1:1:3:1:11:1:8:2:2:8:2:2:8:2:2:8:2:1:3:2:1:8:2:2:3:1:1:3:5:3:2:3:4:1:1:1:2:6:1:11:1:1:8:2:2:2:1:1:4:1:1:1:2:1:2:3:1:1:1:4:2:5:4:2:1:1:8:2:2:1:2:2:1:8:2:2:1:2:2:1:8:2:1:1:12:1:1:1:8:2:2:1:1:1:3:1:1:2:2:1:2:1:1:2:1:1:4:1:2:8:1:1:2:1:8:2:1:4:4:1:1:1:1:1:1:2:3:1:1:2:1:1:8:2:1:1:1:11:1:1:8:2:1:3:1:5:1:1:1:2:1:4:1:2:2:3:2:4:3:1:2:2:1:8:2:1:2:2:5:3:1:1:1:3:1:2:4:2:2:3:1:1:4:11:1:1:8:2:1:2:1:1:5:1:1:1:1:2:1:1:2:1:2:2:1:2:5:1:2:3:1:2:1:8:2:1:1:3:3:5:2:5:1:1:1:1:1:1:2:4:1:2:1:1:1:1:11:1:1:8:2:1:1:2:1:3:1:1:1:1:2:1:1:3:1:1:3:5:1:2:3:2:1:1:2:1:1:8:2:1:1:1:2:2:1:2:3:4:6:4:1:1:6:1:1:1:1:11:1:1:8:2:1:1:1:1:1:1:2:8:2:1:1:1:1:1:1:2:8:1:4:1:2:1:8:1:6:1:1:1:3:3:2:1:1:2:1:1:1:2:2:2:1:3:2:1:2:1:1:11:1:8:1:4:5:1:2:1:1:3:2:1:1:1:1:5:2:2:2:1:1:1:3:1:2:1:8:1:4:1:2:3:1:8:1:4:1:2:3:1:7:2:11:1:1:8:1:3:3:6:1:1:1:1:2:1:2:3:3:1:1:2:5:1:1:1:1:2:1:1:8:1:3:2:1:1:5:1:1:3:3:1:1:1:1:8:1:3:1:3:1:11:1:8:1:3:1:1:5:2:1:2:4:2:3:1:1:2:1:1:3:3:2:1:2:1:1:1:8:1:2:5:2:1:2:1:2:3:1:1:1:1:1:1:3:2:1:1:1:2:3:1:2:11:1:1:8:1:2:3:1:2:1:2:8:1:2:3:1:2:1:2:7:1:2:1:1:1:8:1:2:2:1:4:2:2:1:1:8:1:2:2:1:4:2:2:1:1:1:11:1:1:8:1:2:1:3:8:1:2:1:3:8:1:2:1:3:3:1:1:2:1:8:1:2:1:2:1:1:1:1:2:2:1:2:1:5:1:1:1:2:1:1:2:1:2:3:2:11:1:1:1:8:1:2:1:1:1:2:1:1:2:3:2:2:1:1:4:3:1:3:2:6:1:2:1:1:8:1:1:5:1:1:1:1:1:1:4:1:1:1:8:1:1:4:17:1:1:1:1:1:1:1:1:1:8:1:1:3:2:3:3:2:1:3:1:6:2:2:2:1:2:1:6:1:1:1:1:1:1:1:8:1:1:2:5:1:3:2:1:2:2:1:4:1:1:1:2:1:2:1:2:1:1:2:1:11:1:1:8:1:1:2:2:1:1:2:1:1:1:2:1:2:1:3:2:2:3:2:1:1:5:3:2:1:8:1:1:2:1:1:1:3:2:1:1:2:2:1:2:1:1:4:1:1:1:1:1:1:4:2:1:11:1:1:1:8:1:1:1:3:2:2:1:1:3:1:1:2:2:1:3:3:8:1:1:1:1:2:1:1:1:8:1:1:1:2:1:3:1:8:1:1:1:2:1:3:1:8:1:1:1:1:1:1:11:1:8:1:1:1:1:2:4:4:1:3:2:1:3:1:1:1:3:3:2:3:1:3:1:1:1:1:8:1:1:1:1:1:1:8:1:1:1:1:1:1:8:1:1:1:1:1:2:1:1:1:1:1:1:11:1:1:7:7:7:7:7:7:6:1:2:1:7:6:1:4:3:3:1:2:1:1:2:3:2:1:1:1:4:1:2:1:2:11:1:7:5:2:3:1:2:7:5:2:3:1:2:7:1:1:1:2:1:7:5:1:1:2:1:3:1:1:1:2:2:2:1:2:2:1:1:1:1:3:1:2:2:2:11:1:1:7:4:3:1:2:2:1:1:3:7:4:3:1:2:2:1:1:2:1:2:1:1:1:7:4:1:7:4:1:7:4:1:7:4:1:1:11:1:7:4:1:1:3:1:1:1:6:1:2:2:2:1:2:1:2:4:2:4:1:2:1:7:3:5:1:1:1:1:1:2:1:2:1:1:1:7:3:4:7:11:1:7:3:3:1:1:3:1:2:2:5:2:1:1:4:7:3:1:1:2:1:1:7:3:2:1:3:3:1:1:1:1:1:2:1:2:3:1:2:1:7:3:1:1:1:11:1:7:3:1:3:4:1:1:1:1:4:1:2:3:2:4:2:2:1:4:1:1:2:1:7:3:1:1:7:3:1:1:7:3:1:1:7:2:3:10:1:1:7:3:1:1:1:1:4:4:2:1:3:2:1:1:1:3:1:1:3:3:2:1:1:1:2:1:7:2:4:4:1:2:1:1:1:1:5:1:3:2:2:1:4:2:1:1:3:2:1:7:2:3:2:2:1:1:1:1:1:2:2:1:2:2:7:2:3:2:1:2:1:10:1:1:7:2:2:4:1:1:2:6:1:1:1:1:1:2:2:2:1:1:4:1:5:2:1:1:7:2:2:1:4:1:1:2:1:2:3:4:1:5:7:2:1:2:2:1:1:7:2:1:7:2:1:7:2:1:7:2:1:6:2:1:10:1:7:2:1:2:7:2:1:2:7:2:1:2:7:2:1:1:1:2:1:1:7:2:1:1:7:2:1:1:7:2:1:1:7:2:1:1:3:2:1:1:1:7:2:1:1:1:4:2:3:3:1:3:7:2:1:1:1:4:2:1:1:10:1:1:7:1:7:1:7:1:7:1:7:1:6:3:1:1:1:1:7:1:4:2:2:1:3:2:1:1:3:5:1:1:1:2:1:2:2:1:1:1:1:1:1:2:1:1:7:1:3:2:3:4:2:1:5:1:1:1:1:1:3:1:2:2:1:2:1:1:1:1:1:10:1:7:1:3:1:1:1:2:1:3:4:1:1:2:3:2:2:4:6:3:1:2:1:7:1:2:2:3:1:4:1:1:2:1:1:1:2:1:7:1:2:2:3:1:1:1:2:1:1:7:1:2:1:2:3:1:1:2:7:1:2:1:2:3:1:1:2:6:1:1:10:1:1:7:1:1:7:1:1:7:1:1:7:1:1:7:1:1:2:1:2:1:1:7:1:1:3:1:2:1:5:2:2:1:2:4:3:7:1:1:2:2:1:2:1:7:1:1:2:1:2:2:1:1:3:1:7:1:1:2:1:2:2:1:1:3:1:3:1:10:1:1:7:1:1:1:2:3:4:3:1:4:1:2:2:2:1:1:1:1:1:5:1:2:2:1:1:1:7:1:1:1:1:1:7:1:1:1:1:1:7:1:1:1:1:1:6:7:1:1:1:1:1:1:1:1:6:6:6:6:6:6:10:11:1:1:1:1:6:5:1:3:3:2:1:1:4:2:3:1:1:1:1:2:1:1:1:3:1:2:1:1:2:1:1:6:4:2:2:1:2:1:4:1:1:1:3:2:1:1:1:1:2:1:1:2:2:3:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:3:2:1:10:1:6:3:2:3:1:1:1:3:6:3:2:3:1:1:1:3:5:3:2:1:6:3:1:2:2:3:5:1:1:3:1:1:1:1:2:1:3:2:2:1:1:4:1:2:1:6:2:6:2:6:2:6:2:6:2:5:12:1:1:1:1:6:2:3:1:1:3:2:1:1:1:6:2:3:1:1:3:2:1:1:1:6:1:1:1:1:1:1:1:6:2:2:1:1:3:3:1:4:6:2:2:1:1:3:3:4:3:1:1:1:1:6:2:1:2:1:6:2:1:2:1:6:2:1:2:1:6:2:1:2:1:10:1:6:2:1:1:1:1:2:6:2:1:1:1:1:2:6:2:1:1:1:1:2:4:1:2:1:1:6:1:4:1:1:1:1:3:1:1:6:1:4:1:1:1:1:3:1:1:5:2:1:2:1:6:1:2:3:6:1:2:3:6:1:2:3:6:1:1:12:1:1:1:1:1:6:1:2:1:1:2:2:1:3:1:1:1:1:3:4:2:3:1:6:1:3:1:2:1:1:6:1:1:2:2:6:1:1:2:2:6:1:1:2:2:6:1:1:1:2:2:1:1:6:1:1:1:1:6:1:1:1:1:6:1:1:1:1:6:1:1:1:1:5:1:10:1:1:1:5:5:5:5:5:5:5:5:5:1:1:2:1:1:5:4:1:2:3:1:1:1:2:1:1:3:2:1:5:4:1:2:2:3:2:2:1:5:3:2:1:1:1:2:1:2:5:3:2:1:1:1:2:1:2:5:3:1:1:1:10:1:5:2:4:1:1:1:2:2:1:1:5:2:4:1:1:1:2:2:1:1:4:2:1:2:1:5:2:1:3:5:2:1:3:5:2:1:3:5:2:1:3:1:1:2:1:1:5:1:5:1:5:1:5:1:5:1:5:1:5:1:3:1:1:10:1:5:1:2:2:1:1:1:3:2:5:1:2:2:1:1:1:3:2:5:1:1:3:1:2:1:5:1:1:1:5:1:1:1:5:1:1:1:5:1:1:1:5:1:1:1:4:1:1:2:1:1:4:4:4:4:4:4:4:4:4:4:4:1:1:1:10:1:4:3:1:1:2:1:4:3:1:1:2:1:4:3:1:1:2:1:4:2:2:3:1:1:1:4:2:1:1:2:4:2:1:1:2:4:2:1:1:2:4:2:1:1:2:3:2:2:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:1:2:10:1:1:3:3:3:3:3:3:3:3:3:3:3:3:3:3:1:1:2:1:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:2:1:1:2:1:1:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:1:2:1:10:1:1:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:5:5:2:2:6:2:6:2:2:3:3:4:2:2:2:4:2:4:2:2:2:3:2:2:3:6:2:2:3:2:3:2:3:2:4:3:5:7:5:3:5:2:2:3:2:4:2:3:2:6:3:3:3:2:2:2:2:2:2:3:3:2:5:3:2", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":86,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":859,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.788425664621422e-83,0,0,-1.4902745963168196e-98,0,0]]] +Shrunk 27238 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + + +--- +## Summary +- Passed: 9 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 366 tests +{ seed: 12345, path: "365:0:1:12:1:1:1:1:3:4:2:4:2:1:2:1:2:3:1:1:4:2:1:1:2:1:1:12:1:12:7:1:5:3:4:1:2:1:2:2:4:1:1:1:1:2:1:1:2:1:1:1:12:7:1:3:2:3:1:1:1:4:2:1:1:3:2:5:1:2:1:1:2:1:12:7:1:2:3:2:2:2:1:3:2:1:4:2:7:13:1:1:1:1:12:7:1:2:1:2:2:1:1:4:2:2:1:1:1:2:3:1:1:1:1:2:1:1:2:1:1:12:7:1:1:4:1:4:1:2:2:1:2:1:1:1:6:3:1:3:1:1:1:1:12:7:1:1:2:2:1:2:2:2:2:4:2:4:1:2:2:3:12:1:1:12:7:1:1:1:3:1:1:3:4:2:1:5:3:2:1:3:1:1:2:1:1:12:7:1:1:1:1:2:1:1:1:3:1:1:1:1:1:4:2:4:1:1:5:2:1:12:6:6:6:6:6:6:3:13:1:1:1:1:12:6:5:1:4:2:3:2:1:2:2:2:1:2:1:4:1:1:2:1:1:1:12:6:4:2:3:3:2:1:1:1:6:1:1:1:1:1:5:1:1:1:2:1:12:6:4:1:1:3:1:2:1:1:1:3:3:1:3:4:1:1:1:2:1:12:1:1:12:6:3:3:2:3:3:1:1:3:1:6:2:1:2:5:1:1:1:1:1:1:12:6:3:2:1:2:2:1:1:3:4:1:8:1:1:2:2:1:2:1:1:12:6:3:1:2:2:1:2:1:1:1:1:1:2:3:1:3:2:2:1:3:1:12:1:1:1:12:6:3:1:1:1:1:6:1:1:2:5:1:2:1:2:4:2:1:2:1:12:6:2:4:1:3:2:1:1:1:1:1:1:2:1:1:1:1:2:2:1:1:2:2:1:2:1:1:12:6:2:3:1:1:2:2:1:3:1:2:2:2:1:1:4:1:1:1:2:1:1:1:12:1:12:6:2:2:2:1:1:5:4:1:1:2:1:1:2:1:1:3:1:2:2:2:1:12:6:2:2:1:1:1:1:2:1:6:2:3:1:1:1:5:3:1:1:2:1:1:12:6:2:1:3:1:1:1:2:1:1:2:2:7:1:1:3:2:4:12:1:12:6:2:1:1:8:2:1:3:1:1:1:1:1:2:4:6:2:1:12:6:2:1:1:1:5:1:1:2:3:3:1:1:1:2:3:7:2:1:12:6:1:8:1:2:2:3:1:1:4:2:2:1:2:5:12:1:12:6:1:4:3:3:1:2:1:1:2:3:2:1:1:1:4:1:1:4:1:1:1:1:1:12:6:1:3:4:1:3:1:2:1:1:1:2:1:2:2:3:1:2:2:2:2:1:12:6:1:3:1:2:8:2:3:10:3:13:1:1:1:1:12:6:1:2:4:3:1:2:2:1:2:1:2:1:3:1:2:2:2:1:1:1:1:2:1:12:6:1:2:2:2:2:1:2:2:1:3:1:1:4:1:1:1:3:3:1:2:1:1:1:12:6:1:2:1:3:1:2:5:1:1:2:11:4:1:12:1:12:6:1:2:1:1:2:1:1:2:1:3:2:1:1:1:1:8:2:2:1:1:2:1:1:12:6:1:1:4:5:3:4:1:1:1:2:2:1:3:1:3:1:2:1:1:1:12:6:1:1:3:1:3:3:1:6:2:3:4:1:1:1:1:1:1:1:1:12:1:12:6:1:1:2:2:2:5:1:1:8:1:1:1:2:1:2:1:1:1:1:2:1:1:12:6:1:1:2:1:1:2:1:5:1:6:2:1:1:1:2:2:1:4:2:1:12:6:1:1:1:3:1:11:6:1:1:1:3:1:3:1:12:1:12:6:1:1:1:2:1:1:3:1:1:1:1:3:2:3:1:2:1:1:4:1:1:1:1:1:2:1:12:6:1:1:1:1:2:1:2:1:1:1:1:2:1:3:1:1:2:1:1:2:3:1:1:2:1:2:1:1:12:6:1:1:1:1:1:1:1:1:2:1:1:1:1:10:5:6:12:1:12:5:7:1:1:1:1:1:1:1:1:1:2:1:1:1:2:1:1:1:1:1:8:2:1:12:5:5:5:5:5:5:5:4:1:2:1:1:1:12:5:5:1:3:5:2:1:2:1:1:1:1:2:2:2:1:2:1:3:12:1:12:5:4:5:1:3:2:2:5:1:2:1:2:1:4:1:3:1:1:1:1:12:5:4:2:2:4:6:1:2:3:2:1:1:3:1:2:2:2:1:12:5:4:1:3:2:2:2:1:1:2:1:3:1:2:1:1:2:2:2:1:1:1:1:12:1:12:5:4:1:1:2:1:3:1:1:1:1:2:1:1:1:1:1:1:1:2:2:1:2:2:2:2:1:12:5:3:5:1:1:1:2:4:2:1:2:5:2:4:3:2:1:12:5:3:3:1:4:1:3:1:6:3:1:3:2:2:1:1:1:12:1:1:12:5:3:2:2:3:1:1:1:1:1:3:1:1:3:8:4:1:2:1:12:5:3:2:1:1:2:1:3:2:2:1:1:1:1:1:5:1:1:1:2:1:1:1:1:2:1:1:12:5:3:1:3:1:4:1:4:2:4:1:1:1:1:2:1:4:1:1:1:12:1:12:5:3:1:2:1:1:2:1:3:1:1:2:1:2:3:2:1:1:1:1:2:2:3:1:1:1:1:12:5:3:1:1:2:1:1:2:1:2:1:1:2:3:1:2:2:2:1:1:1:1:5:1:1:1:1:1:1:12:5:2:11:5:2:12:2:2:12:1:12:5:2:4:4:2:1:2:3:1:1:4:1:1:2:2:1:4:2:1:1:12:5:2:3:4:2:2:1:2:1:2:1:3:1:2:3:2:2:2:1:2:1:12:5:2:3:1:2:7:5:2:3:1:2:6:1:12:1:1:1:12:5:2:2:4:2:2:2:1:1:1:3:2:1:2:1:2:2:1:1:2:1:1:1:2:1:12:5:2:2:2:2:1:2:2:1:1:4:1:1:3:1:1:2:5:1:2:2:1:12:5:2:2:1:2:7:2:1:1:1:1:1:1:1:4:2:1:2:2:2:12:1:12:5:2:2:1:1:1:3:2:1:1:1:2:1:1:3:4:2:1:1:1:6:1:1:1:1:1:1:1:12:5:2:1:4:2:2:1:1:4:1:1:2:1:3:1:2:3:2:2:1:2:1:12:5:2:1:3:1:1:4:2:6:1:10:3:13:1:1:1:1:12:5:2:1:2:2:1:2:1:1:6:1:3:1:3:2:1:3:5:1:1:1:1:1:1:12:5:2:1:2:1:1:1:1:1:3:1:2:1:1:2:1:5:3:2:2:4:1:1:1:1:1:12:5:2:1:1:2:5:2:1:4:1:2:1:1:3:3:2:2:1:1:12:1:1:1:12:5:2:1:1:1:4:4:1:5:2:4:1:3:2:1:4:2:1:12:5:2:1:1:1:1:3:1:1:2:1:2:5:6:1:1:1:2:1:1:1:1:1:2:1:12:5:1:7:2:3:1:1:1:5:1:2:4:2:1:2:1:1:12:1:1:1:12:5:1:5:2:1:2:1:3:4:1:2:1:1:1:1:3:4:2:2:1:1:12:5:1:4:2:5:2:1:2:4:4:1:4:1:1:2:2:2:1:12:5:1:4:1:1:3:1:1:1:1:11:5:1:3:1:1:1:12:1:12:5:1:3:3:2:1:1:4:2:3:1:1:1:1:2:1:1:1:3:1:1:1:1:2:1:1:12:5:1:3:2:1:1:2:3:2:3:1:3:1:5:2:1:6:1:1:1:1:1:1:1:12:5:1:3:1:2:1:1:2:1:3:1:2:2:2:3:1:3:1:2:2:13:1:1:1:1:12:5:1:2:9:1:1:2:2:1:2:1:6:1:3:1:1:3:1:1:1:1:12:5:1:2:3:3:4:4:2:2:1:4:1:2:2:1:3:1:2:1:12:5:1:2:2:4:1:1:1:2:2:4:2:1:4:1:1:4:1:1:1:12:1:1:12:5:1:2:2:1:2:2:1:4:2:2:2:3:1:1:1:1:2:1:1:2:2:2:1:12:5:1:2:1:4:1:1:2:4:2:1:2:7:1:1:3:2:2:1:1:12:5:1:2:1:2:1:3:6:2:2:4:4:1:1:1:2:2:1:12:1:12:5:1:2:1:1:2:2:2:1:3:3:1:3:2:2:1:1:3:1:1:1:2:2:1:12:5:1:2:1:1:1:1:1:3:3:3:2:3:2:1:3:3:6:1:1:1:1:1:1:1:12:5:1:1:5:1:1:3:1:7:4:1:1:1:2:1:1:3:13:1:1:1:1:12:5:1:1:3:8:1:3:2:2:2:1:6:3:1:3:1:1:1:1:12:5:1:1:3:1:3:6:3:1:1:4:2:4:1:1:2:1:1:2:1:1:12:5:1:1:2:5:1:1:1:1:3:2:1:3:2:3:2:1:1:1:2:1:1:1:12:1:12:5:1:1:2:2:2:2:1:1:4:1:5:1:1:3:1:2:5:1:2:1:12:5:1:1:2:1:3:1:1:1:2:2:2:1:2:2:1:1:1:1:3:1:2:2:1:1:1:2:1:12:5:1:1:2:1:1:1:3:1:3:2:2:2:2:1:1:2:1:1:3:1:2:1:1:11:1:1:11:5:1:1:1:4:2:1:1:5:6:1:2:3:1:1:1:3:2:1:2:1:12:5:1:1:1:2:2:1:1:1:2:1:2:1:1:1:1:1:2:2:2:1:3:1:3:1:1:1:2:1:12:5:1:1:1:1:6:2:1:2:1:3:1:2:2:1:1:1:1:2:2:2:1:2:1:1:1:12:5:1:1:1:1:2:3:1:2:1:2:1:1:2:3:1:1:1:1:1:2:1:2:14:1:1:1:1:1:1:11:5:1:1:1:1:1:3:2:2:1:1:2:2:3:2:1:1:1:4:1:2:1:2:1:2:1:12:4:8:3:1:3:5:2:2:1:5:2:1:2:3:1:1:1:1:12:4:7:1:2:1:1:2:3:2:1:3:1:1:2:1:2:4:1:1:1:1:2:1:12:4:6:2:1:2:1:2:1:1:1:1:1:2:2:1:1:1:3:3:2:1:12:1:1:1:1:11:4:6:1:1:1:1:1:1:1:2:1:1:2:1:1:1:1:9:4:3:1:1:1:1:12:4:5:1:3:5:2:3:1:4:1:1:2:5:2:3:1:1:1:1:12:4:5:1:1:2:1:3:2:2:1:2:3:1:1:1:1:4:1:1:1:4:1:1:1:1:1:12:4:4:4:4:4:4:4:4:4:15:1:1:1:1:1:1:1:11:4:4:3:1:2:2:2:1:1:1:1:1:2:3:1:3:2:1:4:1:3:1:1:1:1:12:4:4:2:1:1:1:1:1:3:1:1:2:1:2:1:2:1:2:4:1:2:2:1:2:1:12:4:4:1:2:4:1:8:1:3:1:2:3:1:1:2:1:1:2:1:1:1:12:4:4:1:1:4:1:1:1:3:2:1:5:1:1:1:1:1:1:2:3:2:11:1:11:4:4:1:1:1:2:1:5:1:2:4:1:2:1:1:4:1:2:1:1:1:2:1:1:1:12:4:3:4:1:2:2:1:4:1:1:3:1:1:2:2:1:1:2:2:4:1:1:1:1:1:12:4:3:3:2:1:2:3:3:2:4:1:2:1:2:1:1:2:2:1:2:1:1:1:12:4:3:3:1:1:1:1:1:1:5:1:1:1:1:1:2:5:1:2:3:1:1:11:1:1:11:4:3:2:2:4:1:1:1:2:1:1:1:1:2:4:1:1:2:1:1:1:2:1:3:1:1:1:1:12:4:3:2:1:1:2:1:2:1:3:1:2:1:1:1:2:1:2:2:1:1:1:3:1:2:1:1:1:12:4:3:1:4:3:2:4:4:1:1:1:1:1:1:1:1:2:1:1:2:1:1:1:2:1:12:4:3:1:3:1:2:1:1:1:11:4:3:1:2:2:1:11:1:11:4:3:1:2:2:1:1:3:7:4:3:1:2:2:1:1:2:1:1:2:1:1:12:4:3:1:1:2:3:1:2:2:6:1:1:1:1:3:1:1:2:5:2:1:12:4:3:1:1:1:3:2:1:1:2:1:1:1:1:1:1:1:1:3:1:4:3:1:1:1:1:2:1:12:4:3:1:1:1:1:1:5:1:1:2:4:1:2:3:1:2:4:1:1:11:1:1:1:11:4:2:6:2:1:5:1:2:1:1:1:4:2:3:1:1:2:4:1:1:1:1:1:12:4:2:4:1:6:3:1:1:1:1:1:1:4:1:1:1:1:2:1:2:2:2:1:12:4:2:3:5:2:1:1:1:1:5:1:1:6:2:2:1:1:2:2:1:12:4:2:3:2:2:2:1:2:3:1:7:1:1:2:3:1:3:1:11:1:11:4:2:3:1:2:5:2:1:4:2:2:2:2:1:2:1:3:2:1:2:1:12:4:2:2:4:1:2:1:1:3:1:2:4:1:1:1:1:1:1:1:2:1:1:1:1:1:1:2:1:12:4:2:2:2:6:4:1:1:2:1:1:1:2:2:1:3:2:4:2:1:12:4:2:2:2:1:3:1:2:2:2:2:1:1:1:10:2:13:1:1:1:1:1:11:4:2:2:1:4:1:4:5:1:1:1:5:2:2:1:1:1:1:4:1:1:1:1:1:12:4:2:2:1:1:2:2:1:1:1:3:3:3:1:5:1:1:1:1:4:1:2:1:12:4:2:2:1:1:1:1:1:1:2:2:3:3:6:1:2:1:3:2:1:1:1:2:1:12:4:2:1:4:4:1:5:3:1:1:1:2:2:8:2:11:1:11:4:2:1:3:3:4:1:2:2:2:5:1:1:1:1:1:4:5:1:1:1:1:1:1:12:4:2:1:2:3:2:3:4:2:4:1:3:1:1:2:2:2:1:2:1:1:1:12:4:2:1:2:2:1:1:2:3:1:1:2:5:1:1:2:3:6:2:1:1:12:4:2:1:2:1:1:7:1:2:4:3:2:5:2:3:11:1:1:11:4:2:1:1:6:1:3:1:3:1:1:2:4:2:1:3:1:1:1:1:1:1:1:2:1:12:4:2:1:1:2:2:3:2:6:1:1:1:9:2:1:2:2:1:1:12:4:2:1:1:2:1:1:1:4:2:5:1:3:4:1:2:1:1:1:1:1:2:1:1:1:12:4:2:1:1:1:3:1:1:1:2:1:3:3:1:1:3:2:1:1:1:2:1:3:11:1:1:11:4:2:1:1:1:1:4:3:1:1:1:1:1:4:1:1:3:2:1:7:1:2:1:12:4:1:7:4:1:7:4:1:7:3:1:1:2:1:12:4:1:6:1:2:1:1:1:1:2:3:1:4:1:1:1:1:4:4:2:1:1:12:4:1:5:2:1:1:1:4:2:1:1:1:1:2:3:9:2:11:1:11:4:1:4:5:3:3:2:1:2:2:2:2:1:3:1:1:5:2:1:12:4:1:4:1:2:4:1:5:4:2:4:1:2:1:1:2:2:2:1:12:4:1:4:1:1:1:1:8:2:2:1:1:1:1:3:1:2:1:3:1:2:1:1:12:4:1:3:4:1:1:1:2:3:1:1:5:1:2:2:2:5:2:11:1:11:4:1:3:2:4:2:3:1:1:8:4:1:3:2:1:2:2:1:12:4:1:3:1:3:3:2:4:1:1:1:1:4:3:3:1:1:3:1:2:1:12:4:1:3:1:2:1:1:3:2:3:2:1:3:1:1:1:1:2:1:2:1:1:1:1:1:1:2:1:12:4:1:3:1:1:1:9:4:1:3:1:1:1:8:2:11:1:11:4:1:2:7:1:2:1:1:4:2:2:2:2:1:5:3:1:1:2:1:12:4:1:2:3:2:2:3:2:3:3:1:1:7:1:1:6:1:1:1:1:1:1:1:12:4:1:2:3:1:1:1:2:1:1:3:2:1:5:4:1:3:1:2:1:1:1:2:1:12:4:1:2:2:2:4:2:3:3:8:3:1:1:1:1:1:12:1:1:1:1:11:4:1:2:2:1:3:3:1:3:1:4:2:1:2:1:1:2:2:1:1:1:2:1:2:1:12:4:1:2:1:4:2:1:1:1:1:4:1:3:1:1:1:1:1:1:3:2:1:1:2:2:1:12:4:1:2:1:3:1:1:1:1:2:1:1:2:1:2:1:3:1:2:1:5:1:4:1:1:1:1:1:12:4:1:2:1:2:1:3:1:5:1:1:1:1:1:4:1:3:1:2:1:1:3:11:1:11:4:1:2:1:1:4:1:2:2:2:1:1:4:2:1:3:3:2:2:1:2:2:1:12:4:1:2:1:1:1:2:1:2:1:1:1:1:1:2:1:2:3:3:1:3:1:3:1:1:2:1:12:4:1:1:9:1:2:2:2:7:3:1:1:1:1:1:1:1:1:2:1:1:1:12:4:1:1:5:2:2:2:2:1:1:1:4:1:2:1:2:2:2:1:14:1:1:1:1:1:1:11:4:1:1:4:2:3:4:2:1:1:1:1:1:6:1:1:3:2:1:1:2:1:1:1:12:4:1:1:3:2:5:3:1:2:1:1:2:2:6:2:2:2:1:2:1:12:4:1:1:3:1:3:4:1:3:2:1:1:3:3:1:1:4:1:4:1:1:1:1:1:12:4:1:1:3:1:1:1:6:1:2:2:2:1:2:1:2:4:3:1:2:11:1:11:4:1:1:2:4:1:7:2:2:3:2:4:1:3:2:1:3:1:1:1:1:12:4:1:1:2:2:1:3:1:1:1:4:2:2:1:1:1:5:5:1:1:1:1:2:1:12:4:1:1:2:1:4:1:1:1:2:2:2:1:3:1:1:3:2:1:1:3:1:1:2:1:1:1:12:4:1:1:2:1:2:1:2:1:1:3:2:1:2:5:1:1:1:1:1:4:2:11:1:1:11:4:1:1:2:1:1:2:1:1:1:1:5:1:1:2:1:3:2:1:1:1:3:1:4:2:1:12:4:1:1:1:4:2:1:1:2:1:1:1:3:1:1:2:1:3:1:1:1:1:1:1:1:1:3:1:1:1:1:12:4:1:1:1:3:2:2:1:2:1:1:1:2:1:2:1:1:2:3:2:1:1:1:1:2:2:1:1:12:4:1:1:1:3:1:1:1:1:1:1:6:2:2:2:5:1:2:1:1:3:11:1:11:4:1:1:1:2:2:3:1:1:4:1:1:1:2:1:1:2:1:1:2:1:3:2:4:1:1:1:1:1:12:4:1:1:1:2:1:1:1:2:1:1:2:1:1:3:1:1:1:1:5:3:2:2:3:1:1:1:12:4:1:1:1:1:4:1:1:1:1:1:2:2:1:1:1:3:1:1:1:1:1:4:2:1:1:1:2:1:1:12:4:1:1:1:1:2:4:1:1:1:1:1:1:2:2:1:1:1:3:1:3:1:1:1:4:11:1:11:4:1:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:3:1:3:1:1:1:1:12:4:1:1:1:1:1:1:6:5:1:2:1:1:4:3:1:1:1:1:1:1:3:1:1:1:1:12:4:1:1:1:1:1:1:1:2:3:1:7:1:1:6:1:1:2:1:1:2:2:1:1:12:3:9:3:9:3:9:3:1:11:1:1:11:3:8:1:1:2:2:4:2:1:1:1:1:3:4:1:2:2:1:2:2:1:12:3:6:4:2:1:1:1:5:1:1:2:1:3:2:1:3:1:2:2:1:1:12:3:6:2:1:2:4:1:1:5:1:4:1:3:1:1:1:1:2:2:1:1:12:3:6:1:2:1:1:2:3:2:3:2:2:3:1:1:2:3:1:2:11:1:11:3:5:6:2:1:1:1:2:1:2:2:2:1:1:1:1:4:1:1:1:2:1:2:1:12:3:5:2:2:2:1:1:1:3:1:9:3:4:5:1:1:1:1:1:1:12:3:5:1:9:1:1:1:1:1:2:2:2:6:1:4:1:2:1:12:3:5:1:2:2:3:2:1:1:5:1:2:1:1:2:1:1:3:4:11:1:11:3:5:1:1:2:3:3:1:2:1:1:2:2:2:2:5:1:2:2:1:2:1:12:3:4:4:3:3:4:1:2:4:1:2:2:1:3:3:2:1:1:12:3:4:3:3:1:2:1:2:2:1:2:1:1:3:3:7:2:2:1:12:3:4:3:1:1:2:1:2:1:2:1:2:4:5:1:1:5:1:1:11:1:11:3:4:2:2:9:3:4:2:2:8:4:1:1:1:1:12:3:4:2:1:1:1:3:2:2:3:2:2:2:1:1:3:1:1:2:4:2:1:12:3:4:1:4:1:1:11:3:4:1:4:1:1:1:1:1:2:1:12:3:4:1:2:4:2:2:1:3:1:1:1:4:3:1:2:2:1:1:12:1:1:1:1:11:3:4:1:2:1:2:1:1:2:1:2:1:2:3:3:2:3:3:4:2:1:1:12:3:4:1:1:1:6:1:1:1:4:1:3:1:1:3:3:5:2:1:1:12:3:4:1:1:1:1:2:2:1:2:1:1:1:3:2:1:1:4:2:1:1:2:1:1:1:1:2:1:12:3:3:6:2:2:4:1:2:1:1:5:1:3:1:1:1:1:1:1:1:11:1:1:11:3:3:5:1:1:1:1:2:1:3:3:2:1:1:1:1:2:2:3:2:2:1:2:1:12:3:3:3:3:3:3:3:3:3:3:3:3:2:2:2:1:1:12:3:3:3:2:1:1:1:6:1:2:2:1:2:1:1:1:1:2:4:2:2:1:1:12:3:3:3:1:1:3:2:1:1:1:1:1:4:3:7:2:2:12:1:1:1:11:3:3:2:5:1:1:1:1:1:1:4:4:1:1:1:4:1:4:1:1:2:1:1:12:3:3:2:2:1:4:1:1:1:2:1:3:1:3:1:2:1:2:1:1:5:2:1:12:3:3:2:1:4:1:2:1:1:1:3:1:1:2:2:1:4:3:2:1:1:2:1:1:12:3:3:2:1:2:1:1:11:3:3:2:1:2:1:1:3:11:1:1:11:3:3:2:1:1:1:4:4:3:1:1:5:8:2:3:2:1:12:3:3:1:4:1:2:1:2:3:2:1:1:3:5:5:1:2:2:1:1:12:3:3:1:3:1:6:1:1:5:2:1:4:4:2:1:1:2:2:1:12:3:3:1:2:4:2:1:3:1:2:1:1:1:1:3:1:2:2:1:4:1:11:1:1:11:3:3:1:2:2:1:2:2:1:3:1:2:3:1:3:1:1:1:1:1:3:1:2:2:1:1:12:3:3:1:1:5:2:2:5:5:1:1:1:1:1:1:1:1:1:1:5:1:1:1:1:1:1:12:3:3:1:1:3:1:2:2:5:2:1:1:4:8:1:2:1:2:1:12:3:3:1:1:2:2:1:1:1:1:1:3:1:1:1:1:4:2:3:2:5:1:11:1:11:3:3:1:1:1:4:2:4:1:1:1:1:2:1:1:2:2:2:1:1:2:1:1:1:1:1:2:1:1:12:3:3:1:1:1:1:2:1:1:1:1:1:2:4:1:1:4:2:5:1:1:1:2:2:1:12:3:2:8:2:3:1:2:5:2:1:1:2:1:2:2:1:2:1:2:1:12:3:2:6:1:2:2:3:1:1:3:1:3:6:2:1:2:1:11:1:1:11:3:2:5:1:9:2:1:1:3:3:1:1:1:6:4:1:1:1:1:12:3:2:4:2:1:2:2:1:1:1:1:2:1:1:1:3:1:4:1:2:2:1:1:1:2:1:12:3:2:4:1:1:6:1:1:1:1:2:3:1:1:1:1:2:1:1:2:2:3:2:1:12:3:2:3:5:1:7:3:1:1:2:1:1:3:1:1:2:14:1:1:1:1:1:1:11:3:2:3:3:1:2:1:1:3:3:2:1:2:1:1:2:1:2:1:1:1:1:2:1:1:2:1:12:3:2:3:1:4:1:2:1:6:1:5:1:2:4:2:1:2:2:1:12:3:2:3:1:2:1:1:4:1:4:3:4:2:2:1:1:1:1:1:3:2:1:12:3:2:3:1:1:1:3:6:3:2:3:1:1:1:3:5:1:1:11:1:11:3:2:2:6:1:1:1:2:1:2:1:2:2:3:1:4:1:2:3:3:1:1:1:12:3:2:2:3:1:3:1:3:3:4:4:1:1:1:2:2:1:1:1:1:1:1:2:1:12:3:2:2:2:3:4:1:1:3:3:3:2:1:1:1:3:1:1:1:1:1:2:1:1:1:12:3:2:2:2:2:1:1:1:2:2:2:2:1:2:11:3:1:1:11:1:11:3:2:2:2:1:1:2:4:1:1:1:5:5:1:1:2:1:1:3:1:3:1:1:1:12:3:2:2:1:2:8:3:2:2:1:2:8:3:1:1:2:1:12:3:2:2:1:2:1:2:1:7:2:1:4:1:1:3:5:4:1:1:1:1:12:3:2:2:1:1:3:2:1:1:1:1:2:1:6:1:2:2:1:1:2:2:13:1:1:1:1:11:3:2:2:1:1:1:5:1:1:2:1:3:4:5:1:2:2:2:4:1:1:1:1:12:3:2:1:6:1:2:1:1:4:1:3:5:1:1:2:1:3:1:3:1:1:1:12:3:2:1:4:4:1:1:1:4:1:2:5:1:3:3:1:1:2:2:1:1:12:3:2:1:4:1:1:3:1:1:3:1:2:3:1:1:1:2:2:1:1:1:4:1:11:1:11:3:2:1:3:3:1:1:1:1:1:2:1:2:3:1:2:1:7:2:5:1:1:1:1:1:12:3:2:1:3:1:1:1:1:7:6:1:1:2:2:1:4:2:1:2:1:1:12:3:2:1:2:3:3:4:2:4:1:1:6:2:2:1:1:1:2:2:1:12:3:2:1:2:2:2:5:2:9:3:2:1:2:2:1:1:11:1:1:11:3:2:1:2:2:1:1:1:1:3:1:1:1:1:2:1:2:1:2:1:1:1:1:2:1:1:1:5:1:1:1:1:1:1:12:3:2:1:2:1:1:2:2:2:1:1:2:1:3:2:1:4:2:1:2:1:1:1:1:1:1:2:1:12:3:2:1:1:8:3:2:1:1:8:3:2:1:1:2:3:1:1:1:12:3:2:1:1:4:2:1:1:2:1:1:2:2:1:1:1:3:3:4:1:1:3:11:1:11:3:2:1:1:3:2:1:2:1:1:4:4:2:1:2:2:1:1:1:3:3:2:1:1:12:3:2:1:1:2:2:1:2:3:1:1:2:2:5:1:1:1:1:1:1:1:2:1:1:1:2:1:1:12:3:2:1:1:2:1:1:5:1:2:2:1:1:2:2:2:4:1:1:3:1:1:2:1:1:12:3:2:1:1:1:5:1:1:2:9:1:1:4:1:2:3:1:1:1:11:1:11:3:2:1:1:1:3:1:1:2:1:2:2:1:1:1:1:1:11:2:5:1:1:1:1:1:12:3:2:1:1:1:1:3:2:2:2:4:2:1:5:2:1:4:3:1:2:1:12:3:2:1:1:1:1:1:5:3:3:1:3:4:5:1:1:2:1:2:2:1:12:3:2:1:1:1:1:1:1:2:1:1:1:2:1:6:1:1:2:4:2:1:2:2:11:1:1:11:3:1:8:1:1:3:6:2:2:2:1:4:6:3:1:1:1:12:3:1:6:1:1:2:1:2:1:1:2:1:1:3:1:1:2:2:3:3:1:2:2:1:12:3:1:5:2:3:5:1:1:4:5:1:3:1:1:1:2:3:1:1:1:12:3:1:5:1:2:4:1:3:8:3:1:5:1:2:11:1:1:10:3:1:5:1:1:1:1:1:1:2:1:1:1:2:2:3:6:1:1:2:3:2:2:1:1:11:3:1:4:1:2:1:2:2:4:5:1:2:1:1:1:5:1:1:1:2:1:2:1:11:3:1:3:5:6:1:1:6:1:4:6:2:4:1:1:1:1:11:3:1:3:3:1:2:1:8:1:2:3:2:2:2:2:4:2:12:1:11:3:1:3:2:1:4:1:1:1:1:2:3:1:1:2:2:3:4:1:1:1:1:1:1:2:1:11:3:1:3:1:3:4:1:1:1:2:2:1:1:1:1:3:1:1:2:1:2:5:1:12:1:11:3:1:3:1:1:11:3:1:3:1:1:10:4:1:1:1:1:11:3:1:3:1:1:1:2:1:1:3:5:1:5:2:1:2:4:2:1:2:12:1:11:3:1:2:5:1:1:3:3:1:5:3:1:1:3:5:1:4:1:1:1:1:11:3:1:2:3:3:2:1:5:3:2:1:1:1:1:1:5:1:1:2:1:2:12:1:11:3:1:2:3:1:1:1:3:1:3:2:1:2:2:1:2:6:1:1:1:1:2:1:2:1:11:3:1:2:2:2:2:5:11:3:1:2:2:2:1:3:12:1:11:3:1:2:2:1:2:2:1:1:6:2:1:3:1:5:4:2:2:1:2:1:11:3:1:2:1:6:2:1:1:1:3:1:1:2:1:1:1:3:1:3:4:1:2:12:1:11:3:1:2:1:3:1:2:3:1:1:3:2:1:1:4:1:1:2:2:5:1:2:1:1:11:3:1:2:1:2:1:3:11:3:1:2:1:2:1:3:3:13:1:1:1:11:3:1:2:1:1:3:3:1:3:1:1:8:1:1:1:1:1:3:1:2:4:1:1:1:1:11:3:1:2:1:1:1:5:2:4:1:5:1:1:1:1:2:2:2:2:1:14:1:1:1:1:11:3:1:2:1:1:1:1:1:4:1:1:1:1:1:1:2:2:5:4:1:1:1:1:2:2:2:1:11:3:1:1:5:5:1:4:2:1:3:1:2:3:1:1:2:3:3:12:1:11:3:1:1:4:2:4:3:1:2:1:4:1:4:3:2:6:1:1:1:1:1:1:1:11:3:1:1:3:8:1:1:1:1:1:1:3:2:4:1:3:2:1:4:12:1:11:3:1:1:3:2:1:5:1:1:4:1:1:2:1:4:5:2:1:3:2:1:11:3:1:1:3:1:1:6:1:1:3:1:3:1:2:2:3:2:3:1:3:12:1:11:3:1:1:2:4:4:6:1:2:3:1:5:1:1:2:1:1:1:2:2:1:11:3:1:1:2:2:6:2:4:4:2:2:1:2:3:1:1:2:1:2:12:1:11:3:1:1:2:2:1:1:4:1:3:1:1:1:1:1:1:11:2:3:1:2:1:11:3:1:1:2:1:2:4:5:1:4:1:1:1:7:3:2:1:13:1:1:1:11:3:1:1:2:1:1:2:3:2:2:1:11:3:1:1:2:1:1:2:2:1:1:11:3:1:1:1:8:1:2:1:3:2:4:3:2:2:2:1:3:13:1:1:1:11:3:1:1:1:4:1:2:5:1:1:6:1:1:1:3:4:1:1:1:1:1:2:1:1:11:3:1:1:1:3:1:3:2:3:2:3:1:3:1:1:6:3:1:1:1:12:1:1:11:3:1:1:1:2:3:2:1:4:1:1:3:2:3:1:2:2:3:4:3:1:1:1:11:3:1:1:1:2:1:4:1:1:2:1:3:1:1:2:1:2:1:4:1:4:2:1:1:12:1:11:3:1:1:1:2:1:1:1:1:4:2:1:2:6:2:2:1:2:1:1:1:4:1:2:1:11:3:1:1:1:1:3:2:2:5:1:1:1:1:2:1:3:2:1:1:2:6:12:1:1:11:3:1:1:1:1:2:2:1:2:1:1:2:2:3:2:1:1:1:3:3:1:1:2:1:2:1:2:1:11:3:1:1:1:1:1:4:1:2:2:2:3:3:1:2:1:3:1:5:2:2:12:1:11:3:1:1:1:1:1:2:1:1:1:1:1:4:1:2:3:2:1:1:2:2:3:2:2:1:2:1:1:11:3:1:1:1:1:1:1:1:1:4:8:2:1:3:1:1:1:1:2:1:1:1:2:2:12:1:11:2:8:4:1:6:1:1:1:2:2:3:1:2:2:1:2:1:2:2:1:11:2:7:2:2:2:3:2:1:2:2:11:2:2:1:1:12:1:11:2:6:5:1:1:2:1:1:3:3:3:5:1:2:1:2:1:2:2:1:11:2:6:2:1:1:3:1:1:2:2:1:2:1:1:7:1:1:2:15:1:1:1:1:1:1:11:2:6:1:1:2:1:4:2:1:2:1:3:3:2:1:1:2:1:1:3:2:2:1:11:2:5:4:1:1:1:2:1:1:1:1:1:1:1:3:1:2:1:2:3:1:3:1:1:12:1:1:11:2:5:2:2:4:1:1:5:1:3:1:5:2:2:1:1:3:2:1:1:11:2:5:1:5:3:2:3:1:1:3:1:3:3:2:4:2:1:12:1:11:2:5:1:2:1:2:1:1:1:2:2:2:1:2:2:1:3:1:1:2:1:1:2:3:2:1:11:2:5:1:1:1:2:2:2:1:3:1:1:1:1:1:4:2:1:1:2:5:13:1:1:1:11:2:4:5:1:1:1:5:2:3:3:2:3:1:2:4:1:2:2:1:11:2:4:3:2:4:2:1:1:1:3:1:1:1:3:2:1:2:1:5:2:12:1:11:2:4:2:5:3:1:1:1:1:1:1:1:1:1:2:2:1:1:2:2:1:1:1:1:3:2:1:11:2:4:2:2:1:1:4:2:3:1:2:4:4:1:1:1:1:2:1:14:1:1:1:1:11:2:4:2:1:1:2:1:4:1:2:1:1:5:1:3:1:1:4:2:1:1:1:2:1:11:2:4:1:3:7:2:2:1:2:2:2:3:3:2:1:1:1:14:1:1:1:1:11:2:4:1:2:2:2:4:1:7:2:2:1:2:1:4:3:3:1:1:1:11:2:4:1:1:4:4:4:2:1:2:5:3:4:1:2:1:1:12:1:11:2:4:1:1:2:1:1:1:3:1:2:2:1:1:1:4:2:1:1:3:1:4:3:1:1:1:11:2:4:1:1:1:1:1:4:2:4:2:2:2:3:2:1:4:2:1:1:1:12:1:11:2:3:5:3:2:1:1:2:2:3:2:1:1:9:1:1:4:1:1:1:1:11:2:3:4:2:1:2:2:5:2:1:1:2:1:2:1:1:2:1:1:3:1:1:12:1:1:11:2:3:3:4:1:1:1:2:2:1:4:1:2:1:1:1:3:1:2:2:3:2:1:1:11:2:3:3:1:4:4:3:1:2:1:1:1:6:1:3:3:1:2:12:1:11:2:3:3:1:1:1:1:3:2:1:3:2:7:1:1:2:1:2:2:4:1:1:1:1:11:2:3:2:3:2:1:1:2:3:1:1:1:3:1:2:6:1:2:3:1:12:1:1:11:2:3:2:2:1:4:1:1:1:1:1:1:2:4:2:1:1:1:2:1:1:1:1:3:1:1:2:1:11:2:3:2:1:3:1:3:2:1:1:1:5:1:1:1:2:2:1:1:2:4:13:1:1:1:11:2:3:2:1:1:3:1:1:1:4:1:3:2:1:2:1:11:2:2:1:11:2:3:1:7:3:1:2:1:1:2:3:3:4:1:1:1:2:1:1:13:1:1:1:11:2:3:1:4:1:1:2:2:1:5:1:1:1:4:1:4:1:2:2:1:2:2:1:11:2:3:1:3:1:1:5:1:3:1:2:2:1:1:3:1:3:1:1:1:1:3:1:12:1:11:2:3:1:2:2:3:1:2:1:1:1:1:2:3:3:1:2:1:1:3:2:1:1:1:1:2:1:11:2:3:1:2:1:2:1:1:2:2:3:2:3:1:1:1:5:5:1:1:13:1:1:1:11:2:3:1:1:4:3:1:1:2:1:1:1:1:5:2:2:3:1:2:1:3:2:1:1:11:2:3:1:1:2:3:2:3:2:2:2:2:4:1:1:2:1:3:2:1:13:1:1:1:11:2:3:1:1:1:7:4:7:1:4:1:2:1:1:2:2:2:2:1:11:2:3:1:1:1:2:1:2:1:1:1:1:1:2:1:1:2:1:1:1:2:1:1:2:2:3:2:13:1:1:1:11:2:3:1:1:1:1:1:2:1:1:2:1:1:1:4:1:2:1:1:2:1:2:1:1:3:1:2:2:1:1:11:2:2:6:3:1:5:1:4:2:5:1:3:2:2:2:12:1:1:11:2:2:5:2:1:1:2:2:1:4:1:1:1:3:1:2:2:2:1:1:4:2:1:1:11:2:2:4:3:2:3:1:2:3:1:2:1:4:2:3:1:1:2:1:1:12:1:1:11:2:2:4:1:3:1:4:1:3:1:1:1:5:2:2:2:1:2:1:1:1:1:2:1:11:2:2:3:6:5:7:2:1:3:1:1:3:2:1:3:12:1:11:2:2:3:3:1:1:2:1:1:1:1:1:1:1:1:7:4:3:1:4:2:1:1:11:2:2:3:2:1:1:2:11:2:2:3:2:1:1:2:3:2:12:1:11:2:2:3:1:2:2:1:5:2:5:1:2:1:1:7:1:2:3:1:1:1:11:2:2:3:1:1:1:3:4:1:1:2:2:2:1:1:2:1:2:1:1:1:2:2:1:2:12:1:11:2:2:2:5:1:1:3:1:1:3:1:1:1:1:1:2:3:2:1:2:4:1:2:1:1:11:2:2:2:3:2:2:2:2:1:1:1:1:1:1:1:1:1:2:2:2:2:1:2:1:3:1:12:1:11:2:2:2:2:4:1:1:4:1:1:1:2:1:2:7:1:3:1:4:1:1:1:1:1:11:2:2:2:2:1:4:1:1:3:5:3:1:7:1:4:2:1:12:1:11:2:2:2:1:11:2:2:2:1:11:2:1:3:1:1:1:1:11:2:2:2:1:3:1:1:3:2:2:1:2:3:1:1:2:1:6:1:3:1:12:1:1:11:2:2:2:1:2:1:1:3:2:2:1:6:1:1:1:4:4:1:2:2:1:2:1:11:2:2:2:1:1:2:2:1:5:2:1:3:1:1:2:1:1:5:6:1:12:1:11:2:2:2:1:1:1:1:3:2:1:1:3:1:1:1:1:1:1:1:1:1:2:2:2:4:3:1:1:1:1:11:2:2:1:6:1:1:1:1:1:3:1:3:1:1:3:1:2:3:1:1:1:3:1:12:1:1:11:2:2:1:4:2:1:3:1:4:2:2:5:3:1:1:1:2:1:1:1:2:2:1:11:2:2:1:3:3:3:1:1:2:4:3:3:2:1:1:1:2:2:1:1:1:2:12:1:11:2:2:1:3:1:3:1:2:1:1:1:5:2:3:3:2:1:2:3:1:3:1:1:1:11:2:2:1:2:5:5:4:1:1:1:2:3:2:2:1:2:1:2:1:1:1:12:1:11:2:2:1:2:2:5:2:1:2:2:2:1:5:1:1:1:1:2:1:1:1:2:1:1:2:1:11:2:2:1:2:2:1:1:1:1:2:1:4:2:1:4:1:4:1:1:2:5:1:12:1:11:2:2:1:2:1:2:1:1:5:1:2:4:8:1:1:2:1:1:1:2:1:2:1:11:2:2:1:2:1:1:1:1:3:1:2:1:2:1:2:1:1:1:2:4:4:1:1:1:2:1:12:1:11:2:2:1:1:4:2:1:1:2:3:1:2:1:2:1:1:1:4:3:1:2:2:2:2:1:11:2:2:1:1:3:1:2:3:1:1:1:1:4:2:2:1:2:5:1:2:1:2:12:1:1:11:2:2:1:1:2:2:3:2:2:1:2:1:1:1:1:2:1:2:1:3:2:3:2:2:2:1:11:2:2:1:1:2:1:1:5:4:2:1:2:2:1:2:1:2:2:1:1:1:3:1:12:1:1:11:2:2:1:1:1:4:1:1:3:1:1:3:1:2:5:2:5:1:4:2:1:1:11:2:2:1:1:1:2:2:1:4:1:2:1:1:1:1:2:3:2:1:3:2:1:3:1:12:1:1:11:2:2:1:1:1:1:3:2:2:8:3:1:3:1:2:4:2:1:2:2:1:11:2:2:1:1:1:1:1:2:4:2:3:1:4:1:3:1:1:1:2:1:2:3:1:1:12:1:11:2:1:9:1:2:1:1:6:4:3:1:4:2:2:1:1:1:2:1:11:2:1:6:3:2:3:2:1:1:1:1:2:1:8:1:1:1:2:2:12:1:1:11:2:1:5:5:1:1:3:3:1:1:2:1:1:1:2:4:1:1:2:3:1:2:1:11:2:1:5:1:4:6:1:3:1:4:1:4:2:1:4:13:1:1:1:11:2:1:4:8:3:1:2:2:1:1:5:5:1:1:1:3:2:1:1:11:2:1:4:3:1:1:1:1:3:1:1:1:1:4:4:1:1:2:2:5:12:1:1:1:11:2:1:4:2:1:1:1:2:1:1:1:1:2:1:2:2:4:1:1:1:1:1:2:1:1:2:3:1:1:1:11:2:1:4:1:2:1:1:3:1:4:4:1:1:1:7:1:2:3:2:12:1:11:2:1:4:1:1:1:1:2:1:1:2:3:1:1:1:2:1:2:1:3:3:1:1:1:1:1:1:1:2:1:11:2:1:3:4:1:2:2:2:7:1:1:2:3:1:1:1:3:1:2:2:12:1:11:2:1:3:3:1:1:3:1:2:2:1:1:2:1:3:2:2:9:2:1:1:1:11:2:1:3:2:2:1:4:1:2:3:2:1:3:1:2:1:3:4:1:2:12:1:1:11:2:1:3:2:1:1:1:8:1:3:3:2:3:1:1:4:4:2:1:1:11:2:1:3:1:3:2:1:1:2:2:1:1:5:1:2:1:1:2:3:2:1:1:2:1:12:1:11:2:1:3:1:2:1:2:1:2:1:6:1:1:3:2:1:1:1:3:4:1:1:1:2:1:11:2:1:3:1:1:2:2:1:4:1:1:1:2:2:1:2:3:2:1:1:2:4:1:1:12:1:11:2:1:3:1:1:1:1:2:2:1:1:3:1:1:1:3:3:1:1:1:4:3:4:1:1:1:1:1:11:2:1:2:5:2:2:2:1:1:2:1:1:4:1:1:2:2:1:5:2:1:1:12:1:11:2:1:2:4:1:2:2:3:1:2:2:2:1:4:3:1:1:1:1:2:1:2:2:1:1:11:2:1:2:3:2:2:2:5:2:6:1:1:3:1:1:2:1:1:1:1:1:12:1:1:11:2:1:2:3:1:1:2:3:1:1:1:2:2:3:1:1:3:4:4:2:3:1:1:1:11:2:1:2:2:3:2:3:1:1:1:2:1:2:1:1:1:1:2:4:2:3:2:1:12:1:1:11:2:1:2:2:2:1:2:2:5:1:1:1:2:1:2:3:1:1:4:3:2:2:1:1:11:2:1:2:2:1:2:2:2:3:1:1:1:1:2:2:11:2:1:1:1:12:1:1:11:2:1:2:2:1:1:1:2:2:1:5:3:2:1:2:2:3:1:2:2:2:1:1:2:1:11:2:1:2:1:4:2:2:1:1:1:5:1:1:6:1:2:1:1:1:4:1:12:1:1:11:2:1:2:1:3:1:2:1:3:2:2:2:1:1:2:1:1:1:4:3:1:1:3:1:2:1:11:2:1:2:1:2:2:2:1:2:1:1:5:3:4:1:3:3:2:2:13:1:1:1:11:2:1:2:1:2:1:1:2:1:1:1:6:1:1:2:2:3:1:4:3:3:2:1:1:11:2:1:2:1:1:3:1:6:1:1:1:2:1:4:2:3:3:3:1:2:1:12:1:11:2:1:2:1:1:2:1:1:3:5:1:1:2:1:2:1:1:1:2:9:2:1:1:1:11:2:1:2:1:1:1:2:1:3:1:1:1:1:2:1:1:1:2:1:1:1:1:1:2:1:2:2:2:1:1:1:12:1:11:2:1:2:1:1:1:1:1:1:2:2:1:1:1:1:5:4:1:3:1:2:1:3:2:2:1:1:11:2:1:1:6:1:1:6:5:4:1:2:5:2:1:2:2:12:1:11:2:1:1:5:1:1:1:2:2:7:2:2:1:3:1:1:1:3:1:3:2:1:1:11:2:1:1:4:2:1:1:1:2:4:4:2:1:1:1:1:1:1:1:1:1:2:3:1:13:1:1:1:11:2:1:1:4:1:1:1:1:1:1:1:1:1:2:4:1:1:11:2:1:1:1:2:1:1:11:2:1:1:3:2:5:2:1:5:3:3:1:1:2:5:2:2:1:12:1:11:2:1:1:3:1:5:2:1:1:1:4:1:2:2:1:1:1:1:1:1:2:2:1:1:1:1:1:2:1:11:2:1:1:3:1:1:3:3:3:1:2:2:3:2:4:1:2:3:1:2:12:1:1:11:2:1:1:2:6:1:3:2:1:2:1:1:1:1:2:1:1:1:2:3:3:2:2:1:1:1:11:2:1:1:2:3:2:6:1:2:2:1:1:2:1:2:1:3:1:1:1:1:1:3:1:12:1:11:2:1:1:2:2:3:2:4:2:5:1:1:3:1:3:2:7:1:1:1:1:1:1:1:1:11:2:1:1:2:2:1:2:1:5:1:3:1:1:1:2:4:1:1:1:1:1:1:2:3:1:12:1:11:2:1:1:2:1:4:1:1:3:2:1:7:3:1:8:3:1:2:1:11:2:1:1:2:1:2:1:5:3:2:2:2:2:4:2:1:2:1:1:3:1:12:1:1:11:2:1:1:2:1:1:2:3:2:1:1:2:2:1:1:2:2:4:1:4:1:3:2:1:1:1:11:2:1:1:2:1:1:1:1:2:2:2:2:1:2:1:3:1:2:3:2:2:2:1:3:1:12:1:11:2:1:1:1:5:2:1:1:1:1:1:3:6:5:1:1:2:4:3:1:1:1:1:11:2:1:1:1:4:1:1:2:2:5:1:3:2:1:3:2:1:1:4:1:13:1:1:1:1:11:2:1:1:1:3:2:1:1:2:2:5:4:2:2:3:1:4:1:1:1:1:1:2:1:11:2:1:1:1:2:11:2:1:1:1:2:11:1:4:12:1:1:11:2:1:1:1:2:2:4:1:6:1:2:1:2:2:1:2:1:3:5:1:1:2:1:11:2:1:1:1:2:1:4:1:6:1:1:1:1:1:1:2:2:1:2:4:2:14:1:1:1:1:1:11:2:1:1:1:2:1:1:2:3:3:1:3:2:1:1:1:1:2:3:9:1:2:1:11:2:1:1:1:1:5:1:4:1:1:7:6:2:2:1:3:1:13:1:1:1:11:2:1:1:1:1:3:2:1:1:1:1:1:4:1:1:2:1:1:1:2:1:1:2:5:1:1:3:1:1:1:11:2:1:1:1:1:2:2:3:1:3:1:1:3:3:2:5:1:3:1:2:1:1:1:12:1:11:2:1:1:1:1:2:1:1:2:1:2:1:4:2:2:4:4:1:2:1:1:4:2:1:1:11:2:1:1:1:1:1:3:1:2:3:1:2:2:1:1:1:1:2:1:2:2:2:2:5:1:12:1:11:2:1:1:1:1:1:2:1:1:1:1:4:2:1:2:2:7:1:1:4:1:1:1:3:1:1:1:11:2:1:1:1:1:1:1:1:4:6:3:2:2:1:1:2:4:3:2:1:1:1:12:1:11:1:11:1:11:1:11:1:3:2:1:1:1:11:1:8:2:2:5:3:3:1:2:8:1:2:1:1:13:1:1:1:11:1:7:3:1:1:4:1:1:1:1:1:1:3:3:1:1:2:2:6:2:1:1:11:1:7:1:1:3:2:4:11:1:7:1:1:1:1:12:1:11:1:6:3:2:1:2:1:1:2:2:1:1:1:2:6:1:1:2:1:4:1:2:1:11:1:6:2:1:1:2:1:2:1:2:2:1:4:1:1:1:1:1:4:1:2:1:2:12:1:1:11:1:6:1:1:11:1:6:1:1:11:1:1:2:1:11:1:5:6:3:2:1:1:1:3:2:1:1:2:1:3:1:2:1:1:2:1:12:1:1:11:1:5:3:2:3:1:4:1:1:1:2:4:1:2:1:3:3:3:1:2:1:11:1:5:2:3:1:1:3:1:1:1:1:2:1:1:2:6:1:1:1:1:2:3:12:1:1:11:1:5:2:1:1:3:1:1:4:5:1:1:1:5:3:2:2:1:1:2:1:1:11:1:5:1:3:1:4:2:2:1:1:1:2:2:1:1:1:3:2:1:2:1:2:1:12:1:1:11:1:5:1:2:1:1:1:2:1:1:1:1:3:2:2:4:1:5:6:1:2:1:11:1:5:1:1:1:4:1:2:2:3:3:1:3:1:2:1:2:1:1:5:12:1:1:11:1:4:6:4:1:3:1:1:2:1:3:2:2:3:2:3:2:2:1:1:11:1:4:4:2:1:1:3:1:3:1:1:1:1:1:2:1:1:1:3:7:12:1:1:1:11:1:4:3:2:2:6:1:1:2:1:1:1:1:2:2:1:2:5:2:2:1:1:1:11:1:4:3:1:1:1:3:1:1:1:1:5:1:1:1:1:1:1:1:2:1:2:1:4:1:12:1:1:11:1:4:2:3:1:1:1:1:1:1:1:5:3:2:1:1:6:2:5:1:1:1:1:1:1:11:1:4:2:1:4:2:1:1:1:2:1:1:2:2:4:1:3:1:3:1:1:2:1:12:1:11:1:4:2:1:1:2:1:6:1:3:1:8:1:2:1:1:1:2:1:1:2:1:1:11:1:4:1:4:3:3:1:5:1:1:1:2:3:3:2:2:1:3:12:1:1:11:1:4:1:3:1:1:4:1:4:1:1:1:1:1:1:1:1:1:3:1:1:3:1:1:3:1:1:1:1:11:1:4:1:2:2:1:1:1:1:2:3:2:1:1:1:1:4:1:2:4:1:4:1:12:1:11:1:4:1:1:5:2:1:1:1:2:1:2:1:2:1:1:2:3:1:2:3:1:1:1:2:1:1:11:1:4:1:1:2:2:1:3:2:2:1:2:1:2:3:1:2:1:2:2:1:2:1:1:12:1:1:11:1:4:1:1:1:2:3:1:1:4:2:1:1:1:3:2:1:1:2:1:1:1:1:1:1:2:2:1:1:11:1:4:1:1:1:1:1:1:2:2:4:5:3:1:3:4:1:5:12:1:1:11:1:3:5:5:1:2:4:1:2:3:1:3:1:2:1:1:5:2:1:1:11:1:3:4:3:4:1:2:1:1:3:1:1:1:5:5:1:2:1:1:1:12:1:11:1:3:4:1:1:7:1:2:2:2:2:1:2:1:1:3:1:1:3:1:2:1:1:1:11:1:3:3:3:2:1:1:1:2:2:1:3:2:1:3:3:1:3:2:2:12:1:1:1:11:1:3:3:2:1:1:1:1:1:2:5:1:4:1:3:2:1:4:3:1:2:1:1:11:1:3:3:1:1:3:1:2:1:1:1:1:4:2:1:2:2:2:3:1:1:1:3:1:12:1:11:1:3:2:5:1:1:2:1:3:1:2:1:2:2:2:1:3:6:1:1:2:1:1:11:1:3:2:3:1:2:1:2:1:5:4:11:1:3:1:1:12:1:11:1:3:2:2:2:1:1:1:2:1:2:2:1:4:1:1:3:2:1:2:2:1:3:2:1:1:11:1:3:2:1:5:1:1:5:2:5:1:1:3:1:1:1:3:1:1:1:1:12:1:1:11:1:3:2:1:2:2:1:1:1:2:2:1:1:2:1:1:1:1:5:6:2:2:2:1:1:11:1:3:2:1:1:2:2:1:1:1:1:2:4:1:1:2:1:2:4:1:2:2:2:12:1:1:1:11:1:3:1:11:1:3:1:11:1:3:2:1:1:1:1:2:1:11:1:3:1:4:2:3:4:1:2:2:4:3:1:2:2:1:2:1:1:12:1:1:1:11:1:3:1:3:2:4:1:1:1:2:1:1:1:1:3:1:1:3:1:1:6:1:1:2:1:1:11:1:3:1:3:1:1:1:2:2:1:1:3:2:2:1:5:4:3:1:1:1:12:1:1:1:11:1:3:1:2:2:4:1:1:1:1:1:1:2:1:1:1:1:1:1:1:2:1:3:1:5:2:1:1:1:11:1:3:1:2:1:3:1:2:3:1:3:1:1:3:1:2:10:1:1:12:1:1:11:1:3:1:2:1:1:1:2:1:2:4:1:4:2:1:1:1:5:1:1:2:1:2:2:1:1:11:1:3:1:1:3:11:1:3:1:1:3:10:2:12:1:1:11:1:3:1:1:2:3:3:1:2:5:1:1:1:1:1:1:2:1:1:1:1:1:2:1:1:1:1:2:1:1:11:1:3:1:1:2:1:1:3:1:2:2:1:1:1:1:3:2:2:2:2:4:3:1:1:12:1:11:1:3:1:1:1:3:1:1:2:3:1:1:1:1:4:6:1:3:3:4:1:1:1:1:1:11:1:3:1:1:1:1:4:1:1:3:1:1:1:1:1:5:3:1:5:1:3:12:1:1:1:11:1:3:1:1:1:1:1:1:4:2:1:4:6:1:1:4:1:2:1:4:2:1:1:11:1:2:7:1:2:1:1:2:3:1:1:2:2:1:5:1:1:3:1:1:2:1:12:1:11:1:2:5:4:1:6:1:1:1:4:3:4:6:2:2:1:1:11:1:2:5:1:1:5:3:1:3:2:1:3:2:2:1:5:1:1:12:1:1:1:11:1:2:4:3:1:2:1:2:4:1:3:1:1:1:1:3:2:5:2:2:1:1:1:11:1:2:4:1:4:1:5:2:1:2:6:2:4:1:1:1:14:1:1:1:1:1:11:1:2:4:1:1:1:4:5:3:1:2:4:5:1:1:1:1:1:2:2:1:1:11:1:2:3:4:1:1:5:11:1:2:3:3:3:1:12:1:1:11:1:2:3:2:4:1:1:2:2:1:3:1:1:2:1:2:1:2:1:3:1:3:1:1:2:1:11:1:2:3:2:1:1:3:2:2:1:2:1:1:2:3:1:4:1:2:3:3:12:1:1:11:1:2:3:1:3:1:1:1:4:1:1:4:5:2:1:2:2:1:3:2:1:2:1:11:1:2:3:1:1:3:3:1:1:2:1:1:2:1:2:1:3:1:1:4:1:2:1:2:12:1:1:11:1:2:3:1:1:1:1:2:2:1:2:3:1:1:7:2:2:1:5:1:1:1:2:1:11:1:2:2:4:5:1:3:2:3:2:11:1:1:3:1:12:1:11:1:2:2:3:3:1:2:1:2:3:1:2:2:1:1:4:1:3:3:2:1:2:1:1:11:1:2:2:3:1:1:1:3:1:1:2:1:1:2:2:1:1:1:2:3:1:1:1:1:2:2:1:1:12:1:11:1:2:2:2:2:3:1:4:1:1:3:4:3:2:1:1:1:2:3:1:1:1:2:1:11:1:2:2:2:1:2:3:2:2:1:1:3:1:2:1:6:3:1:2:2:12:1:1:1:11:1:2:2:2:1:1:1:1:1:1:1:2:2:1:1:1:1:1:8:1:1:1:5:2:1:2:1:11:1:2:2:1:3:2:1:5:2:2:2:2:1:3:1:1:3:1:3:1:2:12:1:1:11:1:2:2:1:2:2:1:3:4:1:2:1:2:1:1:2:1:1:2:3:1:3:1:1:2:1:1:11:1:2:2:1:1:5:1:4:1:4:1:1:1:1:1:2:1:2:1:5:1:13:1:1:1:1:11:1:2:2:1:1:2:1:3:1:2:1:2:1:1:3:1:2:1:1:1:2:1:1:1:1:3:1:2:1:1:1:11:1:2:2:1:1:1:1:6:1:2:4:7:1:1:3:2:2:2:12:1:1:1:11:1:2:1:7:1:1:3:2:1:3:1:1:1:3:1:1:1:1:1:1:5:1:1:1:2:1:11:1:2:1:5:1:1:1:7:3:2:3:1:2:3:2:3:3:12:1:11:1:2:1:4:1:2:3:1:1:2:1:1:1:1:2:1:3:5:2:7:1:1:1:1:1:1:1:1:11:1:2:1:3:3:2:1:1:1:1:2:1:1:4:2:1:1:1:4:3:4:1:12:1:1:11:1:2:1:3:1:4:1:11:1:2:1:3:1:4:1:4:2:1:11:1:2:1:3:1:1:1:2:4:3:2:1:1:1:6:1:1:2:1:1:2:3:12:1:11:1:2:1:2:3:4:2:2:1:1:1:1:2:4:1:1:1:1:2:2:2:1:3:2:1:1:11:1:2:1:2:2:2:4:2:2:1:3:5:4:1:6:1:13:1:1:1:1:11:1:2:1:2:1:11:1:2:1:2:1:11:1:2:3:1:1:1:1:11:1:2:1:2:1:2:2:1:1:1:6:2:1:1:3:4:2:1:1:2:2:1:1:12:1:1:11:1:2:1:2:1:1:2:1:1:1:5:1:5:1:1:2:1:3:1:1:3:1:1:2:1:2:1:11:1:2:1:1:6:1:2:2:2:3:1:1:3:1:1:2:3:6:13:1:1:1:1:11:1:2:1:1:4:1:1:1:3:2:1:2:1:1:2:1:1:4:5:3:2:2:1:1:1:11:1:2:1:1:3:1:2:1:4:4:5:3:1:2:1:2:3:3:1:1:12:1:11:1:2:1:1:2:3:1:2:1:1:1:1:1:1:3:1:1:1:1:3:1:2:1:1:1:4:1:2:1:1:1:11:1:2:1:1:2:1:3:2:1:1:1:2:1:3:5:1:4:2:2:2:1:1:1:12:1:1:11:1:2:1:1:2:1:1:1:1:2:1:1:1:1:5:1:2:3:1:1:3:7:1:1:2:1:11:1:2:1:1:1:3:2:1:6:3:1:1:1:1:1:1:3:1:1:1:2:1:4:12:1:1:1:11:1:2:1:1:1:2:2:1:1:4:1:1:7:1:1:2:1:3:2:3:2:2:1:1:1:11:1:2:1:1:1:1:4:1:3:1:1:1:2:1:2:2:5:1:2:2:3:1:2:12:1:1:11:1:2:1:1:1:1:2:1:1:1:2:1:2:3:1:1:2:1:3:3:3:1:1:3:2:2:1:1:11:1:2:1:1:1:1:1:1:2:1:1:2:6:2:2:1:1:2:1:1:3:3:1:14:1:1:1:1:1:11:1:1:8:1:1:1:1:1:4:4:1:2:1:1:1:3:1:1:1:1:2:1:1:1:1:2:1:11:1:1:6:2:5:2:1:1:2:7:2:1:2:1:2:3:2:1:12:1:11:1:1:5:6:5:2:1:3:1:2:3:1:1:1:2:2:2:2:1:2:1:11:1:1:5:2:1:3:1:1:3:1:1:1:3:4:5:1:1:1:2:2:12:1:1:1:11:1:1:5:1:1:3:2:2:2:4:1:1:3:1:1:4:2:2:1:3:2:1:11:1:1:4:4:2:1:1:3:1:4:2:6:1:3:1:2:3:1:1:12:1:11:1:1:4:2:3:3:3:1:1:1:3:1:2:1:4:1:2:2:1:3:1:1:2:1:11:1:1:4:2:1:1:1:2:1:2:1:1:2:3:1:3:1:1:2:3:3:1:2:1:12:1:1:11:1:1:4:1:2:2:1:2:1:1:2:1:3:2:2:1:1:2:1:1:1:2:2:1:2:1:1:2:1:11:1:1:4:1:1:1:7:1:2:2:2:1:5:3:1:3:4:1:1:12:1:11:1:1:3:5:3:1:3:2:3:3:1:2:1:1:1:1:1:2:1:3:1:1:1:2:1:11:1:1:3:3:4:1:5:2:3:1:1:1:1:2:2:1:2:1:1:1:3:1:1:12:1:11:1:1:3:3:1:1:1:3:3:2:1:1:3:4:1:1:1:1:1:1:1:1:1:2:2:2:1:1:11:1:1:3:2:2:2:1:4:1:1:2:2:2:2:1:1:3:8:13:1:1:1:1:11:1:1:3:2:1:2:1:1:1:1:1:1:1:2:2:3:1:1:2:1:8:1:1:1:1:1:2:1:11:1:1:3:1:4:3:1:1:4:1:2:1:1:2:3:2:3:1:3:1:1:1:12:1:1:11:1:1:3:1:2:4:1:1:2:2:1:1:1:1:1:2:1:5:2:1:1:1:1:1:3:2:1:11:1:1:3:1:2:1:1:1:2:1:2:1:7:3:1:1:1:2:1:3:2:2:1:12:1:1:11:1:1:3:1:1:2:2:1:1:1:1:4:1:1:1:1:1:1:2:1:2:1:1:1:2:1:2:4:1:1:1:1:1:11:1:1:3:1:1:1:1:4:1:1:1:1:2:1:1:2:1:3:1:5:2:2:15:1:1:1:1:1:1:11:1:1:2:6:1:5:1:1:1:1:2:3:2:2:3:1:3:2:2:1:2:1:1:11:1:1:2:4:3:1:2:3:1:1:2:1:2:3:3:2:2:1:1:1:1:2:1:11:1:1:11:1:1:2:3:6:4:2:3:2:1:2:2:3:4:2:3:2:1:11:1:1:2:3:2:1:2:1:1:2:4:1:1:1:1:3:1:1:2:1:5:1:3:2:1:11:1:1:2:3:1:1:2:1:3:3:2:1:2:2:1:1:4:1:1:1:1:2:2:2:11:1:11:1:1:2:2:3:4:5:2:1:3:1:2:2:1:1:5:1:1:4:1:1:1:1:1:11:1:1:2:2:2:2:2:1:1:1:1:4:1:1:3:2:3:2:1:1:1:2:2:1:2:1:1:1:11:1:1:2:2:1:4:1:2:1:2:2:3:3:1:2:1:3:1:2:2:2:2:11:1:11:1:1:2:2:1:1:5:1:1:2:1:1:1:3:1:2:2:1:3:2:1:1:1:2:2:2:1:11:1:1:2:2:1:1:1:1:1:2:8:3:3:1:2:1:1:1:4:2:3:1:1:1:1:11:1:1:2:1:4:1:6:1:1:1:3:1:1:1:5:1:1:1:1:1:1:2:1:1:11:1:1:1:11:1:1:2:1:3:1:3:1:1:3:4:1:1:1:2:1:6:1:1:3:1:1:2:1:1:1:11:1:1:2:1:2:3:1:1:1:11:1:1:2:1:2:3:1:1:1:3:2:1:1:1:11:1:1:2:1:2:1:2:2:2:4:11:1:1:2:1:2:1:2:1:1:11:1:1:11:1:1:2:1:1:5:1:4:2:3:2:1:4:1:2:3:4:1:1:1:2:1:1:11:1:1:2:1:1:2:5:1:1:1:3:3:6:1:2:1:6:1:1:1:2:1:1:11:1:1:2:1:1:2:1:1:1:2:1:2:1:1:3:1:5:1:4:2:1:1:1:3:1:1:11:1:11:1:1:2:1:1:1:2:1:3:1:2:2:4:3:11:1:1:1:1:2:1:1:1:11:1:1:2:1:1:1:1:1:2:2:1:2:1:4:1:1:4:2:2:1:1:3:3:3:1:1:1:1:11:1:1:1:6:4:1:1:2:2:2:2:3:2:1:2:2:3:3:1:1:1:11:1:11:1:1:1:5:2:1:1:5:3:1:2:4:1:2:1:5:3:2:2:1:11:1:1:1:4:3:2:3:1:2:2:1:1:1:2:1:1:2:3:3:2:2:1:2:1:1:1:11:1:1:1:4:1:3:1:2:1:2:1:1:1:1:3:1:2:5:3:3:1:1:1:11:1:1:11:1:1:1:3:6:1:2:1:2:1:2:2:1:1:4:1:2:1:3:1:3:2:1:1:1:11:1:1:1:3:2:9:2:1:2:2:1:1:1:7:4:3:2:1:11:1:1:1:3:2:1:1:1:3:1:2:1:2:1:3:1:1:3:1:2:3:1:1:1:3:11:1:11:1:1:1:3:1:2:1:3:1:2:1:1:2:1:4:4:2:2:1:2:3:2:2:1:11:1:1:1:3:1:1:1:2:1:2:2:1:2:1:3:1:2:2:4:1:2:1:6:1:1:1:1:1:1:1:11:1:1:1:2:4:3:1:1:1:3:1:2:2:1:2:2:1:1:3:1:6:11:1:1:1:11:1:1:1:2:3:1:4:11:1:1:1:2:3:1:4:3:1:2:1:1:11:1:1:1:2:2:3:1:1:3:3:1:1:1:6:1:2:1:1:1:4:1:1:1:1:1:2:1:11:1:1:1:2:2:1:2:2:1:1:2:1:1:3:1:1:1:1:4:3:8:11:1:1:11:1:1:1:2:1:4:3:2:6:3:3:4:3:1:5:1:2:1:1:11:1:1:1:2:1:2:3:1:2:1:1:3:3:1:3:3:2:2:2:2:2:2:2:1:11:1:1:1:2:1:1:4:3:2:3:2:5:3:2:1:2:3:3:1:11:1:1:11:1:1:1:2:1:1:1:4:2:6:6:1:1:7:2:1:2:2:1:1:1:11:1:1:1:1:11:1:1:1:1:11:1:1:1:2:1:1:1:1:1:1:1:2:1:11:1:1:1:1:5:1:1:1:2:3:3:1:5:1:4:1:7:1:11:1:1:1:11:1:1:1:1:4:1:1:1:3:1:4:1:1:2:2:1:2:2:2:2:1:1:2:2:1:1:2:1:11:1:1:1:1:3:2:1:2:1:4:1:3:4:2:1:2:2:3:1:1:2:1:2:1:1:1:11:1:1:1:1:3:1:1:1:3:1:4:5:1:2:2:1:2:1:6:1:1:1:1:11:1:11:1:1:1:1:2:3:1:7:2:2:1:5:4:4:1:1:1:2:1:2:1:1:11:1:1:1:1:2:2:1:2:1:2:1:7:4:1:2:4:1:2:1:2:3:1:1:1:1:11:1:1:1:1:2:1:2:2:2:1:4:3:1:2:1:4:1:1:1:1:3:2:1:12:1:1:1:1:11:1:1:1:1:2:1:1:1:2:4:1:1:4:4:1:1:1:1:3:1:1:2:1:2:3:1:1:1:1:11:1:1:1:1:1:4:3:1:2:1:1:2:1:1:8:1:2:1:1:3:1:2:2:1:1:1:11:1:1:1:1:1:3:1:3:2:3:1:2:1:1:5:1:2:4:2:2:1:1:1:11:1:1:11:1:1:1:1:1:2:2:4:1:1:2:1:1:1:2:1:2:1:2:1:3:3:2:1:3:2:1:11:1:1:1:1:1:2:1:1:4:2:6:3:2:1:2:2:2:1:2:4:2:1:1:11:1:1:1:1:1:1:3:5:1:5:2:2:2:2:1:2:2:2:4:1:1:11:1:1:11:1:1:1:1:1:1:2:1:6:1:4:1:1:6:1:1:3:1:2:1:1:2:2:1:1:1:11:1:1:1:1:1:1:1:2:7:4:1:1:1:1:3:2:4:2:4:2:2:1:11:1:1:1:1:1:1:1:1:1:11:1:1:1:1:1:1:1:1:1:10:1:1:11:1:1:10:10:10:10:10:3:1:1:1:1:10:9:1:7:3:6:1:2:1:4:2:5:1:2:1:10:8:2:6:1:3:4:1:1:1:1:2:1:3:1:1:2:3:1:11:1:10:8:1:1:5:1:4:2:2:2:1:3:2:2:1:1:3:2:1:2:1:1:10:7:3:4:2:4:1:2:1:2:1:1:1:1:2:1:1:2:1:2:1:1:1:1:2:1:10:7:2:1:4:1:1:1:2:3:2:6:2:1:1:2:5:1:11:1:10:7:1:2:3:2:2:2:1:3:2:1:4:2:8:3:1:1:1:1:10:7:1:1:1:3:1:1:3:4:2:1:5:3:2:1:4:1:2:1:1:1:10:6:4:2:3:3:2:1:1:1:6:1:1:1:1:1:5:1:1:11:1:1:1:10:6:3:1:2:2:1:2:1:1:1:1:1:2:3:1:3:2:2:1:4:1:1:1:2:1:10:6:2:2:2:1:1:5:4:1:1:2:1:1:2:1:1:3:1:3:1:1:1:2:1:10:6:2:1:1:1:5:1:1:2:3:3:1:1:1:2:4:1:1:1:1:1:1:1:11:1:1:10:6:1:3:1:2:8:2:3:10:4:2:2:1:10:6:1:2:1:1:2:1:1:2:1:3:2:1:1:1:1:8:2:2:3:2:1:10:6:1:1:2:1:1:2:1:5:1:6:2:1:1:1:2:2:2:1:1:1:1:11:1:1:10:6:1:1:1:1:1:1:1:1:2:1:1:1:1:10:5:6:2:1:1:10:5:4:5:1:3:2:2:5:1:2:1:2:1:4:3:1:2:1:10:5:3:5:1:1:1:2:4:2:1:2:5:2:5:1:1:1:11:1:1:10:5:3:1:3:1:4:1:4:2:4:1:1:1:1:2:1:4:1:3:1:1:1:1:10:5:2:4:4:2:1:2:3:1:1:4:1:1:2:2:2:1:1:1:1:2:1:1:1:10:5:2:2:2:2:1:2:2:1:1:4:1:1:3:1:1:2:5:2:1:11:1:1:1:10:5:2:1:3:1:1:4:2:6:1:10:4:3:1:1:1:1:10:5:2:1:1:1:4:4:1:5:2:4:1:3:2:2:1:1:1:1:1:2:1:10:5:1:4:2:5:2:1:2:4:4:1:4:1:1:2:2:11:1:1:10:5:1:3:1:2:1:1:2:1:3:1:2:2:2:3:1:3:1:1:2:1:1:1:1:1:2:1:10:5:1:2:2:1:2:2:1:4:2:2:2:3:1:1:1:1:2:1:1:3:1:1:2:1:1:10:5:1:2:1:1:1:1:1:3:3:3:2:3:2:1:3:4:1:1:1:1:11:1:1:1:10:5:1:1:2:5:1:1:1:1:3:2:1:3:2:3:2:1:1:1:2:1:3:1:1:1:1:10:5:1:1:1:4:2:1:1:5:6:1:2:3:1:1:1:3:4:1:1:1:1:1:10:5:1:1:1:1:2:3:1:2:1:2:1:1:2:3:1:1:1:1:1:2:1:3:1:3:11:1:10:4:7:1:2:1:1:2:3:2:1:3:1:1:2:1:2:3:3:1:2:1:1:1:10:4:5:1:3:5:2:3:1:4:1:1:2:5:3:1:1:1:2:1:10:4:4:2:2:1:3:1:1:1:1:2:2:1:1:2:1:2:1:3:2:2:1:12:1:1:1:1:10:4:4:1:1:1:2:1:5:1:2:4:1:2:1:1:4:1:2:1:2:2:1:1:10:4:3:3:1:1:1:1:1:1:5:1:1:1:1:1:2:5:1:2:3:1:3:1:1:1:1:10:4:3:1:4:3:2:4:4:1:1:1:1:1:1:1:1:2:1:1:2:2:1:11:1:10:4:3:1:1:2:3:1:2:2:6:1:1:1:1:3:1:1:3:1:1:1:1:1:1:2:1:10:4:2:5:1:1:2:1:2:2:1:4:1:10:3:2:2:1:1:10:4:2:3:1:2:5:2:1:4:2:2:2:2:1:2:1:3:3:11:1:10:4:2:2:2:1:3:1:2:2:2:2:1:1:1:10:3:2:1:2:1:10:4:2:2:1:1:1:1:1:1:2:2:3:3:6:1:2:1:3:2:3:2:1:10:4:2:1:2:3:2:3:4:2:4:1:3:1:1:2:1:4:2:11:1:10:4:2:1:1:3:2:1:4:1:1:6:2:3:2:1:1:4:4:1:1:1:1:1:10:4:2:1:1:1:1:4:3:1:1:1:1:1:4:1:1:3:2:2:1:1:1:1:1:1:2:1:1:1:10:4:1:5:2:1:1:1:4:2:1:1:1:1:2:3:10:1:1:11:1:10:4:1:4:1:1:1:1:8:2:2:1:1:1:1:3:1:2:1:4:2:2:1:10:4:1:3:1:3:3:2:4:1:1:1:1:4:3:3:1:2:1:1:2:2:1:10:4:1:2:4:2:1:2:1:2:1:1:1:1:3:1:2:2:3:2:1:1:1:1:1:1:1:11:1:10:4:1:2:2:1:3:3:1:3:1:4:2:1:2:1:1:2:2:1:1:1:1:1:2:1:1:1:10:4:1:2:1:2:1:3:1:5:1:1:1:1:1:4:1:3:1:2:1:1:1:1:2:2:1:10:4:1:1:9:1:2:2:2:7:3:1:1:1:1:1:1:3:1:11:1:10:4:1:1:3:2:5:3:1:2:1:1:2:2:6:2:2:2:1:2:1:1:10:4:1:1:2:3:1:1:1:1:1:1:1:3:2:2:5:7:2:2:1:2:1:10:4:1:1:2:1:1:2:1:1:1:1:5:1:1:2:1:3:2:1:1:1:3:1:3:1:11:1:10:4:1:1:1:3:1:1:1:1:1:1:6:2:2:2:5:1:2:1:1:4:2:1:10:4:1:1:1:1:4:1:1:1:1:1:2:2:1:1:1:3:1:1:1:1:1:4:2:1:1:1:2:1:1:1:10:4:1:1:1:1:1:1:6:5:1:2:1:1:4:3:1:1:1:1:1:2:1:11:1:1:10:3:7:1:4:1:3:4:5:3:1:1:2:1:2:1:1:1:1:1:2:1:10:3:5:6:2:1:1:1:2:1:2:2:2:1:1:1:1:4:1:1:1:2:1:2:1:10:3:5:1:2:2:3:2:1:1:5:1:2:1:1:2:1:1:4:1:1:2:11:1:10:3:4:3:3:1:2:1:2:2:1:2:1:1:3:3:8:1:2:1:1:10:3:4:2:1:1:1:3:2:2:3:2:2:2:1:1:3:1:1:2:2:4:1:1:1:1:1:10:3:4:1:1:3:1:5:2:1:4:1:2:1:5:1:2:2:3:11:1:10:3:3:5:1:1:1:1:2:1:3:3:2:1:1:1:1:2:2:3:2:1:2:2:1:10:3:3:3:1:1:3:2:1:1:1:1:1:4:3:7:2:3:1:1:2:1:10:3:3:2:1:4:1:2:1:1:1:3:1:1:2:2:1:4:3:2:1:2:11:1:1:10:3:3:1:4:1:2:1:2:3:2:1:1:3:5:5:2:1:1:2:1:1:10:3:3:1:2:1:1:10:3:3:1:2:1:1:8:2:2:1:10:3:3:1:1:1:4:2:4:1:1:1:1:2:1:1:2:2:2:1:1:2:1:1:1:2:11:1:10:3:2:6:1:2:2:3:1:1:3:1:3:6:2:1:3:2:2:1:10:3:2:4:1:1:6:1:1:1:1:2:3:1:1:1:1:2:1:1:2:2:4:2:1:10:3:2:3:1:4:1:2:1:6:1:5:1:2:4:2:2:12:1:1:1:10:3:2:2:4:1:1:2:2:1:2:2:3:3:1:2:2:1:1:1:4:3:1:1:1:10:3:2:2:2:1:1:2:4:1:1:1:5:5:1:1:2:1:1:3:3:2:1:10:3:2:2:1:1:3:2:1:1:1:1:2:1:6:1:2:2:1:1:2:2:1:13:1:1:1:1:10:3:2:1:4:4:1:1:1:4:1:2:5:1:3:3:1:2:2:1:2:1:10:3:2:1:3:1:1:1:1:7:6:1:1:2:2:1:3:3:4:1:1:1:1:10:3:2:1:2:1:2:2:3:2:1:4:1:1:1:2:1:1:2:1:1:2:3:1:1:1:11:1:10:3:2:1:1:3:2:1:2:1:1:4:4:2:1:2:2:1:1:1:3:4:2:1:10:3:2:1:1:1:5:1:1:2:9:1:1:4:1:2:3:1:2:2:1:1:10:3:2:1:1:1:1:1:5:3:3:1:3:4:5:1:1:2:2:12:1:1:1:10:3:1:6:1:1:2:1:2:1:1:2:1:1:3:1:1:2:2:3:3:2:1:1:2:1:10:3:1:4:3:2:1:4:1:3:1:1:1:2:1:2:1:5:1:6:1:1:1:1:1:1:10:3:1:4:1:1:1:4:2:2:1:1:3:1:1:3:1:2:3:1:4:2:11:1:10:3:1:3:2:3:1:3:1:1:1:3:1:3:10:2:4:2:1:10:3:1:3:1:2:2:4:1:1:1:2:1:1:2:3:2:1:1:1:1:2:1:1:2:2:2:1:10:3:1:2:7:2:2:1:2:2:4:1:5:2:1:1:1:4:1:11:1:10:3:1:2:3:1:4:3:3:1:3:3:2:1:2:1:4:3:1:1:2:1:10:3:1:2:2:2:1:1:1:1:1:1:3:1:1:1:1:2:3:6:1:1:1:2:4:1:1:1:1:10:3:1:2:1:4:1:1:2:2:1:4:1:4:4:2:4:4:1:11:1:10:3:1:2:1:2:1:1:1:2:1:1:3:1:4:2:1:1:1:1:1:1:1:1:2:1:2:4:1:1:1:1:10:3:1:2:1:1:1:2:1:2:3:6:3:3:5:1:1:3:4:1:1:1:1:10:3:1:1:5:1:1:3:2:2:1:1:2:4:7:2:2:1:1:12:1:1:1:10:3:1:1:3:3:1:4:1:1:2:1:2:2:2:2:1:1:1:1:3:6:1:1:1:1:1:1:1:10:3:1:1:3:1:1:1:1:4:1:1:1:3:2:1:2:4:4:6:2:1:1:10:3:1:1:2:2:2:1:3:2:1:2:1:1:3:2:2:5:1:2:2:1:12:1:1:1:10:3:1:1:2:1:2:1:1:2:3:1:6:2:1:1:2:1:2:3:1:2:1:3:1:1:1:10:3:1:1:1:5:1:2:1:1:1:3:2:2:1:1:4:2:2:2:2:3:2:1:1:10:3:1:1:1:3:1:1:1:1:2:1:2:2:1:2:1:2:2:4:2:1:2:2:2:11:1:1:10:3:1:1:1:2:1:1:10:3:1:1:1:2:1:1:9:4:1:1:1:1:10:3:1:1:1:1:2:5:2:1:1:6:3:2:2:2:3:1:1:3:1:2:1:10:3:1:1:1:1:1:2:3:1:4:1:3:2:6:1:1:3:2:1:2:2:11:1:10:2:10:2:10:2:10:2:3:1:2:1:10:2:7:1:2:1:1:3:3:2:1:1:1:2:2:5:4:3:1:2:1:10:2:6:1:2:3:1:4:2:2:5:2:1:1:3:1:1:1:1:1:1:1:11:1:10:2:5:3:1:1:5:2:1:1:2:3:1:2:3:3:2:1:2:2:2:1:10:2:5:1:3:1:1:1:4:3:2:3:1:1:1:1:1:2:1:1:3:1:4:1:1:1:1:10:2:4:7:4:1:2:1:1:5:1:1:1:1:7:2:12:1:1:1:10:2:4:3:1:2:2:1:3:1:1:4:1:3:5:1:1:1:1:3:3:1:1:1:10:2:4:2:1:2:3:2:5:1:4:1:3:1:1:5:3:1:1:2:1:10:2:4:1:3:1:1:2:3:2:2:1:1:2:2:1:2:4:1:1:3:2:1:11:1:10:2:4:1:1:2:4:2:2:2:1:2:3:1:1:3:3:1:2:1:3:2:1:1:10:2:3:7:2:1:1:4:4:1:3:1:1:1:1:3:1:1:1:2:1:1:2:1:10:2:3:4:1:1:2:3:1:1:2:2:1:4:1:1:1:1:3:2:3:3:11:1:10:2:3:3:1:2:1:1:1:2:1:2:2:2:3:2:1:6:1:1:1:3:1:2:1:10:2:3:2:2:3:3:2:4:1:1:1:1:1:1:1:1:2:2:1:2:2:1:1:2:2:1:10:2:3:2:1:2:1:2:4:4:3:3:1:1:3:2:3:2:13:1:1:1:1:10:2:3:1:5:1:1:1:1:1:1:3:1:6:2:1:6:1:1:1:1:1:2:1:1:10:2:3:1:2:4:2:1:1:1:2:1:1:1:1:2:1:1:1:4:2:2:1:1:2:2:2:1:10:2:3:1:2:1:1:1:1:5:1:1:1:1:1:1:1:1:2:2:1:3:5:2:12:1:1:1:10:2:3:1:1:2:1:3:2:1:2:2:1:1:3:5:1:2:2:1:2:3:2:1:1:10:2:3:1:1:1:1:2:3:1:2:1:2:2:1:1:1:2:2:1:1:2:2:1:2:1:2:2:1:1:10:2:2:5:6:1:2:1:1:3:1:2:2:1:3:5:1:1:1:1:1:11:1:10:2:2:4:2:1:5:3:1:4:1:1:10:2:1:3:2:1:10:2:2:3:3:10:2:2:3:3:9:2:1:2:1:10:2:2:3:1:3:9:1:1:1:1:1:1:3:2:1:7:1:1:11:1:1:10:2:2:2:10:2:2:2:10:2:2:2:2:2:2:1:10:2:2:2:3:1:1:4:1:1:2:1:2:6:1:3:2:3:1:1:4:1:1:1:1:10:2:2:2:2:1:1:4:1:1:2:5:3:1:2:2:1:2:1:1:1:3:12:1:1:1:10:2:2:2:1:2:2:2:1:10:2:2:2:1:2:2:2:1:2:3:1:1:1:10:2:2:2:1:1:1:3:4:1:1:1:1:3:1:2:10:2:2:1:1:2:1:10:2:2:1:5:1:1:1:3:4:1:2:2:3:3:1:2:2:1:3:1:11:1:1:10:2:2:1:3:2:1:5:2:1:1:1:3:1:1:5:2:3:1:1:1:3:2:1:10:2:2:1:2:3:3:2:1:4:3:1:2:4:1:2:2:2:1:1:1:1:2:1:1:10:2:2:1:2:1:3:1:2:2:1:1:1:2:3:1:3:1:1:1:1:6:1:1:1:11:1:1:10:2:2:1:1:5:5:1:1:1:1:1:1:1:1:2:2:4:1:1:1:2:1:1:1:1:2:1:1:10:2:2:1:1:2:5:1:1:1:2:2:2:1:2:1:1:2:1:3:3:3:1:2:2:1:10:2:2:1:1:2:1:1:1:1:1:1:2:1:1:1:1:3:1:7:1:4:2:3:1:11:1:10:2:2:1:1:1:2:1:1:2:1:2:1:1:2:2:1:4:1:5:2:2:2:1:2:2:1:10:2:2:1:1:1:1:1:1:2:1:1:1:1:7:1:4:2:1:1:1:2:1:2:1:2:1:2:1:10:2:1:6:1:2:4:2:1:1:1:1:4:1:5:1:1:2:1:2:1:1:11:1:1:10:2:1:5:1:1:4:2:2:4:3:1:10:2:1:2:1:2:1:10:2:1:4:2:2:1:1:2:3:2:1:1:1:2:1:1:2:2:2:1:2:1:1:1:1:1:2:1:1:10:2:1:4:1:1:2:1:5:1:3:1:2:1:2:4:4:1:2:1:1:1:11:1:1:10:2:1:3:3:2:2:4:3:3:1:1:2:2:1:2:1:1:3:1:1:3:1:1:1:1:10:2:1:3:2:1:2:5:1:2:1:3:1:1:1:2:1:1:2:4:3:1:3:1:1:1:10:2:1:3:1:2:3:1:1:5:1:4:6:1:1:2:2:1:2:1:1:11:1:1:10:2:1:3:1:1:1:3:1:8:1:1:6:2:1:1:1:1:1:4:1:2:1:1:10:2:1:2:4:3:2:2:2:3:1:2:1:1:2:3:4:1:1:2:2:2:1:1:10:2:1:2:3:1:3:3:1:1:1:1:1:3:1:2:3:3:1:1:3:1:2:1:11:1:1:10:2:1:2:2:2:3:2:10:2:1:2:2:2:3:2:2:2:2:1:10:2:1:2:2:1:1:3:2:2:2:1:1:1:2:1:3:1:2:3:1:1:2:4:1:2:1:10:2:1:2:1:3:3:1:5:1:4:1:1:2:1:4:1:2:2:1:3:1:11:1:10:2:1:2:1:2:1:3:1:1:3:2:1:1:1:2:1:1:1:1:3:3:1:1:6:1:1:1:1:1:1:1:10:2:1:2:1:1:2:2:4:2:4:1:2:4:4:2:1:1:1:1:2:3:1:1:1:10:2:1:2:1:1:1:1:2:2:3:1:1:2:2:5:1:3:3:4:3:1:11:1:10:2:1:1:5:2:1:3:2:3:2:1:2:8:1:1:2:5:1:1:1:1:1:1:10:2:1:1:4:1:2:1:1:1:3:1:2:2:1:1:3:1:3:2:1:1:4:2:1:2:1:10:2:1:1:3:2:1:3:4:3:1:3:1:5:1:1:4:4:1:11:1:1:10:2:1:1:3:1:1:1:2:2:2:1:1:1:6:1:4:2:1:4:2:2:1:2:1:10:2:1:1:2:3:1:1:3:3:2:1:6:10:1:4:1:2:1:10:2:1:1:2:2:1:1:1:1:2:2:5:2:1:2:2:3:4:2:1:13:1:1:1:1:1:10:2:1:1:2:1:1:6:1:2:1:1:1:2:2:2:1:5:5:2:1:2:2:1:10:2:1:1:1:7:1:2:3:1:1:4:1:1:2:1:1:9:2:1:2:1:10:2:1:1:1:3:3:2:1:1:1:1:5:5:1:1:1:4:1:2:3:1:1:11:1:10:2:1:1:1:2:3:4:1:4:1:10:2:1:1:1:2:2:3:1:1:1:1:10:2:1:1:1:2:1:2:2:1:1:1:3:1:1:1:4:1:4:1:2:3:3:2:1:2:1:10:2:1:1:1:1:4:1:1:1:5:1:4:5:2:1:1:5:2:2:11:1:1:10:2:1:1:1:1:2:1:3:3:1:1:1:1:2:1:1:1:1:3:1:2:2:2:3:1:2:1:2:1:10:2:1:1:1:1:1:2:2:1:4:3:3:3:1:1:1:1:1:1:1:1:2:4:3:1:1:1:1:10:2:1:1:1:1:1:1:1:1:2:10:2:1:1:1:1:1:1:1:1:3:1:1:1:1:1:1:11:1:1:10:1:8:1:1:1:5:3:2:1:3:2:1:1:2:1:1:1:1:1:2:1:1:2:1:1:10:1:6:5:3:2:1:2:1:3:1:1:1:2:2:1:1:2:3:2:1:2:1:1:10:1:6:1:3:1:2:1:2:1:2:2:3:6:1:1:3:1:14:1:1:1:1:1:1:10:1:5:4:2:1:1:3:4:2:6:1:3:1:2:1:1:1:2:1:2:1:10:1:5:2:1:6:1:1:1:1:1:1:1:1:3:1:1:1:2:3:1:1:1:1:3:1:2:1:10:1:5:1:2:2:2:1:2:3:1:2:2:1:1:3:2:1:2:2:1:2:1:1:11:1:1:10:1:5:1:1:1:1:2:1:6:1:2:7:2:2:2:1:2:1:1:2:2:1:10:1:4:3:6:3:1:1:1:1:1:2:2:2:7:1:1:3:2:1:1:1:10:1:4:2:4:1:3:1:1:3:2:2:1:1:10:1:4:1:11:1:10:1:4:2:1:2:1:3:10:1:4:2:1:2:1:3:2:1:1:2:1:10:1:4:1:3:3:2:1:3:4:3:1:1:1:7:6:2:1:1:10:1:4:1:2:1:1:3:2:3:1:3:5:1:1:3:2:1:1:1:1:1:1:2:11:1:10:1:4:1:1:1:10:1:4:1:1:1:10:1:2:2:2:1:1:10:1:3:7:1:1:5:4:1:1:1:1:1:2:2:5:1:1:1:2:2:1:1:10:1:3:4:2:1:1:3:5:2:2:3:2:2:1:1:5:2:1:11:1:1:10:1:3:3:2:2:2:1:2:1:1:2:10:1:3:3:1:4:1:1:1:1:1:10:1:3:3:1:1:1:1:2:2:1:2:1:1:1:1:2:5:2:2:2:3:3:2:1:1:10:1:3:2:2:3:2:2:1:3:6:1:2:2:1:1:2:6:11:1:1:1:10:1:3:2:1:3:1:2:1:3:1:10:1:3:2:1:3:1:1:2:1:1:1:10:1:3:2:1:1:1:2:1:6:2:1:1:1:1:1:4:1:2:2:2:2:1:1:2:1:1:1:10:1:3:1:4:1:1:1:2:1:8:1:1:1:1:1:1:1:1:3:1:3:1:1:1:1:11:1:10:1:3:1:2:4:1:1:1:2:1:2:1:3:3:4:1:1:2:2:4:1:1:2:1:10:1:3:1:2:1:1:5:1:1:2:1:2:1:1:1:2:2:3:3:1:3:2:2:1:1:1:10:1:3:1:1:3:1:2:3:1:2:10:1:3:1:1:3:1:1:2:11:1:1:10:1:3:1:1:1:4:5:3:2:3:1:2:3:1:1:1:1:2:2:1:2:1:2:1:10:1:3:1:1:1:1:2:1:1:4:3:1:4:4:2:1:1:3:5:1:1:2:1:10:1:2:6:1:5:1:2:2:1:2:1:1:3:3:2:1:1:2:1:1:2:11:1:1:10:1:2:4:5:2:1:1:1:2:2:1:1:3:2:2:1:1:2:1:5:1:1:2:1:10:1:2:4:1:2:1:2:1:1:4:2:1:1:2:2:1:4:1:1:4:2:1:2:1:1:10:1:2:3:3:1:4:1:2:1:1:4:10:1:2:3:1:1:1:11:1:10:1:2:3:1:4:5:1:1:3:1:2:1:2:1:2:3:1:1:1:1:2:2:2:1:1:10:1:2:3:1:1:2:1:1:1:1:1:2:1:1:1:2:1:1:2:2:5:2:3:1:1:2:1:1:1:10:1:2:2:4:1:2:1:1:1:3:1:3:4:1:2:4:4:1:1:1:1:1:11:1:10:1:2:2:2:3:10:1:2:2:2:3:9:1:1:2:1:1:10:1:2:2:2:1:1:2:3:6:1:1:1:1:1:2:1:1:1:2:3:2:1:1:1:1:11:1:1:10:1:2:2:1:2:5:2:3:1:1:1:3:1:7:2:1:1:1:3:1:2:1:1:10:1:2:2:1:1:3:1:1:3:1:3:2:1:2:1:1:2:4:1:1:5:1:1:1:11:1:10:1:2:2:1:1:1:1:1:2:1:1:1:3:5:2:2:4:2:1:3:1:2:1:2:1:1:10:1:2:1:4:2:3:2:1:1:2:1:1:1:2:1:1:1:1:1:1:2:2:1:1:2:1:1:1:11:1:1:10:1:2:1:3:2:2:1:1:2:1:1:2:3:2:1:1:1:1:1:1:1:1:3:2:5:1:1:1:1:1:1:10:1:2:1:2:5:2:1:1:10:1:2:1:2:5:15:1:1:1:1:1:1:1:10:1:2:1:2:2:1:2:4:2:1:2:2:3:1:2:2:2:1:2:4:3:1:1:1:1:10:1:2:1:2:1:1:4:1:2:4:2:5:3:1:1:2:1:1:3:1:2:11:1:1:10:1:2:1:1:4:3:2:10:1:2:1:1:4:3:2:2:1:2:1:1:10:1:2:1:1:2:5:6:1:3:1:1:1:3:1:2:1:1:1:2:1:3:1:11:1:1:10:1:2:1:1:2:1:1:4:1:1:1:2:1:6:3:1:5:1:1:1:2:3:1:1:1:1:10:1:2:1:1:1:2:4:5:1:1:1:3:1:2:1:3:2:2:4:2:1:11:1:1:10:1:2:1:1:1:1:2:3:2:2:3:3:1:2:1:2:6:1:1:2:1:1:1:1:2:1:10:1:1:10:1:1:10:1:1:10:1:1:13:1:1:1:1:1:10:1:1:6:1:2:3:2:1:4:2:1:3:1:3:3:1:1:1:1:2:1:1:2:1:10:1:1:5:1:3:1:4:1:1:3:2:4:1:1:1:1:2:3:3:1:1:11:1:1:10:1:1:4:3:1:7:1:4:1:5:2:4:2:1:5:1:1:1:1:1:1:10:1:1:4:1:3:2:3:2:5:2:1:1:1:1:1:1:2:2:2:15:1:1:1:1:1:1:1:10:1:1:4:1:1:1:1:1:3:1:1:4:4:2:2:1:1:3:1:1:1:1:1:1:1:2:1:1:1:10:1:1:3:3:2:1:1:1:1:1:3:1:2:1:1:1:1:2:1:4:2:2:1:1:1:1:1:11:1:1:10:1:1:3:2:1:4:1:4:2:2:4:3:3:1:5:1:1:1:1:1:2:1:10:1:1:3:1:3:2:1:2:2:1:1:5:1:1:2:1:2:1:4:1:1:3:11:1:1:1:10:1:1:3:1:1:3:2:2:2:1:1:3:4:1:1:2:2:2:1:3:3:1:2:1:1:10:1:1:3:1:1:1:1:1:1:1:1:4:1:10:1:1:3:1:1:1:1:1:1:1:1:1:11:1:10:1:1:2:4:1:3:1:1:1:2:2:1:4:2:2:2:2:1:1:1:1:3:1:2:1:1:1:10:1:1:2:3:1:2:4:1:1:1:2:3:3:2:1:1:1:9:12:1:1:1:1:10:1:1:2:2:3:1:1:1:3:1:1:2:4:2:3:3:1:2:1:1:2:1:1:2:1:1:1:10:1:1:2:2:1:2:2:4:2:2:1:3:1:3:1:1:1:1:2:1:1:1:2:1:1:1:1:11:1:10:1:1:2:1:5:2:2:3:1:2:1:1:1:1:2:10:1:1:1:1:1:2:1:1:10:1:1:2:1:2:6:2:4:2:1:1:1:3:1:1:9:13:1:1:1:1:1:10:1:1:2:1:2:1:1:1:5:2:2:2:2:1:1:1:1:3:1:1:1:1:2:2:2:1:1:2:1:10:1:1:2:1:1:2:2:1:1:1:1:1:3:2:1:1:1:4:1:4:2:1:2:1:1:1:11:1:1:1:10:1:1:2:1:1:1:1:3:1:1:1:2:2:3:1:1:1:4:2:1:1:1:1:2:1:1:1:1:1:2:1:1:10:1:1:1:6:1:1:1:1:1:1:1:1:5:1:1:1:1:10:1:1:1:2:11:1:1:10:1:1:1:4:2:1:4:2:4:4:2:1:1:4:1:2:6:2:1:10:1:1:1:3:3:4:1:2:1:1:1:1:2:2:1:3:2:1:2:1:1:1:2:2:11:1:1:1:10:1:1:1:3:1:3:2:1:2:4:1:1:1:2:1:2:1:1:1:1:3:1:1:2:1:1:2:1:1:1:10:1:1:1:2:6:2:3:1:1:1:1:3:1:1:3:1:2:2:1:1:2:1:1:1:1:1:11:1:10:1:1:1:2:3:1:1:1:1:1:1:2:3:5:6:4:2:6:1:1:1:1:1:1:1:10:1:1:1:2:2:1:1:1:2:3:1:1:1:3:1:1:1:1:1:1:1:1:5:5:1:11:1:1:1:10:1:1:1:2:1:2:1:2:2:1:1:6:2:2:3:1:2:1:3:1:1:1:1:2:2:1:10:1:1:1:2:1:1:1:1:4:2:1:1:1:2:1:2:1:3:1:1:4:2:6:11:1:10:1:1:1:1:4:2:1:2:1:2:2:3:1:4:4:8:4:1:1:1:1:1:10:1:1:1:1:3:1:2:2:3:3:1:2:5:2:1:3:1:2:1:2:1:1:1:11:1:1:10:1:1:1:1:2:2:3:1:1:2:1:3:1:1:2:4:1:1:1:1:1:1:2:4:1:2:1:1:1:10:1:1:1:1:2:1:1:3:3:1:1:1:1:3:1:3:1:1:3:2:1:1:1:1:2:1:12:1:1:1:1:10:1:1:1:1:1:3:4:1:6:2:2:1:2:1:2:7:2:3:2:1:10:1:1:1:1:1:2:1:5:1:4:1:5:5:1:2:2:2:1:1:1:1:1:1:11:1:10:1:1:1:1:1:1:2:6:4:1:1:1:3:2:3:2:5:3:2:2:1:10:1:1:1:1:1:1:1:1:10:1:1:1:1:1:1:1:1:9:6:11:1:9:9:9:9:9:6:2:1:9:8:1:6:3:5:1:2:1:3:2:4:1:3:12:1:1:1:1:9:7:2:5:1:3:3:1:1:1:1:1:4:1:1:2:1:1:3:2:2:1:1:1:9:7:1:1:4:1:4:1:2:2:1:2:1:1:1:6:3:2:1:11:1:1:1:9:6:3:3:2:3:3:1:1:3:1:6:2:1:3:1:2:1:2:1:1:9:6:2:1:3:1:1:1:2:1:1:2:2:7:1:1:3:2:15:1:1:1:1:1:1:1:9:6:1:2:2:2:2:1:2:2:1:3:1:1:4:1:1:1:4:2:3:2:1:9:6:1:1:1:2:1:1:3:1:1:1:1:3:2:3:1:2:1:1:4:1:1:1:1:1:1:11:1:9:5:4:1:3:2:2:2:1:1:2:1:3:1:2:1:1:2:2:2:1:2:1:1:2:1:9:5:3:1:1:2:1:1:2:1:2:1:1:2:3:1:2:2:2:1:1:1:1:3:2:11:1:9:5:2:2:1:1:1:3:2:1:1:1:2:1:1:3:4:2:1:1:2:1:1:1:1:1:2:1:1:9:5:1:7:2:3:1:1:1:5:1:2:4:2:1:2:3:1:11:1:9:5:1:2:3:3:4:4:2:2:1:4:1:2:2:1:2:1:3:1:1:1:1:9:5:1:1:3:8:1:3:2:2:2:1:6:3:1:13:1:1:1:1:1:9:5:1:1:1:2:2:1:1:1:2:1:2:1:1:1:1:1:2:2:2:1:3:1:4:1:2:1:1:1:9:4:6:1:1:1:1:1:1:1:2:1:1:2:1:1:1:1:9:4:1:1:1:11:1:9:4:4:1:2:4:1:8:1:3:1:2:3:1:1:2:2:1:2:1:1:1:9:4:3:2:1:3:4:2:1:1:4:3:1:1:1:2:1:2:1:1:3:1:11:1:9:4:3:1:1:1:1:1:5:1:1:2:4:1:2:3:1:2:4:2:1:2:1:1:1:9:4:2:2:4:1:2:1:1:3:1:2:4:1:1:1:1:1:1:1:2:1:1:1:13:1:1:1:1:1:9:4:2:1:3:3:4:1:2:2:2:5:1:1:1:1:1:5:2:2:1:1:9:4:2:1:1:2:1:1:1:4:2:5:1:3:4:1:2:1:1:1:1:3:11:1:9:4:1:4:2:2:2:1:5:6:1:1:3:7:1:1:1:1:2:1:9:4:1:3:1:1:1:9:4:1:3:1:1:1:8:13:1:1:1:1:1:9:4:1:2:1:4:2:1:1:1:1:4:1:3:1:1:1:1:1:1:3:2:2:1:3:1:1:1:1:9:4:1:1:4:2:3:4:2:1:1:1:1:1:6:1:1:3:1:1:2:1:11:1:9:4:1:1:2:1:4:1:1:1:2:2:2:1:3:1:1:3:2:1:1:3:2:2:2:1:9:4:1:1:1:2:1:3:1:1:1:2:5:1:1:1:4:1:1:1:1:1:1:1:1:3:11:1:1:9:3:9:3:9:3:9:3:3:2:1:9:3:5:2:2:2:1:1:1:3:1:9:3:5:1:2:1:11:1:9:3:4:2:2:9:3:4:2:2:8:4:1:1:1:1:9:3:4:1:1:1:1:2:2:1:2:1:1:1:3:2:1:1:4:2:1:1:2:2:2:11:1:9:3:3:2:3:1:1:3:1:1:2:2:1:1:1:1:1:2:3:4:1:4:2:1:1:9:3:3:1:2:4:2:1:3:1:2:1:1:1:1:3:1:2:2:1:5:12:1:1:1:9:3:3:1:1:1:1:2:1:1:1:1:1:2:4:1:1:4:2:5:1:1:2:2:2:1:9:3:2:3:3:1:2:1:1:3:3:2:1:2:1:1:2:1:1:7:1:1:11:1:9:3:2:2:2:3:4:1:1:3:3:3:2:1:1:1:3:1:1:1:2:2:2:1:9:3:2:2:1:1:1:1:2:1:1:1:1:3:1:4:1:1:3:2:1:2:3:3:1:11:1:9:3:2:1:2:2:2:5:2:9:3:2:1:2:2:2:1:1:2:1:9:3:2:1:1:2:2:1:2:3:1:1:2:2:5:1:1:1:1:1:1:1:2:1:1:3:11:1:9:3:1:8:1:1:3:6:2:2:2:1:4:6:2:2:1:9:3:1:4:1:5:1:1:2:1:1:2:2:1:4:1:1:2:2:7:11:1:9:3:1:3:2:1:1:1:2:2:1:1:3:1:1:2:1:2:2:1:2:2:4:1:2:2:1:9:3:1:2:4:1:3:1:1:1:3:1:2:2:1:3:1:1:8:2:1:11:1:9:3:1:2:2:1:1:2:1:1:3:1:1:3:2:1:1:1:1:1:3:1:1:1:1:3:1:1:1:2:1:9:3:1:2:1:1:2:2:2:2:1:2:1:1:1:2:4:1:1:3:3:2:2:1:1:11:1:9:3:1:1:4:1:2:3:4:2:1:2:3:1:2:1:3:1:4:4:1:1:1:1:9:3:1:1:2:3:2:3:3:5:1:1:5:1:1:1:2:2:1:14:1:1:1:1:1:9:3:1:1:2:1:1:1:2:2:1:6:1:2:1:3:3:4:2:1:1:1:1:1:2:1:9:3:1:1:1:2:2:2:1:1:2:1:3:1:3:4:1:2:2:2:2:3:12:1:1:1:9:3:1:1:1:1:2:1:1:2:4:1:1:1:2:5:2:2:3:2:1:2:1:1:1:2:1:9:2:8:1:1:1:6:2:1:2:4:5:1:1:3:1:1:1:1:11:1:9:2:5:6:2:1:2:2:3:3:1:3:2:1:1:1:3:1:1:3:1:1:1:9:2:5:1:1:2:4:3:1:1:3:1:5:2:2:2:1:1:1:1:1:2:11:1:9:2:4:2:3:1:1:1:2:1:3:1:1:1:4:2:1:4:1:1:1:4:2:1:1:9:2:4:1:2:1:2:1:1:3:1:1:4:1:2:4:2:1:3:2:1:1:12:1:1:1:9:2:3:4:9:2:3:4:9:2:3:2:1:1:9:2:3:2:4:3:1:1:1:1:1:1:1:2:2:1:2:2:1:1:2:1:5:12:1:1:1:9:2:3:2:1:1:1:2:3:1:1:1:1:2:1:2:1:1:2:1:2:3:1:2:3:1:2:1:1:9:2:3:1:2:1:5:5:1:1:2:1:1:1:1:2:1:2:1:1:5:1:1:1:11:1:9:2:3:1:1:1:3:1:1:4:1:1:1:2:1:4:2:1:2:1:1:2:2:1:2:2:1:1:9:2:2:5:1:1:1:3:4:2:1:1:5:4:4:1:1:2:1:1:11:1:9:2:2:3:2:2:2:1:1:1:1:1:1:1:2:2:2:1:2:2:2:7:3:1:1:1:9:2:2:2:4:1:1:3:3:2:1:1:1:3:1:2:2:1:1:2:1:1:3:12:1:1:1:9:2:2:2:1:4:1:1:3:2:1:1:1:2:1:1:2:3:1:1:1:1:4:2:1:2:1:1:9:2:2:1:9:2:2:1:9:2:2:1:7:12:1:1:1:9:2:2:1:3:1:1:2:2:1:5:1:1:1:1:4:3:4:1:1:1:2:3:1:1:1:9:2:2:1:2:1:1:2:2:2:1:1:1:3:1:4:1:3:4:1:1:1:1:1:1:1:1:11:1:9:2:2:1:1:2:1:5:1:3:1:1:1:1:5:1:1:1:1:2:1:3:1:1:1:3:1:1:1:9:2:2:1:1:1:1:2:1:3:1:3:5:1:2:5:2:3:3:1:1:1:11:1:9:2:1:5:2:3:2:1:3:1:2:1:2:6:1:4:1:1:3:2:1:1:9:2:1:4:1:3:1:2:2:2:1:2:2:1:1:4:4:2:2:1:2:12:1:1:1:9:2:1:3:2:3:3:3:1:1:1:1:5:2:3:1:1:2:1:2:2:1:2:1:1:9:2:1:3:1:1:4:1:3:1:1:1:1:3:1:2:1:9:2:1:1:12:1:1:1:9:2:1:2:3:4:2:4:1:1:3:1:1:2:2:4:4:1:2:1:1:2:1:9:2:1:2:2:1:4:2:3:3:5:2:2:2:4:1:3:1:1:11:1:1:9:2:1:2:1:2:4:1:2:3:1:4:3:1:2:7:1:1:2:1:1:2:1:9:2:1:2:1:1:1:3:3:1:9:2:1:2:1:1:1:3:3:1:1:1:1:11:1:9:2:1:1:4:3:1:2:1:1:1:5:1:2:1:2:1:3:1:1:3:4:1:2:1:9:2:1:1:3:1:2:3:1:1:1:5:1:4:2:2:5:5:1:11:1:1:9:2:1:1:2:2:2:1:2:1:3:2:2:1:5:9:2:1:1:1:2:1:1:9:2:1:1:2:1:1:1:3:5:7:4:1:3:1:2:2:1:2:1:11:1:1:9:2:1:1:1:3:1:2:1:1:2:2:2:3:1:2:2:1:1:2:1:2:1:3:3:1:2:1:9:2:1:1:1:2:1:1:1:1:2:2:1:3:1:1:3:3:4:2:1:1:5:1:1:11:1:9:2:1:1:1:1:1:4:4:3:2:2:2:4:2:2:1:2:3:1:1:3:1:1:1:9:1:9:1:9:1:9:1:10:1:11:1:9:1:6:2:2:3:3:2:9:1:6:2:1:4:1:1:1:1:1:9:1:5:3:1:1:1:9:1:5:3:1:1:2:1:1:1:1:2:1:1:11:1:9:1:5:1:1:2:5:3:1:1:1:2:1:1:1:3:1:7:2:2:2:1:1:9:1:4:3:1:3:1:1:7:1:1:1:2:1:9:1:3:1:1:11:1:9:1:4:2:1:1:1:1:1:1:1:3:1:1:2:9:1:4:1:4:2:1:1:1:9:1:4:1:1:3:1:3:2:3:3:1:1:1:2:2:1:1:1:4:3:1:1:1:11:1:9:1:3:5:1:2:2:3:1:1:1:4:3:9:1:2:2:2:1:1:9:1:3:3:1:2:3:1:4:1:3:1:1:1:1:1:1:1:3:1:2:1:2:13:1:1:1:1:1:9:1:3:2:2:1:1:3:2:1:1:2:1:5:2:1:1:1:1:4:1:2:2:1:2:1:1:9:1:3:1:5:2:3:2:1:1:1:5:1:2:1:4:1:1:2:1:2:1:11:1:1:9:1:3:1:2:2:1:1:4:3:3:1:1:2:2:3:2:2:2:3:3:1:1:1:1:9:1:3:1:1:2:2:1:1:1:3:1:1:1:1:4:2:1:1:2:1:2:2:1:2:1:2:11:1:1:9:1:2:9:1:2:9:1:2:9:1:2:1:2:1:1:1:9:1:2:4:2:1:5:2:2:2:5:1:1:1:1:4:2:3:1:1:11:1:1:9:1:2:3:2:2:1:1:2:6:6:1:1:2:1:2:4:3:1:1:2:1:9:1:2:2:6:2:1:1:3:1:3:2:4:3:1:5:1:1:1:1:1:11:1:9:1:2:2:2:2:1:1:1:4:1:1:2:3:2:1:1:1:1:5:1:1:1:1:1:1:1:2:1:1:9:1:2:2:1:2:1:2:1:1:1:1:2:5:1:1:1:1:1:1:1:3:2:3:2:2:1:11:1:9:1:2:1:5:3:2:3:2:1:3:1:4:1:1:2:1:3:3:1:1:1:2:1:9:1:2:1:3:1:2:1:1:2:3:5:3:2:2:1:2:1:6:2:11:1:1:9:1:2:1:2:1:3:2:1:1:1:4:2:1:1:4:1:1:2:1:1:1:1:1:6:1:1:1:1:1:1:1:9:1:2:1:1:3:2:2:4:1:1:1:4:2:2:1:4:1:7:1:1:11:1:9:1:2:1:1:1:4:3:1:1:1:1:1:2:2:1:3:2:1:4:5:2:1:1:2:1:9:1:2:1:1:1:1:1:2:2:1:6:1:1:1:1:2:1:1:2:5:1:2:2:12:1:1:1:1:9:1:1:5:3:1:2:2:1:2:2:3:1:1:3:3:2:1:1:2:2:3:1:1:1:1:9:1:1:4:2:1:4:3:2:3:1:2:4:5:1:2:2:2:11:1:1:1:9:1:1:3:4:2:1:3:3:1:3:2:1:2:2:1:1:1:1:2:1:1:3:2:1:1:1:9:1:1:3:2:1:1:1:1:3:1:2:1:2:1:3:2:1:9:1:1:2:1:1:11:1:9:1:1:3:1:1:1:3:3:1:2:1:1:3:2:1:1:4:4:3:1:1:3:1:1:1:1:9:1:1:2:3:3:1:1:3:2:1:3:2:4:1:1:1:1:1:1:1:4:2:1:1:11:1:9:1:1:2:2:2:1:1:3:2:2:1:3:9:1:1:2:2:1:3:1:2:1:1:9:1:1:2:1:3:3:2:1:1:1:3:4:4:1:4:1:1:1:2:3:1:1:11:1:9:1:1:2:1:1:3:2:9:1:1:2:1:1:3:2:8:1:1:2:1:1:9:1:1:1:9:1:1:1:9:1:1:1:9:1:1:1:12:1:1:1:1:9:1:1:1:4:1:1:2:2:3:4:2:1:1:1:3:1:1:2:1:1:6:1:2:1:1:9:1:1:1:3:1:1:2:6:1:2:2:2:3:2:1:3:2:1:1:2:1:2:11:1:9:1:1:1:2:2:2:1:1:2:9:1:1:1:2:2:2:1:1:2:5:1:1:2:1:9:1:1:1:2:1:1:2:3:1:1:4:1:1:1:1:1:1:1:5:4:2:1:2:1:1:11:1:1:9:1:1:1:1:3:3:2:1:1:1:1:2:1:3:4:5:4:1:4:1:2:1:9:1:1:1:1:2:1:4:1:4:5:2:2:1:3:2:6:1:1:1:1:11:1:1:9:1:1:1:1:1:2:5:4:1:1:3:2:2:2:8:6:2:1:9:1:1:1:1:1:1:1:9:1:1:1:1:1:1:1:9:1:1:1:1:1:1:1:1:11:1:1:1:8:8:8:8:8:8:3:1:1:1:1:8:7:1:5:3:4:1:2:1:2:2:4:1:1:1:1:2:2:1:11:1:1:1:8:6:2:4:1:3:2:1:1:1:1:1:1:2:1:1:1:1:2:2:1:1:3:1:1:1:2:1:1:8:6:1:1:3:1:3:3:1:6:2:3:3:19:1:1:1:1:1:1:1:1:1:1:1:8:5:3:2:2:3:1:1:1:1:1:3:1:1:4:1:1:1:1:3:4:1:1:2:1:1:8:5:2:1:2:1:1:1:1:1:3:1:2:1:1:2:1:5:3:2:1:2:2:11:1:1:8:5:1:2:1:2:1:3:6:2:2:4:4:1:1:1:2:2:3:1:1:1:1:8:5:1:1:1:1:1:1:2:1:1:1:8:5:1:1:1:1:1:1:2:1:1:2:1:11:1:8:4:3:4:1:2:2:1:4:1:1:3:1:1:2:2:1:1:2:2:5:1:1:1:1:1:1:8:4:2:3:5:2:1:1:1:1:5:1:1:6:2:2:2:13:1:1:1:1:1:8:4:2:1:2:1:1:7:1:2:4:3:2:5:3:1:3:2:1:8:4:1:3:2:4:2:3:1:1:8:4:1:3:2:1:12:1:1:1:1:8:4:1:2:1:1:2:1:2:3:2:3:8:4:1:2:1:1:1:2:2:1:8:4:1:1:1:4:2:1:1:2:1:1:1:3:1:1:2:1:3:1:1:1:1:1:1:1:3:1:11:1:8:3:6:2:1:2:4:1:1:5:1:4:1:3:1:1:1:1:3:2:1:1:8:3:4:1:2:4:2:2:1:3:1:1:1:4:3:1:2:2:1:2:12:1:1:1:8:3:3:2:1:1:1:4:4:3:1:1:5:8:3:1:2:1:1:8:3:2:4:4:2:1:8:3:2:4:4:1:14:1:1:1:1:1:1:8:3:2:2:1:2:8:3:2:2:1:2:9:1:1:1:1:1:1:2:1:8:3:2:1:1:8:3:2:1:1:8:3:2:1:1:3:12:1:1:1:8:3:1:5:1:2:4:1:3:8:3:1:5:1:2:3:1:1:1:8:3:1:3:1:1:2:2:1:1:3:4:2:1:2:1:2:2:2:1:1:1:2:1:1:1:11:1:8:3:1:2:1:2:3:1:4:1:2:1:1:1:1:3:1:1:1:2:1:1:1:5:1:2:1:1:8:3:1:1:3:1:3:4:1:1:5:2:1:6:1:4:4:11:1:1:8:3:1:1:1:3:3:1:3:2:2:1:2:3:4:1:1:1:2:1:1:2:1:1:1:2:1:8:3:1:1:1:1:1:1:2:1:8:3:1:1:1:1:1:1:2:1:7:13:1:1:1:1:8:2:5:2:1:2:2:1:3:1:4:3:2:1:1:2:2:2:4:2:2:1:8:2:4:1:6:1:3:2:1:1:1:4:1:1:1:1:1:1:2:2:1:1:1:3:11:1:8:2:3:3:2:2:3:1:1:3:1:1:1:4:1:1:2:1:3:2:1:1:3:2:1:8:2:3:1:3:2:2:2:1:1:4:5:1:1:3:3:2:1:1:3:1:11:1:8:2:2:8:2:2:8:2:2:8:2:1:3:2:1:8:2:2:3:1:1:3:5:3:2:3:4:1:1:1:2:6:1:11:1:1:8:2:2:2:1:1:4:1:1:1:2:1:2:3:1:1:1:4:2:5:4:2:1:1:8:2:2:1:2:2:1:8:2:2:1:2:2:1:8:2:1:1:12:1:1:1:8:2:2:1:1:1:3:1:1:2:2:1:2:1:1:2:1:1:4:1:2:8:1:1:2:1:8:2:1:4:4:1:1:1:1:1:1:2:3:1:1:2:1:1:8:2:1:1:1:11:1:1:8:2:1:3:1:5:1:1:1:2:1:4:1:2:2:3:2:4:3:1:2:2:1:8:2:1:2:2:5:3:1:1:1:3:1:2:4:2:2:3:1:1:4:11:1:1:8:2:1:2:1:1:5:1:1:1:1:2:1:1:2:1:2:2:1:2:5:1:2:3:1:2:1:8:2:1:1:3:3:5:2:5:1:1:1:1:1:1:2:4:1:2:1:1:1:1:11:1:1:8:2:1:1:2:1:3:1:1:1:1:2:1:1:3:1:1:3:5:1:2:3:2:1:1:2:1:1:8:2:1:1:1:2:2:1:2:3:4:6:4:1:1:6:1:1:1:1:11:1:1:8:2:1:1:1:1:1:1:2:8:2:1:1:1:1:1:1:2:8:1:4:1:2:1:8:1:6:1:1:1:3:3:2:1:1:2:1:1:1:2:2:2:1:3:2:1:2:1:1:11:1:8:1:4:5:1:2:1:1:3:2:1:1:1:1:5:2:2:2:1:1:1:3:1:2:1:8:1:4:1:2:3:1:8:1:4:1:2:3:1:7:2:11:1:1:8:1:3:3:6:1:1:1:1:2:1:2:3:3:1:1:2:5:1:1:1:1:2:1:1:8:1:3:2:1:1:5:1:1:3:3:1:1:1:1:8:1:3:1:3:1:11:1:8:1:3:1:1:5:2:1:2:4:2:3:1:1:2:1:1:3:3:2:1:2:1:1:1:8:1:2:5:2:1:2:1:2:3:1:1:1:1:1:1:3:2:1:1:1:2:3:1:2:11:1:1:8:1:2:3:1:2:1:2:8:1:2:3:1:2:1:2:7:1:2:1:1:1:8:1:2:2:1:4:2:2:1:1:8:1:2:2:1:4:2:2:1:1:1:11:1:1:8:1:2:1:3:8:1:2:1:3:8:1:2:1:3:3:1:1:2:1:8:1:2:1:2:1:1:1:1:2:2:1:2:1:5:1:1:1:2:1:1:2:1:2:3:2:11:1:1:1:8:1:2:1:1:1:2:1:1:2:3:2:2:1:1:4:3:1:3:2:6:1:2:1:1:8:1:1:5:1:1:1:1:1:1:4:1:1:1:8:1:1:4:17:1:1:1:1:1:1:1:1:1:8:1:1:3:2:3:3:2:1:3:1:6:2:2:2:1:2:1:6:1:1:1:1:1:1:1:8:1:1:2:5:1:3:2:1:2:2:1:4:1:1:1:2:1:2:1:2:1:1:2:1:11:1:1:8:1:1:2:2:1:1:2:1:1:1:2:1:2:1:3:2:2:3:2:1:1:5:3:2:1:8:1:1:2:1:1:1:3:2:1:1:2:2:1:2:1:1:4:1:1:1:1:1:1:4:2:1:11:1:1:1:8:1:1:1:3:2:2:1:1:3:1:1:2:2:1:3:3:8:1:1:1:1:2:1:1:1:8:1:1:1:2:1:3:1:8:1:1:1:2:1:3:1:8:1:1:1:1:1:1:11:1:8:1:1:1:1:2:4:4:1:3:2:1:3:1:1:1:3:3:2:3:1:3:1:1:1:1:8:1:1:1:1:1:1:8:1:1:1:1:1:1:8:1:1:1:1:1:2:1:1:1:1:1:1:11:1:1:7:7:7:7:7:7:6:1:2:1:7:6:1:4:3:3:1:2:1:1:2:3:2:1:1:1:4:1:2:1:2:11:1:7:5:2:3:1:2:7:5:2:3:1:2:7:1:1:1:2:1:7:5:1:1:2:1:3:1:1:1:2:2:2:1:2:2:1:1:1:1:3:1:2:2:2:11:1:1:7:4:3:1:2:2:1:1:3:7:4:3:1:2:2:1:1:2:1:2:1:1:1:7:4:1:7:4:1:7:4:1:7:4:1:1:11:1:7:4:1:1:3:1:1:1:6:1:2:2:2:1:2:1:2:4:2:4:1:2:1:7:3:5:1:1:1:1:1:2:1:2:1:1:1:7:3:4:7:11:1:7:3:3:1:1:3:1:2:2:5:2:1:1:4:7:3:1:1:2:1:1:7:3:2:1:3:3:1:1:1:1:1:2:1:2:3:1:2:1:7:3:1:1:1:11:1:7:3:1:3:4:1:1:1:1:4:1:2:3:2:4:2:2:1:4:1:1:2:1:7:3:1:1:7:3:1:1:7:3:1:1:7:2:3:10:1:1:7:3:1:1:1:1:4:4:2:1:3:2:1:1:1:3:1:1:3:3:2:1:1:1:2:1:7:2:4:4:1:2:1:1:1:1:5:1:3:2:2:1:4:2:1:1:3:2:1:7:2:3:2:2:1:1:1:1:1:2:2:1:2:2:7:2:3:2:1:2:1:10:1:1:7:2:2:4:1:1:2:6:1:1:1:1:1:2:2:2:1:1:4:1:5:2:1:1:7:2:2:1:4:1:1:2:1:2:3:4:1:5:7:2:1:2:2:1:1:7:2:1:7:2:1:7:2:1:7:2:1:6:2:1:10:1:7:2:1:2:7:2:1:2:7:2:1:2:7:2:1:1:1:2:1:1:7:2:1:1:7:2:1:1:7:2:1:1:7:2:1:1:3:2:1:1:1:7:2:1:1:1:4:2:3:3:1:3:7:2:1:1:1:4:2:1:1:10:1:1:7:1:7:1:7:1:7:1:7:1:6:3:1:1:1:1:7:1:4:2:2:1:3:2:1:1:3:5:1:1:1:2:1:2:2:1:1:1:1:1:1:2:1:1:7:1:3:2:3:4:2:1:5:1:1:1:1:1:3:1:2:2:1:2:1:1:1:1:1:10:1:7:1:3:1:1:1:2:1:3:4:1:1:2:3:2:2:4:6:3:1:2:1:7:1:2:2:3:1:4:1:1:2:1:1:1:2:1:7:1:2:2:3:1:1:1:2:1:1:7:1:2:1:2:3:1:1:2:7:1:2:1:2:3:1:1:2:6:1:1:10:1:1:7:1:1:7:1:1:7:1:1:7:1:1:7:1:1:2:1:2:1:1:7:1:1:3:1:2:1:5:2:2:1:2:4:3:7:1:1:2:2:1:2:1:7:1:1:2:1:2:2:1:1:3:1:7:1:1:2:1:2:2:1:1:3:1:3:1:10:1:1:7:1:1:1:2:3:4:3:1:4:1:2:2:2:1:1:1:1:1:5:1:2:2:1:1:1:7:1:1:1:1:1:7:1:1:1:1:1:7:1:1:1:1:1:6:7:1:1:1:1:1:1:1:1:6:6:6:6:6:6:10:11:1:1:1:1:6:5:1:3:3:2:1:1:4:2:3:1:1:1:1:2:1:1:1:3:1:2:1:1:2:1:1:6:4:2:2:1:2:1:4:1:1:1:3:2:1:1:1:1:2:1:1:2:2:3:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:3:2:1:10:1:6:3:2:3:1:1:1:3:6:3:2:3:1:1:1:3:5:3:2:1:6:3:1:2:2:3:5:1:1:3:1:1:1:1:2:1:3:2:2:1:1:4:1:2:1:6:2:6:2:6:2:6:2:6:2:5:12:1:1:1:1:6:2:3:1:1:3:2:1:1:1:6:2:3:1:1:3:2:1:1:1:6:1:1:1:1:1:1:1:6:2:2:1:1:3:3:1:4:6:2:2:1:1:3:3:4:3:1:1:1:1:6:2:1:2:1:6:2:1:2:1:6:2:1:2:1:6:2:1:2:1:10:1:6:2:1:1:1:1:2:6:2:1:1:1:1:2:6:2:1:1:1:1:2:4:1:2:1:1:6:1:4:1:1:1:1:3:1:1:6:1:4:1:1:1:1:3:1:1:5:2:1:2:1:6:1:2:3:6:1:2:3:6:1:2:3:6:1:1:12:1:1:1:1:1:6:1:2:1:1:2:2:1:3:1:1:1:1:3:4:2:3:1:6:1:3:1:2:1:1:6:1:1:2:2:6:1:1:2:2:6:1:1:2:2:6:1:1:1:2:2:1:1:6:1:1:1:1:6:1:1:1:1:6:1:1:1:1:6:1:1:1:1:5:1:10:1:1:1:5:5:5:5:5:5:5:5:5:1:1:2:1:1:5:4:1:2:3:1:1:1:2:1:1:3:2:1:5:4:1:2:2:3:2:2:1:5:3:2:1:1:1:2:1:2:5:3:2:1:1:1:2:1:2:5:3:1:1:1:10:1:5:2:4:1:1:1:2:2:1:1:5:2:4:1:1:1:2:2:1:1:4:2:1:2:1:5:2:1:3:5:2:1:3:5:2:1:3:5:2:1:3:1:1:2:1:1:5:1:5:1:5:1:5:1:5:1:5:1:5:1:3:1:1:10:1:5:1:2:2:1:1:1:3:2:5:1:2:2:1:1:1:3:2:5:1:1:3:1:2:1:5:1:1:1:5:1:1:1:5:1:1:1:5:1:1:1:5:1:1:1:4:1:1:2:1:1:4:4:4:4:4:4:4:4:4:4:4:1:1:1:10:1:4:3:1:1:2:1:4:3:1:1:2:1:4:3:1:1:2:1:4:2:2:3:1:1:1:4:2:1:1:2:4:2:1:1:2:4:2:1:1:2:4:2:1:1:2:3:2:2:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:1:2:10:1:1:3:3:3:3:3:3:3:3:3:3:3:3:3:3:1:1:2:1:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:2:1:1:2:1:1:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:1:2:1:10:1:1:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:5:5:2:2:6:2:6:2:2:3:3:4:2:2:2:4:2:4:2:2:2:3:2:2:3:6:2:2:3:2:3:2:3:2:4:3:5:7:5:3:5:2:2:3:2:4:2:3:2:6:3:3:3:2:2:2:2:2:2:3:3:2:5:3:2", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":86,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":859,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.788425664621422e-83,0,0,-1.4902745963168196e-98,0,0]]] +Shrunk 27238 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 366 tests +{ seed: 12345, path: "365:0:1:12:1:1:1:1:3:4:2:4:2:1:2:1:2:3:1:1:4:2:1:1:2:1:1:12:1:12:7:1:5:3:4:1:2:1:2:2:4:1:1:1:1:2:1:1:2:1:1:1:12:7:1:3:2:3:1:1:1:4:2:1:1:3:2:5:1:2:1:1:2:1:12:7:1:2:3:2:2:2:1:3:2:1:4:2:7:13:1:1:1:1:12:7:1:2:1:2:2:1:1:4:2:2:1:1:1:2:3:1:1:1:1:2:1:1:2:1:1:12:7:1:1:4:1:4:1:2:2:1:2:1:1:1:6:3:1:3:1:1:1:1:12:7:1:1:2:2:1:2:2:2:2:4:2:4:1:2:2:3:12:1:1:12:7:1:1:1:3:1:1:3:4:2:1:5:3:2:1:3:1:1:2:1:1:12:7:1:1:1:1:2:1:1:1:3:1:1:1:1:1:4:2:4:1:1:5:2:1:12:6:6:6:6:6:6:3:13:1:1:1:1:12:6:5:1:4:2:3:2:1:2:2:2:1:2:1:4:1:1:2:1:1:1:12:6:4:2:3:3:2:1:1:1:6:1:1:1:1:1:5:1:1:1:2:1:12:6:4:1:1:3:1:2:1:1:1:3:3:1:3:4:1:1:1:2:1:12:1:1:12:6:3:3:2:3:3:1:1:3:1:6:2:1:2:5:1:1:1:1:1:1:12:6:3:2:1:2:2:1:1:3:4:1:8:1:1:2:2:1:2:1:1:12:6:3:1:2:2:1:2:1:1:1:1:1:2:3:1:3:2:2:1:3:1:12:1:1:1:12:6:3:1:1:1:1:6:1:1:2:5:1:2:1:2:4:2:1:2:1:12:6:2:4:1:3:2:1:1:1:1:1:1:2:1:1:1:1:2:2:1:1:2:2:1:2:1:1:12:6:2:3:1:1:2:2:1:3:1:2:2:2:1:1:4:1:1:1:2:1:1:1:12:1:12:6:2:2:2:1:1:5:4:1:1:2:1:1:2:1:1:3:1:2:2:2:1:12:6:2:2:1:1:1:1:2:1:6:2:3:1:1:1:5:3:1:1:2:1:1:12:6:2:1:3:1:1:1:2:1:1:2:2:7:1:1:3:2:4:12:1:12:6:2:1:1:8:2:1:3:1:1:1:1:1:2:4:6:2:1:12:6:2:1:1:1:5:1:1:2:3:3:1:1:1:2:3:7:2:1:12:6:1:8:1:2:2:3:1:1:4:2:2:1:2:5:12:1:12:6:1:4:3:3:1:2:1:1:2:3:2:1:1:1:4:1:1:4:1:1:1:1:1:12:6:1:3:4:1:3:1:2:1:1:1:2:1:2:2:3:1:2:2:2:2:1:12:6:1:3:1:2:8:2:3:10:3:13:1:1:1:1:12:6:1:2:4:3:1:2:2:1:2:1:2:1:3:1:2:2:2:1:1:1:1:2:1:12:6:1:2:2:2:2:1:2:2:1:3:1:1:4:1:1:1:3:3:1:2:1:1:1:12:6:1:2:1:3:1:2:5:1:1:2:11:4:1:12:1:12:6:1:2:1:1:2:1:1:2:1:3:2:1:1:1:1:8:2:2:1:1:2:1:1:12:6:1:1:4:5:3:4:1:1:1:2:2:1:3:1:3:1:2:1:1:1:12:6:1:1:3:1:3:3:1:6:2:3:4:1:1:1:1:1:1:1:1:12:1:12:6:1:1:2:2:2:5:1:1:8:1:1:1:2:1:2:1:1:1:1:2:1:1:12:6:1:1:2:1:1:2:1:5:1:6:2:1:1:1:2:2:1:4:2:1:12:6:1:1:1:3:1:11:6:1:1:1:3:1:3:1:12:1:12:6:1:1:1:2:1:1:3:1:1:1:1:3:2:3:1:2:1:1:4:1:1:1:1:1:2:1:12:6:1:1:1:1:2:1:2:1:1:1:1:2:1:3:1:1:2:1:1:2:3:1:1:2:1:2:1:1:12:6:1:1:1:1:1:1:1:1:2:1:1:1:1:10:5:6:12:1:12:5:7:1:1:1:1:1:1:1:1:1:2:1:1:1:2:1:1:1:1:1:8:2:1:12:5:5:5:5:5:5:5:4:1:2:1:1:1:12:5:5:1:3:5:2:1:2:1:1:1:1:2:2:2:1:2:1:3:12:1:12:5:4:5:1:3:2:2:5:1:2:1:2:1:4:1:3:1:1:1:1:12:5:4:2:2:4:6:1:2:3:2:1:1:3:1:2:2:2:1:12:5:4:1:3:2:2:2:1:1:2:1:3:1:2:1:1:2:2:2:1:1:1:1:12:1:12:5:4:1:1:2:1:3:1:1:1:1:2:1:1:1:1:1:1:1:2:2:1:2:2:2:2:1:12:5:3:5:1:1:1:2:4:2:1:2:5:2:4:3:2:1:12:5:3:3:1:4:1:3:1:6:3:1:3:2:2:1:1:1:12:1:1:12:5:3:2:2:3:1:1:1:1:1:3:1:1:3:8:4:1:2:1:12:5:3:2:1:1:2:1:3:2:2:1:1:1:1:1:5:1:1:1:2:1:1:1:1:2:1:1:12:5:3:1:3:1:4:1:4:2:4:1:1:1:1:2:1:4:1:1:1:12:1:12:5:3:1:2:1:1:2:1:3:1:1:2:1:2:3:2:1:1:1:1:2:2:3:1:1:1:1:12:5:3:1:1:2:1:1:2:1:2:1:1:2:3:1:2:2:2:1:1:1:1:5:1:1:1:1:1:1:12:5:2:11:5:2:12:2:2:12:1:12:5:2:4:4:2:1:2:3:1:1:4:1:1:2:2:1:4:2:1:1:12:5:2:3:4:2:2:1:2:1:2:1:3:1:2:3:2:2:2:1:2:1:12:5:2:3:1:2:7:5:2:3:1:2:6:1:12:1:1:1:12:5:2:2:4:2:2:2:1:1:1:3:2:1:2:1:2:2:1:1:2:1:1:1:2:1:12:5:2:2:2:2:1:2:2:1:1:4:1:1:3:1:1:2:5:1:2:2:1:12:5:2:2:1:2:7:2:1:1:1:1:1:1:1:4:2:1:2:2:2:12:1:12:5:2:2:1:1:1:3:2:1:1:1:2:1:1:3:4:2:1:1:1:6:1:1:1:1:1:1:1:12:5:2:1:4:2:2:1:1:4:1:1:2:1:3:1:2:3:2:2:1:2:1:12:5:2:1:3:1:1:4:2:6:1:10:3:13:1:1:1:1:12:5:2:1:2:2:1:2:1:1:6:1:3:1:3:2:1:3:5:1:1:1:1:1:1:12:5:2:1:2:1:1:1:1:1:3:1:2:1:1:2:1:5:3:2:2:4:1:1:1:1:1:12:5:2:1:1:2:5:2:1:4:1:2:1:1:3:3:2:2:1:1:12:1:1:1:12:5:2:1:1:1:4:4:1:5:2:4:1:3:2:1:4:2:1:12:5:2:1:1:1:1:3:1:1:2:1:2:5:6:1:1:1:2:1:1:1:1:1:2:1:12:5:1:7:2:3:1:1:1:5:1:2:4:2:1:2:1:1:12:1:1:1:12:5:1:5:2:1:2:1:3:4:1:2:1:1:1:1:3:4:2:2:1:1:12:5:1:4:2:5:2:1:2:4:4:1:4:1:1:2:2:2:1:12:5:1:4:1:1:3:1:1:1:1:11:5:1:3:1:1:1:12:1:12:5:1:3:3:2:1:1:4:2:3:1:1:1:1:2:1:1:1:3:1:1:1:1:2:1:1:12:5:1:3:2:1:1:2:3:2:3:1:3:1:5:2:1:6:1:1:1:1:1:1:1:12:5:1:3:1:2:1:1:2:1:3:1:2:2:2:3:1:3:1:2:2:13:1:1:1:1:12:5:1:2:9:1:1:2:2:1:2:1:6:1:3:1:1:3:1:1:1:1:12:5:1:2:3:3:4:4:2:2:1:4:1:2:2:1:3:1:2:1:12:5:1:2:2:4:1:1:1:2:2:4:2:1:4:1:1:4:1:1:1:12:1:1:12:5:1:2:2:1:2:2:1:4:2:2:2:3:1:1:1:1:2:1:1:2:2:2:1:12:5:1:2:1:4:1:1:2:4:2:1:2:7:1:1:3:2:2:1:1:12:5:1:2:1:2:1:3:6:2:2:4:4:1:1:1:2:2:1:12:1:12:5:1:2:1:1:2:2:2:1:3:3:1:3:2:2:1:1:3:1:1:1:2:2:1:12:5:1:2:1:1:1:1:1:3:3:3:2:3:2:1:3:3:6:1:1:1:1:1:1:1:12:5:1:1:5:1:1:3:1:7:4:1:1:1:2:1:1:3:13:1:1:1:1:12:5:1:1:3:8:1:3:2:2:2:1:6:3:1:3:1:1:1:1:12:5:1:1:3:1:3:6:3:1:1:4:2:4:1:1:2:1:1:2:1:1:12:5:1:1:2:5:1:1:1:1:3:2:1:3:2:3:2:1:1:1:2:1:1:1:12:1:12:5:1:1:2:2:2:2:1:1:4:1:5:1:1:3:1:2:5:1:2:1:12:5:1:1:2:1:3:1:1:1:2:2:2:1:2:2:1:1:1:1:3:1:2:2:1:1:1:2:1:12:5:1:1:2:1:1:1:3:1:3:2:2:2:2:1:1:2:1:1:3:1:2:1:1:11:1:1:11:5:1:1:1:4:2:1:1:5:6:1:2:3:1:1:1:3:2:1:2:1:12:5:1:1:1:2:2:1:1:1:2:1:2:1:1:1:1:1:2:2:2:1:3:1:3:1:1:1:2:1:12:5:1:1:1:1:6:2:1:2:1:3:1:2:2:1:1:1:1:2:2:2:1:2:1:1:1:12:5:1:1:1:1:2:3:1:2:1:2:1:1:2:3:1:1:1:1:1:2:1:2:14:1:1:1:1:1:1:11:5:1:1:1:1:1:3:2:2:1:1:2:2:3:2:1:1:1:4:1:2:1:2:1:2:1:12:4:8:3:1:3:5:2:2:1:5:2:1:2:3:1:1:1:1:12:4:7:1:2:1:1:2:3:2:1:3:1:1:2:1:2:4:1:1:1:1:2:1:12:4:6:2:1:2:1:2:1:1:1:1:1:2:2:1:1:1:3:3:2:1:12:1:1:1:1:11:4:6:1:1:1:1:1:1:1:2:1:1:2:1:1:1:1:9:4:3:1:1:1:1:12:4:5:1:3:5:2:3:1:4:1:1:2:5:2:3:1:1:1:1:12:4:5:1:1:2:1:3:2:2:1:2:3:1:1:1:1:4:1:1:1:4:1:1:1:1:1:12:4:4:4:4:4:4:4:4:4:15:1:1:1:1:1:1:1:11:4:4:3:1:2:2:2:1:1:1:1:1:2:3:1:3:2:1:4:1:3:1:1:1:1:12:4:4:2:1:1:1:1:1:3:1:1:2:1:2:1:2:1:2:4:1:2:2:1:2:1:12:4:4:1:2:4:1:8:1:3:1:2:3:1:1:2:1:1:2:1:1:1:12:4:4:1:1:4:1:1:1:3:2:1:5:1:1:1:1:1:1:2:3:2:11:1:11:4:4:1:1:1:2:1:5:1:2:4:1:2:1:1:4:1:2:1:1:1:2:1:1:1:12:4:3:4:1:2:2:1:4:1:1:3:1:1:2:2:1:1:2:2:4:1:1:1:1:1:12:4:3:3:2:1:2:3:3:2:4:1:2:1:2:1:1:2:2:1:2:1:1:1:12:4:3:3:1:1:1:1:1:1:5:1:1:1:1:1:2:5:1:2:3:1:1:11:1:1:11:4:3:2:2:4:1:1:1:2:1:1:1:1:2:4:1:1:2:1:1:1:2:1:3:1:1:1:1:12:4:3:2:1:1:2:1:2:1:3:1:2:1:1:1:2:1:2:2:1:1:1:3:1:2:1:1:1:12:4:3:1:4:3:2:4:4:1:1:1:1:1:1:1:1:2:1:1:2:1:1:1:2:1:12:4:3:1:3:1:2:1:1:1:11:4:3:1:2:2:1:11:1:11:4:3:1:2:2:1:1:3:7:4:3:1:2:2:1:1:2:1:1:2:1:1:12:4:3:1:1:2:3:1:2:2:6:1:1:1:1:3:1:1:2:5:2:1:12:4:3:1:1:1:3:2:1:1:2:1:1:1:1:1:1:1:1:3:1:4:3:1:1:1:1:2:1:12:4:3:1:1:1:1:1:5:1:1:2:4:1:2:3:1:2:4:1:1:11:1:1:1:11:4:2:6:2:1:5:1:2:1:1:1:4:2:3:1:1:2:4:1:1:1:1:1:12:4:2:4:1:6:3:1:1:1:1:1:1:4:1:1:1:1:2:1:2:2:2:1:12:4:2:3:5:2:1:1:1:1:5:1:1:6:2:2:1:1:2:2:1:12:4:2:3:2:2:2:1:2:3:1:7:1:1:2:3:1:3:1:11:1:11:4:2:3:1:2:5:2:1:4:2:2:2:2:1:2:1:3:2:1:2:1:12:4:2:2:4:1:2:1:1:3:1:2:4:1:1:1:1:1:1:1:2:1:1:1:1:1:1:2:1:12:4:2:2:2:6:4:1:1:2:1:1:1:2:2:1:3:2:4:2:1:12:4:2:2:2:1:3:1:2:2:2:2:1:1:1:10:2:13:1:1:1:1:1:11:4:2:2:1:4:1:4:5:1:1:1:5:2:2:1:1:1:1:4:1:1:1:1:1:12:4:2:2:1:1:2:2:1:1:1:3:3:3:1:5:1:1:1:1:4:1:2:1:12:4:2:2:1:1:1:1:1:1:2:2:3:3:6:1:2:1:3:2:1:1:1:2:1:12:4:2:1:4:4:1:5:3:1:1:1:2:2:8:2:11:1:11:4:2:1:3:3:4:1:2:2:2:5:1:1:1:1:1:4:5:1:1:1:1:1:1:12:4:2:1:2:3:2:3:4:2:4:1:3:1:1:2:2:2:1:2:1:1:1:12:4:2:1:2:2:1:1:2:3:1:1:2:5:1:1:2:3:6:2:1:1:12:4:2:1:2:1:1:7:1:2:4:3:2:5:2:3:11:1:1:11:4:2:1:1:6:1:3:1:3:1:1:2:4:2:1:3:1:1:1:1:1:1:1:2:1:12:4:2:1:1:2:2:3:2:6:1:1:1:9:2:1:2:2:1:1:12:4:2:1:1:2:1:1:1:4:2:5:1:3:4:1:2:1:1:1:1:1:2:1:1:1:12:4:2:1:1:1:3:1:1:1:2:1:3:3:1:1:3:2:1:1:1:2:1:3:11:1:1:11:4:2:1:1:1:1:4:3:1:1:1:1:1:4:1:1:3:2:1:7:1:2:1:12:4:1:7:4:1:7:4:1:7:3:1:1:2:1:12:4:1:6:1:2:1:1:1:1:2:3:1:4:1:1:1:1:4:4:2:1:1:12:4:1:5:2:1:1:1:4:2:1:1:1:1:2:3:9:2:11:1:11:4:1:4:5:3:3:2:1:2:2:2:2:1:3:1:1:5:2:1:12:4:1:4:1:2:4:1:5:4:2:4:1:2:1:1:2:2:2:1:12:4:1:4:1:1:1:1:8:2:2:1:1:1:1:3:1:2:1:3:1:2:1:1:12:4:1:3:4:1:1:1:2:3:1:1:5:1:2:2:2:5:2:11:1:11:4:1:3:2:4:2:3:1:1:8:4:1:3:2:1:2:2:1:12:4:1:3:1:3:3:2:4:1:1:1:1:4:3:3:1:1:3:1:2:1:12:4:1:3:1:2:1:1:3:2:3:2:1:3:1:1:1:1:2:1:2:1:1:1:1:1:1:2:1:12:4:1:3:1:1:1:9:4:1:3:1:1:1:8:2:11:1:11:4:1:2:7:1:2:1:1:4:2:2:2:2:1:5:3:1:1:2:1:12:4:1:2:3:2:2:3:2:3:3:1:1:7:1:1:6:1:1:1:1:1:1:1:12:4:1:2:3:1:1:1:2:1:1:3:2:1:5:4:1:3:1:2:1:1:1:2:1:12:4:1:2:2:2:4:2:3:3:8:3:1:1:1:1:1:12:1:1:1:1:11:4:1:2:2:1:3:3:1:3:1:4:2:1:2:1:1:2:2:1:1:1:2:1:2:1:12:4:1:2:1:4:2:1:1:1:1:4:1:3:1:1:1:1:1:1:3:2:1:1:2:2:1:12:4:1:2:1:3:1:1:1:1:2:1:1:2:1:2:1:3:1:2:1:5:1:4:1:1:1:1:1:12:4:1:2:1:2:1:3:1:5:1:1:1:1:1:4:1:3:1:2:1:1:3:11:1:11:4:1:2:1:1:4:1:2:2:2:1:1:4:2:1:3:3:2:2:1:2:2:1:12:4:1:2:1:1:1:2:1:2:1:1:1:1:1:2:1:2:3:3:1:3:1:3:1:1:2:1:12:4:1:1:9:1:2:2:2:7:3:1:1:1:1:1:1:1:1:2:1:1:1:12:4:1:1:5:2:2:2:2:1:1:1:4:1:2:1:2:2:2:1:14:1:1:1:1:1:1:11:4:1:1:4:2:3:4:2:1:1:1:1:1:6:1:1:3:2:1:1:2:1:1:1:12:4:1:1:3:2:5:3:1:2:1:1:2:2:6:2:2:2:1:2:1:12:4:1:1:3:1:3:4:1:3:2:1:1:3:3:1:1:4:1:4:1:1:1:1:1:12:4:1:1:3:1:1:1:6:1:2:2:2:1:2:1:2:4:3:1:2:11:1:11:4:1:1:2:4:1:7:2:2:3:2:4:1:3:2:1:3:1:1:1:1:12:4:1:1:2:2:1:3:1:1:1:4:2:2:1:1:1:5:5:1:1:1:1:2:1:12:4:1:1:2:1:4:1:1:1:2:2:2:1:3:1:1:3:2:1:1:3:1:1:2:1:1:1:12:4:1:1:2:1:2:1:2:1:1:3:2:1:2:5:1:1:1:1:1:4:2:11:1:1:11:4:1:1:2:1:1:2:1:1:1:1:5:1:1:2:1:3:2:1:1:1:3:1:4:2:1:12:4:1:1:1:4:2:1:1:2:1:1:1:3:1:1:2:1:3:1:1:1:1:1:1:1:1:3:1:1:1:1:12:4:1:1:1:3:2:2:1:2:1:1:1:2:1:2:1:1:2:3:2:1:1:1:1:2:2:1:1:12:4:1:1:1:3:1:1:1:1:1:1:6:2:2:2:5:1:2:1:1:3:11:1:11:4:1:1:1:2:2:3:1:1:4:1:1:1:2:1:1:2:1:1:2:1:3:2:4:1:1:1:1:1:12:4:1:1:1:2:1:1:1:2:1:1:2:1:1:3:1:1:1:1:5:3:2:2:3:1:1:1:12:4:1:1:1:1:4:1:1:1:1:1:2:2:1:1:1:3:1:1:1:1:1:4:2:1:1:1:2:1:1:12:4:1:1:1:1:2:4:1:1:1:1:1:1:2:2:1:1:1:3:1:3:1:1:1:4:11:1:11:4:1:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:3:1:3:1:1:1:1:12:4:1:1:1:1:1:1:6:5:1:2:1:1:4:3:1:1:1:1:1:1:3:1:1:1:1:12:4:1:1:1:1:1:1:1:2:3:1:7:1:1:6:1:1:2:1:1:2:2:1:1:12:3:9:3:9:3:9:3:1:11:1:1:11:3:8:1:1:2:2:4:2:1:1:1:1:3:4:1:2:2:1:2:2:1:12:3:6:4:2:1:1:1:5:1:1:2:1:3:2:1:3:1:2:2:1:1:12:3:6:2:1:2:4:1:1:5:1:4:1:3:1:1:1:1:2:2:1:1:12:3:6:1:2:1:1:2:3:2:3:2:2:3:1:1:2:3:1:2:11:1:11:3:5:6:2:1:1:1:2:1:2:2:2:1:1:1:1:4:1:1:1:2:1:2:1:12:3:5:2:2:2:1:1:1:3:1:9:3:4:5:1:1:1:1:1:1:12:3:5:1:9:1:1:1:1:1:2:2:2:6:1:4:1:2:1:12:3:5:1:2:2:3:2:1:1:5:1:2:1:1:2:1:1:3:4:11:1:11:3:5:1:1:2:3:3:1:2:1:1:2:2:2:2:5:1:2:2:1:2:1:12:3:4:4:3:3:4:1:2:4:1:2:2:1:3:3:2:1:1:12:3:4:3:3:1:2:1:2:2:1:2:1:1:3:3:7:2:2:1:12:3:4:3:1:1:2:1:2:1:2:1:2:4:5:1:1:5:1:1:11:1:11:3:4:2:2:9:3:4:2:2:8:4:1:1:1:1:12:3:4:2:1:1:1:3:2:2:3:2:2:2:1:1:3:1:1:2:4:2:1:12:3:4:1:4:1:1:11:3:4:1:4:1:1:1:1:1:2:1:12:3:4:1:2:4:2:2:1:3:1:1:1:4:3:1:2:2:1:1:12:1:1:1:1:11:3:4:1:2:1:2:1:1:2:1:2:1:2:3:3:2:3:3:4:2:1:1:12:3:4:1:1:1:6:1:1:1:4:1:3:1:1:3:3:5:2:1:1:12:3:4:1:1:1:1:2:2:1:2:1:1:1:3:2:1:1:4:2:1:1:2:1:1:1:1:2:1:12:3:3:6:2:2:4:1:2:1:1:5:1:3:1:1:1:1:1:1:1:11:1:1:11:3:3:5:1:1:1:1:2:1:3:3:2:1:1:1:1:2:2:3:2:2:1:2:1:12:3:3:3:3:3:3:3:3:3:3:3:3:2:2:2:1:1:12:3:3:3:2:1:1:1:6:1:2:2:1:2:1:1:1:1:2:4:2:2:1:1:12:3:3:3:1:1:3:2:1:1:1:1:1:4:3:7:2:2:12:1:1:1:11:3:3:2:5:1:1:1:1:1:1:4:4:1:1:1:4:1:4:1:1:2:1:1:12:3:3:2:2:1:4:1:1:1:2:1:3:1:3:1:2:1:2:1:1:5:2:1:12:3:3:2:1:4:1:2:1:1:1:3:1:1:2:2:1:4:3:2:1:1:2:1:1:12:3:3:2:1:2:1:1:11:3:3:2:1:2:1:1:3:11:1:1:11:3:3:2:1:1:1:4:4:3:1:1:5:8:2:3:2:1:12:3:3:1:4:1:2:1:2:3:2:1:1:3:5:5:1:2:2:1:1:12:3:3:1:3:1:6:1:1:5:2:1:4:4:2:1:1:2:2:1:12:3:3:1:2:4:2:1:3:1:2:1:1:1:1:3:1:2:2:1:4:1:11:1:1:11:3:3:1:2:2:1:2:2:1:3:1:2:3:1:3:1:1:1:1:1:3:1:2:2:1:1:12:3:3:1:1:5:2:2:5:5:1:1:1:1:1:1:1:1:1:1:5:1:1:1:1:1:1:12:3:3:1:1:3:1:2:2:5:2:1:1:4:8:1:2:1:2:1:12:3:3:1:1:2:2:1:1:1:1:1:3:1:1:1:1:4:2:3:2:5:1:11:1:11:3:3:1:1:1:4:2:4:1:1:1:1:2:1:1:2:2:2:1:1:2:1:1:1:1:1:2:1:1:12:3:3:1:1:1:1:2:1:1:1:1:1:2:4:1:1:4:2:5:1:1:1:2:2:1:12:3:2:8:2:3:1:2:5:2:1:1:2:1:2:2:1:2:1:2:1:12:3:2:6:1:2:2:3:1:1:3:1:3:6:2:1:2:1:11:1:1:11:3:2:5:1:9:2:1:1:3:3:1:1:1:6:4:1:1:1:1:12:3:2:4:2:1:2:2:1:1:1:1:2:1:1:1:3:1:4:1:2:2:1:1:1:2:1:12:3:2:4:1:1:6:1:1:1:1:2:3:1:1:1:1:2:1:1:2:2:3:2:1:12:3:2:3:5:1:7:3:1:1:2:1:1:3:1:1:2:14:1:1:1:1:1:1:11:3:2:3:3:1:2:1:1:3:3:2:1:2:1:1:2:1:2:1:1:1:1:2:1:1:2:1:12:3:2:3:1:4:1:2:1:6:1:5:1:2:4:2:1:2:2:1:12:3:2:3:1:2:1:1:4:1:4:3:4:2:2:1:1:1:1:1:3:2:1:12:3:2:3:1:1:1:3:6:3:2:3:1:1:1:3:5:1:1:11:1:11:3:2:2:6:1:1:1:2:1:2:1:2:2:3:1:4:1:2:3:3:1:1:1:12:3:2:2:3:1:3:1:3:3:4:4:1:1:1:2:2:1:1:1:1:1:1:2:1:12:3:2:2:2:3:4:1:1:3:3:3:2:1:1:1:3:1:1:1:1:1:2:1:1:1:12:3:2:2:2:2:1:1:1:2:2:2:2:1:2:11:3:1:1:11:1:11:3:2:2:2:1:1:2:4:1:1:1:5:5:1:1:2:1:1:3:1:3:1:1:1:12:3:2:2:1:2:8:3:2:2:1:2:8:3:1:1:2:1:12:3:2:2:1:2:1:2:1:7:2:1:4:1:1:3:5:4:1:1:1:1:12:3:2:2:1:1:3:2:1:1:1:1:2:1:6:1:2:2:1:1:2:2:13:1:1:1:1:11:3:2:2:1:1:1:5:1:1:2:1:3:4:5:1:2:2:2:4:1:1:1:1:12:3:2:1:6:1:2:1:1:4:1:3:5:1:1:2:1:3:1:3:1:1:1:12:3:2:1:4:4:1:1:1:4:1:2:5:1:3:3:1:1:2:2:1:1:12:3:2:1:4:1:1:3:1:1:3:1:2:3:1:1:1:2:2:1:1:1:4:1:11:1:11:3:2:1:3:3:1:1:1:1:1:2:1:2:3:1:2:1:7:2:5:1:1:1:1:1:12:3:2:1:3:1:1:1:1:7:6:1:1:2:2:1:4:2:1:2:1:1:12:3:2:1:2:3:3:4:2:4:1:1:6:2:2:1:1:1:2:2:1:12:3:2:1:2:2:2:5:2:9:3:2:1:2:2:1:1:11:1:1:11:3:2:1:2:2:1:1:1:1:3:1:1:1:1:2:1:2:1:2:1:1:1:1:2:1:1:1:5:1:1:1:1:1:1:12:3:2:1:2:1:1:2:2:2:1:1:2:1:3:2:1:4:2:1:2:1:1:1:1:1:1:2:1:12:3:2:1:1:8:3:2:1:1:8:3:2:1:1:2:3:1:1:1:12:3:2:1:1:4:2:1:1:2:1:1:2:2:1:1:1:3:3:4:1:1:3:11:1:11:3:2:1:1:3:2:1:2:1:1:4:4:2:1:2:2:1:1:1:3:3:2:1:1:12:3:2:1:1:2:2:1:2:3:1:1:2:2:5:1:1:1:1:1:1:1:2:1:1:1:2:1:1:12:3:2:1:1:2:1:1:5:1:2:2:1:1:2:2:2:4:1:1:3:1:1:2:1:1:12:3:2:1:1:1:5:1:1:2:9:1:1:4:1:2:3:1:1:1:11:1:11:3:2:1:1:1:3:1:1:2:1:2:2:1:1:1:1:1:11:2:5:1:1:1:1:1:12:3:2:1:1:1:1:3:2:2:2:4:2:1:5:2:1:4:3:1:2:1:12:3:2:1:1:1:1:1:5:3:3:1:3:4:5:1:1:2:1:2:2:1:12:3:2:1:1:1:1:1:1:2:1:1:1:2:1:6:1:1:2:4:2:1:2:2:11:1:1:11:3:1:8:1:1:3:6:2:2:2:1:4:6:3:1:1:1:12:3:1:6:1:1:2:1:2:1:1:2:1:1:3:1:1:2:2:3:3:1:2:2:1:12:3:1:5:2:3:5:1:1:4:5:1:3:1:1:1:2:3:1:1:1:12:3:1:5:1:2:4:1:3:8:3:1:5:1:2:11:1:1:10:3:1:5:1:1:1:1:1:1:2:1:1:1:2:2:3:6:1:1:2:3:2:2:1:1:11:3:1:4:1:2:1:2:2:4:5:1:2:1:1:1:5:1:1:1:2:1:2:1:11:3:1:3:5:6:1:1:6:1:4:6:2:4:1:1:1:1:11:3:1:3:3:1:2:1:8:1:2:3:2:2:2:2:4:2:12:1:11:3:1:3:2:1:4:1:1:1:1:2:3:1:1:2:2:3:4:1:1:1:1:1:1:2:1:11:3:1:3:1:3:4:1:1:1:2:2:1:1:1:1:3:1:1:2:1:2:5:1:12:1:11:3:1:3:1:1:11:3:1:3:1:1:10:4:1:1:1:1:11:3:1:3:1:1:1:2:1:1:3:5:1:5:2:1:2:4:2:1:2:12:1:11:3:1:2:5:1:1:3:3:1:5:3:1:1:3:5:1:4:1:1:1:1:11:3:1:2:3:3:2:1:5:3:2:1:1:1:1:1:5:1:1:2:1:2:12:1:11:3:1:2:3:1:1:1:3:1:3:2:1:2:2:1:2:6:1:1:1:1:2:1:2:1:11:3:1:2:2:2:2:5:11:3:1:2:2:2:1:3:12:1:11:3:1:2:2:1:2:2:1:1:6:2:1:3:1:5:4:2:2:1:2:1:11:3:1:2:1:6:2:1:1:1:3:1:1:2:1:1:1:3:1:3:4:1:2:12:1:11:3:1:2:1:3:1:2:3:1:1:3:2:1:1:4:1:1:2:2:5:1:2:1:1:11:3:1:2:1:2:1:3:11:3:1:2:1:2:1:3:3:13:1:1:1:11:3:1:2:1:1:3:3:1:3:1:1:8:1:1:1:1:1:3:1:2:4:1:1:1:1:11:3:1:2:1:1:1:5:2:4:1:5:1:1:1:1:2:2:2:2:1:14:1:1:1:1:11:3:1:2:1:1:1:1:1:4:1:1:1:1:1:1:2:2:5:4:1:1:1:1:2:2:2:1:11:3:1:1:5:5:1:4:2:1:3:1:2:3:1:1:2:3:3:12:1:11:3:1:1:4:2:4:3:1:2:1:4:1:4:3:2:6:1:1:1:1:1:1:1:11:3:1:1:3:8:1:1:1:1:1:1:3:2:4:1:3:2:1:4:12:1:11:3:1:1:3:2:1:5:1:1:4:1:1:2:1:4:5:2:1:3:2:1:11:3:1:1:3:1:1:6:1:1:3:1:3:1:2:2:3:2:3:1:3:12:1:11:3:1:1:2:4:4:6:1:2:3:1:5:1:1:2:1:1:1:2:2:1:11:3:1:1:2:2:6:2:4:4:2:2:1:2:3:1:1:2:1:2:12:1:11:3:1:1:2:2:1:1:4:1:3:1:1:1:1:1:1:11:2:3:1:2:1:11:3:1:1:2:1:2:4:5:1:4:1:1:1:7:3:2:1:13:1:1:1:11:3:1:1:2:1:1:2:3:2:2:1:11:3:1:1:2:1:1:2:2:1:1:11:3:1:1:1:8:1:2:1:3:2:4:3:2:2:2:1:3:13:1:1:1:11:3:1:1:1:4:1:2:5:1:1:6:1:1:1:3:4:1:1:1:1:1:2:1:1:11:3:1:1:1:3:1:3:2:3:2:3:1:3:1:1:6:3:1:1:1:12:1:1:11:3:1:1:1:2:3:2:1:4:1:1:3:2:3:1:2:2:3:4:3:1:1:1:11:3:1:1:1:2:1:4:1:1:2:1:3:1:1:2:1:2:1:4:1:4:2:1:1:12:1:11:3:1:1:1:2:1:1:1:1:4:2:1:2:6:2:2:1:2:1:1:1:4:1:2:1:11:3:1:1:1:1:3:2:2:5:1:1:1:1:2:1:3:2:1:1:2:6:12:1:1:11:3:1:1:1:1:2:2:1:2:1:1:2:2:3:2:1:1:1:3:3:1:1:2:1:2:1:2:1:11:3:1:1:1:1:1:4:1:2:2:2:3:3:1:2:1:3:1:5:2:2:12:1:11:3:1:1:1:1:1:2:1:1:1:1:1:4:1:2:3:2:1:1:2:2:3:2:2:1:2:1:1:11:3:1:1:1:1:1:1:1:1:4:8:2:1:3:1:1:1:1:2:1:1:1:2:2:12:1:11:2:8:4:1:6:1:1:1:2:2:3:1:2:2:1:2:1:2:2:1:11:2:7:2:2:2:3:2:1:2:2:11:2:2:1:1:12:1:11:2:6:5:1:1:2:1:1:3:3:3:5:1:2:1:2:1:2:2:1:11:2:6:2:1:1:3:1:1:2:2:1:2:1:1:7:1:1:2:15:1:1:1:1:1:1:11:2:6:1:1:2:1:4:2:1:2:1:3:3:2:1:1:2:1:1:3:2:2:1:11:2:5:4:1:1:1:2:1:1:1:1:1:1:1:3:1:2:1:2:3:1:3:1:1:12:1:1:11:2:5:2:2:4:1:1:5:1:3:1:5:2:2:1:1:3:2:1:1:11:2:5:1:5:3:2:3:1:1:3:1:3:3:2:4:2:1:12:1:11:2:5:1:2:1:2:1:1:1:2:2:2:1:2:2:1:3:1:1:2:1:1:2:3:2:1:11:2:5:1:1:1:2:2:2:1:3:1:1:1:1:1:4:2:1:1:2:5:13:1:1:1:11:2:4:5:1:1:1:5:2:3:3:2:3:1:2:4:1:2:2:1:11:2:4:3:2:4:2:1:1:1:3:1:1:1:3:2:1:2:1:5:2:12:1:11:2:4:2:5:3:1:1:1:1:1:1:1:1:1:2:2:1:1:2:2:1:1:1:1:3:2:1:11:2:4:2:2:1:1:4:2:3:1:2:4:4:1:1:1:1:2:1:14:1:1:1:1:11:2:4:2:1:1:2:1:4:1:2:1:1:5:1:3:1:1:4:2:1:1:1:2:1:11:2:4:1:3:7:2:2:1:2:2:2:3:3:2:1:1:1:14:1:1:1:1:11:2:4:1:2:2:2:4:1:7:2:2:1:2:1:4:3:3:1:1:1:11:2:4:1:1:4:4:4:2:1:2:5:3:4:1:2:1:1:12:1:11:2:4:1:1:2:1:1:1:3:1:2:2:1:1:1:4:2:1:1:3:1:4:3:1:1:1:11:2:4:1:1:1:1:1:4:2:4:2:2:2:3:2:1:4:2:1:1:1:12:1:11:2:3:5:3:2:1:1:2:2:3:2:1:1:9:1:1:4:1:1:1:1:11:2:3:4:2:1:2:2:5:2:1:1:2:1:2:1:1:2:1:1:3:1:1:12:1:1:11:2:3:3:4:1:1:1:2:2:1:4:1:2:1:1:1:3:1:2:2:3:2:1:1:11:2:3:3:1:4:4:3:1:2:1:1:1:6:1:3:3:1:2:12:1:11:2:3:3:1:1:1:1:3:2:1:3:2:7:1:1:2:1:2:2:4:1:1:1:1:11:2:3:2:3:2:1:1:2:3:1:1:1:3:1:2:6:1:2:3:1:12:1:1:11:2:3:2:2:1:4:1:1:1:1:1:1:2:4:2:1:1:1:2:1:1:1:1:3:1:1:2:1:11:2:3:2:1:3:1:3:2:1:1:1:5:1:1:1:2:2:1:1:2:4:13:1:1:1:11:2:3:2:1:1:3:1:1:1:4:1:3:2:1:2:1:11:2:2:1:11:2:3:1:7:3:1:2:1:1:2:3:3:4:1:1:1:2:1:1:13:1:1:1:11:2:3:1:4:1:1:2:2:1:5:1:1:1:4:1:4:1:2:2:1:2:2:1:11:2:3:1:3:1:1:5:1:3:1:2:2:1:1:3:1:3:1:1:1:1:3:1:12:1:11:2:3:1:2:2:3:1:2:1:1:1:1:2:3:3:1:2:1:1:3:2:1:1:1:1:2:1:11:2:3:1:2:1:2:1:1:2:2:3:2:3:1:1:1:5:5:1:1:13:1:1:1:11:2:3:1:1:4:3:1:1:2:1:1:1:1:5:2:2:3:1:2:1:3:2:1:1:11:2:3:1:1:2:3:2:3:2:2:2:2:4:1:1:2:1:3:2:1:13:1:1:1:11:2:3:1:1:1:7:4:7:1:4:1:2:1:1:2:2:2:2:1:11:2:3:1:1:1:2:1:2:1:1:1:1:1:2:1:1:2:1:1:1:2:1:1:2:2:3:2:13:1:1:1:11:2:3:1:1:1:1:1:2:1:1:2:1:1:1:4:1:2:1:1:2:1:2:1:1:3:1:2:2:1:1:11:2:2:6:3:1:5:1:4:2:5:1:3:2:2:2:12:1:1:11:2:2:5:2:1:1:2:2:1:4:1:1:1:3:1:2:2:2:1:1:4:2:1:1:11:2:2:4:3:2:3:1:2:3:1:2:1:4:2:3:1:1:2:1:1:12:1:1:11:2:2:4:1:3:1:4:1:3:1:1:1:5:2:2:2:1:2:1:1:1:1:2:1:11:2:2:3:6:5:7:2:1:3:1:1:3:2:1:3:12:1:11:2:2:3:3:1:1:2:1:1:1:1:1:1:1:1:7:4:3:1:4:2:1:1:11:2:2:3:2:1:1:2:11:2:2:3:2:1:1:2:3:2:12:1:11:2:2:3:1:2:2:1:5:2:5:1:2:1:1:7:1:2:3:1:1:1:11:2:2:3:1:1:1:3:4:1:1:2:2:2:1:1:2:1:2:1:1:1:2:2:1:2:12:1:11:2:2:2:5:1:1:3:1:1:3:1:1:1:1:1:2:3:2:1:2:4:1:2:1:1:11:2:2:2:3:2:2:2:2:1:1:1:1:1:1:1:1:1:2:2:2:2:1:2:1:3:1:12:1:11:2:2:2:2:4:1:1:4:1:1:1:2:1:2:7:1:3:1:4:1:1:1:1:1:11:2:2:2:2:1:4:1:1:3:5:3:1:7:1:4:2:1:12:1:11:2:2:2:1:11:2:2:2:1:11:2:1:3:1:1:1:1:11:2:2:2:1:3:1:1:3:2:2:1:2:3:1:1:2:1:6:1:3:1:12:1:1:11:2:2:2:1:2:1:1:3:2:2:1:6:1:1:1:4:4:1:2:2:1:2:1:11:2:2:2:1:1:2:2:1:5:2:1:3:1:1:2:1:1:5:6:1:12:1:11:2:2:2:1:1:1:1:3:2:1:1:3:1:1:1:1:1:1:1:1:1:2:2:2:4:3:1:1:1:1:11:2:2:1:6:1:1:1:1:1:3:1:3:1:1:3:1:2:3:1:1:1:3:1:12:1:1:11:2:2:1:4:2:1:3:1:4:2:2:5:3:1:1:1:2:1:1:1:2:2:1:11:2:2:1:3:3:3:1:1:2:4:3:3:2:1:1:1:2:2:1:1:1:2:12:1:11:2:2:1:3:1:3:1:2:1:1:1:5:2:3:3:2:1:2:3:1:3:1:1:1:11:2:2:1:2:5:5:4:1:1:1:2:3:2:2:1:2:1:2:1:1:1:12:1:11:2:2:1:2:2:5:2:1:2:2:2:1:5:1:1:1:1:2:1:1:1:2:1:1:2:1:11:2:2:1:2:2:1:1:1:1:2:1:4:2:1:4:1:4:1:1:2:5:1:12:1:11:2:2:1:2:1:2:1:1:5:1:2:4:8:1:1:2:1:1:1:2:1:2:1:11:2:2:1:2:1:1:1:1:3:1:2:1:2:1:2:1:1:1:2:4:4:1:1:1:2:1:12:1:11:2:2:1:1:4:2:1:1:2:3:1:2:1:2:1:1:1:4:3:1:2:2:2:2:1:11:2:2:1:1:3:1:2:3:1:1:1:1:4:2:2:1:2:5:1:2:1:2:12:1:1:11:2:2:1:1:2:2:3:2:2:1:2:1:1:1:1:2:1:2:1:3:2:3:2:2:2:1:11:2:2:1:1:2:1:1:5:4:2:1:2:2:1:2:1:2:2:1:1:1:3:1:12:1:1:11:2:2:1:1:1:4:1:1:3:1:1:3:1:2:5:2:5:1:4:2:1:1:11:2:2:1:1:1:2:2:1:4:1:2:1:1:1:1:2:3:2:1:3:2:1:3:1:12:1:1:11:2:2:1:1:1:1:3:2:2:8:3:1:3:1:2:4:2:1:2:2:1:11:2:2:1:1:1:1:1:2:4:2:3:1:4:1:3:1:1:1:2:1:2:3:1:1:12:1:11:2:1:9:1:2:1:1:6:4:3:1:4:2:2:1:1:1:2:1:11:2:1:6:3:2:3:2:1:1:1:1:2:1:8:1:1:1:2:2:12:1:1:11:2:1:5:5:1:1:3:3:1:1:2:1:1:1:2:4:1:1:2:3:1:2:1:11:2:1:5:1:4:6:1:3:1:4:1:4:2:1:4:13:1:1:1:11:2:1:4:8:3:1:2:2:1:1:5:5:1:1:1:3:2:1:1:11:2:1:4:3:1:1:1:1:3:1:1:1:1:4:4:1:1:2:2:5:12:1:1:1:11:2:1:4:2:1:1:1:2:1:1:1:1:2:1:2:2:4:1:1:1:1:1:2:1:1:2:3:1:1:1:11:2:1:4:1:2:1:1:3:1:4:4:1:1:1:7:1:2:3:2:12:1:11:2:1:4:1:1:1:1:2:1:1:2:3:1:1:1:2:1:2:1:3:3:1:1:1:1:1:1:1:2:1:11:2:1:3:4:1:2:2:2:7:1:1:2:3:1:1:1:3:1:2:2:12:1:11:2:1:3:3:1:1:3:1:2:2:1:1:2:1:3:2:2:9:2:1:1:1:11:2:1:3:2:2:1:4:1:2:3:2:1:3:1:2:1:3:4:1:2:12:1:1:11:2:1:3:2:1:1:1:8:1:3:3:2:3:1:1:4:4:2:1:1:11:2:1:3:1:3:2:1:1:2:2:1:1:5:1:2:1:1:2:3:2:1:1:2:1:12:1:11:2:1:3:1:2:1:2:1:2:1:6:1:1:3:2:1:1:1:3:4:1:1:1:2:1:11:2:1:3:1:1:2:2:1:4:1:1:1:2:2:1:2:3:2:1:1:2:4:1:1:12:1:11:2:1:3:1:1:1:1:2:2:1:1:3:1:1:1:3:3:1:1:1:4:3:4:1:1:1:1:1:11:2:1:2:5:2:2:2:1:1:2:1:1:4:1:1:2:2:1:5:2:1:1:12:1:11:2:1:2:4:1:2:2:3:1:2:2:2:1:4:3:1:1:1:1:2:1:2:2:1:1:11:2:1:2:3:2:2:2:5:2:6:1:1:3:1:1:2:1:1:1:1:1:12:1:1:11:2:1:2:3:1:1:2:3:1:1:1:2:2:3:1:1:3:4:4:2:3:1:1:1:11:2:1:2:2:3:2:3:1:1:1:2:1:2:1:1:1:1:2:4:2:3:2:1:12:1:1:11:2:1:2:2:2:1:2:2:5:1:1:1:2:1:2:3:1:1:4:3:2:2:1:1:11:2:1:2:2:1:2:2:2:3:1:1:1:1:2:2:11:2:1:1:1:12:1:1:11:2:1:2:2:1:1:1:2:2:1:5:3:2:1:2:2:3:1:2:2:2:1:1:2:1:11:2:1:2:1:4:2:2:1:1:1:5:1:1:6:1:2:1:1:1:4:1:12:1:1:11:2:1:2:1:3:1:2:1:3:2:2:2:1:1:2:1:1:1:4:3:1:1:3:1:2:1:11:2:1:2:1:2:2:2:1:2:1:1:5:3:4:1:3:3:2:2:13:1:1:1:11:2:1:2:1:2:1:1:2:1:1:1:6:1:1:2:2:3:1:4:3:3:2:1:1:11:2:1:2:1:1:3:1:6:1:1:1:2:1:4:2:3:3:3:1:2:1:12:1:11:2:1:2:1:1:2:1:1:3:5:1:1:2:1:2:1:1:1:2:9:2:1:1:1:11:2:1:2:1:1:1:2:1:3:1:1:1:1:2:1:1:1:2:1:1:1:1:1:2:1:2:2:2:1:1:1:12:1:11:2:1:2:1:1:1:1:1:1:2:2:1:1:1:1:5:4:1:3:1:2:1:3:2:2:1:1:11:2:1:1:6:1:1:6:5:4:1:2:5:2:1:2:2:12:1:11:2:1:1:5:1:1:1:2:2:7:2:2:1:3:1:1:1:3:1:3:2:1:1:11:2:1:1:4:2:1:1:1:2:4:4:2:1:1:1:1:1:1:1:1:1:2:3:1:13:1:1:1:11:2:1:1:4:1:1:1:1:1:1:1:1:1:2:4:1:1:11:2:1:1:1:2:1:1:11:2:1:1:3:2:5:2:1:5:3:3:1:1:2:5:2:2:1:12:1:11:2:1:1:3:1:5:2:1:1:1:4:1:2:2:1:1:1:1:1:1:2:2:1:1:1:1:1:2:1:11:2:1:1:3:1:1:3:3:3:1:2:2:3:2:4:1:2:3:1:2:12:1:1:11:2:1:1:2:6:1:3:2:1:2:1:1:1:1:2:1:1:1:2:3:3:2:2:1:1:1:11:2:1:1:2:3:2:6:1:2:2:1:1:2:1:2:1:3:1:1:1:1:1:3:1:12:1:11:2:1:1:2:2:3:2:4:2:5:1:1:3:1:3:2:7:1:1:1:1:1:1:1:1:11:2:1:1:2:2:1:2:1:5:1:3:1:1:1:2:4:1:1:1:1:1:1:2:3:1:12:1:11:2:1:1:2:1:4:1:1:3:2:1:7:3:1:8:3:1:2:1:11:2:1:1:2:1:2:1:5:3:2:2:2:2:4:2:1:2:1:1:3:1:12:1:1:11:2:1:1:2:1:1:2:3:2:1:1:2:2:1:1:2:2:4:1:4:1:3:2:1:1:1:11:2:1:1:2:1:1:1:1:2:2:2:2:1:2:1:3:1:2:3:2:2:2:1:3:1:12:1:11:2:1:1:1:5:2:1:1:1:1:1:3:6:5:1:1:2:4:3:1:1:1:1:11:2:1:1:1:4:1:1:2:2:5:1:3:2:1:3:2:1:1:4:1:13:1:1:1:1:11:2:1:1:1:3:2:1:1:2:2:5:4:2:2:3:1:4:1:1:1:1:1:2:1:11:2:1:1:1:2:11:2:1:1:1:2:11:1:4:12:1:1:11:2:1:1:1:2:2:4:1:6:1:2:1:2:2:1:2:1:3:5:1:1:2:1:11:2:1:1:1:2:1:4:1:6:1:1:1:1:1:1:2:2:1:2:4:2:14:1:1:1:1:1:11:2:1:1:1:2:1:1:2:3:3:1:3:2:1:1:1:1:2:3:9:1:2:1:11:2:1:1:1:1:5:1:4:1:1:7:6:2:2:1:3:1:13:1:1:1:11:2:1:1:1:1:3:2:1:1:1:1:1:4:1:1:2:1:1:1:2:1:1:2:5:1:1:3:1:1:1:11:2:1:1:1:1:2:2:3:1:3:1:1:3:3:2:5:1:3:1:2:1:1:1:12:1:11:2:1:1:1:1:2:1:1:2:1:2:1:4:2:2:4:4:1:2:1:1:4:2:1:1:11:2:1:1:1:1:1:3:1:2:3:1:2:2:1:1:1:1:2:1:2:2:2:2:5:1:12:1:11:2:1:1:1:1:1:2:1:1:1:1:4:2:1:2:2:7:1:1:4:1:1:1:3:1:1:1:11:2:1:1:1:1:1:1:1:4:6:3:2:2:1:1:2:4:3:2:1:1:1:12:1:11:1:11:1:11:1:11:1:3:2:1:1:1:11:1:8:2:2:5:3:3:1:2:8:1:2:1:1:13:1:1:1:11:1:7:3:1:1:4:1:1:1:1:1:1:3:3:1:1:2:2:6:2:1:1:11:1:7:1:1:3:2:4:11:1:7:1:1:1:1:12:1:11:1:6:3:2:1:2:1:1:2:2:1:1:1:2:6:1:1:2:1:4:1:2:1:11:1:6:2:1:1:2:1:2:1:2:2:1:4:1:1:1:1:1:4:1:2:1:2:12:1:1:11:1:6:1:1:11:1:6:1:1:11:1:1:2:1:11:1:5:6:3:2:1:1:1:3:2:1:1:2:1:3:1:2:1:1:2:1:12:1:1:11:1:5:3:2:3:1:4:1:1:1:2:4:1:2:1:3:3:3:1:2:1:11:1:5:2:3:1:1:3:1:1:1:1:2:1:1:2:6:1:1:1:1:2:3:12:1:1:11:1:5:2:1:1:3:1:1:4:5:1:1:1:5:3:2:2:1:1:2:1:1:11:1:5:1:3:1:4:2:2:1:1:1:2:2:1:1:1:3:2:1:2:1:2:1:12:1:1:11:1:5:1:2:1:1:1:2:1:1:1:1:3:2:2:4:1:5:6:1:2:1:11:1:5:1:1:1:4:1:2:2:3:3:1:3:1:2:1:2:1:1:5:12:1:1:11:1:4:6:4:1:3:1:1:2:1:3:2:2:3:2:3:2:2:1:1:11:1:4:4:2:1:1:3:1:3:1:1:1:1:1:2:1:1:1:3:7:12:1:1:1:11:1:4:3:2:2:6:1:1:2:1:1:1:1:2:2:1:2:5:2:2:1:1:1:11:1:4:3:1:1:1:3:1:1:1:1:5:1:1:1:1:1:1:1:2:1:2:1:4:1:12:1:1:11:1:4:2:3:1:1:1:1:1:1:1:5:3:2:1:1:6:2:5:1:1:1:1:1:1:11:1:4:2:1:4:2:1:1:1:2:1:1:2:2:4:1:3:1:3:1:1:2:1:12:1:11:1:4:2:1:1:2:1:6:1:3:1:8:1:2:1:1:1:2:1:1:2:1:1:11:1:4:1:4:3:3:1:5:1:1:1:2:3:3:2:2:1:3:12:1:1:11:1:4:1:3:1:1:4:1:4:1:1:1:1:1:1:1:1:1:3:1:1:3:1:1:3:1:1:1:1:11:1:4:1:2:2:1:1:1:1:2:3:2:1:1:1:1:4:1:2:4:1:4:1:12:1:11:1:4:1:1:5:2:1:1:1:2:1:2:1:2:1:1:2:3:1:2:3:1:1:1:2:1:1:11:1:4:1:1:2:2:1:3:2:2:1:2:1:2:3:1:2:1:2:2:1:2:1:1:12:1:1:11:1:4:1:1:1:2:3:1:1:4:2:1:1:1:3:2:1:1:2:1:1:1:1:1:1:2:2:1:1:11:1:4:1:1:1:1:1:1:2:2:4:5:3:1:3:4:1:5:12:1:1:11:1:3:5:5:1:2:4:1:2:3:1:3:1:2:1:1:5:2:1:1:11:1:3:4:3:4:1:2:1:1:3:1:1:1:5:5:1:2:1:1:1:12:1:11:1:3:4:1:1:7:1:2:2:2:2:1:2:1:1:3:1:1:3:1:2:1:1:1:11:1:3:3:3:2:1:1:1:2:2:1:3:2:1:3:3:1:3:2:2:12:1:1:1:11:1:3:3:2:1:1:1:1:1:2:5:1:4:1:3:2:1:4:3:1:2:1:1:11:1:3:3:1:1:3:1:2:1:1:1:1:4:2:1:2:2:2:3:1:1:1:3:1:12:1:11:1:3:2:5:1:1:2:1:3:1:2:1:2:2:2:1:3:6:1:1:2:1:1:11:1:3:2:3:1:2:1:2:1:5:4:11:1:3:1:1:12:1:11:1:3:2:2:2:1:1:1:2:1:2:2:1:4:1:1:3:2:1:2:2:1:3:2:1:1:11:1:3:2:1:5:1:1:5:2:5:1:1:3:1:1:1:3:1:1:1:1:12:1:1:11:1:3:2:1:2:2:1:1:1:2:2:1:1:2:1:1:1:1:5:6:2:2:2:1:1:11:1:3:2:1:1:2:2:1:1:1:1:2:4:1:1:2:1:2:4:1:2:2:2:12:1:1:1:11:1:3:1:11:1:3:1:11:1:3:2:1:1:1:1:2:1:11:1:3:1:4:2:3:4:1:2:2:4:3:1:2:2:1:2:1:1:12:1:1:1:11:1:3:1:3:2:4:1:1:1:2:1:1:1:1:3:1:1:3:1:1:6:1:1:2:1:1:11:1:3:1:3:1:1:1:2:2:1:1:3:2:2:1:5:4:3:1:1:1:12:1:1:1:11:1:3:1:2:2:4:1:1:1:1:1:1:2:1:1:1:1:1:1:1:2:1:3:1:5:2:1:1:1:11:1:3:1:2:1:3:1:2:3:1:3:1:1:3:1:2:10:1:1:12:1:1:11:1:3:1:2:1:1:1:2:1:2:4:1:4:2:1:1:1:5:1:1:2:1:2:2:1:1:11:1:3:1:1:3:11:1:3:1:1:3:10:2:12:1:1:11:1:3:1:1:2:3:3:1:2:5:1:1:1:1:1:1:2:1:1:1:1:1:2:1:1:1:1:2:1:1:11:1:3:1:1:2:1:1:3:1:2:2:1:1:1:1:3:2:2:2:2:4:3:1:1:12:1:11:1:3:1:1:1:3:1:1:2:3:1:1:1:1:4:6:1:3:3:4:1:1:1:1:1:11:1:3:1:1:1:1:4:1:1:3:1:1:1:1:1:5:3:1:5:1:3:12:1:1:1:11:1:3:1:1:1:1:1:1:4:2:1:4:6:1:1:4:1:2:1:4:2:1:1:11:1:2:7:1:2:1:1:2:3:1:1:2:2:1:5:1:1:3:1:1:2:1:12:1:11:1:2:5:4:1:6:1:1:1:4:3:4:6:2:2:1:1:11:1:2:5:1:1:5:3:1:3:2:1:3:2:2:1:5:1:1:12:1:1:1:11:1:2:4:3:1:2:1:2:4:1:3:1:1:1:1:3:2:5:2:2:1:1:1:11:1:2:4:1:4:1:5:2:1:2:6:2:4:1:1:1:14:1:1:1:1:1:11:1:2:4:1:1:1:4:5:3:1:2:4:5:1:1:1:1:1:2:2:1:1:11:1:2:3:4:1:1:5:11:1:2:3:3:3:1:12:1:1:11:1:2:3:2:4:1:1:2:2:1:3:1:1:2:1:2:1:2:1:3:1:3:1:1:2:1:11:1:2:3:2:1:1:3:2:2:1:2:1:1:2:3:1:4:1:2:3:3:12:1:1:11:1:2:3:1:3:1:1:1:4:1:1:4:5:2:1:2:2:1:3:2:1:2:1:11:1:2:3:1:1:3:3:1:1:2:1:1:2:1:2:1:3:1:1:4:1:2:1:2:12:1:1:11:1:2:3:1:1:1:1:2:2:1:2:3:1:1:7:2:2:1:5:1:1:1:2:1:11:1:2:2:4:5:1:3:2:3:2:11:1:1:3:1:12:1:11:1:2:2:3:3:1:2:1:2:3:1:2:2:1:1:4:1:3:3:2:1:2:1:1:11:1:2:2:3:1:1:1:3:1:1:2:1:1:2:2:1:1:1:2:3:1:1:1:1:2:2:1:1:12:1:11:1:2:2:2:2:3:1:4:1:1:3:4:3:2:1:1:1:2:3:1:1:1:2:1:11:1:2:2:2:1:2:3:2:2:1:1:3:1:2:1:6:3:1:2:2:12:1:1:1:11:1:2:2:2:1:1:1:1:1:1:1:2:2:1:1:1:1:1:8:1:1:1:5:2:1:2:1:11:1:2:2:1:3:2:1:5:2:2:2:2:1:3:1:1:3:1:3:1:2:12:1:1:11:1:2:2:1:2:2:1:3:4:1:2:1:2:1:1:2:1:1:2:3:1:3:1:1:2:1:1:11:1:2:2:1:1:5:1:4:1:4:1:1:1:1:1:2:1:2:1:5:1:13:1:1:1:1:11:1:2:2:1:1:2:1:3:1:2:1:2:1:1:3:1:2:1:1:1:2:1:1:1:1:3:1:2:1:1:1:11:1:2:2:1:1:1:1:6:1:2:4:7:1:1:3:2:2:2:12:1:1:1:11:1:2:1:7:1:1:3:2:1:3:1:1:1:3:1:1:1:1:1:1:5:1:1:1:2:1:11:1:2:1:5:1:1:1:7:3:2:3:1:2:3:2:3:3:12:1:11:1:2:1:4:1:2:3:1:1:2:1:1:1:1:2:1:3:5:2:7:1:1:1:1:1:1:1:1:11:1:2:1:3:3:2:1:1:1:1:2:1:1:4:2:1:1:1:4:3:4:1:12:1:1:11:1:2:1:3:1:4:1:11:1:2:1:3:1:4:1:4:2:1:11:1:2:1:3:1:1:1:2:4:3:2:1:1:1:6:1:1:2:1:1:2:3:12:1:11:1:2:1:2:3:4:2:2:1:1:1:1:2:4:1:1:1:1:2:2:2:1:3:2:1:1:11:1:2:1:2:2:2:4:2:2:1:3:5:4:1:6:1:13:1:1:1:1:11:1:2:1:2:1:11:1:2:1:2:1:11:1:2:3:1:1:1:1:11:1:2:1:2:1:2:2:1:1:1:6:2:1:1:3:4:2:1:1:2:2:1:1:12:1:1:11:1:2:1:2:1:1:2:1:1:1:5:1:5:1:1:2:1:3:1:1:3:1:1:2:1:2:1:11:1:2:1:1:6:1:2:2:2:3:1:1:3:1:1:2:3:6:13:1:1:1:1:11:1:2:1:1:4:1:1:1:3:2:1:2:1:1:2:1:1:4:5:3:2:2:1:1:1:11:1:2:1:1:3:1:2:1:4:4:5:3:1:2:1:2:3:3:1:1:12:1:11:1:2:1:1:2:3:1:2:1:1:1:1:1:1:3:1:1:1:1:3:1:2:1:1:1:4:1:2:1:1:1:11:1:2:1:1:2:1:3:2:1:1:1:2:1:3:5:1:4:2:2:2:1:1:1:12:1:1:11:1:2:1:1:2:1:1:1:1:2:1:1:1:1:5:1:2:3:1:1:3:7:1:1:2:1:11:1:2:1:1:1:3:2:1:6:3:1:1:1:1:1:1:3:1:1:1:2:1:4:12:1:1:1:11:1:2:1:1:1:2:2:1:1:4:1:1:7:1:1:2:1:3:2:3:2:2:1:1:1:11:1:2:1:1:1:1:4:1:3:1:1:1:2:1:2:2:5:1:2:2:3:1:2:12:1:1:11:1:2:1:1:1:1:2:1:1:1:2:1:2:3:1:1:2:1:3:3:3:1:1:3:2:2:1:1:11:1:2:1:1:1:1:1:1:2:1:1:2:6:2:2:1:1:2:1:1:3:3:1:14:1:1:1:1:1:11:1:1:8:1:1:1:1:1:4:4:1:2:1:1:1:3:1:1:1:1:2:1:1:1:1:2:1:11:1:1:6:2:5:2:1:1:2:7:2:1:2:1:2:3:2:1:12:1:11:1:1:5:6:5:2:1:3:1:2:3:1:1:1:2:2:2:2:1:2:1:11:1:1:5:2:1:3:1:1:3:1:1:1:3:4:5:1:1:1:2:2:12:1:1:1:11:1:1:5:1:1:3:2:2:2:4:1:1:3:1:1:4:2:2:1:3:2:1:11:1:1:4:4:2:1:1:3:1:4:2:6:1:3:1:2:3:1:1:12:1:11:1:1:4:2:3:3:3:1:1:1:3:1:2:1:4:1:2:2:1:3:1:1:2:1:11:1:1:4:2:1:1:1:2:1:2:1:1:2:3:1:3:1:1:2:3:3:1:2:1:12:1:1:11:1:1:4:1:2:2:1:2:1:1:2:1:3:2:2:1:1:2:1:1:1:2:2:1:2:1:1:2:1:11:1:1:4:1:1:1:7:1:2:2:2:1:5:3:1:3:4:1:1:12:1:11:1:1:3:5:3:1:3:2:3:3:1:2:1:1:1:1:1:2:1:3:1:1:1:2:1:11:1:1:3:3:4:1:5:2:3:1:1:1:1:2:2:1:2:1:1:1:3:1:1:12:1:11:1:1:3:3:1:1:1:3:3:2:1:1:3:4:1:1:1:1:1:1:1:1:1:2:2:2:1:1:11:1:1:3:2:2:2:1:4:1:1:2:2:2:2:1:1:3:8:13:1:1:1:1:11:1:1:3:2:1:2:1:1:1:1:1:1:1:2:2:3:1:1:2:1:8:1:1:1:1:1:2:1:11:1:1:3:1:4:3:1:1:4:1:2:1:1:2:3:2:3:1:3:1:1:1:12:1:1:11:1:1:3:1:2:4:1:1:2:2:1:1:1:1:1:2:1:5:2:1:1:1:1:1:3:2:1:11:1:1:3:1:2:1:1:1:2:1:2:1:7:3:1:1:1:2:1:3:2:2:1:12:1:1:11:1:1:3:1:1:2:2:1:1:1:1:4:1:1:1:1:1:1:2:1:2:1:1:1:2:1:2:4:1:1:1:1:1:11:1:1:3:1:1:1:1:4:1:1:1:1:2:1:1:2:1:3:1:5:2:2:15:1:1:1:1:1:1:11:1:1:2:6:1:5:1:1:1:1:2:3:2:2:3:1:3:2:2:1:2:1:1:11:1:1:2:4:3:1:2:3:1:1:2:1:2:3:3:2:2:1:1:1:1:2:1:11:1:1:11:1:1:2:3:6:4:2:3:2:1:2:2:3:4:2:3:2:1:11:1:1:2:3:2:1:2:1:1:2:4:1:1:1:1:3:1:1:2:1:5:1:3:2:1:11:1:1:2:3:1:1:2:1:3:3:2:1:2:2:1:1:4:1:1:1:1:2:2:2:11:1:11:1:1:2:2:3:4:5:2:1:3:1:2:2:1:1:5:1:1:4:1:1:1:1:1:11:1:1:2:2:2:2:2:1:1:1:1:4:1:1:3:2:3:2:1:1:1:2:2:1:2:1:1:1:11:1:1:2:2:1:4:1:2:1:2:2:3:3:1:2:1:3:1:2:2:2:2:11:1:11:1:1:2:2:1:1:5:1:1:2:1:1:1:3:1:2:2:1:3:2:1:1:1:2:2:2:1:11:1:1:2:2:1:1:1:1:1:2:8:3:3:1:2:1:1:1:4:2:3:1:1:1:1:11:1:1:2:1:4:1:6:1:1:1:3:1:1:1:5:1:1:1:1:1:1:2:1:1:11:1:1:1:11:1:1:2:1:3:1:3:1:1:3:4:1:1:1:2:1:6:1:1:3:1:1:2:1:1:1:11:1:1:2:1:2:3:1:1:1:11:1:1:2:1:2:3:1:1:1:3:2:1:1:1:11:1:1:2:1:2:1:2:2:2:4:11:1:1:2:1:2:1:2:1:1:11:1:1:11:1:1:2:1:1:5:1:4:2:3:2:1:4:1:2:3:4:1:1:1:2:1:1:11:1:1:2:1:1:2:5:1:1:1:3:3:6:1:2:1:6:1:1:1:2:1:1:11:1:1:2:1:1:2:1:1:1:2:1:2:1:1:3:1:5:1:4:2:1:1:1:3:1:1:11:1:11:1:1:2:1:1:1:2:1:3:1:2:2:4:3:11:1:1:1:1:2:1:1:1:11:1:1:2:1:1:1:1:1:2:2:1:2:1:4:1:1:4:2:2:1:1:3:3:3:1:1:1:1:11:1:1:1:6:4:1:1:2:2:2:2:3:2:1:2:2:3:3:1:1:1:11:1:11:1:1:1:5:2:1:1:5:3:1:2:4:1:2:1:5:3:2:2:1:11:1:1:1:4:3:2:3:1:2:2:1:1:1:2:1:1:2:3:3:2:2:1:2:1:1:1:11:1:1:1:4:1:3:1:2:1:2:1:1:1:1:3:1:2:5:3:3:1:1:1:11:1:1:11:1:1:1:3:6:1:2:1:2:1:2:2:1:1:4:1:2:1:3:1:3:2:1:1:1:11:1:1:1:3:2:9:2:1:2:2:1:1:1:7:4:3:2:1:11:1:1:1:3:2:1:1:1:3:1:2:1:2:1:3:1:1:3:1:2:3:1:1:1:3:11:1:11:1:1:1:3:1:2:1:3:1:2:1:1:2:1:4:4:2:2:1:2:3:2:2:1:11:1:1:1:3:1:1:1:2:1:2:2:1:2:1:3:1:2:2:4:1:2:1:6:1:1:1:1:1:1:1:11:1:1:1:2:4:3:1:1:1:3:1:2:2:1:2:2:1:1:3:1:6:11:1:1:1:11:1:1:1:2:3:1:4:11:1:1:1:2:3:1:4:3:1:2:1:1:11:1:1:1:2:2:3:1:1:3:3:1:1:1:6:1:2:1:1:1:4:1:1:1:1:1:2:1:11:1:1:1:2:2:1:2:2:1:1:2:1:1:3:1:1:1:1:4:3:8:11:1:1:11:1:1:1:2:1:4:3:2:6:3:3:4:3:1:5:1:2:1:1:11:1:1:1:2:1:2:3:1:2:1:1:3:3:1:3:3:2:2:2:2:2:2:2:1:11:1:1:1:2:1:1:4:3:2:3:2:5:3:2:1:2:3:3:1:11:1:1:11:1:1:1:2:1:1:1:4:2:6:6:1:1:7:2:1:2:2:1:1:1:11:1:1:1:1:11:1:1:1:1:11:1:1:1:2:1:1:1:1:1:1:1:2:1:11:1:1:1:1:5:1:1:1:2:3:3:1:5:1:4:1:7:1:11:1:1:1:11:1:1:1:1:4:1:1:1:3:1:4:1:1:2:2:1:2:2:2:2:1:1:2:2:1:1:2:1:11:1:1:1:1:3:2:1:2:1:4:1:3:4:2:1:2:2:3:1:1:2:1:2:1:1:1:11:1:1:1:1:3:1:1:1:3:1:4:5:1:2:2:1:2:1:6:1:1:1:1:11:1:11:1:1:1:1:2:3:1:7:2:2:1:5:4:4:1:1:1:2:1:2:1:1:11:1:1:1:1:2:2:1:2:1:2:1:7:4:1:2:4:1:2:1:2:3:1:1:1:1:11:1:1:1:1:2:1:2:2:2:1:4:3:1:2:1:4:1:1:1:1:3:2:1:12:1:1:1:1:11:1:1:1:1:2:1:1:1:2:4:1:1:4:4:1:1:1:1:3:1:1:2:1:2:3:1:1:1:1:11:1:1:1:1:1:4:3:1:2:1:1:2:1:1:8:1:2:1:1:3:1:2:2:1:1:1:11:1:1:1:1:1:3:1:3:2:3:1:2:1:1:5:1:2:4:2:2:1:1:1:11:1:1:11:1:1:1:1:1:2:2:4:1:1:2:1:1:1:2:1:2:1:2:1:3:3:2:1:3:2:1:11:1:1:1:1:1:2:1:1:4:2:6:3:2:1:2:2:2:1:2:4:2:1:1:11:1:1:1:1:1:1:3:5:1:5:2:2:2:2:1:2:2:2:4:1:1:11:1:1:11:1:1:1:1:1:1:2:1:6:1:4:1:1:6:1:1:3:1:2:1:1:2:2:1:1:1:11:1:1:1:1:1:1:1:2:7:4:1:1:1:1:3:2:4:2:4:2:2:1:11:1:1:1:1:1:1:1:1:1:11:1:1:1:1:1:1:1:1:1:10:1:1:11:1:1:10:10:10:10:10:3:1:1:1:1:10:9:1:7:3:6:1:2:1:4:2:5:1:2:1:10:8:2:6:1:3:4:1:1:1:1:2:1:3:1:1:2:3:1:11:1:10:8:1:1:5:1:4:2:2:2:1:3:2:2:1:1:3:2:1:2:1:1:10:7:3:4:2:4:1:2:1:2:1:1:1:1:2:1:1:2:1:2:1:1:1:1:2:1:10:7:2:1:4:1:1:1:2:3:2:6:2:1:1:2:5:1:11:1:10:7:1:2:3:2:2:2:1:3:2:1:4:2:8:3:1:1:1:1:10:7:1:1:1:3:1:1:3:4:2:1:5:3:2:1:4:1:2:1:1:1:10:6:4:2:3:3:2:1:1:1:6:1:1:1:1:1:5:1:1:11:1:1:1:10:6:3:1:2:2:1:2:1:1:1:1:1:2:3:1:3:2:2:1:4:1:1:1:2:1:10:6:2:2:2:1:1:5:4:1:1:2:1:1:2:1:1:3:1:3:1:1:1:2:1:10:6:2:1:1:1:5:1:1:2:3:3:1:1:1:2:4:1:1:1:1:1:1:1:11:1:1:10:6:1:3:1:2:8:2:3:10:4:2:2:1:10:6:1:2:1:1:2:1:1:2:1:3:2:1:1:1:1:8:2:2:3:2:1:10:6:1:1:2:1:1:2:1:5:1:6:2:1:1:1:2:2:2:1:1:1:1:11:1:1:10:6:1:1:1:1:1:1:1:1:2:1:1:1:1:10:5:6:2:1:1:10:5:4:5:1:3:2:2:5:1:2:1:2:1:4:3:1:2:1:10:5:3:5:1:1:1:2:4:2:1:2:5:2:5:1:1:1:11:1:1:10:5:3:1:3:1:4:1:4:2:4:1:1:1:1:2:1:4:1:3:1:1:1:1:10:5:2:4:4:2:1:2:3:1:1:4:1:1:2:2:2:1:1:1:1:2:1:1:1:10:5:2:2:2:2:1:2:2:1:1:4:1:1:3:1:1:2:5:2:1:11:1:1:1:10:5:2:1:3:1:1:4:2:6:1:10:4:3:1:1:1:1:10:5:2:1:1:1:4:4:1:5:2:4:1:3:2:2:1:1:1:1:1:2:1:10:5:1:4:2:5:2:1:2:4:4:1:4:1:1:2:2:11:1:1:10:5:1:3:1:2:1:1:2:1:3:1:2:2:2:3:1:3:1:1:2:1:1:1:1:1:2:1:10:5:1:2:2:1:2:2:1:4:2:2:2:3:1:1:1:1:2:1:1:3:1:1:2:1:1:10:5:1:2:1:1:1:1:1:3:3:3:2:3:2:1:3:4:1:1:1:1:11:1:1:1:10:5:1:1:2:5:1:1:1:1:3:2:1:3:2:3:2:1:1:1:2:1:3:1:1:1:1:10:5:1:1:1:4:2:1:1:5:6:1:2:3:1:1:1:3:4:1:1:1:1:1:10:5:1:1:1:1:2:3:1:2:1:2:1:1:2:3:1:1:1:1:1:2:1:3:1:3:11:1:10:4:7:1:2:1:1:2:3:2:1:3:1:1:2:1:2:3:3:1:2:1:1:1:10:4:5:1:3:5:2:3:1:4:1:1:2:5:3:1:1:1:2:1:10:4:4:2:2:1:3:1:1:1:1:2:2:1:1:2:1:2:1:3:2:2:1:12:1:1:1:1:10:4:4:1:1:1:2:1:5:1:2:4:1:2:1:1:4:1:2:1:2:2:1:1:10:4:3:3:1:1:1:1:1:1:5:1:1:1:1:1:2:5:1:2:3:1:3:1:1:1:1:10:4:3:1:4:3:2:4:4:1:1:1:1:1:1:1:1:2:1:1:2:2:1:11:1:10:4:3:1:1:2:3:1:2:2:6:1:1:1:1:3:1:1:3:1:1:1:1:1:1:2:1:10:4:2:5:1:1:2:1:2:2:1:4:1:10:3:2:2:1:1:10:4:2:3:1:2:5:2:1:4:2:2:2:2:1:2:1:3:3:11:1:10:4:2:2:2:1:3:1:2:2:2:2:1:1:1:10:3:2:1:2:1:10:4:2:2:1:1:1:1:1:1:2:2:3:3:6:1:2:1:3:2:3:2:1:10:4:2:1:2:3:2:3:4:2:4:1:3:1:1:2:1:4:2:11:1:10:4:2:1:1:3:2:1:4:1:1:6:2:3:2:1:1:4:4:1:1:1:1:1:10:4:2:1:1:1:1:4:3:1:1:1:1:1:4:1:1:3:2:2:1:1:1:1:1:1:2:1:1:1:10:4:1:5:2:1:1:1:4:2:1:1:1:1:2:3:10:1:1:11:1:10:4:1:4:1:1:1:1:8:2:2:1:1:1:1:3:1:2:1:4:2:2:1:10:4:1:3:1:3:3:2:4:1:1:1:1:4:3:3:1:2:1:1:2:2:1:10:4:1:2:4:2:1:2:1:2:1:1:1:1:3:1:2:2:3:2:1:1:1:1:1:1:1:11:1:10:4:1:2:2:1:3:3:1:3:1:4:2:1:2:1:1:2:2:1:1:1:1:1:2:1:1:1:10:4:1:2:1:2:1:3:1:5:1:1:1:1:1:4:1:3:1:2:1:1:1:1:2:2:1:10:4:1:1:9:1:2:2:2:7:3:1:1:1:1:1:1:3:1:11:1:10:4:1:1:3:2:5:3:1:2:1:1:2:2:6:2:2:2:1:2:1:1:10:4:1:1:2:3:1:1:1:1:1:1:1:3:2:2:5:7:2:2:1:2:1:10:4:1:1:2:1:1:2:1:1:1:1:5:1:1:2:1:3:2:1:1:1:3:1:3:1:11:1:10:4:1:1:1:3:1:1:1:1:1:1:6:2:2:2:5:1:2:1:1:4:2:1:10:4:1:1:1:1:4:1:1:1:1:1:2:2:1:1:1:3:1:1:1:1:1:4:2:1:1:1:2:1:1:1:10:4:1:1:1:1:1:1:6:5:1:2:1:1:4:3:1:1:1:1:1:2:1:11:1:1:10:3:7:1:4:1:3:4:5:3:1:1:2:1:2:1:1:1:1:1:2:1:10:3:5:6:2:1:1:1:2:1:2:2:2:1:1:1:1:4:1:1:1:2:1:2:1:10:3:5:1:2:2:3:2:1:1:5:1:2:1:1:2:1:1:4:1:1:2:11:1:10:3:4:3:3:1:2:1:2:2:1:2:1:1:3:3:8:1:2:1:1:10:3:4:2:1:1:1:3:2:2:3:2:2:2:1:1:3:1:1:2:2:4:1:1:1:1:1:10:3:4:1:1:3:1:5:2:1:4:1:2:1:5:1:2:2:3:11:1:10:3:3:5:1:1:1:1:2:1:3:3:2:1:1:1:1:2:2:3:2:1:2:2:1:10:3:3:3:1:1:3:2:1:1:1:1:1:4:3:7:2:3:1:1:2:1:10:3:3:2:1:4:1:2:1:1:1:3:1:1:2:2:1:4:3:2:1:2:11:1:1:10:3:3:1:4:1:2:1:2:3:2:1:1:3:5:5:2:1:1:2:1:1:10:3:3:1:2:1:1:10:3:3:1:2:1:1:8:2:2:1:10:3:3:1:1:1:4:2:4:1:1:1:1:2:1:1:2:2:2:1:1:2:1:1:1:2:11:1:10:3:2:6:1:2:2:3:1:1:3:1:3:6:2:1:3:2:2:1:10:3:2:4:1:1:6:1:1:1:1:2:3:1:1:1:1:2:1:1:2:2:4:2:1:10:3:2:3:1:4:1:2:1:6:1:5:1:2:4:2:2:12:1:1:1:10:3:2:2:4:1:1:2:2:1:2:2:3:3:1:2:2:1:1:1:4:3:1:1:1:10:3:2:2:2:1:1:2:4:1:1:1:5:5:1:1:2:1:1:3:3:2:1:10:3:2:2:1:1:3:2:1:1:1:1:2:1:6:1:2:2:1:1:2:2:1:13:1:1:1:1:10:3:2:1:4:4:1:1:1:4:1:2:5:1:3:3:1:2:2:1:2:1:10:3:2:1:3:1:1:1:1:7:6:1:1:2:2:1:3:3:4:1:1:1:1:10:3:2:1:2:1:2:2:3:2:1:4:1:1:1:2:1:1:2:1:1:2:3:1:1:1:11:1:10:3:2:1:1:3:2:1:2:1:1:4:4:2:1:2:2:1:1:1:3:4:2:1:10:3:2:1:1:1:5:1:1:2:9:1:1:4:1:2:3:1:2:2:1:1:10:3:2:1:1:1:1:1:5:3:3:1:3:4:5:1:1:2:2:12:1:1:1:10:3:1:6:1:1:2:1:2:1:1:2:1:1:3:1:1:2:2:3:3:2:1:1:2:1:10:3:1:4:3:2:1:4:1:3:1:1:1:2:1:2:1:5:1:6:1:1:1:1:1:1:10:3:1:4:1:1:1:4:2:2:1:1:3:1:1:3:1:2:3:1:4:2:11:1:10:3:1:3:2:3:1:3:1:1:1:3:1:3:10:2:4:2:1:10:3:1:3:1:2:2:4:1:1:1:2:1:1:2:3:2:1:1:1:1:2:1:1:2:2:2:1:10:3:1:2:7:2:2:1:2:2:4:1:5:2:1:1:1:4:1:11:1:10:3:1:2:3:1:4:3:3:1:3:3:2:1:2:1:4:3:1:1:2:1:10:3:1:2:2:2:1:1:1:1:1:1:3:1:1:1:1:2:3:6:1:1:1:2:4:1:1:1:1:10:3:1:2:1:4:1:1:2:2:1:4:1:4:4:2:4:4:1:11:1:10:3:1:2:1:2:1:1:1:2:1:1:3:1:4:2:1:1:1:1:1:1:1:1:2:1:2:4:1:1:1:1:10:3:1:2:1:1:1:2:1:2:3:6:3:3:5:1:1:3:4:1:1:1:1:10:3:1:1:5:1:1:3:2:2:1:1:2:4:7:2:2:1:1:12:1:1:1:10:3:1:1:3:3:1:4:1:1:2:1:2:2:2:2:1:1:1:1:3:6:1:1:1:1:1:1:1:10:3:1:1:3:1:1:1:1:4:1:1:1:3:2:1:2:4:4:6:2:1:1:10:3:1:1:2:2:2:1:3:2:1:2:1:1:3:2:2:5:1:2:2:1:12:1:1:1:10:3:1:1:2:1:2:1:1:2:3:1:6:2:1:1:2:1:2:3:1:2:1:3:1:1:1:10:3:1:1:1:5:1:2:1:1:1:3:2:2:1:1:4:2:2:2:2:3:2:1:1:10:3:1:1:1:3:1:1:1:1:2:1:2:2:1:2:1:2:2:4:2:1:2:2:2:11:1:1:10:3:1:1:1:2:1:1:10:3:1:1:1:2:1:1:9:4:1:1:1:1:10:3:1:1:1:1:2:5:2:1:1:6:3:2:2:2:3:1:1:3:1:2:1:10:3:1:1:1:1:1:2:3:1:4:1:3:2:6:1:1:3:2:1:2:2:11:1:10:2:10:2:10:2:10:2:3:1:2:1:10:2:7:1:2:1:1:3:3:2:1:1:1:2:2:5:4:3:1:2:1:10:2:6:1:2:3:1:4:2:2:5:2:1:1:3:1:1:1:1:1:1:1:11:1:10:2:5:3:1:1:5:2:1:1:2:3:1:2:3:3:2:1:2:2:2:1:10:2:5:1:3:1:1:1:4:3:2:3:1:1:1:1:1:2:1:1:3:1:4:1:1:1:1:10:2:4:7:4:1:2:1:1:5:1:1:1:1:7:2:12:1:1:1:10:2:4:3:1:2:2:1:3:1:1:4:1:3:5:1:1:1:1:3:3:1:1:1:10:2:4:2:1:2:3:2:5:1:4:1:3:1:1:5:3:1:1:2:1:10:2:4:1:3:1:1:2:3:2:2:1:1:2:2:1:2:4:1:1:3:2:1:11:1:10:2:4:1:1:2:4:2:2:2:1:2:3:1:1:3:3:1:2:1:3:2:1:1:10:2:3:7:2:1:1:4:4:1:3:1:1:1:1:3:1:1:1:2:1:1:2:1:10:2:3:4:1:1:2:3:1:1:2:2:1:4:1:1:1:1:3:2:3:3:11:1:10:2:3:3:1:2:1:1:1:2:1:2:2:2:3:2:1:6:1:1:1:3:1:2:1:10:2:3:2:2:3:3:2:4:1:1:1:1:1:1:1:1:2:2:1:2:2:1:1:2:2:1:10:2:3:2:1:2:1:2:4:4:3:3:1:1:3:2:3:2:13:1:1:1:1:10:2:3:1:5:1:1:1:1:1:1:3:1:6:2:1:6:1:1:1:1:1:2:1:1:10:2:3:1:2:4:2:1:1:1:2:1:1:1:1:2:1:1:1:4:2:2:1:1:2:2:2:1:10:2:3:1:2:1:1:1:1:5:1:1:1:1:1:1:1:1:2:2:1:3:5:2:12:1:1:1:10:2:3:1:1:2:1:3:2:1:2:2:1:1:3:5:1:2:2:1:2:3:2:1:1:10:2:3:1:1:1:1:2:3:1:2:1:2:2:1:1:1:2:2:1:1:2:2:1:2:1:2:2:1:1:10:2:2:5:6:1:2:1:1:3:1:2:2:1:3:5:1:1:1:1:1:11:1:10:2:2:4:2:1:5:3:1:4:1:1:10:2:1:3:2:1:10:2:2:3:3:10:2:2:3:3:9:2:1:2:1:10:2:2:3:1:3:9:1:1:1:1:1:1:3:2:1:7:1:1:11:1:1:10:2:2:2:10:2:2:2:10:2:2:2:2:2:2:1:10:2:2:2:3:1:1:4:1:1:2:1:2:6:1:3:2:3:1:1:4:1:1:1:1:10:2:2:2:2:1:1:4:1:1:2:5:3:1:2:2:1:2:1:1:1:3:12:1:1:1:10:2:2:2:1:2:2:2:1:10:2:2:2:1:2:2:2:1:2:3:1:1:1:10:2:2:2:1:1:1:3:4:1:1:1:1:3:1:2:10:2:2:1:1:2:1:10:2:2:1:5:1:1:1:3:4:1:2:2:3:3:1:2:2:1:3:1:11:1:1:10:2:2:1:3:2:1:5:2:1:1:1:3:1:1:5:2:3:1:1:1:3:2:1:10:2:2:1:2:3:3:2:1:4:3:1:2:4:1:2:2:2:1:1:1:1:2:1:1:10:2:2:1:2:1:3:1:2:2:1:1:1:2:3:1:3:1:1:1:1:6:1:1:1:11:1:1:10:2:2:1:1:5:5:1:1:1:1:1:1:1:1:2:2:4:1:1:1:2:1:1:1:1:2:1:1:10:2:2:1:1:2:5:1:1:1:2:2:2:1:2:1:1:2:1:3:3:3:1:2:2:1:10:2:2:1:1:2:1:1:1:1:1:1:2:1:1:1:1:3:1:7:1:4:2:3:1:11:1:10:2:2:1:1:1:2:1:1:2:1:2:1:1:2:2:1:4:1:5:2:2:2:1:2:2:1:10:2:2:1:1:1:1:1:1:2:1:1:1:1:7:1:4:2:1:1:1:2:1:2:1:2:1:2:1:10:2:1:6:1:2:4:2:1:1:1:1:4:1:5:1:1:2:1:2:1:1:11:1:1:10:2:1:5:1:1:4:2:2:4:3:1:10:2:1:2:1:2:1:10:2:1:4:2:2:1:1:2:3:2:1:1:1:2:1:1:2:2:2:1:2:1:1:1:1:1:2:1:1:10:2:1:4:1:1:2:1:5:1:3:1:2:1:2:4:4:1:2:1:1:1:11:1:1:10:2:1:3:3:2:2:4:3:3:1:1:2:2:1:2:1:1:3:1:1:3:1:1:1:1:10:2:1:3:2:1:2:5:1:2:1:3:1:1:1:2:1:1:2:4:3:1:3:1:1:1:10:2:1:3:1:2:3:1:1:5:1:4:6:1:1:2:2:1:2:1:1:11:1:1:10:2:1:3:1:1:1:3:1:8:1:1:6:2:1:1:1:1:1:4:1:2:1:1:10:2:1:2:4:3:2:2:2:3:1:2:1:1:2:3:4:1:1:2:2:2:1:1:10:2:1:2:3:1:3:3:1:1:1:1:1:3:1:2:3:3:1:1:3:1:2:1:11:1:1:10:2:1:2:2:2:3:2:10:2:1:2:2:2:3:2:2:2:2:1:10:2:1:2:2:1:1:3:2:2:2:1:1:1:2:1:3:1:2:3:1:1:2:4:1:2:1:10:2:1:2:1:3:3:1:5:1:4:1:1:2:1:4:1:2:2:1:3:1:11:1:10:2:1:2:1:2:1:3:1:1:3:2:1:1:1:2:1:1:1:1:3:3:1:1:6:1:1:1:1:1:1:1:10:2:1:2:1:1:2:2:4:2:4:1:2:4:4:2:1:1:1:1:2:3:1:1:1:10:2:1:2:1:1:1:1:2:2:3:1:1:2:2:5:1:3:3:4:3:1:11:1:10:2:1:1:5:2:1:3:2:3:2:1:2:8:1:1:2:5:1:1:1:1:1:1:10:2:1:1:4:1:2:1:1:1:3:1:2:2:1:1:3:1:3:2:1:1:4:2:1:2:1:10:2:1:1:3:2:1:3:4:3:1:3:1:5:1:1:4:4:1:11:1:1:10:2:1:1:3:1:1:1:2:2:2:1:1:1:6:1:4:2:1:4:2:2:1:2:1:10:2:1:1:2:3:1:1:3:3:2:1:6:10:1:4:1:2:1:10:2:1:1:2:2:1:1:1:1:2:2:5:2:1:2:2:3:4:2:1:13:1:1:1:1:1:10:2:1:1:2:1:1:6:1:2:1:1:1:2:2:2:1:5:5:2:1:2:2:1:10:2:1:1:1:7:1:2:3:1:1:4:1:1:2:1:1:9:2:1:2:1:10:2:1:1:1:3:3:2:1:1:1:1:5:5:1:1:1:4:1:2:3:1:1:11:1:10:2:1:1:1:2:3:4:1:4:1:10:2:1:1:1:2:2:3:1:1:1:1:10:2:1:1:1:2:1:2:2:1:1:1:3:1:1:1:4:1:4:1:2:3:3:2:1:2:1:10:2:1:1:1:1:4:1:1:1:5:1:4:5:2:1:1:5:2:2:11:1:1:10:2:1:1:1:1:2:1:3:3:1:1:1:1:2:1:1:1:1:3:1:2:2:2:3:1:2:1:2:1:10:2:1:1:1:1:1:2:2:1:4:3:3:3:1:1:1:1:1:1:1:1:2:4:3:1:1:1:1:10:2:1:1:1:1:1:1:1:1:2:10:2:1:1:1:1:1:1:1:1:3:1:1:1:1:1:1:11:1:1:10:1:8:1:1:1:5:3:2:1:3:2:1:1:2:1:1:1:1:1:2:1:1:2:1:1:10:1:6:5:3:2:1:2:1:3:1:1:1:2:2:1:1:2:3:2:1:2:1:1:10:1:6:1:3:1:2:1:2:1:2:2:3:6:1:1:3:1:14:1:1:1:1:1:1:10:1:5:4:2:1:1:3:4:2:6:1:3:1:2:1:1:1:2:1:2:1:10:1:5:2:1:6:1:1:1:1:1:1:1:1:3:1:1:1:2:3:1:1:1:1:3:1:2:1:10:1:5:1:2:2:2:1:2:3:1:2:2:1:1:3:2:1:2:2:1:2:1:1:11:1:1:10:1:5:1:1:1:1:2:1:6:1:2:7:2:2:2:1:2:1:1:2:2:1:10:1:4:3:6:3:1:1:1:1:1:2:2:2:7:1:1:3:2:1:1:1:10:1:4:2:4:1:3:1:1:3:2:2:1:1:10:1:4:1:11:1:10:1:4:2:1:2:1:3:10:1:4:2:1:2:1:3:2:1:1:2:1:10:1:4:1:3:3:2:1:3:4:3:1:1:1:7:6:2:1:1:10:1:4:1:2:1:1:3:2:3:1:3:5:1:1:3:2:1:1:1:1:1:1:2:11:1:10:1:4:1:1:1:10:1:4:1:1:1:10:1:2:2:2:1:1:10:1:3:7:1:1:5:4:1:1:1:1:1:2:2:5:1:1:1:2:2:1:1:10:1:3:4:2:1:1:3:5:2:2:3:2:2:1:1:5:2:1:11:1:1:10:1:3:3:2:2:2:1:2:1:1:2:10:1:3:3:1:4:1:1:1:1:1:10:1:3:3:1:1:1:1:2:2:1:2:1:1:1:1:2:5:2:2:2:3:3:2:1:1:10:1:3:2:2:3:2:2:1:3:6:1:2:2:1:1:2:6:11:1:1:1:10:1:3:2:1:3:1:2:1:3:1:10:1:3:2:1:3:1:1:2:1:1:1:10:1:3:2:1:1:1:2:1:6:2:1:1:1:1:1:4:1:2:2:2:2:1:1:2:1:1:1:10:1:3:1:4:1:1:1:2:1:8:1:1:1:1:1:1:1:1:3:1:3:1:1:1:1:11:1:10:1:3:1:2:4:1:1:1:2:1:2:1:3:3:4:1:1:2:2:4:1:1:2:1:10:1:3:1:2:1:1:5:1:1:2:1:2:1:1:1:2:2:3:3:1:3:2:2:1:1:1:10:1:3:1:1:3:1:2:3:1:2:10:1:3:1:1:3:1:1:2:11:1:1:10:1:3:1:1:1:4:5:3:2:3:1:2:3:1:1:1:1:2:2:1:2:1:2:1:10:1:3:1:1:1:1:2:1:1:4:3:1:4:4:2:1:1:3:5:1:1:2:1:10:1:2:6:1:5:1:2:2:1:2:1:1:3:3:2:1:1:2:1:1:2:11:1:1:10:1:2:4:5:2:1:1:1:2:2:1:1:3:2:2:1:1:2:1:5:1:1:2:1:10:1:2:4:1:2:1:2:1:1:4:2:1:1:2:2:1:4:1:1:4:2:1:2:1:1:10:1:2:3:3:1:4:1:2:1:1:4:10:1:2:3:1:1:1:11:1:10:1:2:3:1:4:5:1:1:3:1:2:1:2:1:2:3:1:1:1:1:2:2:2:1:1:10:1:2:3:1:1:2:1:1:1:1:1:2:1:1:1:2:1:1:2:2:5:2:3:1:1:2:1:1:1:10:1:2:2:4:1:2:1:1:1:3:1:3:4:1:2:4:4:1:1:1:1:1:11:1:10:1:2:2:2:3:10:1:2:2:2:3:9:1:1:2:1:1:10:1:2:2:2:1:1:2:3:6:1:1:1:1:1:2:1:1:1:2:3:2:1:1:1:1:11:1:1:10:1:2:2:1:2:5:2:3:1:1:1:3:1:7:2:1:1:1:3:1:2:1:1:10:1:2:2:1:1:3:1:1:3:1:3:2:1:2:1:1:2:4:1:1:5:1:1:1:11:1:10:1:2:2:1:1:1:1:1:2:1:1:1:3:5:2:2:4:2:1:3:1:2:1:2:1:1:10:1:2:1:4:2:3:2:1:1:2:1:1:1:2:1:1:1:1:1:1:2:2:1:1:2:1:1:1:11:1:1:10:1:2:1:3:2:2:1:1:2:1:1:2:3:2:1:1:1:1:1:1:1:1:3:2:5:1:1:1:1:1:1:10:1:2:1:2:5:2:1:1:10:1:2:1:2:5:15:1:1:1:1:1:1:1:10:1:2:1:2:2:1:2:4:2:1:2:2:3:1:2:2:2:1:2:4:3:1:1:1:1:10:1:2:1:2:1:1:4:1:2:4:2:5:3:1:1:2:1:1:3:1:2:11:1:1:10:1:2:1:1:4:3:2:10:1:2:1:1:4:3:2:2:1:2:1:1:10:1:2:1:1:2:5:6:1:3:1:1:1:3:1:2:1:1:1:2:1:3:1:11:1:1:10:1:2:1:1:2:1:1:4:1:1:1:2:1:6:3:1:5:1:1:1:2:3:1:1:1:1:10:1:2:1:1:1:2:4:5:1:1:1:3:1:2:1:3:2:2:4:2:1:11:1:1:10:1:2:1:1:1:1:2:3:2:2:3:3:1:2:1:2:6:1:1:2:1:1:1:1:2:1:10:1:1:10:1:1:10:1:1:10:1:1:13:1:1:1:1:1:10:1:1:6:1:2:3:2:1:4:2:1:3:1:3:3:1:1:1:1:2:1:1:2:1:10:1:1:5:1:3:1:4:1:1:3:2:4:1:1:1:1:2:3:3:1:1:11:1:1:10:1:1:4:3:1:7:1:4:1:5:2:4:2:1:5:1:1:1:1:1:1:10:1:1:4:1:3:2:3:2:5:2:1:1:1:1:1:1:2:2:2:15:1:1:1:1:1:1:1:10:1:1:4:1:1:1:1:1:3:1:1:4:4:2:2:1:1:3:1:1:1:1:1:1:1:2:1:1:1:10:1:1:3:3:2:1:1:1:1:1:3:1:2:1:1:1:1:2:1:4:2:2:1:1:1:1:1:11:1:1:10:1:1:3:2:1:4:1:4:2:2:4:3:3:1:5:1:1:1:1:1:2:1:10:1:1:3:1:3:2:1:2:2:1:1:5:1:1:2:1:2:1:4:1:1:3:11:1:1:1:10:1:1:3:1:1:3:2:2:2:1:1:3:4:1:1:2:2:2:1:3:3:1:2:1:1:10:1:1:3:1:1:1:1:1:1:1:1:4:1:10:1:1:3:1:1:1:1:1:1:1:1:1:11:1:10:1:1:2:4:1:3:1:1:1:2:2:1:4:2:2:2:2:1:1:1:1:3:1:2:1:1:1:10:1:1:2:3:1:2:4:1:1:1:2:3:3:2:1:1:1:9:12:1:1:1:1:10:1:1:2:2:3:1:1:1:3:1:1:2:4:2:3:3:1:2:1:1:2:1:1:2:1:1:1:10:1:1:2:2:1:2:2:4:2:2:1:3:1:3:1:1:1:1:2:1:1:1:2:1:1:1:1:11:1:10:1:1:2:1:5:2:2:3:1:2:1:1:1:1:2:10:1:1:1:1:1:2:1:1:10:1:1:2:1:2:6:2:4:2:1:1:1:3:1:1:9:13:1:1:1:1:1:10:1:1:2:1:2:1:1:1:5:2:2:2:2:1:1:1:1:3:1:1:1:1:2:2:2:1:1:2:1:10:1:1:2:1:1:2:2:1:1:1:1:1:3:2:1:1:1:4:1:4:2:1:2:1:1:1:11:1:1:1:10:1:1:2:1:1:1:1:3:1:1:1:2:2:3:1:1:1:4:2:1:1:1:1:2:1:1:1:1:1:2:1:1:10:1:1:1:6:1:1:1:1:1:1:1:1:5:1:1:1:1:10:1:1:1:2:11:1:1:10:1:1:1:4:2:1:4:2:4:4:2:1:1:4:1:2:6:2:1:10:1:1:1:3:3:4:1:2:1:1:1:1:2:2:1:3:2:1:2:1:1:1:2:2:11:1:1:1:10:1:1:1:3:1:3:2:1:2:4:1:1:1:2:1:2:1:1:1:1:3:1:1:2:1:1:2:1:1:1:10:1:1:1:2:6:2:3:1:1:1:1:3:1:1:3:1:2:2:1:1:2:1:1:1:1:1:11:1:10:1:1:1:2:3:1:1:1:1:1:1:2:3:5:6:4:2:6:1:1:1:1:1:1:1:10:1:1:1:2:2:1:1:1:2:3:1:1:1:3:1:1:1:1:1:1:1:1:5:5:1:11:1:1:1:10:1:1:1:2:1:2:1:2:2:1:1:6:2:2:3:1:2:1:3:1:1:1:1:2:2:1:10:1:1:1:2:1:1:1:1:4:2:1:1:1:2:1:2:1:3:1:1:4:2:6:11:1:10:1:1:1:1:4:2:1:2:1:2:2:3:1:4:4:8:4:1:1:1:1:1:10:1:1:1:1:3:1:2:2:3:3:1:2:5:2:1:3:1:2:1:2:1:1:1:11:1:1:10:1:1:1:1:2:2:3:1:1:2:1:3:1:1:2:4:1:1:1:1:1:1:2:4:1:2:1:1:1:10:1:1:1:1:2:1:1:3:3:1:1:1:1:3:1:3:1:1:3:2:1:1:1:1:2:1:12:1:1:1:1:10:1:1:1:1:1:3:4:1:6:2:2:1:2:1:2:7:2:3:2:1:10:1:1:1:1:1:2:1:5:1:4:1:5:5:1:2:2:2:1:1:1:1:1:1:11:1:10:1:1:1:1:1:1:2:6:4:1:1:1:3:2:3:2:5:3:2:2:1:10:1:1:1:1:1:1:1:1:10:1:1:1:1:1:1:1:1:9:6:11:1:9:9:9:9:9:6:2:1:9:8:1:6:3:5:1:2:1:3:2:4:1:3:12:1:1:1:1:9:7:2:5:1:3:3:1:1:1:1:1:4:1:1:2:1:1:3:2:2:1:1:1:9:7:1:1:4:1:4:1:2:2:1:2:1:1:1:6:3:2:1:11:1:1:1:9:6:3:3:2:3:3:1:1:3:1:6:2:1:3:1:2:1:2:1:1:9:6:2:1:3:1:1:1:2:1:1:2:2:7:1:1:3:2:15:1:1:1:1:1:1:1:9:6:1:2:2:2:2:1:2:2:1:3:1:1:4:1:1:1:4:2:3:2:1:9:6:1:1:1:2:1:1:3:1:1:1:1:3:2:3:1:2:1:1:4:1:1:1:1:1:1:11:1:9:5:4:1:3:2:2:2:1:1:2:1:3:1:2:1:1:2:2:2:1:2:1:1:2:1:9:5:3:1:1:2:1:1:2:1:2:1:1:2:3:1:2:2:2:1:1:1:1:3:2:11:1:9:5:2:2:1:1:1:3:2:1:1:1:2:1:1:3:4:2:1:1:2:1:1:1:1:1:2:1:1:9:5:1:7:2:3:1:1:1:5:1:2:4:2:1:2:3:1:11:1:9:5:1:2:3:3:4:4:2:2:1:4:1:2:2:1:2:1:3:1:1:1:1:9:5:1:1:3:8:1:3:2:2:2:1:6:3:1:13:1:1:1:1:1:9:5:1:1:1:2:2:1:1:1:2:1:2:1:1:1:1:1:2:2:2:1:3:1:4:1:2:1:1:1:9:4:6:1:1:1:1:1:1:1:2:1:1:2:1:1:1:1:9:4:1:1:1:11:1:9:4:4:1:2:4:1:8:1:3:1:2:3:1:1:2:2:1:2:1:1:1:9:4:3:2:1:3:4:2:1:1:4:3:1:1:1:2:1:2:1:1:3:1:11:1:9:4:3:1:1:1:1:1:5:1:1:2:4:1:2:3:1:2:4:2:1:2:1:1:1:9:4:2:2:4:1:2:1:1:3:1:2:4:1:1:1:1:1:1:1:2:1:1:1:13:1:1:1:1:1:9:4:2:1:3:3:4:1:2:2:2:5:1:1:1:1:1:5:2:2:1:1:9:4:2:1:1:2:1:1:1:4:2:5:1:3:4:1:2:1:1:1:1:3:11:1:9:4:1:4:2:2:2:1:5:6:1:1:3:7:1:1:1:1:2:1:9:4:1:3:1:1:1:9:4:1:3:1:1:1:8:13:1:1:1:1:1:9:4:1:2:1:4:2:1:1:1:1:4:1:3:1:1:1:1:1:1:3:2:2:1:3:1:1:1:1:9:4:1:1:4:2:3:4:2:1:1:1:1:1:6:1:1:3:1:1:2:1:11:1:9:4:1:1:2:1:4:1:1:1:2:2:2:1:3:1:1:3:2:1:1:3:2:2:2:1:9:4:1:1:1:2:1:3:1:1:1:2:5:1:1:1:4:1:1:1:1:1:1:1:1:3:11:1:1:9:3:9:3:9:3:9:3:3:2:1:9:3:5:2:2:2:1:1:1:3:1:9:3:5:1:2:1:11:1:9:3:4:2:2:9:3:4:2:2:8:4:1:1:1:1:9:3:4:1:1:1:1:2:2:1:2:1:1:1:3:2:1:1:4:2:1:1:2:2:2:11:1:9:3:3:2:3:1:1:3:1:1:2:2:1:1:1:1:1:2:3:4:1:4:2:1:1:9:3:3:1:2:4:2:1:3:1:2:1:1:1:1:3:1:2:2:1:5:12:1:1:1:9:3:3:1:1:1:1:2:1:1:1:1:1:2:4:1:1:4:2:5:1:1:2:2:2:1:9:3:2:3:3:1:2:1:1:3:3:2:1:2:1:1:2:1:1:7:1:1:11:1:9:3:2:2:2:3:4:1:1:3:3:3:2:1:1:1:3:1:1:1:2:2:2:1:9:3:2:2:1:1:1:1:2:1:1:1:1:3:1:4:1:1:3:2:1:2:3:3:1:11:1:9:3:2:1:2:2:2:5:2:9:3:2:1:2:2:2:1:1:2:1:9:3:2:1:1:2:2:1:2:3:1:1:2:2:5:1:1:1:1:1:1:1:2:1:1:3:11:1:9:3:1:8:1:1:3:6:2:2:2:1:4:6:2:2:1:9:3:1:4:1:5:1:1:2:1:1:2:2:1:4:1:1:2:2:7:11:1:9:3:1:3:2:1:1:1:2:2:1:1:3:1:1:2:1:2:2:1:2:2:4:1:2:2:1:9:3:1:2:4:1:3:1:1:1:3:1:2:2:1:3:1:1:8:2:1:11:1:9:3:1:2:2:1:1:2:1:1:3:1:1:3:2:1:1:1:1:1:3:1:1:1:1:3:1:1:1:2:1:9:3:1:2:1:1:2:2:2:2:1:2:1:1:1:2:4:1:1:3:3:2:2:1:1:11:1:9:3:1:1:4:1:2:3:4:2:1:2:3:1:2:1:3:1:4:4:1:1:1:1:9:3:1:1:2:3:2:3:3:5:1:1:5:1:1:1:2:2:1:14:1:1:1:1:1:9:3:1:1:2:1:1:1:2:2:1:6:1:2:1:3:3:4:2:1:1:1:1:1:2:1:9:3:1:1:1:2:2:2:1:1:2:1:3:1:3:4:1:2:2:2:2:3:12:1:1:1:9:3:1:1:1:1:2:1:1:2:4:1:1:1:2:5:2:2:3:2:1:2:1:1:1:2:1:9:2:8:1:1:1:6:2:1:2:4:5:1:1:3:1:1:1:1:11:1:9:2:5:6:2:1:2:2:3:3:1:3:2:1:1:1:3:1:1:3:1:1:1:9:2:5:1:1:2:4:3:1:1:3:1:5:2:2:2:1:1:1:1:1:2:11:1:9:2:4:2:3:1:1:1:2:1:3:1:1:1:4:2:1:4:1:1:1:4:2:1:1:9:2:4:1:2:1:2:1:1:3:1:1:4:1:2:4:2:1:3:2:1:1:12:1:1:1:9:2:3:4:9:2:3:4:9:2:3:2:1:1:9:2:3:2:4:3:1:1:1:1:1:1:1:2:2:1:2:2:1:1:2:1:5:12:1:1:1:9:2:3:2:1:1:1:2:3:1:1:1:1:2:1:2:1:1:2:1:2:3:1:2:3:1:2:1:1:9:2:3:1:2:1:5:5:1:1:2:1:1:1:1:2:1:2:1:1:5:1:1:1:11:1:9:2:3:1:1:1:3:1:1:4:1:1:1:2:1:4:2:1:2:1:1:2:2:1:2:2:1:1:9:2:2:5:1:1:1:3:4:2:1:1:5:4:4:1:1:2:1:1:11:1:9:2:2:3:2:2:2:1:1:1:1:1:1:1:2:2:2:1:2:2:2:7:3:1:1:1:9:2:2:2:4:1:1:3:3:2:1:1:1:3:1:2:2:1:1:2:1:1:3:12:1:1:1:9:2:2:2:1:4:1:1:3:2:1:1:1:2:1:1:2:3:1:1:1:1:4:2:1:2:1:1:9:2:2:1:9:2:2:1:9:2:2:1:7:12:1:1:1:9:2:2:1:3:1:1:2:2:1:5:1:1:1:1:4:3:4:1:1:1:2:3:1:1:1:9:2:2:1:2:1:1:2:2:2:1:1:1:3:1:4:1:3:4:1:1:1:1:1:1:1:1:11:1:9:2:2:1:1:2:1:5:1:3:1:1:1:1:5:1:1:1:1:2:1:3:1:1:1:3:1:1:1:9:2:2:1:1:1:1:2:1:3:1:3:5:1:2:5:2:3:3:1:1:1:11:1:9:2:1:5:2:3:2:1:3:1:2:1:2:6:1:4:1:1:3:2:1:1:9:2:1:4:1:3:1:2:2:2:1:2:2:1:1:4:4:2:2:1:2:12:1:1:1:9:2:1:3:2:3:3:3:1:1:1:1:5:2:3:1:1:2:1:2:2:1:2:1:1:9:2:1:3:1:1:4:1:3:1:1:1:1:3:1:2:1:9:2:1:1:12:1:1:1:9:2:1:2:3:4:2:4:1:1:3:1:1:2:2:4:4:1:2:1:1:2:1:9:2:1:2:2:1:4:2:3:3:5:2:2:2:4:1:3:1:1:11:1:1:9:2:1:2:1:2:4:1:2:3:1:4:3:1:2:7:1:1:2:1:1:2:1:9:2:1:2:1:1:1:3:3:1:9:2:1:2:1:1:1:3:3:1:1:1:1:11:1:9:2:1:1:4:3:1:2:1:1:1:5:1:2:1:2:1:3:1:1:3:4:1:2:1:9:2:1:1:3:1:2:3:1:1:1:5:1:4:2:2:5:5:1:11:1:1:9:2:1:1:2:2:2:1:2:1:3:2:2:1:5:9:2:1:1:1:2:1:1:9:2:1:1:2:1:1:1:3:5:7:4:1:3:1:2:2:1:2:1:11:1:1:9:2:1:1:1:3:1:2:1:1:2:2:2:3:1:2:2:1:1:2:1:2:1:3:3:1:2:1:9:2:1:1:1:2:1:1:1:1:2:2:1:3:1:1:3:3:4:2:1:1:5:1:1:11:1:9:2:1:1:1:1:1:4:4:3:2:2:2:4:2:2:1:2:3:1:1:3:1:1:1:9:1:9:1:9:1:9:1:10:1:11:1:9:1:6:2:2:3:3:2:9:1:6:2:1:4:1:1:1:1:1:9:1:5:3:1:1:1:9:1:5:3:1:1:2:1:1:1:1:2:1:1:11:1:9:1:5:1:1:2:5:3:1:1:1:2:1:1:1:3:1:7:2:2:2:1:1:9:1:4:3:1:3:1:1:7:1:1:1:2:1:9:1:3:1:1:11:1:9:1:4:2:1:1:1:1:1:1:1:3:1:1:2:9:1:4:1:4:2:1:1:1:9:1:4:1:1:3:1:3:2:3:3:1:1:1:2:2:1:1:1:4:3:1:1:1:11:1:9:1:3:5:1:2:2:3:1:1:1:4:3:9:1:2:2:2:1:1:9:1:3:3:1:2:3:1:4:1:3:1:1:1:1:1:1:1:3:1:2:1:2:13:1:1:1:1:1:9:1:3:2:2:1:1:3:2:1:1:2:1:5:2:1:1:1:1:4:1:2:2:1:2:1:1:9:1:3:1:5:2:3:2:1:1:1:5:1:2:1:4:1:1:2:1:2:1:11:1:1:9:1:3:1:2:2:1:1:4:3:3:1:1:2:2:3:2:2:2:3:3:1:1:1:1:9:1:3:1:1:2:2:1:1:1:3:1:1:1:1:4:2:1:1:2:1:2:2:1:2:1:2:11:1:1:9:1:2:9:1:2:9:1:2:9:1:2:1:2:1:1:1:9:1:2:4:2:1:5:2:2:2:5:1:1:1:1:4:2:3:1:1:11:1:1:9:1:2:3:2:2:1:1:2:6:6:1:1:2:1:2:4:3:1:1:2:1:9:1:2:2:6:2:1:1:3:1:3:2:4:3:1:5:1:1:1:1:1:11:1:9:1:2:2:2:2:1:1:1:4:1:1:2:3:2:1:1:1:1:5:1:1:1:1:1:1:1:2:1:1:9:1:2:2:1:2:1:2:1:1:1:1:2:5:1:1:1:1:1:1:1:3:2:3:2:2:1:11:1:9:1:2:1:5:3:2:3:2:1:3:1:4:1:1:2:1:3:3:1:1:1:2:1:9:1:2:1:3:1:2:1:1:2:3:5:3:2:2:1:2:1:6:2:11:1:1:9:1:2:1:2:1:3:2:1:1:1:4:2:1:1:4:1:1:2:1:1:1:1:1:6:1:1:1:1:1:1:1:9:1:2:1:1:3:2:2:4:1:1:1:4:2:2:1:4:1:7:1:1:11:1:9:1:2:1:1:1:4:3:1:1:1:1:1:2:2:1:3:2:1:4:5:2:1:1:2:1:9:1:2:1:1:1:1:1:2:2:1:6:1:1:1:1:2:1:1:2:5:1:2:2:12:1:1:1:1:9:1:1:5:3:1:2:2:1:2:2:3:1:1:3:3:2:1:1:2:2:3:1:1:1:1:9:1:1:4:2:1:4:3:2:3:1:2:4:5:1:2:2:2:11:1:1:1:9:1:1:3:4:2:1:3:3:1:3:2:1:2:2:1:1:1:1:2:1:1:3:2:1:1:1:9:1:1:3:2:1:1:1:1:3:1:2:1:2:1:3:2:1:9:1:1:2:1:1:11:1:9:1:1:3:1:1:1:3:3:1:2:1:1:3:2:1:1:4:4:3:1:1:3:1:1:1:1:9:1:1:2:3:3:1:1:3:2:1:3:2:4:1:1:1:1:1:1:1:4:2:1:1:11:1:9:1:1:2:2:2:1:1:3:2:2:1:3:9:1:1:2:2:1:3:1:2:1:1:9:1:1:2:1:3:3:2:1:1:1:3:4:4:1:4:1:1:1:2:3:1:1:11:1:9:1:1:2:1:1:3:2:9:1:1:2:1:1:3:2:8:1:1:2:1:1:9:1:1:1:9:1:1:1:9:1:1:1:9:1:1:1:12:1:1:1:1:9:1:1:1:4:1:1:2:2:3:4:2:1:1:1:3:1:1:2:1:1:6:1:2:1:1:9:1:1:1:3:1:1:2:6:1:2:2:2:3:2:1:3:2:1:1:2:1:2:11:1:9:1:1:1:2:2:2:1:1:2:9:1:1:1:2:2:2:1:1:2:5:1:1:2:1:9:1:1:1:2:1:1:2:3:1:1:4:1:1:1:1:1:1:1:5:4:2:1:2:1:1:11:1:1:9:1:1:1:1:3:3:2:1:1:1:1:2:1:3:4:5:4:1:4:1:2:1:9:1:1:1:1:2:1:4:1:4:5:2:2:1:3:2:6:1:1:1:1:11:1:1:9:1:1:1:1:1:2:5:4:1:1:3:2:2:2:8:6:2:1:9:1:1:1:1:1:1:1:9:1:1:1:1:1:1:1:9:1:1:1:1:1:1:1:1:11:1:1:1:8:8:8:8:8:8:3:1:1:1:1:8:7:1:5:3:4:1:2:1:2:2:4:1:1:1:1:2:2:1:11:1:1:1:8:6:2:4:1:3:2:1:1:1:1:1:1:2:1:1:1:1:2:2:1:1:3:1:1:1:2:1:1:8:6:1:1:3:1:3:3:1:6:2:3:3:19:1:1:1:1:1:1:1:1:1:1:1:8:5:3:2:2:3:1:1:1:1:1:3:1:1:4:1:1:1:1:3:4:1:1:2:1:1:8:5:2:1:2:1:1:1:1:1:3:1:2:1:1:2:1:5:3:2:1:2:2:11:1:1:8:5:1:2:1:2:1:3:6:2:2:4:4:1:1:1:2:2:3:1:1:1:1:8:5:1:1:1:1:1:1:2:1:1:1:8:5:1:1:1:1:1:1:2:1:1:2:1:11:1:8:4:3:4:1:2:2:1:4:1:1:3:1:1:2:2:1:1:2:2:5:1:1:1:1:1:1:8:4:2:3:5:2:1:1:1:1:5:1:1:6:2:2:2:13:1:1:1:1:1:8:4:2:1:2:1:1:7:1:2:4:3:2:5:3:1:3:2:1:8:4:1:3:2:4:2:3:1:1:8:4:1:3:2:1:12:1:1:1:1:8:4:1:2:1:1:2:1:2:3:2:3:8:4:1:2:1:1:1:2:2:1:8:4:1:1:1:4:2:1:1:2:1:1:1:3:1:1:2:1:3:1:1:1:1:1:1:1:3:1:11:1:8:3:6:2:1:2:4:1:1:5:1:4:1:3:1:1:1:1:3:2:1:1:8:3:4:1:2:4:2:2:1:3:1:1:1:4:3:1:2:2:1:2:12:1:1:1:8:3:3:2:1:1:1:4:4:3:1:1:5:8:3:1:2:1:1:8:3:2:4:4:2:1:8:3:2:4:4:1:14:1:1:1:1:1:1:8:3:2:2:1:2:8:3:2:2:1:2:9:1:1:1:1:1:1:2:1:8:3:2:1:1:8:3:2:1:1:8:3:2:1:1:3:12:1:1:1:8:3:1:5:1:2:4:1:3:8:3:1:5:1:2:3:1:1:1:8:3:1:3:1:1:2:2:1:1:3:4:2:1:2:1:2:2:2:1:1:1:2:1:1:1:11:1:8:3:1:2:1:2:3:1:4:1:2:1:1:1:1:3:1:1:1:2:1:1:1:5:1:2:1:1:8:3:1:1:3:1:3:4:1:1:5:2:1:6:1:4:4:11:1:1:8:3:1:1:1:3:3:1:3:2:2:1:2:3:4:1:1:1:2:1:1:2:1:1:1:2:1:8:3:1:1:1:1:1:1:2:1:8:3:1:1:1:1:1:1:2:1:7:13:1:1:1:1:8:2:5:2:1:2:2:1:3:1:4:3:2:1:1:2:2:2:4:2:2:1:8:2:4:1:6:1:3:2:1:1:1:4:1:1:1:1:1:1:2:2:1:1:1:3:11:1:8:2:3:3:2:2:3:1:1:3:1:1:1:4:1:1:2:1:3:2:1:1:3:2:1:8:2:3:1:3:2:2:2:1:1:4:5:1:1:3:3:2:1:1:3:1:11:1:8:2:2:8:2:2:8:2:2:8:2:1:3:2:1:8:2:2:3:1:1:3:5:3:2:3:4:1:1:1:2:6:1:11:1:1:8:2:2:2:1:1:4:1:1:1:2:1:2:3:1:1:1:4:2:5:4:2:1:1:8:2:2:1:2:2:1:8:2:2:1:2:2:1:8:2:1:1:12:1:1:1:8:2:2:1:1:1:3:1:1:2:2:1:2:1:1:2:1:1:4:1:2:8:1:1:2:1:8:2:1:4:4:1:1:1:1:1:1:2:3:1:1:2:1:1:8:2:1:1:1:11:1:1:8:2:1:3:1:5:1:1:1:2:1:4:1:2:2:3:2:4:3:1:2:2:1:8:2:1:2:2:5:3:1:1:1:3:1:2:4:2:2:3:1:1:4:11:1:1:8:2:1:2:1:1:5:1:1:1:1:2:1:1:2:1:2:2:1:2:5:1:2:3:1:2:1:8:2:1:1:3:3:5:2:5:1:1:1:1:1:1:2:4:1:2:1:1:1:1:11:1:1:8:2:1:1:2:1:3:1:1:1:1:2:1:1:3:1:1:3:5:1:2:3:2:1:1:2:1:1:8:2:1:1:1:2:2:1:2:3:4:6:4:1:1:6:1:1:1:1:11:1:1:8:2:1:1:1:1:1:1:2:8:2:1:1:1:1:1:1:2:8:1:4:1:2:1:8:1:6:1:1:1:3:3:2:1:1:2:1:1:1:2:2:2:1:3:2:1:2:1:1:11:1:8:1:4:5:1:2:1:1:3:2:1:1:1:1:5:2:2:2:1:1:1:3:1:2:1:8:1:4:1:2:3:1:8:1:4:1:2:3:1:7:2:11:1:1:8:1:3:3:6:1:1:1:1:2:1:2:3:3:1:1:2:5:1:1:1:1:2:1:1:8:1:3:2:1:1:5:1:1:3:3:1:1:1:1:8:1:3:1:3:1:11:1:8:1:3:1:1:5:2:1:2:4:2:3:1:1:2:1:1:3:3:2:1:2:1:1:1:8:1:2:5:2:1:2:1:2:3:1:1:1:1:1:1:3:2:1:1:1:2:3:1:2:11:1:1:8:1:2:3:1:2:1:2:8:1:2:3:1:2:1:2:7:1:2:1:1:1:8:1:2:2:1:4:2:2:1:1:8:1:2:2:1:4:2:2:1:1:1:11:1:1:8:1:2:1:3:8:1:2:1:3:8:1:2:1:3:3:1:1:2:1:8:1:2:1:2:1:1:1:1:2:2:1:2:1:5:1:1:1:2:1:1:2:1:2:3:2:11:1:1:1:8:1:2:1:1:1:2:1:1:2:3:2:2:1:1:4:3:1:3:2:6:1:2:1:1:8:1:1:5:1:1:1:1:1:1:4:1:1:1:8:1:1:4:17:1:1:1:1:1:1:1:1:1:8:1:1:3:2:3:3:2:1:3:1:6:2:2:2:1:2:1:6:1:1:1:1:1:1:1:8:1:1:2:5:1:3:2:1:2:2:1:4:1:1:1:2:1:2:1:2:1:1:2:1:11:1:1:8:1:1:2:2:1:1:2:1:1:1:2:1:2:1:3:2:2:3:2:1:1:5:3:2:1:8:1:1:2:1:1:1:3:2:1:1:2:2:1:2:1:1:4:1:1:1:1:1:1:4:2:1:11:1:1:1:8:1:1:1:3:2:2:1:1:3:1:1:2:2:1:3:3:8:1:1:1:1:2:1:1:1:8:1:1:1:2:1:3:1:8:1:1:1:2:1:3:1:8:1:1:1:1:1:1:11:1:8:1:1:1:1:2:4:4:1:3:2:1:3:1:1:1:3:3:2:3:1:3:1:1:1:1:8:1:1:1:1:1:1:8:1:1:1:1:1:1:8:1:1:1:1:1:2:1:1:1:1:1:1:11:1:1:7:7:7:7:7:7:6:1:2:1:7:6:1:4:3:3:1:2:1:1:2:3:2:1:1:1:4:1:2:1:2:11:1:7:5:2:3:1:2:7:5:2:3:1:2:7:1:1:1:2:1:7:5:1:1:2:1:3:1:1:1:2:2:2:1:2:2:1:1:1:1:3:1:2:2:2:11:1:1:7:4:3:1:2:2:1:1:3:7:4:3:1:2:2:1:1:2:1:2:1:1:1:7:4:1:7:4:1:7:4:1:7:4:1:1:11:1:7:4:1:1:3:1:1:1:6:1:2:2:2:1:2:1:2:4:2:4:1:2:1:7:3:5:1:1:1:1:1:2:1:2:1:1:1:7:3:4:7:11:1:7:3:3:1:1:3:1:2:2:5:2:1:1:4:7:3:1:1:2:1:1:7:3:2:1:3:3:1:1:1:1:1:2:1:2:3:1:2:1:7:3:1:1:1:11:1:7:3:1:3:4:1:1:1:1:4:1:2:3:2:4:2:2:1:4:1:1:2:1:7:3:1:1:7:3:1:1:7:3:1:1:7:2:3:10:1:1:7:3:1:1:1:1:4:4:2:1:3:2:1:1:1:3:1:1:3:3:2:1:1:1:2:1:7:2:4:4:1:2:1:1:1:1:5:1:3:2:2:1:4:2:1:1:3:2:1:7:2:3:2:2:1:1:1:1:1:2:2:1:2:2:7:2:3:2:1:2:1:10:1:1:7:2:2:4:1:1:2:6:1:1:1:1:1:2:2:2:1:1:4:1:5:2:1:1:7:2:2:1:4:1:1:2:1:2:3:4:1:5:7:2:1:2:2:1:1:7:2:1:7:2:1:7:2:1:7:2:1:6:2:1:10:1:7:2:1:2:7:2:1:2:7:2:1:2:7:2:1:1:1:2:1:1:7:2:1:1:7:2:1:1:7:2:1:1:7:2:1:1:3:2:1:1:1:7:2:1:1:1:4:2:3:3:1:3:7:2:1:1:1:4:2:1:1:10:1:1:7:1:7:1:7:1:7:1:7:1:6:3:1:1:1:1:7:1:4:2:2:1:3:2:1:1:3:5:1:1:1:2:1:2:2:1:1:1:1:1:1:2:1:1:7:1:3:2:3:4:2:1:5:1:1:1:1:1:3:1:2:2:1:2:1:1:1:1:1:10:1:7:1:3:1:1:1:2:1:3:4:1:1:2:3:2:2:4:6:3:1:2:1:7:1:2:2:3:1:4:1:1:2:1:1:1:2:1:7:1:2:2:3:1:1:1:2:1:1:7:1:2:1:2:3:1:1:2:7:1:2:1:2:3:1:1:2:6:1:1:10:1:1:7:1:1:7:1:1:7:1:1:7:1:1:7:1:1:2:1:2:1:1:7:1:1:3:1:2:1:5:2:2:1:2:4:3:7:1:1:2:2:1:2:1:7:1:1:2:1:2:2:1:1:3:1:7:1:1:2:1:2:2:1:1:3:1:3:1:10:1:1:7:1:1:1:2:3:4:3:1:4:1:2:2:2:1:1:1:1:1:5:1:2:2:1:1:1:7:1:1:1:1:1:7:1:1:1:1:1:7:1:1:1:1:1:6:7:1:1:1:1:1:1:1:1:6:6:6:6:6:6:10:11:1:1:1:1:6:5:1:3:3:2:1:1:4:2:3:1:1:1:1:2:1:1:1:3:1:2:1:1:2:1:1:6:4:2:2:1:2:1:4:1:1:1:3:2:1:1:1:1:2:1:1:2:2:3:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:4:1:1:1:1:2:1:1:6:3:2:1:10:1:6:3:2:3:1:1:1:3:6:3:2:3:1:1:1:3:5:3:2:1:6:3:1:2:2:3:5:1:1:3:1:1:1:1:2:1:3:2:2:1:1:4:1:2:1:6:2:6:2:6:2:6:2:6:2:5:12:1:1:1:1:6:2:3:1:1:3:2:1:1:1:6:2:3:1:1:3:2:1:1:1:6:1:1:1:1:1:1:1:6:2:2:1:1:3:3:1:4:6:2:2:1:1:3:3:4:3:1:1:1:1:6:2:1:2:1:6:2:1:2:1:6:2:1:2:1:6:2:1:2:1:10:1:6:2:1:1:1:1:2:6:2:1:1:1:1:2:6:2:1:1:1:1:2:4:1:2:1:1:6:1:4:1:1:1:1:3:1:1:6:1:4:1:1:1:1:3:1:1:5:2:1:2:1:6:1:2:3:6:1:2:3:6:1:2:3:6:1:1:12:1:1:1:1:1:6:1:2:1:1:2:2:1:3:1:1:1:1:3:4:2:3:1:6:1:3:1:2:1:1:6:1:1:2:2:6:1:1:2:2:6:1:1:2:2:6:1:1:1:2:2:1:1:6:1:1:1:1:6:1:1:1:1:6:1:1:1:1:6:1:1:1:1:5:1:10:1:1:1:5:5:5:5:5:5:5:5:5:1:1:2:1:1:5:4:1:2:3:1:1:1:2:1:1:3:2:1:5:4:1:2:2:3:2:2:1:5:3:2:1:1:1:2:1:2:5:3:2:1:1:1:2:1:2:5:3:1:1:1:10:1:5:2:4:1:1:1:2:2:1:1:5:2:4:1:1:1:2:2:1:1:4:2:1:2:1:5:2:1:3:5:2:1:3:5:2:1:3:5:2:1:3:1:1:2:1:1:5:1:5:1:5:1:5:1:5:1:5:1:5:1:3:1:1:10:1:5:1:2:2:1:1:1:3:2:5:1:2:2:1:1:1:3:2:5:1:1:3:1:2:1:5:1:1:1:5:1:1:1:5:1:1:1:5:1:1:1:5:1:1:1:4:1:1:2:1:1:4:4:4:4:4:4:4:4:4:4:4:1:1:1:10:1:4:3:1:1:2:1:4:3:1:1:2:1:4:3:1:1:2:1:4:2:2:3:1:1:1:4:2:1:1:2:4:2:1:1:2:4:2:1:1:2:4:2:1:1:2:3:2:2:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:4:1:1:1:2:10:1:1:3:3:3:3:3:3:3:3:3:3:3:3:3:3:1:1:2:1:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:3:1:2:1:1:2:1:1:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:1:2:1:10:1:1:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:2:5:5:2:2:6:2:6:2:2:3:3:4:2:2:2:4:2:4:2:2:2:3:2:2:3:6:2:2:3:2:3:2:3:2:4:3:5:7:5:3:5:2:2:3:2:4:2:3:2:6:3:3:3:2:2:2:2:2:2:3:3:2:5:3:2", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":86,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":859,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.788425664621422e-83,0,0,-1.4902745963168196e-98,0,0]]] +Shrunk 27238 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + + +--- +## Summary +- Passed: 9 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 10 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✗ 14:night-discharge-defers-to-better-price +FAIL + +### Counterexample +```json +Error: Property failed after 1 tests +{ seed: 12345, path: "0:0:0:0:0:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":100,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400},0.23,0.005,250]] +Shrunk 9 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 1 tests +{ seed: 12345, path: "0:0:0:0:0:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":100,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400},0.23,0.005,250]] +Shrunk 9 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 14:night-discharge-defers-to-better-price + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✗ 14:night-discharge-defers-to-better-price +FAIL + +### Counterexample +```json +Error: Property failed after 1 tests +{ seed: 12345, path: "0:0:0:0:0:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":100,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400},0.23,0.005,250]] +Shrunk 9 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 1 tests +{ seed: 12345, path: "0:0:0:0:0:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":100,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400},0.23,0.005,250]] +Shrunk 9 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 14:night-discharge-defers-to-better-price + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 15 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:1:2:2:0:0:0:0:0:0:0:0:0:0:1:3:2:0:0:0:3:3:0:5:1:2:0:1:0:0:0:0:1:0:1:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.0708420415614123e-100,0,0,0,0,0,0]]] +Shrunk 65 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:1:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:4:0:0:2:0:2:2:1:0:0:1:1:0:0:2:0:0:0:0:0:0:0:2:1:0:3:0:2:2:2:2:0:3:0:0:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,-5e-324,0,-5e-324,3.793779399308088e-70,0,0,0,-5e-324,-5e-324,-5e-324]]] +Shrunk 357 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:1:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:4:0:0:2:0:2:2:1:0:0:1:1:0:0:2:0:0:0:0:0:0:0:2:1:0:3:0:2:2:2:2:0:3:0:0:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,-5e-324,0,-5e-324,3.793779399308088e-70,0,0,0,-5e-324,-5e-324,-5e-324]]] +Shrunk 357 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✗ 9:schedule-feasibility +FAIL + +### Counterexample +```json +Error: Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:1:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:4:0:0:2:0:2:2:1:0:0:1:1:0:0:2:0:0:0:0:0:0:0:2:1:0:3:0:2:2:2:2:0:3:0:0:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,-5e-324,0,-5e-324,3.793779399308088e-70,0,0,0,-5e-324,-5e-324,-5e-324]]] +Shrunk 357 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +### Error +``` +Property failed after 2 tests +{ seed: 12345, path: "1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:1:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:4:0:0:2:0:2:2:1:0:0:1:1:0:0:2:0:0:0:0:0:0:0:2:1:0:3:0:2:2:2:2:0:3:0:0:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0", endOnFailure: true } +Counterexample: [[{"battery_efficiency":0.5,"min_soc":0,"max_soc":85,"cycle_cost_per_kwh":0,"export_price_ratio":1},{"capacityKwh":1,"maxChargeW":400,"maxDischargeW":400,"currentSoc":0},[0,0,0,0,0,0,0,0,0,0,0,0,0,0,-5e-324,0,-5e-324,3.793779399308088e-70,0,0,0,-5e-324,-5e-324,-5e-324]]] +Shrunk 357 time(s) + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 14 +- Failed: 1 + +### Failed invariants +- 9:schedule-feasibility + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 15 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 15 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 15 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 15 +- Failed: 0 + +## Invariant 1 — no-grid-charge-on-full-pv + +## ✓ 1:no-grid-charge-full-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 1b — pvStrongCoverage threshold straddling + +## ✓ 1b:pv-strong-threshold-straddling +PASS — 1000 scenarios, no counterexample. + +## Invariant 2 — fill-to-max-on-negative-prices + +## ✓ 2:fill-to-max-on-negative-prices +PASS — 1000 scenarios, no counterexample. + +## Invariant 3 — soc-trace-consistency + +### Fix iteration 1: clamp currentSoc to max_soc in generator + +## ✓ 3:soc-trace-consistency +PASS — 1000 scenarios, no counterexample. + +## Invariant 4 — cycle-counter-monotonic + +## ✓ 4:cycle-counter-monotonic +PASS — 1000 scenarios, no counterexample. + +## Invariant 5 — policy-mode-history-per-slot + +## ✓ 5:policy-mode-history-per-slot +PASS — 1000 scenarios, no counterexample. + +## Invariant 6 — rte-no-double-apply-soc + +### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC + +Rationale: different RTE values lead to different economic decisions at some slots. + +Once actions diverge, absolute socProjected legitimately differs. The real invariant + +is: given same starting SoC AND same action, the SoC delta is RTE-independent. + +We track sync status and only compare deltas while both engines are on the same path. + +## ✓ 6:rte-no-double-apply-soc +PASS — 1000 scenarios, no counterexample. + +## Invariant 7 — soc-bounds-safety + +## ✓ 7:soc-bounds-safety +PASS — 1000 scenarios, no counterexample. + +## Invariant 8 — pv-self-consumption-before-grid + +## ✓ 8:pv-self-consumption-before-grid +PASS — 1000 scenarios, no counterexample. + +## Invariant 9 — schedule-feasibility + +## ✓ 9:schedule-feasibility +PASS — 1000 scenarios, no counterexample. + +## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds + +## ✓ 10:expected-profit-self-sufficiency-bounds +PASS — 1000 scenarios, no counterexample. + +## Invariant 11 — computeExpectedProfit-profit-matches-compute + +## ✓ 11:expected-profit-matches-compute +PASS — 1000 scenarios, no counterexample. + +## Invariant 12 — exportPriceRatio-zero-effect-without-pv + +## ✓ 12:export-ratio-zero-effect-without-pv +PASS — 1000 scenarios, no counterexample. + +## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit + +## ✓ 13:export-ratio-lower-ratio-reduces-profit +PASS — 1000 scenarios, no counterexample. + +## Invariant 14 — night-discharge-defers-to-better-price + +## ✓ 14:night-discharge-defers-to-better-price +PASS — 1000 scenarios, no counterexample. + + +--- +## Summary +- Passed: 15 +- Failed: 0 + diff --git a/settings/index.html b/settings/index.html new file mode 100644 index 00000000..9759fb57 --- /dev/null +++ b/settings/index.html @@ -0,0 +1,3411 @@ + + + + Homewizard Settings + + + + + + + + + + + + +

+ + +

+ +
+
+
+
Uitbreiding
+
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…
+ +
+ +
+ + + +
+ +
+ + +
+

+

+
+ +
+
+
+ + + + + + + + + + + + + +
+
+ + 🔍 + Diagnose — kopieer dit bij een supportvraag + +
+ +
+
+ + +
+
+ + +
+

Capaciteitsuitbreiding

+

+ Vergelijking van 1–4 batterijen op basis van de huidige prijsverwachting. + Wordt bijgewerkt bij elke planningsrun (~15 min). +

+ +
+ + + +
+ +
+ Nog geen analyse beschikbaar. Wacht op de volgende planningsrun (~15 min). +
+ +
+ + +
+
+ + + +
+

Battery Policy Explainability

+
+ +
+

⚡ Batterij Economie

+
+
+
Gisteren
+
+
+
+
Afgelopen 7 dagen
+
+
+
+
+
+
+ +

Redenen

+
    +

    Waarschuwingen

    +
      + +
      + Diagnose — kopieer dit bij een supportvraag +
      + +
      +

      Tijdstip

      +
      +
      + + +
      +

      Legacy Fetch debug

      +
      Loading…
      + +
      + + + + + diff --git a/test/chart-renderer.test.js b/test/chart-renderer.test.js new file mode 100644 index 00000000..6eb7aba8 --- /dev/null +++ b/test/chart-renderer.test.js @@ -0,0 +1,172 @@ +'use strict'; + +const assert = require('assert'); +const CR = require('../lib/chart-renderer'); + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + passed++; + } catch (e) { + console.error(` ✗ ${name}: ${e.message}`); + failed++; + } +} + +console.log('configToJs:'); + +test('serializes primitives correctly', () => { + assert.strictEqual(CR.configToJs(null), 'null'); + assert.strictEqual(CR.configToJs(undefined), 'null'); + assert.strictEqual(CR.configToJs(true), 'true'); + assert.strictEqual(CR.configToJs(42), '42'); + assert.strictEqual(CR.configToJs(3.14), '3.14'); + assert.strictEqual(CR.configToJs('hello'), '"hello"'); +}); + +test('serializes arrays recursively', () => { + assert.strictEqual(CR.configToJs([1, 2, 3]), '[1,2,3]'); + assert.strictEqual(CR.configToJs(['a', 'b']), '["a","b"]'); + assert.strictEqual(CR.configToJs([[1], [2]]), '[[1],[2]]'); +}); + +test('serializes objects recursively', () => { + assert.strictEqual(CR.configToJs({ a: 1, b: 'x' }), '{"a":1,"b":"x"}'); + assert.strictEqual(CR.configToJs({ nested: { v: 5 } }), '{"nested":{"v":5}}'); +}); + +test('preserves function values (key behavior for quickchart.io)', () => { + const fn = function (v) { return v + 1; }; + const out = CR.configToJs({ callback: fn }); + assert.ok(out.includes('function'), 'function keyword preserved'); + assert.ok(out.includes('return'), 'function body preserved'); + assert.ok(!out.includes('null'), 'function not serialized as null'); +}); + +test('handles arrow functions', () => { + const arrow = (v) => v * 2; + const out = CR.configToJs(arrow); + assert.ok(out.includes('=>') || out.includes('function'), 'arrow body preserved'); +}); + +console.log('\nbuildPlanningChartConfig:'); + +test('returns null for empty slots', () => { + assert.strictEqual(CR.buildPlanningChartConfig({ slots: [] }), null); + assert.strictEqual(CR.buildPlanningChartConfig(null), null); + assert.strictEqual(CR.buildPlanningChartConfig({}), null); +}); + +test('returns config with bar + line datasets', () => { + const now = Date.now(); + const slots = Array.from({ length: 24 }, (_, i) => ({ + ts: now - 12 * 3_600_000 + i * 3_600_000, + hour: i, + mode: i < 12 ? 'past' : 'standby', + price: 0.20, + soc: 50 + i, + pvW: 100 * i, + })); + const cfg = CR.buildPlanningChartConfig({ slots, currentSoc: 50, currentMode: 'standby' }); + assert.ok(cfg); + assert.strictEqual(cfg.type, 'bar'); + assert.ok(Array.isArray(cfg.data.datasets)); + const labels = cfg.data.datasets.map(d => d.label); + assert.ok(labels.includes('_prijs'), 'price bars present'); + assert.ok(labels.includes('SoC (%)'), 'SoC line present'); +}); + +test('clips to first 96 slots (max one day)', () => { + const now = Date.now(); + const slots = Array.from({ length: 200 }, (_, i) => ({ + ts: now + i * 900_000, hour: 0, mode: 'standby', price: 0.20, soc: 50, pvW: 0, + })); + const cfg = CR.buildPlanningChartConfig({ slots }); + assert.strictEqual(cfg.data.labels.length, 96); +}); + +console.log('\nbuildPvChartConfig:'); + +test('returns config even with no data (empty inputs)', () => { + const cfg = CR.buildPvChartConfig({}); + assert.ok(cfg); + assert.strictEqual(cfg.type, 'line'); +}); + +test('builds 24h labels by default', () => { + const cfg = CR.buildPvChartConfig({ pvCapacityW: 4000 }); + assert.strictEqual(cfg.data.labels.length, 24); +}); + +test('builds 48h labels when tomorrow forecast present', () => { + const tomorrow = { 6: 100, 12: 800, 18: 300 }; + const cfg = CR.buildPvChartConfig({ pvForecast: [null, tomorrow] }); + assert.strictEqual(cfg.data.labels.length, 48); +}); + +test('omits OM/SC datasets when no data', () => { + const cfg = CR.buildPvChartConfig({}); + const labels = cfg.data.datasets.map(d => d.label); + assert.ok(labels.includes('PV Werkelijk')); + assert.ok(labels.includes('PV Verwachting')); + assert.ok(!labels.includes('Open-Meteo')); + assert.ok(!labels.includes('Solcast')); +}); + +test('includes OM dataset when forecast available', () => { + const cfg = CR.buildPvChartConfig({ pvForecastOM: [{ 12: 1000 }] }); + const labels = cfg.data.datasets.map(d => d.label); + assert.ok(labels.includes('Open-Meteo')); +}); + +console.log('\nbuildModeChartBody:'); + +test('returns null for empty modeHistory', () => { + assert.strictEqual(CR.buildModeChartBody(null), null); + assert.strictEqual(CR.buildModeChartBody([]), null); + assert.strictEqual(CR.buildModeChartBody(undefined), null); +}); + +test('returns JSON body string for valid history', () => { + const history = [ + { h: '2026-05-20T09:00', m: { zero: 4 }, soc: 50 }, + { h: '2026-05-20T09:15', m: { zero: 4 }, soc: 51 }, + ]; + const body = CR.buildModeChartBody(history); + assert.strictEqual(typeof body, 'string'); + const parsed = JSON.parse(body); + assert.strictEqual(parsed.version, '4'); + assert.strictEqual(parsed.width, 900); + assert.strictEqual(parsed.height, 500); + assert.ok(parsed.chart); + assert.strictEqual(parsed.chart.type, 'bar'); +}); + +test('includes SoC line dataset when soc values present', () => { + const history = [{ h: '2026-05-20T09:00', m: { zero: 4 }, soc: 50 }]; + const body = CR.buildModeChartBody(history); + const parsed = JSON.parse(body); + const socDs = parsed.chart.data.datasets.find(d => d.label === 'SoC %'); + assert.ok(socDs, 'SoC dataset present'); + assert.strictEqual(socDs.type, 'line'); +}); + +test('skips modes with zero contribution', () => { + const history = [{ h: '2026-05-20T09:00', m: { zero: 4 }, soc: 50 }]; + const body = CR.buildModeChartBody(history); + const parsed = JSON.parse(body); + const labels = parsed.chart.data.datasets.map(d => d.label); + // Only 'Nul' (zero) and 'SoC %' expected — other modes filtered out + assert.ok(labels.includes('Nul')); + assert.ok(!labels.includes('Laden')); + assert.ok(!labels.includes('Standby')); +}); + +console.log('\n══════════════════════════════'); +console.log(`Passed: ${passed}, Failed: ${failed}`); +console.log('══════════════════════════════'); +if (failed > 0) process.exit(1); diff --git a/test/optimizer-properties.test.js b/test/optimizer-properties.test.js new file mode 100644 index 00000000..336aa8d6 --- /dev/null +++ b/test/optimizer-properties.test.js @@ -0,0 +1,985 @@ +'use strict'; + +/** + * Property-based test suite for OptimizationEngine. + * Uses fast-check to generate 1000 randomized scenarios per invariant. + * + * Invariants under test: + * 1. No grid charge when PV surplus fully covers charge power for the full slot + * 2. Schedule fills to maxSoc when enough negative-price slots exist + capacity allows + * 3. Displayed socProjected exactly matches the forward-pass action trace + * 4. Cycle counter (kwhDischarged sum) is monotonic across simulated restarts + * 5. policy_mode_history has exactly one entry per unique 15-min bucket + * 6. RTE factor never double-applied: same action → same SoC delta regardless of RTE + */ + +const fc = require('fast-check'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const OptimizationEngine = require('../lib/optimization-engine'); + +const LOG_FILE = path.join(__dirname, '..', 'refactor-log.md'); +const RUNS = 1000; +const SEED = 12345; + +// ─── Log ───────────────────────────────────────────────────────────────────── + +function initLog(header) { + fs.writeFileSync(LOG_FILE, + `# Optimizer Property-Based Test Log\n\nGenerated: ${new Date().toISOString()}\n\n${header}\n\n`); +} + +function log(msg) { + fs.appendFileSync(LOG_FILE, msg + '\n'); +} + +// ─── Engine helpers ─────────────────────────────────────────────────────────── + +// Start slots 2 hours from now (whole hour) so slot0RemainingFrac = 1.0 +function makePriceSlots(prices, slotHours = 1) { + const start = new Date(); + start.setMinutes(0, 0, 0, 0); + start.setHours(start.getHours() + 2); + return prices.map((price, i) => ({ + timestamp: new Date(start.getTime() + i * slotHours * 3_600_000).toISOString(), + price + })); +} + +function makePvForecast(priceSlots, pvWValues) { + return priceSlots.map((slot, i) => ({ + timestamp: slot.timestamp, + pvPowerW: pvWValues[i] ?? 0 + })); +} + +function runCompute(settings, scenario) { + const eng = new OptimizationEngine(settings); + eng.compute( + scenario.prices, + scenario.currentSoc, + scenario.capacityKwh, + scenario.maxChargeW, + scenario.maxDischargeW, + scenario.pvForecast ?? null, + null, // rte — use settings.battery_efficiency + scenario.consumptionW ?? null, + scenario.minDischargePrice ?? 0, + scenario.consumptionMargin ?? 1.0, + scenario.pvKwhTomorrow ?? 0 + ); + return eng; +} + +// ─── Arbitraries ───────────────────────────────────────────────────────────── + +const settingsArb = fc.record({ + battery_efficiency: fc.double({ min: 0.50, max: 1.00, noNaN: true, noDefaultInfinity: true }), + min_soc: fc.constant(0), + max_soc: fc.integer({ min: 85, max: 100 }), + cycle_cost_per_kwh: fc.double({ min: 0, max: 0.10, noNaN: true, noDefaultInfinity: true }), + export_price_ratio: fc.constant(1.0), +}); + +const baseArb = fc.record({ + capacityKwh: fc.double({ min: 1.0, max: 12.0, noNaN: true, noDefaultInfinity: true }), + maxChargeW: fc.integer({ min: 400, max: 3000 }), + maxDischargeW: fc.integer({ min: 400, max: 3000 }), + currentSoc: fc.double({ min: 0, max: 94, noNaN: true, noDefaultInfinity: true }), +}); + +// ─── Test harness ───────────────────────────────────────────────────────────── + +let totalPassed = 0; +let totalFailed = 0; +const failedInvariants = []; + +function testInvariant(name, arb, predFn, runs = RUNS) { + process.stdout.write(`[${name}] ... `); + try { + fc.assert( + fc.property(arb, predFn), + { numRuns: runs, seed: SEED, verbose: false } + ); + console.log('✓ PASS'); + log(`## ✓ ${name}\nPASS — ${runs} scenarios, no counterexample.\n`); + totalPassed++; + return true; + } catch (err) { + const ce = err.counterexample + ? JSON.stringify(err.counterexample, null, 2).slice(0, 1200) + : String(err); + console.log('✗ FAIL'); + console.error(` ${String(err.message || err).split('\n')[0]}`); + log(`## ✗ ${name}\nFAIL\n\n### Counterexample\n\`\`\`json\n${ce}\n\`\`\`\n\n### Error\n\`\`\`\n${err.message}\n\`\`\`\n`); + failedInvariants.push({ name, err, ce }); + totalFailed++; + return false; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 1 +// "Plan never charges from grid when PV surplus covers full charge power" +// +// When pvW − consW ≥ maxChargeW for every slot (pvCoverage ≥ 1.0), the DP must +// not schedule 'charge' because there is no grid draw. The semantically correct +// action is 'preserve' (→ zero_charge_only firmware mode), which lets PV fill the +// battery while correctly accounting for the foregone export opportunity cost. +// +// Known risk: the DP's vCharge formula does NOT subtract the PV export opportunity +// cost (price × pvCoverage × exportRatio × kWh) whereas vPreserve does. When +// pvCoverage = 1.0, cycleCost = 0 this makes vCharge > vPreserve → invariant fails. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 1 — no-grid-charge-on-full-pv\n'); + +testInvariant('1:no-grid-charge-full-pv', + fc.tuple( + // Force cycle_cost=0 to exercise the hardest case: vCharge and vPreserve are equal + // (no cycle penalty to tip the balance). With the fixed effectiveChargeCost formula, + // vCharge = vPreserve at pvCoverage=1.0 so preserve wins the tie. + fc.record({ + battery_efficiency: fc.double({ min: 0.50, max: 1.00, noNaN: true, noDefaultInfinity: true }), + min_soc: fc.constant(0), + max_soc: fc.integer({ min: 85, max: 100 }), + cycle_cost_per_kwh: fc.constant(0), + export_price_ratio: fc.constant(1.0), + }), + baseArb, + // 24 strictly positive price slots (avoid negative-price special paths) + fc.array(fc.double({ min: 0.01, max: 0.50, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 }), + // per-slot consumption: 100–400 W + fc.array(fc.integer({ min: 100, max: 400 }), { minLength: 24, maxLength: 24 }) + ), + ([settings, base, priceValues, consValues]) => { + // PV = maxChargeW + max(consW) + 200 → pvCoverage = (pvW − consW) / maxChargeW > 1.0 + const pvW = base.maxChargeW + Math.max(...consValues) + 200; + + const prices = makePriceSlots(priceValues); + const pvForecast = makePvForecast(prices, Array(24).fill(pvW)); + const pvKwhTomorrow = (pvW / 1000) * 24; // plenty tomorrow + + const eng = runCompute(settings, { + ...base, + prices, + pvForecast, + consumptionW: consValues, + pvKwhTomorrow + }); + + if (!eng._schedule) return true; + + for (const slot of eng._schedule.slots) { + if (slot.action === 'charge') return false; + } + return true; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 1b +// "pvStrongCoverage threshold boundary: preserve chosen when pvCoverage straddles +// the 400W / maxChargeW threshold" +// +// pvStrongCoverage = 400 / maxChargeW. Only above this threshold does the DP +// apply free SoC gain to vPreserve and does the firmware run zero_charge_only. +// Below it: preserve = standby (no free PV charging). +// +// Three sub-cases: +// below (pvCoverage ∈ [0.38, pvStrong)): preserve gets no free gain → standby/preserve ok +// at (pvCoverage = pvStrong exactly): first slot where free gain activates +// above (pvCoverage ∈ (pvStrong, 0.60]): free gain active → preserve must beat charge +// +// Invariant: with cycleCost=0 and pvCoverage just above pvStrongCoverage, no slot +// should be 'charge' (same guarantee as invariant 1, but in the narrow threshold band). +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 1b — pvStrongCoverage threshold straddling\n'); + +testInvariant('1b:pv-strong-threshold-straddling', + fc.tuple( + fc.record({ + battery_efficiency: fc.double({ min: 0.50, max: 1.00, noNaN: true, noDefaultInfinity: true }), + min_soc: fc.constant(0), + max_soc: fc.integer({ min: 85, max: 100 }), + cycle_cost_per_kwh: fc.constant(0), + export_price_ratio: fc.constant(1.0), + }), + fc.record({ + capacityKwh: fc.double({ min: 1.0, max: 12.0, noNaN: true, noDefaultInfinity: true }), + maxChargeW: fc.integer({ min: 400, max: 3000 }), + maxDischargeW: fc.integer({ min: 400, max: 3000 }), + currentSoc: fc.double({ min: 0, max: 94, noNaN: true, noDefaultInfinity: true }), + }), + // prices: positive only (avoid negative-price special paths) + fc.array(fc.double({ min: 0.01, max: 0.50, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 }), + // per-slot consumption: 100–400 W + fc.array(fc.integer({ min: 100, max: 400 }), { minLength: 24, maxLength: 24 }), + // pvCoverage multiplier: 0.38–0.60 relative to maxChargeW + // pvStrongCoverage = 400/maxChargeW (e.g. 0.13 for 3000W, 1.0 for 400W) + // We specifically target coverage ABOVE pvStrongCoverage but below 1.0 — + // the zone where preserve gets free PV charging but charge still has grid cost. + fc.double({ min: 0.38, max: 0.60, noNaN: true, noDefaultInfinity: true }) + ).map(([settings, base, priceValues, consValues, coverageFrac]) => [ + settings, + { ...base, currentSoc: Math.min(base.currentSoc, settings.max_soc) }, + priceValues, + consValues, + coverageFrac, + ]), + ([settings, base, priceValues, consValues, coverageFrac]) => { + // pvStrongCoverage = 400 / maxChargeW + const pvStrongCoverage = 400 / base.maxChargeW; + + // Skip if coverageFrac is below pvStrongCoverage for this maxChargeW — + // below the threshold preserve gets no free gain, so invariant doesn't apply. + if (coverageFrac <= pvStrongCoverage) return true; + + // pvW = consW + coverageFrac * maxChargeW → pvCoverage = coverageFrac (0.38–0.60) + // Per slot: use the slot's own consW so pvCoverage stays close to coverageFrac + const prices = makePriceSlots(priceValues); + const pvForecast = makePvForecast(prices, consValues.map( + c => Math.round(c + coverageFrac * base.maxChargeW) + )); + const pvKwhTomorrow = (coverageFrac * base.maxChargeW / 1000) * 24; + + const eng = runCompute(settings, { + ...base, + prices, + pvForecast, + consumptionW: consValues, + pvKwhTomorrow, + }); + + if (!eng._schedule) return true; + + // When pvCoverage > pvStrongCoverage and exportPriceRatio=1.0: + // vCharge cost = price (full, including foregone export) ≥ vPreserve (free PV gain + export). + // So 'charge' must not appear when PV is above the strong threshold. + for (const slot of eng._schedule.slots) { + if (slot.action === 'charge') return false; + } + return true; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 2 +// "Plan fills to maxSoc when tomorrow has cheaper-than-today negative slots +// and capacity allows" +// +// Set-up: 12 standby slots (today), then enough negative-price slots to fill +// battery from 0 % to maxSoc. cycle_cost = 0 so charging is unambiguously +// profitable at any negative price. The DP must reach maxSoc. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 2 — fill-to-max-on-negative-prices\n'); + +testInvariant('2:fill-to-max-on-negative-prices', + fc.tuple( + fc.record({ + battery_efficiency: fc.double({ min: 0.50, max: 1.00, noNaN: true, noDefaultInfinity: true }), + min_soc: fc.constant(0), + max_soc: fc.integer({ min: 85, max: 100 }), + cycle_cost_per_kwh: fc.constant(0), // no cycle cost → always profitable to charge at neg price + export_price_ratio: fc.constant(1.0), + }), + fc.record({ + capacityKwh: fc.double({ min: 1.0, max: 5.0, noNaN: true, noDefaultInfinity: true }), + maxChargeW: fc.integer({ min: 800, max: 3000 }), // at least 800 W so fill is feasible + maxDischargeW: fc.integer({ min: 400, max: 3000 }), + }), + fc.double({ min: -0.50, max: -0.05, noNaN: true, noDefaultInfinity: true }) + ), + ([settings, base, negPrice]) => { + const slotH = 1; // hourly slots + // Slots needed to charge from 0 to maxSoc (full capacity × maxSoc%) + const maxSocFrac = settings.max_soc / 100; + const kwhNeeded = base.capacityKwh * maxSocFrac; + const kwhPerSlot = (base.maxChargeW / 1000) * slotH; + const slotsNeeded = Math.ceil(kwhNeeded / kwhPerSlot); + // Add 2 extra negative slots as margin + const negSlots = slotsNeeded + 2; + const todaySlots = 4; // small number of positive-price "today" slots + + const prices = [ + ...Array(todaySlots).fill(0.20), // today: positive, no incentive to charge + ...Array(negSlots).fill(negPrice), // tomorrow: negative, charge is profitable + ...Array(Math.max(0, 24 - todaySlots - negSlots)).fill(0.20) // padding + ].slice(0, Math.max(24, todaySlots + negSlots)); + + const currentSoc = 0; // start empty + + const eng = runCompute(settings, { + ...base, + currentSoc, + prices: makePriceSlots(prices), + pvKwhTomorrow: 0 // no PV interference + }); + + if (!eng._schedule) return true; + + const maxSocReached = eng._schedule.slots.some( + s => s.socProjected >= settings.max_soc - 1.5 // 1.5 % rounding tolerance + ); + return maxSocReached; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 3 +// "Displayed plan exactly matches DP decision trace" +// +// Re-execute the forward pass manually using the scheduled actions and verify +// that socProjected values are consistent: +// charge → SoC must increase (or be capped at maxSoc) +// discharge → SoC must decrease (or be capped at minSoc) +// preserve/standby/trickle → SoC must not significantly decrease +// All socProjected values must lie within [minSoc − ε, maxSoc + ε]. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 3 — soc-trace-consistency\n'); +log('### Fix iteration 1: clamp currentSoc to max_soc in generator\n'); + +testInvariant('3:soc-trace-consistency', + fc.tuple( + settingsArb, + baseArb, + fc.array( + fc.double({ min: -0.30, max: 0.60, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 } + ) + // Clamp currentSoc to max_soc so the engine never starts above its own ceiling. + // Without this, socProjected[0] = currentSoc (clamped to GRID_TOTAL, not maxSocG) + // which legitimately exceeds max_soc and falsely triggers the bounds check. + ).map(([settings, base, prices]) => [ + settings, + { ...base, currentSoc: Math.min(base.currentSoc, settings.max_soc) }, + prices + ]), + ([settings, base, priceValues]) => { + const eng = runCompute(settings, { + ...base, + prices: makePriceSlots(priceValues) + }); + + if (!eng._schedule) return true; + const slots = eng._schedule.slots; + if (slots.length < 2) return true; + + // SoC bounds tolerance: 0.5% (GRID=10 → 0.1% resolution; 0.5% covers accumulation) + const EPS = 0.5; + const minSoc = settings.min_soc; + const maxSoc = settings.max_soc; + + // Check bounds + for (const slot of slots) { + if (slot.socProjected < minSoc - EPS) return false; + if (slot.socProjected > maxSoc + EPS) return false; + } + + // Check action-to-soc direction consistency + for (let i = 0; i < slots.length - 1; i++) { + const s = slots[i]; + const next = slots[i + 1]; + const delta = next.socProjected - s.socProjected; + + if (s.action === 'charge') { + // SoC must not significantly decrease after a charge slot + if (delta < -EPS) return false; + } else if (s.action === 'discharge') { + // SoC must not significantly increase after a discharge slot + if (delta > EPS) return false; + } else { + // preserve / standby / trickle: SoC must not significantly decrease + if (delta < -EPS) return false; + } + } + + return true; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 4 +// "Cycle counter is monotonic across simulated restarts" +// +// Simulate battery_cycle_history accumulation (mirrors device.js logic). +// After completing N cycles, persist the history. Simulate a restart (reset +// in-memory accumulators, restore persisted history). The total kwhDischarged +// after restart must equal the total before restart (no loss, no inflation). +// Subsequent cycles may only increase the total. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 4 — cycle-counter-monotonic\n'); + +testInvariant('4:cycle-counter-monotonic', + fc.array( + fc.record({ + kwhDischarged: fc.double({ min: 0.05, max: 5.0, noNaN: true, noDefaultInfinity: true }), + avgChargePrice: fc.double({ min: 0, max: 0.50, noNaN: true, noDefaultInfinity: true }), + avgDischargePrice: fc.double({ min: 0, max: 0.50, noNaN: true, noDefaultInfinity: true }), + profitEur: fc.double({ min: -2, max: 5.0, noNaN: true, noDefaultInfinity: true }), + }), + { minLength: 1, maxLength: 30 } + ), + (cycles) => { + // Phase 1: accumulate cycles into history (mirrors device.js) + const history = []; + for (const c of cycles) { + history.push({ date: '2026-01-15', ...c }); + if (history.length > 60) history.shift(); // FIFO cap + } + const totalBefore = history.reduce((s, e) => s + e.kwhDischarged, 0); + + // Simulate restart: in-memory state resets, history restored from persistent store + const restoredHistory = history.map(e => ({ ...e })); + // in-memory accumulators reset to 0 — but persisted history unchanged + let inMemoryKwh = 0; + + // Phase 2: continue after restart — add one more cycle + const newCycleKwh = 1.0; + inMemoryKwh += newCycleKwh; + restoredHistory.push({ + date: '2026-01-16', + kwhDischarged: inMemoryKwh, + avgChargePrice: 0.10, + avgDischargePrice: 0.30, + profitEur: 0.20, + }); + if (restoredHistory.length > 60) restoredHistory.shift(); + inMemoryKwh = 0; // reset after recording + + const totalAfter = restoredHistory.reduce((s, e) => s + e.kwhDischarged, 0); + + // Post-restart total ≥ pre-restart total (monotonic; the new cycle adds to it) + return totalAfter >= totalBefore - 0.001; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 5 +// "policy_mode_history has exactly one entry per unique 15-min slot" +// +// Simulate the history-write logic (mirrors device.js lines 1545–1570). +// Given a sequence of policy runs at distinct 15-min bucket timestamps, the +// resulting history must contain exactly one entry per unique bucket, with no +// duplicates and no missing entries (up to the 192-entry FIFO cap). +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 5 — policy-mode-history-per-slot\n'); + +testInvariant('5:policy-mode-history-per-slot', + // Generate N distinct 15-min bucket indices in [0, 191] (48h) + fc.array(fc.integer({ min: 0, max: 191 }), { minLength: 1, maxLength: 96 }) + .map(arr => [...new Set(arr)].sort((a, b) => a - b)), + (bucketIndices) => { + if (bucketIndices.length === 0) return true; + + const BASE_MS = new Date('2026-01-15T00:00:00+01:00').getTime(); + const BUCKET_MS = 15 * 60 * 1000; + const MAX_HIST = 192; + + const modeHistory = []; + + for (const idx of bucketIndices) { + const rawTs = BASE_MS + idx * BUCKET_MS; + // Round to 15-min boundary (mirrors device.js) + const roundedTs = Math.round(rawTs / BUCKET_MS) * BUCKET_MS; + + const entry = { ts: roundedTs, hwMode: 'standby', price: 0.20, soc: 50 }; + + const existingIdx = modeHistory.findIndex(e => e.ts === roundedTs); + if (existingIdx >= 0) { + modeHistory[existingIdx] = entry; // update existing bucket + } else { + modeHistory.push(entry); + if (modeHistory.length > MAX_HIST) modeHistory.shift(); + } + } + + // Each bucket must appear exactly once + const tsBuckets = modeHistory.map(e => e.ts); + const uniqueBuckets = new Set(tsBuckets); + if (uniqueBuckets.size !== modeHistory.length) return false; + + // Number of entries must equal number of unique input buckets + // (capped at MAX_HIST when input > MAX_HIST distinct buckets) + const expectedCount = Math.min(bucketIndices.length, MAX_HIST); + if (modeHistory.length !== expectedCount) return false; + + return true; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 6 +// "RTE factor never double-applied to SoC delta" +// +// Physical SoC change depends on charge/discharge power and slot duration, not +// on round-trip efficiency. RTE only affects economic value (discharge revenue). +// When two DP runs share the same action at slot t, their socProjected[t] must +// be identical regardless of RTE. If they differ by > 0.3 % with the same +// action, RTE has leaked into the SoC calculation. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 6 — rte-no-double-apply-soc\n'); +log('### Fix iteration 1: compare SoC DELTA while trajectories are synced, not absolute SoC\n'); +log('Rationale: different RTE values lead to different economic decisions at some slots.\n'); +log('Once actions diverge, absolute socProjected legitimately differs. The real invariant\n'); +log('is: given same starting SoC AND same action, the SoC delta is RTE-independent.\n'); +log('We track sync status and only compare deltas while both engines are on the same path.\n'); + +testInvariant('6:rte-no-double-apply-soc', + fc.tuple( + fc.record({ + min_soc: fc.constant(0), + max_soc: fc.integer({ min: 85, max: 100 }), + cycle_cost_per_kwh: fc.constant(0), + export_price_ratio: fc.constant(1.0), + }), + baseArb, + fc.array( + fc.double({ min: -0.30, max: 0.60, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 } + ) + // Clamp currentSoc to max_soc (same reason as invariant 3) + ).map(([baseSettings, base, prices]) => [ + baseSettings, + { ...base, currentSoc: Math.min(base.currentSoc, baseSettings.max_soc) }, + prices + ]), + ([baseSettings, base, priceValues]) => { + const prices = makePriceSlots(priceValues); + + const eng75 = runCompute({ ...baseSettings, battery_efficiency: 0.75 }, { ...base, prices }); + const eng90 = runCompute({ ...baseSettings, battery_efficiency: 0.90 }, { ...base, prices }); + + if (!eng75._schedule || !eng90._schedule) return true; + + const s75 = eng75._schedule.slots; + const s90 = eng90._schedule.slots; + if (s75.length !== s90.length) return true; + + // Both engines start at the same currentSoc → slot 0's socProjected must match + if (Math.abs(s75[0].socProjected - s90[0].socProjected) > 0.2) return false; + + // Track synchronization: while both engines have taken identical actions from + // slot 0, they're on the same SoC trajectory. Compare SoC deltas in that window. + // Once actions diverge, trajectories legitimately differ → stop checking. + for (let i = 0; i < s75.length - 1; i++) { + if (s75[i].action !== s90[i].action) break; // diverged — stop + // Same action, same starting SoC → delta must be identical (RTE not in SoC formula) + const delta75 = s75[i + 1].socProjected - s75[i].socProjected; + const delta90 = s90[i + 1].socProjected - s90[i].socProjected; + // Tolerance: 0.3 % (GRID=10 → 0.1 % resolution; small rounding on Math.round) + if (Math.abs(delta75 - delta90) > 0.3) return false; + } + + return true; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 7: SoC bounds safety +// +// socProjected must never exceed maxSoc or fall below minSoc across the full +// schedule. Catches over-charge bugs, clipping errors, rounding drift. +// Tighter than invariant 3 (which also checks action-direction consistency): +// tolerance here is 0.15 % (1.5 GRID units) vs 0.5 % in invariant 3. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 7 — soc-bounds-safety\n'); + +testInvariant('7:soc-bounds-safety', + fc.tuple( + settingsArb, + baseArb, + fc.array( + fc.double({ min: -0.30, max: 0.60, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 } + ) + ).map(([settings, base, prices]) => [ + settings, + { ...base, currentSoc: Math.min(base.currentSoc, settings.max_soc) }, + prices + ]), + ([settings, base, priceValues]) => { + const eng = runCompute(settings, { ...base, prices: makePriceSlots(priceValues) }); + if (!eng._schedule) return true; + + const EPS = 0.15; // 1.5 GRID units — tighter than inv 3 + for (const slot of eng._schedule.slots) { + if (slot.socProjected < settings.min_soc - EPS) return false; + if (slot.socProjected > settings.max_soc + EPS) return false; + } + return true; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 8: PV self-consumption before grid import +// +// Generate per-slot pvCoverage values (some < 1.0, some > 1.0, some straddling +// pvStrongCoverage) and verify that whenever pvCoverage >= 1.0 in a slot the +// scheduled action is NOT 'charge'. Grid import only occurs when PV surplus +// is genuinely insufficient to cover the charge demand (pvCoverage < 1.0). +// +// Complements invariant 1 (which uses fixed pvW = maxChargeW + max(consW) + 200 +// for all slots) by using VARIED per-slot pvW, producing mixed scenarios where +// some slots have surplus and some don't. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 8 — pv-self-consumption-before-grid\n'); + +testInvariant('8:pv-self-consumption-before-grid', + fc.tuple( + fc.record({ + battery_efficiency: fc.double({ min: 0.50, max: 1.00, noNaN: true, noDefaultInfinity: true }), + min_soc: fc.constant(0), + max_soc: fc.integer({ min: 85, max: 100 }), + cycle_cost_per_kwh: fc.constant(0), + export_price_ratio: fc.constant(1.0), + }), + baseArb, + fc.array(fc.double({ min: 0.01, max: 0.50, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 }), + fc.array(fc.integer({ min: 100, max: 400 }), { minLength: 24, maxLength: 24 }), + // per-slot pvCoverage fraction (0–1.6): varied so some slots are above 1.0 + fc.array(fc.double({ min: 0, max: 1.6, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 }) + ).map(([settings, base, prices, cons, coverages]) => [ + settings, + { ...base, currentSoc: Math.min(base.currentSoc, settings.max_soc) }, + prices, cons, coverages + ]), + ([settings, base, priceValues, consValues, pvCoverages]) => { + // pvW = consW + coverage * maxChargeW → pvCoverage = (pvW − consW) / maxChargeW = coverage + const pvWValues = consValues.map((c, t) => c + pvCoverages[t] * base.maxChargeW); + const prices = makePriceSlots(priceValues); + const pvForecast = makePvForecast(prices, pvWValues); + const pvKwhTomorrow = pvWValues.reduce((s, w) => s + w / 1000, 0); + + const eng = runCompute(settings, { + ...base, + prices, + pvForecast, + consumptionW: consValues, + pvKwhTomorrow, + }); + + if (!eng._schedule) return true; + + for (let t = 0; t < eng._schedule.slots.length; t++) { + const slot = eng._schedule.slots[t]; + const pvW = pvWValues[t] ?? 0; + const consW = consValues[t] ?? 0; + const surplus = Math.max(0, pvW - consW); + const pvCov = Math.min(1, surplus / base.maxChargeW); + + // pvCoverage >= 1.0 → PV surplus alone covers full charge power. + // Routing rule: use PV before grid → no grid needed → action must NOT be 'charge'. + if (pvCov >= 1.0 - 1e-9 && slot.action === 'charge') return false; + + // When action='charge' and pvCov > 0, grid import fraction = 1 − pvCov ∈ (0, 1). + // I.e., partial PV reduces but does not eliminate grid draw (routing is correct). + if (slot.action === 'charge' && pvCov < 0) return false; // pvCov always ≥ 0 by construction + } + return true; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 9: Schedule feasibility — action preconditions +// +// The DP enforces SoC preconditions before assigning actions: +// discharge only when socG > minSocG (battery has energy to give) +// charge only when socG < maxSocG (battery has room to accept) +// +// The forward pass must preserve this: a 'discharge' slot starting at minSoc is +// a feasibility violation (nothing to discharge), likewise 'charge' at maxSoc. +// +// EPS = 0.05 % (half a GRID unit). The DP allows charge when socG < maxSocG, +// so the closest valid charge start is socProjected = maxSoc − 0.1 %. +// EPS=0.05 correctly accepts that (0.1 > 0.05 from the boundary) while still +// catching a true violation at socProjected = maxSoc (0.0 < 0.05 from boundary). +// EPS=0.15 was too wide: it rejected the legitimate near-boundary case. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 9 — schedule-feasibility\n'); + +testInvariant('9:schedule-feasibility', + fc.tuple( + settingsArb, + baseArb, + fc.array( + fc.double({ min: -0.30, max: 0.60, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 } + ) + ).map(([settings, base, prices]) => [ + settings, + { ...base, currentSoc: Math.min(base.currentSoc, settings.max_soc) }, + prices + ]), + ([settings, base, priceValues]) => { + const eng = runCompute(settings, { ...base, prices: makePriceSlots(priceValues) }); + if (!eng._schedule) return true; + + const EPS = 0.05; // half a GRID unit (0.1 % resolution) + const minSoc = settings.min_soc; + const maxSoc = settings.max_soc; + + for (const slot of eng._schedule.slots) { + // 'discharge' at minSoc: nothing to discharge + if (slot.action === 'discharge' && slot.socProjected <= minSoc + EPS) return false; + // 'charge' at maxSoc: no room to charge + if (slot.action === 'charge' && slot.socProjected >= maxSoc - EPS) return false; + } + return true; + } +); + +// ─── Invariant 10 — computeExpectedProfit self-sufficiency bounds ───────────── +// +// selfSufficiencyPct must always be in [0, 100] regardless of inputs. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 10 — computeExpectedProfit-self-sufficiency-bounds\n'); + +testInvariant('10:expected-profit-self-sufficiency-bounds', + fc.tuple(settingsArb, baseArb, fc.array( + fc.double({ min: -0.30, max: 0.60, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 } + )), + ([settings, base, priceValues]) => { + const eng = new OptimizationEngine(settings); + const prices = makePriceSlots(priceValues); + const result = eng.computeExpectedProfit( + prices, base.currentSoc, base.capacityKwh, + base.maxChargeW, base.maxDischargeW, + null, null, null, 0, 1.0, 0 + ); + return result.selfSufficiencyPct >= 0 && result.selfSufficiencyPct <= 100; + } +); + +// ─── Invariant 11 — computeExpectedProfit profit matches compute() ──────────── +// +// Both methods call _runBackwardDP with the same params; dp[initialSocG] must +// be equal. compute() stores this as _schedule.projectedProfit. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 11 — computeExpectedProfit-profit-matches-compute\n'); + +testInvariant('11:expected-profit-matches-compute', + fc.tuple(settingsArb, baseArb, fc.array( + fc.double({ min: -0.30, max: 0.60, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 } + )), + ([settings, base, priceValues]) => { + const prices = makePriceSlots(priceValues); + const eng = runCompute(settings, { ...base, prices }); + if (!eng._schedule) return true; + const result = eng.computeExpectedProfit( + prices, base.currentSoc, base.capacityKwh, + base.maxChargeW, base.maxDischargeW, + null, null, null, 0, 1.0, 0 + ); + // Allow 1e-9 floating point tolerance + return Math.abs(result.profit - eng._schedule.projectedProfit) < 1e-6; + } +); + +// ─── Invariant 12 — exportPriceRatio zero-effect without PV or consumption ──── +// +// When pvForecast=null and consumptionWPerSlot=null, exportPriceRatio does not +// appear in either the effectiveChargeCost formula (pvCoverage=0) or the +// discharge value formula (consumptionKwh=null path). Schedule must be identical +// for ratio=1.0 vs ratio=0.3. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 12 — exportPriceRatio-zero-effect-without-pv\n'); + +testInvariant('12:export-ratio-zero-effect-without-pv', + fc.tuple(baseArb, fc.array( + fc.double({ min: -0.30, max: 0.60, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 } + )), + ([base, priceValues]) => { + const prices = makePriceSlots(priceValues); + const baseSettings = { battery_efficiency: 0.9, min_soc: 0, max_soc: 95, + cycle_cost_per_kwh: 0, export_price_ratio: 1.0 }; + + const eng1 = runCompute({ ...baseSettings, export_price_ratio: 1.0 }, { ...base, prices }); + const eng2 = runCompute({ ...baseSettings, export_price_ratio: 0.3 }, { ...base, prices }); + + if (!eng1._schedule || !eng2._schedule) return !eng1._schedule && !eng2._schedule; + + const slots1 = eng1._schedule.slots; + const slots2 = eng2._schedule.slots; + if (slots1.length !== slots2.length) return false; + for (let i = 0; i < slots1.length; i++) { + if (slots1[i].action !== slots2[i].action) return false; + if (Math.abs(slots1[i].socProjected - slots2[i].socProjected) > 0.15) return false; + } + return true; + } +); + +// ─── Invariant 13 — exportPriceRatio: lower ratio reduces discharge profit ──── +// +// When consumption=0 for all slots (all discharge goes to export), ratio=1.0 +// earns full retail per kWh while ratio=0.3 earns only 30%. Therefore: +// profit(ratio=1.0) >= profit(ratio=0.3) +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 13 — exportPriceRatio-lower-ratio-reduces-profit\n'); + +testInvariant('13:export-ratio-lower-ratio-reduces-profit', + fc.tuple(baseArb, fc.array( + fc.double({ min: 0.01, max: 0.60, noNaN: true, noDefaultInfinity: true }), + { minLength: 24, maxLength: 24 } + )), + ([base, priceValues]) => { + const prices = makePriceSlots(priceValues); + const consumption = new Array(24).fill(0); + const baseSettings = { battery_efficiency: 0.9, min_soc: 0, max_soc: 95, + cycle_cost_per_kwh: 0, export_price_ratio: 1.0 }; + + const eng1 = new OptimizationEngine({ ...baseSettings, export_price_ratio: 1.0 }); + const eng2 = new OptimizationEngine({ ...baseSettings, export_price_ratio: 0.3 }); + + const r1 = eng1.computeExpectedProfit(prices, base.currentSoc, base.capacityKwh, + base.maxChargeW, base.maxDischargeW, null, null, consumption, 0, 1.0, 0); + const r2 = eng2.computeExpectedProfit(prices, base.currentSoc, base.capacityKwh, + base.maxChargeW, base.maxDischargeW, null, null, consumption, 0, 1.0, 0); + + return r1.profit >= r2.profit - 1e-9; + } +); + +// ───────────────────────────────────────────────────────────────────────────── +// INVARIANT 14: Night-slot ordering — discharge deferred to higher-priced slot +// +// When pvKwhTomorrow >= 80% capacity (PV refills tomorrow) and a later night +// slot has a strictly higher discharge price, the DP must NOT discharge at the +// cheaper earlier slot when SoC allows only one discharge. +// +// Setup: [nightA(priceA), nightB(priceB>priceA), pvDay × 8 slots]. +// The PV-day slots ensure pvKwhFromT[1] >= capacityKwh, which normally triggers +// dp-flattening at slot 0 — destroying the price gradient. Our fix blocks +// flattening when a better-priced night slot is ahead. +// +// consumptionWPerSlot is randomised per slot: it determines effective discharge +// power (= min(maxDischargeW, consW)), so each slot's revenue = price × consKwh. +// To isolate price ordering, consW is equal for both night slots — so revenue +// is driven by price alone. PV-day slots carry their own (lower) consumption. +// ───────────────────────────────────────────────────────────────────────────── + +log('## Invariant 14 — night-discharge-defers-to-better-price\n'); + +testInvariant('14:night-discharge-defers-to-better-price', + fc.tuple( + fc.record({ + battery_efficiency: fc.double({ min: 0.50, max: 0.90, noNaN: true, noDefaultInfinity: true }), + min_soc: fc.constant(0), + max_soc: fc.constant(100), + cycle_cost_per_kwh: fc.double({ min: 0, max: 0.08, noNaN: true, noDefaultInfinity: true }), + export_price_ratio: fc.constant(1.0), + }), + fc.record({ + capacityKwh: fc.double({ min: 1.0, max: 12.0, noNaN: true, noDefaultInfinity: true }), + maxChargeW: fc.integer({ min: 400, max: 3000 }), + maxDischargeW: fc.integer({ min: 400, max: 800 }), + }), + // priceA (lower night slot), priceB = priceA + delta (higher night slot) + fc.double({ min: 0.230, max: 0.340, noNaN: true, noDefaultInfinity: true }), + fc.double({ min: 0.005, max: 0.040, noNaN: true, noDefaultInfinity: true }), + // house consumption for both night slots (same → price drives the ordering) + fc.integer({ min: 250, max: 700 }), + ), + ([settings, base, priceA, priceDelta, nightConsW]) => { + const priceB = priceA + priceDelta; // priceB > priceA + const minDischargePrice = 0.220; + const { capacityKwh, maxChargeW, maxDischargeW } = base; + + // Effective discharge power per slot = min(maxDischargeW, consW) + const effectiveDischargeW = Math.min(maxDischargeW, nightConsW); + // SoC % consumed per 1-hour discharge slot (no RTE on SoC projection) + const perSlotSocPct = (effectiveDischargeW / 1000) * 100 / capacityKwh; + + // Skip degenerate cases: discharge delta too small to matter + if (perSlotSocPct < 0.5) return true; + + // currentSoc = exactly 1 slot's worth: enough for 1 discharge but not 2. + // After discharge socG drops to 0 = minSoc → can't discharge again. + const currentSoc = Math.min(99, perSlotSocPct); + + // Guard: skip if SoC too small to discharge even once + if (currentSoc < perSlotSocPct - 0.1) return true; + + // PV-day slots: high PV so pvKwhFromT[slot 1 onwards] >= capacityKwh. + // 8 hourly slots at maxChargeW W each → pvKwh = maxChargeW/1000 * 8 + const pvDaySlots = 8; + const pvDayW = maxChargeW; // full charge power + const pvDayConsW = 150; // low consumption during PV hours + const pvKwhFromDay = (pvDayW / 1000) * pvDaySlots; // kWh PV available + // Only proceed when PV day can actually refill battery (triggers flatten guard) + if (pvKwhFromDay < capacityKwh * 0.8) return true; + + // Price array: [nightA, nightB, pvDay×8 at low price (no discharge incentive)] + const priceValues = [priceA, priceB, ...Array(pvDaySlots).fill(0.05)]; + const prices = makePriceSlots(priceValues); + + // PV forecast: no PV for night slots, pvDayW for PV slots + const pvWValues = [0, 0, ...Array(pvDaySlots).fill(pvDayW)]; + const pvForecast = makePvForecast(prices, pvWValues); + + // consumptionWPerSlot: nightConsW for night, pvDayConsW for PV day + const consumptionW = [nightConsW, nightConsW, ...Array(pvDaySlots).fill(pvDayConsW)]; + + // pvKwhTomorrow: net PV surplus that enters battery (pvW - consW, clamped ≥ 0) + const pvKwhTomorrow = pvDaySlots * Math.max(0, pvDayW - pvDayConsW) / 1000; + + const eng = runCompute(settings, { + ...base, + currentSoc, + prices, + pvForecast, + consumptionW, + minDischargePrice, + pvKwhTomorrow, + }); + + if (!eng._schedule) return true; + const slots = eng._schedule.slots; + if (slots.length < 2) return true; + + // Invariant: slot 0 (priceA, cheaper) must NOT be discharged. + // The DP must preserve SoC for slot 1 (priceB, more expensive). + return slots[0].action !== 'discharge'; + } +); + +// ─── Summary ────────────────────────────────────────────────────────────────── + +console.log('\n' + '─'.repeat(60)); +console.log(`Results: ${totalPassed} passed, ${totalFailed} failed`); + +if (failedInvariants.length > 0) { + console.log('\nFailed:'); + for (const f of failedInvariants) console.log(` ✗ ${f.name}`); +} + +log(`\n---\n## Summary\n- Passed: ${totalPassed}\n- Failed: ${totalFailed}\n`); +if (failedInvariants.length > 0) { + log('### Failed invariants\n' + failedInvariants.map(f => `- ${f.name}`).join('\n') + '\n'); +} + +console.log(`\nFull log: ${LOG_FILE}\n`); + +if (totalFailed > 0) process.exit(1); diff --git a/test/policy-planning.test.js b/test/policy-planning.test.js new file mode 100644 index 00000000..75d210fc --- /dev/null +++ b/test/policy-planning.test.js @@ -0,0 +1,116 @@ +'use strict'; + +const assert = require('assert'); +const PolicyEngine = require('../lib/policy-engine'); + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + passed++; + } catch (e) { + console.error(` ✗ ${name}: ${e.message}`); + failed++; + } +} + +const homey = { log() {} }; +const SETTINGS = { + tariff_type: 'dynamic', + battery_efficiency: 0.7415781214938163, + min_soc: 0, + max_soc: 100, + cycle_cost_per_kwh: 0.075, + max_charge_price: 0.12, + min_discharge_price: 0.25, + respect_minmax: true, + policy_mode: 'balanced', +}; + +function makeEngine() { + return new PolicyEngine(homey, { ...SETTINGS }); +} + +console.log('\nPolicyEngine — charge/planning regressions\n'); + +test('runtime charge uses dynamic max and keeps to_full when PV does not provide net surplus', () => { + const engine = makeEngine(); + const mode = engine._mapPolicyToHwMode('charge', { + policyMode: 'balanced', + dynamicMaxChargePrice: 0.175, + battery: { + stateOfCharge: 11, + maxChargePowerW: 1600, + }, + tariff: { + currentPrice: 0.164, + }, + p1: { + resolved_gridPower: 15, + battery_power: 0, + pv_power_estimated: 1974, + avg_consumption_w: 1989, + }, + }); + assert.strictEqual(mode, 'to_full'); +}); + +test('planning mapping keeps charge as to_full on cheap hours with PV but no surplus', () => { + const engine = makeEngine(); + const mapped = engine._mapActionToHwModeForPlanning('charge', { + price: 0.164, + soc: 11, + pvW: 1974, + consumptionW: 1989, + tariffType: 'dynamic', + userPolicyMode: 'balanced', + maxChargePrice: 0.175, + minDischargePrice: 0.25, + minSoc: 0, + maxSoc: 100, + futurePrices: [], + battChargePowerW: 1600, + }); + assert.strictEqual(mapped.hwMode, 'to_full'); +}); + +test('planning schedule uses actual battery capacity for SoC projection', () => { + const engine = makeEngine(); + const start = new Date('2026-05-12T13:00:00.000Z'); + const slots = [ + { + timestamp: start.toISOString(), + action: 'preserve', + price: 0.21, + socProjected: 0, + consumptionW: 0, + }, + { + timestamp: new Date(start.getTime() + 3_600_000).toISOString(), + action: 'preserve', + price: 0.22, + socProjected: 0, + consumptionW: 0, + }, + ]; + const pvForecast = [ + { timestamp: slots[0].timestamp, pvPowerW: 1600 }, + { timestamp: slots[1].timestamp, pvPowerW: 1600 }, + ]; + const schedule = engine.buildPlanningSchedule( + slots, + pvForecast, + null, + 1600, + 0.175, + 5.376 + ); + assert.ok(schedule[1].socProjected > 29 && schedule[1].socProjected < 31, + `Expected ~30% SoC after 1h @ 1600W on 5.376kWh, got ${schedule[1].socProjected}%`); +}); + +console.log(`\n${passed} passed, ${failed} failed\n`); +if (failed > 0) process.exit(1); diff --git a/test/terminal-value.test.js b/test/terminal-value.test.js new file mode 100644 index 00000000..152e40be --- /dev/null +++ b/test/terminal-value.test.js @@ -0,0 +1,132 @@ +'use strict'; + +const assert = require('assert'); +const OptimizationEngine = require('../lib/optimization-engine'); + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + passed++; + } catch (e) { + console.error(` ✗ ${name}: ${e.message}`); + failed++; + } +} + +// Build 24 hourly price slots starting 1h from now. +function makePrices(n = 24, basePrice = 0.20) { + const start = new Date(); + start.setMinutes(0, 0, 0); + start.setHours(start.getHours() + 1); + return Array.from({ length: n }, (_, i) => ({ + timestamp: new Date(start.getTime() + i * 3_600_000).toISOString(), + price: basePrice + })); +} + +const CAPACITY_KWH = 2.688; +const MAX_POWER_W = 800; +const SETTINGS = { battery_efficiency: 0.90, min_soc: 0, max_soc: 95, cycle_cost_per_kwh: 0 }; + +function profit(pvKwhTomorrow, soc = 50) { + const eng = new OptimizationEngine(SETTINGS); + const result = eng.computeExpectedProfit( + makePrices(), soc, CAPACITY_KWH, MAX_POWER_W, MAX_POWER_W, + null, null, null, 0, 1.0, pvKwhTomorrow + ); + return result.profit; +} + +console.log('\nOptimizationEngine — terminal value\n'); + +// ── Empty battery + flat prices → no arbitrage, no terminal value ──────────── +// SoC = 0: nothing stored, flat prices = no spread → profit = 0. +test('SoC 0, flat prices, no PV tomorrow → profit = 0', () => { + const p = profit(0, 0); + assert.ok(Math.abs(p) < 0.01, `Expected ~0 profit on empty battery + flat prices, got ${p}`); +}); + +// ── Terminal value lifts profit when tomorrow has little PV ────────────────── +// Prices: cheap first half, expensive second half → DP should charge low, discharge high. +test('cheap/expensive price pattern → positive profit', () => { + const eng = new OptimizationEngine(SETTINGS); + const prices = makePrices(24).map((p, i) => ({ + ...p, + price: i < 8 ? 0.10 : i >= 16 ? 0.30 : 0.20 + })); + const result = eng.computeExpectedProfit( + prices, 10, CAPACITY_KWH, MAX_POWER_W, MAX_POWER_W, + null, null, null, 0, 1.0, 0 + ); + assert.ok(result.profit > 0, `Expected positive profit, got ${result.profit}`); +}); + +// ── pvKwhTomorrow ≥ 80% capacity → terminal value discounted to 0 ──────────── +// When tomorrow's PV can fully refill the battery, stored energy has no residual +// value — DP should empty the battery by end of horizon. +// With expensive discharge prices, profit with full PV refill ≥ profit without. +test('full PV tomorrow → profit >= no PV tomorrow (battery can be emptied freely)', () => { + const eng1 = new OptimizationEngine(SETTINGS); + const eng2 = new OptimizationEngine(SETTINGS); + const prices = makePrices(24).map((p, i) => ({ + ...p, price: i >= 18 ? 0.35 : 0.15 + })); + const pNoPv = eng1.computeExpectedProfit(prices, 50, CAPACITY_KWH, MAX_POWER_W, MAX_POWER_W, + null, null, null, 0, 1.0, 0).profit; + const pFullPv = eng2.computeExpectedProfit(prices, 50, CAPACITY_KWH, MAX_POWER_W, MAX_POWER_W, + null, null, null, 0, 1.0, CAPACITY_KWH).profit; + assert.ok(pFullPv >= pNoPv - 0.001, + `Expected full-PV profit (${pFullPv}) >= no-PV profit (${pNoPv})`); +}); + +// ── pvKwhTomorrow = 0 → terminal value active, held charge has value ────────── +// SoC 90%, flat low price: no discharge opportunity in horizon, but stored energy +// retains value via terminal → projected profit should be positive. +test('high SoC, flat low price, no PV tomorrow → terminal value > 0', () => { + const eng = new OptimizationEngine({ ...SETTINGS, cycle_cost_per_kwh: 0 }); + const prices = makePrices(24, 0.15); + const result = eng.computeExpectedProfit( + prices, 90, CAPACITY_KWH, MAX_POWER_W, MAX_POWER_W, + null, null, null, 0, 1.0, 0 + ); + assert.ok(result.profit > 0, `Expected positive terminal value, got ${result.profit}`); +}); + +// ── pvKwhTomorrow at 80% threshold → terminal factor ≈ 0 ───────────────────── +// At exactly pvRefill = 0.8 × capacity, terminalFactor = 1 - 0.8/0.8 = 0. +test('pvKwhTomorrow = 80% capacity → terminal value ≈ 0', () => { + const pvKwh80 = CAPACITY_KWH * 0.8; + const pvKwh79 = CAPACITY_KWH * 0.79; + const eng80 = new OptimizationEngine({ ...SETTINGS, cycle_cost_per_kwh: 0 }); + const eng79 = new OptimizationEngine({ ...SETTINGS, cycle_cost_per_kwh: 0 }); + const prices = makePrices(24, 0.15); + const p80 = eng80.computeExpectedProfit(prices, 90, CAPACITY_KWH, MAX_POWER_W, MAX_POWER_W, + null, null, null, 0, 1.0, pvKwh80).profit; + const p79 = eng79.computeExpectedProfit(prices, 90, CAPACITY_KWH, MAX_POWER_W, MAX_POWER_W, + null, null, null, 0, 1.0, pvKwh79).profit; + // At 80% threshold terminal = 0; just below 80% terminal > 0 → p79 >= p80 + assert.ok(p79 >= p80 - 0.001, + `Expected p79 (${p79.toFixed(4)}) >= p80 (${p80.toFixed(4)})`); +}); + +// ── computeExpectedProfit returns { profit, selfSufficiencyPct } ────────────── +test('return shape: { profit: number, selfSufficiencyPct: number }', () => { + const eng = new OptimizationEngine(SETTINGS); + const result = eng.computeExpectedProfit(makePrices(), 50, CAPACITY_KWH, MAX_POWER_W, MAX_POWER_W); + assert.strictEqual(typeof result.profit, 'number'); + assert.strictEqual(typeof result.selfSufficiencyPct, 'number'); +}); + +// ── Empty prices → returns { profit: 0, selfSufficiencyPct: 0 } ────────────── +test('empty prices → profit 0', () => { + const eng = new OptimizationEngine(SETTINGS); + const result = eng.computeExpectedProfit([], 50, CAPACITY_KWH, MAX_POWER_W, MAX_POWER_W); + assert.strictEqual(result.profit, 0); +}); + +console.log(`\n${passed} passed, ${failed} failed\n`); +if (failed > 0) process.exit(1); diff --git a/test/weather-attenuation.test.js b/test/weather-attenuation.test.js new file mode 100644 index 00000000..9d90d8ad --- /dev/null +++ b/test/weather-attenuation.test.js @@ -0,0 +1,85 @@ +'use strict'; + +const assert = require('assert'); +const WeatherForecaster = require('../lib/weather-forecaster'); + +const att = WeatherForecaster._weatherAttenuation.bind(WeatherForecaster); + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + passed++; + } catch (e) { + console.error(` ✗ ${name}: ${e.message}`); + failed++; + } +} + +function approx(a, b, eps = 0.001) { + assert.ok(Math.abs(a - b) <= eps, `Expected ~${b}, got ${a}`); +} + +console.log('\nWeatherForecaster._weatherAttenuation\n'); + +// ── No attenuation codes ───────────────────────────────────────────────────── +test('clear sky (code 0) → 1.0', () => assert.strictEqual(att(0), 1.0)); +test('partly cloudy (code 2) → 1.0', () => assert.strictEqual(att(2), 1.0)); +test('overcast (code 3) → 1.0', () => assert.strictEqual(att(3), 1.0)); +test('drizzle (code 51) → 1.0', () => assert.strictEqual(att(51), 1.0)); +test('light rain (code 61) → 1.0', () => assert.strictEqual(att(61), 1.0)); + +// ── Fog (bypass precipProb gate) ───────────────────────────────────────────── +test('fog (45) at 0% precipProb → 0.12 (no confidence gate)', () => approx(att(45, 0), 0.12)); +test('fog (45) at 100% precipProb → 0.12', () => approx(att(45, 100), 0.12)); +test('rime fog (48) → 0.12', () => approx(att(48, 100), 0.12)); + +// ── Snow ───────────────────────────────────────────────────────────────────── +test('heavy snow (75) at high precipProb → 0.05', () => approx(att(75, 100), 0.05)); +test('snow grains (77) at high precipProb → 0.05', () => approx(att(77, 100), 0.05)); +test('light snow (71) at high precipProb → 0.30', () => approx(att(71, 100), 0.30)); +test('moderate snow (73) at high precipProb → 0.30', () => approx(att(73, 100), 0.30)); +test('light snow showers (85) at high precipProb → 0.20', () => approx(att(85, 100), 0.20)); +test('heavy snow showers (86) at high precipProb → 0.10', () => approx(att(86, 100), 0.10)); + +// ── Rain ───────────────────────────────────────────────────────────────────── +test('heavy rain (65) at high precipProb → 0.50', () => approx(att(65, 100), 0.50)); +test('freezing rain (67) at high precipProb → 0.50', () => approx(att(67, 100), 0.50)); + +// ── Thunderstorm ───────────────────────────────────────────────────────────── +test('thunderstorm (95) at high precipProb → 0.30', () => approx(att(95, 100), 0.30)); +test('thunderstorm+hail (96) at high precipProb → 0.30', () => approx(att(96, 100), 0.30)); +test('thunderstorm+heavy hail (99) at high precipProb → 0.30', () => approx(att(99, 100), 0.30)); + +// ── Confidence gate (precipProb < 40) ──────────────────────────────────────── +test('heavy snow (75) at 0% precipProb → 1.0 (no attenuation)', () => approx(att(75, 0), 1.0)); +test('heavy snow (75) at 20% precipProb → blend 0.5', () => { + // confidence = 20/40 = 0.5; factor = 1.0 - (1 - 0.05) * 0.5 = 1.0 - 0.475 = 0.525 + approx(att(75, 20), 0.525); +}); +test('heavy snow (75) at 40% precipProb → full attenuation', () => approx(att(75, 40), 0.05)); +test('heavy snow (75) at 100% precipProb → full attenuation', () => approx(att(75, 100), 0.05)); + +// ── Default precipProb = 100 when omitted ──────────────────────────────────── +test('thunderstorm (95) with no precipProb arg → 0.30', () => approx(att(95), 0.30)); + +// ── Return type ────────────────────────────────────────────────────────────── +test('return value is always a number', () => { + assert.strictEqual(typeof att(0), 'number'); + assert.strictEqual(typeof att(45, 50), 'number'); + assert.strictEqual(typeof att(999, 0), 'number'); +}); +test('return value is always in [0, 1]', () => { + [0, 45, 48, 71, 73, 75, 77, 85, 86, 65, 67, 95, 96, 99].forEach(code => { + [0, 20, 40, 80, 100].forEach(prob => { + const f = att(code, prob); + assert.ok(f >= 0 && f <= 1, `code=${code} prob=${prob} → ${f} out of range`); + }); + }); +}); + +console.log(`\n${passed} passed, ${failed} failed\n`); +if (failed > 0) process.exit(1); diff --git a/test/ws-manager.test.js b/test/ws-manager.test.js new file mode 100644 index 00000000..f66d2eb9 --- /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', 'predictive']; + 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/widgets/battery-planning/api.js b/widgets/battery-planning/api.js new file mode 100644 index 00000000..bc3cc2af --- /dev/null +++ b/widgets/battery-planning/api.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = { + async getPlanningData({ homey }) { + try { + const devices = homey.drivers.getDriver('battery-policy').getDevices(); + const device = devices && devices[0]; + return device?._widgetData || null; + } catch (e) { + return null; + } + } +}; diff --git a/widgets/battery-planning/preview-dark.png b/widgets/battery-planning/preview-dark.png new file mode 100644 index 00000000..b625e0f5 Binary files /dev/null and b/widgets/battery-planning/preview-dark.png differ diff --git a/widgets/battery-planning/preview-light.png b/widgets/battery-planning/preview-light.png new file mode 100644 index 00000000..2a48003b Binary files /dev/null and b/widgets/battery-planning/preview-light.png differ diff --git a/widgets/battery-planning/public/index.html b/widgets/battery-planning/public/index.html new file mode 100644 index 00000000..6c4b2bdc --- /dev/null +++ b/widgets/battery-planning/public/index.html @@ -0,0 +1,439 @@ + + + + + + + + + +
      + Batterij planning + + +
      + +
      +
      Laden...
      +
      + + + +
      + +
      +
      Wacht op eerstvolgende beleidsrun…
      +
      + +
      + + + + diff --git a/widgets/battery-planning/widget.compose.json b/widgets/battery-planning/widget.compose.json new file mode 100644 index 00000000..5aebc84c --- /dev/null +++ b/widgets/battery-planning/widget.compose.json @@ -0,0 +1,14 @@ +{ + "name": { + "en": "Battery Planning", + "nl": "Batterij Planning" + }, + "height": 300, + "transparent": false, + "api": { + "getPlanningData": { + "method": "GET", + "path": "/" + } + } +} 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 +}