Compare commits

..

2 Commits

Author SHA1 Message Date
Gunnar Skjold
32fa2f5632 Merge branch 'master' into dev-v2.1.8 2022-10-06 17:30:49 +02:00
Gunnar Skjold
12be475b02 Reverted HA changes for 2.1.8 release 2022-10-06 17:29:01 +02:00
229 changed files with 6827 additions and 14112 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
custom: ["https://amsleser.no"]
custom: ["https://paypal.me/gskjold"]

View File

@@ -33,7 +33,6 @@ If applicable, add screenshots to help explain your problem.
**Relevant firmware information:**
- Version: [e.g. 2.1.0]
- MQTT: [yes/no]
- MQTT payload type: [e.g. JSON]
- HAN GPIO: [e.g. GPIO5]
- HAN baud and parity: [e.g. 2400 8E1]
- Temperature sensors [e.g. 3xDS18B20]

View File

@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Meter configuration
url: https://github.com/UtilitechAS/amsreader-firmware/wiki/Known-hardware-configurations
url: https://github.com/gskjold/AmsToMqttBridge/wiki/Known-hardware-configurations
about: Please check your meter configuration here first.
- name: Frequently asked questions
url: https://github.com/UtilitechAS/amsreader-firmware/wiki/FAQ
url: https://github.com/gskjold/AmsToMqttBridge/wiki/FAQ
about: Please check frequently asked questions first.

View File

@@ -20,7 +20,6 @@ A clear and concise description of what the problem is.
**Relevant firmware information:**
- Version: [e.g. 2.1.0]
- MQTT: [yes/no]
- MQTT payload type: [e.g. JSON]
- HAN GPIO: [e.g. GPIO5]
- HAN baud and parity: [e.g. 2400 8E1]
- Temperature sensors [e.g. 3xDS18B20]

View File

