diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b66749f6..f03782df 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: Meter configuration - url: https://github.com/gskjold/AmsToMqttBridge/wiki/Known-hardware-configurations + url: https://github.com/UtilitechAS/amsreader-firmware/wiki/Known-hardware-configurations about: Please check your meter configuration here first. - name: Frequently asked questions - url: https://github.com/gskjold/AmsToMqttBridge/wiki/FAQ + url: https://github.com/UtilitechAS/amsreader-firmware/wiki/FAQ about: Please check frequently asked questions first. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 568eea05..d39778a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,7 @@ on: - scripts/** - web/** - platformio.ini + - .github/workflows/** branches: - '*' tags: @@ -22,6 +23,12 @@ jobs: steps: - name: Check out code from repo uses: actions/checkout@v1 + + - name: Inject secrets into ini file + run: | + sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini + sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini + - name: Cache Python dependencies uses: actions/cache@v1 with: @@ -40,6 +47,18 @@ jobs: run: | python -m pip install --upgrade pip pip install -U platformio css_html_js_minify + - name: Set up node + uses: actions/setup-node@v1 + with: + node-version: '16.x' + - name: Build with node + run: | + cd lib/SvelteUi/app + npm ci + npm run build + cd - + env: + CI: true - name: PlatformIO lib install run: pio lib install - name: PlatformIO run diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9f958c7..a3e5b4b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,12 @@ jobs: env: GITHUB_REF: ${{ github.ref }} run: echo "GITHUB_TAG=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV + + - name: Inject secrets into ini file + run: | + sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini + sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini + - name: Cache Python dependencies uses: actions/cache@v1 with: @@ -41,12 +47,23 @@ jobs: run: | python -m pip install --upgrade pip pip install -U platformio css_html_js_minify + + - name: Set up node + uses: actions/setup-node@v1 + with: + node-version: '16.x' + - name: Build with node + run: | + cd lib/SvelteUi/app + npm ci + npm run build + cd - + env: + CI: false + - name: PlatformIO lib install run: pio lib install - - name: PlatformIO run - run: pio run - - name: Create zip files - run: /bin/sh scripts/mkzip.sh + - name: Create Release id: create_release uses: actions/create-release@v1.0.0 @@ -58,6 +75,19 @@ jobs: draft: false prerelease: false + - name: Build esp8266 firmware + run: pio run -e esp8266 + - name: Create esp8266 zip file + run: /bin/sh scripts/esp8266/mkzip.sh + - name: Upload esp8266 binary to release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: .pio/build/esp8266/firmware.bin + asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin + asset_content_type: application/octet-stream - name: Upload esp8266 zip to release uses: actions/upload-release-asset@v1 env: @@ -67,6 +97,20 @@ jobs: asset_path: esp8266.zip asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.zip asset_content_type: application/zip + + - name: Build esp32 firmware + run: pio run -e esp32 + - name: Create esp32 zip file + run: /bin/sh scripts/esp32/mkzip.sh + - name: Upload esp32 binary to release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: .pio/build/esp32/firmware.bin + asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin + asset_content_type: application/octet-stream - name: Upload esp32 zip to release uses: actions/upload-release-asset@v1 env: @@ -76,6 +120,20 @@ jobs: asset_path: esp32.zip asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.zip asset_content_type: application/zip + + - name: Build esp32s2 firmware + run: pio run -e esp32s2 + - name: Create esp32s2 zip file + run: /bin/sh scripts/esp32s2/mkzip.sh + - name: Upload esp32s2 binary to release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: .pio/build/esp32s2/firmware.bin + asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin + asset_content_type: application/octet-stream - name: Upload esp32s2 zip to release uses: actions/upload-release-asset@v1 env: @@ -86,24 +144,10 @@ jobs: asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.zip asset_content_type: application/zip - - name: Upload esp8266 binary to release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: .pio/build/esp8266/firmware.bin - asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin - asset_content_type: application/octet-stream - - name: Upload esp32 binary to release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: .pio/build/esp32/firmware.bin - asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin - asset_content_type: application/octet-stream + - name: Build esp32solo firmware + run: pio run -e esp32solo + - name: Create esp32solo zip file + run: /bin/sh scripts/esp32solo/mkzip.sh - name: Upload esp32solo binary to release uses: actions/upload-release-asset@v1 env: @@ -113,12 +157,12 @@ jobs: asset_path: .pio/build/esp32solo/firmware.bin asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.bin asset_content_type: application/octet-stream - - name: Upload esp32s2 binary to release + - name: Upload esp32solo zip to release uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: .pio/build/esp32s2/firmware.bin - asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin - asset_content_type: application/octet-stream + asset_path: esp32solo.zip + asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.zip + asset_content_type: application/zip diff --git a/.gitignore b/.gitignore index 8fa788c6..a2cc6f3a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ .vscode .pio platformio-user.ini -/src/version.h +/lib/AmsConfiguration/include/version.h /src/web/root /src/AmsToMqttBridge.ino.cpp /test @@ -15,3 +15,6 @@ platformio-user.ini /sdkconfig /.tmp /*.zip +node_modules +/gui/dist +/scripts/*dev diff --git a/README.md b/README.md index c32a12f7..61ab2af8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# AMS MQTT Bridge +# AMS Reader This code is designed to decode data from electric smart meters installed in many countries in Europe these days. The data is presented in a graphical web interface and can also send the data to a MQTT broker which makes it suitable for home automation project. Originally it was only designed to work with Norwegian meters, but has since been adapter to read any IEC-62056-7-5 or IEC-62056-21 compliant meters. -Later development have added Energy usage graph for both day and month, as well as future energy price (Prices only available for ESP32). The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/gskjold/AmsToMqttBridge/wiki). If you don't have the knowledge to set up a ESP device yourself, have a look at the shop at [amsleser.no](https://amsleser.no/). +Later development have added Energy usage graph for both day and month, as well as future energy price. The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki). If you don't have the knowledge to set up a ESP device yourself, have a look at the shop at [amsleser.no](https://amsleser.no/). - + -Go to the [WiKi](https://github.com/gskjold/AmsToMqttBridge/wiki) for information on how to get your own device! And find the latest prebuilt firmware file at the [release section](https://github.com/gskjold/AmsToMqttBridge/releases). +Go to the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) for information on how to get your own device! And find the latest prebuilt firmware file at the [release section](https://github.com/UtilitechAS/amsreader-firmware/releases). ## Building this project with PlatformIO To build this project, you need [PlatformIO](https://platformio.org/) installed. diff --git a/hardware/README.md b/hardware/README.md index 422bcb43..fd2bf581 100644 --- a/hardware/README.md +++ b/hardware/README.md @@ -1 +1 @@ -[See Hardware page in Wiki](https://github.com/gskjold/AmsToMqttBridge/wiki) +[See Hardware page in Wiki](https://github.com/UtilitechAS/amsreader-firmware/wiki) diff --git a/hardware/v1/kicad/HAN_ESP_TSS721-backups/HAN_ESP_TSS721-2022-06-25_174436.zip b/hardware/v1/kicad/HAN_ESP_TSS721-backups/HAN_ESP_TSS721-2022-06-25_174436.zip new file mode 100644 index 00000000..cebef764 Binary files /dev/null and b/hardware/v1/kicad/HAN_ESP_TSS721-backups/HAN_ESP_TSS721-2022-06-25_174436.zip differ diff --git a/hardware/v1/kicad/HAN_ESP_TSS721-backups/HAN_ESP_TSS721-2022-06-25_190751.zip b/hardware/v1/kicad/HAN_ESP_TSS721-backups/HAN_ESP_TSS721-2022-06-25_190751.zip new file mode 100644 index 00000000..9fd0b14f Binary files /dev/null and b/hardware/v1/kicad/HAN_ESP_TSS721-backups/HAN_ESP_TSS721-2022-06-25_190751.zip differ diff --git a/hardware/v1/kicad/HAN_ESP_TSS721.kicad_prl b/hardware/v1/kicad/HAN_ESP_TSS721.kicad_prl new file mode 100644 index 00000000..4c89d5ae --- /dev/null +++ b/hardware/v1/kicad/HAN_ESP_TSS721.kicad_prl @@ -0,0 +1,76 @@ +{ + "board": { + "active_layer": 0, + "active_layer_preset": "", + "auto_track_width": true, + "hidden_nets": [], + "high_contrast_mode": 0, + "net_color_mode": 1, + "opacity": { + "pads": 1.0, + "tracks": 1.0, + "vias": 1.0, + "zones": 0.6 + }, + "ratsnest_display_mode": 0, + "selection_filter": { + "dimensions": true, + "footprints": true, + "graphics": true, + "keepouts": true, + "lockedItems": true, + "otherItems": true, + "pads": true, + "text": true, + "tracks": true, + "vias": true, + "zones": true + }, + "visible_items": [ + 0, + 1, + 2, + 3, + 4, + 5, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 32, + 33, + 34, + 35, + 36 + ], + "visible_layers": "fffffff_ffffffff", + "zone_display_mode": 0 + }, + "meta": { + "filename": "HAN_ESP_TSS721.kicad_prl", + "version": 3 + }, + "project": { + "files": [] + } +} diff --git a/hardware/v1/kicad/HAN_ESP_TSS721.kicad_pro b/hardware/v1/kicad/HAN_ESP_TSS721.kicad_pro new file mode 100644 index 00000000..1fbda6af --- /dev/null +++ b/hardware/v1/kicad/HAN_ESP_TSS721.kicad_pro @@ -0,0 +1,440 @@ +{ + "board": { + "design_settings": { + "defaults": { + "board_outline_line_width": 0.15, + "copper_line_width": 0.19999999999999998, + "copper_text_italic": false, + "copper_text_size_h": 1.5, + "copper_text_size_v": 1.5, + "copper_text_thickness": 0.3, + "copper_text_upright": false, + "courtyard_line_width": 0.049999999999999996, + "dimension_precision": 4, + "dimension_units": 3, + "dimensions": { + "arrow_length": 1270000, + "extension_offset": 500000, + "keep_text_aligned": true, + "suppress_zeroes": false, + "text_position": 0, + "units_format": 1 + }, + "fab_line_width": 0.09999999999999999, + "fab_text_italic": false, + "fab_text_size_h": 1.0, + "fab_text_size_v": 1.0, + "fab_text_thickness": 0.15, + "fab_text_upright": false, + "other_line_width": 0.09999999999999999, + "other_text_italic": false, + "other_text_size_h": 1.0, + "other_text_size_v": 1.0, + "other_text_thickness": 0.15, + "other_text_upright": false, + "pads": { + "drill": 0.762, + "height": 1.524, + "width": 1.524 + }, + "silk_line_width": 0.15, + "silk_text_italic": false, + "silk_text_size_h": 1.0, + "silk_text_size_v": 1.0, + "silk_text_thickness": 0.15, + "silk_text_upright": false, + "zones": { + "45_degree_only": true, + "min_clearance": 0.508 + } + }, + "diff_pair_dimensions": [], + "drc_exclusions": [], + "meta": { + "filename": "board_design_settings.json", + "version": 2 + }, + "rule_severities": { + "annular_width": "error", + "clearance": "error", + "copper_edge_clearance": "error", + "courtyards_overlap": "error", + "diff_pair_gap_out_of_range": "error", + "diff_pair_uncoupled_length_too_long": "error", + "drill_out_of_range": "error", + "duplicate_footprints": "warning", + "extra_footprint": "warning", + "footprint_type_mismatch": "error", + "hole_clearance": "error", + "hole_near_hole": "error", + "invalid_outline": "error", + "item_on_disabled_layer": "error", + "items_not_allowed": "error", + "length_out_of_range": "error", + "malformed_courtyard": "error", + "microvia_drill_out_of_range": "error", + "missing_courtyard": "ignore", + "missing_footprint": "warning", + "net_conflict": "warning", + "npth_inside_courtyard": "ignore", + "padstack": "error", + "pth_inside_courtyard": "ignore", + "shorting_items": "error", + "silk_over_copper": "warning", + "silk_overlap": "warning", + "skew_out_of_range": "error", + "through_hole_pad_without_hole": "error", + "too_many_vias": "error", + "track_dangling": "warning", + "track_width": "error", + "tracks_crossing": "error", + "unconnected_items": "error", + "unresolved_variable": "error", + "via_dangling": "warning", + "zone_has_empty_net": "error", + "zones_intersect": "error" + }, + "rules": { + "allow_blind_buried_vias": false, + "allow_microvias": false, + "max_error": 0.005, + "min_clearance": 0.0, + "min_copper_edge_clearance": 0.075, + "min_hole_clearance": 0.25, + "min_hole_to_hole": 0.25, + "min_microvia_diameter": 0.19999999999999998, + "min_microvia_drill": 0.09999999999999999, + "min_silk_clearance": 0.0, + "min_through_hole_diameter": 0.3, + "min_track_width": 0.19999999999999998, + "min_via_annular_width": 0.049999999999999996, + "min_via_diameter": 0.39999999999999997, + "use_height_for_length_calcs": true + }, + "track_widths": [ + 0.0, + 0.2, + 0.4, + 0.6, + 1.0 + ], + "via_dimensions": [], + "zones_allow_external_fillets": false, + "zones_use_no_outline": true + }, + "layer_presets": [] + }, + "boards": [], + "cvpcb": { + "equivalence_files": [] + }, + "erc": { + "erc_exclusions": [], + "meta": { + "version": 0 + }, + "pin_map": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 2 + ], + [ + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2 + ], + [ + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 2, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ] + ], + "rule_severities": { + "bus_definition_conflict": "error", + "bus_entry_needed": "error", + "bus_label_syntax": "error", + "bus_to_bus_conflict": "error", + "bus_to_net_conflict": "error", + "different_unit_footprint": "error", + "different_unit_net": "error", + "duplicate_reference": "error", + "duplicate_sheet_names": "error", + "extra_units": "error", + "global_label_dangling": "warning", + "hier_label_mismatch": "error", + "label_dangling": "error", + "lib_symbol_issues": "warning", + "multiple_net_names": "warning", + "net_not_bus_member": "warning", + "no_connect_connected": "warning", + "no_connect_dangling": "warning", + "pin_not_connected": "error", + "pin_not_driven": "error", + "pin_to_pin": "warning", + "power_pin_not_driven": "error", + "similar_labels": "warning", + "unannotated": "error", + "unit_value_mismatch": "error", + "unresolved_variable": "error", + "wire_dangling": "error" + } + }, + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "HAN_ESP_TSS721.kicad_pro", + "version": 1 + }, + "net_settings": { + "classes": [ + { + "bus_width": 12.0, + "clearance": 0.2, + "diff_pair_gap": 0.25, + "diff_pair_via_gap": 0.25, + "diff_pair_width": 0.2, + "line_style": 0, + "microvia_diameter": 0.3, + "microvia_drill": 0.1, + "name": "Default", + "pcb_color": "rgba(0, 0, 0, 0.000)", + "schematic_color": "rgba(0, 0, 0, 0.000)", + "track_width": 0.25, + "via_diameter": 0.6, + "via_drill": 0.4, + "wire_width": 6.0 + }, + { + "bus_width": 12.0, + "clearance": 0.5, + "diff_pair_gap": 0.25, + "diff_pair_via_gap": 0.25, + "diff_pair_width": 0.2, + "line_style": 0, + "microvia_diameter": 0.5, + "microvia_drill": 0.2, + "name": "PWR", + "nets": [ + "+3V3" + ], + "pcb_color": "rgba(0, 0, 0, 0.000)", + "schematic_color": "rgba(0, 0, 0, 0.000)", + "track_width": 0.5, + "via_diameter": 0.8, + "via_drill": 0.6, + "wire_width": 6.0 + } + ], + "meta": { + "version": 2 + }, + "net_colors": null + }, + "pcbnew": { + "last_paths": { + "gencad": "", + "idf": "", + "netlist": "", + "specctra_dsn": "", + "step": "", + "vrml": "" + }, + "page_layout_descr_file": "" + }, + "schematic": { + "annotate_start_num": 0, + "drawing": { + "default_line_thickness": 6.0, + "default_text_size": 50.0, + "field_names": [], + "intersheets_ref_own_page": false, + "intersheets_ref_prefix": "", + "intersheets_ref_short": false, + "intersheets_ref_show": false, + "intersheets_ref_suffix": "", + "junction_size_choice": 3, + "label_size_ratio": 0.25, + "pin_symbol_size": 0.0, + "text_offset_ratio": 0.08 + }, + "legacy_lib_dir": "", + "legacy_lib_list": [], + "meta": { + "version": 1 + }, + "net_format_name": "", + "ngspice": { + "fix_include_paths": true, + "fix_passive_vals": false, + "meta": { + "version": 0 + }, + "model_mode": 0, + "workbook_filename": "" + }, + "page_layout_descr_file": "", + "plot_directory": "", + "spice_adjust_passive_values": false, + "spice_external_command": "spice \"%I\"", + "subpart_first_id": 65, + "subpart_id_separator": 0 + }, + "sheets": [], + "text_variables": {} +} diff --git a/hardware/v1/kicad/fp-info-cache b/hardware/v1/kicad/fp-info-cache new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/hardware/v1/kicad/fp-info-cache @@ -0,0 +1 @@ +0 diff --git a/hardware/wemos_mbus_shield/kicad/d1_mini_shield-backups/d1_mini_shield-2023-01-06_143235.zip b/hardware/wemos_mbus_shield/kicad/d1_mini_shield-backups/d1_mini_shield-2023-01-06_143235.zip new file mode 100644 index 00000000..6281d2a3 Binary files /dev/null and b/hardware/wemos_mbus_shield/kicad/d1_mini_shield-backups/d1_mini_shield-2023-01-06_143235.zip differ diff --git a/hardware/wemos_mbus_shield/kicad/d1_mini_shield-rescue.dcm b/hardware/wemos_mbus_shield/kicad/d1_mini_shield-rescue.dcm new file mode 100644 index 00000000..5f3ed79b --- /dev/null +++ b/hardware/wemos_mbus_shield/kicad/d1_mini_shield-rescue.dcm @@ -0,0 +1,3 @@ +EESchema-DOCLIB Version 2.0 +# +#End Doc Library diff --git a/hardware/wemos_mbus_shield/kicad/d1_mini_shield-rescue.lib b/hardware/wemos_mbus_shield/kicad/d1_mini_shield-rescue.lib index e33391c9..53ec0cf4 100644 --- a/hardware/wemos_mbus_shield/kicad/d1_mini_shield-rescue.lib +++ b/hardware/wemos_mbus_shield/kicad/d1_mini_shield-rescue.lib @@ -1,6 +1,21 @@ EESchema-LIBRARY Version 2.4 #encoding utf-8 # +# +3.3V-power +# +DEF +3.3V-power #PWR 0 0 Y Y 1 F P +F0 "#PWR" 0 -150 50 H I C CNN +F1 "+3.3V-power" 0 140 50 H V C CNN +F2 "" 0 0 50 H I C CNN +F3 "" 0 0 50 H I C CNN +DRAW +P 2 0 1 0 -30 50 0 100 N +P 2 0 1 0 0 0 0 100 N +P 2 0 1 0 0 100 30 50 N +X +3V3 1 0 0 0 U 50 50 1 1 W N +ENDDRAW +ENDDEF +# # CONN_01X08 # DEF CONN_01X08 P 0 40 Y N 1 F N @@ -35,4 +50,23 @@ X P8 8 -200 -350 150 R 50 50 1 1 P ENDDRAW ENDDEF # +# Jumper-Device +# +DEF Jumper-Device JP 0 30 Y N 1 F N +F0 "JP" 0 150 50 H V C CNN +F1 "Jumper-Device" 0 -80 50 H V C CNN +F2 "" 0 0 50 H I C CNN +F3 "" 0 0 50 H I C CNN +$FPLIST + SolderJumper* +$ENDFPLIST +DRAW +C -100 0 35 0 1 0 N +A 0 -26 125 375 1422 0 1 0 N 99 50 -98 50 +C 100 0 35 0 1 0 N +X 1 1 -300 0 165 R 50 50 0 1 P +X 2 2 300 0 165 L 50 50 0 1 P +ENDDRAW +ENDDEF +# #End Library diff --git a/hardware/wemos_mbus_shield/kicad/d1_mini_shield.kicad_prl b/hardware/wemos_mbus_shield/kicad/d1_mini_shield.kicad_prl new file mode 100644 index 00000000..2528e579 --- /dev/null +++ b/hardware/wemos_mbus_shield/kicad/d1_mini_shield.kicad_prl @@ -0,0 +1,75 @@ +{ + "board": { + "active_layer": 0, + "active_layer_preset": "", + "auto_track_width": true, + "hidden_nets": [], + "high_contrast_mode": 0, + "net_color_mode": 1, + "opacity": { + "pads": 1.0, + "tracks": 1.0, + "vias": 1.0, + "zones": 0.6 + }, + "ratsnest_display_mode": 0, + "selection_filter": { + "dimensions": true, + "footprints": true, + "graphics": true, + "keepouts": true, + "lockedItems": true, + "otherItems": true, + "pads": true, + "text": true, + "tracks": true, + "vias": true, + "zones": true + }, + "visible_items": [ + 0, + 1, + 2, + 3, + 4, + 5, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 32, + 33, + 34, + 35, + 36 + ], + "visible_layers": "fffffff_ffffffff", + "zone_display_mode": 0 + }, + "meta": { + "filename": "d1_mini_shield.kicad_prl", + "version": 3 + }, + "project": { + "files": [] + } +} diff --git a/hardware/wemos_mbus_shield/kicad/d1_mini_shield.kicad_pro b/hardware/wemos_mbus_shield/kicad/d1_mini_shield.kicad_pro new file mode 100644 index 00000000..7ff505c9 --- /dev/null +++ b/hardware/wemos_mbus_shield/kicad/d1_mini_shield.kicad_pro @@ -0,0 +1,356 @@ +{ + "board": { + "design_settings": { + "defaults": { + "board_outline_line_width": 0.15, + "copper_line_width": 0.2, + "copper_text_italic": false, + "copper_text_size_h": 1.5, + "copper_text_size_v": 1.5, + "copper_text_thickness": 0.3, + "copper_text_upright": true, + "courtyard_line_width": 0.05, + "other_line_width": 0.15, + "other_text_italic": false, + "other_text_size_h": 1.0, + "other_text_size_v": 1.0, + "other_text_thickness": 0.15, + "other_text_upright": true, + "silk_line_width": 0.15, + "silk_text_italic": false, + "silk_text_size_h": 1.0, + "silk_text_size_v": 1.0, + "silk_text_thickness": 0.15, + "silk_text_upright": true + }, + "diff_pair_dimensions": [ + { + "gap": 0.25, + "via_gap": 0.25, + "width": 0.2 + } + ], + "drc_exclusions": [], + "rule_severitieslegacy_courtyards_overlap": true, + "rule_severitieslegacy_no_courtyard_defined": false, + "rules": { + "allow_blind_buried_vias": false, + "allow_microvias": false, + "min_hole_to_hole": 0.25, + "min_microvia_diameter": 0.2, + "min_microvia_drill": 0.09999999999999999, + "min_through_hole_diameter": 0.3, + "min_track_width": 0.2, + "min_via_diameter": 0.4, + "solder_mask_clearance": 0.2, + "solder_mask_min_width": 0.0, + "solder_paste_clearance": 0.0, + "solder_paste_margin_ratio": -0.0 + }, + "track_widths": [ + 0.25, + 0.5 + ], + "via_dimensions": [ + { + "diameter": 0.6, + "drill": 0.4 + } + ] + }, + "layer_presets": [] + }, + "boards": [], + "cvpcb": { + "equivalence_files": [] + }, + "erc": { + "erc_exclusions": [], + "meta": { + "version": 0 + }, + "pin_map": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 2 + ], + [ + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2 + ], + [ + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 2, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ] + ], + "rule_severities": { + "bus_definition_conflict": "error", + "bus_entry_needed": "error", + "bus_label_syntax": "error", + "bus_to_bus_conflict": "error", + "bus_to_net_conflict": "error", + "different_unit_footprint": "error", + "different_unit_net": "error", + "duplicate_reference": "error", + "duplicate_sheet_names": "error", + "extra_units": "error", + "global_label_dangling": "warning", + "hier_label_mismatch": "error", + "label_dangling": "error", + "lib_symbol_issues": "warning", + "multiple_net_names": "warning", + "net_not_bus_member": "warning", + "no_connect_connected": "warning", + "no_connect_dangling": "warning", + "pin_not_connected": "error", + "pin_not_driven": "error", + "pin_to_pin": "warning", + "power_pin_not_driven": "error", + "similar_labels": "warning", + "unannotated": "error", + "unit_value_mismatch": "error", + "unresolved_variable": "error", + "wire_dangling": "error" + } + }, + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "d1_mini_shield.kicad_pro", + "version": 1 + }, + "net_settings": { + "classes": [ + { + "bus_width": 12.0, + "clearance": 0.2, + "diff_pair_gap": 0.25, + "diff_pair_via_gap": 0.25, + "diff_pair_width": 0.2, + "line_style": 0, + "microvia_diameter": 0.3, + "microvia_drill": 0.1, + "name": "Default", + "pcb_color": "rgba(0, 0, 0, 0.000)", + "schematic_color": "rgba(0, 0, 0, 0.000)", + "track_width": 0.25, + "via_diameter": 0.8, + "via_drill": 0.4, + "wire_width": 6.0 + } + ], + "meta": { + "version": 2 + }, + "net_colors": null + }, + "pcbnew": { + "last_paths": { + "gencad": "", + "idf": "", + "netlist": "d1_mini_shield.net", + "specctra_dsn": "", + "step": "", + "vrml": "" + }, + "page_layout_descr_file": "" + }, + "schematic": { + "annotate_start_num": 0, + "drawing": { + "default_line_thickness": 6.0, + "default_text_size": 50.0, + "field_names": [], + "intersheets_ref_own_page": false, + "intersheets_ref_prefix": "", + "intersheets_ref_short": false, + "intersheets_ref_show": false, + "intersheets_ref_suffix": "", + "junction_size_choice": 3, + "label_size_ratio": 0.25, + "pin_symbol_size": 0.0, + "text_offset_ratio": 0.08 + }, + "legacy_lib_dir": "", + "legacy_lib_list": [], + "meta": { + "version": 1 + }, + "net_format_name": "Pcbnew", + "ngspice": { + "fix_include_paths": true, + "fix_passive_vals": false, + "meta": { + "version": 0 + }, + "model_mode": 0, + "workbook_filename": "" + }, + "page_layout_descr_file": "", + "plot_directory": "", + "spice_adjust_passive_values": false, + "spice_external_command": "spice \"%I\"", + "subpart_first_id": 65, + "subpart_id_separator": 0 + }, + "sheets": [], + "text_variables": {} +} diff --git a/images/dashboard.png b/images/dashboard.png index 29eb7992..66083b87 100644 Binary files a/images/dashboard.png and b/images/dashboard.png differ diff --git a/images/dayplot.png b/images/dayplot.png index 6e3098e2..0e58b925 100644 Binary files a/images/dayplot.png and b/images/dayplot.png differ diff --git a/images/future-energy-price.png b/images/future-energy-price.png index 119ed1d6..fa956585 100644 Binary files a/images/future-energy-price.png and b/images/future-energy-price.png differ diff --git a/images/main-header.png b/images/main-header.png index 738442a3..80cb3fa9 100644 Binary files a/images/main-header.png and b/images/main-header.png differ diff --git a/images/monthplot.png b/images/monthplot.png index 9d6a7f4f..d1d89bca 100644 Binary files a/images/monthplot.png and b/images/monthplot.png differ diff --git a/images/real-time-calculation.png b/images/real-time-calculation.png index e5af9129..2b20e3d4 100644 Binary files a/images/real-time-calculation.png and b/images/real-time-calculation.png differ diff --git a/images/sensor-displays.png b/images/sensor-displays.png index e06bf053..0d78fd1d 100644 Binary files a/images/sensor-displays.png and b/images/sensor-displays.png differ diff --git a/images/status-bar.png b/images/status-bar.png deleted file mode 100644 index 37000f73..00000000 Binary files a/images/status-bar.png and /dev/null differ diff --git a/images/tempsensors.png b/images/tempsensors.png index 7484f170..47701be4 100644 Binary files a/images/tempsensors.png and b/images/tempsensors.png differ diff --git a/src/AmsConfiguration.h b/lib/AmsConfiguration/include/AmsConfiguration.h similarity index 81% rename from src/AmsConfiguration.h rename to lib/AmsConfiguration/include/AmsConfiguration.h index 1413e45d..531118fe 100644 --- a/src/AmsConfiguration.h +++ b/lib/AmsConfiguration/include/AmsConfiguration.h @@ -4,12 +4,14 @@ #include "Arduino.h" #define EEPROM_SIZE 1024*3 -#define EEPROM_CHECK_SUM 96 // Used to check if config is stored. Change if structure changes +#define EEPROM_CHECK_SUM 101 // Used to check if config is stored. Change if structure changes +#define EEPROM_CLEARED_INDICATOR 0xFC #define EEPROM_CONFIG_ADDRESS 0 #define EEPROM_TEMP_CONFIG_ADDRESS 2048 #define CONFIG_SYSTEM_START 8 #define CONFIG_METER_START 32 +#define CONFIG_UI_START 248 #define CONFIG_GPIO_START 266 #define CONFIG_ENTSOE_START 290 #define CONFIG_WIFI_START 360 @@ -29,7 +31,11 @@ struct SystemConfig { uint8_t boardType; -}; // 1 + bool vendorConfigured; + bool userConfigured; + uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined + char country[3]; +}; // 7 struct WiFiConfig91 { char ssid[32]; @@ -55,7 +61,9 @@ struct WiFiConfig { bool mdns; uint8_t power; uint8_t sleep; -}; // 211 + uint8_t mode; + bool autoreboot; +}; // 213 struct MqttConfig86 { char host[128]; @@ -88,6 +96,23 @@ struct WebConfig { }; // 129 struct MeterConfig { + uint32_t baud; + uint8_t parity; + bool invert; + uint8_t distributionSystem; + uint16_t mainFuse; + uint16_t productionCapacity; + uint8_t encryptionKey[16]; + uint8_t authenticationKey[16]; + uint32_t wattageMultiplier; + uint32_t voltageMultiplier; + uint32_t amperageMultiplier; + uint32_t accumulatedMultiplier; + uint8_t source; + uint8_t parser; +}; // 52 + +struct MeterConfig100 { uint32_t baud; uint8_t parity; bool invert; @@ -165,6 +190,13 @@ struct DomoticzConfig { }; // 10 struct NtpConfig { + bool enable; + bool dhcp; + char server[64]; + char timezone[32]; +}; // 98 + +struct NtpConfig96 { bool enable; bool dhcp; int16_t offset; @@ -177,6 +209,7 @@ struct EntsoeConfig { char area[17]; char currency[4]; uint32_t multiplier; + bool enabled; }; // 62 struct EnergyAccountingConfig { @@ -184,6 +217,20 @@ struct EnergyAccountingConfig { uint8_t hours; }; // 11 +struct UiConfig { + uint8_t showImport; + uint8_t showExport; + uint8_t showVoltage; + uint8_t showAmperage; + uint8_t showReactive; + uint8_t showRealtime; + uint8_t showPeaks; + uint8_t showPricePlot; + uint8_t showDayPlot; + uint8_t showMonthPlot; + uint8_t showTemperaturePlot; +}; // 11 + struct TempSensorConfig { uint8_t address[8]; char name[16]; @@ -260,6 +307,10 @@ public: bool isEnergyAccountingChanged(); void ackEnergyAccountingChange(); + bool getUiConfig(UiConfig&); + bool setUiConfig(UiConfig&); + void clearUiConfig(UiConfig&); + void loadTempSensors(); void saveTempSensors(); uint8_t getTempSensorCount(); @@ -280,14 +331,14 @@ private: uint8_t tempSensorCount = 0; TempSensorConfig** tempSensors = NULL; - bool relocateConfig86(); // 1.5.0 - bool relocateConfig87(); // 1.5.4 bool relocateConfig90(); // 2.0.0 bool relocateConfig91(); // 2.0.2 bool relocateConfig92(); // 2.0.3 bool relocateConfig93(); // 2.1.0 - bool relocateConfig94(); // 2.1.4 - bool relocateConfig95(); // 2.1.13 + bool relocateConfig94(); // 2.1.0 + bool relocateConfig95(); // 2.1.4 + bool relocateConfig96(); // 2.1.14 + bool relocateConfig100(); // 2.2-dev void saveToFs(); bool loadFromFs(uint8_t version); diff --git a/src/AmsStorage.h b/lib/AmsConfiguration/include/AmsStorage.h similarity index 100% rename from src/AmsStorage.h rename to lib/AmsConfiguration/include/AmsStorage.h diff --git a/lib/AmsConfiguration/include/Timezones.h b/lib/AmsConfiguration/include/Timezones.h new file mode 100644 index 00000000..1e7be4f4 --- /dev/null +++ b/lib/AmsConfiguration/include/Timezones.h @@ -0,0 +1,86 @@ +#include + +#define JULY1970 15634800 + +TimeChangeRule TC_GMT = {"GMT", Last, Sun, Jan, 0, 0}; +TimeChangeRule TC_WET = {"WET", Last, Sun, Oct, 2, 0}; +TimeChangeRule TC_WEST = {"WEST", Last, Sun, Mar, 1, 60}; +TimeChangeRule TC_CET = {"CET", Last, Sun, Oct, 3, 60}; +TimeChangeRule TC_CEST = {"CEST", Last, Sun, Mar, 2, 120}; +TimeChangeRule TC_EET = {"EET", Last, Sun, Oct, 4, 120}; +TimeChangeRule TC_EEST = {"EEST", Last, Sun, Mar, 3, 180}; + +Timezone GMT = Timezone(TC_GMT); +Timezone WesterEuropean = Timezone(TC_WET, TC_WEST); +Timezone CentralEuropean = Timezone(TC_CET, TC_CEST); +Timezone EasternEuropean = Timezone(TC_EET, TC_EEST); + +Timezone* resolveTimezone(char* name) { + if(strncmp_P(name, PSTR("Europe/"), 7) == 0) { + if(strncmp_P(name+7, PSTR("Amsterdam"), 9) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Athens"), 6) == 0) + return &EasternEuropean; + if(strncmp_P(name+7, PSTR("Belfast"), 7) == 0) + return &WesterEuropean; + if(strncmp_P(name+7, PSTR("Berlin"), 6) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Bratislava"), 10) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Brussels"), 8) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Bucharest"), 9) == 0) + return &EasternEuropean; + if(strncmp_P(name+7, PSTR("Budapest"), 8) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Copenhagen"), 10) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Dublin"), 6) == 0) + return &WesterEuropean; + if(strncmp_P(name+7, PSTR("Helsinki"), 8) == 0) + return &EasternEuropean; + if(strncmp_P(name+7, PSTR("Lisbon"), 6) == 0) + return &WesterEuropean; + if(strncmp_P(name+7, PSTR("Ljubljana"), 9) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("London"), 6) == 0) + return &WesterEuropean; + if(strncmp_P(name+7, PSTR("Luxembourg"), 10) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Madrid"), 6) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Malta"), 5) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Nicosia"), 7) == 0) + return &EasternEuropean; + if(strncmp_P(name+7, PSTR("Oslo"), 4) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Paris"), 5) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Podgorica"), 9) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Prague"), 6) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Riga"), 4) == 0) + return &EasternEuropean; + if(strncmp_P(name+7, PSTR("Rome"), 4) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Sofia"), 5) == 0) + return &EasternEuropean; + if(strncmp_P(name+7, PSTR("Stockholm"), 9) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Tallinn"), 7) == 0) + return &EasternEuropean; + if(strncmp_P(name+7, PSTR("Vienna"), 6) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Vilnius"), 7) == 0) + return &EasternEuropean; + if(strncmp_P(name+7, PSTR("Warsaw"), 6) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Zagreb"), 6) == 0) + return &CentralEuropean; + if(strncmp_P(name+7, PSTR("Zurich"), 6) == 0) + return &CentralEuropean; + } + return &GMT; +} diff --git a/src/hexutils.h b/lib/AmsConfiguration/include/hexutils.h similarity index 79% rename from src/hexutils.h rename to lib/AmsConfiguration/include/hexutils.h index c5fa278c..28e2c93d 100644 --- a/src/hexutils.h +++ b/lib/AmsConfiguration/include/hexutils.h @@ -7,5 +7,6 @@ String toHex(uint8_t* in); String toHex(uint8_t* in, uint16_t size); void fromHex(uint8_t *out, String in, uint16_t size); +void stripNonAscii(uint8_t* in, uint16_t size); #endif \ No newline at end of file diff --git a/src/AmsConfiguration.cpp b/lib/AmsConfiguration/src/AmsConfiguration.cpp similarity index 82% rename from src/AmsConfiguration.cpp rename to lib/AmsConfiguration/src/AmsConfiguration.cpp index 07a14b63..3cbbf751 100644 --- a/src/AmsConfiguration.cpp +++ b/lib/AmsConfiguration/src/AmsConfiguration.cpp @@ -1,18 +1,26 @@ #include "AmsConfiguration.h" +#include "hexutils.h" bool AmsConfiguration::getSystemConfig(SystemConfig& config) { - if(hasConfig()) { - EEPROM.begin(EEPROM_SIZE); + EEPROM.begin(EEPROM_SIZE); + uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS); + if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) { EEPROM.get(CONFIG_SYSTEM_START, config); EEPROM.end(); return true; } else { + config.boardType = 0xFF; + config.vendorConfigured = false; + config.userConfigured = false; + config.dataCollectionConsent = 0; + strcpy(config.country, ""); return false; } } bool AmsConfiguration::setSystemConfig(SystemConfig& config) { EEPROM.begin(EEPROM_SIZE); + stripNonAscii((uint8_t*) config.country, 2); EEPROM.put(CONFIG_SYSTEM_START, config); bool ret = EEPROM.commit(); EEPROM.end(); @@ -48,9 +56,21 @@ bool AmsConfiguration::setWiFiConfig(WiFiConfig& config) { wifiChanged |= strcmp(config.hostname, existing.hostname) != 0; wifiChanged |= config.power != existing.power; wifiChanged |= config.sleep != existing.sleep; + wifiChanged |= config.mode != existing.mode; + wifiChanged |= config.autoreboot != existing.autoreboot; } else { wifiChanged = true; } + + stripNonAscii((uint8_t*) config.ssid, 32); + stripNonAscii((uint8_t*) config.psk, 64); + stripNonAscii((uint8_t*) config.ip, 16); + stripNonAscii((uint8_t*) config.gateway, 16); + stripNonAscii((uint8_t*) config.subnet, 16); + stripNonAscii((uint8_t*) config.dns1, 16); + stripNonAscii((uint8_t*) config.dns2, 16); + stripNonAscii((uint8_t*) config.hostname, 32); + EEPROM.begin(EEPROM_SIZE); EEPROM.put(CONFIG_WIFI_START, config); bool ret = EEPROM.commit(); @@ -119,6 +139,14 @@ bool AmsConfiguration::setMqttConfig(MqttConfig& config) { } else { mqttChanged = true; } + + stripNonAscii((uint8_t*) config.host, 128); + stripNonAscii((uint8_t*) config.clientId, 32); + stripNonAscii((uint8_t*) config.publishTopic, 64); + stripNonAscii((uint8_t*) config.subscribeTopic, 64); + stripNonAscii((uint8_t*) config.username, 128); + stripNonAscii((uint8_t*) config.password, 256); + EEPROM.begin(EEPROM_SIZE); EEPROM.put(CONFIG_MQTT_START, config); bool ret = EEPROM.commit(); @@ -163,6 +191,10 @@ bool AmsConfiguration::getWebConfig(WebConfig& config) { } bool AmsConfiguration::setWebConfig(WebConfig& config) { + + stripNonAscii((uint8_t*) config.username, 64); + stripNonAscii((uint8_t*) config.password, 64); + EEPROM.begin(EEPROM_SIZE); EEPROM.put(CONFIG_WEB_START, config); bool ret = EEPROM.commit(); @@ -210,11 +242,11 @@ bool AmsConfiguration::setMeterConfig(MeterConfig& config) { } void AmsConfiguration::clearMeter(MeterConfig& config) { - config.baud = 2400; - config.parity = 11; // 8E1 + config.baud = 0; + config.parity = 0; config.invert = false; - config.distributionSystem = 0; - config.mainFuse = 0; + config.distributionSystem = 2; + config.mainFuse = 40; config.productionCapacity = 0; memset(config.encryptionKey, 0, 16); memset(config.authenticationKey, 0, 16); @@ -222,6 +254,8 @@ void AmsConfiguration::clearMeter(MeterConfig& config) { config.voltageMultiplier = 0; config.amperageMultiplier = 0; config.accumulatedMultiplier = 0; + config.source = 1; // Serial + config.parser = 0; // Auto } bool AmsConfiguration::isMeterChanged() { @@ -430,12 +464,15 @@ bool AmsConfiguration::setNtpConfig(NtpConfig& config) { } } ntpChanged |= config.dhcp != existing.dhcp; - ntpChanged |= config.offset != existing.offset; - ntpChanged |= config.summerOffset != existing.summerOffset; ntpChanged |= strcmp(config.server, existing.server) != 0; + ntpChanged |= strcmp(config.timezone, existing.timezone) != 0; } else { ntpChanged = true; } + + stripNonAscii((uint8_t*) config.server, 64); + stripNonAscii((uint8_t*) config.timezone, 32); + EEPROM.begin(EEPROM_SIZE); EEPROM.put(CONFIG_NTP_START, config); bool ret = EEPROM.commit(); @@ -454,9 +491,8 @@ void AmsConfiguration::ackNtpChange() { void AmsConfiguration::clearNtp(NtpConfig& config) { config.enable = true; config.dhcp = true; - config.offset = 360; - config.summerOffset = 360; strcpy(config.server, "pool.ntp.org"); + strcpy(config.timezone, "Europe/Oslo"); } bool AmsConfiguration::getEntsoeConfig(EntsoeConfig& config) { @@ -480,9 +516,15 @@ bool AmsConfiguration::setEntsoeConfig(EntsoeConfig& config) { entsoeChanged |= strcmp(config.area, existing.area) != 0; entsoeChanged |= strcmp(config.currency, existing.currency) != 0; entsoeChanged |= config.multiplier != existing.multiplier; + entsoeChanged |= config.enabled != existing.enabled; } else { entsoeChanged = true; } + + stripNonAscii((uint8_t*) config.token, 37); + stripNonAscii((uint8_t*) config.area, 17); + stripNonAscii((uint8_t*) config.currency, 4); + EEPROM.begin(EEPROM_SIZE); EEPROM.put(CONFIG_ENTSOE_START, config); bool ret = EEPROM.commit(); @@ -540,7 +582,6 @@ bool AmsConfiguration::setEnergyAccountingConfig(EnergyAccountingConfig& config) bool ret = EEPROM.commit(); EEPROM.end(); return ret; - } void AmsConfiguration::clearEnergyAccountingConfig(EnergyAccountingConfig& config) { @@ -565,9 +606,53 @@ void AmsConfiguration::ackEnergyAccountingChange() { energyAccountingChanged = false; } +bool AmsConfiguration::getUiConfig(UiConfig& config) { + if(hasConfig()) { + EEPROM.begin(EEPROM_SIZE); + EEPROM.get(CONFIG_UI_START, config); + if(config.showImport > 2) clearUiConfig(config); // Must be wrong + EEPROM.end(); + return true; + } else { + clearUiConfig(config); + return false; + } +} + +bool AmsConfiguration::setUiConfig(UiConfig& config) { + EEPROM.begin(EEPROM_SIZE); + EEPROM.put(CONFIG_UI_START, config); + bool ret = EEPROM.commit(); + EEPROM.end(); + return ret; +} + +void AmsConfiguration::clearUiConfig(UiConfig& config) { + // 1 = Always, 2 = If value present, 0 = Hidden + config.showImport = 1; + config.showExport = 2; + config.showVoltage = 2; + config.showAmperage = 2; + config.showReactive = 0; + config.showRealtime = 1; + config.showPeaks = 2; + config.showPricePlot = 2; + config.showDayPlot = 1; + config.showMonthPlot = 1; + config.showTemperaturePlot = 2; +} + void AmsConfiguration::clear() { EEPROM.begin(EEPROM_SIZE); + + SystemConfig sys; + EEPROM.get(CONFIG_SYSTEM_START, sys); + sys.userConfigured = false; + sys.dataCollectionConsent = 0; + strcpy(sys.country, ""); + EEPROM.put(CONFIG_SYSTEM_START, sys); + MeterConfig meter; clearMeter(meter); EEPROM.put(CONFIG_METER_START, meter); @@ -600,7 +685,15 @@ void AmsConfiguration::clear() { clearEnergyAccountingConfig(eac); EEPROM.put(CONFIG_ENERGYACCOUNTING_START, eac); - EEPROM.put(EEPROM_CONFIG_ADDRESS, -1); + DebugConfig debug; + clearDebug(debug); + EEPROM.put(CONFIG_DEBUG_START, debug); + + UiConfig ui; + clearUiConfig(ui); + EEPROM.put(CONFIG_UI_START, ui); + + EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR); EEPROM.commit(); EEPROM.end(); } @@ -619,22 +712,6 @@ bool AmsConfiguration::hasConfig() { } } else { switch(configVersion) { - case 86: - configVersion = -1; // Prevent loop - if(relocateConfig86()) { - configVersion = 87; - } else { - configVersion = 0; - return false; - } - case 87: - configVersion = -1; // Prevent loop - if(relocateConfig87()) { - configVersion = 88; - } else { - configVersion = 0; - return false; - } case 90: configVersion = -1; // Prevent loop if(relocateConfig90()) { @@ -683,6 +760,22 @@ bool AmsConfiguration::hasConfig() { configVersion = 0; return false; } + case 96: + configVersion = -1; // Prevent loop + if(relocateConfig96()) { + configVersion = 100; + } else { + configVersion = 0; + return false; + } + case 100: + configVersion = -1; // Prevent loop + if(relocateConfig100()) { + configVersion = 101; + } else { + configVersion = 0; + return false; + } case EEPROM_CHECK_SUM: return true; default: @@ -735,51 +828,6 @@ void AmsConfiguration::saveTempSensors() { } } -bool AmsConfiguration::relocateConfig86() { - MqttConfig86 mqtt86; - MqttConfig mqtt; - EEPROM.begin(EEPROM_SIZE); - EEPROM.get(CONFIG_MQTT_START_86, mqtt86); - strcpy(mqtt.host, mqtt86.host); - mqtt.port = mqtt86.port; - strcpy(mqtt.clientId, mqtt86.clientId); - strcpy(mqtt.publishTopic, mqtt86.publishTopic); - strcpy(mqtt.subscribeTopic, mqtt86.subscribeTopic); - strcpy(mqtt.username, mqtt86.username); - strcpy(mqtt.password, mqtt86.password); - mqtt.payloadFormat = mqtt86.payloadFormat; - mqtt.ssl = mqtt86.ssl; - EEPROM.put(CONFIG_MQTT_START, mqtt); - EEPROM.put(EEPROM_CONFIG_ADDRESS, 87); - bool ret = EEPROM.commit(); - EEPROM.end(); - return ret; -} - -bool AmsConfiguration::relocateConfig87() { - MeterConfig87 meter87 = {0,0,0,0,0,0,0}; - MeterConfig meter; - EEPROM.begin(EEPROM_SIZE); - EEPROM.get(CONFIG_METER_START_87, meter87); - if(meter87.type < 5) { - meter.baud = 2400; - meter.parity = meter87.type == 3 || meter87.type == 4 ? 3 : 11; - meter.invert = false; - } else { - meter.baud = 115200; - meter.parity = 3; - meter.invert = meter87.type == 6; - } - meter.distributionSystem = meter87.distributionSystem; - meter.mainFuse = meter87.mainFuse; - meter.productionCapacity = meter87.productionCapacity; - EEPROM.put(CONFIG_METER_START, meter); - EEPROM.put(EEPROM_CONFIG_ADDRESS, 88); - bool ret = EEPROM.commit(); - EEPROM.end(); - return ret; -} - bool AmsConfiguration::relocateConfig90() { EntsoeConfig entsoe; EEPROM.begin(EEPROM_SIZE); @@ -877,6 +925,112 @@ bool AmsConfiguration::relocateConfig95() { return ret; } +bool AmsConfiguration::relocateConfig96() { + EEPROM.begin(EEPROM_SIZE); + SystemConfig sys; + EEPROM.get(CONFIG_SYSTEM_START, sys); + + MeterConfig meter; + EEPROM.get(CONFIG_METER_START, meter); + meter.source = 1; // Serial + meter.parser = 0; // Auto + EEPROM.put(CONFIG_METER_START, meter); + + #if defined(ESP8266) + GpioConfig gpio; + EEPROM.get(CONFIG_GPIO_START, gpio); + + switch(sys.boardType) { + case 3: // Pow UART0 -- Now Pow-K UART0 + case 4: // Pow GPIO12 -- Now Pow-U UART0 + case 5: // Pow-K+ -- Now also Pow-K GPIO12 + case 7: // Pow-U+ -- Now also Pow-U GPIO12 + if(meter.baud == 2400 && meter.parity == 3) { // 3 == 8N1, assuming Pow-K + if(gpio.hanPin == 3) { // UART0 + sys.boardType = 3; + } else if(gpio.hanPin == 12) { + sys.boardType = 5; + } + } else { // Assuming Pow-U + if(gpio.hanPin == 3) { // UART0 + sys.boardType = 4; + } else if(gpio.hanPin == 12) { + sys.boardType = 7; + } + } + break; + } + #endif + + sys.vendorConfigured = true; + sys.userConfigured = true; + sys.dataCollectionConsent = 0; + strcpy(sys.country, ""); + EEPROM.put(CONFIG_SYSTEM_START, sys); + + WiFiConfig wifi; + EEPROM.get(CONFIG_WIFI_START, wifi); + wifi.mode = 1; // WIFI_STA + wifi.autoreboot = true; + EEPROM.put(CONFIG_WIFI_START, wifi); + + NtpConfig ntp; + NtpConfig96 ntp96; + EEPROM.get(CONFIG_NTP_START, ntp96); + ntp.enable = ntp96.enable; + ntp.dhcp = ntp96.dhcp; + if(ntp96.offset == 360 && ntp96.summerOffset == 360) { + strcpy(ntp.timezone, "Europe/Oslo"); + } else { + strcpy(ntp.timezone, "GMT"); + } + strcpy(ntp.server, ntp96.server); + EEPROM.put(CONFIG_NTP_START, ntp); + + EntsoeConfig entsoe; + EEPROM.get(CONFIG_ENTSOE_START, entsoe); + entsoe.enabled = strlen(entsoe.token) > 0; + EEPROM.put(CONFIG_ENTSOE_START, entsoe); + + EEPROM.put(EEPROM_CONFIG_ADDRESS, 100); + bool ret = EEPROM.commit(); + EEPROM.end(); + return ret; +} + +bool AmsConfiguration::relocateConfig100() { + EEPROM.begin(EEPROM_SIZE); + + MeterConfig100 meter100; + EEPROM.get(CONFIG_METER_START, meter100); + MeterConfig meter; + meter.baud = meter100.baud; + meter.parity = meter100.parity; + meter.invert = meter100.invert; + meter.distributionSystem = meter100.distributionSystem; + meter.mainFuse = meter100.mainFuse; + meter.productionCapacity = meter100.productionCapacity; + memcpy(meter.encryptionKey, meter100.encryptionKey, 16); + memcpy(meter.authenticationKey, meter100.authenticationKey, 16); + meter.wattageMultiplier = meter100.wattageMultiplier; + meter.voltageMultiplier = meter100.voltageMultiplier; + meter.amperageMultiplier = meter100.amperageMultiplier; + meter.accumulatedMultiplier = meter100.accumulatedMultiplier; + meter.source = meter100.source; + meter.parser = meter100.parser; + + EEPROM.put(CONFIG_METER_START, meter); + + UiConfig ui; + clearUiConfig(ui); + EEPROM.put(CONFIG_UI_START, ui); + + EEPROM.put(EEPROM_CONFIG_ADDRESS, 101); + bool ret = EEPROM.commit(); + EEPROM.end(); + return ret; +} + bool AmsConfiguration::save() { EEPROM.begin(EEPROM_SIZE); EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM); @@ -1080,8 +1234,7 @@ void AmsConfiguration::print(Print* debugger) debugger->println("--NTP configuration--"); debugger->printf("Enabled: %s\r\n", ntp.enable ? "Yes" : "No"); if(ntp.enable) { - debugger->printf("Offset: %i\r\n", ntp.offset); - debugger->printf("Summer offset: %i\r\n", ntp.summerOffset); + debugger->printf("Timezone: %s\r\n", ntp.timezone); debugger->printf("Server: %s\r\n", ntp.server); debugger->printf("DHCP: %s\r\n", ntp.dhcp ? "Yes" : "No"); } @@ -1092,12 +1245,12 @@ void AmsConfiguration::print(Print* debugger) EntsoeConfig entsoe; if(getEntsoeConfig(entsoe)) { - debugger->println("--ENTSO-E configuration--"); - debugger->printf("Token: %s\r\n", entsoe.token); - if(strlen(entsoe.token) > 0) { + if(strlen(entsoe.area) > 0) { + debugger->println("--ENTSO-E configuration--"); debugger->printf("Area: %s\r\n", entsoe.area); debugger->printf("Currency: %s\r\n", entsoe.currency); debugger->printf("Multiplier: %f\r\n", entsoe.multiplier / 1000.0); + debugger->printf("Token: %s\r\n", entsoe.token); } debugger->println(""); delay(10); diff --git a/src/hexutils.cpp b/lib/AmsConfiguration/src/hexutils.cpp similarity index 56% rename from src/hexutils.cpp rename to lib/AmsConfiguration/src/hexutils.cpp index 230cedc4..54f95412 100644 --- a/src/hexutils.cpp +++ b/lib/AmsConfiguration/src/hexutils.cpp @@ -20,4 +20,17 @@ void fromHex(uint8_t *out, String in, uint16_t size) { for(int i = 0; i < size*2; i += 2) { out[i/2] = strtol(in.substring(i, i+2).c_str(), 0, 16); } +} + +void stripNonAscii(uint8_t* in, uint16_t size) { + for(uint16_t i = 0; i < size; i++) { + if(in[i] == 0) { // Clear the rest with null-terminator + memset(in+i, 0, size-i); + break; + } + if(in[i] < 32 || in[i] > 126) { + memset(in+i, ' ', 1); + } + } + memset(in+size-1, 0, 1); // Make sure the last character is null-terminator } \ No newline at end of file diff --git a/src/AmsData.h b/lib/AmsData/include/AmsData.h similarity index 95% rename from src/AmsData.h rename to lib/AmsData/include/AmsData.h index 02e7df55..c28038b7 100644 --- a/src/AmsData.h +++ b/lib/AmsData/include/AmsData.h @@ -12,7 +12,6 @@ enum AmsType { AmsTypeIskra = 0x08, AmsTypeLandisGyr = 0x09, AmsTypeSagemcom = 0x0A, - AmsTypeLng = 0x0B, AmsTypeCustom = 0x88, AmsTypeUnknown = 0xFF }; @@ -70,6 +69,9 @@ public: bool isThreePhase(); bool isTwoPhase(); + int8_t getLastError(); + void setLastError(int8_t); + protected: unsigned long lastUpdateMillis = 0; unsigned long lastList2 = 0; @@ -84,6 +86,9 @@ protected: float powerFactor = 0, l1PowerFactor = 0, l2PowerFactor = 0, l3PowerFactor = 0; double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0; bool threePhase = false, twoPhase = false, counterEstimated = false; + + int8_t lastError = 0x00; + uint8_t lastErrorCount = 0; }; #endif diff --git a/src/mqtt/AmsMqttHandler.h b/lib/AmsData/include/AmsMqttHandler.h similarity index 96% rename from src/mqtt/AmsMqttHandler.h rename to lib/AmsData/include/AmsMqttHandler.h index 109c2a8d..7aab475e 100644 --- a/src/mqtt/AmsMqttHandler.h +++ b/lib/AmsData/include/AmsMqttHandler.h @@ -7,7 +7,7 @@ #include "AmsConfiguration.h" #include "EnergyAccounting.h" #include "HwTools.h" -#include "entsoe/EntsoeApi.h" +#include "EntsoeApi.h" class AmsMqttHandler { public: diff --git a/src/AmsData.cpp b/lib/AmsData/src/AmsData.cpp similarity index 89% rename from src/AmsData.cpp rename to lib/AmsData/src/AmsData.cpp index e6d536bf..8e892cca 100644 --- a/src/AmsData.cpp +++ b/lib/AmsData/src/AmsData.cpp @@ -64,7 +64,6 @@ void AmsData::apply(AmsData& other) { this->meterType = other.getMeterType(); this->meterModel = other.getMeterModel(); this->reactiveImportPower = other.getReactiveImportPower(); - this->activeExportPower = other.getActiveExportPower(); this->reactiveExportPower = other.getReactiveExportPower(); this->l1current = other.getL1Current(); this->l2current = other.getL2Current(); @@ -74,9 +73,13 @@ void AmsData::apply(AmsData& other) { this->l3voltage = other.getL3Voltage(); this->threePhase = other.isThreePhase(); this->twoPhase = other.isTwoPhase(); - case 1: - this->activeImportPower = other.getActiveImportPower(); } + + // Moved outside switch to handle meters alternating between sending active and accumulated values + if(other.getListType() == 1 || (other.getActiveImportPower() > 0 || other.getActiveExportPower() > 0)) + this->activeImportPower = other.getActiveImportPower(); + if(other.getListType() == 2 || (other.getActiveImportPower() > 0 || other.getActiveExportPower() > 0)) + this->activeExportPower = other.getActiveExportPower(); } unsigned long AmsData::getLastUpdateMillis() { @@ -214,3 +217,16 @@ bool AmsData::isThreePhase() { bool AmsData::isTwoPhase() { return this->twoPhase; } + +int8_t AmsData::getLastError() { + return lastErrorCount > 3 ? lastError : 0; +} + +void AmsData::setLastError(int8_t lastError) { + this->lastError = lastError; + if(lastError == 0) { + lastErrorCount = 0; + } else { + lastErrorCount++; + } +} \ No newline at end of file diff --git a/src/AmsDataStorage.h b/lib/AmsDataStorage/include/AmsDataStorage.h similarity index 69% rename from src/AmsDataStorage.h rename to lib/AmsDataStorage/include/AmsDataStorage.h index f542bc99..46e1bdb9 100644 --- a/src/AmsDataStorage.h +++ b/lib/AmsDataStorage/include/AmsDataStorage.h @@ -12,7 +12,8 @@ struct DayDataPoints { uint32_t activeImport; uint32_t activeExport; uint16_t hExport[24]; -}; // 112 bytes + uint8_t accuracy; +}; // 113 bytes struct MonthDataPoints { uint8_t version; @@ -21,17 +22,18 @@ struct MonthDataPoints { uint32_t activeImport; uint32_t activeExport; uint16_t dExport[31]; -}; // 141 bytes + uint8_t accuracy; +}; // 142 bytes class AmsDataStorage { public: AmsDataStorage(RemoteDebug*); void setTimezone(Timezone*); bool update(AmsData*); - int32_t getHourImport(uint8_t); - int32_t getHourExport(uint8_t); - int32_t getDayImport(uint8_t); - int32_t getDayExport(uint8_t); + uint32_t getHourImport(uint8_t); + uint32_t getHourExport(uint8_t); + uint32_t getDayImport(uint8_t); + uint32_t getDayExport(uint8_t); bool load(); bool save(); @@ -40,6 +42,11 @@ public: MonthDataPoints getMonthData(); bool setMonthData(MonthDataPoints&); + uint8_t getDayAccuracy(); + void setDayAccuracy(uint8_t); + uint8_t getMonthAccuracy(); + void setMonthAccuracy(uint8_t); + bool isHappy(); bool isDayHappy(); bool isMonthHappy(); @@ -50,19 +57,21 @@ private: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 10 }; MonthDataPoints month = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 10 }; RemoteDebug* debugger; - void setHourImport(uint8_t, int32_t); - void setHourExport(uint8_t, int32_t); - void setDayImport(uint8_t, int32_t); - void setDayExport(uint8_t, int32_t); + void setHourImport(uint8_t, uint32_t); + void setHourExport(uint8_t, uint32_t); + void setDayImport(uint8_t, uint32_t); + void setDayExport(uint8_t, uint32_t); }; #endif diff --git a/src/AmsDataStorage.cpp b/lib/AmsDataStorage/src/AmsDataStorage.cpp similarity index 75% rename from src/AmsDataStorage.cpp rename to lib/AmsDataStorage/src/AmsDataStorage.cpp index 5031f2e1..66c012e4 100644 --- a/src/AmsDataStorage.cpp +++ b/lib/AmsDataStorage/src/AmsDataStorage.cpp @@ -5,8 +5,10 @@ #include "version.h" AmsDataStorage::AmsDataStorage(RemoteDebug* debugger) { - day.version = 4; - month.version = 5; + day.version = 5; + day.accuracy = 1; + month.version = 6; + month.accuracy = 1; this->debugger = debugger; } @@ -237,44 +239,160 @@ bool AmsDataStorage::update(AmsData* data) { return ret; } -void AmsDataStorage::setHourImport(uint8_t hour, int32_t val) { +void AmsDataStorage::setHourImport(uint8_t hour, uint32_t val) { if(hour < 0 || hour > 24) return; - day.hImport[hour] = val / 10; + + uint8_t accuracy = day.accuracy; + uint32_t update = val / pow(10, accuracy); + while(update > UINT16_MAX) { + accuracy++; + update = val / pow(10, accuracy); + } + + if(accuracy != day.accuracy) { + setDayAccuracy(accuracy); + } + + day.hImport[hour] = update; + + uint32_t max = 0; + for(uint8_t i = 0; i < 24; i++) { + if(day.hImport[i] > max) + max = day.hImport[i]; + if(day.hExport[i] > max) + max = day.hExport[i]; + } + + while(max < UINT16_MAX/10 && accuracy > 0) { + accuracy--; + max = max*10; + } + + if(accuracy != day.accuracy) { + setDayAccuracy(accuracy); + } } -int32_t AmsDataStorage::getHourImport(uint8_t hour) { +uint32_t AmsDataStorage::getHourImport(uint8_t hour) { if(hour < 0 || hour > 24) return 0; - return day.hImport[hour] * 10; + return day.hImport[hour] * pow(10, day.accuracy); } -void AmsDataStorage::setHourExport(uint8_t hour, int32_t val) { +void AmsDataStorage::setHourExport(uint8_t hour, uint32_t val) { if(hour < 0 || hour > 24) return; - day.hExport[hour] = val / 10; + + uint8_t accuracy = day.accuracy; + uint32_t update = val / pow(10, accuracy); + while(update > UINT16_MAX) { + accuracy++; + update = val / pow(10, accuracy); + } + + if(accuracy != day.accuracy) { + setDayAccuracy(accuracy); + } + + day.hExport[hour] = update; + + uint32_t max = 0; + for(uint8_t i = 0; i < 24; i++) { + if(day.hImport[i] > max) + max = day.hImport[i]; + if(day.hExport[i] > max) + max = day.hExport[i]; + } + + while(max < UINT16_MAX/10 && accuracy > 0) { + accuracy--; + max = max*10; + } + + if(accuracy != day.accuracy) { + setDayAccuracy(accuracy); + } } -int32_t AmsDataStorage::getHourExport(uint8_t hour) { +uint32_t AmsDataStorage::getHourExport(uint8_t hour) { if(hour < 0 || hour > 24) return 0; - return day.hExport[hour] * 10; + return day.hExport[hour] * pow(10, day.accuracy); } -void AmsDataStorage::setDayImport(uint8_t day, int32_t val) { +void AmsDataStorage::setDayImport(uint8_t day, uint32_t val) { if(day < 1 || day > 31) return; - month.dImport[day-1] = val / 10; + + uint8_t accuracy = month.accuracy; + uint32_t update = val / pow(10, accuracy); + while(update > UINT16_MAX) { + accuracy++; + update = val / pow(10, accuracy); + } + + if(accuracy != month.accuracy) { + setMonthAccuracy(accuracy); + } + + month.dImport[day-1] = update; + + uint32_t max = 0; + for(uint8_t i = 0; i < 31; i++) { + if(month.dImport[i] > max) + max = month.dImport[i]; + if(month.dExport[i] > max) + max = month.dExport[i]; + } + + while(max < UINT16_MAX/10 && accuracy > 0) { + accuracy--; + max = max*10; + } + + if(accuracy != month.accuracy) { + setMonthAccuracy(accuracy); + } } -int32_t AmsDataStorage::getDayImport(uint8_t day) { +uint32_t AmsDataStorage::getDayImport(uint8_t day) { if(day < 1 || day > 31) return 0; - return (month.dImport[day-1] * 10); + return (month.dImport[day-1] * pow(10, month.accuracy)); } -void AmsDataStorage::setDayExport(uint8_t day, int32_t val) { +void AmsDataStorage::setDayExport(uint8_t day, uint32_t val) { if(day < 1 || day > 31) return; - month.dExport[day-1] = val / 10; + + uint8_t accuracy = month.accuracy; + uint32_t update = val / pow(10, accuracy); + while(update > UINT16_MAX) { + accuracy++; + update = val / pow(10, accuracy); + } + + if(accuracy != month.accuracy) { + setMonthAccuracy(accuracy); + } + + month.dExport[day-1] = update; + + uint32_t max = 0; + for(uint8_t i = 0; i < 31; i++) { + if(month.dImport[i] > max) + max = month.dImport[i]; + if(month.dExport[i] > max) + max = month.dExport[i]; + } + + while(max < UINT16_MAX/10 && accuracy > 0) { + accuracy--; + max = max*10; + } + + if(accuracy != month.accuracy) { + setMonthAccuracy(accuracy); + } } -int32_t AmsDataStorage::getDayExport(uint8_t day) { +uint32_t AmsDataStorage::getDayExport(uint8_t day) { if(day < 1 || day > 31) return 0; - return (month.dExport[day-1] * 10); + return (month.dExport[day-1] * pow(10, month.accuracy)); } bool AmsDataStorage::load() { @@ -348,31 +466,74 @@ MonthDataPoints AmsDataStorage::getMonthData() { } bool AmsDataStorage::setDayData(DayDataPoints& day) { - if(day.version == 4) { + if(day.version == 5) { this->day = day; return true; + } else if(day.version == 4) { + this->day = day; + this->day.accuracy = 1; + this->day.version = 5; + return true; } else if(day.version == 3) { this->day = day; for(uint8_t i = 0; i < 24; i++) this->day.hExport[i] = 0; - this->day.version = 4; + this->day.accuracy = 1; + this->day.version = 5; return true; } return false; } bool AmsDataStorage::setMonthData(MonthDataPoints& month) { - if(month.version == 5) { + if(month.version == 6) { this->month = month; return true; + } else if(month.version == 5) { + this->month = month; + this->month.accuracy = 1; + this->month.version = 6; + return true; } else if(month.version == 4) { this->month = month; for(uint8_t i = 0; i < 31; i++) this->month.dExport[i] = 0; - this->month.version = 5; + this->month.accuracy = 1; + this->month.version = 6; return true; } return false; } +uint8_t AmsDataStorage::getDayAccuracy() { + return day.accuracy; +} + +void AmsDataStorage::setDayAccuracy(uint8_t accuracy) { + if(day.accuracy != accuracy) { + uint16_t multiplier = pow(10, day.accuracy)/pow(10, accuracy); + for(uint8_t i = 0; i < 24; i++) { + day.hImport[i] = day.hImport[i] * multiplier; + day.hExport[i] = day.hExport[i] * multiplier; + } + day.accuracy = accuracy; + } +} + +uint8_t AmsDataStorage::getMonthAccuracy() { + return month.accuracy; +} + +void AmsDataStorage::setMonthAccuracy(uint8_t accuracy) { + if(month.accuracy != accuracy) { + uint16_t multiplier = pow(10, month.accuracy)/pow(10, accuracy); + for(uint8_t i = 0; i < 31; i++) { + month.dImport[i] = month.dImport[i] * multiplier; + month.dExport[i] = month.dExport[i] * multiplier; + } + month.accuracy = accuracy; + } + month.accuracy = accuracy; +} + bool AmsDataStorage::isHappy() { return isDayHappy() && isMonthHappy(); } diff --git a/src/ams/Cosem.h b/lib/AmsDecoder/include/Cosem.h similarity index 100% rename from src/ams/Cosem.h rename to lib/AmsDecoder/include/Cosem.h diff --git a/src/ams/DataParser.h b/lib/AmsDecoder/include/DataParser.h similarity index 100% rename from src/ams/DataParser.h rename to lib/AmsDecoder/include/DataParser.h diff --git a/src/ams/DataParsers.h b/lib/AmsDecoder/include/DataParsers.h similarity index 100% rename from src/ams/DataParsers.h rename to lib/AmsDecoder/include/DataParsers.h diff --git a/src/ams/DlmsParser.h b/lib/AmsDecoder/include/DlmsParser.h similarity index 100% rename from src/ams/DlmsParser.h rename to lib/AmsDecoder/include/DlmsParser.h diff --git a/src/ams/DsmrParser.h b/lib/AmsDecoder/include/DsmrParser.h similarity index 100% rename from src/ams/DsmrParser.h rename to lib/AmsDecoder/include/DsmrParser.h diff --git a/src/ams/GbtParser.h b/lib/AmsDecoder/include/GbtParser.h similarity index 100% rename from src/ams/GbtParser.h rename to lib/AmsDecoder/include/GbtParser.h diff --git a/src/ams/GcmParser.h b/lib/AmsDecoder/include/GcmParser.h similarity index 100% rename from src/ams/GcmParser.h rename to lib/AmsDecoder/include/GcmParser.h diff --git a/src/ams/HdlcParser.h b/lib/AmsDecoder/include/HdlcParser.h similarity index 100% rename from src/ams/HdlcParser.h rename to lib/AmsDecoder/include/HdlcParser.h diff --git a/src/ams/LlcParser.h b/lib/AmsDecoder/include/LlcParser.h similarity index 100% rename from src/ams/LlcParser.h rename to lib/AmsDecoder/include/LlcParser.h diff --git a/src/ams/MbusParser.h b/lib/AmsDecoder/include/MbusParser.h similarity index 100% rename from src/ams/MbusParser.h rename to lib/AmsDecoder/include/MbusParser.h diff --git a/src/ams/crc.h b/lib/AmsDecoder/include/crc.h similarity index 100% rename from src/ams/crc.h rename to lib/AmsDecoder/include/crc.h diff --git a/src/ams/ntohll.h b/lib/AmsDecoder/include/ntohll.h similarity index 100% rename from src/ams/ntohll.h rename to lib/AmsDecoder/include/ntohll.h diff --git a/src/ams/Cosem.cpp b/lib/AmsDecoder/src/Cosem.cpp similarity index 100% rename from src/ams/Cosem.cpp rename to lib/AmsDecoder/src/Cosem.cpp diff --git a/src/ams/DlmsParser.cpp b/lib/AmsDecoder/src/DlmsParser.cpp similarity index 100% rename from src/ams/DlmsParser.cpp rename to lib/AmsDecoder/src/DlmsParser.cpp diff --git a/src/ams/DsmrParser.cpp b/lib/AmsDecoder/src/DsmrParser.cpp similarity index 100% rename from src/ams/DsmrParser.cpp rename to lib/AmsDecoder/src/DsmrParser.cpp diff --git a/src/ams/GbtParser.cpp b/lib/AmsDecoder/src/GbtParser.cpp similarity index 100% rename from src/ams/GbtParser.cpp rename to lib/AmsDecoder/src/GbtParser.cpp diff --git a/src/ams/GcmParser.cpp b/lib/AmsDecoder/src/GcmParser.cpp similarity index 100% rename from src/ams/GcmParser.cpp rename to lib/AmsDecoder/src/GcmParser.cpp diff --git a/src/ams/HdlcParser.cpp b/lib/AmsDecoder/src/HdlcParser.cpp similarity index 100% rename from src/ams/HdlcParser.cpp rename to lib/AmsDecoder/src/HdlcParser.cpp diff --git a/src/ams/LlcParser.cpp b/lib/AmsDecoder/src/LlcParser.cpp similarity index 100% rename from src/ams/LlcParser.cpp rename to lib/AmsDecoder/src/LlcParser.cpp diff --git a/src/ams/MbusParser.cpp b/lib/AmsDecoder/src/MbusParser.cpp similarity index 100% rename from src/ams/MbusParser.cpp rename to lib/AmsDecoder/src/MbusParser.cpp diff --git a/src/ams/crc.cpp b/lib/AmsDecoder/src/crc.cpp similarity index 100% rename from src/ams/crc.cpp rename to lib/AmsDecoder/src/crc.cpp diff --git a/src/ams/ntohll.cpp b/lib/AmsDecoder/src/ntohll.cpp similarity index 100% rename from src/ams/ntohll.cpp rename to lib/AmsDecoder/src/ntohll.cpp diff --git a/web/application.js b/lib/ClassicUi/html/application.js similarity index 100% rename from web/application.js rename to lib/ClassicUi/html/application.js diff --git a/web/boot.css b/lib/ClassicUi/html/boot.css similarity index 100% rename from web/boot.css rename to lib/ClassicUi/html/boot.css diff --git a/web/configfile.html b/lib/ClassicUi/html/configfile.html similarity index 100% rename from web/configfile.html rename to lib/ClassicUi/html/configfile.html diff --git a/web/data.json b/lib/ClassicUi/html/data.json similarity index 85% rename from web/data.json rename to lib/ClassicUi/html/data.json index a713055a..02bf78d0 100644 --- a/web/data.json +++ b/lib/ClassicUi/html/data.json @@ -40,17 +40,20 @@ "h" : { "u" : %.2f, "c" : %.2f, - "p" : %.2f + "p" : %.2f, + "i" : %.2f }, "d" : { "u" : %.2f, "c" : %.2f, - "p" : %.2f + "p" : %.2f, + "i" : %.2f }, "m" : { "u" : %.2f, "c" : %.2f, - "p" : %.2f + "p" : %.2f, + "i" : %.2f } }, "c" : %u diff --git a/web/dayplot.json b/lib/ClassicUi/html/dayplot.json similarity index 100% rename from web/dayplot.json rename to lib/ClassicUi/html/dayplot.json diff --git a/web/debugging.html b/lib/ClassicUi/html/debugging.html similarity index 100% rename from web/debugging.html rename to lib/ClassicUi/html/debugging.html diff --git a/web/delete.html b/lib/ClassicUi/html/delete.html similarity index 100% rename from web/delete.html rename to lib/ClassicUi/html/delete.html diff --git a/web/domoticz.html b/lib/ClassicUi/html/domoticz.html similarity index 100% rename from web/domoticz.html rename to lib/ClassicUi/html/domoticz.html diff --git a/web/energyprice.json b/lib/ClassicUi/html/energyprice.json similarity index 100% rename from web/energyprice.json rename to lib/ClassicUi/html/energyprice.json diff --git a/web/firmware.html b/lib/ClassicUi/html/firmware.html similarity index 100% rename from web/firmware.html rename to lib/ClassicUi/html/firmware.html diff --git a/web/foot.html b/lib/ClassicUi/html/foot.html similarity index 100% rename from web/foot.html rename to lib/ClassicUi/html/foot.html diff --git a/web/github.svg b/lib/ClassicUi/html/github.svg similarity index 100% rename from web/github.svg rename to lib/ClassicUi/html/github.svg diff --git a/web/gpio.html b/lib/ClassicUi/html/gpio.html similarity index 100% rename from web/gpio.html rename to lib/ClassicUi/html/gpio.html diff --git a/web/head.html b/lib/ClassicUi/html/head.html similarity index 92% rename from web/head.html rename to lib/ClassicUi/html/head.html index 6e34c7aa..bc831aa2 100644 --- a/web/head.html +++ b/lib/ClassicUi/html/head.html @@ -48,7 +48,7 @@
-
AMS reader ${version}
+
AMS reader ${version}
    @@ -64,9 +64,9 @@ MQTT Web NTP - ENTSO-E API + Price API
    - Documentation + Documentation
@@ -96,7 +96,7 @@
  • - +
  • diff --git a/web/index.html b/lib/ClassicUi/html/index.html similarity index 100% rename from web/index.html rename to lib/ClassicUi/html/index.html diff --git a/web/meter.html b/lib/ClassicUi/html/meter.html similarity index 97% rename from web/meter.html rename to lib/ClassicUi/html/meter.html index 5659c543..076fcd60 100644 --- a/web/meter.html +++ b/lib/ClassicUi/html/meter.html @@ -65,7 +65,7 @@
    - Known hardware configurations + Known hardware configurations
    diff --git a/web/meteradvanced.html b/lib/ClassicUi/html/meteradvanced.html similarity index 100% rename from web/meteradvanced.html rename to lib/ClassicUi/html/meteradvanced.html diff --git a/web/monthplot.json b/lib/ClassicUi/html/monthplot.json similarity index 100% rename from web/monthplot.json rename to lib/ClassicUi/html/monthplot.json diff --git a/web/mqtt.html b/lib/ClassicUi/html/mqtt.html similarity index 100% rename from web/mqtt.html rename to lib/ClassicUi/html/mqtt.html diff --git a/web/notfound.html b/lib/ClassicUi/html/notfound.html similarity index 100% rename from web/notfound.html rename to lib/ClassicUi/html/notfound.html diff --git a/web/ntp.html b/lib/ClassicUi/html/ntp.html similarity index 100% rename from web/ntp.html rename to lib/ClassicUi/html/ntp.html diff --git a/web/entsoe.html b/lib/ClassicUi/html/priceapi.html similarity index 70% rename from web/entsoe.html rename to lib/ClassicUi/html/priceapi.html index 44b4847e..60dc4e6f 100644 --- a/web/entsoe.html +++ b/lib/ClassicUi/html/priceapi.html @@ -1,38 +1,31 @@
    -
    ENTSO-E API
    +
    Price API
    -
    -
    -
    - Token -
    - -
    -
    Region
    - - - - + + + +
    @@ -71,6 +64,14 @@
    +
    +
    +
    + ENTSO-E token +
    + +
    +

    diff --git a/web/reset.html b/lib/ClassicUi/html/reset.html similarity index 100% rename from web/reset.html rename to lib/ClassicUi/html/reset.html diff --git a/web/restart.html b/lib/ClassicUi/html/restart.html similarity index 100% rename from web/restart.html rename to lib/ClassicUi/html/restart.html diff --git a/web/restartwait.html b/lib/ClassicUi/html/restartwait.html similarity index 96% rename from web/restartwait.html rename to lib/ClassicUi/html/restartwait.html index 4ef22ae8..3d0071fd 100644 --- a/web/restartwait.html +++ b/lib/ClassicUi/html/restartwait.html @@ -12,7 +12,7 @@
    AMS reader ${version}
    • - +
    • diff --git a/web/setup.html b/lib/ClassicUi/html/setup.html similarity index 98% rename from web/setup.html rename to lib/ClassicUi/html/setup.html index 87d047f9..3c0b1e37 100644 --- a/web/setup.html +++ b/lib/ClassicUi/html/setup.html @@ -12,7 +12,7 @@
      AMS reader ${version}
      • - +
      • diff --git a/web/temperature.html b/lib/ClassicUi/html/temperature.html similarity index 100% rename from web/temperature.html rename to lib/ClassicUi/html/temperature.html diff --git a/web/tempsensor.json b/lib/ClassicUi/html/tempsensor.json similarity index 100% rename from web/tempsensor.json rename to lib/ClassicUi/html/tempsensor.json diff --git a/web/thresholds.html b/lib/ClassicUi/html/thresholds.html similarity index 100% rename from web/thresholds.html rename to lib/ClassicUi/html/thresholds.html diff --git a/web/upload.html b/lib/ClassicUi/html/upload.html similarity index 100% rename from web/upload.html rename to lib/ClassicUi/html/upload.html diff --git a/web/web.html b/lib/ClassicUi/html/web.html similarity index 100% rename from web/web.html rename to lib/ClassicUi/html/web.html diff --git a/web/wifi.html b/lib/ClassicUi/html/wifi.html similarity index 100% rename from web/wifi.html rename to lib/ClassicUi/html/wifi.html diff --git a/lib/ClassicUi/include/.gitignore b/lib/ClassicUi/include/.gitignore new file mode 100644 index 00000000..1530ce82 --- /dev/null +++ b/lib/ClassicUi/include/.gitignore @@ -0,0 +1 @@ +root/*.h diff --git a/src/web/AmsWebHeaders.h b/lib/ClassicUi/include/AmsWebHeaders.h similarity index 100% rename from src/web/AmsWebHeaders.h rename to lib/ClassicUi/include/AmsWebHeaders.h diff --git a/src/web/AmsWebServer.h b/lib/ClassicUi/include/AmsWebServer.h similarity index 98% rename from src/web/AmsWebServer.h rename to lib/ClassicUi/include/AmsWebServer.h index 00417d48..b6d3decf 100644 --- a/src/web/AmsWebServer.h +++ b/lib/ClassicUi/include/AmsWebServer.h @@ -13,7 +13,7 @@ #include "EnergyAccounting.h" #include "Uptime.h" #include "RemoteDebug.h" -#include "entsoe/EntsoeApi.h" +#include "EntsoeApi.h" #if defined(ESP8266) #include @@ -89,7 +89,7 @@ private: void configMqttHtml(); void configWebHtml(); void configDomoticzHtml(); - void configEntsoeHtml(); + void configPriceApiHtml(); void configNtpHtml(); void configGpioHtml(); void configDebugHtml(); diff --git a/scripts/makeweb.py b/lib/ClassicUi/scripts/generate_includes.py similarity index 96% rename from scripts/makeweb.py rename to lib/ClassicUi/scripts/generate_includes.py index 658f4ca4..bb5efbeb 100644 --- a/scripts/makeweb.py +++ b/lib/ClassicUi/scripts/generate_includes.py @@ -25,8 +25,8 @@ except: print("WARN: Unable to load minifier") -webroot = "web" -srcroot = "src/web/root" +webroot = "lib/ClassicUi/html" +srcroot = "lib/ClassicUi/include/root" version = os.environ.get('GITHUB_TAG') if version == None: diff --git a/src/web/AmsWebServer.cpp b/lib/ClassicUi/src/AmsWebServer.cpp similarity index 95% rename from src/web/AmsWebServer.cpp rename to lib/ClassicUi/src/AmsWebServer.cpp index 7cb12c45..cdb778fe 100644 --- a/src/web/AmsWebServer.cpp +++ b/lib/ClassicUi/src/AmsWebServer.cpp @@ -18,7 +18,7 @@ #include "root/mqtt_html.h" #include "root/web_html.h" #include "root/domoticz_html.h" -#include "root/entsoe_html.h" +#include "root/priceapi_html.h" #include "root/ntp_html.h" #include "root/gpio_html.h" #include "root/debugging_html.h" @@ -71,7 +71,7 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter server.on(F("/mqtt"), HTTP_GET, std::bind(&AmsWebServer::configMqttHtml, this)); server.on(F("/web"), HTTP_GET, std::bind(&AmsWebServer::configWebHtml, this)); server.on(F("/domoticz"),HTTP_GET, std::bind(&AmsWebServer::configDomoticzHtml, this)); - server.on(F("/entsoe"),HTTP_GET, std::bind(&AmsWebServer::configEntsoeHtml, this)); + server.on(F("/priceapi"),HTTP_GET, std::bind(&AmsWebServer::configPriceApiHtml, this)); server.on(F("/thresholds"),HTTP_GET, std::bind(&AmsWebServer::configThresholdsHtml, this)); server.on(F("/boot.css"), HTTP_GET, std::bind(&AmsWebServer::bootCss, this)); server.on(F("/github.svg"), HTTP_GET, std::bind(&AmsWebServer::githubSvg, this)); @@ -544,8 +544,8 @@ void AmsWebServer::configDomoticzHtml() { server.sendContent_P(FOOT_HTML); } -void AmsWebServer::configEntsoeHtml() { - printD(F("Serving /entsoe.html over http...")); +void AmsWebServer::configPriceApiHtml() { + printD(F("Serving /priceapi.html over http...")); if(!checkSecurity(1)) return; @@ -553,52 +553,54 @@ void AmsWebServer::configEntsoeHtml() { EntsoeConfig entsoe; config->getEntsoeConfig(entsoe); - if(ESP.getFreeHeap() > 25000) { - String html = String((const __FlashStringHelper*) ENTSOE_HTML); + String html = String((const __FlashStringHelper*) PRICEAPI_HTML); - html.replace(F("{et}"), entsoe.token); - html.replace(F("{em}"), String(entsoe.multiplier / 1000.0, 3)); - - html.replace(F("{eaNo1}"), strcmp(entsoe.area, "10YNO-1--------2") == 0 ? F("selected") : F("")); - html.replace(F("{eaNo2}"), strcmp(entsoe.area, "10YNO-2--------T") == 0 ? F("selected") : F("")); - html.replace(F("{eaNo3}"), strcmp(entsoe.area, "10YNO-3--------J") == 0 ? F("selected") : F("")); - html.replace(F("{eaNo4}"), strcmp(entsoe.area, "10YNO-4--------9") == 0 ? F("selected") : F("")); - html.replace(F("{eaNo5}"), strcmp(entsoe.area, "10Y1001A1001A48H") == 0 ? F("selected") : F("")); - - html.replace(F("{eaSe1}"), strcmp(entsoe.area, "10Y1001A1001A44P") == 0 ? F("selected") : F("")); - html.replace(F("{eaSe2}"), strcmp(entsoe.area, "10Y1001A1001A45N") == 0 ? F("selected") : F("")); - html.replace(F("{eaSe3}"), strcmp(entsoe.area, "10Y1001A1001A46L") == 0 ? F("selected") : F("")); - html.replace(F("{eaSe4}"), strcmp(entsoe.area, "10Y1001A1001A47J") == 0 ? F("selected") : F("")); - - html.replace(F("{eaDk1}"), strcmp(entsoe.area, "10YDK-1--------W") == 0 ? F("selected") : F("")); - html.replace(F("{eaDk2}"), strcmp(entsoe.area, "10YDK-2--------M") == 0 ? F("selected") : F("")); - - html.replace(F("{at}"), strcmp(entsoe.area, "10YAT-APG------L") == 0 ? F("selected") : F("")); - html.replace(F("{be}"), strcmp(entsoe.area, "10YBE----------2") == 0 ? F("selected") : F("")); - html.replace(F("{cz}"), strcmp(entsoe.area, "10YCZ-CEPS-----N") == 0 ? F("selected") : F("")); - html.replace(F("{ee}"), strcmp(entsoe.area, "10Y1001A1001A39I") == 0 ? F("selected") : F("")); - html.replace(F("{fi}"), strcmp(entsoe.area, "10YFI-1--------U") == 0 ? F("selected") : F("")); - html.replace(F("{fr}"), strcmp(entsoe.area, "10YFR-RTE------C") == 0 ? F("selected") : F("")); - html.replace(F("{de}"), strcmp(entsoe.area, "10Y1001A1001A83F") == 0 ? F("selected") : F("")); - html.replace(F("{gb}"), strcmp(entsoe.area, "10YGB----------A") == 0 ? F("selected") : F("")); - html.replace(F("{lv}"), strcmp(entsoe.area, "10YLV-1001A00074") == 0 ? F("selected") : F("")); - html.replace(F("{lt}"), strcmp(entsoe.area, "10YLT-1001A0008Q") == 0 ? F("selected") : F("")); - html.replace(F("{nl}"), strcmp(entsoe.area, "10YNL----------L") == 0 ? F("selected") : F("")); - html.replace(F("{pl}"), strcmp(entsoe.area, "10YPL-AREA-----S") == 0 ? F("selected") : F("")); - html.replace(F("{ch}"), strcmp(entsoe.area, "10YCH-SWISSGRIDZ") == 0 ? F("selected") : F("")); - - html.replace(F("{ecNOK}"), strcmp(entsoe.currency, "NOK") == 0 ? F("selected") : F("")); - html.replace(F("{ecSEK}"), strcmp(entsoe.currency, "SEK") == 0 ? F("selected") : F("")); - html.replace(F("{ecDKK}"), strcmp(entsoe.currency, "DKK") == 0 ? F("selected") : F("")); - html.replace(F("{ecEUR}"), strcmp(entsoe.currency, "EUR") == 0 ? F("selected") : F("")); - - server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN); - server.send_P(200, MIME_HTML, HEAD_HTML); - server.sendContent(html); - server.sendContent_P(FOOT_HTML); + if(ESP.getFreeHeap() > 32000) { + html.replace("{et}", entsoe.token); + html.replace("{dt}", ""); } else { - notFound(); + html.replace("{et}", ""); + html.replace("{dt}", "d-none"); } + html.replace("{em}", String(entsoe.multiplier / 1000.0, 3)); + + html.replace(F("{no1}"), strcmp(entsoe.area, "10YNO-1--------2") == 0 ? "selected" : ""); + html.replace(F("{no2}"), strcmp(entsoe.area, "10YNO-2--------T") == 0 ? "selected" : ""); + html.replace(F("{no3}"), strcmp(entsoe.area, "10YNO-3--------J") == 0 ? "selected" : ""); + html.replace(F("{no4}"), strcmp(entsoe.area, "10YNO-4--------9") == 0 ? "selected" : ""); + html.replace(F("{no5}"), strcmp(entsoe.area, "10Y1001A1001A48H") == 0 ? "selected" : ""); + + html.replace(F("{se1}"), strcmp(entsoe.area, "10Y1001A1001A44P") == 0 ? "selected" : ""); + html.replace(F("{se2}"), strcmp(entsoe.area, "10Y1001A1001A45N") == 0 ? "selected" : ""); + html.replace(F("{se3}"), strcmp(entsoe.area, "10Y1001A1001A46L") == 0 ? "selected" : ""); + html.replace(F("{se4}"), strcmp(entsoe.area, "10Y1001A1001A47J") == 0 ? "selected" : ""); + + html.replace(F("{dk1}"), strcmp(entsoe.area, "10YDK-1--------W") == 0 ? "selected" : ""); + html.replace(F("{dk2}"), strcmp(entsoe.area, "10YDK-2--------M") == 0 ? "selected" : ""); + + html.replace(F("{at}"), strcmp(entsoe.area, "10YAT-APG------L") == 0 ? F("selected") : F("")); + html.replace(F("{be}"), strcmp(entsoe.area, "10YBE----------2") == 0 ? F("selected") : F("")); + html.replace(F("{cz}"), strcmp(entsoe.area, "10YCZ-CEPS-----N") == 0 ? F("selected") : F("")); + html.replace(F("{ee}"), strcmp(entsoe.area, "10Y1001A1001A39I") == 0 ? F("selected") : F("")); + html.replace(F("{fi}"), strcmp(entsoe.area, "10YFI-1--------U") == 0 ? F("selected") : F("")); + html.replace(F("{fr}"), strcmp(entsoe.area, "10YFR-RTE------C") == 0 ? F("selected") : F("")); + html.replace(F("{de}"), strcmp(entsoe.area, "10Y1001A1001A83F") == 0 ? F("selected") : F("")); + html.replace(F("{gb}"), strcmp(entsoe.area, "10YGB----------A") == 0 ? F("selected") : F("")); + html.replace(F("{lv}"), strcmp(entsoe.area, "10YLV-1001A00074") == 0 ? F("selected") : F("")); + html.replace(F("{lt}"), strcmp(entsoe.area, "10YLT-1001A0008Q") == 0 ? F("selected") : F("")); + html.replace(F("{nl}"), strcmp(entsoe.area, "10YNL----------L") == 0 ? F("selected") : F("")); + html.replace(F("{pl}"), strcmp(entsoe.area, "10YPL-AREA-----S") == 0 ? F("selected") : F("")); + html.replace(F("{ch}"), strcmp(entsoe.area, "10YCH-SWISSGRIDZ") == 0 ? F("selected") : F("")); + + html.replace(F("{nok}"), strcmp(entsoe.currency, "NOK") == 0 ? "selected" : ""); + html.replace(F("{sek}"), strcmp(entsoe.currency, "SEK") == 0 ? "selected" : ""); + html.replace(F("{dkk}"), strcmp(entsoe.currency, "DKK") == 0 ? "selected" : ""); + html.replace(F("{eur}"), strcmp(entsoe.currency, "EUR") == 0 ? "selected" : ""); + + server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN); + server.send_P(200, MIME_HTML, HEAD_HTML); + server.sendContent(html); + server.sendContent_P(FOOT_HTML); } void AmsWebServer::configThresholdsHtml() { @@ -725,7 +727,7 @@ void AmsWebServer::dataJson() { } float price = ENTSOE_NO_VALUE; - if(eapi != NULL && strlen(eapi->getToken()) > 0) + if(eapi != NULL) price = eapi->getValueForHour(0); String peaks = ""; @@ -733,7 +735,7 @@ void AmsWebServer::dataJson() { if(peakCount > 5) peakCount = 5; for(uint8_t i = 1; i <= peakCount; i++) { if(!peaks.isEmpty()) peaks += ","; - peaks += String(ea->getPeak(i)); + peaks += String(ea->getPeak(i).value / 100.0); } snprintf_P(buf, BufferSize, DATA_JSON, @@ -777,12 +779,15 @@ void AmsWebServer::dataJson() { ea->getUseThisHour(), ea->getCostThisHour(), ea->getProducedThisHour(), + ea->getIncomeThisHour(), ea->getUseToday(), ea->getCostToday(), ea->getProducedToday(), + ea->getIncomeToday(), ea->getUseThisMonth(), ea->getCostThisMonth(), ea->getProducedThisMonth(), + ea->getIncomeThisMonth(), (uint32_t) time(nullptr) ); @@ -1830,7 +1835,7 @@ void AmsWebServer::restartWaitHtml() { performRestart = false; } else if(performUpgrade) { WiFiClient client; - String url = customFirmwareUrl.isEmpty() || !customFirmwareUrl.startsWith(F("http")) ? F("http://ams2mqtt.rewiredinvent.no/hub/firmware/update") : customFirmwareUrl; + String url = customFirmwareUrl.isEmpty() || !customFirmwareUrl.startsWith(F("http")) ? F("http://hub.amsleser.no/hub/firmware/update") : customFirmwareUrl; #if defined(ESP8266) String chipType = F("esp8266"); #elif defined(CONFIG_IDF_TARGET_ESP32S2) diff --git a/lib/DomoticzMqttHandler/include/.gitignore b/lib/DomoticzMqttHandler/include/.gitignore new file mode 100644 index 00000000..1907c991 --- /dev/null +++ b/lib/DomoticzMqttHandler/include/.gitignore @@ -0,0 +1 @@ +json/*.h diff --git a/src/mqtt/DomoticzMqttHandler.h b/lib/DomoticzMqttHandler/include/DomoticzMqttHandler.h similarity index 100% rename from src/mqtt/DomoticzMqttHandler.h rename to lib/DomoticzMqttHandler/include/DomoticzMqttHandler.h diff --git a/web/domoticz.json b/lib/DomoticzMqttHandler/json/domoticz.json similarity index 100% rename from web/domoticz.json rename to lib/DomoticzMqttHandler/json/domoticz.json diff --git a/lib/DomoticzMqttHandler/scripts/generate_includes.py b/lib/DomoticzMqttHandler/scripts/generate_includes.py new file mode 100644 index 00000000..43f41f5d --- /dev/null +++ b/lib/DomoticzMqttHandler/scripts/generate_includes.py @@ -0,0 +1,76 @@ +import os +import re +import shutil +import subprocess + +try: + from css_html_js_minify import js_minify +except: + from SCons.Script import ( + ARGUMENTS, + COMMAND_LINE_TARGETS, + DefaultEnvironment, + ) + env = DefaultEnvironment() + + env.Execute( + env.VerboseAction( + '$PYTHONEXE -m pip install "css_html_js_minify" ', + "Installing Python dependencies", + ) + ) + try: + from css_html_js_minify import js_minify + except: + print("WARN: Unable to load minifier") + + +webroot = "lib/DomoticzMqttHandler/json" +srcroot = "lib/DomoticzMqttHandler/include/json" + +version = os.environ.get('GITHUB_TAG') +if version == None: + try: + result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False) + if result.returncode == 0: + version = result.stdout.decode('utf-8').strip() + else: + version = "SNAPSHOT" + except: + version = "SNAPSHOT" + +if os.path.exists(srcroot): + shutil.rmtree(srcroot) + os.mkdir(srcroot) +else: + os.mkdir(srcroot) + +for filename in os.listdir(webroot): + basename = re.sub("[^0-9a-zA-Z]+", "_", filename) + + srcfile = webroot + "/" + filename + dstfile = srcroot + "/" + basename + ".h" + + varname = basename.upper() + + with open(srcfile, encoding="utf-8") as f: + content = f.read().replace("${version}", version) + + try: + if (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"): + content = js_minify(content) + except: + print("WARN: Unable to minify") + + with open(dstfile, "w") as dst: + dst.write("static const char ") + dst.write(varname) + dst.write("[] PROGMEM = R\"==\"==(") + dst.write(content) + dst.write(")==\"==\";\n") + dst.write("const int "); + dst.write(varname) + dst.write("_LEN PROGMEM = "); + dst.write(str(len(content))) + dst.write(";"); + \ No newline at end of file diff --git a/src/mqtt/DomoticzMqttHandler.cpp b/lib/DomoticzMqttHandler/src/DomoticzMqttHandler.cpp similarity index 98% rename from src/mqtt/DomoticzMqttHandler.cpp rename to lib/DomoticzMqttHandler/src/DomoticzMqttHandler.cpp index d173e0dd..4a2cdd5c 100644 --- a/src/mqtt/DomoticzMqttHandler.cpp +++ b/lib/DomoticzMqttHandler/src/DomoticzMqttHandler.cpp @@ -1,5 +1,5 @@ #include "DomoticzMqttHandler.h" -#include "web/root/domoticz_json.h" +#include "json/domoticz_json.h" bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) { bool ret = false; diff --git a/src/EnergyAccounting.h b/lib/EnergyAccounting/include/EnergyAccounting.h similarity index 77% rename from src/EnergyAccounting.h rename to lib/EnergyAccounting/include/EnergyAccounting.h index c0c76e32..912fd870 100644 --- a/src/EnergyAccounting.h +++ b/lib/EnergyAccounting/include/EnergyAccounting.h @@ -4,7 +4,7 @@ #include "Arduino.h" #include "AmsData.h" #include "AmsDataStorage.h" -#include "entsoe/EntsoeApi.h" +#include "EntsoeApi.h" struct EnergyAccountingPeak { uint8_t day; @@ -12,6 +12,18 @@ struct EnergyAccountingPeak { }; struct EnergyAccountingData { + uint8_t version; + uint8_t month; + uint16_t costYesterday; + uint16_t costThisMonth; + uint16_t costLastMonth; + uint16_t incomeYesterday; + uint16_t incomeThisMonth; + uint16_t incomeLastMonth; + EnergyAccountingPeak peaks[5]; +}; + +struct EnergyAccountingData4 { uint8_t version; uint8_t month; uint16_t costYesterday; @@ -55,9 +67,15 @@ public: double getCostThisMonth(); uint16_t getCostLastMonth(); + double getIncomeThisHour(); + double getIncomeToday(); + double getIncomeYesterday(); + double getIncomeThisMonth(); + uint16_t getIncomeLastMonth(); + float getMonthMax(); uint8_t getCurrentThreshold(); - float getPeak(uint8_t); + EnergyAccountingPeak getPeak(uint8_t); EnergyAccountingData getData(); void setData(EnergyAccountingData&); @@ -72,7 +90,7 @@ private: Timezone *tz = NULL; uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0; double use, costHour, costDay; - double produce; + double produce, incomeHour, incomeDay; EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 }; void calcDayCost(); diff --git a/src/EnergyAccounting.cpp b/lib/EnergyAccounting/src/EnergyAccounting.cpp similarity index 84% rename from src/EnergyAccounting.cpp rename to lib/EnergyAccounting/src/EnergyAccounting.cpp index 6ba7cd44..50ac23a4 100644 --- a/src/EnergyAccounting.cpp +++ b/lib/EnergyAccounting/src/EnergyAccounting.cpp @@ -45,8 +45,9 @@ bool EnergyAccounting::update(AmsData* amsData) { if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Initializing data at %lu\n", (int32_t) now); if(!load()) { if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Unable to load existing data\n"); - data = { 4, local.Month, - 0, 0, 0, + data = { 5, local.Month, + 0, 0, 0, // Cost + 0, 0, 0, // Income 0, 0, // Peak 1 0, 0, // Peak 2 0, 0, // Peak 3 @@ -58,6 +59,7 @@ bool EnergyAccounting::update(AmsData* amsData) { debugger->printf("(EnergyAccounting) Peak hour from day %d: %d\n", data.peaks[i].day, data.peaks[i].value*10); } debugger->printf("(EnergyAccounting) Loaded cost yesterday: %.2f, this month: %d, last month: %d\n", data.costYesterday / 10.0, data.costThisMonth, data.costLastMonth); + debugger->printf("(EnergyAccounting) Loaded income yesterday: %.2f, this month: %d, last month: %d\n", data.incomeYesterday / 10.0, data.incomeThisMonth, data.incomeLastMonth); } init = true; } @@ -85,13 +87,18 @@ bool EnergyAccounting::update(AmsData* amsData) { use = 0; produce = 0; costHour = 0; - currentHour = local.Hour; + incomeHour = 0; if(local.Day != currentDay) { if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New day %d\n", local.Day); data.costYesterday = costDay * 10; data.costThisMonth += costDay; costDay = 0; + + data.incomeYesterday = incomeDay * 10; + data.incomeThisMonth += incomeDay; + incomeDay = 0; + currentDay = local.Day; ret = true; } @@ -100,6 +107,8 @@ bool EnergyAccounting::update(AmsData* amsData) { if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New month %d\n", local.Month); data.costLastMonth = data.costThisMonth; data.costThisMonth = 0; + data.incomeLastMonth = data.incomeThisMonth; + data.incomeThisMonth = 0; for(uint8_t i = 0; i < 5; i++) { data.peaks[i] = { 0, 0 }; } @@ -127,6 +136,13 @@ bool EnergyAccounting::update(AmsData* amsData) { if(kwhe > 0) { if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh export\n", kwhe); produce += kwhe; + if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) { + float price = eapi->getValueForHour(0); + float income = price * kwhe; + if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", income / 100.0, eapi->getCurrency()); + incomeHour += income; + incomeDay += income; + } } if(config != NULL) { @@ -144,13 +160,19 @@ void EnergyAccounting::calcDayCost() { breakTime(tz->toLocal(now), local); if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) { - if(initPrice) costDay = 0; + if(initPrice) { + costDay = 0; + incomeDay = 0; + } for(int i = 0; i < currentHour; i++) { float price = eapi->getValueForHour(i - local.Hour); if(price == ENTSOE_NO_VALUE) break; breakTime(now - ((local.Hour - i) * 3600), utc); int16_t wh = ds->getHourImport(utc.Hour); costDay += price * (wh / 1000.0); + + wh = ds->getHourExport(utc.Hour); + incomeDay += price * (wh / 1000.0); } initPrice = true; } @@ -230,6 +252,26 @@ uint16_t EnergyAccounting::getCostLastMonth() { return data.costLastMonth; } +double EnergyAccounting::getIncomeThisHour() { + return incomeHour; +} + +double EnergyAccounting::getIncomeToday() { + return incomeDay; +} + +double EnergyAccounting::getIncomeYesterday() { + return data.incomeYesterday / 10.0; +} + +double EnergyAccounting::getIncomeThisMonth() { + return data.incomeThisMonth + getIncomeToday(); +} + +uint16_t EnergyAccounting::getIncomeLastMonth() { + return data.incomeLastMonth; +} + uint8_t EnergyAccounting::getCurrentThreshold() { if(config == NULL) return 0; @@ -265,8 +307,8 @@ float EnergyAccounting::getMonthMax() { return maxHour > 0 ? maxHour / count / 100.0 : 0.0; } -float EnergyAccounting::getPeak(uint8_t num) { - if(num < 1 || num > 5) return 0.0; +EnergyAccountingPeak EnergyAccounting::getPeak(uint8_t num) { + if(num < 1 || num > 5) return EnergyAccountingPeak({0,0}); uint8_t count = 0; bool included[5] = { false, false, false, false, false }; @@ -292,10 +334,10 @@ float EnergyAccounting::getPeak(uint8_t num) { if(!included[i]) continue; pos++; if(pos == num) { - return data.peaks[i].value / 100.0; + return data.peaks[i]; } } - return 0.0; + return EnergyAccountingPeak({0,0}); } bool EnergyAccounting::load() { @@ -313,14 +355,27 @@ bool EnergyAccounting::load() { file.readBytes(buf, file.size()); if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Data version %d\n", buf[0]); - if(buf[0] == 4) { + if(buf[0] == 5) { EnergyAccountingData* data = (EnergyAccountingData*) buf; memcpy(&this->data, data, sizeof(this->data)); ret = true; + } else if(buf[0] == 4) { + EnergyAccountingData4* data = (EnergyAccountingData4*) buf; + this->data = { 5, data->month, + (uint16_t) (data->costYesterday / 10), (uint16_t) (data->costThisMonth / 100), (uint16_t) (data->costLastMonth / 100), + 0,0,0, // Income from production + data->peaks[0].day, data->peaks[0].value, + data->peaks[1].day, data->peaks[1].value, + data->peaks[2].day, data->peaks[2].value, + data->peaks[3].day, data->peaks[3].value, + data->peaks[4].day, data->peaks[4].value + }; + ret = true; } else if(buf[0] == 3) { EnergyAccountingData* data = (EnergyAccountingData*) buf; - this->data = { 4, data->month, + this->data = { 5, data->month, (uint16_t) (data->costYesterday / 10), (uint16_t) (data->costThisMonth / 100), (uint16_t) (data->costLastMonth / 100), + 0,0,0, // Income from production data->peaks[0].day, data->peaks[0].value, data->peaks[1].day, data->peaks[1].value, data->peaks[2].day, data->peaks[2].value, @@ -329,8 +384,9 @@ bool EnergyAccounting::load() { }; ret = true; } else { - data = { 4, 0, - 0, 0, 0, + data = { 5, 0, + 0, 0, 0, // Cost + 0,0,0, // Income from production 0, 0, // Peak 1 0, 0, // Peak 2 0, 0, // Peak 3 diff --git a/src/entsoe/DnbCurrParser.h b/lib/EntsoePriceApi/include/DnbCurrParser.h similarity index 100% rename from src/entsoe/DnbCurrParser.h rename to lib/EntsoePriceApi/include/DnbCurrParser.h diff --git a/src/entsoe/EntsoeA44Parser.h b/lib/EntsoePriceApi/include/EntsoeA44Parser.h similarity index 92% rename from src/entsoe/EntsoeA44Parser.h rename to lib/EntsoePriceApi/include/EntsoeA44Parser.h index 2f538d10..55201a62 100644 --- a/src/entsoe/EntsoeA44Parser.h +++ b/lib/EntsoePriceApi/include/EntsoeA44Parser.h @@ -2,6 +2,7 @@ #define _ENTSOEA44PARSER_H #include "Stream.h" +#include "PricesContainer.h" #define DOCPOS_SEEK 0 #define DOCPOS_CURRENCY 1 @@ -26,6 +27,7 @@ public: void flush(); size_t write(const uint8_t *buffer, size_t size); size_t write(uint8_t); + void get(PricesContainer*); private: char currency[4]; diff --git a/src/entsoe/EntsoeApi.h b/lib/EntsoePriceApi/include/EntsoeApi.h similarity index 71% rename from src/entsoe/EntsoeApi.h rename to lib/EntsoePriceApi/include/EntsoeApi.h index b382719c..af7aaff7 100644 --- a/src/entsoe/EntsoeApi.h +++ b/lib/EntsoePriceApi/include/EntsoeApi.h @@ -4,8 +4,8 @@ #include "TimeLib.h" #include "Timezone.h" #include "RemoteDebug.h" -#include "EntsoeA44Parser.h" #include "AmsConfiguration.h" +#include "EntsoeA44Parser.h" #if defined(ESP8266) #include @@ -25,33 +25,44 @@ public: char* getToken(); char* getCurrency(); + char* getArea(); float getValueForHour(int8_t); float getValueForHour(time_t, int8_t); + int16_t getLastError(); + private: RemoteDebug* debugger; EntsoeConfig* config = NULL; + HTTPClient http; uint8_t currentDay = 0, currentHour = 0; - uint32_t tomorrowFetchMillis = 36000000; // Number of ms before midnight. Default fetch 10hrs before midnight (14:00 CE(S)T) - uint64_t midnightMillis = 0; + uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices uint64_t lastTodayFetch = 0; uint64_t lastTomorrowFetch = 0; uint64_t lastCurrencyFetch = 0; - EntsoeA44Parser* today = NULL; - EntsoeA44Parser* tomorrow = NULL; + PricesContainer* today = NULL; + PricesContainer* tomorrow = NULL; Timezone* tz = NULL; static const uint16_t BufferSize = 256; char* buf; + bool hub = false; + uint8_t* key = NULL; + uint8_t* auth = NULL; + float currencyMultiplier = 0; + int16_t lastError = 0; + + PricesContainer* fetchPrices(time_t); bool retrieve(const char* url, Stream* doc); - float getCurrencyMultiplier(const char* from, const char* to); + float getCurrencyMultiplier(const char* from, const char* to, time_t t); void printD(String fmt, ...); void printE(String fmt, ...); + void debugPrint(byte *buffer, int start, int length); }; #endif diff --git a/lib/EntsoePriceApi/include/PricesContainer.h b/lib/EntsoePriceApi/include/PricesContainer.h new file mode 100644 index 00000000..cb6d687a --- /dev/null +++ b/lib/EntsoePriceApi/include/PricesContainer.h @@ -0,0 +1,8 @@ +#ifndef _PRICESCONTAINER_H +#define _PRICESCONTAINER_H +struct PricesContainer { + char currency[4]; + char measurementUnit[4]; + int32_t points[24]; +}; +#endif diff --git a/src/entsoe/DnbCurrParser.cpp b/lib/EntsoePriceApi/src/DnbCurrParser.cpp similarity index 100% rename from src/entsoe/DnbCurrParser.cpp rename to lib/EntsoePriceApi/src/DnbCurrParser.cpp diff --git a/src/entsoe/EntsoeA44Parser.cpp b/lib/EntsoePriceApi/src/EntsoeA44Parser.cpp similarity index 51% rename from src/entsoe/EntsoeA44Parser.cpp rename to lib/EntsoePriceApi/src/EntsoeA44Parser.cpp index 9dc41de6..c64cd145 100644 --- a/src/entsoe/EntsoeA44Parser.cpp +++ b/lib/EntsoePriceApi/src/EntsoeA44Parser.cpp @@ -106,3 +106,35 @@ size_t EntsoeA44Parser::write(uint8_t byte) { } return 1; } + +void EntsoeA44Parser::get(PricesContainer* container) { + strcpy(container->currency, currency); + strcpy(container->measurementUnit, measurementUnit); + + container->points[0] = points[0] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[0] * 10000; + container->points[1] = points[1] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[1] * 10000; + container->points[2] = points[2] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[2] * 10000; + container->points[3] = points[3] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[3] * 10000; + container->points[4] = points[4] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[4] * 10000; + container->points[5] = points[5] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[5] * 10000; + container->points[6] = points[6] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[6] * 10000; + container->points[7] = points[7] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[7] * 10000; + container->points[8] = points[8] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[8] * 10000; + container->points[9] = points[9] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[9] * 10000; + + container->points[10] = points[10] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[10] * 10000; + container->points[11] = points[11] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[11] * 10000; + container->points[12] = points[12] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[12] * 10000; + container->points[13] = points[13] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[13] * 10000; + container->points[14] = points[14] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[14] * 10000; + container->points[15] = points[15] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[15] * 10000; + container->points[16] = points[16] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[16] * 10000; + container->points[17] = points[17] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[17] * 10000; + container->points[18] = points[18] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[18] * 10000; + container->points[19] = points[19] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[19] * 10000; + + container->points[20] = points[20] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[20] * 10000; + container->points[21] = points[21] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[21] * 10000; + container->points[22] = points[22] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[22] * 10000; + container->points[23] = points[23] == ENTSOE_NO_VALUE ? ENTSOE_NO_VALUE : points[23] * 10000; +} \ No newline at end of file diff --git a/lib/EntsoePriceApi/src/EntsoeApi.cpp b/lib/EntsoePriceApi/src/EntsoeApi.cpp new file mode 100644 index 00000000..3b6dc474 --- /dev/null +++ b/lib/EntsoePriceApi/src/EntsoeApi.cpp @@ -0,0 +1,410 @@ +#include "EntsoeApi.h" +#include +#include "Uptime.h" +#include "TimeLib.h" +#include "DnbCurrParser.h" +#include "version.h" + +#include "GcmParser.h" + +#if defined(ESP32) +#include +#endif + +EntsoeApi::EntsoeApi(RemoteDebug* Debug) { + this->buf = (char*) malloc(BufferSize); + + debugger = Debug; + + // Entso-E uses CET/CEST + TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; + TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; + tz = new Timezone(CEST, CET); + + tomorrowFetchMinute = 15 + random(45); // Random between 13:15 and 14:00 +} + +void EntsoeApi::setup(EntsoeConfig& config) { + if(this->config == NULL) { + this->config = new EntsoeConfig(); + } + memcpy(this->config, &config, sizeof(config)); + lastTodayFetch = lastTomorrowFetch = lastCurrencyFetch = 0; + if(today != NULL) delete today; + if(tomorrow != NULL) delete tomorrow; + today = tomorrow = NULL; + + http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + http.setReuse(false); + http.setTimeout(60000); + http.setUserAgent("ams2mqtt/" + String(VERSION)); + http.useHTTP10(true); + + #if defined(AMS2MQTT_PRICE_KEY) + key = new uint8_t[16] AMS2MQTT_PRICE_KEY; + hub = true; + #else + hub = false; + #endif + #if defined(AMS2MQTT_PRICE_AUTHENTICATION) + auth = new uint8_t[16] AMS2MQTT_PRICE_AUTHENTICATION; + hub = hub && true; + #else + hub = false; + #endif + +} + +char* EntsoeApi::getToken() { + return this->config->token; +} + +char* EntsoeApi::getCurrency() { + return this->config->currency; +} + +char* EntsoeApi::getArea() { + return this->config->area; +} + +float EntsoeApi::getValueForHour(int8_t hour) { + time_t cur = time(nullptr); + return getValueForHour(cur, hour); +} + +float EntsoeApi::getValueForHour(time_t cur, int8_t hour) { + tmElements_t tm; + if(tz != NULL) + cur = tz->toLocal(cur); + breakTime(cur, tm); + int pos = tm.Hour + hour; + if(pos >= 48) + return ENTSOE_NO_VALUE; + + double value = ENTSOE_NO_VALUE; + double multiplier = config->multiplier / 1000.0; + if(pos > 23) { + if(tomorrow == NULL) + return ENTSOE_NO_VALUE; + if(tomorrow->points[pos-24] == ENTSOE_NO_VALUE) + return ENTSOE_NO_VALUE; + value = tomorrow->points[pos-24] / 10000.0; + if(strcmp(tomorrow->measurementUnit, "KWH") == 0) { + // Multiplier is 1 + } else if(strcmp(tomorrow->measurementUnit, "MWH") == 0) { + multiplier *= 0.001; + } else { + return ENTSOE_NO_VALUE; + } + float mult = getCurrencyMultiplier(tomorrow->currency, config->currency, cur); + if(mult == 0) return ENTSOE_NO_VALUE; + multiplier *= mult; + } else if(pos >= 0) { + if(today == NULL) + return ENTSOE_NO_VALUE; + if(today->points[pos] == ENTSOE_NO_VALUE) + return ENTSOE_NO_VALUE; + value = today->points[pos] / 10000.0; + if(strcmp(today->measurementUnit, "KWH") == 0) { + // Multiplier is 1 + } else if(strcmp(today->measurementUnit, "MWH") == 0) { + multiplier *= 0.001; + } else { + return ENTSOE_NO_VALUE; + } + float mult = getCurrencyMultiplier(today->currency, config->currency, cur); + if(mult == 0) return ENTSOE_NO_VALUE; + multiplier *= mult; + } + return value * multiplier; +} + +bool EntsoeApi::loop() { + uint64_t now = millis64(); + if(now < 10000) return false; // Grace period + + time_t t = time(nullptr); + if(t < BUILD_EPOCH) return false; + + #ifndef AMS2MQTT_PRICE_KEY + if(strlen(getToken()) == 0) { + return false; + } + #endif + if(!config->enabled) + return false; + if(strlen(config->area) == 0) + return false; + if(strlen(config->currency) == 0) + return false; + + tmElements_t tm; + breakTime(tz->toLocal(t), tm); + + if(currentDay == 0) { + currentDay = tm.Day; + currentHour = tm.Hour; + } + + if(currentDay != tm.Day) { + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Rotating price objects at %lu\n", t); + if(today != NULL) delete today; + if(tomorrow != NULL) { + today = tomorrow; + tomorrow = NULL; + } + currentDay = tm.Day; + currentHour = tm.Hour; + return today != NULL; // Only trigger MQTT publish if we have todays prices. + } else if(currentHour != tm.Hour) { + currentHour = tm.Hour; + return today != NULL; // Only trigger MQTT publish if we have todays prices. + } + + if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) { + try { + lastTodayFetch = now; + today = fetchPrices(t); + } catch(const std::exception& e) { + if(lastError == 0) lastError = 900; + today = NULL; + } + return today != NULL; // Only trigger MQTT publish if we have todays prices. + } + + // Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will + // fetch with one hour (with some random delay) and retry every 15 minutes + if(tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000)) { + try { + lastTomorrowFetch = now; + tomorrow = fetchPrices(t+SECS_PER_DAY); + } catch(const std::exception& e) { + if(lastError == 0) lastError = 900; + tomorrow = NULL; + } + return tomorrow != NULL; + } + + return false; +} + +bool EntsoeApi::retrieve(const char* url, Stream* doc) { + #if defined(ESP32) + if(http.begin(url)) { + printD("Connection established"); + + #if defined(ESP32) + esp_task_wdt_reset(); + #elif defined(ESP8266) + ESP.wdtFeed(); + #endif + + int status = http.GET(); + + #if defined(ESP32) + esp_task_wdt_reset(); + #elif defined(ESP8266) + ESP.wdtFeed(); + #endif + + if(status == HTTP_CODE_OK) { + printD("Receiving data"); + http.writeToStream(doc); + http.end(); + lastError = 0; + return true; + } else { + lastError = status; + if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Communication error, returned status: %d\n", status); + printE(http.errorToString(status)); + printD(http.getString()); + + http.end(); + return false; + } + } else { + return false; + } + #endif + return false; +} + +float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to, time_t t) { + if(strcmp(from, to) == 0) + return 1.00; + + uint64_t now = millis64(); + if(now > lastCurrencyFetch && (lastCurrencyFetch == 0 || (now - lastCurrencyFetch) > 60000)) { + lastCurrencyFetch = now; + + DnbCurrParser p; + + #if defined(ESP32) + esp_task_wdt_reset(); + #elif defined(ESP8266) + ESP.wdtFeed(); + #endif + + snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", from); + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", from); + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf); + if(retrieve(buf, &p)) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue()); + currencyMultiplier = p.getValue(); + if(strncmp(to, "NOK", 3) != 0) { + snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", to); + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", to); + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf); + if(retrieve(buf, &p)) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue()); + currencyMultiplier /= p.getValue(); + } else { + return 0; + } + } + } else { + return 0; + } + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier); + tmElements_t tm; + breakTime(t, tm); + lastCurrencyFetch = now + (SECS_PER_DAY * 1000) - (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000); + } + return currencyMultiplier; +} + +PricesContainer* EntsoeApi::fetchPrices(time_t t) { + tmElements_t tm; + breakTime(t, tm); + if(strlen(getToken()) > 0) { + time_t e1 = t - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second; // UTC midnight + time_t e2 = e1 + SECS_PER_DAY; + tmElements_t d1, d2; + breakTime(tz->toUTC(e1), d1); // To get day and hour for CET/CEST at UTC midnight + breakTime(tz->toUTC(e2), d2); + + snprintf(buf, BufferSize, "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s", + "https://transparency.entsoe.eu/api", getToken(), + d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00, + d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00, + config->area, config->area); + + #if defined(ESP32) + esp_task_wdt_reset(); + #elif defined(ESP8266) + ESP.wdtFeed(); + #endif + + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for %d.%d.%d\n", tm.Day, tm.Month, tm.Year+1970); + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf); + EntsoeA44Parser a44; + if(retrieve(buf, &a44) && a44.getPoint(0) != ENTSOE_NO_VALUE) { + PricesContainer* ret = new PricesContainer(); + a44.get(ret); + return ret; + } else { + return NULL; + } + } else if(hub) { + String data; + snprintf(buf, BufferSize, "%s/%s/%d/%d/%d?currency=%s", + "http://hub.amsleser.no/hub/price", + config->area, + tm.Year+1970, + tm.Month, + tm.Day, + config->currency + ); + #if defined(ESP8266) + WiFiClient client; + client.setTimeout(5000); + if(http.begin(client, buf)) { + #elif defined(ESP32) + if(http.begin(buf)) { + #endif + int status = http.GET(); + + #if defined(ESP32) + esp_task_wdt_reset(); + #elif defined(ESP8266) + ESP.wdtFeed(); + #endif + + if(status == HTTP_CODE_OK) { + printD("Receiving data"); + data = http.getString(); + http.end(); + lastError = 0; + } else { + lastError = status; + if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Communication error, returned status: %d\n", status); + printE(http.errorToString(status)); + printD(http.getString()); + + http.end(); + } + } + uint8_t* content = (uint8_t*) (data.c_str()); + if(debugger->isActive(RemoteDebug::DEBUG)) { + printD("Received content for prices:"); + debugPrint(content, 0, data.length()); + } + + DataParserContext ctx; + ctx.length = data.length(); + GCMParser gcm(key, auth); + int8_t gcmRet = gcm.parse(content, ctx); + if(debugger->isActive(RemoteDebug::DEBUG)) { + printD("Decrypted content for prices:"); + debugPrint(content, 0, data.length()); + } + if(gcmRet > 0) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Price data starting at: %d\n", gcmRet); + PricesContainer* ret = new PricesContainer(); + memcpy(ret, content+gcmRet, sizeof(*ret)); + for(uint8_t i = 0; i < 24; i++) { + ret->points[i] = ntohl(ret->points[i]); + } + lastError = 0; + return ret; + } else { + lastError = gcmRet; + if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Error code while decrypting prices: %d\n", gcmRet); + } + } + return NULL; +} + +void EntsoeApi::printD(String fmt, ...) { + va_list args; + va_start(args, fmt); + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args); + va_end(args); +} + +void EntsoeApi::printE(String fmt, ...) { + va_list args; + va_start(args, fmt); + if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args); + va_end(args); +} + +void EntsoeApi::debugPrint(byte *buffer, int start, int length) { + for (int i = start; i < start + length; i++) { + if (buffer[i] < 0x10) + debugger->print("0"); + debugger->print(buffer[i], HEX); + debugger->print(" "); + if ((i - start + 1) % 16 == 0) + debugger->println(""); + else if ((i - start + 1) % 4 == 0) + debugger->print(" "); + + yield(); // Let other get some resources too + } + debugger->println(""); +} + +int16_t EntsoeApi::getLastError() { + return lastError; +} \ No newline at end of file diff --git a/lib/HomeAssistantMqttHandler/include/.gitignore b/lib/HomeAssistantMqttHandler/include/.gitignore new file mode 100644 index 00000000..1907c991 --- /dev/null +++ b/lib/HomeAssistantMqttHandler/include/.gitignore @@ -0,0 +1 @@ +json/*.h diff --git a/src/mqtt/HomeAssistantMqttHandler.h b/lib/HomeAssistantMqttHandler/include/HomeAssistantMqttHandler.h similarity index 96% rename from src/mqtt/HomeAssistantMqttHandler.h rename to lib/HomeAssistantMqttHandler/include/HomeAssistantMqttHandler.h index 05675eee..878efa4a 100644 --- a/src/mqtt/HomeAssistantMqttHandler.h +++ b/lib/HomeAssistantMqttHandler/include/HomeAssistantMqttHandler.h @@ -31,6 +31,5 @@ private: String clientId; String topic; HwTools* hw; - uint8_t sequence = 0, listType = 0; }; #endif diff --git a/src/mqtt/HomeAssistantStatic.h b/lib/HomeAssistantMqttHandler/include/HomeAssistantStatic.h similarity index 97% rename from src/mqtt/HomeAssistantStatic.h rename to lib/HomeAssistantMqttHandler/include/HomeAssistantStatic.h index 7bb251ce..1ae24f90 100644 --- a/src/mqtt/HomeAssistantStatic.h +++ b/lib/HomeAssistantMqttHandler/include/HomeAssistantStatic.h @@ -38,10 +38,10 @@ HomeAssistantSensor HA_SENSORS[HA_SENSOR_COUNT] PROGMEM = { {"Accumulated active export", "/energy", "tPO", "kWh", "energy", "\"total_increasing\""}, {"Accumulated reactive import","/energy", "tQI", "kvarh","energy", "\"total_increasing\""}, {"Accumulated reactive export","/energy", "tQO", "kvarh","energy", "\"total_increasing\""}, - {"Power factor", "/power", "PF", "", "power_factor", "\"measurement\""}, - {"L1 power factor", "/power", "PF1", "", "power_factor", "\"measurement\""}, - {"L2 power factor", "/power", "PF2", "", "power_factor", "\"measurement\""}, - {"L3 power factor", "/power", "PF3", "", "power_factor", "\"measurement\""}, + {"Power factor", "/power", "PF", "%", "power_factor", "\"measurement\""}, + {"L1 power factor", "/power", "PF1", "%", "power_factor", "\"measurement\""}, + {"L2 power factor", "/power", "PF2", "%", "power_factor", "\"measurement\""}, + {"L3 power factor", "/power", "PF3", "%", "power_factor", "\"measurement\""}, {"Price current hour", "/prices", "prices['0']", "", "monetary", ""}, {"Price next hour", "/prices", "prices['1']", "", "monetary", ""}, {"Price in two hour", "/prices", "prices['2']", "", "monetary", ""}, diff --git a/web/ha1.json b/lib/HomeAssistantMqttHandler/json/ha1.json similarity index 100% rename from web/ha1.json rename to lib/HomeAssistantMqttHandler/json/ha1.json diff --git a/web/ha2.json b/lib/HomeAssistantMqttHandler/json/ha2.json similarity index 100% rename from web/ha2.json rename to lib/HomeAssistantMqttHandler/json/ha2.json diff --git a/web/ha3.json b/lib/HomeAssistantMqttHandler/json/ha3.json similarity index 100% rename from web/ha3.json rename to lib/HomeAssistantMqttHandler/json/ha3.json diff --git a/web/ha4.json b/lib/HomeAssistantMqttHandler/json/ha4.json similarity index 100% rename from web/ha4.json rename to lib/HomeAssistantMqttHandler/json/ha4.json diff --git a/web/hadiscover.json b/lib/HomeAssistantMqttHandler/json/hadiscover.json similarity index 100% rename from web/hadiscover.json rename to lib/HomeAssistantMqttHandler/json/hadiscover.json diff --git a/web/realtime.json b/lib/HomeAssistantMqttHandler/json/realtime.json similarity index 100% rename from web/realtime.json rename to lib/HomeAssistantMqttHandler/json/realtime.json diff --git a/lib/HomeAssistantMqttHandler/scripts/generate_includes.py b/lib/HomeAssistantMqttHandler/scripts/generate_includes.py new file mode 100644 index 00000000..5829cb81 --- /dev/null +++ b/lib/HomeAssistantMqttHandler/scripts/generate_includes.py @@ -0,0 +1,76 @@ +import os +import re +import shutil +import subprocess + +try: + from css_html_js_minify import js_minify +except: + from SCons.Script import ( + ARGUMENTS, + COMMAND_LINE_TARGETS, + DefaultEnvironment, + ) + env = DefaultEnvironment() + + env.Execute( + env.VerboseAction( + '$PYTHONEXE -m pip install "css_html_js_minify" ', + "Installing Python dependencies", + ) + ) + try: + from css_html_js_minify import js_minify + except: + print("WARN: Unable to load minifier") + + +webroot = "lib/HomeAssistantMqttHandler/json" +srcroot = "lib/HomeAssistantMqttHandler/include/json" + +version = os.environ.get('GITHUB_TAG') +if version == None: + try: + result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False) + if result.returncode == 0: + version = result.stdout.decode('utf-8').strip() + else: + version = "SNAPSHOT" + except: + version = "SNAPSHOT" + +if os.path.exists(srcroot): + shutil.rmtree(srcroot) + os.mkdir(srcroot) +else: + os.mkdir(srcroot) + +for filename in os.listdir(webroot): + basename = re.sub("[^0-9a-zA-Z]+", "_", filename) + + srcfile = webroot + "/" + filename + dstfile = srcroot + "/" + basename + ".h" + + varname = basename.upper() + + with open(srcfile, encoding="utf-8") as f: + content = f.read().replace("${version}", version) + + try: + if (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"): + content = js_minify(content) + except: + print("WARN: Unable to minify") + + with open(dstfile, "w") as dst: + dst.write("static const char ") + dst.write(varname) + dst.write("[] PROGMEM = R\"==\"==(") + dst.write(content) + dst.write(")==\"==\";\n") + dst.write("const int "); + dst.write(varname) + dst.write("_LEN PROGMEM = "); + dst.write(str(len(content))) + dst.write(";"); + \ No newline at end of file diff --git a/src/mqtt/HomeAssistantMqttHandler.cpp b/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp similarity index 89% rename from src/mqtt/HomeAssistantMqttHandler.cpp rename to lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp index 7880c254..02ec96b8 100644 --- a/src/mqtt/HomeAssistantMqttHandler.cpp +++ b/lib/HomeAssistantMqttHandler/src/HomeAssistantMqttHandler.cpp @@ -3,20 +3,19 @@ #include "hexutils.h" #include "Uptime.h" #include "version.h" -#include "web/root/ha1_json.h" -#include "web/root/ha2_json.h" -#include "web/root/ha3_json.h" -#include "web/root/ha4_json.h" -#include "web/root/jsonsys_json.h" -#include "web/root/jsonprices_json.h" -#include "web/root/hadiscover_json.h" -#include "web/root/realtime_json.h" +#include "json/ha1_json.h" +#include "json/ha2_json.h" +#include "json/ha3_json.h" +#include "json/ha4_json.h" +#include "json/jsonsys_json.h" +#include "json/jsonprices_json.h" +#include "json/hadiscover_json.h" +#include "json/realtime_json.h" bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) { if(topic.isEmpty() || !mqtt->connected()) return false; - listType = data->getListType(); // for discovery stuff in publishSystem() if(data->getListType() >= 3) { // publish energy counts snprintf_P(json, BufferSize, HA2_JSON, data->getActiveImportCounter(), @@ -86,7 +85,7 @@ bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState, En if(peakCount > 5) peakCount = 5; for(uint8_t i = 1; i <= peakCount; i++) { if(!peaks.isEmpty()) peaks += ","; - peaks += String(ea->getPeak(i), 2); + peaks += String(ea->getPeak(i).value / 100.0, 2); } snprintf_P(json, BufferSize, REALTIME_JSON, ea->getMonthMax(), @@ -136,7 +135,7 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) { if(topic.isEmpty() || !mqtt->connected()) return false; - if(strlen(eapi->getToken()) == 0) + if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE) return false; time_t now = time(nullptr); @@ -192,6 +191,7 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) { } char ts1hr[24]; + memset(ts1hr, 0, 24); if(min1hrIdx > -1) { time_t ts = now + (SECS_PER_HOUR * min1hrIdx); //Serial.printf("1hr: %d %lu\n", min1hrIdx, ts); @@ -200,6 +200,7 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) { sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); } char ts3hr[24]; + memset(ts3hr, 0, 24); if(min3hrIdx > -1) { time_t ts = now + (SECS_PER_HOUR * min3hrIdx); //Serial.printf("3hr: %d %lu\n", min3hrIdx, ts); @@ -208,6 +209,7 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) { sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); } char ts6hr[24]; + memset(ts6hr, 0, 24); if(min6hrIdx > -1) { time_t ts = now + (SECS_PER_HOUR * min6hrIdx); //Serial.printf("6hr: %d %lu\n", min6hrIdx, ts); @@ -240,23 +242,19 @@ bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) { } bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) { - if(topic.isEmpty() || !mqtt->connected()){ - sequence = 0; + if(topic.isEmpty() || !mqtt->connected()) return false; - } - if(sequence % 3 == 0){ - snprintf_P(json, BufferSize, JSONSYS_JSON, - WiFi.macAddress().c_str(), - clientId.c_str(), - (uint32_t) (millis64()/1000), - hw->getVcc(), - hw->getWifiRssi(), - hw->getTemperature(), - VERSION - ); - mqtt->publish(topic + "/state", json); - } + snprintf_P(json, BufferSize, JSONSYS_JSON, + WiFi.macAddress().c_str(), + clientId.c_str(), + (uint32_t) (millis64()/1000), + hw->getVcc(), + hw->getWifiRssi(), + hw->getTemperature(), + VERSION + ); + mqtt->publish(topic + "/state", json); if(!autodiscoverInit) { #if defined(ESP8266) @@ -280,12 +278,19 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, Energ String uom = String(sensor.uom); if(strncmp(sensor.devcl, "monetary", 8) == 0) { if(eapi == NULL) continue; - uom = String(eapi->getCurrency()); + if(strncmp(sensor.path, "prices", 5) == 0) { + uom = String(eapi->getCurrency()) + "/kWh"; + } else { + uom = String(eapi->getCurrency()); + } } if(strncmp(sensor.path, "peaks[", 6) == 0) { if(peaks >= peakCount) continue; peaks++; } + if(strncmp(sensor.path, "temp", 4) == 0) { + if(hw->getTemperature() < 0) continue; + } snprintf_P(json, BufferSize, HADISCOVER_JSON, sensor.name, topic.c_str(), sensor.topic, @@ -308,6 +313,5 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, Energ autodiscoverInit = true; } - if(listType>0) sequence++; return true; } diff --git a/src/HwTools.h b/lib/HwTools/include/HwTools.h similarity index 100% rename from src/HwTools.h rename to lib/HwTools/include/HwTools.h diff --git a/src/HwTools.cpp b/lib/HwTools/src/HwTools.cpp similarity index 99% rename from src/HwTools.cpp rename to lib/HwTools/src/HwTools.cpp index fc185fab..b97215d9 100644 --- a/src/HwTools.cpp +++ b/lib/HwTools/src/HwTools.cpp @@ -371,9 +371,9 @@ bool HwTools::ledOff(uint8_t color) { bool HwTools::ledBlink(uint8_t color, uint8_t blink) { for(int i = 0; i < blink; i++) { if(!ledOn(color)) return false; - delay(50); + delay(75); ledOff(color); - delay(200); + if(i != blink) delay(75); } return true; } diff --git a/lib/JsonMqttHandler/include/.gitignore b/lib/JsonMqttHandler/include/.gitignore new file mode 100644 index 00000000..1907c991 --- /dev/null +++ b/lib/JsonMqttHandler/include/.gitignore @@ -0,0 +1 @@ +json/*.h diff --git a/src/mqtt/JsonMqttHandler.h b/lib/JsonMqttHandler/include/JsonMqttHandler.h similarity index 96% rename from src/mqtt/JsonMqttHandler.h rename to lib/JsonMqttHandler/include/JsonMqttHandler.h index db3e12bc..63032e36 100644 --- a/src/mqtt/JsonMqttHandler.h +++ b/lib/JsonMqttHandler/include/JsonMqttHandler.h @@ -19,6 +19,5 @@ private: String clientId; String topic; HwTools* hw; - bool init = false; }; #endif diff --git a/web/json1.json b/lib/JsonMqttHandler/json/json1.json similarity index 100% rename from web/json1.json rename to lib/JsonMqttHandler/json/json1.json diff --git a/web/json2.json b/lib/JsonMqttHandler/json/json2.json similarity index 100% rename from web/json2.json rename to lib/JsonMqttHandler/json/json2.json diff --git a/web/json3.json b/lib/JsonMqttHandler/json/json3.json similarity index 100% rename from web/json3.json rename to lib/JsonMqttHandler/json/json3.json diff --git a/web/json4.json b/lib/JsonMqttHandler/json/json4.json similarity index 100% rename from web/json4.json rename to lib/JsonMqttHandler/json/json4.json diff --git a/web/jsonprices.json b/lib/JsonMqttHandler/json/jsonprices.json similarity index 100% rename from web/jsonprices.json rename to lib/JsonMqttHandler/json/jsonprices.json diff --git a/web/jsonsys.json b/lib/JsonMqttHandler/json/jsonsys.json similarity index 100% rename from web/jsonsys.json rename to lib/JsonMqttHandler/json/jsonsys.json diff --git a/lib/JsonMqttHandler/scripts/generate_includes.py b/lib/JsonMqttHandler/scripts/generate_includes.py new file mode 100644 index 00000000..285e8fb7 --- /dev/null +++ b/lib/JsonMqttHandler/scripts/generate_includes.py @@ -0,0 +1,76 @@ +import os +import re +import shutil +import subprocess + +try: + from css_html_js_minify import js_minify +except: + from SCons.Script import ( + ARGUMENTS, + COMMAND_LINE_TARGETS, + DefaultEnvironment, + ) + env = DefaultEnvironment() + + env.Execute( + env.VerboseAction( + '$PYTHONEXE -m pip install "css_html_js_minify" ', + "Installing Python dependencies", + ) + ) + try: + from css_html_js_minify import js_minify + except: + print("WARN: Unable to load minifier") + + +webroot = "lib/JsonMqttHandler/json" +srcroot = "lib/JsonMqttHandler/include/json" + +version = os.environ.get('GITHUB_TAG') +if version == None: + try: + result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False) + if result.returncode == 0: + version = result.stdout.decode('utf-8').strip() + else: + version = "SNAPSHOT" + except: + version = "SNAPSHOT" + +if os.path.exists(srcroot): + shutil.rmtree(srcroot) + os.mkdir(srcroot) +else: + os.mkdir(srcroot) + +for filename in os.listdir(webroot): + basename = re.sub("[^0-9a-zA-Z]+", "_", filename) + + srcfile = webroot + "/" + filename + dstfile = srcroot + "/" + basename + ".h" + + varname = basename.upper() + + with open(srcfile, encoding="utf-8") as f: + content = f.read().replace("${version}", version) + + try: + if (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"): + content = js_minify(content) + except: + print("WARN: Unable to minify") + + with open(dstfile, "w") as dst: + dst.write("static const char ") + dst.write(varname) + dst.write("[] PROGMEM = R\"==\"==(") + dst.write(content) + dst.write(")==\"==\";\n") + dst.write("const int "); + dst.write(varname) + dst.write("_LEN PROGMEM = "); + dst.write(str(len(content))) + dst.write(";"); + \ No newline at end of file diff --git a/src/mqtt/JsonMqttHandler.cpp b/lib/JsonMqttHandler/src/JsonMqttHandler.cpp similarity index 95% rename from src/mqtt/JsonMqttHandler.cpp rename to lib/JsonMqttHandler/src/JsonMqttHandler.cpp index 9448b762..8aea11cf 100644 --- a/src/mqtt/JsonMqttHandler.cpp +++ b/lib/JsonMqttHandler/src/JsonMqttHandler.cpp @@ -2,12 +2,12 @@ #include "version.h" #include "hexutils.h" #include "Uptime.h" -#include "web/root/json1_json.h" -#include "web/root/json2_json.h" -#include "web/root/json3_json.h" -#include "web/root/json4_json.h" -#include "web/root/jsonsys_json.h" -#include "web/root/jsonprices_json.h" +#include "json/json1_json.h" +#include "json/json2_json.h" +#include "json/json3_json.h" +#include "json/json4_json.h" +#include "json/jsonsys_json.h" +#include "json/jsonprices_json.h" bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea) { if(topic.isEmpty() || !mqtt->connected()) @@ -175,7 +175,7 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) { if(topic.isEmpty() || !mqtt->connected()) return false; - if(strlen(eapi->getToken()) == 0) + if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE) return false; time_t now = time(nullptr); @@ -231,6 +231,7 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) { } char ts1hr[24]; + memset(ts1hr, 0, 24); if(min1hrIdx > -1) { time_t ts = now + (SECS_PER_HOUR * min1hrIdx); //Serial.printf("1hr: %d %lu\n", min1hrIdx, ts); @@ -239,6 +240,7 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) { sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); } char ts3hr[24]; + memset(ts3hr, 0, 24); if(min3hrIdx > -1) { time_t ts = now + (SECS_PER_HOUR * min3hrIdx); //Serial.printf("3hr: %d %lu\n", min3hrIdx, ts); @@ -247,6 +249,7 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) { sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); } char ts6hr[24]; + memset(ts6hr, 0, 24); if(min6hrIdx > -1) { time_t ts = now + (SECS_PER_HOUR * min6hrIdx); //Serial.printf("6hr: %d %lu\n", min6hrIdx, ts); @@ -279,7 +282,7 @@ bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) { } bool JsonMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounting* ea) { - if(init || topic.isEmpty() || !mqtt->connected()) + if(topic.isEmpty() || !mqtt->connected()) return false; snprintf_P(json, BufferSize, JSONSYS_JSON, @@ -291,6 +294,5 @@ bool JsonMqttHandler::publishSystem(HwTools* hw, EntsoeApi* eapi, EnergyAccounti hw->getTemperature(), VERSION ); - init = mqtt->publish(topic, json); - return init; + return mqtt->publish(topic, json); } diff --git a/src/mqtt/RawMqttHandler.h b/lib/RawMqttHandler/include/RawMqttHandler.h similarity index 100% rename from src/mqtt/RawMqttHandler.h rename to lib/RawMqttHandler/include/RawMqttHandler.h diff --git a/src/mqtt/RawMqttHandler.cpp b/lib/RawMqttHandler/src/RawMqttHandler.cpp similarity index 97% rename from src/mqtt/RawMqttHandler.cpp rename to lib/RawMqttHandler/src/RawMqttHandler.cpp index a4ff9f32..35a516f8 100644 --- a/src/mqtt/RawMqttHandler.cpp +++ b/lib/RawMqttHandler/src/RawMqttHandler.cpp @@ -92,15 +92,17 @@ bool RawMqttHandler::publish(AmsData* data, AmsData* meterState, EnergyAccountin } mqtt->publish(topic + "/realtime/import/hour", String(ea->getUseThisHour(), 3)); mqtt->publish(topic + "/realtime/import/day", String(ea->getUseToday(), 2)); + mqtt->publish(topic + "/realtime/import/month", String(ea->getUseThisMonth(), 1)); uint8_t peakCount = ea->getConfig()->hours; if(peakCount > 5) peakCount = 5; for(uint8_t i = 1; i <= peakCount; i++) { - mqtt->publish(topic + "/realtime/import/peak/" + String(i, 10), String(ea->getPeak(i), 10), true, 0); + mqtt->publish(topic + "/realtime/import/peak/" + String(i, 10), String(ea->getPeak(i).value / 100.0, 10), true, 0); } mqtt->publish(topic + "/realtime/import/threshold", String(ea->getCurrentThreshold(), 10), true, 0); mqtt->publish(topic + "/realtime/import/monthmax", String(ea->getMonthMax(), 3), true, 0); mqtt->publish(topic + "/realtime/export/hour", String(ea->getProducedThisHour(), 3)); mqtt->publish(topic + "/realtime/export/day", String(ea->getProducedToday(), 2)); + mqtt->publish(topic + "/realtime/export/month", String(ea->getProducedThisMonth(), 1)); return true; } @@ -121,7 +123,7 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) bool RawMqttHandler::publishPrices(EntsoeApi* eapi) { if(topic.isEmpty() || !mqtt->connected()) return false; - if(strcmp(eapi->getToken(), "") == 0) + if(eapi->getValueForHour(0) == ENTSOE_NO_VALUE) return false; time_t now = time(nullptr); diff --git a/lib/SvelteUi/app/.gitignore b/lib/SvelteUi/app/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/lib/SvelteUi/app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/lib/SvelteUi/app/index.html b/lib/SvelteUi/app/index.html new file mode 100644 index 00000000..b1a58959 --- /dev/null +++ b/lib/SvelteUi/app/index.html @@ -0,0 +1,14 @@ + + + + + + + + AMS reader + + +
        + + + diff --git a/lib/SvelteUi/app/jsconfig.json b/lib/SvelteUi/app/jsconfig.json new file mode 100644 index 00000000..ee5e92f2 --- /dev/null +++ b/lib/SvelteUi/app/jsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "moduleResolution": "Node", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "importsNotUsedAsValues": "error", + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/lib/SvelteUi/app/manifest.json b/lib/SvelteUi/app/manifest.json new file mode 100644 index 00000000..9f8e30eb --- /dev/null +++ b/lib/SvelteUi/app/manifest.json @@ -0,0 +1,19 @@ +// HTTPS required for this to work +// Remember: +{ + "short_name": "amsreader", + "name": "AMS reader", + "icons": [ + { + "src": "/favicon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ], + "start_url": "/", + "background_color": "#f3f4f6", + "display": "standalone", + "scope": "/", + "theme_color": "#7c3aed" +} diff --git a/lib/SvelteUi/app/package-lock.json b/lib/SvelteUi/app/package-lock.json new file mode 100644 index 00000000..e1f46dd0 --- /dev/null +++ b/lib/SvelteUi/app/package-lock.json @@ -0,0 +1,4420 @@ +{ + "name": "svelte-gui", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "svelte-gui", + "version": "0.0.0", + "dependencies": { + "cssnano": "^5.1.14" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "@tailwindcss/forms": "^0.5.2", + "autoprefixer": "^10.4.7", + "http-proxy-middleware": "^2.0.1", + "postcss": "^8.4.14", + "postcss-load-config": "^4.0.1", + "svelte": "^3.49.0", + "svelte-navigator": "^3.2.2", + "svelte-preprocess": "^4.10.7", + "tailwindcss": "^3.1.5", + "vite": "^3.0.7" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "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/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.4.tgz", + "integrity": "sha512-UZco2fdj0OVuRWC0SUJjEOftITc2IeHLFJNp00ym9MuQ9dShnlO4P29G8KUxRlcS7kSpzHuko6eCR9MOALj7lQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^4.2.1", + "debug": "^4.3.4", + "deepmerge": "^4.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.26.2", + "svelte-hmr": "^0.14.12" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "diff-match-patch": "^1.0.5", + "svelte": "^3.44.0", + "vite": "^3.0.0 || ^3.1.0-beta.1" + }, + "peerDependenciesMeta": { + "diff-match-patch": { + "optional": true + } + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.3.tgz", + "integrity": "sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/http-proxy": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", + "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.7.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz", + "integrity": "sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==", + "dev": true + }, + "node_modules/@types/pug": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", + "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", + "dev": true + }, + "node_modules/@types/sass": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.43.1.tgz", + "integrity": "sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "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-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.8", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz", + "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.3", + "caniuse-lite": "^1.0.30001373", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "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/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001434", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", + "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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/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/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "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/css-declaration-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", + "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==", + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.14.tgz", + "integrity": "sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw==", + "dependencies": { + "cssnano-preset-default": "^5.2.13", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz", + "integrity": "sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==", + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.0", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.3", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.1", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz", + "integrity": "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", + "dev": true + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "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.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "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/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "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==", + "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/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "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-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-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-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/magic-string": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.3.tgz", + "integrity": "sha512-u1Po0NDyFcwdg2nzHT88wSK0+Rih0N1M+Ph1Sp08k8yvFFU3KR72wryS7e1qMPJypt99WB7fIFVCA92mQrMjrg==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "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.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "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/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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-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/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "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/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-colormin": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.0.tgz", + "integrity": "sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==", + "dependencies": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz", + "integrity": "sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.6" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz", + "integrity": "sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "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/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "2.77.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", + "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "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/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/sorcery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", + "integrity": "sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0", + "sourcemap-codec": "^1.3.0" + }, + "bin": { + "sorcery": "bin/index.js" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "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/svelte": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz", + "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/svelte-hmr": { + "version": "0.14.12", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.14.12.tgz", + "integrity": "sha512-4QSW/VvXuqVcFZ+RhxiR8/newmwOCTlbYIezvkeN6302YFRE8cXy0naamHcjz8Y9Ce3ITTZtrHrIL0AGfyo61w==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": ">=3.19.0" + } + }, + "node_modules/svelte-navigator": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/svelte-navigator/-/svelte-navigator-3.2.2.tgz", + "integrity": "sha512-Xio4ohLUG1nQJ+ENNbLphXXu9L189fnI1WGg+2Q3CIMPe8Jm2ipytKQthdBs8t0mN7p3Eb03SE9hq0xZAqwQNQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "svelte2tsx": "^0.1.151" + }, + "peerDependencies": { + "svelte": "3.x" + } + }, + "node_modules/svelte-preprocess": { + "version": "4.10.7", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz", + "integrity": "sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.4", + "@types/sass": "^1.16.0", + "detect-indent": "^6.0.0", + "magic-string": "^0.25.7", + "sorcery": "^0.10.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 9.11.2" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0", + "svelte": "^3.23.0", + "typescript": "^3.9.5 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte-preprocess/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/svelte2tsx": { + "version": "0.1.193", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.1.193.tgz", + "integrity": "sha512-vzy4YQNYDnoqp2iZPnJy7kpPAY6y121L0HKrSBjU/IWW7DQ6T7RMJed2VVHFmVYm0zAGYMDl9urPc6R4DDUyhg==", + "dev": true, + "dependencies": { + "dedent-js": "^1.0.1", + "pascal-case": "^3.1.1" + }, + "peerDependencies": { + "svelte": "^3.24", + "typescript": "^4.1.2" + } + }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.8.tgz", + "integrity": "sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==", + "dev": true, + "dependencies": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.14", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "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/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, + "node_modules/typescript": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz", + "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vite": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.0.9.tgz", + "integrity": "sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.47", + "postcss": "^8.4.16", + "resolve": "^1.22.1", + "rollup": ">=2.75.6 <2.77.0 || ~2.77.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "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/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", + "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==", + "dev": true, + "engines": { + "node": ">= 14" + } + } + }, + "dependencies": { + "@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "dev": true, + "optional": true + }, + "@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, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@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 + }, + "@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, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, + "@sveltejs/vite-plugin-svelte": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.4.tgz", + "integrity": "sha512-UZco2fdj0OVuRWC0SUJjEOftITc2IeHLFJNp00ym9MuQ9dShnlO4P29G8KUxRlcS7kSpzHuko6eCR9MOALj7lQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^4.2.1", + "debug": "^4.3.4", + "deepmerge": "^4.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.26.2", + "svelte-hmr": "^0.14.12" + } + }, + "@tailwindcss/forms": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.3.tgz", + "integrity": "sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==", + "dev": true, + "requires": { + "mini-svg-data-uri": "^1.2.3" + } + }, + "@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" + }, + "@types/http-proxy": { + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", + "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "18.7.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz", + "integrity": "sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==", + "dev": true + }, + "@types/pug": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", + "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", + "dev": true + }, + "@types/sass": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.43.1.tgz", + "integrity": "sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "autoprefixer": { + "version": "10.4.8", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz", + "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==", + "dev": true, + "requires": { + "browserslist": "^4.21.3", + "caniuse-lite": "^1.0.30001373", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "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 + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "requires": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001434", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", + "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==" + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "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, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "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 + }, + "colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "css-declaration-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", + "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==", + "requires": {} + }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "cssnano": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.14.tgz", + "integrity": "sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw==", + "requires": { + "cssnano-preset-default": "^5.2.13", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + } + } + }, + "cssnano-preset-default": { + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz", + "integrity": "sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==", + "requires": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.0", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.3", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.1", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + } + }, + "cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "requires": {} + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "requires": { + "css-tree": "^1.1.2" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "dedent-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz", + "integrity": "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==", + "dev": true + }, + "detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true + }, + "detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "requires": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "requires": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "dev": true, + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "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, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true + }, + "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 + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "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" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "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, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "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 + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true + }, + "kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true + }, + "lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "magic-string": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.3.tgz", + "integrity": "sha512-u1Po0NDyFcwdg2nzHT88wSK0+Rih0N1M+Ph1Sp08k8yvFFU3KR72wryS7e1qMPJypt99WB7fIFVCA92mQrMjrg==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true + }, + "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, + "requires": { + "wrappy": "1" + } + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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 + }, + "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 + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "postcss": { + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "requires": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-colormin": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.0.tgz", + "integrity": "sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==", + "requires": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "requires": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "requires": {} + }, + "postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "requires": {} + }, + "postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "requires": {} + }, + "postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "requires": {} + }, + "postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + } + }, + "postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "requires": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + } + }, + "postcss-merge-rules": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz", + "integrity": "sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==", + "requires": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "requires": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "requires": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "requires": { + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.6" + } + }, + "postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "requires": {} + }, + "postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "requires": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "requires": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "requires": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-reduce-initial": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz", + "integrity": "sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==", + "requires": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "requires": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + } + }, + "postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "requires": { + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "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 + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "2.77.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", + "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "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, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "requires": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "sorcery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", + "integrity": "sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==", + "dev": true, + "requires": { + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0", + "sourcemap-codec": "^1.3.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "requires": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + } + }, + "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 + }, + "svelte": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.49.0.tgz", + "integrity": "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==", + "dev": true + }, + "svelte-hmr": { + "version": "0.14.12", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.14.12.tgz", + "integrity": "sha512-4QSW/VvXuqVcFZ+RhxiR8/newmwOCTlbYIezvkeN6302YFRE8cXy0naamHcjz8Y9Ce3ITTZtrHrIL0AGfyo61w==", + "dev": true, + "requires": {} + }, + "svelte-navigator": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/svelte-navigator/-/svelte-navigator-3.2.2.tgz", + "integrity": "sha512-Xio4ohLUG1nQJ+ENNbLphXXu9L189fnI1WGg+2Q3CIMPe8Jm2ipytKQthdBs8t0mN7p3Eb03SE9hq0xZAqwQNQ==", + "dev": true, + "requires": { + "svelte2tsx": "^0.1.151" + } + }, + "svelte-preprocess": { + "version": "4.10.7", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz", + "integrity": "sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==", + "dev": true, + "requires": { + "@types/pug": "^2.0.4", + "@types/sass": "^1.16.0", + "detect-indent": "^6.0.0", + "magic-string": "^0.25.7", + "sorcery": "^0.10.0", + "strip-indent": "^3.0.0" + }, + "dependencies": { + "magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + } + } + }, + "svelte2tsx": { + "version": "0.1.193", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.1.193.tgz", + "integrity": "sha512-vzy4YQNYDnoqp2iZPnJy7kpPAY6y121L0HKrSBjU/IWW7DQ6T7RMJed2VVHFmVYm0zAGYMDl9urPc6R4DDUyhg==", + "dev": true, + "requires": { + "dedent-js": "^1.0.1", + "pascal-case": "^3.1.1" + } + }, + "svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "requires": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + } + }, + "tailwindcss": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.8.tgz", + "integrity": "sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==", + "dev": true, + "requires": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.14", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "dependencies": { + "postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + } + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } + } + }, + "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, + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, + "typescript": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz", + "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==", + "dev": true, + "peer": true + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "vite": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.0.9.tgz", + "integrity": "sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw==", + "dev": true, + "requires": { + "esbuild": "^0.14.47", + "fsevents": "~2.3.2", + "postcss": "^8.4.16", + "resolve": "^1.22.1", + "rollup": ">=2.75.6 <2.77.0 || ~2.77.0" + } + }, + "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 + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yaml": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", + "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==", + "dev": true + } + } +} diff --git a/lib/SvelteUi/app/package.json b/lib/SvelteUi/app/package.json new file mode 100644 index 00000000..1de63b5d --- /dev/null +++ b/lib/SvelteUi/app/package.json @@ -0,0 +1,27 @@ +{ + "name": "svelte-gui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "@tailwindcss/forms": "^0.5.2", + "autoprefixer": "^10.4.7", + "http-proxy-middleware": "^2.0.1", + "postcss": "^8.4.14", + "postcss-load-config": "^4.0.1", + "svelte": "^3.49.0", + "svelte-navigator": "^3.2.2", + "svelte-preprocess": "^4.10.7", + "tailwindcss": "^3.1.5", + "vite": "^3.0.7" + }, + "dependencies": { + "cssnano": "^5.1.14" + } +} diff --git a/lib/SvelteUi/app/postcss.config.cjs b/lib/SvelteUi/app/postcss.config.cjs new file mode 100644 index 00000000..04e4301f --- /dev/null +++ b/lib/SvelteUi/app/postcss.config.cjs @@ -0,0 +1,15 @@ +const tailwindcss = require("tailwindcss"); +const autoprefixer = require("autoprefixer"); +const cssnano = require("cssnano"); + +const config = { + plugins: [ + //Some plugins, like tailwindcss/nesting, need to run before Tailwind, + tailwindcss(), + //But others, like autoprefixer, need to run after, + autoprefixer, + cssnano() + ], +}; + +module.exports = config; diff --git a/lib/SvelteUi/app/service-worker.js b/lib/SvelteUi/app/service-worker.js new file mode 100644 index 00000000..55683df7 --- /dev/null +++ b/lib/SvelteUi/app/service-worker.js @@ -0,0 +1,11 @@ +self.addEventListener('install', (event) => { + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + return self.clients.claim(); +}); + +self.addEventListener('fetch', function(event) { + event.respondWith(fetch(event.request)); +}); \ No newline at end of file diff --git a/lib/SvelteUi/app/src/App.svelte b/lib/SvelteUi/app/src/App.svelte new file mode 100644 index 00000000..dae7d266 --- /dev/null +++ b/lib/SvelteUi/app/src/App.svelte @@ -0,0 +1,69 @@ + + +
        + +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {#if sysinfo.upgrading} + + {:else if sysinfo.booting} + + {/if} +
        diff --git a/lib/SvelteUi/app/src/app.postcss b/lib/SvelteUi/app/src/app.postcss new file mode 100644 index 00000000..dace2220 --- /dev/null +++ b/lib/SvelteUi/app/src/app.postcss @@ -0,0 +1,152 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.gh-logo { + width: 2rem; + height: 2rem; +} + +.cnt { + @apply bg-white m-2 p-2 rounded shadow-lg +} + +.in-pre { + @apply flex items-center bg-gray-100 rounded-l-md border border-r-0 border-gray-300 px-3 whitespace-nowrap text-sm +} + +.in-post { + @apply flex items-center bg-gray-100 rounded-r-md border border-l-0 border-gray-300 px-3 whitespace-nowrap text-sm +} + +.in-txt { + @apply h-10 shadow-sm border-gray-300 disabled:bg-gray-200 +} +.in-f { + @apply in-txt rounded-l-md +} +.in-m { + @apply in-txt border-l-0 +} +.in-l { + @apply in-txt border-l-0 rounded-r-md +} +.in-s { + @apply in-txt rounded-md w-full +} +.tr { + @apply text-right +} + +.bd-grn { + @apply my-auto bg-green-500 text-green-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded +} +.bd-ylo { + @apply my-auto bg-yellow-500 text-yellow-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded +} +.bd-red { + @apply my-auto bg-red-500 text-red-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded +} +.bd-blu { + @apply my-auto bg-blue-500 text-blue-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded +} +.bd-gry { + @apply my-auto bg-gray-500 text-gray-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded +} + +.btn-pri { + @apply py-2 px-4 rounded bg-blue-500 text-white mr-3 +} + +.pl-root { + position: relative; +} +.pl-ov { + position: absolute; + top: 27%; + left: 25%; + width: 50%; + text-align: center; +} +.pl-val { + font-size: 1.7rem; +} +.pl-unt { + font-size: 1.0rem; + color: grey; +} +.pl-sub { + padding-top: 10px; + font-size: 1.0rem; +} +.pl-snt { + font-size: 0.7rem; + color: grey; +} +.pl-lab { + font-size: 1.0rem; +} + +.chart { + width: 100%; + height: 100%; + margin: 0 auto; +} + +svg { + position: relative; + width: 100%; +} + +.tick { + font-family: Helvetica, Arial; + font-size: 0.85em; + font-weight: 200; +} + +.tick line { + stroke: #e2e2e2; + stroke-dasharray: 2; +} + +.tick text { + fill: #999; + text-anchor: start; +} + +.tick.tick-0 line { + stroke-dasharray: 0; +} + +.tick.tick-green line { + stroke: #32d900 !important; +} + +.tick.tick-green text { + fill: #32d900 !important; +} + +.tick.tick-orange line { + stroke: #d95600 !important; +} + +.tick.tick-orange text { + fill: #d95600 !important; +} + +.x-axis .tick text { + text-anchor: middle; +} + +.bars rect { + stroke: rgb(0,0,0); + stroke-opacity: 0.25; + opacity: 0.9; +} + +.bars text { + font-family: Helvetica, Arial; + font-size: 0.85em; + display: block; + text-align: center; +} diff --git a/lib/SvelteUi/app/src/assets/favicon.svg b/lib/SvelteUi/app/src/assets/favicon.svg new file mode 100644 index 00000000..b00c064f --- /dev/null +++ b/lib/SvelteUi/app/src/assets/favicon.svg @@ -0,0 +1,19 @@ + + + + Amsleser + + + + + + + + + diff --git a/lib/SvelteUi/app/src/assets/github.svg b/lib/SvelteUi/app/src/assets/github.svg new file mode 100644 index 00000000..f25b68d5 --- /dev/null +++ b/lib/SvelteUi/app/src/assets/github.svg @@ -0,0 +1,6 @@ + + + + GitHub + + diff --git a/lib/SvelteUi/app/src/lib/AccountingData.svelte b/lib/SvelteUi/app/src/lib/AccountingData.svelte new file mode 100644 index 00000000..c21dafbb --- /dev/null +++ b/lib/SvelteUi/app/src/lib/AccountingData.svelte @@ -0,0 +1,54 @@ + + +
        + Real time calculation + + {#if data && data.h !== undefined} +
        +
        Hour
        +
        {data.h.u ? data.h.u.toFixed(2) : '-'} kWh {#if currency && (hasExport)}/ {data.h.c ? data.h.c.toFixed(2) : '-'} {currency}{/if}
        +
        +
        +
        Day
        +
        {data.d.u ? data.d.u.toFixed(1) : '-'} kWh {#if currency && (hasExport)}/ {data.d.c ? data.d.c.toFixed(2) : '-'} {currency}{/if}
        +
        +
        +
        Month
        +
        {data.m.u ? data.m.u.toFixed(0) : '-'} kWh {#if currency && (hasExport)}/ {data.m.c ? data.m.c.toFixed(2) : '-'} {currency}{/if}
        +
        +
        + {#if hasExport} +
        +
        Hour
        +
        {data.h.p ? data.h.p.toFixed(2) : '-'} kWh {#if currency}/ {data.h.i ? data.h.i.toFixed(2) : '-'} {currency}{/if}
        +
        +
        +
        Day
        +
        {data.d.p ? data.d.p.toFixed(1) : '-'} kWh {#if currency}/ {data.d.i ? data.d.i.toFixed(2) : '-'} {currency}{/if}
        +
        +
        +
        Month
        +
        {data.m.p ? data.m.p.toFixed(0) : '-'} kWh {#if currency}/ {data.m.i ? data.m.i.toFixed(2) : '-'} {currency}{/if}
        +
        + {:else} +
        +
        Hour
        +
        {data.h.c ? data.h.c.toFixed(2) : '-'} {currency}
        +
        +
        +
        Day
        +
        {data.d.c ? data.d.c.toFixed(2) : '-'} {currency}
        +
        +
        +
        Month
        +
        {data.m.c ? data.m.c.toFixed(2) : '-'} {currency}
        +
        + {/if} +
        + {/if} +
        \ No newline at end of file diff --git a/lib/SvelteUi/app/src/lib/AmpPlot.svelte b/lib/SvelteUi/app/src/lib/AmpPlot.svelte new file mode 100644 index 00000000..3e5c9a75 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/AmpPlot.svelte @@ -0,0 +1,62 @@ + + diff --git a/lib/SvelteUi/app/src/lib/Badge.svelte b/lib/SvelteUi/app/src/lib/Badge.svelte new file mode 100644 index 00000000..ae1eb16c --- /dev/null +++ b/lib/SvelteUi/app/src/lib/Badge.svelte @@ -0,0 +1,16 @@ + +{#if color == 'green'} +{text} +{:else if color === `yellow`} +{text} +{:else if color === `red`} +{text} +{:else if color === `blue`} +{text} +{:else if color === `gray`} +{text} +{/if} \ No newline at end of file diff --git a/lib/SvelteUi/app/src/lib/BarChart.svelte b/lib/SvelteUi/app/src/lib/BarChart.svelte new file mode 100644 index 00000000..e2ba9732 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/BarChart.svelte @@ -0,0 +1,106 @@ + + +
        + {#if config.title} + {config.title} + {/if} + + + + {#each config.y.ticks as tick} + + + {tick.label} + + {/each} + + + + + {#each config.x.ticks as point, i} + + {point.label} + + {/each} + + + + {#each config.points as point, i} + {#if point.value !== undefined} + + + {#if barWidth > 15} + {point.label} + {/if} + {/if} + {#if point.value2 > 0.0001} + + {#if barWidth > 15} + {point.label2} + {/if} + {/if} + {/each} + + +
        diff --git a/lib/SvelteUi/app/src/lib/BoardTypeSelectOptions.svelte b/lib/SvelteUi/app/src/lib/BoardTypeSelectOptions.svelte new file mode 100644 index 00000000..d0d51c39 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/BoardTypeSelectOptions.svelte @@ -0,0 +1,48 @@ + + + +{#if chip == 'esp8266'} + + + + + + + + + + + + + + + +{/if} +{#if chip == 'esp32'} + + + + + + +{/if} +{#if chip == 'esp32s2'} + + + + + + + + + +{/if} +{#if chip == 'esp32solo'} + + + +{/if} diff --git a/lib/SvelteUi/app/src/lib/Clock.svelte b/lib/SvelteUi/app/src/lib/Clock.svelte new file mode 100644 index 00000000..e0f5dc57 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/Clock.svelte @@ -0,0 +1,11 @@ + + +{#if Math.abs(new Date().getTime()-timestamp.getTime()) < 300000 } +{`${zeropad(timestamp.getDate())}. ${monthnames[timestamp.getMonth()]} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`} +{:else} +{`${zeropad(timestamp.getDate())}.${zeropad(timestamp.getMonth())}.${timestamp.getFullYear()} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`} +{/if} diff --git a/lib/SvelteUi/app/src/lib/ConfigurationPanel.svelte b/lib/SvelteUi/app/src/lib/ConfigurationPanel.svelte new file mode 100644 index 00000000..29bc503d --- /dev/null +++ b/lib/SvelteUi/app/src/lib/ConfigurationPanel.svelte @@ -0,0 +1,768 @@ + + + +
        +
        + General + + +
        +
        +
        + Hostname
        + +
        +
        + Time zone
        + +
        +
        +
        + +
        + Price region
        + +
        +
        +
        +
        + Currency
        + +
        +
        + Multiplier
        + +
        +
        +
        +
        + + {#if configuration.p.e && sysinfo.chip != 'esp8266'} +
        + {/if} +
        +
        + Security
        + +
        + {#if configuration.g.s > 0} +
        + Username
        + +
        +
        + Password
        + +
        + {/if} +
        +
        + Meter + + +
        + Serial configuration +
        + + + +
        +
        + +
        + Voltage
        + +
        +
        +
        + Main fuse
        + +
        +
        + Production
        + +
        +
        +
        +
        + +
        + + {#if configuration.m.e.e} +
        + {/if} +
        + {#if configuration.m.e.e} +
        + Authentication key
        + +
        + {/if} + + + {#if configuration.m.m.e} +
        +
        + Watt
        + +
        +
        + Volt
        + +
        +
        + Amp
        + +
        +
        + kWh
        + +
        +
        + {/if} +
        +
        + WiFi + + +
        + SSID
        + +
        +
        + Password
        + +
        +
        +
        + Power saving
        + +
        +
        + Power
        +
        + + dBm +
        +
        +
        +
        + +
        +
        +
        + Network + +
        + IP
        +
        + + + +
        +
        + {#if configuration.n.m == 'static'} +
        + Gateway
        + +
        +
        + DNS
        +
        + + +
        +
        + {/if} +
        + +
        + +
        + NTP
        +
        + +
        +
        +
        +
        + MQTT + + +
        + Server + {#if sysinfo.chip != 'esp8266'} + + {/if} +
        +
        + + +
        +
        + {#if configuration.q.s.e} +
        +
        + + {#if configuration.q.s.c} + + {:else} + + {/if} + + + + {#if configuration.q.s.r} + + {:else} + + {/if} + + + + {#if configuration.q.s.k} + + {:else} + + {/if} + +
        +
        + {/if} +
        + Username
        + +
        +
        + Password
        + +
        +
        +
        + Client ID
        + +
        +
        + Payload
        + +
        +
        +
        + Publish topic
        + +
        +
        + {#if configuration.q.m == 3} +
        + Domoticz + + +
        +
        + Electricity IDX
        + +
        +
        + Current IDX
        + +
        +
        +
        + Voltage IDX: L1, L2 & L3 +
        + + + +
        +
        +
        + {/if} + {#if configuration.p.r.startsWith("10YNO") || configuration.p.r == '10Y1001A1001A48H'} +
        + Tariff thresholds + + +
        + + + + + + + + + +
        + +
        + {/if} +
        + User interface + + +
        +
        + Import gauge
        + +
        +
        + Export gauge
        + +
        +
        + Voltage
        + +
        +
        + Amperage
        + +
        +
        + Reactive
        + +
        +
        + Realtime
        + +
        +
        + Peaks
        + +
        +
        + Price
        + +
        +
        + Day plot
        + +
        +
        + Month plot
        + +
        +
        + Temperature plot
        + +
        +
        +
        + {#if sysinfo.board > 20 || sysinfo.chip == 'esp8266'} +
        + Hardware + + {#if sysinfo.board > 20} + +
        +
        + HAN
        + +
        +
        + AP button
        + +
        +
        + LED
        +
        + +
        +
        +
        + RGB
        +
        + + + +
        +
        +
        + Temperature
        + +
        +
        + Analog temp
        + +
        + {#if sysinfo.chip != 'esp8266'} +
        + Vcc
        + +
        + {/if} + {#if configuration.i.v.p > 0} +
        + Voltage divider
        +
        + + +
        +
        + {/if} +
        + {/if} + {#if sysinfo.chip == 'esp8266'} + +
        +
        + Vcc offset
        + +
        +
        + Multiplier
        + +
        + {#if sysinfo.board == 2 || sysinfo.board == 100} +
        + Boot limit
        + +
        + {/if} +
        + {/if} +
        + {/if} +
        + Debugging + + +
        + +
        + {#if configuration.d.s} +
        Debug can cause sudden reboots. Do not leave on!
        +
        + +
        + {#if configuration.d.t} +
        Telnet is unsafe and should be off when not in use
        + {/if} +
        + +
        + {/if} +
        +
        +
        +
        + +
        +
        + +
        +
        + +
        +
        + + + + + diff --git a/lib/SvelteUi/app/src/lib/ConfigurationStore.js b/lib/SvelteUi/app/src/lib/ConfigurationStore.js new file mode 100644 index 00000000..c6a678ec --- /dev/null +++ b/lib/SvelteUi/app/src/lib/ConfigurationStore.js @@ -0,0 +1,10 @@ +import { writable } from 'svelte/store'; + +let configuration = {}; +export const configurationStore = writable(configuration); + +export async function getConfiguration() { + const response = await fetch("/configuration.json"); + configuration = (await response.json()) + configurationStore.set(configuration); +}; diff --git a/lib/SvelteUi/app/src/lib/ConsentComponent.svelte b/lib/SvelteUi/app/src/lib/ConsentComponent.svelte new file mode 100644 index 00000000..cf69870a --- /dev/null +++ b/lib/SvelteUi/app/src/lib/ConsentComponent.svelte @@ -0,0 +1,54 @@ + + +
        +
        +
        +
        + Various permissions we need to do stuff: +
        +
        +
        + Enable one-click upgrade? (implies data collection)
        + Read more
        +
        +
        +
        + +
        +
        +
        +
        + + diff --git a/lib/SvelteUi/app/src/lib/CountrySelectOptions.svelte b/lib/SvelteUi/app/src/lib/CountrySelectOptions.svelte new file mode 100644 index 00000000..c3ff3ac5 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/CountrySelectOptions.svelte @@ -0,0 +1,10 @@ + + + +{#each europe as c} + +{/each} diff --git a/lib/SvelteUi/app/src/lib/Dashboard.svelte b/lib/SvelteUi/app/src/lib/Dashboard.svelte new file mode 100644 index 00000000..8cc35c26 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/Dashboard.svelte @@ -0,0 +1,103 @@ + + +
        + {#if uiVisibility(sysinfo.ui.i, data.i)} +
        +
        +
        + +
        +
        {data.mt ? metertype(data.mt) : '-'}
        +
        {data.ic ? data.ic.toFixed(1) : '-'} kWh
        +
        +
        + {/if} + {#if uiVisibility(sysinfo.ui.e, data.om || data.e > 0)} +
        +
        +
        + +
        +
        +
        {data.ec ? data.ec.toFixed(1) : '-'} kWh
        +
        +
        + {/if} + {#if uiVisibility(sysinfo.ui.v, data.u1 > 100 || data.u2 > 100 || data.u3 > 100)} +
        + +
        + {/if} + {#if uiVisibility(sysinfo.ui.a, data.i1 > 0.01 || data.i2 > 0.01 || data.i3 > 0.01)} +
        + +
        + {/if} + {#if uiVisibility(sysinfo.ui.r, data.ri > 0 || data.re > 0 || data.ric > 0 || data.rec > 0)} +
        + +
        + {/if} + {#if uiVisibility(sysinfo.ui.c, data.ea)} +
        + +
        + {/if} + {#if data && data.pr && (data.pr.startsWith("10YNO") || data.pr == '10Y1001A1001A48H')} +
        + +
        + {/if} + {#if uiVisibility(sysinfo.ui.p, (typeof data.p == "number") && !Number.isNaN(data.p))} +
        + +
        + {/if} + {#if uiVisibility(sysinfo.ui.d, dayPlot)} +
        + +
        + {/if} + {#if uiVisibility(sysinfo.ui.m, monthPlot)} +
        + +
        + {/if} + {#if uiVisibility(sysinfo.ui.s, data.t && data.t != -127 && temperatures.c > 1)} +
        + +
        + {/if} +
        \ No newline at end of file diff --git a/lib/SvelteUi/app/src/lib/DataStores.js b/lib/SvelteUi/app/src/lib/DataStores.js new file mode 100644 index 00000000..d664fc25 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/DataStores.js @@ -0,0 +1,183 @@ +import { readable, writable } from 'svelte/store'; +import { isBusPowered } from './Helpers'; + +async function fetchWithTimeout(resource, options = {}) { + const { timeout = 8000 } = options; + + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + const response = await fetch(resource, { + ...options, + signal: controller.signal + }); + clearTimeout(id); + return response; + } + +let sysinfo = { + version: '', + chip: '', + mac: null, + apmac: null, + vndcfg: null, + usrcfg: null, + fwconsent: null, + booting: false, + upgrading: false, + ui: {}, + security: 0 +}; +export const sysinfoStore = writable(sysinfo); +export async function getSysinfo() { + const response = await fetchWithTimeout("/sysinfo.json?t=" + Math.floor(Date.now() / 1000)); + sysinfo = (await response.json()) + sysinfoStore.set(sysinfo); +}; + +let tries = 0; +let lastTemp = -127; +let lastPrice = null; +let data = {}; +export const dataStore = readable(data, (set) => { + let timeout; + async function getData() { + fetchWithTimeout("/data.json") + .then((res) => res.json()) + .then((data) => { + set(data); + if(lastTemp != data.t) { + lastTemp = data.t; + setTimeout(getTemperatures, 2000); + } + if(lastPrice != data.p) { + lastPrice = data.p; + setTimeout(getPrices, 4000); + } + if(sysinfo.upgrading) { + window.location.reload(); + } else if(!sysinfo || !sysinfo.chip || sysinfo.booting || (tries > 1 && !isBusPowered(sysinfo.board))) { + getSysinfo(); + if(dayPlotTimeout) clearTimeout(dayPlotTimeout); + dayPlotTimeout = setTimeout(getDayPlot, 2000); + if(monthPlotTimeout) clearTimeout(monthPlotTimeout); + monthPlotTimeout = setTimeout(getMonthPlot, 3000); + } + let to = 5000; + if(isBusPowered(sysinfo.board) && data.v > 2.5) { + let diff = (3.3 - Math.min(3.3, data.v)); + if(diff > 0) { + to = Math.max(diff, 0.1) * 10 * 5000; + } + } + if(to > 5000) console.log("Scheduling next data fetch in " + to + "ms"); + if(timeout) clearTimeout(timeout); + timeout = setTimeout(getData, to); + tries = 0; + }) + .catch((err) => { + tries++; + if(tries > 3) { + set({ + em: 3, + hm: 0, + wm: 0, + mm: 0 + }); + timeout = setTimeout(getData, 15000); + } else { + timeout = setTimeout(getData, isBusPowered(sysinfo.board) ? 10000 : 5000); + } + }); + } + getData(); + return function stop() { + clearTimeout(timeout); + } +}); + +let prices = {}; +export const pricesStore = writable(prices); +export async function getPrices() { + const response = await fetchWithTimeout("/energyprice.json"); + prices = (await response.json()) + pricesStore.set(prices); +} + +let dayPlot = {}; +let dayPlotTimeout; +export async function getDayPlot() { + if(dayPlotTimeout) { + clearTimeout(dayPlotTimeout); + dayPlotTimeout = 0; + } + const response = await fetchWithTimeout("/dayplot.json"); + dayPlot = (await response.json()) + dayPlotStore.set(dayPlot); + + let date = new Date(); + dayPlotTimeout = setTimeout(getDayPlot, ((60-date.getMinutes())*60000)+20) +} + +export const dayPlotStore = writable(dayPlot, (set) => { + getDayPlot(); + return function stop() {} +}); + +let monthPlot = {}; +let monthPlotTimeout; +export async function getMonthPlot() { + if(monthPlotTimeout) { + clearTimeout(monthPlotTimeout); + monthPlotTimeout = 0; + } + const response = await fetchWithTimeout("/monthplot.json"); + monthPlot = (await response.json()) + monthPlotStore.set(monthPlot); + + let date = new Date(); + monthPlotTimeout = setTimeout(getMonthPlot, ((24-date.getHours())*3600000)+40) +} + +export const monthPlotStore = writable(monthPlot, (set) => { + getMonthPlot(); + return function stop() {} +}); + +let temperatures = {}; +export async function getTemperatures() { + const response = await fetchWithTimeout("/temperature.json"); + temperatures = (await response.json()) + temperaturesStore.set(temperatures); +} + +export const temperaturesStore = writable(temperatures, (set) => { + getTemperatures(); + return function stop() {} +}); + +let tariff = {}; +let tariffTimeout; +export async function getTariff() { + if(tariffTimeout) { + clearTimeout(tariffTimeout); + tariffTimeout = 0; + } + const response = await fetchWithTimeout("/tariff.json"); + tariff = (await response.json()) + tariffStore.set(tariff); + let date = new Date(); + tariffTimeout = setTimeout(getTariff, ((60-date.getMinutes())*60000)+30) +} + +export const tariffStore = writable(tariff, (set) => { + return function stop() {} +}); + +let releases = []; +export const gitHubReleaseStore = writable(releases); + +export async function getGitHubReleases() { + const response = await fetchWithTimeout("https://api.github.com/repos/UtilitechAS/amsreader-firmware/releases"); + releases = (await response.json()) + gitHubReleaseStore.set(releases); +}; diff --git a/lib/SvelteUi/app/src/lib/DayPlot.svelte b/lib/SvelteUi/app/src/lib/DayPlot.svelte new file mode 100644 index 00000000..7ee09bdc --- /dev/null +++ b/lib/SvelteUi/app/src/lib/DayPlot.svelte @@ -0,0 +1,101 @@ + + + diff --git a/lib/SvelteUi/app/src/lib/DownloadIcon.svelte b/lib/SvelteUi/app/src/lib/DownloadIcon.svelte new file mode 100644 index 00000000..ea9993ba --- /dev/null +++ b/lib/SvelteUi/app/src/lib/DownloadIcon.svelte @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/lib/SvelteUi/app/src/lib/FileUploadComponent.svelte b/lib/SvelteUi/app/src/lib/FileUploadComponent.svelte new file mode 100644 index 00000000..a47fbf24 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/FileUploadComponent.svelte @@ -0,0 +1,22 @@ + + +
        +
        + Upload {title} +

        Select a suitable file and click upload

        +
        uploading=true} autocomplete="off"> + +
        + +
        +
        +
        +
        + diff --git a/lib/SvelteUi/app/src/lib/GearIcon.svelte b/lib/SvelteUi/app/src/lib/GearIcon.svelte new file mode 100644 index 00000000..dbcbe8b3 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/GearIcon.svelte @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/lib/SvelteUi/app/src/lib/Header.svelte b/lib/SvelteUi/app/src/lib/Header.svelte new file mode 100644 index 00000000..b0d870bc --- /dev/null +++ b/lib/SvelteUi/app/src/lib/Header.svelte @@ -0,0 +1,100 @@ + + +
        +
        +
        + AMS reader {sysinfo.version} +
        +
        +
        + {#if data.t > -50} +
        { data.t > -50 ? data.t.toFixed(1) : '-' }°C
        + {/if} +
        Free mem: {data.m ? (data.m/1000).toFixed(1) : '-'}kb
        +
        +
        + 2.0 ? data.v.toFixed(2)+"V" : "ESP"} color={sysinfo.booting ? 'yellow' : data.em === 1 ? 'green' : data.em === 2 ? 'yellow' : data.em === 3 ? 'red' : 'gray'}/> + + + +
        + {#if data.he < 0 || data.he > 0} +
        { 'HAN: ' + hanError(data.he) }
        + {/if} + {#if data.me < 0} +
        { 'MQTT: ' + mqttError(data.me) }
        + {/if} + {#if data.ee > 0 || data.ee < 0} +
        { 'PriceAPI: ' + priceError(data.ee) }
        + {/if} +
        +
        + GitHub repo +
        +
        + +
        + {#if sysinfo.vndcfg && sysinfo.usrcfg} +
        + +
        +
        + +
        + {/if} +
        + +
        + {#if sysinfo.fwconsent === 1 && nextVersion} +
        + {#if sysinfo.security == 0 || data.a} + + {:else} + New version: {nextVersion.tag_name} + {/if} +
        + {/if} +
        +
        +
        \ No newline at end of file diff --git a/lib/SvelteUi/app/src/lib/HelpIcon.svelte b/lib/SvelteUi/app/src/lib/HelpIcon.svelte new file mode 100644 index 00000000..0916f204 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/HelpIcon.svelte @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/lib/SvelteUi/app/src/lib/Helpers.js b/lib/SvelteUi/app/src/lib/Helpers.js new file mode 100644 index 00000000..e9580d05 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/Helpers.js @@ -0,0 +1,155 @@ +export let monthnames = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; + +export function voltcol(volt) { + if(volt > 218 && volt < 242) return '#32d900'; + if(volt > 212 && volt < 248) return '#b1d900'; + if(volt > 208 && volt < 252) return '#ffb800'; + return '#d90000'; +}; + +export function ampcol(pct) { + if(pct > 90) return '#d90000'; + else if(pct > 85) return'#e32100'; + else if(pct > 80) return '#ffb800'; + else if(pct > 75) return '#dcd800'; + else return '#32d900'; +}; + +export function metertype(mt) { + switch(mt) { + case 1: + return "Aidon"; + case 2: + return "Kaifa"; + case 3: + return "Kamstrup"; + case 8: + return "Iskra"; + case 9: + return "Landis+Gyr"; + case 10: + return "Sagemcom"; + default: + return ""; + } +} + +export function zeropad(num) { + num = num.toString(); + while (num.length < 2) num = "0" + num; + return num; +} + +export function boardtype(c, b) { + switch(b) { + case 5: + switch(c) { + case 'esp8266': + return "Pow-K (GPIO12)"; + case 'esp32s2': + return "Pow-K+"; + } + case 7: + switch(c) { + case 'esp8266': + return "Pow-U (GPIO12)"; + case 'esp32s2': + return "Pow-U+"; + } + case 6: + return "Pow-P1"; + case 51: + return "Wemos S2 mini"; + case 50: + return "Generic ESP32-S2"; + case 201: + return "Wemos LOLIN D32"; + case 202: + return "Adafruit HUZZAH32"; + case 203: + return "DevKitC"; + case 200: + return "Generic ESP32"; + case 2: + return "HAN Reader 2.0 by Max Spencer"; + case 0: + return "Custom hardware by Roar Fredriksen"; + case 1: + return "Kamstrup module by Egil Opsahl" + case 3: + return "Pow-K (UART0)"; + case 4: + return "Pow-U (UART0)"; + case 101: + return "Wemos D1 mini"; + case 100: + return "Generic ESP8266"; + } +} + +export function hanError(err) { + switch(err) { + case -1: return "Parse error"; + case -2: return "Incomplete data received"; + case -3: return "Payload boundry flag missing"; + case -4: return "Header checksum error"; + case -5: return "Footer checksum error"; + case -9: return "Unknown data received, check meter config"; + case -41: return "Frame length not equal"; + case -51: return "Authentication failed"; + case -52: return "Decryption failed"; + case -53: return "Encryption key invalid"; + case 90: return "No HAN data received last 30s"; + case 98: return "Exception in code, debugging necessary"; + case 99: return "Autodetection failed"; + } + if(err < 0) return "Unspecified error "+err; + return ""; +} + +export function mqttError(err) { + switch(err) { + case -3: return "Connection failed"; + case -4: return "Network timeout"; + case -10: return "Connection denied"; + case -11: return "Failed to subscribe"; + case -13: return "Connection lost"; + } + + if(err < 0) return "Unspecified error "+err; + return ""; +} + +export function priceError(err) { + switch(err) { + case 401: + case 403: + return "Unauthorized, check API key"; + case 404: + return "Price unavailable, not found"; + case 500: + return "Internal server error"; + case -2: return "Incomplete data received"; + case -3: return "Invalid data, tag missing"; + case -51: return "Authentication failed"; + case -52: return "Decryption failed"; + case -53: return "Encryption key invalid"; + } + + if(err < 0) return "Unspecified error "+err; + return ""; +} + +export function isBusPowered(boardType) { + switch(boardType) { + case 2: + case 4: + case 7: + return true; + } + return false; +} + +export function uiVisibility(choice, state) { + return choice == 1 || (choice == 2 && state); +} diff --git a/lib/SvelteUi/app/src/lib/InfoIcon.svelte b/lib/SvelteUi/app/src/lib/InfoIcon.svelte new file mode 100644 index 00000000..44ae7666 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/InfoIcon.svelte @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/lib/SvelteUi/app/src/lib/Mask.svelte b/lib/SvelteUi/app/src/lib/Mask.svelte new file mode 100644 index 00000000..206afeea --- /dev/null +++ b/lib/SvelteUi/app/src/lib/Mask.svelte @@ -0,0 +1,14 @@ + + +{#if active} +
        +
        + {#if message} +
        {message}
        + {/if} +
        +
        +{/if} diff --git a/lib/SvelteUi/app/src/lib/MonthPlot.svelte b/lib/SvelteUi/app/src/lib/MonthPlot.svelte new file mode 100644 index 00000000..15991410 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/MonthPlot.svelte @@ -0,0 +1,103 @@ + + + diff --git a/lib/SvelteUi/app/src/lib/PowerGauge.svelte b/lib/SvelteUi/app/src/lib/PowerGauge.svelte new file mode 100644 index 00000000..65cc757f --- /dev/null +++ b/lib/SvelteUi/app/src/lib/PowerGauge.svelte @@ -0,0 +1,26 @@ + + +
        + + + {label} +
        + {val} + {unit} + {#if sub} +
        + {sub} + {subunit}/kWh + {/if} +
        +
        diff --git a/lib/SvelteUi/app/src/lib/PowerGaugeSvg.svelte b/lib/SvelteUi/app/src/lib/PowerGaugeSvg.svelte new file mode 100644 index 00000000..eef9456a --- /dev/null +++ b/lib/SvelteUi/app/src/lib/PowerGaugeSvg.svelte @@ -0,0 +1,31 @@ + + + + + + diff --git a/lib/SvelteUi/app/src/lib/PricePlot.svelte b/lib/SvelteUi/app/src/lib/PricePlot.svelte new file mode 100644 index 00000000..c7d5d78a --- /dev/null +++ b/lib/SvelteUi/app/src/lib/PricePlot.svelte @@ -0,0 +1,97 @@ + + +Provided by ENTSO-E + diff --git a/lib/SvelteUi/app/src/lib/ReactiveData.svelte b/lib/SvelteUi/app/src/lib/ReactiveData.svelte new file mode 100644 index 00000000..35e8df89 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/ReactiveData.svelte @@ -0,0 +1,24 @@ + + +
        + Reactive + +
        +
        Instant in
        +
        {typeof importInstant !== 'undefined' ? importInstant.toFixed(0) : '-'} VAr
        +
        Instant out
        +
        {typeof exportInstant !== 'undefined' ? exportInstant.toFixed(0) : '-'} VAr
        +
        + +
        +
        Total in
        +
        {typeof importTotal !== 'undefined' ? importTotal.toFixed(1) : '-'} kVArh
        +
        Total out
        +
        {typeof exportTotal !== 'undefined' ? exportTotal.toFixed(1) : '-'} kVArh
        +
        +
        \ No newline at end of file diff --git a/lib/SvelteUi/app/src/lib/SetupPanel.svelte b/lib/SvelteUi/app/src/lib/SetupPanel.svelte new file mode 100644 index 00000000..d1ef3c36 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/SetupPanel.svelte @@ -0,0 +1,121 @@ + + + +
        +
        +
        + + Setup +
        + SSID
        + +
        +
        + PSK
        + +
        +
        + Hostname: + +
        +
        + + {#if staticIp} +
        +
        + + +
        + {/if} +
        + {#if staticIp} +
        +
        + Gateway
        + +
        +
        + DNS
        + +
        +
        + {/if} +
        + +
        +
        +
        +
        + + diff --git a/lib/SvelteUi/app/src/lib/StatusPage.svelte b/lib/SvelteUi/app/src/lib/StatusPage.svelte new file mode 100644 index 00000000..3b8344a1 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/StatusPage.svelte @@ -0,0 +1,188 @@ + + +
        +
        + Device information +
        + Chip: {sysinfo.chip} +
        +
        + Device: {boardtype(sysinfo.chip, sysinfo.board)} +
        +
        + MAC: {sysinfo.mac} +
        + {#if sysinfo.apmac && sysinfo.apmac != sysinfo.mac} +
        + AP MAC: {sysinfo.apmac} +
        + {/if} +
        + + Update consents + + +
        +
        + {#if sysinfo.meter} +
        + Meter +
        + Manufacturer: {metertype(sysinfo.meter.mfg)} +
        +
        + Model: {sysinfo.meter.model} +
        +
        + ID: {sysinfo.meter.id} +
        +
        + {/if} + {#if sysinfo.net} +
        + Network +
        + IP: {sysinfo.net.ip} +
        +
        + Mask: {sysinfo.net.mask} +
        +
        + Gateway: {sysinfo.net.gw} +
        +
        + DNS: {sysinfo.net.dns1} {#if sysinfo.net.dns2 && sysinfo.net.dns2 != '0.0.0.0'}/ {sysinfo.net.dns2}{/if} +
        +
        + {/if} +
        + Firmware +
        + Installed version: {sysinfo.version} +
        + {#if nextVersion} +
        + Latest version: + {nextVersion.tag_name} + {#if (sysinfo.security == 0 || data.a) && sysinfo.fwconsent === 1 && nextVersion && nextVersion.tag_name} +
        + +
        + {/if} +
        + {#if sysinfo.fwconsent === 2} +
        +
        You have disabled one-click firmware upgrade, link to self-upgrade is disabled
        +
        + {/if} + {/if} + {#if (sysinfo.security == 0 || data.a) && isBusPowered(sysinfo.board) } +
        + {boardtype(sysinfo.chip, sysinfo.board)} must be connected to an external power supply during firmware upgrade. Failure to do so may cause power-down during upload resulting in non-functioning unit. +
        + {/if} + {#if sysinfo.security == 0 || data.a} +
        +
        firmwareUploading=true} autocomplete="off"> + + {#if firmwareFiles.length == 0} + + {:else} + {firmwareFiles[0].name} + + {/if} +
        +
        + {/if} +
        + {#if sysinfo.security == 0 || data.a} +
        + Configuration +
        +
        + + + + + + + + + + +
        + {#if configFiles.length == 0} + + {/if} +
        +
        configUploading=true} autocomplete="off"> + + {#if configFiles.length == 0} + + {:else} + {configFiles[0].name} + + {/if} +
        +
        + {/if} +
        + + diff --git a/lib/SvelteUi/app/src/lib/TariffPeakChart.svelte b/lib/SvelteUi/app/src/lib/TariffPeakChart.svelte new file mode 100644 index 00000000..cbe48724 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/TariffPeakChart.svelte @@ -0,0 +1,88 @@ + + + diff --git a/lib/SvelteUi/app/src/lib/TemperaturePlot.svelte b/lib/SvelteUi/app/src/lib/TemperaturePlot.svelte new file mode 100644 index 00000000..204fd87d --- /dev/null +++ b/lib/SvelteUi/app/src/lib/TemperaturePlot.svelte @@ -0,0 +1,66 @@ + + + diff --git a/lib/SvelteUi/app/src/lib/UartSelectOptions.svelte b/lib/SvelteUi/app/src/lib/UartSelectOptions.svelte new file mode 100644 index 00000000..19896a9a --- /dev/null +++ b/lib/SvelteUi/app/src/lib/UartSelectOptions.svelte @@ -0,0 +1,58 @@ + + + +{#if chip == 'esp8266'} + +{/if} +{#if chip == 'esp32' || chip == 'esp32solo'} + + +{/if} +{#if chip == 'esp32s2'} + +{/if} + + +{#if chip.startsWith('esp32')} + + + +{/if} +{#if chip == 'esp8266'} + +{/if} + +{#if chip.startsWith('esp32')} + +{/if} + + + + + +{#if chip.startsWith('esp32')} + +{#if chip != 'esp32s2'} + +{/if} + + + + + + + + + + + +{/if} +{#if chip == 'esp32s2'} + + + + + +{/if} diff --git a/lib/SvelteUi/app/src/lib/UpgradeHelper.js b/lib/SvelteUi/app/src/lib/UpgradeHelper.js new file mode 100644 index 00000000..ec2cb068 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/UpgradeHelper.js @@ -0,0 +1,63 @@ +export async function upgrade(version) { + const data = new URLSearchParams() + data.append('version', version.tag_name); + const response = await fetch('/upgrade', { + method: 'POST', + body: data + }); + let res = (await response.json()) +} + +export function getNextVersion(currentVersion, releases) { + if(/^v\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(currentVersion)) { + var v = currentVersion.substring(1).split('.'); + var v_major = parseInt(v[0]); + var v_minor = parseInt(v[1]); + var v_patch = parseInt(v[2]); + + releases.reverse(); + var next_patch; + var next_minor; + var next_major; + for(var i = 0; i < releases.length; i++) { + var release = releases[i]; + var ver2 = release.tag_name; + var v2 = ver2.substring(1).split('.'); + var v2_major = parseInt(v2[0]); + var v2_minor = parseInt(v2[1]); + var v2_patch = parseInt(v2[2]); + + if(v2_major == v_major) { + if(v2_minor == v_minor) { + if(v2_patch > v_patch) { + next_patch = release; + } + } else if(v2_minor == v_minor+1) { + next_minor = release; + } + } else if(v2_major == v_major+1) { + if(next_major) { + var mv = next_major.tag_name.substring(1).split('.'); + var mv_major = parseInt(mv[0]); + var mv_minor = parseInt(mv[1]); + var mv_patch = parseInt(mv[2]); + if(v2_minor == mv_minor) { + next_major = release; + } + } else { + next_major = release; + } + } + }; + if(next_minor) { + return next_minor; + } else if(next_major) { + return next_major; + } else if(next_patch) { + return next_patch; + } + return false; + } else { + return releases[0]; + } +} diff --git a/lib/SvelteUi/app/src/lib/Uptime.svelte b/lib/SvelteUi/app/src/lib/Uptime.svelte new file mode 100644 index 00000000..cfac45f2 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/Uptime.svelte @@ -0,0 +1,29 @@ + +{#if epoch} + Up + {#if days > 1} + {days} days + {:else if days > 0} + {days} day + {:else if hours > 1} + {hours} hours + {:else if hours > 0} + {hours} hour + {:else if minutes > 1} + {minutes} minutes + {:else if minutes > 0} + {minutes} minute + {:else} + {epoch} seconds + {/if} +{/if} diff --git a/lib/SvelteUi/app/src/lib/VendorPanel.svelte b/lib/SvelteUi/app/src/lib/VendorPanel.svelte new file mode 100644 index 00000000..032927a5 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/VendorPanel.svelte @@ -0,0 +1,65 @@ + + +
        +
        +
        + + Initial configuration +
        + Board type
        + +
        + {#if sysinfo.board && sysinfo.board > 20} +
        + HAN GPIO
        + +
        + {/if} +
        + +
        +
        + +
        +   +
        +
        +
        + diff --git a/lib/SvelteUi/app/src/lib/VoltPlot.svelte b/lib/SvelteUi/app/src/lib/VoltPlot.svelte new file mode 100644 index 00000000..265f2829 --- /dev/null +++ b/lib/SvelteUi/app/src/lib/VoltPlot.svelte @@ -0,0 +1,59 @@ + + diff --git a/lib/SvelteUi/app/src/main.js b/lib/SvelteUi/app/src/main.js new file mode 100644 index 00000000..bc3d21d3 --- /dev/null +++ b/lib/SvelteUi/app/src/main.js @@ -0,0 +1,9 @@ +import "./app.postcss"; +import App from "./App.svelte"; + + +const app = new App({ + target: document.getElementById("app"), +}); + +export default app; diff --git a/lib/SvelteUi/app/src/vite-env.d.ts b/lib/SvelteUi/app/src/vite-env.d.ts new file mode 100644 index 00000000..4078e747 --- /dev/null +++ b/lib/SvelteUi/app/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/lib/SvelteUi/app/svelte.config.js b/lib/SvelteUi/app/svelte.config.js new file mode 100644 index 00000000..678adc0f --- /dev/null +++ b/lib/SvelteUi/app/svelte.config.js @@ -0,0 +1,11 @@ +import preprocess from "svelte-preprocess"; + +const config = { + preprocess: [ + preprocess({ + postcss: true, + }), + ], +}; + +export default config; diff --git a/lib/SvelteUi/app/tailwind.config.cjs b/lib/SvelteUi/app/tailwind.config.cjs new file mode 100644 index 00000000..122253e3 --- /dev/null +++ b/lib/SvelteUi/app/tailwind.config.cjs @@ -0,0 +1,13 @@ +const config = { + content: ["./index.html","./src/**/*.{html,js,svelte,ts}"], + + theme: { + extend: {}, + }, + + plugins: [ + require('@tailwindcss/forms') + ], +}; + +module.exports = config; diff --git a/lib/SvelteUi/app/vite.config.js b/lib/SvelteUi/app/vite.config.js new file mode 100644 index 00000000..a862d15f --- /dev/null +++ b/lib/SvelteUi/app/vite.config.js @@ -0,0 +1,34 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vitejs.dev/config/ +export default defineConfig({ + build: { + outDir: 'dist', + assetsDir: '.', + rollupOptions: { + output: { + assetFileNames: '[name][extname]', + chunkFileNames: '[name].js', + entryFileNames: '[name].js' + } + } + }, + plugins: [svelte()], + server: { + proxy: { + "/data.json": "http://192.168.233.244", + "/energyprice.json": "http://192.168.233.244", + "/dayplot.json": "http://192.168.233.244", + "/monthplot.json": "http://192.168.233.244", + "/temperature.json": "http://192.168.233.244", + "/sysinfo.json": "http://192.168.233.244", + "/configuration.json": "http://192.168.233.244", + "/tariff.json": "http://192.168.233.244", + "/save": "http://192.168.233.244", + "/reboot": "http://192.168.233.244", + "/configfile": "http://192.168.233.244", + "/upgrade": "http://192.168.233.244" + } + } +}) diff --git a/lib/SvelteUi/include/.gitignore b/lib/SvelteUi/include/.gitignore new file mode 100644 index 00000000..ed940275 --- /dev/null +++ b/lib/SvelteUi/include/.gitignore @@ -0,0 +1,2 @@ +html/*.h +json/*.h diff --git a/lib/SvelteUi/include/AmsWebHeaders.h b/lib/SvelteUi/include/AmsWebHeaders.h new file mode 100644 index 00000000..5f2bf1ce --- /dev/null +++ b/lib/SvelteUi/include/AmsWebHeaders.h @@ -0,0 +1,17 @@ +static const char HEADER_CACHE_CONTROL[] PROGMEM = "Cache-Control"; +static const char HEADER_PRAGMA[] PROGMEM = "Pragma"; +static const char HEADER_EXPIRES[] PROGMEM = "Expires"; +static const char HEADER_AUTHENTICATE[] PROGMEM = "WWW-Authenticate"; +static const char HEADER_LOCATION[] PROGMEM = "Location"; + +static const char CACHE_CONTROL_NO_CACHE[] PROGMEM = "no-cache, no-store, must-revalidate"; +static const char CACHE_1HR[] PROGMEM = "public, max-age=3600"; +static const char PRAGMA_NO_CACHE[] PROGMEM = "no-cache"; +static const char EXPIRES_OFF[] PROGMEM = "-1"; +static const char AUTHENTICATE_BASIC[] PROGMEM = "Basic realm=\"Secure Area\""; + +static const char MIME_PLAIN[] PROGMEM = "text/plain"; +static const char MIME_HTML[] PROGMEM = "text/html"; +static const char MIME_JSON[] PROGMEM = "application/json"; +static const char MIME_CSS[] PROGMEM = "text/css"; +static const char MIME_JS[] PROGMEM = "text/javascript"; diff --git a/lib/SvelteUi/include/AmsWebServer.h b/lib/SvelteUi/include/AmsWebServer.h new file mode 100644 index 00000000..bc576203 --- /dev/null +++ b/lib/SvelteUi/include/AmsWebServer.h @@ -0,0 +1,118 @@ +#ifndef _AMSWEBSERVER_h +#define _AMSWEBSERVER_h + +#include "Arduino.h" +#include +#include "AmsConfiguration.h" +#include "HwTools.h" +#include "AmsData.h" +#include "AmsStorage.h" +#include "AmsDataStorage.h" +#include "EnergyAccounting.h" +#include "Uptime.h" +#include "RemoteDebug.h" +#include "EntsoeApi.h" + +#if defined(ESP8266) + #include + #include + #include + #include +#elif defined(ESP32) // ARDUINO_ARCH_ESP32 + #include + #include + #include + #include +#else + #warning "Unsupported board type" +#endif + +#include "LittleFS.h" + +class AmsWebServer { +public: + AmsWebServer(uint8_t* buf, RemoteDebug* Debug, HwTools* hw); + void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, AmsDataStorage*, EnergyAccounting*); + void loop(); + void setMqtt(MQTTClient* mqtt); + void setTimezone(Timezone* tz); + void setMqttEnabled(bool); + void setEntsoeApi(EntsoeApi* eapi); + void setPriceRegion(String); + +private: + RemoteDebug* debugger; + bool mqttEnabled = false; + int maxPwr = 0; + HwTools* hw; + Timezone* tz; + EntsoeApi* eapi = NULL; + AmsConfiguration* config; + GpioConfig* gpioConfig; + MeterConfig* meterConfig; + WebConfig webConfig; + AmsData* meterState; + AmsDataStorage* ds; + EnergyAccounting* ea = NULL; + MQTTClient* mqtt = NULL; + bool uploading = false; + File file; + bool performRestart = false; + bool performUpgrade = false; + bool rebootForUpgrade = false; + String priceRegion = ""; + #if defined(AMS2MQTT_FIRMWARE_URL) + String customFirmwareUrl = AMS2MQTT_FIRMWARE_URL; + #else + String customFirmwareUrl; + #endif + + static const uint16_t BufferSize = 2048; + char* buf; + +#if defined(ESP8266) + ESP8266WebServer server; +#elif defined(ESP32) + WebServer server; +#endif + + bool checkSecurity(byte level, bool send401 = true); + + void indexHtml(); + void indexJs(); + void indexCss(); + void githubSvg(); + void faviconSvg(); + + void sysinfoJson(); + void dataJson(); + void dayplotJson(); + void monthplotJson(); + void energyPriceJson(); + void temperatureJson(); + void tariffJson(); + + void configurationJson(); + void handleSave(); + void reboot(); + void upgrade(); + void firmwareHtml(); + void firmwarePost(); + void firmwareUpload(); + void isAliveCheck(); + + void mqttCaUpload(); + void mqttCertUpload(); + void mqttKeyUpload(); + HTTPUpload& uploadFile(const char* path); + + void configFileDownload(); + void configFileUpload(); + void factoryResetPost(); + + void notFound(); + void redirectToMain(); + void robotstxt(); +}; + +#endif diff --git a/lib/SvelteUi/json/conf_debug.json b/lib/SvelteUi/json/conf_debug.json new file mode 100644 index 00000000..e9ec284b --- /dev/null +++ b/lib/SvelteUi/json/conf_debug.json @@ -0,0 +1,5 @@ +"d": { + "s": %s, + "t": %s, + "l": %d +}, diff --git a/lib/SvelteUi/json/conf_domoticz.json b/lib/SvelteUi/json/conf_domoticz.json new file mode 100644 index 00000000..3c11d996 --- /dev/null +++ b/lib/SvelteUi/json/conf_domoticz.json @@ -0,0 +1,7 @@ +"o": { + "e" : %d, + "c" : %d, + "u1" : %d, + "u2" : %d, + "u3" : %d +} diff --git a/lib/SvelteUi/json/conf_general.json b/lib/SvelteUi/json/conf_general.json new file mode 100644 index 00000000..b1333cef --- /dev/null +++ b/lib/SvelteUi/json/conf_general.json @@ -0,0 +1,7 @@ +"g": { + "t": "%s", + "h": "%s", + "s": %d, + "u": "%s", + "p": "%s" +}, \ No newline at end of file diff --git a/lib/SvelteUi/json/conf_gpio.json b/lib/SvelteUi/json/conf_gpio.json new file mode 100644 index 00000000..c887d2f3 --- /dev/null +++ b/lib/SvelteUi/json/conf_gpio.json @@ -0,0 +1,28 @@ +"i": { + "h": %s, + "a": %s, + "l": { + "p": %s, + "i": %s + }, + "r": { + "r": %s, + "g": %s, + "b": %s, + "i": %s + }, + "t": { + "d": %s, + "a": %s + }, + "v": { + "p": %s, + "o": %.2f, + "m": %.3f, + "d": { + "v": %d, + "g": %d + }, + "b": %.1f + } +}, diff --git a/lib/SvelteUi/json/conf_meter.json b/lib/SvelteUi/json/conf_meter.json new file mode 100644 index 00000000..b31d753f --- /dev/null +++ b/lib/SvelteUi/json/conf_meter.json @@ -0,0 +1,20 @@ +"m": { + "b": %d, + "p": %d, + "i": %s, + "d": %d, + "f": %d, + "r": %d, + "e": { + "e": %s, + "k": "%s", + "a": "%s" + }, + "m": { + "e": %s, + "w": %.3f, + "v": %.3f, + "a": %.3f, + "c": %.3f + } +}, \ No newline at end of file diff --git a/lib/SvelteUi/json/conf_mqtt.json b/lib/SvelteUi/json/conf_mqtt.json new file mode 100644 index 00000000..c0030dbb --- /dev/null +++ b/lib/SvelteUi/json/conf_mqtt.json @@ -0,0 +1,15 @@ +"q": { + "h": "%s", + "p": %d, + "u": "%s", + "a": "%s", + "c": "%s", + "b": "%s", + "m": %d, + "s": { + "e": %s, + "c": %s, + "r": %s, + "k": %s + } +}, diff --git a/lib/SvelteUi/json/conf_net.json b/lib/SvelteUi/json/conf_net.json new file mode 100644 index 00000000..4eaa1141 --- /dev/null +++ b/lib/SvelteUi/json/conf_net.json @@ -0,0 +1,11 @@ +"n": { + "m": "%s", + "i": "%s", + "s": "%s", + "g": "%s", + "d1": "%s", + "d2": "%s", + "d": %s, + "n1": "%s", + "h": %s +}, diff --git a/lib/SvelteUi/json/conf_price.json b/lib/SvelteUi/json/conf_price.json new file mode 100644 index 00000000..124b4697 --- /dev/null +++ b/lib/SvelteUi/json/conf_price.json @@ -0,0 +1,7 @@ +"p": { + "e": %s, + "t": "%s", + "r": "%s", + "c": "%s", + "m": %.3f +}, diff --git a/lib/SvelteUi/json/conf_thresholds.json b/lib/SvelteUi/json/conf_thresholds.json new file mode 100644 index 00000000..fd911368 --- /dev/null +++ b/lib/SvelteUi/json/conf_thresholds.json @@ -0,0 +1,15 @@ +"t": { + "t": [ + %d, + %d, + %d, + %d, + %d, + %d, + %d, + %d, + %d, + %d + ], + "h": %d +}, \ No newline at end of file diff --git a/lib/SvelteUi/json/conf_ui.json b/lib/SvelteUi/json/conf_ui.json new file mode 100644 index 00000000..97fd05e3 --- /dev/null +++ b/lib/SvelteUi/json/conf_ui.json @@ -0,0 +1,13 @@ +"u": { + "i": %d, + "e": %d, + "v": %d, + "a": %d, + "r": %d, + "c": %d, + "t": %d, + "p": %d, + "d": %d, + "m": %d, + "s": %d +}, \ No newline at end of file diff --git a/lib/SvelteUi/json/conf_wifi.json b/lib/SvelteUi/json/conf_wifi.json new file mode 100644 index 00000000..3433a640 --- /dev/null +++ b/lib/SvelteUi/json/conf_wifi.json @@ -0,0 +1,7 @@ +"w": { + "s": "%s", + "p": "%s", + "w": %.1f, + "z": %d, + "a": %s +}, diff --git a/lib/SvelteUi/json/data.json b/lib/SvelteUi/json/data.json new file mode 100644 index 00000000..00d89d87 --- /dev/null +++ b/lib/SvelteUi/json/data.json @@ -0,0 +1,64 @@ +{ + "im" : %d, + "om" : %d, + "mf" : %d, + "i" : %d, + "e" : %d, + "ri" : %d, + "re" : %d, + "ic" : %.3f, + "ec" : %.3f, + "ric" : %.3f, + "rec" : %.3f, + "u1" : %.2f, + "u2" : %.2f, + "u3" : %.2f, + "i1" : %.2f, + "i2" : %.2f, + "i3" : %.2f, + "f" : %.2f, + "f1" : %.2f, + "f2" : %.2f, + "f3" : %.2f, + "v" : %.3f, + "r" : %d, + "t" : %.2f, + "u" : %lu, + "m" : %lu, + "em" : %d, + "hm" : %d, + "wm" : %d, + "mm" : %d, + "me" : %d, + "p" : %s, + "mt" : %d, + "ds" : %d, + "ea" : { + "x" : %.1f, + "p" : [ %s ], + "t" : %d, + "h" : { + "u" : %.2f, + "c" : %.2f, + "p" : %.2f, + "i" : %.2f + }, + "d" : { + "u" : %.2f, + "c" : %.2f, + "p" : %.2f, + "i" : %.2f + }, + "m" : { + "u" : %.2f, + "c" : %.2f, + "p" : %.2f, + "i" : %.2f + } + }, + "pr" : "%s", + "he" : %d, + "ee" : %d, + "c" : %lu, + "a" : %s +} \ No newline at end of file diff --git a/lib/SvelteUi/json/dayplot.json b/lib/SvelteUi/json/dayplot.json new file mode 100644 index 00000000..ed782610 --- /dev/null +++ b/lib/SvelteUi/json/dayplot.json @@ -0,0 +1,50 @@ +{ + "i00" : %.2f, + "i01" : %.2f, + "i02" : %.2f, + "i03" : %.2f, + "i04" : %.2f, + "i05" : %.2f, + "i06" : %.2f, + "i07" : %.2f, + "i08" : %.2f, + "i09" : %.2f, + "i10" : %.2f, + "i11" : %.2f, + "i12" : %.2f, + "i13" : %.2f, + "i14" : %.2f, + "i15" : %.2f, + "i16" : %.2f, + "i17" : %.2f, + "i18" : %.2f, + "i19" : %.2f, + "i20" : %.2f, + "i21" : %.2f, + "i22" : %.2f, + "i23" : %.2f, + "e00" : %.2f, + "e01" : %.2f, + "e02" : %.2f, + "e03" : %.2f, + "e04" : %.2f, + "e05" : %.2f, + "e06" : %.2f, + "e07" : %.2f, + "e08" : %.2f, + "e09" : %.2f, + "e10" : %.2f, + "e11" : %.2f, + "e12" : %.2f, + "e13" : %.2f, + "e14" : %.2f, + "e15" : %.2f, + "e16" : %.2f, + "e17" : %.2f, + "e18" : %.2f, + "e19" : %.2f, + "e20" : %.2f, + "e21" : %.2f, + "e22" : %.2f, + "e23" : %.2f +} diff --git a/lib/SvelteUi/json/energyprice.json b/lib/SvelteUi/json/energyprice.json new file mode 100644 index 00000000..9231ba9c --- /dev/null +++ b/lib/SvelteUi/json/energyprice.json @@ -0,0 +1,39 @@ +{ + "currency" : "%s", + "00" : %s, + "01" : %s, + "02" : %s, + "03" : %s, + "04" : %s, + "05" : %s, + "06" : %s, + "07" : %s, + "08" : %s, + "09" : %s, + "10" : %s, + "11" : %s, + "12" : %s, + "13" : %s, + "14" : %s, + "15" : %s, + "16" : %s, + "17" : %s, + "18" : %s, + "19" : %s, + "20" : %s, + "21" : %s, + "22" : %s, + "23" : %s, + "24" : %s, + "25" : %s, + "26" : %s, + "27" : %s, + "28" : %s, + "29" : %s, + "30" : %s, + "31" : %s, + "32" : %s, + "33" : %s, + "34" : %s, + "35" : %s +} diff --git a/lib/SvelteUi/json/firmware.html b/lib/SvelteUi/json/firmware.html new file mode 100644 index 00000000..7594c521 --- /dev/null +++ b/lib/SvelteUi/json/firmware.html @@ -0,0 +1,29 @@ + + +
        + Firmware install +
        + File: + +
        + or

        +
        + URL: + +
        +
        +
        + Configuration upload +
        + File: + +
        +
        +
        + Factory reset +
        + +
        +
        + + \ No newline at end of file diff --git a/lib/SvelteUi/json/monthplot.json b/lib/SvelteUi/json/monthplot.json new file mode 100644 index 00000000..e4271129 --- /dev/null +++ b/lib/SvelteUi/json/monthplot.json @@ -0,0 +1,64 @@ +{ + "i01" : %.2f, + "i02" : %.2f, + "i03" : %.2f, + "i04" : %.2f, + "i05" : %.2f, + "i06" : %.2f, + "i07" : %.2f, + "i08" : %.2f, + "i09" : %.2f, + "i10" : %.2f, + "i11" : %.2f, + "i12" : %.2f, + "i13" : %.2f, + "i14" : %.2f, + "i15" : %.2f, + "i16" : %.2f, + "i17" : %.2f, + "i18" : %.2f, + "i19" : %.2f, + "i20" : %.2f, + "i21" : %.2f, + "i22" : %.2f, + "i23" : %.2f, + "i24" : %.2f, + "i25" : %.2f, + "i26" : %.2f, + "i27" : %.2f, + "i28" : %.2f, + "i29" : %.2f, + "i30" : %.2f, + "i31" : %.2f, + "e01" : %.2f, + "e02" : %.2f, + "e03" : %.2f, + "e04" : %.2f, + "e05" : %.2f, + "e06" : %.2f, + "e07" : %.2f, + "e08" : %.2f, + "e09" : %.2f, + "e10" : %.2f, + "e11" : %.2f, + "e12" : %.2f, + "e13" : %.2f, + "e14" : %.2f, + "e15" : %.2f, + "e16" : %.2f, + "e17" : %.2f, + "e18" : %.2f, + "e19" : %.2f, + "e20" : %.2f, + "e21" : %.2f, + "e22" : %.2f, + "e23" : %.2f, + "e24" : %.2f, + "e25" : %.2f, + "e26" : %.2f, + "e27" : %.2f, + "e28" : %.2f, + "e29" : %.2f, + "e30" : %.2f, + "e31" : %.2f +} diff --git a/lib/SvelteUi/json/peak.json b/lib/SvelteUi/json/peak.json new file mode 100644 index 00000000..7f58d14c --- /dev/null +++ b/lib/SvelteUi/json/peak.json @@ -0,0 +1,4 @@ +{ + "d": %d, + "v": %.2f +} \ No newline at end of file diff --git a/lib/SvelteUi/json/response.json b/lib/SvelteUi/json/response.json new file mode 100644 index 00000000..7a5c6ee3 --- /dev/null +++ b/lib/SvelteUi/json/response.json @@ -0,0 +1,5 @@ +{ + "success": %s, + "message": "%s", + "reboot": %s +} \ No newline at end of file diff --git a/lib/SvelteUi/json/sysinfo.json b/lib/SvelteUi/json/sysinfo.json new file mode 100644 index 00000000..f22661ee --- /dev/null +++ b/lib/SvelteUi/json/sysinfo.json @@ -0,0 +1,40 @@ +{ + "version": "%s", + "chip": "%s", + "chipId": "%s", + "mac": "%s", + "apmac": "%s", + "board": %d, + "vndcfg": %s, + "usrcfg": %s, + "fwconsent": %d, + "hostname": "%s", + "booting": %s, + "upgrading": %s, + "net": { + "ip": "%s", + "mask": "%s", + "gw": "%s", + "dns1": "%s", + "dns2": "%s" + }, + "meter": { + "mfg": %d, + "model": "%s", + "id": "%s" + }, + "ui": { + "i": %d, + "e": %d, + "v": %d, + "a": %d, + "r": %d, + "c": %d, + "t": %d, + "p": %d, + "d": %d, + "m": %d, + "s": %d + }, + "security": %d +} \ No newline at end of file diff --git a/lib/SvelteUi/json/tariff.json b/lib/SvelteUi/json/tariff.json new file mode 100644 index 00000000..e832ff11 --- /dev/null +++ b/lib/SvelteUi/json/tariff.json @@ -0,0 +1,17 @@ +{ + "t": [ + %d, + %d, + %d, + %d, + %d, + %d, + %d, + %d, + %d, + %d + ], + "p": [ %s ], + "c": %d, + "m": %.2f +} \ No newline at end of file diff --git a/lib/SvelteUi/json/tempsensor.json b/lib/SvelteUi/json/tempsensor.json new file mode 100644 index 00000000..573b756c --- /dev/null +++ b/lib/SvelteUi/json/tempsensor.json @@ -0,0 +1,7 @@ +{ + "i" : %d, + "a" : "%s", + "n" : "%s", + "c" : %d, + "v" : %.1f +}, \ No newline at end of file diff --git a/lib/SvelteUi/scripts/generate_includes.py b/lib/SvelteUi/scripts/generate_includes.py new file mode 100644 index 00000000..6bbd8e75 --- /dev/null +++ b/lib/SvelteUi/scripts/generate_includes.py @@ -0,0 +1,82 @@ +import os +import re +import shutil +import subprocess + +try: + from css_html_js_minify import html_minify, js_minify, css_minify +except: + from SCons.Script import ( + ARGUMENTS, + COMMAND_LINE_TARGETS, + DefaultEnvironment, + ) + env = DefaultEnvironment() + + env.Execute( + env.VerboseAction( + '$PYTHONEXE -m pip install "css_html_js_minify" ', + "Installing Python dependencies", + ) + ) + try: + from css_html_js_minify import html_minify, js_minify, css_minify + except: + print("WARN: Unable to load minifier") + + +srcroot = "lib/SvelteUi/include/html" + +version = os.environ.get('GITHUB_TAG') +if version == None: + try: + result = subprocess.run(['git','rev-parse','--short','HEAD'], capture_output=True, check=False) + if result.returncode == 0: + version = result.stdout.decode('utf-8').strip() + else: + version = "SNAPSHOT" + except: + version = "SNAPSHOT" + +if os.path.exists(srcroot): + shutil.rmtree(srcroot) + os.mkdir(srcroot) +else: + os.mkdir(srcroot) + +for webroot in ["lib/SvelteUi/app/dist", "lib/SvelteUi/json"]: + for filename in os.listdir(webroot): + basename = re.sub("[^0-9a-zA-Z]+", "_", filename) + + srcfile = webroot + "/" + filename + dstfile = srcroot + "/" + basename + ".h" + + varname = basename.upper() + + with open(srcfile, encoding="utf-8") as f: + content = f.read() + content = content.replace("index.js", "index-"+version+".js") + content = content.replace("index.css", "index-"+version+".css") + + try: + if filename.endswith(".html"): + content = html_minify(content) + elif filename.endswith(".css"): + content = css_minify(content) + elif filename.endswith(".json"): + content = js_minify(content) + except: + print("WARN: Unable to minify") + + with open(dstfile, "w") as dst: + dst.write("static const char ") + dst.write(varname) + dst.write("[] PROGMEM = R\"==\"==(") + dst.write(content) + dst.write(")==\"==\";\n") + dst.write("const int "); + dst.write(varname) + dst.write("_LEN PROGMEM = "); + dst.write(str(len(content))) + dst.write(";"); + \ No newline at end of file diff --git a/lib/SvelteUi/src/AmsWebServer.cpp b/lib/SvelteUi/src/AmsWebServer.cpp new file mode 100644 index 00000000..42c6eea3 --- /dev/null +++ b/lib/SvelteUi/src/AmsWebServer.cpp @@ -0,0 +1,2152 @@ +#include "AmsWebServer.h" +#include "AmsWebHeaders.h" +#include "base64.h" +#include "hexutils.h" + +#include "html/index_html.h" +#include "html/index_css.h" +#include "html/index_js.h" +#include "html/github_svg.h" +#include "html/favicon_svg.h" +#include "html/data_json.h" +#include "html/dayplot_json.h" +#include "html/monthplot_json.h" +#include "html/energyprice_json.h" +#include "html/tempsensor_json.h" +#include "html/response_json.h" +#include "html/sysinfo_json.h" +#include "html/tariff_json.h" +#include "html/peak_json.h" +#include "html/conf_general_json.h" +#include "html/conf_meter_json.h" +#include "html/conf_wifi_json.h" +#include "html/conf_net_json.h" +#include "html/conf_mqtt_json.h" +#include "html/conf_price_json.h" +#include "html/conf_thresholds_json.h" +#include "html/conf_debug_json.h" +#include "html/conf_gpio_json.h" +#include "html/conf_domoticz_json.h" +#include "html/conf_ui_json.h" +#include "html/firmware_html.h" + +#include "version.h" + +#if defined(ESP32) +#include +#include +#endif + + +AmsWebServer::AmsWebServer(uint8_t* buf, RemoteDebug* Debug, HwTools* hw) { + this->debugger = Debug; + this->hw = hw; + this->buf = (char*) buf; +} + +void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, MeterConfig* meterConfig, AmsData* meterState, AmsDataStorage* ds, EnergyAccounting* ea) { + this->config = config; + this->gpioConfig = gpioConfig; + this->meterConfig = meterConfig; + this->meterState = meterState; + this->ds = ds; + this->ea = ea; + + server.on(F("/"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this)); + snprintf_P(buf, 32, PSTR("/index-%s.js"), VERSION); + server.on(buf, HTTP_GET, std::bind(&AmsWebServer::indexJs, this)); + snprintf_P(buf, 32, PSTR("/index-%s.css"), VERSION); + server.on(buf, HTTP_GET, std::bind(&AmsWebServer::indexCss, this)); + + server.on(F("/configuration"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this)); + server.on(F("/status"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this)); + server.on(F("/consent"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this)); + server.on(F("/vendor"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this)); + server.on(F("/setup"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this)); + server.on(F("/mqtt-ca"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this)); + server.on(F("/mqtt-cert"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this)); + server.on(F("/mqtt-key"), HTTP_GET, std::bind(&AmsWebServer::indexHtml, this)); + + server.on(F("/github.svg"), HTTP_GET, std::bind(&AmsWebServer::githubSvg, this)); + server.on(F("/favicon.svg"), HTTP_GET, std::bind(&AmsWebServer::faviconSvg, this)); + server.on(F("/sysinfo.json"), HTTP_GET, std::bind(&AmsWebServer::sysinfoJson, this)); + server.on(F("/data.json"), HTTP_GET, std::bind(&AmsWebServer::dataJson, this)); + server.on(F("/dayplot.json"), HTTP_GET, std::bind(&AmsWebServer::dayplotJson, this)); + server.on(F("/monthplot.json"), HTTP_GET, std::bind(&AmsWebServer::monthplotJson, this)); + server.on(F("/energyprice.json"), HTTP_GET, std::bind(&AmsWebServer::energyPriceJson, this)); + server.on(F("/temperature.json"), HTTP_GET, std::bind(&AmsWebServer::temperatureJson, this)); + server.on(F("/tariff.json"), HTTP_GET, std::bind(&AmsWebServer::tariffJson, this)); + + server.on(F("/configuration.json"), HTTP_GET, std::bind(&AmsWebServer::configurationJson, this)); + server.on(F("/save"), HTTP_POST, std::bind(&AmsWebServer::handleSave, this)); + server.on(F("/reboot"), HTTP_POST, std::bind(&AmsWebServer::reboot, this)); + server.on(F("/upgrade"), HTTP_POST, std::bind(&AmsWebServer::upgrade, this)); + server.on(F("/firmware"), HTTP_GET, std::bind(&AmsWebServer::firmwareHtml, this)); + server.on(F("/firmware"), HTTP_POST, std::bind(&AmsWebServer::firmwarePost, this), std::bind(&AmsWebServer::firmwareUpload, this)); + server.on(F("/is-alive"), HTTP_GET, std::bind(&AmsWebServer::isAliveCheck, this)); + + server.on(F("/reset"), HTTP_POST, std::bind(&AmsWebServer::factoryResetPost, this)); + + server.on(F("/robots.txt"), HTTP_GET, std::bind(&AmsWebServer::robotstxt, this)); + + server.on(F("/mqtt-ca"), HTTP_POST, std::bind(&AmsWebServer::firmwarePost, this), std::bind(&AmsWebServer::mqttCaUpload, this)); + server.on(F("/mqtt-cert"), HTTP_POST, std::bind(&AmsWebServer::firmwarePost, this), std::bind(&AmsWebServer::mqttCertUpload, this)); + server.on(F("/mqtt-key"), HTTP_POST, std::bind(&AmsWebServer::firmwarePost, this), std::bind(&AmsWebServer::mqttKeyUpload, this)); + + server.on(F("/configfile"), HTTP_POST, std::bind(&AmsWebServer::firmwarePost, this), std::bind(&AmsWebServer::configFileUpload, this)); + server.on(F("/configfile.cfg"), HTTP_GET, std::bind(&AmsWebServer::configFileDownload, this)); + + /* These trigger captive portal. Only problem is that after you have "signed in", the portal is closed and the user has no idea how to reach the device + server.on(F("/generate_204"), HTTP_GET, std::bind(&AmsWebServer::redirectToMain, this)); // Android captive portal check: http://connectivitycheck.gstatic.com/generate_204 + server.on(F("/ncsi.txt"), HTTP_GET, std::bind(&AmsWebServer::redirectToMain, this)); // Microsoft connectivity check: http://www.msftncsi.com/ncsi.txt + server.on(F("/fwlink"), HTTP_GET, std::bind(&AmsWebServer::redirectToMain, this)); // Microsoft connectivity check + server.on(F("/library/test/success.html"), HTTP_GET, std::bind(&AmsWebServer::redirectToMain, this)); // Apple connectivity check: http://www.apple.com/library/test/success.html + */ + + server.onNotFound(std::bind(&AmsWebServer::notFound, this)); + + server.begin(); // Web server start + + config->getWebConfig(webConfig); + MqttConfig mqttConfig; + config->getMqttConfig(mqttConfig); + mqttEnabled = strlen(mqttConfig.host) > 0; +} + + +void AmsWebServer::setMqtt(MQTTClient* mqtt) { + this->mqtt = mqtt; +} + +void AmsWebServer::setTimezone(Timezone* tz) { + this->tz = tz; +} + +void AmsWebServer::setMqttEnabled(bool enabled) { + mqttEnabled = enabled; +} + +void AmsWebServer::setEntsoeApi(EntsoeApi* eapi) { + this->eapi = eapi; +} + +void AmsWebServer::loop() { + server.handleClient(); + + if(maxPwr == 0 && meterState->getListType() > 1 && meterConfig->mainFuse > 0 && meterConfig->distributionSystem > 0) { + int voltage = meterConfig->distributionSystem == 2 ? 400 : 230; + if(meterState->isThreePhase()) { + maxPwr = meterConfig->mainFuse * sqrt(3) * voltage; + } else if(meterState->isTwoPhase()) { + maxPwr = meterConfig->mainFuse * voltage; + } else { + maxPwr = meterConfig->mainFuse * 230; + } + } +} + +bool AmsWebServer::checkSecurity(byte level, bool send401) { + bool access = WiFi.getMode() == WIFI_AP || webConfig.security < level; + if(!access && webConfig.security >= level && server.hasHeader("Authorization")) { + String expectedAuth = String(webConfig.username) + ":" + String(webConfig.password); + + String providedPwd = server.header("Authorization"); + providedPwd.replace("Basic ", ""); + + #if defined(ESP8266) + String expectedBase64 = base64::encode(expectedAuth, false); + #elif defined(ESP32) + String expectedBase64 = base64::encode(expectedAuth); + #endif + + debugger->printf("Expected auth: %s\n", expectedBase64.c_str()); + debugger->printf("Provided auth: %s\n", providedPwd.c_str()); + + access = providedPwd.equals(expectedBase64); + } + + if(!access && send401) { + server.sendHeader(HEADER_AUTHENTICATE, AUTHENTICATE_BASIC); + server.setContentLength(0); + server.send(401, MIME_HTML, ""); + } + return access; +} + +void AmsWebServer::notFound() { + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + server.send_P(404, MIME_HTML, PSTR("Not found")); +} + +void AmsWebServer::githubSvg() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /github.svg over http...\n"); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_1HR); + server.send_P(200, "image/svg+xml", GITHUB_SVG); +} + +void AmsWebServer::faviconSvg() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /favicon.ico over http...\n"); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_1HR); + server.send_P(200, "image/svg+xml", FAVICON_SVG); +} + +void AmsWebServer::sysinfoJson() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /sysinfo.json over http...\n"); + + SystemConfig sys; + config->getSystemConfig(sys); + + uint32_t chipId; + #if defined(ESP32) + chipId = ( ESP.getEfuseMac() >> 32 ) % 0xFFFFFFFF; + #else + chipId = ESP.getChipId(); + #endif + String chipIdStr = String(chipId, HEX); + + String hostname; + if(sys.userConfigured) { + WiFiConfig wifiConfig; + config->getWiFiConfig(wifiConfig); + hostname = String(wifiConfig.hostname); + } else { + hostname = "ams-"+chipIdStr; + } + + IPAddress dns1 = WiFi.dnsIP(0); + IPAddress dns2 = WiFi.dnsIP(1); + + char macStr[18] = { 0 }; + char apMacStr[18] = { 0 }; + + uint8_t mac[6]; + uint8_t apmac[6]; + + #if defined(ESP8266) + wifi_get_macaddr(STATION_IF, mac); + wifi_get_macaddr(SOFTAP_IF, apmac); + #elif defined(ESP32) + esp_wifi_get_mac((wifi_interface_t)ESP_IF_WIFI_STA, mac); + esp_wifi_get_mac((wifi_interface_t)ESP_IF_WIFI_AP, apmac); + #endif + + sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + sprintf(apMacStr, "%02X:%02X:%02X:%02X:%02X:%02X", apmac[0], apmac[1], apmac[2], apmac[3], apmac[4], apmac[5]); + + UiConfig ui; + config->getUiConfig(ui); + + int size = snprintf_P(buf, BufferSize, SYSINFO_JSON, + VERSION, + #if defined(CONFIG_IDF_TARGET_ESP32S2) + "esp32s2", + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + "esp32c3", + #elif defined(ESP32) + "esp32", + #elif defined(ESP8266) + "esp8266", + #endif + chipIdStr.c_str(), + macStr, + apMacStr, + sys.boardType, + sys.vendorConfigured ? "true" : "false", + sys.userConfigured ? "true" : "false", + sys.dataCollectionConsent, + hostname.c_str(), + performRestart ? "true" : "false", + rebootForUpgrade ? "true" : "false", + WiFi.localIP().toString().c_str(), + WiFi.subnetMask().toString().c_str(), + WiFi.gatewayIP().toString().c_str(), + #if defined(ESP8266) + dns1.isSet() ? dns1.toString().c_str() : "", + dns2.isSet() ? dns2.toString().c_str() : "", + #else + dns1.toString().c_str(), + dns2.toString().c_str(), + #endif + meterState->getMeterType(), + meterState->getMeterModel().c_str(), + meterState->getMeterId().c_str(), + ui.showImport, + ui.showExport, + ui.showVoltage, + ui.showAmperage, + ui.showReactive, + ui.showRealtime, + ui.showPeaks, + ui.showPricePlot, + ui.showDayPlot, + ui.showMonthPlot, + ui.showTemperaturePlot, + webConfig.security + ); + + stripNonAscii((uint8_t*) buf, size+1); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); + + server.handleClient(); + delay(250); + + if(performRestart || rebootForUpgrade) { + if(ds != NULL) { + ds->save(); + } + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(PSTR("Rebooting")); + delay(1000); + #if defined(ESP8266) + ESP.reset(); + #elif defined(ESP32) + ESP.restart(); + #endif + performRestart = false; + } +} + +void AmsWebServer::dataJson() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /data.json over http...\n"); + uint64_t millis = millis64(); + + if(!checkSecurity(2, true)) + return; + + float vcc = hw->getVcc(); + int rssi = hw->getWifiRssi(); + + uint8_t espStatus; + #if defined(ESP8266) + if(vcc < 2.0) { // Voltage not correct, ESP would not run on this voltage + espStatus = 1; + } else if(vcc > 3.1 && vcc < 3.5) { + espStatus = 1; + } else if(vcc > 3.0 && vcc < 3.6) { + espStatus = 2; + } else { + espStatus = 3; + } + #elif defined(ESP32) + if(vcc < 2.0) { // Voltage not correct, ESP would not run on this voltage + espStatus = 1; + } else if(vcc > 2.8 && vcc < 3.5) { + espStatus = 1; + } else if(vcc > 2.7 && vcc < 3.6) { + espStatus = 2; + } else { + espStatus = 3; + } + #endif + + + uint8_t hanStatus; + if(meterState->getLastError() != 0) { + hanStatus = 3; + } else if((meterConfig->baud == 0 || meterState->getLastUpdateMillis() == 0) && millis < 30000) { + hanStatus = 0; + } else if(millis - meterState->getLastUpdateMillis() < 15000) { + hanStatus = 1; + } else if(millis - meterState->getLastUpdateMillis() < 30000) { + hanStatus = 2; + } else { + hanStatus = 3; + } + + uint8_t wifiStatus; + if(rssi > -75) { + wifiStatus = 1; + } else if(rssi > -95) { + wifiStatus = 2; + } else { + wifiStatus = 3; + } + + uint8_t mqttStatus; + if(!mqttEnabled) { + mqttStatus = 0; + } else if(mqtt != NULL && mqtt->connected()) { + mqttStatus = 1; + } else if(mqtt != NULL && mqtt->lastError() == 0) { + mqttStatus = 2; + } else { + mqttStatus = 3; + } + + float price = ENTSOE_NO_VALUE; + if(eapi != NULL) + price = eapi->getValueForHour(0); + + String peaks = ""; + for(uint8_t i = 1; i <= ea->getConfig()->hours; i++) { + if(!peaks.isEmpty()) peaks += ","; + peaks += String(ea->getPeak(i).value / 100.0); + } + + time_t now = time(nullptr); + + snprintf_P(buf, BufferSize, DATA_JSON, + maxPwr == 0 ? meterState->isThreePhase() ? 20000 : 10000 : maxPwr, + meterConfig->productionCapacity, + meterConfig->mainFuse == 0 ? 32 : meterConfig->mainFuse, + meterState->getActiveImportPower(), + meterState->getActiveExportPower(), + meterState->getReactiveImportPower(), + meterState->getReactiveExportPower(), + meterState->getActiveImportCounter(), + meterState->getActiveExportCounter(), + meterState->getReactiveImportCounter(), + meterState->getReactiveExportCounter(), + meterState->getL1Voltage(), + meterState->getL2Voltage(), + meterState->getL3Voltage(), + meterState->getL1Current(), + meterState->getL2Current(), + meterState->getL3Current(), + meterState->getPowerFactor(), + meterState->getL1PowerFactor(), + meterState->getL2PowerFactor(), + meterState->getL3PowerFactor(), + vcc, + rssi, + hw->getTemperature(), + (uint32_t) (millis / 1000), + ESP.getFreeHeap(), + espStatus, + hanStatus, + wifiStatus, + mqttStatus, + mqtt == NULL ? 0 : (int) mqtt->lastError(), + price == ENTSOE_NO_VALUE ? "null" : String(price, 2).c_str(), + meterState->getMeterType(), + meterConfig->distributionSystem, + ea->getMonthMax(), + peaks.c_str(), + ea->getCurrentThreshold(), + ea->getUseThisHour(), + ea->getCostThisHour(), + ea->getProducedThisHour(), + ea->getIncomeThisHour(), + ea->getUseToday(), + ea->getCostToday(), + ea->getProducedToday(), + ea->getIncomeToday(), + ea->getUseThisMonth(), + ea->getCostThisMonth(), + ea->getProducedThisMonth(), + ea->getIncomeThisMonth(), + priceRegion.c_str(), + meterState->getLastError(), + eapi == NULL ? 0 : eapi->getLastError(), + (uint32_t) now, + checkSecurity(1, false) ? "true" : "false" + ); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); +} + +void AmsWebServer::dayplotJson() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /dayplot.json over http...\n"); + + if(!checkSecurity(2)) + return; + + if(ds == NULL) { + notFound(); + } else { + snprintf_P(buf, BufferSize, DAYPLOT_JSON, + ds->getHourImport(0) / 1000.0, + ds->getHourImport(1) / 1000.0, + ds->getHourImport(2) / 1000.0, + ds->getHourImport(3) / 1000.0, + ds->getHourImport(4) / 1000.0, + ds->getHourImport(5) / 1000.0, + ds->getHourImport(6) / 1000.0, + ds->getHourImport(7) / 1000.0, + ds->getHourImport(8) / 1000.0, + ds->getHourImport(9) / 1000.0, + ds->getHourImport(10) / 1000.0, + ds->getHourImport(11) / 1000.0, + ds->getHourImport(12) / 1000.0, + ds->getHourImport(13) / 1000.0, + ds->getHourImport(14) / 1000.0, + ds->getHourImport(15) / 1000.0, + ds->getHourImport(16) / 1000.0, + ds->getHourImport(17) / 1000.0, + ds->getHourImport(18) / 1000.0, + ds->getHourImport(19) / 1000.0, + ds->getHourImport(20) / 1000.0, + ds->getHourImport(21) / 1000.0, + ds->getHourImport(22) / 1000.0, + ds->getHourImport(23) / 1000.0, + ds->getHourExport(0) / 1000.0, + ds->getHourExport(1) / 1000.0, + ds->getHourExport(2) / 1000.0, + ds->getHourExport(3) / 1000.0, + ds->getHourExport(4) / 1000.0, + ds->getHourExport(5) / 1000.0, + ds->getHourExport(6) / 1000.0, + ds->getHourExport(7) / 1000.0, + ds->getHourExport(8) / 1000.0, + ds->getHourExport(9) / 1000.0, + ds->getHourExport(10) / 1000.0, + ds->getHourExport(11) / 1000.0, + ds->getHourExport(12) / 1000.0, + ds->getHourExport(13) / 1000.0, + ds->getHourExport(14) / 1000.0, + ds->getHourExport(15) / 1000.0, + ds->getHourExport(16) / 1000.0, + ds->getHourExport(17) / 1000.0, + ds->getHourExport(18) / 1000.0, + ds->getHourExport(19) / 1000.0, + ds->getHourExport(20) / 1000.0, + ds->getHourExport(21) / 1000.0, + ds->getHourExport(22) / 1000.0, + ds->getHourExport(23) / 1000.0 + ); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); + } +} + +void AmsWebServer::monthplotJson() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /monthplot.json over http...\n"); + + if(!checkSecurity(2)) + return; + + if(ds == NULL) { + notFound(); + } else { + snprintf_P(buf, BufferSize, MONTHPLOT_JSON, + ds->getDayImport(1) / 1000.0, + ds->getDayImport(2) / 1000.0, + ds->getDayImport(3) / 1000.0, + ds->getDayImport(4) / 1000.0, + ds->getDayImport(5) / 1000.0, + ds->getDayImport(6) / 1000.0, + ds->getDayImport(7) / 1000.0, + ds->getDayImport(8) / 1000.0, + ds->getDayImport(9) / 1000.0, + ds->getDayImport(10) / 1000.0, + ds->getDayImport(11) / 1000.0, + ds->getDayImport(12) / 1000.0, + ds->getDayImport(13) / 1000.0, + ds->getDayImport(14) / 1000.0, + ds->getDayImport(15) / 1000.0, + ds->getDayImport(16) / 1000.0, + ds->getDayImport(17) / 1000.0, + ds->getDayImport(18) / 1000.0, + ds->getDayImport(19) / 1000.0, + ds->getDayImport(20) / 1000.0, + ds->getDayImport(21) / 1000.0, + ds->getDayImport(22) / 1000.0, + ds->getDayImport(23) / 1000.0, + ds->getDayImport(24) / 1000.0, + ds->getDayImport(25) / 1000.0, + ds->getDayImport(26) / 1000.0, + ds->getDayImport(27) / 1000.0, + ds->getDayImport(28) / 1000.0, + ds->getDayImport(29) / 1000.0, + ds->getDayImport(30) / 1000.0, + ds->getDayImport(31) / 1000.0, + ds->getDayExport(1) / 1000.0, + ds->getDayExport(2) / 1000.0, + ds->getDayExport(3) / 1000.0, + ds->getDayExport(4) / 1000.0, + ds->getDayExport(5) / 1000.0, + ds->getDayExport(6) / 1000.0, + ds->getDayExport(7) / 1000.0, + ds->getDayExport(8) / 1000.0, + ds->getDayExport(9) / 1000.0, + ds->getDayExport(10) / 1000.0, + ds->getDayExport(11) / 1000.0, + ds->getDayExport(12) / 1000.0, + ds->getDayExport(13) / 1000.0, + ds->getDayExport(14) / 1000.0, + ds->getDayExport(15) / 1000.0, + ds->getDayExport(16) / 1000.0, + ds->getDayExport(17) / 1000.0, + ds->getDayExport(18) / 1000.0, + ds->getDayExport(19) / 1000.0, + ds->getDayExport(20) / 1000.0, + ds->getDayExport(21) / 1000.0, + ds->getDayExport(22) / 1000.0, + ds->getDayExport(23) / 1000.0, + ds->getDayExport(24) / 1000.0, + ds->getDayExport(25) / 1000.0, + ds->getDayExport(26) / 1000.0, + ds->getDayExport(27) / 1000.0, + ds->getDayExport(28) / 1000.0, + ds->getDayExport(29) / 1000.0, + ds->getDayExport(30) / 1000.0, + ds->getDayExport(31) / 1000.0 + ); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); + } +} + +void AmsWebServer::energyPriceJson() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /energyprice.json over http...\n"); + + if(!checkSecurity(2)) + return; + + float prices[36]; + for(int i = 0; i < 36; i++) { + prices[i] = eapi == NULL ? ENTSOE_NO_VALUE : eapi->getValueForHour(i); + } + + snprintf_P(buf, BufferSize, ENERGYPRICE_JSON, + eapi == NULL ? "" : eapi->getCurrency(), + prices[0] == ENTSOE_NO_VALUE ? "null" : String(prices[0], 4).c_str(), + prices[1] == ENTSOE_NO_VALUE ? "null" : String(prices[1], 4).c_str(), + prices[2] == ENTSOE_NO_VALUE ? "null" : String(prices[2], 4).c_str(), + prices[3] == ENTSOE_NO_VALUE ? "null" : String(prices[3], 4).c_str(), + prices[4] == ENTSOE_NO_VALUE ? "null" : String(prices[4], 4).c_str(), + prices[5] == ENTSOE_NO_VALUE ? "null" : String(prices[5], 4).c_str(), + prices[6] == ENTSOE_NO_VALUE ? "null" : String(prices[6], 4).c_str(), + prices[7] == ENTSOE_NO_VALUE ? "null" : String(prices[7], 4).c_str(), + prices[8] == ENTSOE_NO_VALUE ? "null" : String(prices[8], 4).c_str(), + prices[9] == ENTSOE_NO_VALUE ? "null" : String(prices[9], 4).c_str(), + prices[10] == ENTSOE_NO_VALUE ? "null" : String(prices[10], 4).c_str(), + prices[11] == ENTSOE_NO_VALUE ? "null" : String(prices[11], 4).c_str(), + prices[12] == ENTSOE_NO_VALUE ? "null" : String(prices[12], 4).c_str(), + prices[13] == ENTSOE_NO_VALUE ? "null" : String(prices[13], 4).c_str(), + prices[14] == ENTSOE_NO_VALUE ? "null" : String(prices[14], 4).c_str(), + prices[15] == ENTSOE_NO_VALUE ? "null" : String(prices[15], 4).c_str(), + prices[16] == ENTSOE_NO_VALUE ? "null" : String(prices[16], 4).c_str(), + prices[17] == ENTSOE_NO_VALUE ? "null" : String(prices[17], 4).c_str(), + prices[18] == ENTSOE_NO_VALUE ? "null" : String(prices[18], 4).c_str(), + prices[19] == ENTSOE_NO_VALUE ? "null" : String(prices[19], 4).c_str(), + prices[20] == ENTSOE_NO_VALUE ? "null" : String(prices[20], 4).c_str(), + prices[21] == ENTSOE_NO_VALUE ? "null" : String(prices[21], 4).c_str(), + prices[22] == ENTSOE_NO_VALUE ? "null" : String(prices[22], 4).c_str(), + prices[23] == ENTSOE_NO_VALUE ? "null" : String(prices[23], 4).c_str(), + prices[24] == ENTSOE_NO_VALUE ? "null" : String(prices[24], 4).c_str(), + prices[25] == ENTSOE_NO_VALUE ? "null" : String(prices[25], 4).c_str(), + prices[26] == ENTSOE_NO_VALUE ? "null" : String(prices[26], 4).c_str(), + prices[27] == ENTSOE_NO_VALUE ? "null" : String(prices[27], 4).c_str(), + prices[28] == ENTSOE_NO_VALUE ? "null" : String(prices[28], 4).c_str(), + prices[29] == ENTSOE_NO_VALUE ? "null" : String(prices[29], 4).c_str(), + prices[30] == ENTSOE_NO_VALUE ? "null" : String(prices[30], 4).c_str(), + prices[31] == ENTSOE_NO_VALUE ? "null" : String(prices[31], 4).c_str(), + prices[32] == ENTSOE_NO_VALUE ? "null" : String(prices[32], 4).c_str(), + prices[33] == ENTSOE_NO_VALUE ? "null" : String(prices[33], 4).c_str(), + prices[34] == ENTSOE_NO_VALUE ? "null" : String(prices[34], 4).c_str(), + prices[35] == ENTSOE_NO_VALUE ? "null" : String(prices[35], 4).c_str() + ); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); +} + +void AmsWebServer::temperatureJson() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /temperature.json over http...\n"); + + if(!checkSecurity(2)) + return; + + int count = hw->getTempSensorCount(); + snprintf(buf, 16, "{\"c\":%d,\"s\":[", count); + + for(int i = 0; i < count; i++) { + TempSensorData* data = hw->getTempSensorData(i); + if(data == NULL) continue; + + TempSensorConfig* conf = config->getTempSensorConfig(data->address); + char* pos = buf+strlen(buf); + snprintf_P(pos, 72, TEMPSENSOR_JSON, + i, + toHex(data->address, 8).c_str(), + conf == NULL ? "" : String(conf->name).substring(0,16).c_str(), + conf == NULL || conf->common ? 1 : 0, + data->lastRead + ); + delay(10); + } + char* pos = buf+strlen(buf); + snprintf(count == 0 ? pos : pos-1, 8, "]}"); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); +} + +void AmsWebServer::indexHtml() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /index.html over http...\n"); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + if(!checkSecurity(2)) + return; + server.setContentLength(INDEX_HTML_LEN); + server.send_P(200, MIME_HTML, INDEX_HTML); +} + +void AmsWebServer::indexCss() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /index.css over http...\n"); + + if(!checkSecurity(2)) + return; + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_1HR); + server.setContentLength(INDEX_CSS_LEN); + server.send_P(200, MIME_CSS, INDEX_CSS); +} + +void AmsWebServer::indexJs() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /index.js over http...\n"); + + if(!checkSecurity(2)) + return; + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_1HR); + server.setContentLength(INDEX_JS_LEN); + server.send_P(200, MIME_JS, INDEX_JS); +} + +void AmsWebServer::configurationJson() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /configuration.json over http...\n"); + + if(!checkSecurity(1)) + return; + + NtpConfig ntpConfig; + config->getNtpConfig(ntpConfig); + WiFiConfig wifiConfig; + config->getWiFiConfig(wifiConfig); + + bool encen = false; + for(uint8_t i = 0; i < 16; i++) { + if(meterConfig->encryptionKey[i] > 0) { + encen = true; + } + } + + EnergyAccountingConfig* eac = ea->getConfig(); + MqttConfig mqttConfig; + config->getMqttConfig(mqttConfig); + + EntsoeConfig entsoe; + config->getEntsoeConfig(entsoe); + DebugConfig debugConfig; + config->getDebugConfig(debugConfig); + DomoticzConfig domo; + config->getDomoticzConfig(domo); + UiConfig ui; + config->getUiConfig(ui); + + bool qsc = false; + bool qsr = false; + bool qsk = false; + + if(LittleFS.begin()) { + qsc = LittleFS.exists(FILE_MQTT_CA); + qsr = LittleFS.exists(FILE_MQTT_CERT); + qsk = LittleFS.exists(FILE_MQTT_KEY); + LittleFS.end(); + } + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.send_P(200, MIME_JSON, PSTR("{\"version\":\"")); + server.sendContent_P(VERSION); + server.sendContent_P(PSTR("\",")); + snprintf_P(buf, BufferSize, CONF_GENERAL_JSON, + ntpConfig.timezone, + wifiConfig.hostname, + webConfig.security, + webConfig.username, + strlen(webConfig.password) > 0 ? "***" : "" + ); + server.sendContent(buf); + snprintf_P(buf, BufferSize, CONF_METER_JSON, + meterConfig->baud, + meterConfig->parity, + meterConfig->invert ? "true" : "false", + meterConfig->distributionSystem, + meterConfig->mainFuse, + meterConfig->productionCapacity, + encen ? "true" : "false", + toHex(meterConfig->encryptionKey, 16).c_str(), + toHex(meterConfig->authenticationKey, 16).c_str(), + meterConfig->wattageMultiplier > 1 || meterConfig->voltageMultiplier > 1 || meterConfig->amperageMultiplier > 1 || meterConfig->accumulatedMultiplier > 1 ? "true" : "false", + meterConfig->wattageMultiplier / 1000.0, + meterConfig->voltageMultiplier / 1000.0, + meterConfig->amperageMultiplier / 1000.0, + meterConfig->accumulatedMultiplier / 1000.0 + ); + server.sendContent(buf); + + snprintf_P(buf, BufferSize, CONF_THRESHOLDS_JSON, + eac->thresholds[0], + eac->thresholds[1], + eac->thresholds[2], + eac->thresholds[3], + eac->thresholds[4], + eac->thresholds[5], + eac->thresholds[6], + eac->thresholds[7], + eac->thresholds[8], + eac->thresholds[9], + eac->hours + ); + server.sendContent(buf); + snprintf_P(buf, BufferSize, CONF_WIFI_JSON, + wifiConfig.ssid, + strlen(wifiConfig.psk) > 0 ? "***" : "", + wifiConfig.power / 10.0, + wifiConfig.sleep, + wifiConfig.autoreboot ? "true" : "false" + ); + server.sendContent(buf); + snprintf_P(buf, BufferSize, CONF_NET_JSON, + strlen(wifiConfig.ip) > 0 ? "static" : "dhcp", + wifiConfig.ip, + wifiConfig.subnet, + wifiConfig.gateway, + wifiConfig.dns1, + wifiConfig.dns2, + wifiConfig.mdns ? "true" : "false", + ntpConfig.server, + ntpConfig.dhcp ? "true" : "false" + ); + server.sendContent(buf); + snprintf_P(buf, BufferSize, CONF_MQTT_JSON, + mqttConfig.host, + mqttConfig.port, + mqttConfig.username, + strlen(mqttConfig.password) > 0 ? "***" : "", + mqttConfig.clientId, + mqttConfig.publishTopic, + mqttConfig.payloadFormat, + mqttConfig.ssl ? "true" : "false", + qsc ? "true" : "false", + qsr ? "true" : "false", + qsk ? "true" : "false" + ); + server.sendContent(buf); + snprintf_P(buf, BufferSize, CONF_PRICE_JSON, + entsoe.enabled ? "true" : "false", + entsoe.token, + entsoe.area, + entsoe.currency, + entsoe.multiplier / 1000.0 + ); + server.sendContent(buf); + snprintf_P(buf, BufferSize, CONF_DEBUG_JSON, + debugConfig.serial ? "true" : "false", + debugConfig.telnet ? "true" : "false", + debugConfig.level + ); + server.sendContent(buf); + snprintf_P(buf, BufferSize, CONF_GPIO_JSON, + gpioConfig->hanPin == 0xff ? "null" : String(gpioConfig->hanPin, 10).c_str(), + gpioConfig->apPin == 0xff ? "null" : String(gpioConfig->apPin, 10).c_str(), + gpioConfig->ledPin == 0xff ? "null" : String(gpioConfig->ledPin, 10).c_str(), + gpioConfig->ledInverted ? "true" : "false", + gpioConfig->ledPinRed == 0xff ? "null" : String(gpioConfig->ledPinRed, 10).c_str(), + gpioConfig->ledPinGreen == 0xff ? "null" : String(gpioConfig->ledPinGreen, 10).c_str(), + gpioConfig->ledPinBlue == 0xff ? "null" : String(gpioConfig->ledPinBlue, 10).c_str(), + gpioConfig->ledRgbInverted ? "true" : "false", + gpioConfig->tempSensorPin == 0xff ? "null" : String(gpioConfig->tempSensorPin, 10).c_str(), + gpioConfig->tempAnalogSensorPin == 0xff ? "null" : String(gpioConfig->tempAnalogSensorPin, 10).c_str(), + gpioConfig->vccPin == 0xff ? "null" : String(gpioConfig->vccPin, 10).c_str(), + gpioConfig->vccOffset / 100.0, + gpioConfig->vccMultiplier / 1000.0, + gpioConfig->vccResistorVcc, + gpioConfig->vccResistorGnd, + gpioConfig->vccBootLimit / 10.0 + ); + server.sendContent(buf); + snprintf_P(buf, BufferSize, CONF_UI_JSON, + ui.showImport, + ui.showExport, + ui.showVoltage, + ui.showAmperage, + ui.showReactive, + ui.showRealtime, + ui.showPeaks, + ui.showPricePlot, + ui.showDayPlot, + ui.showMonthPlot, + ui.showTemperaturePlot + ); + server.sendContent(buf); + snprintf_P(buf, BufferSize, CONF_DOMOTICZ_JSON, + domo.elidx, + domo.cl1idx, + domo.vl1idx, + domo.vl2idx, + domo.vl3idx + ); + server.sendContent(buf); + server.sendContent("}"); +} + +void AmsWebServer::handleSave() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Handling save method from http")); + if(!checkSecurity(1)) + return; + + bool success = true; + if(server.hasArg(F("v")) && server.arg(F("v")) == F("true")) { + int boardType = server.arg(F("vb")).toInt(); + int hanPin = server.arg(F("vh")).toInt(); + if(server.hasArg(F("vr")) && server.arg(F("vr")) == F("true")) { + config->clear(); + } + + #if defined(CONFIG_IDF_TARGET_ESP32S2) + switch(boardType) { + case 5: // Pow-K+ + case 7: // Pow-U+ + case 6: // Pow-P1 + config->clearGpio(*gpioConfig); + gpioConfig->hanPin = 16; + gpioConfig->apPin = 0; + gpioConfig->ledPinRed = 13; + gpioConfig->ledPinGreen = 14; + gpioConfig->ledRgbInverted = true; + gpioConfig->vccPin = 10; + gpioConfig->vccResistorGnd = 22; + gpioConfig->vccResistorVcc = 33; + break; + case 51: // Wemos S2 mini + gpioConfig->ledPin = 15; + gpioConfig->ledInverted = false; + gpioConfig->apPin = 0; + case 50: // Generic ESP32-S2 + gpioConfig->hanPin = hanPin > 0 ? hanPin : 18; + break; + default: + success = false; + } + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + #elif defined(ESP32) + switch(boardType) { + case 201: // D32 + gpioConfig->hanPin = hanPin > 0 ? hanPin : 16; + gpioConfig->apPin = 4; + gpioConfig->ledPin = 5; + gpioConfig->ledInverted = true; + break; + case 202: // Feather + case 203: // DevKitC + case 200: // ESP32 + gpioConfig->hanPin = hanPin > 0 ? hanPin : 16; + gpioConfig->ledPin = 2; + gpioConfig->ledInverted = false; + break; + default: + success = false; + } + #elif defined(ESP8266) + switch(boardType) { + case 2: // spenceme + config->clearGpio(*gpioConfig); + gpioConfig->vccBootLimit = 32; + gpioConfig->hanPin = 3; + gpioConfig->apPin = 0; + gpioConfig->ledPin = 2; + gpioConfig->ledInverted = true; + gpioConfig->tempSensorPin = 5; + break; + case 0: // roarfred + config->clearGpio(*gpioConfig); + gpioConfig->hanPin = 3; + gpioConfig->apPin = 0; + gpioConfig->ledPin = 2; + gpioConfig->ledInverted = true; + gpioConfig->tempSensorPin = 5; + break; + case 1: // Arnio Kamstrup + case 3: // Pow-K UART0 + case 4: // Pow-U UART0 + config->clearGpio(*gpioConfig); + gpioConfig->hanPin = 3; + gpioConfig->apPin = 0; + gpioConfig->ledPin = 2; + gpioConfig->ledInverted = true; + gpioConfig->ledPinRed = 13; + gpioConfig->ledPinGreen = 14; + gpioConfig->ledRgbInverted = true; + break; + case 5: // Pow-K GPIO12 + case 7: // Pow-U GPIO12 + config->clearGpio(*gpioConfig); + gpioConfig->hanPin = 12; + gpioConfig->apPin = 0; + gpioConfig->ledPin = 2; + gpioConfig->ledInverted = true; + gpioConfig->ledPinRed = 13; + gpioConfig->ledPinGreen = 14; + gpioConfig->ledRgbInverted = true; + break; + case 101: // D1 + gpioConfig->hanPin = hanPin > 0 ? hanPin : 5; + gpioConfig->apPin = 4; + gpioConfig->ledPin = 2; + gpioConfig->ledInverted = true; + gpioConfig->vccMultiplier = 1100; + break; + case 100: // ESP8266 + gpioConfig->hanPin = hanPin > 0 ? hanPin : 3; + gpioConfig->ledPin = 2; + gpioConfig->ledInverted = true; + break; + default: + success = false; + } + #endif + config->setGpioConfig(*gpioConfig); + + SystemConfig sys; + config->getSystemConfig(sys); + sys.boardType = success ? boardType : 0xFF; + sys.vendorConfigured = success; + config->setSystemConfig(sys); + } + + if(server.hasArg(F("s")) && server.arg(F("s")) == F("true")) { + SystemConfig sys; + config->getSystemConfig(sys); + + config->clear(); + + WiFiConfig wifi; + config->clearWifi(wifi); + + strcpy(wifi.ssid, server.arg(F("ss")).c_str()); + + String psk = server.arg(F("sp")); + if(!psk.equals("***")) { + strcpy(wifi.psk, psk.c_str()); + } + wifi.mode = 1; // WIFI_STA + + if(server.hasArg(F("sm")) && server.arg(F("sm")) == "static") { + strcpy(wifi.ip, server.arg(F("si")).c_str()); + strcpy(wifi.gateway, server.arg(F("sg")).c_str()); + strcpy(wifi.subnet, server.arg(F("su")).c_str()); + strcpy(wifi.dns1, server.arg(F("sd")).c_str()); + } + + if(server.hasArg(F("sh")) && !server.arg(F("sh")).isEmpty()) { + strcpy(wifi.hostname, server.arg(F("sh")).c_str()); + wifi.mdns = true; + } else { + wifi.mdns = false; + } + + switch(sys.boardType) { + case 6: // Pow-P1 + meterConfig->baud = 115200; + meterConfig->parity = 3; // 8N1 + break; + case 3: // Pow-K UART0 + case 5: // Pow-K+ + meterConfig->parity = 3; // 8N1 + case 2: // spenceme + case 50: // Generic ESP32-S2 + case 51: // Wemos S2 mini + meterConfig->baud = 2400; + wifi.sleep = 1; // Modem sleep + break; + case 4: // Pow-U UART0 + case 7: // Pow-U+ + wifi.sleep = 2; // Light sleep + break; + } + config->setWiFiConfig(wifi); + config->setMeterConfig(*meterConfig); + + sys.userConfigured = success; + sys.dataCollectionConsent = 0; + config->setSystemConfig(sys); + + performRestart = true; + } else if(server.hasArg(F("sf")) && !server.arg(F("sf")).isEmpty()) { + SystemConfig sys; + config->getSystemConfig(sys); + sys.dataCollectionConsent = server.hasArg(F("sf")) && (server.arg(F("sf")) == F("true") || server.arg(F("sf")) == F("1")) ? 1 : 2; + config->setSystemConfig(sys); + } + + if(server.hasArg(F("m")) && server.arg(F("m")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received meter config")); + config->getMeterConfig(*meterConfig); + meterConfig->baud = server.arg(F("mb")).toInt(); + meterConfig->parity = server.arg(F("mp")).toInt(); + meterConfig->invert = server.hasArg(F("mi")) && server.arg(F("mi")) == F("true"); + meterConfig->distributionSystem = server.arg(F("md")).toInt(); + meterConfig->mainFuse = server.arg(F("mf")).toInt(); + meterConfig->productionCapacity = server.arg(F("mr")).toInt(); + maxPwr = 0; + + String encryptionKeyHex = server.arg(F("mek")); + if(!encryptionKeyHex.isEmpty()) { + encryptionKeyHex.replace(F("0x"), F("")); + fromHex(meterConfig->encryptionKey, encryptionKeyHex, 16); + } + + String authenticationKeyHex = server.arg(F("mea")); + if(!authenticationKeyHex.isEmpty()) { + authenticationKeyHex.replace(F("0x"), F("")); + fromHex(meterConfig->authenticationKey, authenticationKeyHex, 16); + } + + meterConfig->wattageMultiplier = server.arg(F("mmw")).toDouble() * 1000; + meterConfig->voltageMultiplier = server.arg(F("mmv")).toDouble() * 1000; + meterConfig->amperageMultiplier = server.arg(F("mma")).toDouble() * 1000; + meterConfig->accumulatedMultiplier = server.arg(F("mmc")).toDouble() * 1000; + config->setMeterConfig(*meterConfig); + } + + if(server.hasArg(F("w")) && server.arg(F("w")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received WiFi config")); + WiFiConfig wifi; + config->getWiFiConfig(wifi); + strcpy(wifi.ssid, server.arg(F("ws")).c_str()); + String psk = server.arg(F("wp")); + if(!psk.equals("***")) { + strcpy(wifi.psk, psk.c_str()); + } + wifi.power = server.arg(F("ww")).toFloat() * 10; + wifi.sleep = server.arg(F("wz")).toInt(); + wifi.autoreboot = server.hasArg(F("wa")) && server.arg(F("wa")) == F("true"); + config->setWiFiConfig(wifi); + + if(server.hasArg(F("nm")) && server.arg(F("nm")) == "static") { + strcpy(wifi.ip, server.arg(F("ni")).c_str()); + strcpy(wifi.gateway, server.arg(F("ng")).c_str()); + strcpy(wifi.subnet, server.arg(F("ns")).c_str()); + strcpy(wifi.dns1, server.arg(F("nd1")).c_str()); + strcpy(wifi.dns2, server.arg(F("nd2")).c_str()); + } + wifi.mdns = server.hasArg(F("nd")) && server.arg(F("nd")) == F("true"); + config->setWiFiConfig(wifi); + } + + if(server.hasArg(F("ntp")) && server.arg(F("ntp")) == F("true")) { + NtpConfig ntp; + config->getNtpConfig(ntp); + ntp.enable = true; + ntp.dhcp = server.hasArg(F("ntpd")) && server.arg(F("ntpd")) == F("true"); + strcpy(ntp.server, server.arg(F("ntph")).c_str()); + config->setNtpConfig(ntp); + } + + if(server.hasArg(F("q")) && server.arg(F("q")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received MQTT config")); + MqttConfig mqtt; + config->getMqttConfig(mqtt); + if(server.hasArg(F("qh")) && !server.arg(F("qh")).isEmpty()) { + strcpy(mqtt.host, server.arg(F("qh")).c_str()); + strcpy(mqtt.clientId, server.arg(F("qc")).c_str()); + strcpy(mqtt.publishTopic, server.arg(F("qb")).c_str()); + strcpy(mqtt.subscribeTopic, server.arg(F("qr")).c_str()); + strcpy(mqtt.username, server.arg(F("qu")).c_str()); + String pass = server.arg(F("qa")); + if(!pass.equals("***")) { + strcpy(mqtt.password, pass.c_str()); + } + mqtt.payloadFormat = server.arg(F("qm")).toInt(); + #if defined(ESP8266) + mqtt.ssl = false; + #else + mqtt.ssl = server.arg(F("qs")) == F("true"); + #endif + + mqtt.port = server.arg(F("qp")).toInt(); + if(mqtt.port == 0) { + mqtt.port = mqtt.ssl ? 8883 : 1883; + } + } else { + config->clearMqtt(mqtt); + } + config->setMqttConfig(mqtt); + } + + if(server.hasArg(F("o")) && server.arg(F("o")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received Domoticz config")); + DomoticzConfig domo { + static_cast(server.arg(F("oe")).toInt()), + static_cast(server.arg(F("ou1")).toInt()), + static_cast(server.arg(F("ou2")).toInt()), + static_cast(server.arg(F("ou3")).toInt()), + static_cast(server.arg(F("oc")).toInt()) + }; + config->setDomoticzConfig(domo); + } + + + if(server.hasArg(F("g")) && server.arg(F("g")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received web config")); + webConfig.security = server.arg(F("gs")).toInt(); + if(webConfig.security > 0) { + strcpy(webConfig.username, server.arg(F("gu")).c_str()); + String pass = server.arg(F("gp")); + if(!pass.equals("***")) { + strcpy(webConfig.password, pass.c_str()); + } + debugger->setPassword(webConfig.password); + } else { + strcpy_P(webConfig.username, PSTR("")); + strcpy_P(webConfig.password, PSTR("")); + debugger->setPassword(F("")); + } + config->setWebConfig(webConfig); + + WiFiConfig wifi; + config->getWiFiConfig(wifi); + if(server.hasArg(F("gh")) && !server.arg(F("gh")).isEmpty()) { + strcpy(wifi.hostname, server.arg(F("gh")).c_str()); + } + config->setWiFiConfig(wifi); + + NtpConfig ntp; + config->getNtpConfig(ntp); + strcpy(ntp.timezone, server.arg(F("gt")).c_str()); + config->setNtpConfig(ntp); + } + + if(server.hasArg(F("i")) && server.arg(F("i")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received GPIO config")); + gpioConfig->hanPin = server.hasArg(F("ih")) && !server.arg(F("ih")).isEmpty() ? server.arg(F("ih")).toInt() : 3; + gpioConfig->ledPin = server.hasArg(F("ilp")) && !server.arg(F("ilp")).isEmpty() ? server.arg(F("ilp")).toInt() : 0xFF; + gpioConfig->ledInverted = server.hasArg(F("ili")) && server.arg(F("ili")) == F("true"); + gpioConfig->ledPinRed = server.hasArg(F("irr")) && !server.arg(F("irr")).isEmpty() ? server.arg(F("irr")).toInt() : 0xFF; + gpioConfig->ledPinGreen = server.hasArg(F("irg")) && !server.arg(F("irg")).isEmpty() ? server.arg(F("irg")).toInt() : 0xFF; + gpioConfig->ledPinBlue = server.hasArg(F("irb")) && !server.arg(F("irb")).isEmpty() ? server.arg(F("irb")).toInt() : 0xFF; + gpioConfig->ledRgbInverted = server.hasArg(F("iri")) && server.arg(F("iri")) == F("true"); + gpioConfig->apPin = server.hasArg(F("ia")) && !server.arg(F("ia")).isEmpty() ? server.arg(F("ia")).toInt() : 0xFF; + gpioConfig->tempSensorPin = server.hasArg(F("itd")) && !server.arg(F("itd")).isEmpty() ?server.arg(F("itd")).toInt() : 0xFF; + gpioConfig->tempAnalogSensorPin = server.hasArg(F("ita")) && !server.arg(F("ita")).isEmpty() ?server.arg(F("ita")).toInt() : 0xFF; + gpioConfig->vccPin = server.hasArg(F("ivp")) && !server.arg(F("ivp")).isEmpty() ? server.arg(F("ivp")).toInt() : 0xFF; + gpioConfig->vccResistorGnd = server.hasArg(F("ivdg")) && !server.arg(F("ivdg")).isEmpty() ? server.arg(F("ivdg")).toInt() : 0; + gpioConfig->vccResistorVcc = server.hasArg(F("ivdv")) && !server.arg(F("ivdv")).isEmpty() ? server.arg(F("ivdv")).toInt() : 0; + config->setGpioConfig(*gpioConfig); + } + + if(server.hasArg(F("iv")) && server.arg(F("iv")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received Vcc config")); + gpioConfig->vccOffset = server.hasArg(F("ivo")) && !server.arg(F("ivo")).isEmpty() ? server.arg(F("ivo")).toFloat() * 100 : 0; + gpioConfig->vccMultiplier = server.hasArg(F("ivm")) && !server.arg(F("ivm")).isEmpty() ? server.arg(F("ivm")).toFloat() * 1000 : 1000; + gpioConfig->vccBootLimit = server.hasArg(F("ivb")) && !server.arg(F("ivb")).isEmpty() ? server.arg(F("ivb")).toFloat() * 10 : 0; + config->setGpioConfig(*gpioConfig); + } + + if(server.hasArg(F("d")) && server.arg(F("d")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received Debug config")); + DebugConfig debug; + config->getDebugConfig(debug); + bool active = debug.serial || debug.telnet; + + debug.telnet = server.hasArg(F("dt")) && server.arg(F("dt")) == F("true"); + debug.serial = server.hasArg(F("ds")) && server.arg(F("ds")) == F("true"); + debug.level = server.arg(F("dl")).toInt(); + + if(debug.telnet || debug.serial) { + if(webConfig.security > 0) { + debugger->setPassword(webConfig.password); + } else { + debugger->setPassword(F("")); + } + debugger->setSerialEnabled(debug.serial); + WiFiConfig wifi; + if(config->getWiFiConfig(wifi) && strlen(wifi.hostname) > 0) { + debugger->begin(wifi.hostname, (uint8_t) debug.level); + if(!debug.telnet) { + debugger->stop(); + } + } + } else if(active) { + performRestart = true; + } + config->setDebugConfig(debug); + } + + if(server.hasArg(F("u")) && server.arg(F("u")) == F("true")) { + UiConfig ui; + config->getUiConfig(ui); + ui.showImport = server.arg(F("ui")).toInt(); + ui.showExport = server.arg(F("ue")).toInt(); + ui.showVoltage = server.arg(F("uv")).toInt(); + ui.showAmperage = server.arg(F("ua")).toInt(); + ui.showReactive = server.arg(F("ur")).toInt(); + ui.showRealtime = server.arg(F("uc")).toInt(); + ui.showPeaks = server.arg(F("ut")).toInt(); + ui.showPricePlot = server.arg(F("up")).toInt(); + ui.showDayPlot = server.arg(F("ud")).toInt(); + ui.showMonthPlot = server.arg(F("um")).toInt(); + ui.showTemperaturePlot = server.arg(F("us")).toInt(); + config->setUiConfig(ui); + } + + if(server.hasArg(F("p")) && server.arg(F("p")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received price API config")); + + priceRegion = server.arg(F("pr")); + + EntsoeConfig entsoe; + entsoe.enabled = server.hasArg(F("pe")) && server.arg(F("pe")) == F("true"); + strcpy(entsoe.token, server.arg(F("pt")).c_str()); + strcpy(entsoe.area, priceRegion.c_str()); + strcpy(entsoe.currency, server.arg(F("pc")).c_str()); + entsoe.multiplier = server.arg(F("pm")).toFloat() * 1000; + config->setEntsoeConfig(entsoe); + } + + if(server.hasArg(F("t")) && server.arg(F("t")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Received energy accounting config")); + EnergyAccountingConfig eac; + eac.thresholds[0] = server.arg(F("t0")).toInt(); + eac.thresholds[1] = server.arg(F("t1")).toInt(); + eac.thresholds[2] = server.arg(F("t2")).toInt(); + eac.thresholds[3] = server.arg(F("t3")).toInt(); + eac.thresholds[4] = server.arg(F("t4")).toInt(); + eac.thresholds[5] = server.arg(F("t5")).toInt(); + eac.thresholds[6] = server.arg(F("t6")).toInt(); + eac.thresholds[7] = server.arg(F("t7")).toInt(); + eac.thresholds[8] = server.arg(F("t8")).toInt(); + eac.hours = server.arg(F("th")).toInt(); + config->setEnergyAccountingConfig(eac); + } + + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(PSTR("Saving configuration now...")); + + if (config->save()) { + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(PSTR("Successfully saved.")); + if(config->isWifiChanged() || performRestart) { + performRestart = true; + } else { + hw->setup(gpioConfig, config); + } + } else { + success = false; + } + + snprintf_P(buf, BufferSize, RESPONSE_JSON, + success ? "true" : "false", + "", + performRestart ? "true" : "false" + ); + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); + + server.handleClient(); + delay(250); + + if(performRestart || rebootForUpgrade) { + if(ds != NULL) { + ds->save(); + } + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(PSTR("Rebooting")); + delay(1000); + #if defined(ESP8266) + ESP.reset(); + #elif defined(ESP32) + ESP.restart(); + #endif + performRestart = false; + } +} + +void AmsWebServer::reboot() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /reboot over http...\n"); + + if(!checkSecurity(1)) + return; + + server.send(200, MIME_JSON, "{\"reboot\":true}"); + + server.handleClient(); + delay(250); + + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(PSTR("Rebooting")); + delay(1000); + #if defined(ESP8266) + ESP.reset(); + #elif defined(ESP32) + ESP.restart(); + #endif + performRestart = false; +} + +void AmsWebServer::upgrade() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /upgrade over http...\n"); + + if(!checkSecurity(1)) + return; + + SystemConfig sys; + config->getSystemConfig(sys); + + snprintf_P(buf, BufferSize, RESPONSE_JSON, + sys.dataCollectionConsent == 1 ? "true" : "false", + "", + sys.dataCollectionConsent == 1 ? "true" : "false" + ); + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); + + if(sys.dataCollectionConsent == 1) { + server.handleClient(); + delay(250); + + if(server.hasArg(F("url"))) { + customFirmwareUrl = server.arg(F("url")); + } + + String url = customFirmwareUrl.isEmpty() || !customFirmwareUrl.startsWith(F("http")) ? F("http://hub.amsleser.no/hub/firmware/update") : customFirmwareUrl; + + if(server.hasArg(F("version"))) { + url += "/" + server.arg(F("version")); + } + + WiFiClient client; + #if defined(ESP8266) + String chipType = F("esp8266"); + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + String chipType = F("esp32s2"); + #elif defined(ESP32) + #if defined(CONFIG_FREERTOS_UNICORE) + String chipType = F("esp32solo"); + #else + String chipType = F("esp32"); + #endif + #endif + + #if defined(ESP8266) + ESPhttpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + t_httpUpdate_return ret = ESPhttpUpdate.update(client, url, VERSION); + #elif defined(ESP32) + HTTPUpdate httpUpdate; + httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + HTTPUpdateResult ret = httpUpdate.update(client, url, String(VERSION) + "-" + chipType); + #endif + + switch(ret) { + case HTTP_UPDATE_FAILED: + debugger->printf(PSTR("Update failed")); + break; + case HTTP_UPDATE_NO_UPDATES: + debugger->printf(PSTR("No Update")); + break; + case HTTP_UPDATE_OK: + debugger->printf(PSTR("Update OK")); + break; + } + } +} +void AmsWebServer::firmwareHtml() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Serving /firmware.html over http...")); + + if(!checkSecurity(1)) + return; + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(FIRMWARE_HTML_LEN); + server.send_P(200, MIME_HTML, FIRMWARE_HTML); +} + +void AmsWebServer::firmwarePost() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Handling firmware post...")); + if(!checkSecurity(1)) + return; + + if(rebootForUpgrade) { + server.send(200); + } else { + if(server.hasArg(F("url"))) { + String url = server.arg(F("url")); + if(!url.isEmpty() && (url.startsWith(F("http://")) || url.startsWith(F("https://")))) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Custom firmware URL was provided")); + + WiFiClient client; + #if defined(ESP8266) + String chipType = F("esp8266"); + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + String chipType = F("esp32s2"); + #elif defined(ESP32) + #if defined(CONFIG_FREERTOS_UNICORE) + String chipType = F("esp32solo"); + #else + String chipType = F("esp32"); + #endif + #endif + + #if defined(ESP8266) + ESPhttpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + t_httpUpdate_return ret = ESPhttpUpdate.update(client, url, VERSION); + #elif defined(ESP32) + HTTPUpdate httpUpdate; + httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + HTTPUpdateResult ret = httpUpdate.update(client, url, String(VERSION) + "-" + chipType); + #endif + + switch(ret) { + case HTTP_UPDATE_FAILED: + debugger->printf(PSTR("Update failed")); + break; + case HTTP_UPDATE_NO_UPDATES: + debugger->printf(PSTR("No Update")); + break; + case HTTP_UPDATE_OK: + debugger->printf(PSTR("Update OK")); + break; + } + server.send(200, MIME_PLAIN, "OK"); + return; + } + } + server.sendHeader(HEADER_LOCATION,F("/firmware")); + server.send(303); + } +} + + +void AmsWebServer::firmwareUpload() { + if(!checkSecurity(1)) + return; + + HTTPUpload& upload = server.upload(); + if(upload.status == UPLOAD_FILE_START) { + String filename = upload.filename; + if(filename.isEmpty()) { + if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("No file, falling back to post\n")); + return; + } + if(!filename.endsWith(".bin")) { + server.send(500, MIME_PLAIN, "500: couldn't create file"); + } else { + #if defined(ESP32) + esp_task_wdt_delete(NULL); + esp_task_wdt_deinit(); + #elif defined(ESP8266) + ESP.wdtDisable(); + #endif + } + } + uploadFile(FILE_FIRMWARE); + if(upload.status == UPLOAD_FILE_END) { + rebootForUpgrade = true; + server.sendHeader("Location","/"); + server.send(302); + } +} + +HTTPUpload& AmsWebServer::uploadFile(const char* path) { + HTTPUpload& upload = server.upload(); + if(upload.status == UPLOAD_FILE_START){ + if(uploading) { + if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("Upload already in progress\n")); + server.send_P(500, MIME_HTML, PSTR("

        Upload already in progress!

        ")); + } else if (!LittleFS.begin()) { + if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("An Error has occurred while mounting LittleFS\n")); + server.send_P(500, MIME_HTML, PSTR("

        Unable to mount LittleFS!

        ")); + } else { + uploading = true; + if(debugger->isActive(RemoteDebug::DEBUG)) { + debugger->printf_P(PSTR("handleFileUpload file: %s\n"), path); + } + if(LittleFS.exists(path)) { + LittleFS.remove(path); + } + file = LittleFS.open(path, "w"); + if(debugger->isActive(RemoteDebug::DEBUG)) { + debugger->printf_P(PSTR("handleFileUpload Open file and write: %u\n"), upload.currentSize); + } + size_t written = file.write(upload.buf, upload.currentSize); + if(debugger->isActive(RemoteDebug::DEBUG)) { + debugger->printf_P(PSTR("handleFileUpload Written: %u\n"), written); + } + } + } else if(upload.status == UPLOAD_FILE_WRITE) { + if(debugger->isActive(RemoteDebug::DEBUG)) { + debugger->printf_P(PSTR("handleFileUpload Writing: %u\n"), upload.currentSize); + } + if(file) { + size_t written = file.write(upload.buf, upload.currentSize); + if(debugger->isActive(RemoteDebug::DEBUG)) { + debugger->printf_P(PSTR("handleFileUpload Written: %u\n"), written); + } + delay(1); + if(written != upload.currentSize) { + file.flush(); + file.close(); + LittleFS.remove(path); + LittleFS.end(); + + if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(PSTR("An Error has occurred while writing file")); + snprintf_P(buf, BufferSize, RESPONSE_JSON, + "false", + "File size does not match", + "false" + ); + server.setContentLength(strlen(buf)); + server.send(500, MIME_JSON, buf); + } + } + } else if(upload.status == UPLOAD_FILE_END) { + if(debugger->isActive(RemoteDebug::DEBUG)) { + debugger->printf_P(PSTR("handleFileUpload Ended\n")); + } + if(file) { + file.flush(); + file.close(); +// LittleFS.end(); + } else { + debugger->printf_P(PSTR("File was not valid in the end... Write error: %d, \n"), file.getWriteError()); + snprintf_P(buf, BufferSize, RESPONSE_JSON, + "false", + "Upload ended, but file is missing", + "false" + ); + server.setContentLength(strlen(buf)); + server.send(500, MIME_JSON, buf); + } + } + return upload; +} + +void AmsWebServer::isAliveCheck() { + server.sendHeader(F("Access-Control-Allow-Origin"), F("*")); + server.send(200); +} + +void AmsWebServer::factoryResetPost() { + if(!checkSecurity(1)) + return; + + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Performing factory reset")); + + bool success = false; + if(server.hasArg(F("perform")) && server.arg(F("perform")) == F("true")) { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Formatting LittleFS")); + LittleFS.format(); + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Clearing configuration")); + config->clear(); + + success = true; + } + + snprintf_P(buf, BufferSize, RESPONSE_JSON, + success ? "true" : "false", + "", + "true" + ); + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); + + server.handleClient(); + delay(250); + + if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(PSTR("Rebooting")); + delay(1000); + #if defined(ESP8266) + ESP.reset(); + #elif defined(ESP32) + ESP.restart(); + #endif +} + +void AmsWebServer::robotstxt() { + server.send_P(200, MIME_HTML, PSTR("User-agent: *\nDisallow: /\n")); +} + +void AmsWebServer::mqttCaUpload() { + if(!checkSecurity(1)) + return; + + uploadFile(FILE_MQTT_CA); + HTTPUpload& upload = server.upload(); + if(upload.status == UPLOAD_FILE_END) { + server.sendHeader(HEADER_LOCATION,F("/configuration")); + server.send(303); + + MqttConfig mqttConfig; + if(config->getMqttConfig(mqttConfig) && mqttConfig.ssl) { + config->setMqttChanged(); + } + } +} + +void AmsWebServer::mqttCertUpload() { + if(!checkSecurity(1)) + return; + + uploadFile(FILE_MQTT_CERT); + HTTPUpload& upload = server.upload(); + if(upload.status == UPLOAD_FILE_END) { + server.sendHeader(HEADER_LOCATION,F("/configuration")); + server.send(303); + MqttConfig mqttConfig; + if(config->getMqttConfig(mqttConfig) && mqttConfig.ssl) { + config->setMqttChanged(); + } + } +} + +void AmsWebServer::mqttKeyUpload() { + if(!checkSecurity(1)) + return; + + uploadFile(FILE_MQTT_KEY); + HTTPUpload& upload = server.upload(); + if(upload.status == UPLOAD_FILE_END) { + server.sendHeader(HEADER_LOCATION,F("/configuration")); + server.send(303); + MqttConfig mqttConfig; + if(config->getMqttConfig(mqttConfig) && mqttConfig.ssl) { + config->setMqttChanged(); + } + } +} + +void AmsWebServer::tariffJson() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /tariff.json over http...\n"); + + if(!checkSecurity(2)) + return; + + EnergyAccountingConfig* eac = ea->getConfig(); + + String peaks; + for(uint8_t x = 0;x < min((uint8_t) 5, eac->hours); x++) { + EnergyAccountingPeak peak = ea->getPeak(x+1); + int len = snprintf_P(buf, BufferSize, PEAK_JSON, + peak.day, + peak.value / 100.0 + ); + buf[len] = '\0'; + if(!peaks.isEmpty()) peaks += ","; + peaks += String(buf); + } + + snprintf_P(buf, BufferSize, TARIFF_JSON, + eac->thresholds[0], + eac->thresholds[1], + eac->thresholds[2], + eac->thresholds[3], + eac->thresholds[4], + eac->thresholds[5], + eac->thresholds[6], + eac->thresholds[7], + eac->thresholds[8], + eac->thresholds[9], + peaks.c_str(), + ea->getCurrentThreshold(), + ea->getMonthMax() + ); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + + server.setContentLength(strlen(buf)); + server.send(200, MIME_JSON, buf); +} + +void AmsWebServer::setPriceRegion(String priceRegion) { + this->priceRegion = priceRegion; +} + +void AmsWebServer::configFileDownload() { + if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /configfile.cfg over http...\n"); + + if(!checkSecurity(1)) + return; + + bool includeSecrets = server.hasArg(F("ic")) && server.arg(F("ic")) == F("true"); + bool includeWifi = server.hasArg(F("iw")) && server.arg(F("iw")) == F("true"); + bool includeMqtt = server.hasArg(F("im")) && server.arg(F("im")) == F("true"); + bool includeWeb = server.hasArg(F("ie")) && server.arg(F("ie")) == F("true"); + bool includeMeter = server.hasArg(F("it")) && server.arg(F("it")) == F("true"); + bool includeGpio = server.hasArg(F("ig")) && server.arg(F("ig")) == F("true"); + bool includeDomo = server.hasArg(F("id")) && server.arg(F("id")) == F("true"); + bool includeNtp = server.hasArg(F("in")) && server.arg(F("in")) == F("true"); + bool includeEntsoe = server.hasArg(F("is")) && server.arg(F("is")) == F("true"); + bool includeThresholds = server.hasArg(F("ih")) && server.arg(F("ih")) == F("true"); + + SystemConfig sys; + config->getSystemConfig(sys); + + server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE); + server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE); + server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF); + server.sendHeader(F("Content-Disposition"), F("attachment; filename=configfile.cfg")); + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + + server.send_P(200, MIME_PLAIN, PSTR("amsconfig\n")); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("version %s\n"), VERSION)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("boardType %d\n"), sys.boardType)); + + if(includeWifi) { + WiFiConfig wifi; + config->getWiFiConfig(wifi); + if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("hostname %s\n"), wifi.hostname)); + if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ssid %s\n"), wifi.ssid)); + if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("psk %s\n"), wifi.psk)); + if(strlen(wifi.ip) > 0) { + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ip %s\n"), wifi.ip)); + if(strlen(wifi.gateway) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gateway %s\n"), wifi.gateway)); + if(strlen(wifi.subnet) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("subnet %s\n"), wifi.subnet)); + if(strlen(wifi.dns1) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("dns1 %s\n"), wifi.dns1)); + if(strlen(wifi.dns2) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("dns2 %s\n"), wifi.dns2)); + } + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mdns %d\n"), wifi.mdns ? 1 : 0)); + } + + if(includeMqtt) { + MqttConfig mqtt; + config->getMqttConfig(mqtt); + if(strlen(mqtt.host) > 0) { + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mqttHost %s\n"), mqtt.host)); + if(mqtt.port > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mqttPort %d\n"), mqtt.port)); + if(strlen(mqtt.clientId) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mqttClientId %s\n"), mqtt.clientId)); + if(strlen(mqtt.publishTopic) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mqttPublishTopic %s\n"), mqtt.publishTopic)); + if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mqttUsername %s\n"), mqtt.username)); + if(includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mqttPassword %s\n"), mqtt.password)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mqttPayloadFormat %d\n"), mqtt.payloadFormat)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("mqttSsl %d\n"), mqtt.ssl ? 1 : 0)); + } + } + + if(includeWeb && includeSecrets) { + WebConfig web; + config->getWebConfig(web); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("webSecurity %d\n"), web.security)); + if(web.security > 0) { + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("webUsername %s\n"), web.username)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("webPassword %s\n"), web.password)); + } + } + + if(includeMeter) { + MeterConfig meter; + config->getMeterConfig(meter); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("meterBaud %d\n"), meter.baud)); + char parity[4] = ""; + switch(meter.parity) { + case 2: + strcpy_P(parity, PSTR("7N1")); + break; + case 3: + strcpy_P(parity, PSTR("8N1")); + break; + case 10: + strcpy_P(parity, PSTR("7E1")); + break; + case 11: + strcpy_P(parity, PSTR("8E1")); + break; + } + if(strlen(parity) > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("meterParity %s\n"), parity)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("meterInvert %d\n"), meter.invert ? 1 : 0)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("meterDistributionSystem %d\n"), meter.distributionSystem)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("meterMainFuse %d\n"), meter.mainFuse)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("meterProductionCapacity %d\n"), meter.productionCapacity)); + if(includeSecrets) { + if(meter.encryptionKey[0] != 0x00) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("meterEncryptionKey %s\n"), toHex(meter.encryptionKey, 16).c_str())); + if(meter.authenticationKey[0] != 0x00) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("meterAuthenticationKey %s\n"), toHex(meter.authenticationKey, 16).c_str())); + } + } + + if(includeGpio) { + GpioConfig gpio; + config->getGpioConfig(gpio); + if(gpio.hanPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioHanPin %d\n"), gpio.hanPin)); + if(gpio.apPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioApPin %d\n"), gpio.apPin)); + if(gpio.ledPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioLedPin %d\n"), gpio.ledPin)); + if(gpio.ledPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioLedInverted %d\n"), gpio.ledInverted ? 1 : 0)); + if(gpio.ledPinRed != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioLedPinRed %d\n"), gpio.ledPinRed)); + if(gpio.ledPinGreen != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioLedPinGreen %d\n"), gpio.ledPinGreen)); + if(gpio.ledPinBlue != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioLedPinBlue %d\n"), gpio.ledPinBlue)); + if(gpio.ledPinRed != 0xFF || gpio.ledPinGreen != 0xFF || gpio.ledPinBlue != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioLedRgbInverted %d\n"), gpio.ledRgbInverted ? 1 : 0)); + if(gpio.tempSensorPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioTempSensorPin %d\n"), gpio.tempSensorPin)); + if(gpio.tempAnalogSensorPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioTempAnalogSensorPin %d\n"), gpio.tempAnalogSensorPin)); + if(gpio.vccPin != 0xFF) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioVccPin %d\n"), gpio.vccPin)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioVccOffset %.2f\n"), gpio.vccOffset / 100.0)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioVccMultiplier %.3f\n"), gpio.vccMultiplier / 1000.0)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioVccBootLimit %.1f\n"), gpio.vccBootLimit / 10.0)); + if(gpio.vccPin != 0xFF && gpio.vccResistorGnd != 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioVccResistorGnd %d\n"), gpio.vccResistorGnd)); + if(gpio.vccPin != 0xFF && gpio.vccResistorVcc != 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("gpioVccResistorVcc %d\n"), gpio.vccResistorVcc)); + } + + if(includeDomo) { + DomoticzConfig domo; + config->getDomoticzConfig(domo); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("domoticzElidx %d\n"), domo.elidx)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("domoticzVl1idx %d\n"), domo.vl1idx)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("domoticzVl2idx %d\n"), domo.vl2idx)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("domoticzVl3idx %d\n"), domo.vl3idx)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("domoticzCl1idx %d\n"), domo.cl1idx)); + } + + if(includeNtp) { + NtpConfig ntp; + config->getNtpConfig(ntp); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ntpEnable %d\n"), ntp.enable ? 1 : 0)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ntpDhcp %d\n"), ntp.dhcp ? 1 : 0)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ntpTimezone %s\n"), ntp.timezone)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("ntpServer %s\n"), ntp.server)); + } + + if(includeEntsoe) { + EntsoeConfig entsoe; + config->getEntsoeConfig(entsoe); + if(strlen(entsoe.token) == 36 && includeSecrets) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("entsoeToken %s\n"), entsoe.token)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("entsoeArea %s\n"), entsoe.area)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("entsoeCurrency %s\n"), entsoe.currency)); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("entsoeMultiplier %.3f\n"), entsoe.multiplier / 1000.0)); + } + + if(includeThresholds) { + EnergyAccountingConfig eac; + config->getEnergyAccountingConfig(eac); + + if(eac.thresholds[9] > 0) server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("thresholds %d %d %d %d %d %d %d %d %d %d %d\n"), + eac.thresholds[0], + eac.thresholds[1], + eac.thresholds[2], + eac.thresholds[3], + eac.thresholds[4], + eac.thresholds[5], + eac.thresholds[6], + eac.thresholds[7], + eac.thresholds[8], + eac.thresholds[9], + eac.hours + )); + } + + + if(ds != NULL) { + DayDataPoints day = ds->getDayData(); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("dayplot %d %lu %lu %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"), + day.version, + (int32_t) day.lastMeterReadTime, + day.activeImport, + day.accuracy, + ds->getHourImport(0), + ds->getHourImport(1), + ds->getHourImport(2), + ds->getHourImport(3), + ds->getHourImport(4), + ds->getHourImport(5), + ds->getHourImport(6), + ds->getHourImport(7), + ds->getHourImport(8), + ds->getHourImport(9), + ds->getHourImport(10), + ds->getHourImport(11), + ds->getHourImport(12), + ds->getHourImport(13), + ds->getHourImport(14), + ds->getHourImport(15), + ds->getHourImport(16), + ds->getHourImport(17), + ds->getHourImport(18), + ds->getHourImport(19), + ds->getHourImport(20), + ds->getHourImport(21), + ds->getHourImport(22), + ds->getHourImport(23) + )); + if(day.activeExport > 0) { + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR(" %u %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d\n"), + day.activeExport, + ds->getHourExport(0), + ds->getHourExport(1), + ds->getHourExport(2), + ds->getHourExport(3), + ds->getHourExport(4), + ds->getHourExport(5), + ds->getHourExport(6), + ds->getHourExport(7), + ds->getHourExport(8), + ds->getHourExport(9), + ds->getHourExport(10), + ds->getHourExport(11), + ds->getHourExport(12), + ds->getHourExport(13), + ds->getHourExport(14), + ds->getHourExport(15), + ds->getHourExport(16), + ds->getHourExport(17), + ds->getHourExport(18), + ds->getHourExport(19), + ds->getHourExport(20), + ds->getHourExport(21), + ds->getHourExport(22), + ds->getHourExport(23) + )); + } else { + server.sendContent(F("\n")); + } + + MonthDataPoints month = ds->getMonthData(); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("monthplot %d %lu %lu %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"), + month.version, + (int32_t) month.lastMeterReadTime, + month.activeImport, + month.accuracy, + ds->getDayImport(1), + ds->getDayImport(2), + ds->getDayImport(3), + ds->getDayImport(4), + ds->getDayImport(5), + ds->getDayImport(6), + ds->getDayImport(7), + ds->getDayImport(8), + ds->getDayImport(9), + ds->getDayImport(10), + ds->getDayImport(11), + ds->getDayImport(12), + ds->getDayImport(13), + ds->getDayImport(14), + ds->getDayImport(15), + ds->getDayImport(16), + ds->getDayImport(17), + ds->getDayImport(18), + ds->getDayImport(19), + ds->getDayImport(20), + ds->getDayImport(21), + ds->getDayImport(22), + ds->getDayImport(23), + ds->getDayImport(24), + ds->getDayImport(25), + ds->getDayImport(26), + ds->getDayImport(27), + ds->getDayImport(28), + ds->getDayImport(29), + ds->getDayImport(30), + ds->getDayImport(31) + )); + if(month.activeExport > 0) { + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR(" %u %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d\n"), + month.activeExport, + ds->getDayExport(1), + ds->getDayExport(2), + ds->getDayExport(3), + ds->getDayExport(4), + ds->getDayExport(5), + ds->getDayExport(6), + ds->getDayExport(7), + ds->getDayExport(8), + ds->getDayExport(9), + ds->getDayExport(10), + ds->getDayExport(11), + ds->getDayExport(12), + ds->getDayExport(13), + ds->getDayExport(14), + ds->getDayExport(15), + ds->getDayExport(16), + ds->getDayExport(17), + ds->getDayExport(18), + ds->getDayExport(19), + ds->getDayExport(20), + ds->getDayExport(21), + ds->getDayExport(22), + ds->getDayExport(23), + ds->getDayExport(24), + ds->getDayExport(25), + ds->getDayExport(26), + ds->getDayExport(27), + ds->getDayExport(28), + ds->getDayExport(29), + ds->getDayExport(30), + ds->getDayExport(31) + )); + } else { + server.sendContent(F("\n")); + } + } + + if(ea != NULL) { + EnergyAccountingConfig eac; + config->getEnergyAccountingConfig(eac); + EnergyAccountingData ead = ea->getData(); + server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("energyaccounting %d %d %.2f %d %d %.2f %d %d %d %.2f %d %.2f %d %.2f %d %.2f %d %.2f"), + ead.version, + ead.month, + ead.costYesterday / 10.0, + ead.costThisMonth, + ead.costLastMonth, + ead.incomeYesterday / 10.0, + ead.incomeThisMonth, + ead.incomeLastMonth, + ead.peaks[0].day, + ead.peaks[0].value / 100.0, + ead.peaks[1].day, + ead.peaks[1].value / 100.0, + ead.peaks[2].day, + ead.peaks[2].value / 100.0, + ead.peaks[3].day, + ead.peaks[3].value / 100.0, + ead.peaks[4].day, + ead.peaks[4].value / 100.0 + )); + server.sendContent("\n"); + } +} + +void AmsWebServer::configFileUpload() { + if(!checkSecurity(1)) + return; + + HTTPUpload& upload = uploadFile(FILE_CFG); + if(upload.status == UPLOAD_FILE_END) { + performRestart = true; + server.sendHeader("Location","/"); + server.send(303); + } +} + +void AmsWebServer::redirectToMain() { + server.sendHeader("Location","/"); + server.send(302); +} \ No newline at end of file diff --git a/src/Uptime.h b/lib/Uptime/include/Uptime.h similarity index 100% rename from src/Uptime.h rename to lib/Uptime/include/Uptime.h diff --git a/src/Uptime.cpp b/lib/Uptime/src/Uptime.cpp similarity index 100% rename from src/Uptime.cpp rename to lib/Uptime/src/Uptime.cpp diff --git a/platformio-user.ini-example b/platformio-user.ini-example index 276bb8a1..a6c816ce 100644 --- a/platformio-user.ini-example +++ b/platformio-user.ini-example @@ -2,19 +2,16 @@ default_envs = dev [env:dev] -platform = espressif8266 +platform = espressif8266@3.2.0 +framework = arduino board = esp12e board_build.ldscript = eagle.flash.4m2m.ld -framework = arduino -lib_deps = ${common.lib_deps} -lib_ignore = ${common.lib_ignore} +build_flags = ${common.build_flags} -D DEBUG_MODE=1 +lib_ldf_mode = off lib_compat_mode = off -build_flags = - -D WEBSOCKET_DISABLED=1 - -D DEBUG_MODE=1 -extra_scripts = - pre:scripts/addversion.py - scripts/makeweb.py +lib_deps = ESP8266WiFi, ESP8266mDNS, ESP8266WebServer, ESP8266HTTPClient, ESP8266httpUpdate, ${common.lib_deps} +lib_ignore = ${common.lib_ignore} +extra_scripts = ${common.extra_scripts} monitor_speed = 115200 ; If serial port is shared with HAN, use baud and parity configured for meter monitor_flags = --parity diff --git a/platformio.ini b/platformio.ini index 37670bed..7b1fde3e 100755 --- a/platformio.ini +++ b/platformio.ini @@ -2,63 +2,74 @@ extra_configs = platformio-user.ini [common] -lib_deps = Timezone@1.2.4, 256dpi/MQTT@2.5.0, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.1 +lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.0, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.1, Timezone@1.2.4, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, EntsoePriceApi, EnergyAccounting, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, SvelteUi lib_ignore = OneWire +extra_scripts = + pre:scripts/addversion.py + lib/JsonMqttHandler/scripts/generate_includes.py + lib/DomoticzMqttHandler/scripts/generate_includes.py + lib/HomeAssistantMqttHandler/scripts/generate_includes.py + lib/SvelteUi/scripts/generate_includes.py +build_flags = + -D WEBSOCKET_DISABLED=1 + -D NO_AMS2MQTT_PRICE_KEY + -D NO_AMS2MQTT_PRICE_AUTHENTICATION + -fexceptions + +[esp32] +lib_deps = WiFi, ESPmDNS, WiFiClientSecure, HTTPClient, FS, Update, HTTPUpdate, WebServer, ${common.lib_deps} [env:esp8266] platform = espressif8266@3.2.0 framework = arduino board = esp12e board_build.ldscript = eagle.flash.4m2m.ld -build_flags = -D WEBSOCKET_DISABLED=1 -lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} +lib_ldf_mode = off +lib_compat_mode = off +lib_deps = ESP8266WiFi, ESP8266mDNS, ESP8266WebServer, ESP8266HTTPClient, ESP8266httpUpdate, ${common.lib_deps} lib_ignore = ${common.lib_ignore} -extra_scripts = - pre:scripts/addversion.py - scripts/makeweb.py - -# Sticking to v2.0.3 because of #298 +extra_scripts = ${common.extra_scripts} [env:esp32] -platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.3/platform-espressif32-2.0.3.zip +platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.5.3/platform-espressif32-2.0.5.3.zip framework = arduino board = esp32dev board_build.f_cpu = 160000000L -build_flags = -D WEBSOCKET_DISABLED=1 -fexceptions -lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} +lib_ldf_mode = off +lib_compat_mode = off +lib_deps = ${esp32.lib_deps} lib_ignore = ${common.lib_ignore} -extra_scripts = - pre:scripts/addversion.py - scripts/makeweb.py +extra_scripts = ${common.extra_scripts} # Tasmota has pre-built platform for C3, S2, S3 and Solo, more information at: # https://github.com/Jason2866/esp32-arduino-lib-builder [env:esp32s2] -platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.3/platform-espressif32-2.0.3.zip -platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.3 +platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.5.3/platform-espressif32-2.0.5.3.zip framework = arduino -board = esp32dev +board = esp32-s2-saola-1 board_build.mcu = esp32s2 board_build.variant = esp32s2 board_build.flash_mode = qio board_build.f_cpu = 160000000L board_build.f_flash = 40000000L -build_flags = -D WEBSOCKET_DISABLED=1 -lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} +lib_ldf_mode = off +lib_compat_mode = off +lib_deps = ${esp32.lib_deps} lib_ignore = ${common.lib_ignore} -extra_scripts = - pre:scripts/addversion.py - scripts/makeweb.py +extra_scripts = ${common.extra_scripts} [env:esp32solo] -platform = https://github.com/tasmota/platform-espressif32/releases/download/v.2.0.3/platform-espressif32-solo1-v.2.0.3.zip +platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.5.3/platform-espressif32-2.0.5.3.zip framework = arduino -board = esp32dev +board = esp32-solo1 board_build.f_cpu = 160000000L -build_flags = -D WEBSOCKET_DISABLED=1 -fexceptions -lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} -DFRAMEWORK_ARDUINO_SOLO1 +lib_ldf_mode = off +lib_compat_mode = off +lib_deps = ${esp32.lib_deps} lib_ignore = ${common.lib_ignore} -extra_scripts = - pre:scripts/addversion.py - scripts/makeweb.py +extra_scripts = ${common.extra_scripts} diff --git a/scripts/addversion.py b/scripts/addversion.py index c2295ea4..5fc40508 100644 --- a/scripts/addversion.py +++ b/scripts/addversion.py @@ -2,7 +2,7 @@ import os import subprocess from time import time -FILENAME_VERSION_H = 'src/version.h' +FILENAME_VERSION_H = 'lib/AmsConfiguration/include/version.h' version = os.environ.get('GITHUB_TAG') if version == None: try: diff --git a/scripts/esp32c3/flash.sh b/scripts/esp32c3/flash.sh new file mode 100755 index 00000000..5dd6acf8 --- /dev/null +++ b/scripts/esp32c3/flash.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +if [ -z "$1" ];then + echo "Usage: " + echo " Flashing first time : $0 flash /dev/ttyUSB0" + echo " When upgrading to new version : $0 upgrade /dev/ttyUSB0" + echo " NOTE: Replace /dev/ttyUSB0 with correct port" + exit 1 +fi + +if [ -z "$2" ];then + echo "Please specify port" + exit 1 +fi + +esptool=`which esptool` +if [ -z "$esptool" ];then + esptool=`which esptool.py` +fi +if [ -z "$esptool" ];then + if [ -f esptool.py ];then + esptool="esptool.py" + fi +fi +if [ -z "$esptool" ];then + echo "esptool.py not available to run following command: " + esptool="echo esptool.py" +fi + +if [ "$1" = "flash" ];then + $esptool --chip esp32c3 --port $2 --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect \ + 0x1000 bootloader_qio_40m.bin \ + 0x8000 partitions.bin \ + 0xe000 boot_app0.bin \ + 0x10000 firmware.bin + exit $? +elif [ "$1" = "upgrade" ];then + $esptool --chip esp32c3 --port $2 --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x10000 firmware.bin + exit $? +fi diff --git a/scripts/esp32c3/mkzip.sh b/scripts/esp32c3/mkzip.sh new file mode 100755 index 00000000..00176ebf --- /dev/null +++ b/scripts/esp32c3/mkzip.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +env="esp32c3" +build_dir=".pio/build/$env/" + +if [ ! -d $build_dir ];then + echo "No build directory" + exit 1 +fi + +cp ~/.platformio/packages/framework-arduinoespressif32/tools/sdk/$env/bin/bootloader_qio_40m.bin $build_dir +cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin $build_dir +chmod +x scripts/$env/flash.sh +zip -j $env.zip $build_dir/*.bin scripts/$env/flash.sh diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index 6421372a..782d1fc1 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -49,16 +49,16 @@ ADC_MODE(ADC_VCC); #include "hexutils.h" #include "HwTools.h" -#include "entsoe/EntsoeApi.h" +#include "EntsoeApi.h" -#include "web/AmsWebServer.h" +#include "AmsWebServer.h" #include "AmsConfiguration.h" -#include "mqtt/AmsMqttHandler.h" -#include "mqtt/JsonMqttHandler.h" -#include "mqtt/RawMqttHandler.h" -#include "mqtt/DomoticzMqttHandler.h" -#include "mqtt/HomeAssistantMqttHandler.h" +#include "AmsMqttHandler.h" +#include "JsonMqttHandler.h" +#include "RawMqttHandler.h" +#include "DomoticzMqttHandler.h" +#include "HomeAssistantMqttHandler.h" #include "Uptime.h" @@ -71,7 +71,8 @@ ADC_MODE(ADC_VCC); #include "IEC6205675.h" #include "LNG.h" -#include "ams/DataParsers.h" +#include "DataParsers.h" +#include "Timezones.h" uint8_t commonBuffer[BUF_SIZE_COMMON]; uint8_t hanBuffer[BUF_SIZE_HAN]; @@ -162,15 +163,13 @@ void setup() { hw.ledBlink(LED_GREEN, 1); hw.ledBlink(LED_BLUE, 1); - #if defined(ESP32) EntsoeConfig entsoe; - if(config.getEntsoeConfig(entsoe) && strlen(entsoe.token) > 0) { + if(config.getEntsoeConfig(entsoe) && entsoe.enabled && strlen(entsoe.area) > 0) { eapi = new EntsoeApi(&Debug); eapi->setup(entsoe); ws.setEntsoeApi(eapi); } - #endif - + ws.setPriceRegion(entsoe.area); bool shared = false; config.getMeterConfig(meterConfig); Serial.flush(); @@ -197,9 +196,9 @@ void setup() { break; } #if defined(ESP32) - Serial.begin(meterConfig.baud, serialConfig, -1, -1, meterConfig.invert); + Serial.begin(meterConfig.baud == 0 ? 2400 : meterConfig.baud, serialConfig, -1, -1, meterConfig.invert); #else - Serial.begin(meterConfig.baud, serialConfig, SERIAL_FULL, 1, meterConfig.invert); + Serial.begin(meterConfig.baud == 0 ? 2400 : meterConfig.baud, serialConfig, SERIAL_FULL, 1, meterConfig.invert); #endif } @@ -217,7 +216,7 @@ void setup() { debugI("Voltage: %.2fV", vcc); } - float vccBootLimit = gpioConfig.vccBootLimit == 0 ? 0 : gpioConfig.vccBootLimit / 10.0; + float vccBootLimit = gpioConfig.vccBootLimit == 0 ? 0 : min(3.29, gpioConfig.vccBootLimit / 10.0); // Make sure it is never above 3.3v if(vccBootLimit > 2.5 && vccBootLimit < 3.3 && (gpioConfig.apPin == 0xFF || digitalRead(gpioConfig.apPin) == HIGH)) { // Skip if user is holding AP button while booting (HIGH = button is released) if (vcc < vccBootLimit) { if(Debug.isActive(RemoteDebug::INFO)) { @@ -321,12 +320,10 @@ void setup() { NtpConfig ntp; if(config.getNtpConfig(ntp)) { - configTime(ntp.offset*10, ntp.summerOffset*10, ntp.enable ? strlen(ntp.server) > 0 ? ntp.server : (char*) F("pool.ntp.org") : (char*) F("")); // Add NTP server by default if none is configured + tz = resolveTimezone(ntp.timezone); + configTime(tz->toLocal(0), tz->toLocal(JULY1970)-JULY1970, ntp.enable ? strlen(ntp.server) > 0 ? ntp.server : (char*) F("pool.ntp.org") : (char*) F("")); // Add NTP server by default if none is configured sntp_servermode_dhcp(ntp.enable && ntp.dhcp ? 1 : 0); ntpEnabled = ntp.enable; - TimeChangeRule std = {"STD", Last, Sun, Oct, 3, ntp.offset / 6}; - TimeChangeRule dst = {"DST", Last, Sun, Mar, 2, (ntp.offset + ntp.summerOffset) / 6}; - tz = new Timezone(dst, std); ws.setTimezone(tz); ds.setTimezone(tz); ea.setTimezone(tz); @@ -347,6 +344,7 @@ void setup() { config.ackEnergyAccountingChange(); } ea.setup(&ds, eac); + ea.load(); ea.setEapi(eapi); ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &ds, &ea); @@ -370,6 +368,13 @@ unsigned long lastSysupdate = 0; unsigned long lastErrorBlink = 0; int lastError = 0; +bool meterAutodetect = false; +unsigned long meterAutodetectLastChange = 0; +uint8_t meterAutoIndex = 0; +uint32_t bauds[] = { 2400, 2400, 115200, 115200 }; +uint8_t parities[] = { 11, 3, 3, 3 }; +bool inverts[] = { false, false, false, true }; + void loop() { Debug.handle(); unsigned long now = millis(); @@ -411,9 +416,39 @@ void loop() { wifiReconnectCount = 0; if(!wifiConnected) { wifiConnected = true; - + WiFiConfig wifi; if(config.getWiFiConfig(wifi)) { + #if defined(ESP32) + if(wifi.power >= 195) + WiFi.setTxPower(WIFI_POWER_19_5dBm); + else if(wifi.power >= 190) + WiFi.setTxPower(WIFI_POWER_19dBm); + else if(wifi.power >= 185) + WiFi.setTxPower(WIFI_POWER_18_5dBm); + else if(wifi.power >= 170) + WiFi.setTxPower(WIFI_POWER_17dBm); + else if(wifi.power >= 150) + WiFi.setTxPower(WIFI_POWER_15dBm); + else if(wifi.power >= 130) + WiFi.setTxPower(WIFI_POWER_13dBm); + else if(wifi.power >= 110) + WiFi.setTxPower(WIFI_POWER_11dBm); + else if(wifi.power >= 85) + WiFi.setTxPower(WIFI_POWER_8_5dBm); + else if(wifi.power >= 70) + WiFi.setTxPower(WIFI_POWER_7dBm); + else if(wifi.power >= 50) + WiFi.setTxPower(WIFI_POWER_5dBm); + else if(wifi.power >= 20) + WiFi.setTxPower(WIFI_POWER_2dBm); + else + WiFi.setTxPower(WIFI_POWER_MINUS_1dBm); + #elif defined(ESP8266) + WiFi.setOutputPower(wifi.power / 10.0); + #endif + + WebConfig web; if(config.getWebConfig(web) && web.security > 0) { Debug.setPassword(web.password); @@ -452,14 +487,11 @@ void loop() { if(config.isNtpChanged()) { NtpConfig ntp; if(config.getNtpConfig(ntp)) { - configTime(ntp.offset*10, ntp.summerOffset*10, ntp.enable ? ntp.server : ""); + tz = resolveTimezone(ntp.timezone); + configTime(tz->toLocal(0), tz->toLocal(JULY1970)-JULY1970, ntp.enable ? strlen(ntp.server) > 0 ? ntp.server : (char*) F("pool.ntp.org") : (char*) F("")); // Add NTP server by default if none is configured sntp_servermode_dhcp(ntp.enable && ntp.dhcp ? 1 : 0); ntpEnabled = ntp.enable; - if(tz != NULL) delete tz; - TimeChangeRule std = {"STD", Last, Sun, Oct, 3, ntp.offset / 6}; - TimeChangeRule dst = {"DST", Last, Sun, Mar, 2, (ntp.offset + ntp.summerOffset) / 6}; - tz = new Timezone(dst, std); ws.setTimezone(tz); ds.setTimezone(tz); ea.setTimezone(tz); @@ -481,7 +513,6 @@ void loop() { mqtt->disconnect(); } - #if defined(ESP32) try { if(eapi != NULL && ntpEnabled) { if(eapi->loop() && mqtt != NULL && mqttHandler != NULL && mqtt->connected()) { @@ -491,7 +522,7 @@ void loop() { if(config.isEntsoeChanged()) { EntsoeConfig entsoe; - if(config.getEntsoeConfig(entsoe) && strlen(entsoe.token) > 0) { + if(config.getEntsoeConfig(entsoe) && entsoe.enabled && strlen(entsoe.area) > 0) { if(eapi == NULL) { eapi = new EntsoeApi(&Debug); ea.setEapi(eapi); @@ -503,12 +534,12 @@ void loop() { eapi = NULL; ws.setEntsoeApi(NULL); } + ws.setPriceRegion(entsoe.area); config.ackEntsoeChange(); } } catch(const std::exception& e) { debugE("Exception in ENTSO-E loop (%s)", e.what()); } - #endif ws.loop(); } if(mqtt != NULL) { @@ -542,25 +573,54 @@ void loop() { ea.setup(&ds, eac); config.ackEnergyAccountingChange(); } + try { + if(readHanPort() || now - meterState.getLastUpdateMillis() > 30000) { + if(now - lastTemperatureRead > 15000) { + unsigned long start = millis(); + hw.updateTemperatures(); + lastTemperatureRead = now; - if(readHanPort() || now - meterState.getLastUpdateMillis() > 30000) { - if(now - lastTemperatureRead > 15000) { - unsigned long start = millis(); - hw.updateTemperatures(); - lastTemperatureRead = now; - - if(mqtt != NULL && mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt->connected() && !topic.isEmpty()) { - mqttHandler->publishTemperatures(&config, &hw); + if(mqtt != NULL && mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt->connected() && !topic.isEmpty()) { + mqttHandler->publishTemperatures(&config, &hw); + } + debugD("Used %ld ms to update temperature", millis()-start); } - debugD("Used %ld ms to update temperature", millis()-start); - } - if(now - lastSysupdate > 10000) { - if(mqtt != NULL && mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt->connected() && !topic.isEmpty()) { - mqttHandler->publishSystem(&hw, eapi, &ea); + if(now - lastSysupdate > 60000) { + if(mqtt != NULL && mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt->connected() && !topic.isEmpty()) { + mqttHandler->publishSystem(&hw, eapi, &ea); + } + lastSysupdate = now; } - lastSysupdate = now; } + } catch(const std::exception& e) { + debugE("Exception in readHanPort (%s)", e.what()); + meterState.setLastError(98); } + try { + if(meterState.getLastError() > 0) { + if(now - meterAutodetectLastChange > 15000 && (meterConfig.baud == 0 || meterConfig.parity == 0)) { + meterAutodetect = true; + meterAutoIndex++; // Default is to try the first one in setup() + debugI("Meter serial autodetect, swapping to: %d, %d, %s", bauds[meterAutoIndex], parities[meterAutoIndex], inverts[meterAutoIndex] ? "true" : "false"); + if(meterAutoIndex >= 4) meterAutoIndex = 0; + setupHanPort(gpioConfig.hanPin, bauds[meterAutoIndex], parities[meterAutoIndex], inverts[meterAutoIndex]); + meterAutodetectLastChange = now; + } + } else if(meterAutodetect) { + meterAutoIndex--; // Last one worked, so lets use that + debugI("Meter serial autodetected, saving: %d, %d, %s", bauds[meterAutoIndex], parities[meterAutoIndex], inverts[meterAutoIndex] ? "true" : "false"); + meterAutodetect = false; + meterConfig.baud = bauds[meterAutoIndex]; + meterConfig.parity = parities[meterAutoIndex]; + meterConfig.invert = inverts[meterAutoIndex]; + config.setMeterConfig(meterConfig); + setupHanPort(gpioConfig.hanPin, meterConfig.baud, meterConfig.parity, meterConfig.invert); + } + } catch(const std::exception& e) { + debugE("Exception in meter autodetect (%s)", e.what()); + meterState.setLastError(99); + } + delay(1); // Needed for auto modem sleep #if defined(ESP32) esp_task_wdt_reset(); @@ -572,6 +632,15 @@ void loop() { void setupHanPort(uint8_t pin, uint32_t baud, uint8_t parityOrdinal, bool invert) { if(Debug.isActive(RemoteDebug::INFO)) Debug.printf((char*) F("(setupHanPort) Setting up HAN on pin %d with baud %d and parity %d\n"), pin, baud, parityOrdinal); + if(baud == 0) { + baud = bauds[meterAutoIndex]; + parityOrdinal = parities[meterAutoIndex]; + invert = inverts[meterAutoIndex]; + } + if(parityOrdinal == 0) { + parityOrdinal = 3; // 8N1 + } + HardwareSerial *hwSerial = NULL; if(pin == 3 || pin == 113) { hwSerial = &Serial; @@ -689,6 +758,7 @@ void errorBlink() { if(lastErrorBlink - meterState.getLastUpdateMillis() > 30000) { debugW("No HAN data received last 30s, single blink"); hw.ledBlink(LED_RED, 1); // If no message received from AMS in 30 sec, blink once + if(meterState.getLastError() == 0) meterState.setLastError(90); return; } break; @@ -725,14 +795,31 @@ void swapWifiMode() { if (mode != WIFI_AP || !config.hasConfig()) { if(Debug.isActive(RemoteDebug::INFO)) debugI("Swapping to AP mode"); - WiFi.softAP((char*) F("AMS2MQTT")); + + //wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, 0); // Disable default gw + + /* Example code to set captive portal option in DHCP + auto& server = WiFi.softAPDhcpServer(); + server.onSendOptions([](const DhcpServer& server, auto& options) { + // Captive Portal URI + const IPAddress gateway = netif_ip4_addr(server.getNetif()); + const String captive = F("http://") + gateway.toString(); + options.add(114, captive.c_str(), captive.length()); + }); + */ + + WiFi.softAP(PSTR("AMS2MQTT")); WiFi.mode(WIFI_AP); if(dnsServer == NULL) { dnsServer = new DNSServer(); } dnsServer->setErrorReplyCode(DNSReplyCode::NoError); - dnsServer->start(53, (char*) F("*"), WiFi.softAPIP()); + dnsServer->start(53, PSTR("*"), WiFi.softAPIP()); + #if defined(DEBUG_MODE) + Debug.setSerialEnabled(true); + Debug.begin("192.168.4.1", 23, RemoteDebug::VERBOSE); + #endif } else { if(Debug.isActive(RemoteDebug::INFO)) debugI("Swapping to STA mode"); if(dnsServer != NULL) { @@ -789,6 +876,7 @@ bool readHanPort() { if(pos == DATA_PARSE_INCOMPLETE) { return false; } else if(pos == DATA_PARSE_UNKNOWN_DATA) { + meterState.setLastError(pos); debugV("Unknown data payload:"); len = len + hanSerial->readBytes(hanBuffer+len, BUF_SIZE_HAN-len); debugPrint(hanBuffer, 0, len); @@ -800,6 +888,7 @@ bool readHanPort() { len = 0; return false; } else if(pos < 0) { + meterState.setLastError(pos); printHanReadError(pos); len += hanSerial->readBytes(hanBuffer+len, BUF_SIZE_HAN-len); if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) { @@ -815,6 +904,7 @@ bool readHanPort() { for(int i = pos+ctx.length; i 3) { + if(wifiReconnectCount > 3 && wifi.autoreboot) { ESP.restart(); return; } @@ -991,7 +1087,6 @@ void WiFi_connect() { #endif MDNS.end(); - WiFi.persistent(false); WiFi.disconnect(true); WiFi.softAPdisconnect(true); WiFi.enableAP(false); @@ -1002,12 +1097,6 @@ void WiFi_connect() { } wifiTimeout = WIFI_CONNECTION_TIMEOUT; - WiFiConfig wifi; - if(!config.getWiFiConfig(wifi) || strlen(wifi.ssid) == 0) { - swapWifiMode(); - return; - } - if (Debug.isActive(RemoteDebug::INFO)) debugI("Connecting to WiFi network: %s", wifi.ssid); wifiReconnectCount++; @@ -1018,34 +1107,7 @@ void WiFi_connect() { } #endif WiFi.mode(WIFI_STA); - #if defined(ESP32) - if(wifi.power >= 195) - WiFi.setTxPower(WIFI_POWER_19_5dBm); - else if(wifi.power >= 190) - WiFi.setTxPower(WIFI_POWER_19dBm); - else if(wifi.power >= 185) - WiFi.setTxPower(WIFI_POWER_18_5dBm); - else if(wifi.power >= 170) - WiFi.setTxPower(WIFI_POWER_17dBm); - else if(wifi.power >= 150) - WiFi.setTxPower(WIFI_POWER_15dBm); - else if(wifi.power >= 130) - WiFi.setTxPower(WIFI_POWER_13dBm); - else if(wifi.power >= 110) - WiFi.setTxPower(WIFI_POWER_11dBm); - else if(wifi.power >= 85) - WiFi.setTxPower(WIFI_POWER_8_5dBm); - else if(wifi.power >= 70) - WiFi.setTxPower(WIFI_POWER_7dBm); - else if(wifi.power >= 50) - WiFi.setTxPower(WIFI_POWER_5dBm); - else if(wifi.power >= 20) - WiFi.setTxPower(WIFI_POWER_2dBm); - else - WiFi.setTxPower(WIFI_POWER_MINUS_1dBm); - #elif defined(ESP8266) - WiFi.setOutputPower(wifi.power / 10.0); - #endif + if(strlen(wifi.ip) > 0) { IPAddress ip, gw, sn(255,255,255,0), dns1, dns2; ip.fromString(wifi.ip); @@ -1075,8 +1137,11 @@ void WiFi_connect() { WiFi.hostname(wifi.hostname); } #endif + #if defined(ESP32) + WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); + WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); + #endif WiFi.setAutoReconnect(true); - WiFi.persistent(true); if(WiFi.begin(wifi.ssid, wifi.psk)) { if(wifi.sleep <= 2) { switch(wifi.sleep) { @@ -1287,12 +1352,19 @@ void MQTT_connect() { break; } + time_t epoch = time(nullptr); if(mqttConfig.ssl) { + if(epoch < BUILD_EPOCH) { + debugI("NTP not ready for MQTT SSL"); + return; + } debugI("MQTT SSL is configured (%dkb free heap)", ESP.getFreeHeap()); if(mqttSecureClient == NULL) { mqttSecureClient = new WiFiClientSecure(); #if defined(ESP8266) mqttSecureClient->setBufferSizes(512, 512); + debugD("ESP8266 firmware does not have enough memory..."); + return; #endif if(LittleFS.begin()) { @@ -1308,40 +1380,43 @@ void MQTT_connect() { mqttSecureClient->loadCACert(file, file.size()); #endif file.close(); + + if(LittleFS.exists(FILE_MQTT_CERT) && LittleFS.exists(FILE_MQTT_KEY)) { + #if defined(ESP8266) + debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap()); + file = LittleFS.open(FILE_MQTT_CERT, (char*) "r"); + BearSSL::X509List *serverCertList = new BearSSL::X509List(file); + file.close(); + + debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap()); + file = LittleFS.open(FILE_MQTT_KEY, (char*) "r"); + BearSSL::PrivateKey *serverPrivKey = new BearSSL::PrivateKey(file); + file.close(); + + debugD("Setting client certificates (%dkb free heap)", ESP.getFreeHeap()); + mqttSecureClient->setClientRSACert(serverCertList, serverPrivKey); + #elif defined(ESP32) + debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap()); + file = LittleFS.open(FILE_MQTT_CERT, (char*) "r"); + mqttSecureClient->loadCertificate(file, file.size()); + file.close(); + + debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap()); + file = LittleFS.open(FILE_MQTT_KEY, (char*) "r"); + mqttSecureClient->loadPrivateKey(file, file.size()); + file.close(); + #endif + mqttClient = mqttSecureClient; + } } - if(LittleFS.exists(FILE_MQTT_CERT) && LittleFS.exists(FILE_MQTT_KEY)) { - #if defined(ESP8266) - debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap()); - file = LittleFS.open(FILE_MQTT_CERT, (char*) "r"); - BearSSL::X509List *serverCertList = new BearSSL::X509List(file); - file.close(); - - debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap()); - file = LittleFS.open(FILE_MQTT_KEY, (char*) "r"); - BearSSL::PrivateKey *serverPrivKey = new BearSSL::PrivateKey(file); - file.close(); - - debugD("Setting client certificates (%dkb free heap)", ESP.getFreeHeap()); - mqttSecureClient->setClientRSACert(serverCertList, serverPrivKey); - #elif defined(ESP32) - debugI("Found MQTT certificate file (%dkb free heap)", ESP.getFreeHeap()); - file = LittleFS.open(FILE_MQTT_CERT, (char*) "r"); - mqttSecureClient->loadCertificate(file, file.size()); - file.close(); - - debugI("Found MQTT key file (%dkb free heap)", ESP.getFreeHeap()); - file = LittleFS.open(FILE_MQTT_KEY, (char*) "r"); - mqttSecureClient->loadPrivateKey(file, file.size()); - file.close(); - #endif - } LittleFS.end(); debugD("MQTT SSL setup complete (%dkb free heap)", ESP.getFreeHeap()); } } - mqttClient = mqttSecureClient; - } else if(mqttClient == NULL) { + } + + if(mqttClient == NULL) { mqttClient = new WiFiClient(); } @@ -1372,7 +1447,7 @@ void MQTT_connect() { if (strlen(mqttConfig.subscribeTopic) > 0) { mqtt->onMessage(mqttMessageReceived); mqtt->subscribe(String(mqttConfig.subscribeTopic) + "/#"); - debugI(" Subscribing to [%s]\r\n", mqttConfig.subscribeTopic); + debugI(" Subscribing to [%s]\n", mqttConfig.subscribeTopic); } } else { if (Debug.isActive(RemoteDebug::ERROR)) { @@ -1388,7 +1463,6 @@ void MQTT_connect() { yield(); } - void configFileParse() { debugD("Parsing config file"); @@ -1427,54 +1501,62 @@ void configFileParse() { char* buf = (char*) commonBuffer; memset(buf, 0, 1024); while((size = file.readBytesUntil('\n', buf, 1024)) > 0) { + for(uint16_t i = 0; i < size; i++) { + if(buf[i] < 32 || buf[i] > 126) { + memset(buf+i, 0, size-i); + debugD("Found non-ascii, shortening line from %d to %d", size, i); + size = i; + break; + } + } if(strncmp_P(buf, PSTR("boardType "), 10) == 0) { if(!lSys) { config.getSystemConfig(sys); lSys = true; }; sys.boardType = String(buf+10).toInt(); } else if(strncmp_P(buf, PSTR("ssid "), 5) == 0) { if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; }; - memcpy(wifi.ssid, buf+5, size-5); + strcpy(wifi.ssid, buf+5); } else if(strncmp_P(buf, PSTR("psk "), 4) == 0) { if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; }; - memcpy(wifi.psk, buf+4, size-4); + strcpy(wifi.psk, buf+4); } else if(strncmp_P(buf, PSTR("ip "), 3) == 0) { if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; }; - memcpy(wifi.ip, buf+3, size-3); + strcpy(wifi.ip, buf+3); } else if(strncmp_P(buf, PSTR("gateway "), 8) == 0) { if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; }; - memcpy(wifi.gateway, buf+8, size-8); + strcpy(wifi.gateway, buf+8); } else if(strncmp_P(buf, PSTR("subnet "), 7) == 0) { if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; }; - memcpy(wifi.subnet, buf+7, size-7); + strcpy(wifi.subnet, buf+7); } else if(strncmp_P(buf, PSTR("dns1 "), 5) == 0) { if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; }; - memcpy(wifi.dns1, buf+5, size-5); + strcpy(wifi.dns1, buf+5); } else if(strncmp_P(buf, PSTR("dns2 "), 5) == 0) { if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; }; - memcpy(wifi.dns2, buf+5, size-5); + strcpy(wifi.dns2, buf+5); } else if(strncmp_P(buf, PSTR("hostname "), 9) == 0) { if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; }; - memcpy(wifi.hostname, buf+9, size-9); + strcpy(wifi.hostname, buf+9); } else if(strncmp_P(buf, PSTR("mdns "), 5) == 0) { if(!lWiFi) { config.getWiFiConfig(wifi); lWiFi = true; }; wifi.mdns = String(buf+5).toInt() == 1;; } else if(strncmp_P(buf, PSTR("mqttHost "), 9) == 0) { if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; }; - memcpy(mqtt.host, buf+9, size-9); + strcpy(mqtt.host, buf+9); } else if(strncmp_P(buf, PSTR("mqttPort "), 9) == 0) { if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; }; mqtt.port = String(buf+9).toInt(); } else if(strncmp_P(buf, PSTR("mqttClientId "), 13) == 0) { if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; }; - memcpy(mqtt.clientId, buf+13, size-13); + strcpy(mqtt.clientId, buf+13); } else if(strncmp_P(buf, PSTR("mqttPublishTopic "), 17) == 0) { if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; }; - memcpy(mqtt.publishTopic, buf+17, size-17); + strcpy(mqtt.publishTopic, buf+17); } else if(strncmp_P(buf, PSTR("mqttUsername "), 13) == 0) { if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; }; - memcpy(mqtt.username, buf+13, size-13); + strcpy(mqtt.username, buf+13); } else if(strncmp_P(buf, PSTR("mqttPassword "), 13) == 0) { if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; }; - memcpy(mqtt.password, buf+13, size-13); + strcpy(mqtt.password, buf+13); } else if(strncmp_P(buf, PSTR("mqttPayloadFormat "), 18) == 0) { if(!lMqtt) { config.getMqttConfig(mqtt); lMqtt = true; }; mqtt.payloadFormat = String(buf+18).toInt(); @@ -1486,10 +1568,10 @@ void configFileParse() { web.security = String(buf+12).toInt(); } else if(strncmp_P(buf, PSTR("webUsername "), 12) == 0) { if(!lWeb) { config.getWebConfig(web); lWeb = true; }; - memcpy(web.username, buf+12, size-12); + strcpy(web.username, buf+12); } else if(strncmp_P(buf, PSTR("webPassword "), 12) == 0) { if(!lWeb) { config.getWebConfig(web); lWeb = true; }; - memcpy(web.username, buf+12, size-12); + strcpy(web.username, buf+12); } else if(strncmp_P(buf, PSTR("meterBaud "), 10) == 0) { if(!lMeter) { config.getMeterConfig(meter); lMeter = true; }; meter.baud = String(buf+10).toInt(); @@ -1586,24 +1668,21 @@ void configFileParse() { } else if(strncmp_P(buf, PSTR("ntpDhcp "), 8) == 0) { if(!lNtp) { config.getNtpConfig(ntp); lNtp = true; }; ntp.dhcp = String(buf+8).toInt() == 1; - } else if(strncmp_P(buf, PSTR("ntpOffset "), 10) == 0) { - if(!lNtp) { config.getNtpConfig(ntp); lNtp = true; }; - ntp.offset = String(buf+10).toInt() / 10; - } else if(strncmp_P(buf, PSTR("ntpSummerOffset "), 16) == 0) { - if(!lNtp) { config.getNtpConfig(ntp); lNtp = true; }; - ntp.summerOffset = String(buf+16).toInt() / 10; } else if(strncmp_P(buf, PSTR("ntpServer "), 10) == 0) { if(!lNtp) { config.getNtpConfig(ntp); lNtp = true; }; - memcpy(ntp.server, buf+10, size-10); + strcpy(ntp.server, buf+10); + } else if(strncmp_P(buf, PSTR("ntpTimezone "), 12) == 0) { + if(!lNtp) { config.getNtpConfig(ntp); lNtp = true; }; + strcpy(ntp.timezone, buf+12); } else if(strncmp_P(buf, PSTR("entsoeToken "), 12) == 0) { if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; }; - memcpy(entsoe.token, buf+12, size-12); + strcpy(entsoe.token, buf+12); } else if(strncmp_P(buf, PSTR("entsoeArea "), 11) == 0) { if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; }; - memcpy(entsoe.area, buf+11, size-11); + strcpy(entsoe.area, buf+11); } else if(strncmp_P(buf, PSTR("entsoeCurrency "), 15) == 0) { if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; }; - memcpy(entsoe.currency, buf+15, size-15); + strcpy(entsoe.currency, buf+15); } else if(strncmp_P(buf, PSTR("entsoeMultiplier "), 17) == 0) { if(!lEntsoe) { config.getEntsoeConfig(entsoe); lEntsoe = true; }; entsoe.multiplier = String(buf+17).toDouble() * 1000; @@ -1618,20 +1697,38 @@ void configFileParse() { eac.hours = String(pch).toInt(); } else if(strncmp_P(buf, PSTR("dayplot "), 8) == 0) { int i = 0; - DayDataPoints day = { 4 }; // Use a version we know the multiplier of the data points + DayDataPoints day = { 0 }; char * pch = strtok (buf+8," "); while (pch != NULL) { int64_t val = String(pch).toInt(); - if(i == 1) { - day.lastMeterReadTime = val; - } else if(i == 2) { - day.activeImport = val; - } else if(i > 2 && i < 27) { - day.hImport[i-3] = val / 10; - } else if(i == 27) { - day.activeExport = val; - } else if(i > 27 && i < 52) { - day.hExport[i-28] = val / 10; + if(day.version < 5) { + if(i == 0) { + day.version = val; + } else if(i == 1) { + day.lastMeterReadTime = val; + } else if(i == 2) { + day.activeImport = val; + } else if(i > 2 && i < 27) { + day.hImport[i-3] = val / 10; + } else if(i == 27) { + day.activeExport = val; + } else if(i > 27 && i < 52) { + day.hExport[i-28] = val / 10; + } + } else { + if(i == 1) { + day.lastMeterReadTime = val; + } else if(i == 2) { + day.activeImport = val; + } else if(i == 3) { + day.accuracy = val; + } else if(i > 3 && i < 28) { + day.hImport[i-4] = val / pow(10, day.accuracy); + } else if(i == 28) { + day.activeExport = val; + } else if(i > 28 && i < 53) { + day.hExport[i-29] = val / pow(10, day.accuracy); + } } pch = strtok (NULL, " "); @@ -1641,22 +1738,39 @@ void configFileParse() { sDs = true; } else if(strncmp_P(buf, PSTR("monthplot "), 10) == 0) { int i = 0; - MonthDataPoints month = { 5 }; // Use a version we know the multiplier of the data points + MonthDataPoints month = { 0 }; char * pch = strtok (buf+10," "); while (pch != NULL) { int64_t val = String(pch).toInt(); - if(i == 1) { - month.lastMeterReadTime = val; - } else if(i == 2) { - month.activeImport = val; - } else if(i > 2 && i < 34) { - month.dImport[i-3] = val / 10; - } else if(i == 34) { - month.activeExport = val; - } else if(i > 34 && i < 66) { - month.dExport[i-35] = val / 10; + if(month.version < 6) { + if(i == 0) { + month.version = val; + } else if(i == 1) { + month.lastMeterReadTime = val; + } else if(i == 2) { + month.activeImport = val; + } else if(i > 2 && i < 34) { + month.dImport[i-3] = val / 10; + } else if(i == 34) { + month.activeExport = val; + } else if(i > 34 && i < 66) { + month.dExport[i-35] = val / 10; + } + } else { + if(i == 1) { + month.lastMeterReadTime = val; + } else if(i == 2) { + month.activeImport = val; + } else if(i == 3) { + month.accuracy = val; + } else if(i > 3 && i < 35) { + month.dImport[i-4] = val / pow(10, month.accuracy); + } else if(i == 35) { + month.activeExport = val; + } else if(i > 35 && i < 67) { + month.dExport[i-36] = val / pow(10, month.accuracy); + } } - pch = strtok (NULL, " "); i++; } @@ -1664,48 +1778,94 @@ void configFileParse() { sDs = true; } else if(strncmp_P(buf, PSTR("energyaccounting "), 17) == 0) { uint8_t i = 0; - EnergyAccountingData ead = { 4, 0, - 0, 0, 0, + EnergyAccountingData ead = { 0, 0, + 0, 0, 0, // Cost + 0, 0, 0, // Income 0, 0, // Peak 1 0, 0, // Peak 2 0, 0, // Peak 3 0, 0, // Peak 4 0, 0 // Peak 5 }; + uint8_t peak = 0; char * pch = strtok (buf+17," "); while (pch != NULL) { - if(i == 0) { - // Ignore version - } else if(i == 1) { - long val = String(pch).toInt(); - ead.month = val; - } else if(i == 2) { - double val = String(pch).toDouble(); - if(val > 0.0) { - ead.peaks[0] = { 1, (uint16_t) (val*100) }; - } - } else if(i == 3) { - double val = String(pch).toDouble(); - ead.costYesterday = val * 10; - } else if(i == 4) { - double val = String(pch).toDouble(); - ead.costThisMonth = val; - } else if(i == 5) { - double val = String(pch).toDouble(); - ead.costLastMonth = val; - } else if(i >= 6 && i < 18) { - uint8_t hour = i-6; - if(hour%2 == 0) { + if(ead.version < 5) { + if(i == 0) { long val = String(pch).toInt(); - ead.peaks[hour/2].day = val; - } else { + ead.version = val; + } else if(i == 1) { + long val = String(pch).toInt(); + ead.month = val; + } else if(i == 2) { double val = String(pch).toDouble(); - ead.peaks[hour/2].value = val * 100; + if(val > 0.0) { + ead.peaks[0] = { 1, (uint16_t) (val*100) }; + } + } else if(i == 3) { + double val = String(pch).toDouble(); + ead.costYesterday = val * 10; + } else if(i == 4) { + double val = String(pch).toDouble(); + ead.costThisMonth = val; + } else if(i == 5) { + double val = String(pch).toDouble(); + ead.costLastMonth = val; + } else if(i >= 6 && i < 18) { + uint8_t hour = i-6; + { + long val = String(pch).toInt(); + ead.peaks[peak].day = val; + } + pch = strtok (NULL, " "); + i++; + { + double val = String(pch).toDouble(); + ead.peaks[peak].value = val * 100; + } + peak++; + } + } else { + if(i == 1) { + long val = String(pch).toInt(); + ead.month = val; + } else if(i == 2) { + double val = String(pch).toDouble(); + ead.costYesterday = val * 10; + } else if(i == 3) { + double val = String(pch).toDouble(); + ead.costThisMonth = val; + } else if(i == 4) { + double val = String(pch).toDouble(); + ead.costLastMonth = val; + } else if(i == 5) { + double val = String(pch).toDouble(); + ead.incomeYesterday= val * 10; + } else if(i == 6) { + double val = String(pch).toDouble(); + ead.incomeThisMonth = val; + } else if(i == 7) { + double val = String(pch).toDouble(); + ead.incomeLastMonth = val; + } else if(i >= 8 && i < 20) { + uint8_t hour = i-8; + { + long val = String(pch).toInt(); + ead.peaks[peak].day = val; + } + pch = strtok (NULL, " "); + i++; + { + double val = String(pch).toDouble(); + ead.peaks[peak].value = val * 100; + } + peak++; } } pch = strtok (NULL, " "); i++; } + ead.version = 5; ea.setData(ead); sEa = true; } @@ -1731,4 +1891,4 @@ void configFileParse() { if(sDs) ds.save(); if(sEa) ea.save(); config.save(); -} +} \ No newline at end of file diff --git a/src/IEC6205675.cpp b/src/IEC6205675.cpp index bae43a02..ef690518 100644 --- a/src/IEC6205675.cpp +++ b/src/IEC6205675.cpp @@ -1,7 +1,7 @@ #include "IEC6205675.h" #include "lwip/def.h" #include "Timezone.h" -#include "ams/ntohll.h" +#include "ntohll.h" IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx) { double val; @@ -351,13 +351,6 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo l2PowerFactor /= 100; if(l3PowerFactor != 0) l3PowerFactor /= 100; - - int watt = abs((l1voltage * l1current) + (l2voltage * l2current) + (l3voltage * l3current)); - if(watt / (activeImportPower + activeExportPower + reactiveImportPower + reactiveExportPower) > 2) { - l1current = l1current != 0 ? l1current / 10 : 0; - l2current = l2current != 0 ? l2current / 10 : 0; - l3current = l3current != 0 ? l3current / 10 : 0; - } } else if(meterType == AmsTypeSagemcom) { CosemData* meterTs = getCosemDataAt(1, ((char *) (d))); if(meterTs != NULL) { diff --git a/src/IEC6205675.h b/src/IEC6205675.h index 75cf0134..9b5f9093 100644 --- a/src/IEC6205675.h +++ b/src/IEC6205675.h @@ -3,8 +3,8 @@ #include "AmsData.h" #include "AmsConfiguration.h" -#include "ams/DataParser.h" -#include "ams/Cosem.h" +#include "DataParser.h" +#include "Cosem.h" #define NOVALUE 0xFFFFFFFF diff --git a/src/LNG.cpp b/src/LNG.cpp index cde094e7..97c2087e 100644 --- a/src/LNG.cpp +++ b/src/LNG.cpp @@ -1,11 +1,11 @@ #include "LNG.h" #include "lwip/def.h" -#include "ams/ntohll.h" +#include "ntohll.h" LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, RemoteDebug* debugger) { LngHeader* h = (LngHeader*) payload; if(h->tag == CosemTypeStructure && h->arrayTag == CosemTypeArray) { - meterType = AmsTypeLng; + meterType = AmsTypeLandisGyr; this->packageTimestamp = ctx.timestamp; uint8_t* ptr = (uint8_t*) &h[1]; @@ -26,6 +26,7 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da if(descriptor->obis[3] == 7) { if(descriptor->obis[4] == 0) { o170 = getNumber(item); + listType = listType >= 1 ? listType : 1; if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o170); } } else if(descriptor->obis[3] == 8) { @@ -36,9 +37,11 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da activeImportCounter = o180 / 1000.0; } else if(descriptor->obis[4] == 1) { o181 = getNumber(item); + listType = listType >= 3 ? listType : 3; if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o181); } else if(descriptor->obis[4] == 2) { o182 = getNumber(item); + listType = listType >= 3 ? listType : 3; if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o182); } } @@ -46,6 +49,7 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da if(descriptor->obis[3] == 7) { if(descriptor->obis[4] == 0) { o270 = getNumber(item); + listType = listType >= 2 ? listType : 2; if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o270); } } else if(descriptor->obis[3] == 8) { @@ -56,9 +60,11 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da activeExportCounter = o280 / 1000.0; } else if(descriptor->obis[4] == 1) { o281 = getNumber(item); + listType = listType >= 3 ? listType : 3; if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o281); } else if(descriptor->obis[4] == 2) { o282 = getNumber(item); + listType = listType >= 3 ? listType : 3; if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %lu", o282); } } @@ -69,12 +75,14 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da memcpy(str, item->oct.data, item->oct.length); str[item->oct.length] = '\0'; meterId = String(str); + listType = listType >= 2 ? listType : 2; if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %s (oct)", str); } else if(descriptor->obis[4] == 1) { char str[item->oct.length+1]; memcpy(str, item->oct.data, item->oct.length); str[item->oct.length] = '\0'; meterModel = String(str); + listType = listType >= 2 ? listType : 2; if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf(" and value %s (oct)", str); } } @@ -85,21 +93,18 @@ LNG::LNG(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, Da if(o170 > 0 || o270 > 0) { int32_t sum = o170-o270; if(sum > 0) { - listType = listType >= 1 ? listType : 1; activeImportPower = sum; } else { - listType = listType >= 2 ? listType : 2; activeExportPower = sum * -1; + listType = listType >= 2 ? listType : 2; } } if(o181 > 0 || o182 > 0) { activeImportCounter = (o181 + o182) / 1000.0; - listType = listType >= 3 ? listType : 3; } if(o281 > 0 || o282 > 0) { activeExportCounter = (o281 + o282) / 1000.0; - listType = listType >= 3 ? listType : 3; } if((*data) == 0x09) { diff --git a/src/LNG.h b/src/LNG.h index 0cf4e6df..0b994a18 100644 --- a/src/LNG.h +++ b/src/LNG.h @@ -3,8 +3,8 @@ #include "AmsData.h" #include "AmsConfiguration.h" -#include "ams/DataParser.h" -#include "ams/Cosem.h" +#include "DataParser.h" +#include "Cosem.h" #include "RemoteDebug.h" struct LngHeader { diff --git a/src/entsoe/EntsoeApi.cpp b/src/entsoe/EntsoeApi.cpp deleted file mode 100644 index bc8bf07b..00000000 --- a/src/entsoe/EntsoeApi.cpp +++ /dev/null @@ -1,297 +0,0 @@ -#include "EntsoeApi.h" -#include -#include "Uptime.h" -#include "TimeLib.h" -#include "DnbCurrParser.h" -#include "version.h" - -#if defined(ESP32) -#include -#endif - -EntsoeApi::EntsoeApi(RemoteDebug* Debug) { - this->buf = (char*) malloc(BufferSize); - - debugger = Debug; - - // Entso-E uses CET/CEST - TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; - TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; - tz = new Timezone(CEST, CET); - - tomorrowFetchMillis = 36000000 + (random(1800) * 1000); // Random between 13:30 and 14:00 -} - -void EntsoeApi::setup(EntsoeConfig& config) { - if(this->config == NULL) { - this->config = new EntsoeConfig(); - } - memcpy(this->config, &config, sizeof(config)); - lastTodayFetch = lastTomorrowFetch = lastCurrencyFetch = 0; - if(today != NULL) delete today; - if(tomorrow != NULL) delete tomorrow; - today = tomorrow = NULL; -} - -char* EntsoeApi::getToken() { - return this->config->token; -} - -char* EntsoeApi::getCurrency() { - return this->config->currency; -} - -float EntsoeApi::getValueForHour(int8_t hour) { - time_t cur = time(nullptr); - return getValueForHour(cur, hour); -} - -float EntsoeApi::getValueForHour(time_t cur, int8_t hour) { - tmElements_t tm; - if(tz != NULL) - cur = tz->toLocal(cur); - breakTime(cur, tm); - int pos = tm.Hour + hour; - if(pos >= 48) - return ENTSOE_NO_VALUE; - - double value = ENTSOE_NO_VALUE; - double multiplier = config->multiplier / 1000.0; - if(pos > 23) { - if(tomorrow == NULL) - return ENTSOE_NO_VALUE; - value = tomorrow->getPoint(pos-24); - if(value != ENTSOE_NO_VALUE && strcmp(tomorrow->getMeasurementUnit(), "MWH") == 0) { - multiplier *= 0.001; - } else { - return ENTSOE_NO_VALUE; - } - float mult = getCurrencyMultiplier(tomorrow->getCurrency(), config->currency); - if(mult == 0) return ENTSOE_NO_VALUE; - multiplier *= mult; - } else if(pos >= 0) { - if(today == NULL) - return ENTSOE_NO_VALUE; - value = today->getPoint(pos); - if(value != ENTSOE_NO_VALUE && strcmp(today->getMeasurementUnit(), "MWH") == 0) { - multiplier *= 0.001; - } else { - return ENTSOE_NO_VALUE; - } - float mult = getCurrencyMultiplier(today->getCurrency(), config->currency); - if(mult == 0) return ENTSOE_NO_VALUE; - multiplier *= mult; - } - return value * multiplier; -} - -bool EntsoeApi::loop() { - if(strlen(getToken()) == 0) - return false; - - uint64_t now = millis64(); - if(now < 10000) return false; // Grace period - - time_t t = time(nullptr); - if(t < BUILD_EPOCH) return false; - - bool ret = false; - tmElements_t tm; - breakTime(tz->toLocal(t), tm); - if(currentHour != tm.Hour) { - currentHour = tm.Hour; - ret = today != NULL; // Only trigger MQTT publish if we have todays prices. - } - - if(midnightMillis == 0) { - uint32_t curDayMillis = (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000); - midnightMillis = now + (SECS_PER_DAY * 1000) - curDayMillis; - if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Setting midnight millis %llu\n", midnightMillis); - currentDay = tm.Day; - return false; - } else if(now > midnightMillis && currentDay != tm.Day) { - if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Rotating price objects at %lu\n", t); - if(today != NULL) delete today; - if(tomorrow != NULL) { - today = tomorrow; - tomorrow = NULL; - } - currentDay = tm.Day; - midnightMillis = 0; // Force new midnight millis calculation - return true; - } else { - breakTime(t, tm); // Break UTC to find UTC midnight - if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) { - lastTodayFetch = now; - time_t e1 = t - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second; // UTC midnight - time_t e2 = e1 + SECS_PER_DAY; - tmElements_t d1, d2; - breakTime(tz->toUTC(e1), d1); // To get day and hour for CET/CEST at UTC midnight - breakTime(tz->toUTC(e2), d2); - - snprintf(buf, BufferSize, "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s", - "https://transparency.entsoe.eu/api", getToken(), - d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00, - d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00, - config->area, config->area); - - #if defined(ESP32) - esp_task_wdt_reset(); - #elif defined(ESP8266) - ESP.wdtFeed(); - #endif - - - if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for today\n"); - if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf); - EntsoeA44Parser* a44 = new EntsoeA44Parser(); - if(retrieve(buf, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) { - today = a44; - return true; - } else if(a44 != NULL) { - delete a44; - today = NULL; - return false; - } - } - - // Prices for next day are published at 13:00 CE(S)T, but to avoid heavy server traffic at that time, we will - // fetch 1 hr after that (with some random delay) and retry every 15 minutes - if(tomorrow == NULL - && midnightMillis - now < tomorrowFetchMillis - && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 900000) - ) { - lastTomorrowFetch = now; - time_t e1 = t - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second + (SECS_PER_DAY); - time_t e2 = e1 + SECS_PER_DAY; - tmElements_t d1, d2; - breakTime(tz->toUTC(e1), d1); - breakTime(tz->toUTC(e2), d2); - - snprintf(buf, BufferSize, "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s", - "https://transparency.entsoe.eu/api", getToken(), - d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00, - d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00, - config->area, config->area); - - #if defined(ESP32) - esp_task_wdt_reset(); - #elif defined(ESP8266) - ESP.wdtFeed(); - #endif - - if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for tomorrow\n"); - if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf); - EntsoeA44Parser* a44 = new EntsoeA44Parser(); - if(retrieve(buf, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) { - tomorrow = a44; - return true; - } else if(a44 != NULL) { - delete a44; - tomorrow = NULL; - return false; - } - } - } - return ret; -} - -bool EntsoeApi::retrieve(const char* url, Stream* doc) { - HTTPClient https; - https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - https.setReuse(false); - https.setTimeout(50000); - https.setUserAgent("ams2mqtt/" + String(VERSION)); - #if defined(ESP32) - if(https.begin(url)) { - printD("Connection established"); - - #if defined(ESP32) - esp_task_wdt_reset(); - #elif defined(ESP8266) - ESP.wdtFeed(); - #endif - - int status = https.GET(); - - #if defined(ESP32) - esp_task_wdt_reset(); - #elif defined(ESP8266) - ESP.wdtFeed(); - #endif - - if(status == HTTP_CODE_OK) { - printD("Receiving data"); - https.writeToStream(doc); - https.end(); - return true; - } else { - if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf("(EntsoeApi) Communication error, returned status: %d\n", status); - printE(https.errorToString(status)); - printD(https.getString()); - - https.end(); - return false; - } - } else { - return false; - } - #endif - return false; -} - -float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) { - if(strcmp(from, to) == 0) - return 1.00; - - uint64_t now = millis64(); - if(now > lastCurrencyFetch && (lastCurrencyFetch == 0 || (now - lastCurrencyFetch) > 60000)) { - lastCurrencyFetch = now; - - DnbCurrParser p; - - #if defined(ESP32) - esp_task_wdt_reset(); - #elif defined(ESP8266) - ESP.wdtFeed(); - #endif - - snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", from); - if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", from); - if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf); - if(retrieve(buf, &p)) { - if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue()); - currencyMultiplier = p.getValue(); - if(strncmp(to, "NOK", 3) != 0) { - snprintf(buf, BufferSize, "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", to); - if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", to); - if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", buf); - if(retrieve(buf, &p)) { - if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue()); - currencyMultiplier /= p.getValue(); - } else { - return 0; - } - } - } else { - return 0; - } - if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier); - lastCurrencyFetch = midnightMillis; - } - return currencyMultiplier; -} - -void EntsoeApi::printD(String fmt, ...) { - va_list args; - va_start(args, fmt); - if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args); - va_end(args); -} - -void EntsoeApi::printE(String fmt, ...) { - va_list args; - va_start(args, fmt); - if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args); - va_end(args); -} diff --git a/src/version.h b/src/version.h new file mode 100644 index 00000000..3f8382a8 --- /dev/null +++ b/src/version.h @@ -0,0 +1,5 @@ + +#ifndef VERSION + #define VERSION "538de5e" +#endif +#define BUILD_EPOCH 1668532199 diff --git a/webui.png b/webui.png deleted file mode 100644 index b39ea7a6..00000000 Binary files a/webui.png and /dev/null differ