@@ -8,7 +8,6 @@ on:
- scripts/**
- web/**
- platformio.ini
- .github/workflows/**
branches:
- '*'
tags:
@@ -23,12 +22,6 @@ 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:
@@ -47,18 +40,6 @@ 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

View File

@@ -23,12 +23,6 @@ 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:
@@ -47,23 +41,12 @@ 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
@@ -75,19 +58,6 @@ 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:
@@ -97,20 +67,6 @@ 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:
@@ -120,20 +76,6 @@ 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:
@@ -144,10 +86,24 @@ jobs:
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32solo firmware
run: pio run -e esp32solo
- name: Create esp32solo zip file
run: /bin/sh scripts/esp32solo/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 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 esp32solo binary to release
uses: actions/upload-release-asset@v1
env:
@@ -157,35 +113,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 esp32solo zip to release
- 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: esp32solo.zip
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip
- name: Build esp32c3 firmware
run: pio run -e esp32c3
- name: Create esp32c3 zip file
run: /bin/sh scripts/esp32c3/mkzip.sh
- name: Upload esp32c3 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/esp32c3/firmware.bin
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.bin
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 esp32c3 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: esp32c3.zip
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.zip
asset_content_type: application/zip

5
.gitignore vendored
View File

@@ -7,7 +7,7 @@
.vscode
.pio
platformio-user.ini
/lib/AmsConfiguration/include/version.h
/src/version.h
/src/web/root
/src/AmsToMqttBridge.ino.cpp
/test
@@ -15,6 +15,3 @@ platformio-user.ini
/sdkconfig
/.tmp
/*.zip
node_modules
/gui/dist
/scripts/*dev

View File

@@ -1,12 +1,12 @@
# AMS Reader
# AMS MQTT Bridge
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. 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/).
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/).
<img src="images/dashboard.png">
<img src="webui.png">
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).
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).
## Building this project with PlatformIO
To build this project, you need [PlatformIO](https://platformio.org/) installed.

View File

@@ -1 +1 @@
[See Hardware page in Wiki](https://github.com/UtilitechAS/amsreader-firmware/wiki)
[See Hardware page in Wiki](https://github.com/gskjold/AmsToMqttBridge/wiki)

View File

@@ -1,76 +0,0 @@
{
"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": []
}
}

View File

@@ -1,440 +0,0 @@
{
"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": {}
}

View File

@@ -1 +0,0 @@
0

View File

@@ -1,3 +0,0 @@
EESchema-DOCLIB Version 2.0
#
#End Doc Library

View File

@@ -1,21 +1,6 @@
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
@@ -50,23 +35,4 @@ 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

View File

@@ -1,75 +0,0 @@
{
"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": []
}
}

View File

@@ -1,356 +0,0 @@
{
"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": {}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 35 KiB

BIN
images/status-bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,86 +0,0 @@
#include <Timezone.h>
#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;
}

View File

@@ -1,38 +0,0 @@
#include "hexutils.h"
String toHex(uint8_t* in) {
return toHex(in, sizeof(in)*2);
}
String toHex(uint8_t* in, uint16_t size) {
String hex;
for(int i = 0; i < size; i++) {
if(in[i] < 0x10) {
hex += '0';
}
hex += String(in[i], HEX);
}
hex.toUpperCase();
return hex;
}
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, bool extended) {
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(extended && (in[i] < 32 || in[i] == 127 || in[i] == 129 || in[i] == 141 || in[i] == 143 || in[i] == 144 || in[i] == 157)) {
memset(in+i, ' ', 1);
} else 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
}

View File

@@ -1 +0,0 @@
json/*.h

View File

@@ -1,76 +0,0 @@
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(";");

View File

@@ -1,8 +0,0 @@
#ifndef _PRICESCONTAINER_H
#define _PRICESCONTAINER_H
struct PricesContainer {
char currency[4];
char measurementUnit[4];
int32_t points[24];
};
#endif

View File

@@ -1,410 +0,0 @@
#include "EntsoeApi.h"
#include <EEPROM.h>
#include "Uptime.h"
#include "TimeLib.h"
#include "DnbCurrParser.h"
#include "version.h"
#include "GcmParser.h"
#if defined(ESP32)
#include <esp_task_wdt.h>
#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://web-api.tp.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();
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);
}
} 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 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;
}

View File

@@ -1 +0,0 @@
json/*.h

View File

@@ -1,81 +0,0 @@
#ifndef _HOMEASSISTANTSTATIC_H
#define _HOMEASSISTANTSTATIC_H
#include "Arduino.h"
struct HomeAssistantSensor {
const char* name;
const char* topic;
const char* path;
const char* uom;
const char* devcl;
const char* stacl;
};
const uint8_t HA_SENSOR_COUNT PROGMEM = 60;
HomeAssistantSensor HA_SENSORS[HA_SENSOR_COUNT] PROGMEM = {
{"Status", "/state", "rssi", "dBm", "signal_strength", "\"measurement\""},
{"Supply volt", "/state", "vcc", "V", "voltage", "\"measurement\""},
{"Temperature", "/state", "temp", "°C", "temperature", "\"measurement\""},
{"Active import", "/power", "P", "W", "power", "\"measurement\""},
{"L1 active import", "/power", "P1", "W", "power", "\"measurement\""},
{"L2 active import", "/power", "P2", "W", "power", "\"measurement\""},
{"L3 active import", "/power", "P3", "W", "power", "\"measurement\""},
{"Reactive import", "/power", "Q", "var", "reactive_power", "\"measurement\""},
{"Active export", "/power", "PO", "W", "power", "\"measurement\""},
{"L1 active export", "/power", "PO1", "W", "power", "\"measurement\""},
{"L2 active export", "/power", "PO2", "W", "power", "\"measurement\""},
{"L3 active export", "/power", "PO3", "W", "power", "\"measurement\""},
{"Reactive export", "/power", "QO", "var", "reactive_power", "\"measurement\""},
{"L1 current", "/power", "I1", "A", "current", "\"measurement\""},
{"L2 current", "/power", "I2", "A", "current", "\"measurement\""},
{"L3 current", "/power", "I3", "A", "current", "\"measurement\""},
{"L1 voltage", "/power", "U1", "V", "voltage", "\"measurement\""},
{"L2 voltage", "/power", "U2", "V", "voltage", "\"measurement\""},
{"L3 voltage", "/power", "U3", "V", "voltage", "\"measurement\""},
{"Accumulated active import", "/energy", "tPI", "kWh", "energy", "\"total_increasing\""},
{"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\""},
{"Price current hour", "/prices", "prices['0']", "", "monetary", ""},
{"Price next hour", "/prices", "prices['1']", "", "monetary", ""},
{"Price in two hour", "/prices", "prices['2']", "", "monetary", ""},
{"Price in three hour", "/prices", "prices['3']", "", "monetary", ""},
{"Price in four hour", "/prices", "prices['4']", "", "monetary", ""},
{"Price in five hour", "/prices", "prices['5']", "", "monetary", ""},
{"Price in six hour", "/prices", "prices['6']", "", "monetary", ""},
{"Price in seven hour", "/prices", "prices['7']", "", "monetary", ""},
{"Price in eight hour", "/prices", "prices['8']", "", "monetary", ""},
{"Price in nine hour", "/prices", "prices['9']", "", "monetary", ""},
{"Price in ten hour", "/prices", "prices['10']", "", "monetary", ""},
{"Price in eleven hour", "/prices", "prices['11']", "", "monetary", ""},
{"Minimum price ahead", "/prices", "prices.min", "", "monetary", ""},
{"Maximum price ahead", "/prices", "prices.max", "", "monetary", ""},
{"Cheapest 1hr period ahead", "/prices", "prices.cheapest1hr","", "timestamp", ""},
{"Cheapest 3hr period ahead", "/prices", "prices.cheapest3hr","", "timestamp", ""},
{"Cheapest 6hr period ahead", "/prices", "prices.cheapest6hr","", "timestamp", ""},
{"Month max", "/realtime","max", "kWh", "energy", "\"total_increasing\""},
{"Tariff threshold", "/realtime","threshold", "kWh", "energy", "\"total_increasing\""},
{"Current hour used", "/realtime","hour.use", "kWh", "energy", "\"total_increasing\""},
{"Current hour cost", "/realtime","hour.cost", "", "monetary", "\"total_increasing\""},
{"Current hour produced", "/realtime","hour.produced", "kWh", "energy", "\"total_increasing\""},
{"Current day used", "/realtime","day.use", "kWh", "energy", "\"total_increasing\""},
{"Current day cost", "/realtime","day.cost", "", "monetary", "\"total_increasing\""},
{"Current day produced", "/realtime","day.produced", "kWh", "energy", "\"total_increasing\""},
{"Current month used", "/realtime","month.use", "kWh", "energy", "\"total_increasing\""},
{"Current month cost", "/realtime","month.cost", "", "monetary", "\"total_increasing\""},
{"Current month produced", "/realtime","month.produced", "kWh", "energy", "\"total_increasing\""},
{"Current month peak 1", "/realtime","peaks[0]", "kWh", "energy", ""},
{"Current month peak 2", "/realtime","peaks[1]", "kWh", "energy", ""},
{"Current month peak 3", "/realtime","peaks[2]", "kWh", "energy", ""},
{"Current month peak 4", "/realtime","peaks[3]", "kWh", "energy", ""},
{"Current month peak 5", "/realtime","peaks[4]", "kWh", "energy", ""},
};
#endif

View File

@@ -1,15 +0,0 @@
{
"lv" : "%s",
"id" : "%s",
"type" : "%s",
"P" : %d,
"Q" : %d,
"PO" : %d,
"QO" : %d,
"I1" : %.2f,
"I2" : %.2f,
"I3" : %.2f,
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f
}

View File

@@ -1,76 +0,0 @@
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(";");

View File

@@ -1 +0,0 @@
json/*.h

View File

@@ -1,22 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<title>Amsleser</title>
<g transform="translate(-29.5,-83)">
<circle r="4.8016944" cy="123.56455" cx="55.064552"
style="fill:none;stroke:#045c7c;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 41.298717,103.9049 a 24,24 0 0 1 27.531669,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 35.562952,95.713384 a 34,34 0 0 1 39.003199,-2e-6"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 47.034482,112.09642 a 14,14 0 0 1 16.06014,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="105.99158" cx="38.181862"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="97.959579" cx="77.491386"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg">
<link rel="mask-icon" href="/favicon.svg" color="#000000">
<title>AMS reader</title>
<script type="module" crossorigin src="/index.js"></script>
<link rel="stylesheet" href="/index.css">
</head>
<body class="bg-gray-100">
<div id="app"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg">
<link rel="mask-icon" href="/favicon.svg" color="#000000">
<title>AMS reader</title>
</head>
<body class="bg-gray-100">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -1,34 +0,0 @@
{
"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"]
}

View File

@@ -1,19 +0,0 @@
// HTTPS required for this to work
// Remember: <link rel="manifest" href="manifest.json" />
{
"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"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
{
"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"
}
}

View File

@@ -1,15 +0,0 @@
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;

View File

@@ -1,11 +0,0 @@
self.addEventListener('install', (event) => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
return self.clients.claim();
});
self.addEventListener('fetch', function(event) {
event.respondWith(fetch(event.request));
});

View File

@@ -1,73 +0,0 @@
<script>
import { Router, Route, navigate } from "svelte-navigator";
import { getSysinfo, sysinfoStore, dataStore } from './lib/DataStores.js';
import Header from './lib/Header.svelte';
import Dashboard from './lib/Dashboard.svelte';
import ConfigurationPanel from './lib/ConfigurationPanel.svelte';
import StatusPage from './lib/StatusPage.svelte';
import VendorPanel from './lib/VendorPanel.svelte';
import SetupPanel from './lib/SetupPanel.svelte';
import Mask from './lib/Mask.svelte';
import FileUploadComponent from "./lib/FileUploadComponent.svelte";
import ConsentComponent from "./lib/ConsentComponent.svelte";
let sysinfo = {};
sysinfoStore.subscribe(update => {
sysinfo = update;
if(sysinfo.vndcfg === false) {
navigate("/vendor");
} else if(sysinfo.usrcfg === false) {
navigate("/setup");
} else if(sysinfo.fwconsent === 0) {
navigate("/consent");
}
});
getSysinfo();
let data = {};
dataStore.subscribe(update => {
data = update;
});
</script>
<div class="container mx-auto m-3">
<Router>
<Header data={data}/>
<Route path="/">
<Dashboard data={data} sysinfo={sysinfo}/>
</Route>
<Route path="/configuration">
<ConfigurationPanel sysinfo={sysinfo}/>
</Route>
<Route path="/status">
<StatusPage sysinfo={sysinfo} data={data}/>
</Route>
<Route path="/mqtt-ca">
<FileUploadComponent title="CA" action="/mqtt-ca"/>
</Route>
<Route path="/mqtt-cert">
<FileUploadComponent title="certificate" action="/mqtt-cert"/>
</Route>
<Route path="/mqtt-key">
<FileUploadComponent title="private key" action="/mqtt-key"/>
</Route>
<Route path="/consent">
<ConsentComponent sysinfo={sysinfo}/>
</Route>
<Route path="/setup">
<SetupPanel sysinfo={sysinfo}/>
</Route>
<Route path="/vendor">
<VendorPanel sysinfo={sysinfo}/>
</Route>
</Router>
{#if sysinfo.upgrading}
<Mask active=true message="Device is upgrading, please wait"/>
{:else if sysinfo.booting}
{#if sysinfo.trying}
<Mask active=true message="Device is booting, please wait. Trying to reach it on {sysinfo.trying}"/>
{:else}
<Mask active=true message="Device is booting, please wait"/>
{/if}
{/if}
</div>

View File

@@ -1,159 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.gh-logo {
width: 2rem;
height: 2rem;
}
.cnt {
@apply bg-white m-2 p-2 rounded shadow-lg
}
.gwf {
@apply 2xl:col-span-6 xl:col-span-5 lg:col-span-4 md:col-span-3 sm:col-span-2 h-64
}
.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-green {
@apply my-auto bg-green-500 text-green-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded
}
.bd-yellow {
@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-blue {
@apply my-auto bg-blue-500 text-blue-100 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded
}
.bd-gray {
@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
}
.btn-pri-sm {
@apply text-xs py-1 px-2 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;
}

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<title>Amsleser</title>
<g transform="translate(-29.5,-83)">
<circle r="4.8016944" cy="123.56455" cx="55.064552"
style="fill:none;stroke:#045c7c;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 41.298717,103.9049 a 24,24 0 0 1 27.531669,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 35.562952,95.713384 a 34,34 0 0 1 39.003199,-2e-6"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 47.034482,112.09642 a 14,14 0 0 1 16.06014,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="105.99158" cx="38.181862"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="97.959579" cx="77.491386"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 499.36" focusable="false">
<title>GitHub</title>
<path d="M256 0C114.64 0 0 114.61 0 256c0 113.09 73.34 209 175.08 242.9 12.8 2.35 17.47-5.56 17.47-12.34 0-6.08-.22-22.18-.35-43.54-71.2 15.49-86.2-34.34-86.2-34.34-11.64-29.57-28.42-37.45-28.42-37.45-23.27-15.84 1.73-15.55 1.73-15.55 25.69 1.81 39.21 26.38 39.21 26.38 22.84 39.12 59.92 27.82 74.5 21.27 2.33-16.54 8.94-27.82 16.25-34.22-56.84-6.43-116.6-28.43-116.6-126.49 0-27.95 10-50.8 26.35-68.69-2.63-6.48-11.42-32.5 2.51-67.75 0 0 21.49-6.88 70.4 26.24a242.65 242.65 0 0 1 128.18 0c48.87-33.13 70.33-26.24 70.33-26.24 14 35.25 5.18 61.27 2.55 67.75 16.41 17.9 26.31 40.75 26.31 68.69 0 98.35-59.85 120-116.88 126.32 9.19 7.9 17.38 23.53 17.38 47.41 0 34.22-.31 61.83-.31 70.23 0 6.85 4.61 14.81 17.6 12.31C438.72 464.97 512 369.08 512 256.02 512 114.62 397.37 0 256 0z" fill="#f8f9fa" fill-rule="evenodd"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,68 +0,0 @@
<script>
import { fmtnum } from "./Helpers";
export let data;
export let currency;
export let hasExport;
let cols = 3
$: {
cols = currency ? 3 : 2;
}
</script>
<div class="mx-2 text-sm">
<strong>Real time calculation</strong>
<br/><br/>
{#if data}
{#if hasExport}
<strong>Import</strong>
<div class="grid grid-cols-{cols} mb-3">
<div>Hour</div>
<div class="text-right">{fmtnum(data.h.u,2)} kWh</div>
{#if currency}<div class="text-right">{fmtnum(data.h.c,2)} {currency}</div>{/if}
<div>Day</div>
<div class="text-right">{fmtnum(data.d.u,1)} kWh</div>
{#if currency}<div class="text-right">{fmtnum(data.d.c,1)} {currency}</div>{/if}
<div>Month</div>
<div class="text-right">{fmtnum(data.m.u)} kWh</div>
{#if currency}<div class="text-right">{fmtnum(data.m.c)} {currency}</div>{/if}
</div>
<strong>Export</strong>
<div class="grid grid-cols-{cols}">
<div>Hour</div>
<div class="text-right">{fmtnum(data.h.p,2)} kWh</div>
{#if currency}<div class="text-right">{fmtnum(data.h.i,2)} {currency}</div>{/if}
<div>Day</div>
<div class="text-right">{fmtnum(data.d.p,1)} kWh</div>
{#if currency}<div class="text-right">{fmtnum(data.d.i,1)} {currency}</div>{/if}
<div>Month</div>
<div class="text-right">{fmtnum(data.m.p)} kWh</div>
{#if currency}<div class="text-right">{fmtnum(data.m.i)} {currency}</div>{/if}
</div>
{:else}
<strong>Consumption</strong>
<div class="grid grid-cols-2 mb-3">
<div>Hour</div>
<div class="text-right">{fmtnum(data.h.u,2)} kWh</div>
<div>Day</div>
<div class="text-right">{fmtnum(data.d.u,1)} kWh</div>
<div>Month</div>
<div class="text-right">{fmtnum(data.m.u)} kWh</div>
</div>
{#if currency}
<strong>Cost</strong>
<div class="grid grid-cols-2">
<div>Hour</div>
<div class="text-right">{fmtnum(data.h.c,2)} {currency}</div>
<div>Day</div>
<div class="text-right">{fmtnum(data.d.c,1)} {currency}</div>
<div>Month</div>
<div class="text-right">{fmtnum(data.m.c)} {currency}</div>
</div>
{/if}
{/if}
{/if}
</div>

View File

@@ -1,58 +0,0 @@
<script>
import BarChart from './BarChart.svelte';
import { ampcol, fmtnum } from './Helpers.js';
export let u1;
export let u2;
export let u3;
export let i1;
export let i2;
export let i3;
export let max;
let config = {};
function point(v) {
return {
label: fmtnum(v) +'A',
value: isNaN(v) ? 0 : v,
color: ampcol(v ? (v)/(max)*100 : 0)
};
};
$: {
let xTicks = [];
let points = [];
if(u1 > 0) {
xTicks.push({ label: 'L1' });
points.push(point(i1));
}
if(u2 > 0) {
xTicks.push({ label: 'L2' });
points.push(point(i2));
}
if(u3 > 0) {
xTicks.push({ label: 'L3' });
points.push(point(i3));
}
config = {
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: 0,
max: max,
ticks: [
{ value: 0, label: '0%' },
{ value: max/4, label: '25%' },
{ value: max/2, label: '50%' },
{ value: (max/4)*3, label: '75%' },
{ value: max, label: '100%' }
]
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<BarChart config={config} />

View File

@@ -1,6 +0,0 @@
<script>
export let color;
export let title;
export let text;
</script>
<span title={title} class="bd-{color}">{text}</span>

View File

@@ -1,106 +0,0 @@
<script>
export let config;
let width;
let height;
let barWidth;
let xScale;
let yScale;
let heightAvailable;
let labelOffset;
$: {
heightAvailable = height-(config.title ? 20 : 0);
let innerWidth = width - (config.padding.left + config.padding.right);
barWidth = innerWidth / config.points.length;
labelOffset = barWidth < 25 ? 28 : 17;
let yPerUnit = (heightAvailable-config.padding.top-config.padding.bottom)/(config.y.max-config.y.min);
xScale = function(i) {
return (i*barWidth)+config.padding.left;
};
yScale = function(i) {
let ret = 0;
if(i > config.y.max)
ret = config.padding.bottom;
else if(i < config.y.min)
ret = heightAvailable-config.padding.bottom;
else
ret = heightAvailable-config.padding.bottom-((i-config.y.min)*yPerUnit);
return ret > heightAvailable || ret < 0.0 ? 0.0 : ret;
};
};
</script>
<div class="chart" bind:clientWidth={width} bind:clientHeight={height}>
{#if config.title}
<strong class="text-sm">{config.title}</strong>
{/if}
<svg height="{heightAvailable}">
<!-- y axis -->
<g class="axis y-axis">
{#each config.y.ticks as tick}
<g class="tick tick-{tick.value} tick-{tick.color}" transform="translate(0, {yScale(tick.value)})">
<line x2="100%"></line>
<text y="-4" x={tick.align == 'right' ? '85%' : ''}>{tick.label}</text>
</g>
{/each}
</g>
<!-- x axis -->
<g class="axis x-axis">
{#each config.x.ticks as point, i}
<g class="tick" transform="translate({xScale(i)},{heightAvailable})">
<text x="{barWidth/2}" y="-4">{point.label}</text>
</g>
{/each}
</g>
<g class='bars'>
{#each config.points as point, i}
{#if point.value !== undefined}
<rect
x="{xScale(i) + 2}"
y="{yScale(point.value)}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(Math.min(config.y.min, 0) + point.value)}"
fill="{point.color}"
/>
{#if barWidth > 15}
<text
y="{yScale(point.value) > yScale(0)-labelOffset ? yScale(point.value) - labelOffset : yScale(point.value)+10}"
x="{xScale(i) + barWidth/2}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{barWidth < 25 ? 'left' : 'middle'}"
fill="{yScale(point.value) > yScale(0)-labelOffset ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value) > yScale(0)-labelOffset ? yScale(point.value) - labelOffset : yScale(point.value)+9})"
>{point.label}</text>
{/if}
{/if}
{#if point.value2 > 0.0001}
<rect
x="{xScale(i) + 2}"
y="{yScale(0)}"
width="{barWidth - 4}"
height="{yScale(config.y.min) - yScale(config.y.min + point.value2)}"
fill="{point.color}"
/>
{#if barWidth > 15}
<text
y="{yScale(-point.value2) < yScale(0)+12 ? yScale(-point.value2) + 12 : yScale(-point.value2)-10}"
x="{xScale(i) + barWidth/2}"
width="{barWidth - 4}"
dominant-baseline="middle"
text-anchor="{barWidth < 25 ? 'left' : 'middle'}"
fill="{yScale(-point.value2) < yScale(0)+12 ? point.color : 'white'}"
transform="rotate({barWidth < 25 ? 90 : 0}, {xScale(i) + (barWidth/2)}, {yScale(point.value2 - config.y.min) > yScale(0)-12 ? yScale(point.value2 - config.y.min) - 12 : yScale(point.value2 - config.y.min)+9})"
>{point.label2}</text>
{/if}
{/if}
{/each}
</g>
</svg>
</div>

View File

@@ -1,54 +0,0 @@
<script>
import {boardtype} from './Helpers.js'
export let chip;
</script>
<option value={-1}></option>
{#if chip == 'esp8266'}
<optgroup label="amsleser.no">
<option value={7}>{boardtype(chip, 7)}</option>
<option value={5}>{boardtype(chip, 5)}</option>
<option value={4}>{boardtype(chip, 4)}</option>
<option value={3}>{boardtype(chip, 3)}</option>
</optgroup>
<optgroup label="Custom hardware">
<option value={2}>{boardtype(chip, 2)}</option>
<option value={1}>{boardtype(chip, 1)}</option>
<option value={0}>{boardtype(chip, 0)}</option>
</optgroup>
<optgroup label="Generic hardware">
<option value={101}>{boardtype(chip, 101)}</option>
<option value={100}>{boardtype(chip, 100)}</option>
</optgroup>
{/if}
{#if chip == 'esp32'}
<optgroup label="Generic hardware">
<option value={201}>{boardtype(chip, 201)}</option>
<option value={202}>{boardtype(chip, 202)}</option>
<option value={203}>{boardtype(chip, 203)}</option>
<option value={200}>{boardtype(chip, 200)}</option>
</optgroup>
{/if}
{#if chip == 'esp32s2'}
<optgroup label="amsleser.no">
<option value={7}>{boardtype(chip, 7)}</option>
<option value={6}>{boardtype(chip, 6)}</option>
<option value={5}>{boardtype(chip, 5)}</option>
</optgroup>
<optgroup label="Generic hardware">
<option value={51}>{boardtype(chip, 51)}</option>
<option value={50}>{boardtype(chip, 50)}</option>
</optgroup>
{/if}
{#if chip == 'esp32c3'}
<optgroup label="Generic hardware">
<option value={71}>{boardtype(chip, 71)}</option>
<option value={70}>{boardtype(chip, 70)}</option>
</optgroup>
{/if}
{#if chip == 'esp32solo'}
<optgroup label="Generic hardware">
<option value={200}>{boardtype(chip, 200)}</option>
</optgroup>
{/if}

View File

@@ -1,11 +0,0 @@
<script>
import { zeropad, monthnames } from './Helpers.js';
export let timestamp;
</script>
{#if Math.abs(new Date().getTime()-timestamp.getTime()) < 300000 }
{`${zeropad(timestamp.getDate())}. ${monthnames[timestamp.getMonth()]} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`}
{:else}
<span class="text-red-500">{`${zeropad(timestamp.getDate())}.${zeropad(timestamp.getMonth())}.${timestamp.getFullYear()} ${zeropad(timestamp.getHours())}:${zeropad(timestamp.getMinutes())}`}</span>
{/if}

View File

@@ -1,691 +0,0 @@
<script>
import { getConfiguration, configurationStore } from './ConfigurationStore'
import { sysinfoStore } from './DataStores.js';
import { wiki } from './Helpers.js';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
import Badge from './Badge.svelte';
import HelpIcon from './HelpIcon.svelte';
import CountrySelectOptions from './CountrySelectOptions.svelte';
import { Link, navigate } from 'svelte-navigator';
import SubnetOptions from './SubnetOptions.svelte';
export let sysinfo = {}
let uiElements = [{
name: 'Import gauge',
key: 'i'
},{
name: 'Export gauge',
key: 'e'
},{
name: 'Voltage',
key: 'v'
},{
name: 'Amperage',
key: 'a'
},{
name: 'Reactive',
key: 'r'
},{
name: 'Realtime',
key: 'c'
},{
name: 'Peaks',
key: 't'
},{
name: 'Price',
key: 'p'
},{
name: 'Day plot',
key: 'd'
},{
name: 'Month plot',
key: 'm'
},{
name: 'Temperature plot',
key: 's'
}];
let loading = true;
let saving = false;
let configuration = {
g: {
t: '', h: '', s: 0, u: '', p: ''
},
m: {
b: 2400, p: 11, i: false, d: 0, f: 0, r: 0,
e: { e: false, k: '', a: '' },
m: { e: false, w: false, v: false, a: false, c: false }
},
w: { s: '', p: '', w: 0.0, z: 255, a: true },
n: {
m: '', i: '', s: '', g: '', d1: '', d2: '', d: false, n1: '', n2: '', h: false
},
q: {
h: '', p: 1883, u: '', a: '', b: '',
s: { e: false, c: false, r: true, k: false }
},
o: {
e: '',
c: '',
u1: '',
u2: '',
u3: ''
},
t: {
t: [0,0,0,0,0,0,0,0,0,0], h: 1
},
p: {
e: false, t: '', r: '', c: '', m: 1.0
},
d: {
s: false, t: false, l: 5
},
u: {
i: 0, e: 0, v: 0, a: 0, r: 0, c: 0, t: 0, p: 0, d: 0, m: 0, s: 0
},
i: {
h: null, a: null,
l: { p: null, i: false },
r: { r: null, g: null, b: null, i: false },
t: { d: null, a: null },
v: { p: null, d: { v: null, g: null }, o: null, m: null, b: null }
}
};
configurationStore.subscribe(update => {
if(update.version) {
configuration = update;
loading = false;
}
});
getConfiguration();
let isFactoryReset = false;
let isFactoryResetComplete = false;
async function factoryReset() {
if(confirm("Are you sure you want to factory reset the device?")) {
isFactoryReset = true;
const data = new URLSearchParams();
data.append("perform", "true");
const response = await fetch('/reset', {
method: 'POST',
body: data
});
let res = (await response.json());
isFactoryReset = false;
isFactoryResetComplete = res.success;
}
}
async function handleSubmit(e) {
saving = true;
const formData = new FormData(e.target);
const data = new URLSearchParams();
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
sysinfoStore.update(s => {
s.booting = res.reboot;
s.ui = configuration.u;
return s;
});
saving = false;
navigate("/");
}
async function reboot() {
const response = await fetch('/reboot', {
method: 'POST'
});
let res = (await response.json())
}
const askReboot = function() {
if(confirm('Are you sure you want to reboot the device?')) {
sysinfoStore.update(s => {
s.booting = true;
return s;
});
reboot();
}
}
const updateMqttPort = function() {
if(configuration.q.s.e) {
if(configuration.q.p == 1883) configuration.q.p = 8883;
} else {
if(configuration.q.p == 8883) configuration.q.p = 1883;
}
}
let gpioMax = 44;
$: {
gpioMax = sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39;
}
</script>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong class="text-sm">General</strong>
<a href="{wiki('General-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="g" value="true"/>
<div class="my-1">
<div class="flex">
<div>
Hostname<br/>
<input name="gh" bind:value={configuration.g.h} type="text" class="in-f w-full" pattern="[A-Za-z0-9-]+"/>
</div>
<div>
Time zone<br/>
<select name="gt" bind:value={configuration.g.t} class="in-l w-full">
<CountrySelectOptions/>
</select>
</div>
</div>
</div>
<input type="hidden" name="p" value="true"/>
<div class="my-1">
Price region<br/>
<select name="pr" bind:value={configuration.p.r} class="in-s">
<optgroup label="Norway">
<option value="10YNO-1--------2">NO1</option>
<option value="10YNO-2--------T">NO2</option>
<option value="10YNO-3--------J">NO3</option>
<option value="10YNO-4--------9">NO4</option>
<option value="10Y1001A1001A48H">NO5</option>
</optgroup>
<optgroup label="Sweden">
<option value="10Y1001A1001A44P">SE1</option>
<option value="10Y1001A1001A45N">SE2</option>
<option value="10Y1001A1001A46L">SE3</option>
<option value="10Y1001A1001A47J">SE4</option>
</optgroup>
<optgroup label="Denmark">
<option value="10YDK-1--------W">DK1</option>
<option value="10YDK-2--------M">DK2</option>
</optgroup>
<option value="10YAT-APG------L">Austria</option>
<option value="10YBE----------2">Belgium</option>
<option value="10YCZ-CEPS-----N">Czech Republic</option>
<option value="10Y1001A1001A39I">Estonia</option>
<option value="10YFI-1--------U">Finland</option>
<option value="10YFR-RTE------C">France</option>
<option value="10Y1001A1001A83F">Germany</option>
<option value="10YGB----------A">Great Britain</option>
<option value="10YLV-1001A00074">Latvia</option>
<option value="10YLT-1001A0008Q">Lithuania</option>
<option value="10YNL----------L">Netherland</option>
<option value="10YPL-AREA-----S">Poland</option>
<option value="10YCH-SWISSGRIDZ">Switzerland</option>
</select>
</div>
<div class="my-1">
<div class="flex">
<div class="w-1/2">
Currency<br/>
<select name="pc" bind:value={configuration.p.c} class="in-f w-full">
{#each ["NOK","SEK","DKK","EUR"] as c}
<option value={c}>{c}</option>
{/each}
</select>
</div>
<div class="w-1/2">
Multiplier<br/>
<input name="pm" bind:value={configuration.p.m} type="number" min="0.001" max="1000" step="0.001" class="in-l tr w-full"/>
</div>
</div>
</div>
<div class="my-1">
<label><input type="checkbox" name="pe" value="true" bind:checked={configuration.p.e} class="rounded mb-1"/> Enable price fetch from remote server</label>
{#if configuration.p.e && sysinfo.chip != 'esp8266'}
<br/><input name="pt" bind:value={configuration.p.t} type="text" class="in-s" placeholder="ENTSO-E API key, optional, read docs"/>
{/if}
</div>
<div class="my-1">
Security<br/>
<select name="gs" bind:value={configuration.g.s} class="in-s">
<option value={0}>None</option>
<option value={1}>Only configuration</option>
<option value={2}>Everything</option>
</select>
</div>
{#if configuration.g.s > 0}
<div class="my-1">
Username<br/>
<input name="gu" bind:value={configuration.g.u} type="text" class="in-s"/>
</div>
<div class="my-1">
Password<br/>
<input name="gp" bind:value={configuration.g.p} type="password" class="in-s"/>
</div>
{/if}
</div>
<div class="cnt">
<strong class="text-sm">Meter</strong>
<a href="{wiki('Meter-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="m" value="true"/>
<div class="my-1">
<span>Serial configuration</span>
<div class="flex">
<select name="mb" bind:value={configuration.m.b} class="in-f">
<option value={0} disabled={configuration.m.b != 0}>Autodetect</option>
{#each [24,48,96,192,384,576,1152] as b}
<option value={b*100}>{b*100}</option>
{/each}
</select>
<select name="mp" bind:value={configuration.m.p} class="in-l" disabled={configuration.m.b == 0}>
<option value={0} disabled={configuration.m.b != 0}>-</option>
<option value={2}>7N1</option>
<option value={3}>8N1</option>
<option value={10}>7E1</option>
<option value={11}>8E1</option>
</select>
<label class="mt-2 ml-3 whitespace-nowrap"><input name="mi" value="true" bind:checked={configuration.m.i} type="checkbox" class="rounded mb-1"/> inverted</label>
</div>
</div>
<div class="my-1">
Voltage<br/>
<select name="md" bind:value={configuration.m.d} class="in-s">
<option value={2}>400V (TN)</option>
<option value={1}>230V (IT/TT)</option>
</select>
</div>
<div class="my-1 flex">
<div class="mx-1">
Main fuse<br/>
<label class="flex">
<input name="mf" bind:value={configuration.m.f} type="number" min="5" max="65535" class="in-f tr w-full"/>
<span class="in-post">A</span>
</label>
</div>
<div class="mx-1">
Production<br/>
<label class="flex">
<input name="mr" bind:value={configuration.m.r} type="number" min="0" max="65535" class="in-f tr w-full"/>
<span class="in-post">kWp</span>
</label>
</div>
</div>
<div class="my-1">
</div>
<div class="my-1">
<label><input type="checkbox" name="me" value="true" bind:checked={configuration.m.e.e} class="rounded mb-1"/> Meter is encrypted</label>
{#if configuration.m.e.e}
<br/><input name="mek" bind:value={configuration.m.e.k} type="text" class="in-s"/>
{/if}
</div>
{#if configuration.m.e.e}
<div class="my-1">
Authentication key<br/>
<input name="mea" bind:value={configuration.m.e.a} type="text" class="in-s"/>
</div>
{/if}
<label><input type="checkbox" name="mm" value="true" bind:checked={configuration.m.m.e} class="rounded mb-1"/> Multipliers</label>
{#if configuration.m.m.e}
<div class="flex my-1">
<div class="w-1/4">
Watt<br/>
<input name="mmw" bind:value={configuration.m.m.w} type="number" min="0.00" max="655.35" step="0.01" class="in-f tr w-full"/>
</div>
<div class="w-1/4">
Volt<br/>
<input name="mmv" bind:value={configuration.m.m.v} type="number" min="0.00" max="655.35" step="0.01" class="in-m tr w-full"/>
</div>
<div class="w-1/4">
Amp<br/>
<input name="mma" bind:value={configuration.m.m.a} type="number" min="0.00" max="655.35" step="0.01" class="in-m tr w-full"/>
</div>
<div class="w-1/4">
kWh<br/>
<input name="mmc" bind:value={configuration.m.m.c} type="number" min="0.00" max="655.35" step="0.01" class="in-l tr w-full"/>
</div>
</div>
{/if}
</div>
<div class="cnt">
<strong class="text-sm">WiFi</strong>
<a href="{wiki('WiFi-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="w" value="true"/>
<div class="my-1">
SSID<br/>
<input name="ws" bind:value={configuration.w.s} type="text" class="in-s"/>
</div>
<div class="my-1">
Password<br/>
<input name="wp" bind:value={configuration.w.p} type="password" class="in-s"/>
</div>
<div class="my-1 flex">
<div class="w-1/2">
Power saving<br/>
<select name="wz" bind:value={configuration.w.z} class="in-s">
<option value={255}>Default</option>
<option value={0}>Off</option>
<option value={1}>Minimum</option>
<option value={2}>Maximum</option>
</select>
</div>
<div class="ml-2 w-1/2">
Power<br/>
<div class="flex">
<input name="ww" bind:value={configuration.w.w} type="number" min="0" max="20.5" step="0.5" class="in-f tr w-full"/>
<span class="in-post">dBm</span>
</div>
</div>
</div>
<div class="my-3">
<label><input type="checkbox" name="wa" value="true" bind:checked={configuration.w.a} class="rounded mb-1"/> Auto reboot on connection problem</label>
</div>
</div>
<div class="cnt">
<strong class="text-sm">Network</strong>
<a href="{wiki('Network-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<div class="my-1">
IP<br/>
<div class="flex">
<select name="nm" bind:value={configuration.n.m} class="in-f">
<option value="dhcp">DHCP</option>
<option value="static">Static</option>
</select>
<input name="ni" bind:value={configuration.n.i} type="text" class="in-m w-full" disabled={configuration.n.m == 'dhcp'} required={configuration.n.m == 'static'}/>
<select name="ns" bind:value={configuration.n.s} class="in-l" disabled={configuration.n.m == 'dhcp'} required={configuration.n.m == 'static'}>
<SubnetOptions/>
</select>
</div>
</div>
{#if configuration.n.m == 'static'}
<div class="my-1">
Gateway<br/>
<input name="ng" bind:value={configuration.n.g} type="text" class="in-s"/>
</div>
<div class="my-1">
DNS<br/>
<div class="flex">
<input name="nd1" bind:value={configuration.n.d1} type="text" class="in-f w-full"/>
<input name="nd2" bind:value={configuration.n.d2} type="text" class="in-l w-full"/>
</div>
</div>
{/if}
<div class="my-1">
<label><input name="nd" value="true" bind:checked={configuration.n.d} type="checkbox" class="rounded mb-1"/> enable mDNS</label>
</div>
<input type="hidden" name="ntp" value="true"/>
<div class="my-1">
NTP <label class="ml-4"><input name="ntpd" value="true" bind:checked={configuration.n.h} type="checkbox" class="rounded mb-1"/> obtain from DHCP</label><br/>
<div class="flex">
<input name="ntph" bind:value={configuration.n.n1} type="text" class="in-s"/>
</div>
</div>
</div>
<div class="cnt">
<strong class="text-sm">MQTT</strong>
<a href="{wiki('MQTT-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="q" value="true"/>
<div class="my-1">
Server
{#if sysinfo.chip != 'esp8266'}
<label class="float-right mr-3"><input type="checkbox" name="qs" value="true" bind:checked={configuration.q.s.e} class="rounded mb-1" on:change={updateMqttPort}/> SSL</label>
{/if}
<br/>
<div class="flex">
<input name="qh" bind:value={configuration.q.h} type="text" class="in-f w-3/4"/>
<input name="qp" bind:value={configuration.q.p} type="number" min="1024" max="65535" class="in-l tr w-1/4"/>
</div>
</div>
{#if configuration.q.s.e}
<div class="my-1">
<div>
<Link to="/mqtt-ca">
{#if configuration.q.s.c}
<Badge color="green" text="CA OK" title="Click here to replace CA"/>
{:else}
<Badge color="blue" text="Upload CA" title="Click here to upload CA"/>
{/if}
</Link>
<Link to="/mqtt-cert">
{#if configuration.q.s.r}
<Badge color="green" text="Cert OK" title="Click here to replace certificate"/>
{:else}
<Badge color="blue" text="Upload cert" title="Click here to upload certificate"/>
{/if}
</Link>
<Link to="/mqtt-key">
{#if configuration.q.s.k}
<Badge color="green" text="Key OK" title="Click here to replace key"/>
{:else}
<Badge color="blue" text="Upload key" title="Click here to upload key"/>
{/if}
</Link>
</div>
</div>
{/if}
<div class="my-1">
Username<br/>
<input name="qu" bind:value={configuration.q.u} type="text" class="in-s"/>
</div>
<div class="my-1">
Password<br/>
<input name="qa" bind:value={configuration.q.a} type="password" class="in-s"/>
</div>
<div class="my-1 flex">
<div>
Client ID<br/>
<input name="qc" bind:value={configuration.q.c} type="text" class="in-f w-full"/>
</div>
<div>
Payload<br/>
<select name="qm" bind:value={configuration.q.m} class="in-l">
<option value={0}>JSON</option>
<option value={1}>Raw (minimal)</option>
<option value={2}>Raw (full)</option>
<option value={3}>Domoticz</option>
<option value={4}>HomeAssistant</option>
<option value={255}>HEX dump</option>
</select>
</div>
</div>
<div class="my-1">
Publish topic<br/>
<input name="qb" bind:value={configuration.q.b} type="text" class="in-s"/>
</div>
</div>
{#if configuration.q.m == 3}
<div class="cnt">
<strong class="text-sm">Domoticz</strong>
<a href="{wiki('MQTT-configuration#domoticz')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="o" value="true"/>
<div class="my-1 flex">
<div class="w-1/2">
Electricity IDX<br/>
<input name="oe" bind:value={configuration.o.e} type="text" class="in-f tr w-full"/>
</div>
<div class="w-1/2">
Current IDX<br/>
<input name="oc" bind:value={configuration.o.c} type="text" class="in-l tr w-full"/>
</div>
</div>
<div class="my-1">
Voltage IDX: L1, L2 & L3
<div class="flex">
<input name="ou1" bind:value={configuration.o.u1} type="text" class="in-f tr w-1/3"/>
<input name="ou2" bind:value={configuration.o.u2} type="text" class="in-m tr w-1/3"/>
<input name="ou3" bind:value={configuration.o.u3} type="text" class="in-l tr w-1/3"/>
</div>
</div>
</div>
{/if}
{#if configuration.p.r.startsWith("10YNO") || configuration.p.r == '10Y1001A1001A48H'}
<div class="cnt">
<strong class="text-sm">Tariff thresholds</strong>
<a href="{wiki('Threshold-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="t" value="true"/>
<div class="flex flex-wrap my-1">
{#each {length: 9} as _, i}
<label class="flex w-40 m-1">
<span class="in-pre">{i+1}</span>
<input name="t{i}" bind:value={configuration.t.t[i]} type="number" min="0" max="65535" class="in-txt w-full"/>
<span class="in-post">kWh</span>
</label>
{/each}
</div>
<label class="flex m-1">
<span class="in-pre">Average of</span>
<input name="th" bind:value={configuration.t.h} type="number" min="0" max="255" class="in-txt tr w-full"/>
<span class="in-post">hours</span>
</label>
</div>
{/if}
<div class="cnt">
<strong class="text-sm">User interface</strong>
<a href="{wiki('User-interface')}" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="u" value="true"/>
<div class="flex flex-wrap">
{#each uiElements as el}
<div class="w-1/2">
{el.name}<br/>
<select name="u{el.key}" bind:value={configuration.u[el.key]} class="in-s">
<option value={0}>Hide</option>
<option value={1}>Show</option>
<option value={2}>Dynamic</option>
</select>
</div>
{/each}
</div>
</div>
{#if sysinfo.board > 20 || sysinfo.chip == 'esp8266'}
<div class="cnt">
<strong class="text-sm">Hardware</strong>
<a href="{wiki('GPIO-configuration')}" target="_blank" class="float-right"><HelpIcon/></a>
{#if sysinfo.board > 20}
<input type="hidden" name="i" value="true"/>
<div class="flex flex-wrap">
<div class="w-1/3">
HAN<br/>
<select name="ih" bind:value={configuration.i.h} class="in-f w-full">
<UartSelectOptions chip={sysinfo.chip}/>
</select>
</div>
<div class="w-1/3">
AP button<br/>
<input name="ia" bind:value={configuration.i.a} type="number" min="0" max={gpioMax} class="in-m tr w-full"/>
</div>
<div class="w-1/3">
LED<label class="ml-4"><input name="ili" value="true" bind:checked={configuration.i.l.i} type="checkbox" class="rounded mb-1"/> inv</label><br/>
<div class="flex">
<input name="ilp" bind:value={configuration.i.l.p} type="number" min="0" max={gpioMax} class="in-l tr w-full"/>
</div>
</div>
<div class="w-full">
RGB<label class="ml-4"><input name="iri" value="true" bind:checked={configuration.i.r.i} type="checkbox" class="rounded mb-1"/> inverted</label><br/>
<div class="flex">
<input name="irr" bind:value={configuration.i.r.r} type="number" min="0" max={gpioMax} class="in-f tr w-1/3"/>
<input name="irg" bind:value={configuration.i.r.g} type="number" min="0" max={gpioMax} class="in-m tr w-1/3"/>
<input name="irb" bind:value={configuration.i.r.b} type="number" min="0" max={gpioMax} class="in-l tr w-1/3"/>
</div>
</div>
<div class="my-1 w-1/3">
Temperature<br/>
<input name="itd" bind:value={configuration.i.t.d} type="number" min="0" max={gpioMax} class="in-f tr w-full"/>
</div>
<div class="my-1 pr-1 w-1/3">
Analog temp<br/>
<input name="ita" bind:value={configuration.i.t.a} type="number" min="0" max={gpioMax} class="in-l tr w-full"/>
</div>
{#if sysinfo.chip != 'esp8266'}
<div class="my-1 pl-1 w-1/3">
Vcc<br/>
<input name="ivp" bind:value={configuration.i.v.p} type="number" min="0" max={gpioMax} class="in-s tr w-full"/>
</div>
{/if}
{#if configuration.i.v.p > 0}
<div class="my-1">
Voltage divider<br/>
<div class="flex">
<input name="ivdv" bind:value={configuration.i.v.d.v} type="number" min="0" max="65535" class="in-f tr w-full" placeholder="VCC"/>
<input name="ivdg" bind:value={configuration.i.v.d.g} type="number" min="0" max="65535" class="in-l tr w-full" placeholder="GND"/>
</div>
</div>
{/if}
</div>
{/if}
{#if sysinfo.chip == 'esp8266'}
<input type="hidden" name="iv" value="true"/>
<div class="my-1 flex flex-wrap">
<div class="w-1/3">
Vcc offset<br/>
<input name="ivo" bind:value={configuration.i.v.o} type="number" min="0.0" max="3.5" step="0.01" class="in-f tr w-full"/>
</div>
<div class="w-1/3 pr-1">
Multiplier<br/>
<input name="ivm" bind:value={configuration.i.v.m} type="number" min="0.1" max="10" step="0.01" class="in-l tr w-full"/>
</div>
{#if sysinfo.board == 2 || sysinfo.board == 100}
<div class="w-1/3 pl-1">
Boot limit<br/>
<input name="ivb" bind:value={configuration.i.v.b} type="number" min="2.5" max="3.5" step="0.1" class="in-s tr w-full"/>
</div>
{/if}
</div>
{/if}
</div>
{/if}
<div class="cnt">
<strong class="text-sm">Debugging</strong>
<a href="https://amsleser.no/blog/post/24-telnet-debug" target="_blank" class="float-right"><HelpIcon/></a>
<input type="hidden" name="d" value="true"/>
<div class="mt-3">
<label><input type="checkbox" name="ds" value="true" bind:checked={configuration.d.s} class="rounded mb-1"/> Enable debugging</label>
</div>
{#if configuration.d.s}
<div class="bd-red">Debug can cause sudden reboots. Do not leave on!</div>
<div class="my-1">
<label><input type="checkbox" name="dt" value="true" bind:checked={configuration.d.t} class="rounded mb-1"/> Enable telnet</label>
</div>
{#if configuration.d.t}
<div class="bd-red">Telnet is unsafe and should be off when not in use</div>
{/if}
<div class="my-1">
<select name="dl" bind:value={configuration.d.l} class="in-s">
<option value={1}>Verbose</option>
<option value={2}>Debug</option>
<option value={3}>Info</option>
<option value={4}>Warning</option>
</select>
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-3">
<div>
<button type="button" on:click={factoryReset} class="py-2 px-4 rounded bg-red-500 text-white ml-2">Factory reset</button>
</div>
<div class="text-center">
<button type="button" on:click={askReboot} class="py-2 px-4 rounded bg-yellow-500 text-white">Reboot</button>
</div>
<div class="text-right">
<button type="submit" class="btn-pri">Save</button>
</div>
</div>
</form>
<Mask active={loading} message="Loading configuration"/>
<Mask active={saving} message="Saving configuration"/>
<Mask active={isFactoryReset} message="Performing factory reset"/>
<Mask active={isFactoryResetComplete} message="Device have been factory reset and switched to AP mode"/>

View File

@@ -1,10 +0,0 @@
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);
};

View File

@@ -1,55 +0,0 @@
<script>
import { sysinfoStore } from './DataStores.js';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
import { wiki } from './Helpers';
export let sysinfo = {}
let loadingOrSaving = false;
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
const data = new URLSearchParams()
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.fwconsent = formData['sf'] === true ? 1 : formData['sf'] === false ? 2 : 0;
s.booting = res.reboot;
return s;
});
navigate("/");
}
</script>
<div class="grid xl:grid-cols-3 lg:grid-cols-2">
<div class="cnt">
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div>
Various permissions we need to do stuff:
</div>
<hr/>
<div class="my-3">
Enable one-click upgrade? (implies data collection)<br/>
<a href="{wiki('Data-collection-on-one-click-firmware-upgrade')}" target="_blank" class="text-blue-600 hover:text-blue-800">Read more</a><br/>
<label><input type="radio" name="sf" value={1} checked={sysinfo.fwconsent === 1} class="rounded m-2" required/> Yes</label><label><input type="radio" name="sf" value={2} checked={sysinfo.fwconsent === 2} class="rounded m-2" required/> No</label><br/>
</div>
<div class="my-3">
<button type="submit" class="btn-pri">Save</button>
</div>
</form>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving preferences"/>

View File

@@ -1,10 +0,0 @@
<script>
let europe = ["Amsterdam","Athens","Belfast","Berlin","Bratislava","Brussels","Bucharest","Budapest","Copenhagen","Dublin",
"Helsinki","Lisbon","Ljubljana","London","Luxembourg","Madrid","Malta","Nicosia","Oslo","Paris","Prague","Riga","Rome",
"Sofia","Stockholm","Tallinn","Vienna","Vilnius","Warsaw","Zagreb","Zurich"];
</script>
<option>GMT</option>
{#each europe as c}
<option>Europe/{c}</option>
{/each}

View File

@@ -1,103 +0,0 @@
<script>
import { pricesStore, dayPlotStore, monthPlotStore, temperaturesStore } from './DataStores.js';
import { ampcol, exportcol, metertype, uiVisibility } from './Helpers.js';
import PowerGauge from './PowerGauge.svelte';
import VoltPlot from './VoltPlot.svelte';
import AmpPlot from './AmpPlot.svelte';
import ReactiveData from './ReactiveData.svelte';
import AccountingData from './AccountingData.svelte';
import PricePlot from './PricePlot.svelte';
import DayPlot from './DayPlot.svelte';
import MonthPlot from './MonthPlot.svelte';
import TemperaturePlot from './TemperaturePlot.svelte';
import TariffPeakChart from './TariffPeakChart.svelte';
export let data = {}
export let sysinfo = {}
let prices = {}
let dayPlot = {}
let monthPlot = {}
let temperatures = {};
pricesStore.subscribe(update => {
prices = update;
});
dayPlotStore.subscribe(update => {
dayPlot = update;
});
monthPlotStore.subscribe(update => {
monthPlot = update;
});
temperaturesStore.subscribe(update => {
temperatures = update;
});
</script>
<div class="grid 2xl:grid-cols-6 xl:grid-cols-5 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2">
{#if uiVisibility(sysinfo.ui.i, data.i)}
<div class="cnt">
<div class="grid grid-cols-2">
<div class="col-span-2">
<PowerGauge val={data.i ? data.i : 0} max={data.im ? data.im : 15000} unit="W" label="Import" sub={data.p} subunit={prices.currency} colorFn={ampcol}/>
</div>
<div>{data.mt ? metertype(data.mt) : '-'}</div>
<div class="text-right">{data.ic ? data.ic.toFixed(1) : '-'} kWh</div>
</div>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.e, data.om || data.e > 0)}
<div class="cnt">
<div class="grid grid-cols-2">
<div class="col-span-2">
<PowerGauge val={data.e ? data.e : 0} max={data.om ? data.om * 1000 : 10000} unit="W" label="Export" colorFn={exportcol}/>
</div>
<div></div>
<div class="text-right">{data.ec ? data.ec.toFixed(1) : '-'} kWh</div>
</div>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.v, data.u1 > 100 || data.u2 > 100 || data.u3 > 100)}
<div class="cnt">
<VoltPlot u1={data.u1} u2={data.u2} u3={data.u3} ds={data.ds}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.a, data.i1 > 0.01 || data.i2 > 0.01 || data.i3 > 0.01)}
<div class="cnt">
<AmpPlot u1={data.u1} u2={data.u2} u3={data.u3} i1={data.i1} i2={data.i2} i3={data.i3} max={data.mf ? data.mf : 32}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.r, data.ri > 0 || data.re > 0 || data.ric > 0 || data.rec > 0)}
<div class="cnt">
<ReactiveData importInstant={data.ri} exportInstant={data.re} importTotal={data.ric} exportTotal={data.rec}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.c, data.ea)}
<div class="cnt">
<AccountingData data={data.ea} currency={prices.currency} hasExport={data.om > 0 || data.e > 0}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.t, data.pr && (data.pr.startsWith("10YNO") || data.pr == '10Y1001A1001A48H'))}
<div class="cnt h-64">
<TariffPeakChart />
</div>
{/if}
{#if uiVisibility(sysinfo.ui.p, (typeof data.p == "number") && !Number.isNaN(data.p))}
<div class="cnt gwf">
<PricePlot json={prices}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.d, dayPlot)}
<div class="cnt gwf">
<DayPlot json={dayPlot} />
</div>
{/if}
{#if uiVisibility(sysinfo.ui.m, monthPlot)}
<div class="cnt gwf">
<MonthPlot json={monthPlot} />
</div>
{/if}
{#if uiVisibility(sysinfo.ui.s, data.t && data.t != -127 && temperatures.c > 1)}
<div class="cnt gwf">
<TemperaturePlot json={temperatures} />
</div>
{/if}
</div>

View File

@@ -1,184 +0,0 @@
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,
trying: null
};
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);
};

View File

@@ -1,101 +0,0 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let i = 0;
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
let offset = -cur.getTimezoneOffset()/60;
for(i = cur.getUTCHours(); i<24; i++) {
let imp = json["i"+zeropad(i)];
let exp = json["e"+zeropad(i)];
if(imp === undefined) imp = 0;
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad((i+offset)%24)
});
points.push({
label: imp.toFixed(1),
value: imp*10,
label2: exp.toFixed(1),
value2: exp*10,
color: '#7c3aed'
});
min = Math.max(min, exp*10);
max = Math.max(max, imp*10);
};
for(i = 0; i < cur.getUTCHours(); i++) {
let imp = json["i"+zeropad(i)];
let exp = json["e"+zeropad(i)];
if(imp === undefined) imp = 0;
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad((i+offset)%24)
});
points.push({
label: imp.toFixed(1),
value: imp*10,
label2: exp.toFixed(1),
value2: exp*10,
color: '#7c3aed'
});
min = Math.max(min, exp*10);
max = Math.max(max, imp*10);
};
let boundary = Math.ceil(Math.max(min, max));
max = boundary;
min = min == 0 ? 0 : boundary*-1;
if(min < 0) {
let yTickDistDown = min/4;
for(i = 1; i < 5; i++) {
let val = (yTickDistDown*i);
yTicks.push({
value: val,
label: (val/10).toFixed(1)
});
}
}
let yTickDistUp = max/4;
for(i = 0; i < 5; i++) {
let val = (yTickDistUp*i);
yTicks.push({
value: val,
label: (val/10).toFixed(1)
});
}
config = {
title: "Energy use last 24 hours (kWh)",
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<BarChart config={config} />

View File

@@ -1,6 +0,0 @@
<script></script>
<!-- Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15M9 12l3 3m0 0l3-3m-3 3V2.25" />
</svg>

View File

@@ -1,22 +0,0 @@
<script>
import Mask from "./Mask.svelte";
export let action;
export let title;
let uploading = false;
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>Upload {title}</strong>
<p class="mb-4">Select a suitable file and click upload</p>
<form action="{action}" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri">Upload</button>
</div>
</form>
</div>
</div>
<Mask active={uploading} message="Uploading file, please wait"/>

View File

@@ -1,7 +0,0 @@
<script></script>
<!-- Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>

View File

@@ -1,100 +0,0 @@
<script>
import { Link } from "svelte-navigator";
import { sysinfoStore, getGitHubReleases, gitHubReleaseStore } from './DataStores.js';
import { upgrade, getNextVersion, upgradeWarningText } from './UpgradeHelper';
import { boardtype, hanError, mqttError, priceError, isBusPowered, wiki, bcol } from './Helpers.js';
import AmsleserSvg from "./../assets/favicon.svg";
import GitHubLogo from './../assets/github.svg';
import Uptime from "./Uptime.svelte";
import Badge from './Badge.svelte';
import Clock from './Clock.svelte';
import GearIcon from './GearIcon.svelte';
import InfoIcon from "./InfoIcon.svelte";
import HelpIcon from "./HelpIcon.svelte";
import DownloadIcon from "./DownloadIcon.svelte";
export let data = {}
let sysinfo = {}
let nextVersion = {};
function askUpgrade() {
if(confirm('Do you want to upgrade this device to ' + nextVersion.tag_name + '?')) {
if(!isBusPowered(sysinfo.board) || confirm(upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board)))) {
sysinfoStore.update(s => {
s.upgrading = true;
return s;
});
upgrade(nextVersion);
}
}
}
sysinfoStore.subscribe(update => {
sysinfo = update;
if(update.fwconsent === 1) {
getGitHubReleases();
}
});
gitHubReleaseStore.subscribe(releases => {
nextVersion = getNextVersion(sysinfo.version, releases);
});
</script>
<nav class="bg-violet-600 p-1 rounded-md mx-2">
<div class="flex flex-wrap space-x-4 text-sm text-gray-300">
<div class="flex text-lg text-gray-100 p-2">
<Link to="/">AMS reader <span>{sysinfo.version}</span></Link>
</div>
<div class="flex-none my-auto p-2 flex space-x-4">
<div class="flex-none my-auto"><Uptime epoch={data.u}/></div>
{#if data.t > -50}
<div class="flex-none my-auto">{ data.t > -50 ? data.t.toFixed(1) : '-' }&deg;C</div>
{/if}
<div class="flex-none my-auto">Free mem: {data.m ? (data.m/1000).toFixed(1) : '-'}kb</div>
</div>
<div class="flex-auto flex-wrap my-auto justify-center p-2">
<Badge title="ESP" text={sysinfo.booting ? 'Booting' : data.v > 2.0 ? data.v.toFixed(2)+"V" : "ESP"} color={bcol(sysinfo.booting ? 2 : data.em)}/>
<Badge title="HAN" text="HAN" color={bcol(sysinfo.booting ? 9 : data.hm)}/>
<Badge title="WiFi" text={data.r ? data.r.toFixed(0)+"dBm" : "WiFi"} color={bcol(sysinfo.booting ? 9 : data.wm)}/>
<Badge title="MQTT" text="MQTT" color={bcol(sysinfo.booting ? 9 : data.mm)}/>
</div>
{#if data.he < 0 || data.he > 0}
<div class="bd-red">{ 'HAN: ' + hanError(data.he) }</div>
{/if}
{#if data.me < 0}
<div class="bd-red">{ 'MQTT: ' + mqttError(data.me) }</div>
{/if}
{#if data.ee > 0 || data.ee < 0}
<div class="bd-red">{ 'PriceAPI: ' + priceError(data.ee) }</div>
{/if}
<div class="flex-auto p-2 flex flex-row-reverse flex-wrap">
<div class="flex-none">
<a class="float-right" href='https://github.com/UtilitechAS/amsreader-firmware' target='_blank' rel="noreferrer" aria-label="GitHub"><img class="gh-logo" src={GitHubLogo} alt="GitHub repo"/></a>
</div>
<div class="flex-none my-auto px-2">
<Clock timestamp={ data.c ? new Date(data.c * 1000) : new Date(0) } />
</div>
{#if sysinfo.vndcfg && sysinfo.usrcfg}
<div class="flex-none px-1 mt-1" title="Configuration">
<Link to="/configuration"><GearIcon/></Link>
</div>
<div class="flex-none px-1 mt-1" title="Device information">
<Link to="/status"><InfoIcon/></Link>
</div>
{/if}
<div class="flex-none px-1 mt-1" title="Documentation">
<a href={wiki('')} target='_blank' rel="noreferrer"><HelpIcon/></a>
</div>
{#if sysinfo.fwconsent === 1 && nextVersion}
<div class="flex-none mr-3 text-yellow-500" title="New version: {nextVersion.tag_name}">
{#if sysinfo.security == 0 || data.a}
<button on:click={askUpgrade} class="flex"><span class="mt-1">New version: {nextVersion.tag_name}</span> <DownloadIcon/></button>
{:else}
<span>New version: {nextVersion.tag_name}</span>
{/if}
</div>
{/if}
</div>
</div>
</nav>

View File

@@ -1,6 +0,0 @@
<script></script>
<!-- Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>

View File

@@ -1,183 +0,0 @@
export let monthnames = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
export function bcol(num) {
return num === 1 ? 'green' : num === 2 ? 'yellow' : num === 3 ? 'red' : 'gray';
}
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 exportcol(pct) {
if(pct > 75) return '#32d900';
else if(pct > 50) return '#77d900';
else if(pct > 25) return '#94d900';
else return '#dcd800';
};
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";
case 70:
return "Generic ESP32-C3";
case 71:
return "ESP32-C3-DevKitM-1";
}
}
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 425:
return "Server says its too early";
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);
}
export function wiki(page) {
return "https://github.com/UtilitechAS/amsreader-firmware/wiki/" + page;
}
export function fmtnum(v,d) {
if(isNaN(v)) return '-';
if(isNaN(d))
d = v < 10 ? 1 : 0;
return v.toFixed(d);
}

View File

@@ -1,6 +0,0 @@
<script></script>
<!-- Heroicons -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>

View File

@@ -1,14 +0,0 @@
<script>
export let active;
export let message;
</script>
{#if active}
<div class="z-50" aria-modal="true">
<div class="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center">
{#if message}
<div class="bg-white m-2 p-3 rounded-md shadow-lg pb-4 text-gray-700 w-96">{message}</div>
{/if}
</div>
</div>
{/if}

View File

@@ -1,103 +0,0 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let i = 0;
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
let lm = new Date();
lm.setDate(0);
for(i = cur.getDate(); i<=lm.getDate(); i++) {
let imp = json["i"+zeropad(i)];
let exp = json["e"+zeropad(i)];
if(imp === undefined) imp = 0;
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad(i)
});
points.push({
label: imp.toFixed(0),
value: imp,
label2: exp.toFixed(0),
value2: exp,
color: '#7c3aed'
});
min = Math.max(min, exp);
max = Math.max(max, imp);
}
for(i = 1; i < cur.getDate(); i++) {
let imp = json["i"+zeropad(i)];
let exp = json["e"+zeropad(i)];
if(imp === undefined) imp = 0;
if(exp === undefined) exp = 0;
xTicks.push({
label: zeropad(i)
});
points.push({
label: imp.toFixed(0),
value: imp,
label2: exp.toFixed(0),
value2: exp,
color: '#7c3aed'
});
min = Math.max(min, exp);
max = Math.max(max, imp);
}
let boundary = Math.ceil(Math.max(min, max)/10)*10;
max = boundary;
min = min == 0 ? 0 : boundary*-1;
if(min < 0) {
let yTickDistDown = min/4;
for(i = 0; i < 5; i++) {
let val = (yTickDistDown*i);
yTicks.push({
value: val,
label: val.toFixed(0)
});
}
}
let yTickDistUp = max/4;
for(i = 0; i < 5; i++) {
let val = (yTickDistUp*i);
yTicks.push({
value: val,
label: val.toFixed(0)
});
}
config = {
title: "Energy use last month (kWh)",
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<BarChart config={config} />

View File

@@ -1,31 +0,0 @@
<script>
import PowerGaugeSvg from './PowerGaugeSvg.svelte';
export let val;
export let max;
export let unit;
export let label;
export let sub = "";
export let subunit = "";
export let colorFn;
let pct = 0;
$: {
pct = (Math.min(val,max)/max) * 100
}
</script>
<div class="pl-root">
<PowerGaugeSvg pct={pct} color={colorFn(pct)}/>
<span class="pl-ov">
<span class="pl-lab">{label}</span>
<br/>
<span class="pl-val">{val}</span>
<span class="pl-unt">{unit}</span>
{#if sub}
<br/>
<span class="pl-sub">{sub}</span>
<span class="pl-snt">{subunit}/kWh</span>
{/if}
</span>
</div>

View File

@@ -1,31 +0,0 @@
<script>
export let pct = 0;
export let color = "red";
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle){
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, arcSweep, 0, end.x, end.y
].join(" ");
return d;
}
</script>
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" height="100%">
<path d="{ describeArc(150, 150, 115, 210, 510) }" stroke="#eee" fill="none" stroke-width="55"/>
<path d="{ describeArc(150, 150, 115, 210, 210 + (300*pct/100)) }" stroke={color} fill="none" stroke-width="55"/>
</svg>

View File

@@ -1,97 +0,0 @@
<script>
import { zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let hour = new Date().getUTCHours();
let i = 0;
let val = 0;
let h = 0;
let d = json["20"] == null ? 2 : 1;
let yTicks = [];
let xTicks = [];
let points = [];
let cur = new Date();
for(i = hour; i<24; i++) {
cur.setUTCHours(i);
val = json[zeropad(h++)];
if(val == null) break;
xTicks.push({
label: zeropad(cur.getHours())
});
points.push({
label: val > 0 ? val.toFixed(d) : '',
value: val > 0 ? Math.abs(val*100) : 0,
label2: val < 0 ? val.toFixed(d) : '',
value2: val < 0 ? Math.abs(val*100) : 0,
color: '#7c3aed'
});
min = Math.min(min, val*100);
max = Math.max(max, val*100);
};
for(i = 0; i < 24; i++) {
cur.setUTCHours(i);
val = json[zeropad(h++)];
if(val == null) break;
xTicks.push({
label: zeropad(cur.getHours())
});
points.push({
label: val > 0 ? val.toFixed(d) : '',
value: val > 0 ? Math.abs(val*100) : 0,
label2: val < 0 ? val.toFixed(d) : '',
value2: val < 0 ? Math.abs(val*100) : 0,
color: '#7c3aed'
});
min = Math.min(min, val*100);
max = Math.max(max, val*100);
};
max = Math.ceil(max);
min = Math.floor(min);
if(min < 0) {
let yTickDistDown = min/4;
for(i = 1; i < 5; i++) {
let val = (yTickDistDown*i);
yTicks.push({
value: val,
label: (val/100).toFixed(2)
});
}
}
let yTickDistUp = max/4;
for(i = 0; i < 5; i++) {
let val = (yTickDistUp*i);
yTicks.push({
value: val,
label: (val/100).toFixed(2)
});
}
config = {
title: "Future energy price (" + json.currency + ")",
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<a href="https://transparency.entsoe.eu/" target="_blank" class="text-xs float-right z-40">Provided by ENTSO-E</a>
<BarChart config={config} />

View File

@@ -1,24 +0,0 @@
<script>
export let importInstant;
export let exportInstant;
export let importTotal;
export let exportTotal;
</script>
<div class="mx-2 text-sm">
<strong>Reactive</strong>
<div class="grid grid-cols-2 mt-4">
<div>Instant in</div>
<div class="text-right">{typeof importInstant !== 'undefined' ? importInstant.toFixed(0) : '-'} VAr</div>
<div>Instant out</div>
<div class="text-right">{typeof exportInstant !== 'undefined' ? exportInstant.toFixed(0) : '-'} VAr</div>
</div>
<div class="grid grid-cols-2 mt-4">
<div>Total in</div>
<div class="text-right">{typeof importTotal !== 'undefined' ? importTotal.toFixed(1) : '-'} kVArh</div>
<div>Total out</div>
<div class="text-right">{typeof exportTotal !== 'undefined' ? exportTotal.toFixed(1) : '-'} kVArh</div>
</div>
</div>

View File

@@ -1,132 +0,0 @@
<script>
import { sysinfoStore } from './DataStores.js';
import Mask from './Mask.svelte'
import SubnetOptions from './SubnetOptions.svelte';
export let sysinfo = {}
let staticIp = false;
let loadingOrSaving = false;
let tries = 0;
function scanForDevice() {
var url = "";
tries++;
var retry = function() {
setTimeout(scanForDevice, 1000);
};
if(sysinfo.net.ip && tries%3 == 0) {
if(!sysinfo.net.ip) {
retry();
return;
};
url = "http://" + sysinfo.net.ip;
} else if(sysinfo.hostname && tries%3 == 1) {
url = "http://" + sysinfo.hostname;
} else if(sysinfo.hostname && tries%3 == 2) {
url = "http://" + sysinfo.hostname + ".local";
} else {
url = "";
}
if(console) console.log("Trying url " + url);
sysinfoStore.update(s => {
s.trying = url;
return s;
});
var xhr = new XMLHttpRequest();
xhr.timeout = 5000;
xhr.addEventListener('abort', retry);
xhr.addEventListener('error', retry);
xhr.addEventListener('timeout', retry);
xhr.addEventListener('load', function(e) {
window.location.href = url ? url : "/";
});
xhr.open("GET", url + "/is-alive", true);
xhr.send();
};
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target);
const data = new URLSearchParams();
for (let field of formData) {
const [key, value] = field;
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.hostname = formData.get('sh');
s.usrcfg = res.success;
s.booting = res.reboot;
if(staticIp) {
s.net.ip = formData.get('si');
s.net.mask = formData.get('su');
s.net.gw = formData.get('sg');
s.net.dns1 = formData.get('sd');
}
setTimeout(scanForDevice, 5000);
return s;
});
}
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
<div class="cnt">
<form on:submit|preventDefault={handleSubmit}>
<input type="hidden" name="s" value="true"/>
<strong class="text-sm">Setup</strong>
<div class="my-3">
SSID<br/>
<input name="ss" type="text" class="in-s" required/>
</div>
<div class="my-3">
PSK<br/>
<input name="sp" type="password" class="in-s" autocomplete="off"/>
</div>
<div>
Hostname
<input name="sh" bind:value={sysinfo.hostname} type="text" class="in-s" maxlength="32" pattern="[a-z0-9_-]+" placeholder="Optional, ex.: ams-reader" autocomplete="off"/>
</div>
<div class="my-3">
<label><input type="checkbox" name="sm" value="static" class="rounded mb-1" bind:checked={staticIp} /> Static IP</label>
{#if staticIp}
<br/>
<div class="flex">
<input name="si" type="text" class="in-f w-full" required={staticIp}/>
<select name="su" class="in-l" required={staticIp}>
<SubnetOptions/>
</select>
</div>
{/if}
</div>
{#if staticIp}
<div class="my-3 flex">
<div>
Gateway<br/>
<input name="sg" type="text" class="in-f w-full"/>
</div>
<div>
DNS<br/>
<input name="sd" type="text" class="in-l w-full"/>
</div>
</div>
{/if}
<div class="my-3">
<button type="submit" class="btn-pri">Save</button>
</div>
</form>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving your configuration to the device"/>

View File

@@ -1,211 +0,0 @@
<script>
import { metertype, boardtype, isBusPowered } from './Helpers.js';
import { getSysinfo, gitHubReleaseStore, sysinfoStore } from './DataStores.js';
import { upgrade, getNextVersion, upgradeWarningText } from './UpgradeHelper';
import DownloadIcon from './DownloadIcon.svelte';
import { Link } from 'svelte-navigator';
import Mask from './Mask.svelte';
export let data;
export let sysinfo;
let cfgItems = [{
name: 'WiFi',
key: 'iw'
},{
name: 'MQTT',
key: 'im'
},{
name: 'Web',
key: 'ie'
},{
name: 'Meter',
key: 'it'
},{
name: 'Thresholds',
key: 'ih'
},{
name: 'GPIO',
key: 'ig'
},{
name: 'Domoticz',
key: 'id'
},{
name: 'NTP',
key: 'in'
},{
name: 'Price API',
key: 'is'
}];
let nextVersion = {};
gitHubReleaseStore.subscribe(releases => {
nextVersion = getNextVersion(sysinfo.version, releases);
if(!nextVersion) {
nextVersion = releases[0];
}
});
function askUpgrade() {
if(confirm('Do you want to upgrade this device to ' + nextVersion.tag_name + '?')) {
if((sysinfo.board != 2 && sysinfo.board != 4 && sysinfo.board != 7) || confirm(upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board)))) {
sysinfoStore.update(s => {
s.upgrading = true;
return s;
});
upgrade(nextVersion);
}
}
}
async function reboot() {
const response = await fetch('/reboot', {
method: 'POST'
});
let res = (await response.json())
}
const askReboot = function() {
if(confirm('Are you sure you want to reboot the device?')) {
sysinfoStore.update(s => {
s.booting = true;
return s;
});
reboot();
}
}
let firmwareFileInput;
let firmwareFiles = [];
let firmwareUploading = false;
let configFileInput;
let configFiles = [];
let configUploading = false;
getSysinfo();
</script>
<div class="grid xl:grid-cols-5 lg:grid-cols-3 md:grid-cols-2">
<div class="cnt">
<strong class="text-sm">Device information</strong>
<div class="my-2">
Chip: {sysinfo.chip}
</div>
<div class="my-2">
Device: <Link to="/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</Link>
</div>
<div class="my-2">
MAC: {sysinfo.mac}
</div>
{#if sysinfo.apmac && sysinfo.apmac != sysinfo.mac}
<div class="my-2">
AP MAC: {sysinfo.apmac}
</div>
{/if}
<div class="my-2">
<Link to="/consent">
<span class="btn-pri-sm">Update consents</span>
</Link>
<button on:click={askReboot} class="text-xs py-1 px-2 rounded bg-yellow-500 text-white mr-3 float-right">Reboot</button>
</div>
</div>
{#if sysinfo.meter}
<div class="cnt">
<strong class="text-sm">Meter</strong>
<div class="my-2">
Manufacturer: {metertype(sysinfo.meter.mfg)}
</div>
<div class="my-2">
Model: {sysinfo.meter.model}
</div>
<div class="my-2">
ID: {sysinfo.meter.id}
</div>
</div>
{/if}
{#if sysinfo.net}
<div class="cnt">
<strong class="text-sm">Network</strong>
<div class="my-2">
IP: {sysinfo.net.ip}
</div>
<div class="my-2">
Mask: {sysinfo.net.mask}
</div>
<div class="my-2">
Gateway: {sysinfo.net.gw}
</div>
<div class="my-2">
DNS: {sysinfo.net.dns1} {#if sysinfo.net.dns2}/ {sysinfo.net.dns2}{/if}
</div>
</div>
{/if}
<div class="cnt">
<strong class="text-sm">Firmware</strong>
<div class="my-2">
Installed version: {sysinfo.version}
</div>
{#if nextVersion}
<div class="my-2 flex">
Latest version:
<a href={nextVersion.html_url} class="ml-2 text-blue-600 hover:text-blue-800" target='_blank' rel="noreferrer">{nextVersion.tag_name}</a>
{#if (sysinfo.security == 0 || data.a) && sysinfo.fwconsent === 1 && nextVersion && nextVersion.tag_name}
<div class="flex-none ml-2 text-green-500" title="Install this version">
<button on:click={askUpgrade}><DownloadIcon/></button>
</div>
{/if}
</div>
{#if sysinfo.fwconsent === 2}
<div class="my-2">
<div class="bd-yellow">You have disabled one-click firmware upgrade, link to self-upgrade is disabled</div>
</div>
{/if}
{/if}
{#if (sysinfo.security == 0 || data.a) && isBusPowered(sysinfo.board) }
<div class="bd-red">
{upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board))}
</div>
{/if}
{#if sysinfo.security == 0 || data.a}
<div class="my-2 flex">
<form action="/firmware" enctype="multipart/form-data" method="post" on:submit={() => firmwareUploading=true} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".bin" bind:this={firmwareFileInput} bind:files={firmwareFiles}>
{#if firmwareFiles.length == 0}
<button type="button" on:click={()=>{firmwareFileInput.click();}} class="btn-pri-sm float-right">Select firmware file for upgrade</button>
{:else}
{firmwareFiles[0].name}
<button type="submit" class="btn-pri-sm float-right">Upload</button>
{/if}
</form>
</div>
{/if}
</div>
{#if sysinfo.security == 0 || data.a}
<div class="cnt">
<strong class="text-sm">Backup & restore</strong>
<form method="get" action="/configfile.cfg" autocomplete="off">
<div class="grid grid-cols-2">
{#each cfgItems as el}
<label class="my-1 mx-3"><input type="checkbox" class="rounded" name="{el.key}" value="true" checked/> {el.name}</label>
{/each}
<label class="my-1 mx-3 col-span-2"><input type="checkbox" class="rounded" name="ic" value="true"/> Include Secrets<br/><small>(SSID, PSK, passwords and tokens)</small></label>
</div>
{#if configFiles.length == 0}
<button type="submit" class="btn-pri-sm float-right">Download</button>
{/if}
</form>
<form action="/configfile" enctype="multipart/form-data" method="post" on:submit={() => configUploading=true} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".cfg" bind:this={configFileInput} bind:files={configFiles}>
{#if configFiles.length == 0}
<button type="button" on:click={()=>{configFileInput.click();}} class="btn-pri-sm">Select file...</button>
{:else}
{configFiles[0].name}
<button type="submit" class="btn-pri-sm">Upload</button>
{/if}
</form>
</div>
{/if}
</div>
<Mask active={firmwareUploading} message="Uploading firmware, please wait"/>
<Mask active={configUploading} message="Uploading configuration, please wait"/>

View File

@@ -1,16 +0,0 @@
<optgroup label="Most common is /24 (255.255.255.0)">
<option value="255.255.255.0">/24</option>
</optgroup>
<optgroup label="Smaller subnets">
<option value="255.255.255.128">/25</option>
<option value="255.255.255.192">/26</option>
<option value="255.255.255.224">/27</option>
<option value="255.255.255.240">/28</option>
<option value="255.255.255.248">/29</option>
</optgroup>
<optgroup label="Larger subnets">
<option value="255.255.254.0">/23</option>
<option value="255.255.252.0">/22</option>
<option value="255.255.0.0">/16</option>
</optgroup>

View File

@@ -1,88 +0,0 @@
<script>
import { monthnames, zeropad } from './Helpers.js';
import BarChart from './BarChart.svelte';
import { tariffStore, getTariff } from './DataStores';
let config = {};
let max = 0;
let min = 0;
let tariffData;
tariffStore.subscribe(update => {
tariffData = update;
});
getTariff();
$: {
let i = 0;
let yTicks = [];
let xTicks = [];
let points = [];
yTicks.push({
value: 0,
label: 0
});
if(tariffData && tariffData.p) {
for(i = 0; i < tariffData.p.length; i++) {
let peak = tariffData.p[i];
points.push({
label: peak.v.toFixed(2),
value: peak.v,
color: '#7c3aed'
});
xTicks.push({
label: peak.d > 0 ? zeropad(peak.d) + "." + monthnames[new Date().getMonth()] : "-"
})
max = Math.max(max, peak.v);
}
}
if(tariffData && tariffData.t) {
for(i = 0; i < tariffData.t.length; i++) {
let val = tariffData.t[i];
if(val >= max) break;
yTicks.push({
value: val,
label: val
});
}
yTicks.push({
label: tariffData.m.toFixed(1),
align: 'right',
color: 'green',
value: tariffData.m,
});
}
if(tariffData && tariffData.c) {
yTicks.push({
label: tariffData.c.toFixed(0),
color: 'orange',
value: tariffData.c,
});
max = Math.max(max, tariffData.c);
}
max = Math.ceil(max);
config = {
title: "Tariff peaks",
padding: { top: 20, right: 35, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
}
</script>
<BarChart config={config} />

View File

@@ -1,66 +0,0 @@
<script>
import BarChart from './BarChart.svelte';
export let json;
let config = {};
let max = 0;
let min = 0;
$: {
let i = 0;
let val = 0;
let yTicks = [];
let xTicks = [];
let points = [];
if(json.s) {
json.s.forEach((obj, i) => {
var name = obj.n ? obj.n : obj.a;
val = obj.v;
if(val == -127) val = 0;
xTicks.push({
label: name.slice(-4)
});
points.push({
label: val.toFixed(1),
value: val,
color: '#7c3aed'
});
min = Math.min(min, val);
max = Math.max(max, val);
});
}
max = Math.ceil(max);
min = Math.floor(min);
let range = max;
if(min < 0) range += Math.abs(min);
let yTickDist = range/4;
for(i = 0; i < 5; i++) {
val = min + (yTickDist*i);
yTicks.push({
value: val,
label: val.toFixed(1)
});
}
config = {
title: "Temperature sensors (°C)",
height: 226,
width: 1520,
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: min,
max: max,
ticks: yTicks
},
x: {
ticks: xTicks
},
points: points
};
};
</script>
<BarChart config={config} />

View File

@@ -1,30 +0,0 @@
<script>
export let chip;
let gpioMax = 44;
$: {
gpioMax = chip == 'esp8266' ? 16 : chip == 'esp32s2' ? 44 : 39;
}
</script>
<option value={3}>UART0</option>
{#if chip == 'esp8266'}
<option value={113}>UART2</option>
{/if}
{#if chip == 'esp32' || chip == 'esp32solo'}
<option value={9}>UART1</option>
<option value={16}>UART2</option>
{/if}
{#if chip == 'esp32s2'}
<option value={18}>UART1</option>
{/if}
{#each {length: gpioMax+1} as _, i}
{#if i > 3
&& !(chip == 'esp32' && (i == 9 || i == 16))
&& !(chip == 'esp32s2' && i == 18)
&& !(chip == 'esp8266' && (i == 3 || i == 113))
}
<option value={i}>GPIO{i}</option>
{/if}
{/each}

View File

@@ -1,65 +0,0 @@
export function upgradeWarningText(board) {
return 'WARNING: ' + 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.'
}
export async function upgrade() {
const response = await fetch('/upgrade', {
method: 'POST'
});
await response.json();
}
export function getNextVersion(currentVersion, releases_orig) {
if(/^v\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(currentVersion)) {
let v = currentVersion.substring(1).split('.');
let v_major = parseInt(v[0]);
let v_minor = parseInt(v[1]);
let v_patch = parseInt(v[2]);
let releases = [...releases_orig];
releases.reverse();
let next_patch;
let next_minor;
let next_major;
for(let i = 0; i < releases.length; i++) {
let release = releases[i];
let ver2 = release.tag_name;
let v2 = ver2.substring(1).split('.');
let v2_major = parseInt(v2[0]);
let v2_minor = parseInt(v2[1]);
let 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) {
let mv = next_major.tag_name.substring(1).split('.');
let mv_major = parseInt(mv[0]);
let mv_minor = parseInt(mv[1]);
let 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_orig[0];
}
}

View File

@@ -1,29 +0,0 @@
<script>
export let epoch;
let days = 0;
let hours = 0;
let minutes = 0;
$: {
days = Math.floor(epoch/86400);
hours = Math.floor(epoch/3600);
minutes = Math.floor(epoch/60);
}
</script>
{#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}

View File

@@ -1,73 +0,0 @@
<script>
import { sysinfoStore } from './DataStores.js';
import BoardTypeSelectOptions from './BoardTypeSelectOptions.svelte';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
export let sysinfo = {}
let loadingOrSaving = false;
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
const data = new URLSearchParams()
for (let field of formData) {
const [key, value] = field
data.append(key, value)
}
const response = await fetch('/save', {
method: 'POST',
body: data
});
let res = (await response.json())
loadingOrSaving = false;
sysinfoStore.update(s => {
s.vndcfg = res.success;
s.booting = res.reboot;
return s;
});
navigate(sysinfo.usrcfg ? "/" : "/setup");
}
let cc = false;
$: {
cc = !sysinfo.usrcfg;
}
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
<div class="cnt">
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<input type="hidden" name="v" value="true"/>
<strong class="text-sm">Initial configuration</strong>
{#if sysinfo.usrcfg}
<div class="bd-red">WARNING: Changing this configuration will affect basic configuration of your device. Only make changes here if instructed by vendor</div>
{/if}
<div class="my-3">
Board type<br/>
<select name="vb" bind:value={sysinfo.board} class="in-s">
<BoardTypeSelectOptions chip={sysinfo.chip}/>
</select>
</div>
{#if sysinfo.board && sysinfo.board > 20}
<div class="my-3">
HAN GPIO<br/>
<select name="vh" class="in-s">
<UartSelectOptions chip={sysinfo.chip}/>
</select>
</div>
{/if}
<div class="my-3">
<label><input type="checkbox" name="vr" value="true" class="rounded mb-1" bind:checked={cc} /> Clear all other configuration</label>
</div>
<div class="my-3">
<button type="submit" class="btn-pri">Save</button>
</div>
<span class="clear-both">&nbsp;</span>
</form>
</div>
</div>
<Mask active={loadingOrSaving} message="Saving device configuration" />

View File

@@ -1,53 +0,0 @@
<script>
import BarChart from './BarChart.svelte';
import { fmtnum, voltcol } from './Helpers.js';
export let u1;
export let u2;
export let u3;
export let ds;
let config = {};
function point(v) {
return {
label: fmtnum(v) + 'V',
value: isNaN(v) ? 0 : v,
color: voltcol(v ? v : 0)
};
};
$: {
let xTicks = [];
let points = [];
if(u1 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L2' : 'L1' });
points.push(point(u1));
}
if(u2 > 0) {
xTicks.push({ label: ds === 1 ? 'L1-L3' : 'L2' });
points.push(point(u2));
}
if(u3 > 0) {
xTicks.push({ label: ds === 1 ? 'L2-L3' : 'L3' });
points.push(point(u3));
}
config = {
padding: { top: 20, right: 15, bottom: 20, left: 35 },
y: {
min: 200,
max: 260,
ticks: [
{ value: 207, label: '-10%' },
{ value: 230, label: '230v' },
{ value: 253, label: '+10%' }
]
},
x: {
ticks: xTicks
},
points: points
};
}
</script>
<BarChart config={config} />

View File

@@ -1,9 +0,0 @@
import "./app.postcss";
import App from "./App.svelte";
const app = new App({
target: document.getElementById("app"),
});
export default app;

View File

@@ -1,2 +0,0 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -1,11 +0,0 @@
import preprocess from "svelte-preprocess";
const config = {
preprocess: [
preprocess({
postcss: true,
}),
],
};
export default config;

View File

@@ -1,13 +0,0 @@
const config = {
content: ["./index.html","./src/**/*.{html,js,svelte,ts}"],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms')
],
};
module.exports = config;

View File

@@ -1,34 +0,0 @@
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.235",
"/energyprice.json": "http://192.168.233.235",
"/dayplot.json": "http://192.168.233.235",
"/monthplot.json": "http://192.168.233.235",
"/temperature.json": "http://192.168.233.235",
"/sysinfo.json": "http://192.168.233.235",
"/configuration.json": "http://192.168.233.235",
"/tariff.json": "http://192.168.233.235",
"/save": "http://192.168.233.235",
"/reboot": "http://192.168.233.235",
"/configfile": "http://192.168.233.235",
"/upgrade": "http://192.168.233.235"
}
}
})

View File

@@ -1,2 +0,0 @@
html/*.h
json/*.h

View File

@@ -1,5 +0,0 @@
"d": {
"s": %s,
"t": %s,
"l": %d
},

View File

@@ -1,7 +0,0 @@
"o": {
"e" : %d,
"c" : %d,
"u1" : %d,
"u2" : %d,
"u3" : %d
}

View File

@@ -1,7 +0,0 @@
"g": {
"t": "%s",
"h": "%s",
"s": %d,
"u": "%s",
"p": "%s"
},

View File

@@ -1,28 +0,0 @@
"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
}
},

Some files were not shown because too many files have changed in this diff Show More