├── .clang-format ├── .github └── workflows │ ├── build-and-test.yml │ ├── clang-format.yml │ ├── doxygen.yml │ └── reuse.yml ├── .gitignore ├── CMakeLists.txt ├── Doxyfile ├── LICENSE ├── LICENSES ├── BSD-2-Clause.txt ├── CC-BY-SA-4.0.txt ├── CC0-1.0.txt ├── GPL-2.0-or-later.txt └── LGPL-2.1-or-later.txt ├── QAlphaCloudConfig.cmake.in ├── README.md ├── artwork ├── Screenshot_infocenter.png ├── Screenshot_infocenter.png.license ├── Screenshot_kdednotifier.png ├── Screenshot_kdednotifier.png.license ├── Screenshot_systemmonitor.png └── Screenshot_systemmonitor.png.license ├── autotests ├── CMakeLists.txt ├── configurationtest.cpp ├── data │ ├── api_error.json │ ├── api_error.json.license │ ├── config_empty.ini │ ├── config_empty.ini.license │ ├── config_garbled.ini │ ├── config_garbled.ini.license │ ├── config_good.ini │ ├── config_good.ini.license │ ├── garbled.json │ ├── garbled.json.license │ ├── lastpowerdata_1.json │ ├── lastpowerdata_1.json.license │ ├── lastpowerdata_2.json │ ├── lastpowerdata_2.json.license │ ├── onedateenergy_1.json │ ├── onedateenergy_1.json.license │ ├── onedateenergy_2.json │ ├── onedateenergy_2.json.license │ ├── onedaypower.json │ ├── onedaypower.json.license │ ├── qalphacloud.ini │ ├── qalphacloud.ini.license │ ├── storagesystems_multiple.json │ ├── storagesystems_multiple.json.license │ ├── storagesystems_single.json │ └── storagesystems_single.json.license ├── lastpowerdatatest.cpp ├── namespacetest.cpp ├── onedateenergytest.cpp ├── onedaypowermodeltest.cpp ├── storagesystemsmodeltest.cpp ├── testnetworkaccessmanager.cpp └── testnetworkaccessmanager.h ├── config-alphacloud.h.cmake ├── examples ├── CMakeLists.txt ├── cpp │ ├── CMakeLists.txt │ └── livedatawidget │ │ ├── CMakeLists.txt │ │ ├── livedatawidget.cpp │ │ ├── livedatawidget.h │ │ ├── livedatawidget.ui │ │ └── main.cpp └── qml │ └── livedata.qml └── src ├── CMakeLists.txt ├── cli ├── CMakeLists.txt └── main.cpp ├── infocenter ├── CMakeLists.txt ├── kcm.cpp ├── kcm.h ├── kcm_qalphacloud.json ├── kcm_qalphacloud.json.license └── package │ └── contents │ └── ui │ ├── PieChart.qml │ ├── PieLegend.qml │ ├── PieLegendItem.qml │ └── main.qml ├── lib ├── CMakeLists.txt ├── apirequest.cpp ├── apirequest.h ├── configuration.cpp ├── configuration.h ├── connector.cpp ├── connector.h ├── lastpowerdata.cpp ├── lastpowerdata.h ├── onedateenergy.cpp ├── onedateenergy.h ├── onedaypowermodel.cpp ├── onedaypowermodel.h ├── qalphacloud.cpp ├── qalphacloud.h ├── storagesystemsmodel.cpp ├── storagesystemsmodel.h ├── utils.cpp └── utils_p.h ├── qml ├── CMakeLists.txt ├── qmldir └── qmlplugin.cpp └── systemstats ├── CMakeLists.txt ├── dailydataobject.cpp ├── dailydataobject.h ├── livedataobject.cpp ├── livedataobject.h ├── metadata.json ├── metadata.json.license ├── plugin.cpp ├── plugin.h ├── systemobject.cpp └── systemobject.h /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 4 | 5 | # This file is adapted from the KDE Frameworks coding style. 6 | --- 7 | 8 | Language: Cpp 9 | 10 | BasedOnStyle: WebKit 11 | Standard: Cpp11 12 | TabWidth: 4 13 | ColumnLimit: 160 14 | SortIncludes: true 15 | BreakBeforeBraces: Linux 16 | PointerAlignment: Right 17 | AlignAfterOpenBracket: Align 18 | AllowAllParametersOfDeclarationOnNextLine: false 19 | AllowShortFunctionsOnASingleLine: None 20 | AlwaysBreakBeforeMultilineStrings: true 21 | BinPackArguments: false 22 | BinPackParameters: false 23 | BreakBeforeBinaryOperators: NonAssignment 24 | Cpp11BracedListStyle: true 25 | SpaceBeforeCpp11BracedList: false 26 | KeepEmptyLinesAtTheStartOfBlocks: false 27 | NamespaceIndentation: None 28 | SpaceAfterTemplateKeyword: false 29 | AlwaysBreakTemplateDeclarations: true 30 | AllowShortLambdasOnASingleLine: Empty 31 | AllowAllArgumentsOnNextLine: false 32 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # 3 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 4 | 5 | name: Builds and Tests 6 | 7 | on: ["push", "pull_request"] 8 | 9 | jobs: 10 | build-and-test-library: 11 | strategy: 12 | matrix: 13 | config: 14 | - {name: "Ubuntu 22.04 (GCC)", os: "ubuntu-22.04", CC: "gcc", CXX: "g++"} 15 | #- {name: "Ubuntu 22.04 (Clang)", os: "ubuntu-22.04", CC: "clang", CXX: "clang++"} 16 | 17 | name: ${{matrix.config.name}} 18 | runs-on: ${{matrix.config.os}} 19 | 20 | steps: 21 | - name: Install dependencies 22 | env: 23 | DEBIAN_FRONTEND: "noninteractive" 24 | run: sudo apt-get update && sudo apt-get install -y build-essential cmake qtbase5-dev extra-cmake-modules lcov 25 | 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | with: 29 | submodules: recursive 30 | 31 | - name: Configure 32 | env: 33 | CC: ${{matrix.config.CC}} 34 | CXX: ${{matrix.config.CXX}} 35 | BUILD_DIR: "build/" 36 | run: | 37 | cmake -B ${BUILD_DIR} -DBUILD_TESTING=ON -DBUILD_COVERAGE=ON -DBUILD_QML=OFF -DBUILD_KSYSTEMSTATS=OFF -DBUILD_KINFOCENTER=OFF 38 | 39 | - name: Build 40 | env: 41 | CC: ${{matrix.config.CC}} 42 | CXX: ${{matrix.config.CXX}} 43 | BUILD_DIR: "build/" 44 | run: | 45 | MAKEFLAGS=-j$(nproc) cmake --build ${BUILD_DIR} 46 | 47 | - name: XDG Base Dirs 48 | run: | 49 | mkdir -p "$HOME/.qttest/cache" 50 | mkdir -p "$HOME/.qttest/config" 51 | 52 | - name: Test 53 | env: 54 | CC: ${{matrix.config.CC}} 55 | CXX: ${{matrix.config.CXX}} 56 | BUILD_DIR: "build/" 57 | run: | 58 | ctest --test-dir ${BUILD_DIR} -V 59 | 60 | - name: Generate Coverage Report 61 | env: 62 | CC: ${{matrix.config.CC}} 63 | CXX: ${{matrix.config.CXX}} 64 | BUILD_DIR: "build/" 65 | run: | 66 | lcov --capture --directory ${BUILD_DIR}src/lib --output-file ${BUILD_DIR}coverage.info 67 | lcov --remove ${BUILD_DIR}coverage.info -o ${BUILD_DIR}coverage.info '/usr/*' '*/moc_*' 68 | if: matrix.config.CC == 'gcc' 69 | # TODO upload report somewhere, add a badge to the README, etc. 70 | 71 | build-full: 72 | strategy: 73 | matrix: 74 | config: 75 | - {name: "Ubuntu 22.04 (GCC)", os: "ubuntu-22.04", CC: "gcc", CXX: "g++"} 76 | #- {name: "Ubuntu 22.04 (Clang)", os: "ubuntu-22.04", CC: "clang", CXX: "clang++"} 77 | 78 | name: ${{matrix.config.name}} 79 | runs-on: ${{matrix.config.os}} 80 | 81 | steps: 82 | - name: Install dependencies 83 | env: 84 | DEBIAN_FRONTEND: "noninteractive" 85 | run: sudo apt-get update && sudo apt-get install -y build-essential cmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev extra-cmake-modules libkf5coreaddons-dev libkf5declarative-dev libkf5sysguard-dev libsensors-dev 86 | # TODO libsensors should be pulled in by KSystemStats... 87 | 88 | - name: Checkout repository 89 | uses: actions/checkout@v2 90 | with: 91 | submodules: recursive 92 | 93 | - name: Configure 94 | env: 95 | CC: ${{matrix.config.CC}} 96 | CXX: ${{matrix.config.CXX}} 97 | BUILD_DIR: "build/" 98 | run: | 99 | cmake -B ${BUILD_DIR} -DBUILD_EXAMPLES=ON -DBUILD_TESTING=OFF 100 | 101 | - name: Build 102 | env: 103 | CC: ${{matrix.config.CC}} 104 | CXX: ${{matrix.config.CXX}} 105 | BUILD_DIR: "build/" 106 | run: | 107 | MAKEFLAGS=-j$(nproc) cmake --build ${BUILD_DIR} 108 | 109 | build-presentation: 110 | strategy: 111 | matrix: 112 | config: 113 | - {name: "Ubuntu 22.04 (GCC)", os: "ubuntu-22.04", CC: "gcc", CXX: "g++"} 114 | #- {name: "Ubuntu 22.04 (Clang)", os: "ubuntu-22.04", CC: "clang", CXX: "clang++"} 115 | 116 | name: ${{matrix.config.name}} 117 | runs-on: ${{matrix.config.os}} 118 | 119 | steps: 120 | - name: Install dependencies 121 | env: 122 | DEBIAN_FRONTEND: "noninteractive" 123 | run: sudo apt-get update && sudo apt-get install -y build-essential cmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev extra-cmake-modules libkf5coreaddons-dev libkf5declarative-dev libkf5sysguard-dev libsensors-dev 124 | # TODO libsensors should be pulled in by KSystemStats... 125 | 126 | - name: Checkout repository 127 | uses: actions/checkout@v2 128 | with: 129 | submodules: recursive 130 | 131 | - name: Configure 132 | env: 133 | CC: ${{matrix.config.CC}} 134 | CXX: ${{matrix.config.CXX}} 135 | BUILD_DIR: "build/" 136 | run: | 137 | cmake -B ${BUILD_DIR} -DBUILD_EXAMPLES=ON -DBUILD_TESTING=OFF -DPRESENTATION_BUILD=ON 138 | 139 | - name: Build 140 | env: 141 | CC: ${{matrix.config.CC}} 142 | CXX: ${{matrix.config.CXX}} 143 | BUILD_DIR: "build/" 144 | run: | 145 | MAKEFLAGS=-j$(nproc) cmake --build ${BUILD_DIR} 146 | -------------------------------------------------------------------------------- /.github/workflows/clang-format.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # 3 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 4 | 5 | name: Code Formatting 6 | 7 | on: ["push", "pull_request"] 8 | 9 | jobs: 10 | doxygen: 11 | strategy: 12 | matrix: 13 | config: 14 | - {name: "Ubuntu 22.04 (GCC)", os: "ubuntu-22.04", CC: "gcc", CXX: "g++"} 15 | 16 | name: ${{matrix.config.name}} 17 | runs-on: ${{matrix.config.os}} 18 | 19 | steps: 20 | - name: Install dependencies 21 | env: 22 | DEBIAN_FRONTEND: "noninteractive" 23 | run: sudo apt-get update && sudo apt-get install -y clang-format 24 | 25 | - name: Checkout repository 26 | uses: actions/checkout@v2 27 | with: 28 | submodules: recursive 29 | 30 | - name: Check code formatting 31 | uses: jidicula/clang-format-action@v4.11.0 32 | with: 33 | clang-format-version: '13' 34 | -------------------------------------------------------------------------------- /.github/workflows/doxygen.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # 3 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 4 | 5 | name: Documentation 6 | 7 | on: ["push"] 8 | 9 | jobs: 10 | doxygen: 11 | strategy: 12 | matrix: 13 | config: 14 | - {name: "Ubuntu 22.04 (GCC)", os: "ubuntu-22.04", CC: "gcc", CXX: "g++"} 15 | 16 | name: ${{matrix.config.name}} 17 | runs-on: ${{matrix.config.os}} 18 | 19 | steps: 20 | - name: Install dependencies 21 | env: 22 | DEBIAN_FRONTEND: "noninteractive" 23 | run: sudo apt-get update && sudo apt-get install -y doxygen graphviz 24 | 25 | - name: Checkout repository 26 | uses: actions/checkout@v2 27 | with: 28 | submodules: recursive 29 | 30 | - name: Run Doxygen 31 | run: | 32 | doxygen 33 | 34 | - name: Publish Documentation 35 | uses: JamesIves/github-pages-deploy-action@v4 36 | with: 37 | branch: gh-pages 38 | folder: docs/html 39 | 40 | # TODO 41 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | name: REUSE Compliance Check 6 | 7 | on: [push, pull_request] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: REUSE Compliance Check 15 | uses: fsfe/reuse-action@v1 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | .directory 5 | CMakeLists.txt.user* 6 | build/* 7 | docs/* 8 | coverage/* 9 | coverage.info 10 | *.kate-swp 11 | .cache 12 | compile_commands.json 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | cmake_minimum_required(VERSION 3.18) 5 | 6 | set(QALPHACLOUD_VERSION 0.0.1) 7 | project(qalphacloud VERSION ${QALPHACLOUD_VERSION}) 8 | 9 | set(QT_MIN_VERSION "5.15.2") 10 | set(KF_MIN_VERSION "5.80.0") 11 | 12 | set(CMAKE_CXX_STANDARD 17) 13 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 14 | 15 | include(FeatureSummary) 16 | 17 | find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) 18 | set_package_properties(ECM PROPERTIES TYPE REQUIRED 19 | DESCRIPTION "Extra CMake Modules." 20 | URL "https://invent.kde.org/frameworks/extra-cmake-modules") 21 | set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) 22 | 23 | include_directories("${CMAKE_CURRENT_BINARY_DIR}") 24 | 25 | include(CMakePackageConfigHelpers) 26 | include(ECMAddTests) 27 | include(ECMGenerateExportHeader) 28 | include(ECMGenerateHeaders) 29 | include(ECMCoverageOption) 30 | include(ECMFindQmlModule) 31 | include(ECMQtDeclareLoggingCategory) 32 | include(ECMSetupVersion) 33 | include(KDEInstallDirs) 34 | include(KDECMakeSettings) 35 | include(KDECompilerSettings NO_POLICY_SCOPE) 36 | include(QtVersionOption) 37 | 38 | option(BUILD_QML "Build QML bindings" ON) 39 | add_feature_info(QML ${BUILD_QML} "QML bindings") 40 | 41 | option(BUILD_KSYSTEMSTATS "Build plug-in for KDE's System Monitor" ON) 42 | add_feature_info(KSYSTEMSTATS ${BUILD_KSYSTEMSTATS} "Plug-in for KDE's System Monitor") 43 | 44 | option(BUILD_KINFOCENTER "Build plug-in for KDE's Info Center" ON) 45 | add_feature_info(KINFOCENTER ${BUILD_KINFOCENTER} "Plug-in for KDE's Info Center") 46 | 47 | option(BUILD_EXAMPLES "Build examples" OFF) 48 | add_feature_info(BUILD_EXAMPLES ${BUILD_EXAMPLES} "Example code") 49 | 50 | set(API_URL "https://openapi.alphaess.com/api/" CACHE STRING "Default API URL") 51 | add_feature_info(API_URL On "Default API URL is '${API_URL}'") 52 | 53 | option(PRESENTATION_BUILD "Hide sensitive information, such as serial numbers, for use in a presentation" OFF) 54 | add_feature_info(PRESENTATION_BUILD ${PRESENTATION_BUILD} "Presentation build") 55 | 56 | # Those options come from ECM 57 | add_feature_info(BUILD_TESTING ${BUILD_TESTING} "Unit tests") 58 | add_feature_info(BUILD_COVERAGE ${BUILD_COVERAGE} "Code coverage (gcov)") 59 | 60 | find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Network) 61 | 62 | if (BUILD_QML) 63 | find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED Qml) 64 | endif() 65 | 66 | if (BUILD_KSYSTEMSTATS) 67 | find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS CoreAddons) # KPluginFactory 68 | 69 | find_package(KSysGuard REQUIRED) 70 | # TODO upstream this 71 | set(KSYSTEMSTATS_PLUGIN_INSTALL_DIR ${KDE_INSTALL_PLUGINDIR}/ksystemstats) 72 | endif() 73 | 74 | if (BUILD_KINFOCENTER) 75 | find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS CoreAddons Declarative Package) 76 | 77 | find_package(Qt${QT_MAJOR_VERSION}QuickControls2 ${QT_MIN_VERSION}) 78 | set_package_properties(Qt${QT_MAJOR_VERSION}QuickControls2 PROPERTIES 79 | TYPE RUNTIME 80 | ) 81 | 82 | ecm_find_qmlmodule(org.kde.kirigami 2.20) 83 | ecm_find_qmlmodule(org.kde.ksysguard.faces 1.0) 84 | ecm_find_qmlmodule(org.kde.ksysguard.formatter 1.0) 85 | ecm_find_qmlmodule(org.kde.quickcharts 1.0) 86 | ecm_find_qmlmodule(org.kde.quickcharts.controls 1.0) 87 | endif() 88 | 89 | ecm_setup_version(PROJECT VARIABLE_PREFIX QALPHACLOUD 90 | VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/qalphacloud_version.h" 91 | PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/QAlphaCloudConfigVersion.cmake" 92 | SOVERSION 0 93 | ) 94 | 95 | set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/QAlphaCloud") 96 | 97 | configure_package_config_file( 98 | "${CMAKE_CURRENT_SOURCE_DIR}/QAlphaCloudConfig.cmake.in" 99 | "${CMAKE_CURRENT_BINARY_DIR}/QAlphaCloudConfig.cmake" 100 | INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} 101 | ) 102 | 103 | install(FILES 104 | "${CMAKE_CURRENT_BINARY_DIR}/QAlphaCloudConfig.cmake" 105 | "${CMAKE_CURRENT_BINARY_DIR}/QAlphaCloudConfigVersion.cmake" 106 | DESTINATION "${CMAKECONFIG_INSTALL_DIR}" 107 | COMPONENT Devel 108 | ) 109 | 110 | install(EXPORT QAlphaCloudTargets 111 | DESTINATION "${CMAKECONFIG_INSTALL_DIR}" 112 | FILE QAlphaCloudTargets.cmake 113 | NAMESPACE QAlphaCloud:: 114 | ) 115 | 116 | install(FILES 117 | ${CMAKE_CURRENT_BINARY_DIR}/qalphacloud_version.h 118 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/QAlphaCloud 119 | COMPONENT Devel 120 | ) 121 | 122 | configure_file(config-alphacloud.h.cmake config-alphacloud.h) 123 | 124 | # TODO cmake configs and pkgconfig 125 | 126 | add_subdirectory(src) 127 | 128 | if (BUILD_TESTING) 129 | find_package(Qt${QT_MAJOR_VERSION} ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test) 130 | add_subdirectory(autotests) 131 | endif() 132 | 133 | if (BUILD_EXAMPLES) 134 | find_package(Qt${QT_MAJOR_VERSION} ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets) 135 | add_subdirectory(examples) 136 | endif() 137 | 138 | feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) 139 | -------------------------------------------------------------------------------- /Doxyfile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | DOXYFILE_ENCODING = UTF-8 5 | PROJECT_NAME = "QAlphaCloud" 6 | PROJECT_NUMBER = 0.0.1 7 | PROJECT_BRIEF = Qt bindings for Alpha Cloud 8 | # TODO 9 | PROJECT_LOGO = 10 | OUTPUT_DIRECTORY = docs 11 | CREATE_SUBDIRS = NO 12 | ALLOW_UNICODE_NAMES = NO 13 | OUTPUT_LANGUAGE = English 14 | OUTPUT_TEXT_DIRECTION = None 15 | 16 | # TODO do we want this? 17 | ALWAYS_DETAILED_SEC = NO 18 | 19 | INLINE_INHERITED_MEMB = NO 20 | FULL_PATH_NAMES = YES 21 | 22 | # TODO 23 | STRIP_FROM_PATH = 24 | # TODO 25 | STRIP_FROM_INC_PATH = 26 | 27 | # TODO 28 | ALIASES = 29 | 30 | MARKDOWN_SUPPORT = YES 31 | 32 | # Do we want that? 33 | BUILTIN_STL_SUPPORT = NO 34 | 35 | EXTRACT_ALL = NO 36 | EXTRACT_PRIVATE = NO 37 | EXTRACT_PRIV_VIRTUAL = NO 38 | EXTRACT_STATIC = NO 39 | EXTRACT_LOCAL_CLASSES = YES 40 | EXTRACT_ANON_NSPACES = NO 41 | RESOLVE_UNNAMED_PARAMS = YES 42 | 43 | 44 | HIDE_UNDOC_MEMBERS = NO 45 | # Hide all the Private classes. 46 | HIDE_UNDOC_CLASSES = YES 47 | 48 | HIDE_FRIEND_COMPOUNDS = YES 49 | HIDE_IN_BODY_DOCS = NO 50 | INTERNAL_DOCS = NO 51 | 52 | CASE_SENSE_NAMES = YES 53 | 54 | STRICT_PROTO_MATCHING = NO 55 | 56 | # Do we want any of this? 57 | GENERATE_TODOLIST = YES 58 | GENERATE_TESTLIST = YES 59 | GENERATE_BUGLIST = YES 60 | GENERATE_DEPRECATEDLIST= YES 61 | 62 | # TODO 63 | ENABLED_SECTIONS = 64 | FILE_VERSION_FILTER = 65 | LAYOUT_FILE = 66 | 67 | QUIET = NO 68 | WARNINGS = YES 69 | WARN_IF_UNDOCUMENTED = YES 70 | WARN_IF_DOC_ERROR = YES 71 | WARN_NO_PARAMDOC = NO 72 | WARN_AS_ERROR = NO 73 | WARN_LOGFILE = 74 | 75 | INPUT = README.md src/lib 76 | INPUT_ENCODING = UTF-8 77 | 78 | FILE_PATTERNS = *.h *.cpp *.qml *.md 79 | 80 | RECURSIVE = NO 81 | EXCLUDE = 82 | EXCLUDE_PATTERNS = *_p.h 83 | EXCLUDE_SYMBOLS = 84 | 85 | # TODO 86 | EXAMPLE_PATH = 87 | EXAMPLE_PATTERNS = * 88 | 89 | IMAGE_PATH = 90 | 91 | INPUT_FILTER = 92 | 93 | FILTER_PATTERNS = 94 | FILTER_SOURCE_FILES = NO 95 | FILTER_SOURCE_PATTERNS = 96 | 97 | # You can browse it yourself :-) 98 | SOURCE_BROWSER = NO 99 | INLINE_SOURCES = NO 100 | 101 | GENERATE_HTML = YES 102 | HTML_OUTPUT = html 103 | HTML_FILE_EXTENSION = .html 104 | #HTML_HEADER = 105 | #HTML_FOOTER = 106 | # TODO make me pretty. 107 | #HTML_STYLESHEET = 108 | #HTML_EXTRA_STYLESHEET = 109 | #HTML_EXTRA_FILES = 110 | #HTML_COLORSTYLE_HUE = 220 111 | #HTML_COLORSTYLE_SAT = 100 112 | #HTML_COLORSTYLE_GAMMA = 80 113 | 114 | # Do we want this? 115 | HTML_TIMESTAMP = NO 116 | 117 | HTML_DYNAMIC_MENUS = YES 118 | # Do we want this? 119 | HTML_DYNAMIC_SECTIONS = NO 120 | 121 | #DISABLE_INDEX = NO 122 | 123 | GENERATE_TREEVIEW = NO 124 | 125 | # TODO look at the rest of the default config... 126 | 127 | # Sorry :-) 128 | GENERATE_LATEX = NO 129 | 130 | USE_MDFILE_AS_MAINPAGE = README.md 131 | -------------------------------------------------------------------------------- /LICENSES/BSD-2-Clause.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /QAlphaCloudConfig.cmake.in: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | @PACKAGE_INIT@ 5 | 6 | include(CMakeFindDependencyMacro) 7 | find_dependency(Qt@QT_MAJOR_VERSION@Gui @REQUIRED_QT_VERSION@) 8 | 9 | include("${CMAKE_CURRENT_LIST_DIR}/QAlphaCloudTargets.cmake") 10 | @PACKAGE_INCLUDE_QCHTARGETS@ 11 | -------------------------------------------------------------------------------- /artwork/Screenshot_infocenter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbroulik/qalphacloud/6e4828816409e842e2811ad26d9b7dedb1771f37/artwork/Screenshot_infocenter.png -------------------------------------------------------------------------------- /artwork/Screenshot_infocenter.png.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC-BY-SA-4.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /artwork/Screenshot_kdednotifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbroulik/qalphacloud/6e4828816409e842e2811ad26d9b7dedb1771f37/artwork/Screenshot_kdednotifier.png -------------------------------------------------------------------------------- /artwork/Screenshot_kdednotifier.png.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC-BY-SA-4.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /artwork/Screenshot_systemmonitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbroulik/qalphacloud/6e4828816409e842e2811ad26d9b7dedb1771f37/artwork/Screenshot_systemmonitor.png -------------------------------------------------------------------------------- /artwork/Screenshot_systemmonitor.png.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC-BY-SA-4.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | ecm_add_test(namespacetest.cpp 5 | TEST_NAME 6 | qalphacloud-namespacetest 7 | LINK_LIBRARIES 8 | Qt${QT_MAJOR_VERSION}::Test 9 | QAlphaCloud 10 | ) 11 | 12 | ecm_add_test(configurationtest.cpp 13 | TEST_NAME 14 | qalphacloud-configurationtest 15 | LINK_LIBRARIES 16 | Qt${QT_MAJOR_VERSION}::Test 17 | QAlphaCloud 18 | ) 19 | 20 | ecm_add_test(storagesystemsmodeltest.cpp 21 | testnetworkaccessmanager.cpp 22 | TEST_NAME 23 | qalphacloud-storagesystemsmodeltest 24 | LINK_LIBRARIES 25 | Qt${QT_MAJOR_VERSION}::Test 26 | QAlphaCloud 27 | ) 28 | 29 | ecm_add_test(lastpowerdatatest.cpp 30 | testnetworkaccessmanager.cpp 31 | TEST_NAME 32 | qalphacloud-lastpowerdatatest 33 | LINK_LIBRARIES 34 | Qt${QT_MAJOR_VERSION}::Test 35 | QAlphaCloud 36 | ) 37 | 38 | ecm_add_test(onedateenergytest.cpp 39 | testnetworkaccessmanager.cpp 40 | TEST_NAME 41 | qalphacloud-onedateenergytest 42 | LINK_LIBRARIES 43 | Qt${QT_MAJOR_VERSION}::Test 44 | QAlphaCloud 45 | ) 46 | 47 | ecm_add_test(onedaypowermodeltest.cpp 48 | testnetworkaccessmanager.cpp 49 | TEST_NAME 50 | qalphacloud-onedaypowermodeltest 51 | LINK_LIBRARIES 52 | Qt${QT_MAJOR_VERSION}::Test 53 | QAlphaCloud 54 | ) 55 | -------------------------------------------------------------------------------- /autotests/configurationtest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | #include "config-alphacloud.h" 15 | 16 | using namespace QAlphaCloud; 17 | 18 | static int g_defaultTimeout = 30000; 19 | 20 | static const QUrl g_defaultUrl = QUrl(QStringLiteral(API_URL)); 21 | 22 | class ConfigurationTest : public QObject 23 | { 24 | Q_OBJECT 25 | 26 | private Q_SLOTS: 27 | void initTestCase(); 28 | void cleanupTestCase(); 29 | 30 | void testLoadFromFile(); 31 | void testLoadFromEmptyFile(); 32 | void testLoadFromBrokenFile(); 33 | void testDefault(); 34 | // TODO test load from QSettings. 35 | // TODO test resetApiUrl/resetTimeout 36 | }; 37 | 38 | void ConfigurationTest::initTestCase() 39 | { 40 | QStandardPaths::setTestModeEnabled(true); 41 | } 42 | 43 | void ConfigurationTest::cleanupTestCase() 44 | { 45 | QFile::remove(Configuration::defaultConfigurationPath()); 46 | } 47 | 48 | void ConfigurationTest::testLoadFromFile() 49 | { 50 | Configuration config; 51 | QCOMPARE(config.apiUrl(), g_defaultUrl); 52 | QVERIFY(config.appId().isEmpty()); 53 | QVERIFY(config.appSecret().isEmpty()); 54 | QCOMPARE(config.requestTimeout(), g_defaultTimeout); 55 | 56 | QSignalSpy validChangedSpy(&config, &Configuration::validChanged); 57 | QSignalSpy apiUrlChangedSpy(&config, &Configuration::apiUrlChanged); 58 | QSignalSpy appSecretChangedSpy(&config, &Configuration::appSecretChanged); 59 | QSignalSpy requestTimeoutChangedSpy(&config, &Configuration::requestTimeoutChanged); 60 | config.loadFromFile(QFINDTESTDATA("data/config_good.ini")); 61 | 62 | QVERIFY(config.valid()); 63 | QCOMPARE(config.apiUrl(), QUrl(QStringLiteral("https://www.example.com/api/"))); 64 | QCOMPARE(config.appId(), QStringLiteral("alpha123456")); 65 | QCOMPARE(config.appSecret(), QStringLiteral("abc123456789")); 66 | QCOMPARE(config.requestTimeout(), 31337); 67 | 68 | QCOMPARE(validChangedSpy.count(), 1); 69 | QCOMPARE(apiUrlChangedSpy.count(), 1); 70 | QCOMPARE(appSecretChangedSpy.count(), 1); 71 | QCOMPARE(requestTimeoutChangedSpy.count(), 1); 72 | } 73 | 74 | void ConfigurationTest::testLoadFromEmptyFile() 75 | { 76 | Configuration config; 77 | config.loadFromFile(QFINDTESTDATA("data/config_empty.ini")); 78 | 79 | QVERIFY(!config.valid()); 80 | QCOMPARE(config.apiUrl(), QUrl(QStringLiteral(API_URL))); 81 | QVERIFY(config.appId().isEmpty()); 82 | QVERIFY(config.appSecret().isEmpty()); 83 | QCOMPARE(config.requestTimeout(), g_defaultTimeout); 84 | } 85 | 86 | void ConfigurationTest::testLoadFromBrokenFile() 87 | { 88 | Configuration config; 89 | config.loadFromFile(QFINDTESTDATA("data/config_garbled.ini")); 90 | 91 | QVERIFY(!config.valid()); 92 | QCOMPARE(config.apiUrl(), g_defaultUrl); 93 | QVERIFY(config.appId().isEmpty()); 94 | QVERIFY(config.appSecret().isEmpty()); 95 | QCOMPARE(config.requestTimeout(), g_defaultTimeout); 96 | } 97 | 98 | void ConfigurationTest::testDefault() 99 | { 100 | const QString testConfig = QFINDTESTDATA("data/qalphacloud.ini"); 101 | QFile::copy(testConfig, Configuration::defaultConfigurationPath()); 102 | 103 | auto config = std::unique_ptr(Configuration::defaultConfiguration(nullptr)); 104 | QVERIFY(config->valid()); 105 | QCOMPARE(config->apiUrl(), QUrl(QStringLiteral("https://www.example.com/api/"))); 106 | QCOMPARE(config->appId(), QStringLiteral("alpha123456")); 107 | QCOMPARE(config->appSecret(), QStringLiteral("abc123456789")); 108 | } 109 | 110 | QTEST_GUILESS_MAIN(ConfigurationTest) 111 | #include "configurationtest.moc" 112 | -------------------------------------------------------------------------------- /autotests/data/api_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 6001, 3 | "msg": "Parameter error", 4 | "data": [] 5 | } 6 | -------------------------------------------------------------------------------- /autotests/data/api_error.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/config_empty.ini: -------------------------------------------------------------------------------- 1 | [Api] 2 | ApiUrl= 3 | AppId= 4 | AppSecret= 5 | Timeout= 6 | -------------------------------------------------------------------------------- /autotests/data/config_empty.ini.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/config_garbled.ini: -------------------------------------------------------------------------------- 1 | This is not an INI file. 2 | -------------------------------------------------------------------------------- /autotests/data/config_garbled.ini.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/config_good.ini: -------------------------------------------------------------------------------- 1 | [Api] 2 | ApiUrl=https://www.example.com/api/ 3 | AppId=alpha123456 4 | AppSecret=abc123456789 5 | Timeout=31337 6 | -------------------------------------------------------------------------------- /autotests/data/config_good.ini.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/garbled.json: -------------------------------------------------------------------------------- 1 | This is not JSON. 2 | -------------------------------------------------------------------------------- /autotests/data/garbled.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/lastpowerdata_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "msg": "", 4 | "data": { 5 | "pbat": 111, 6 | "pev": 0, 7 | "pgrid": -4358, 8 | "pload": 610, 9 | "ppv": 4397, 10 | "soc": 98 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /autotests/data/lastpowerdata_1.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/lastpowerdata_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "msg": "", 4 | "data": { 5 | "pbat": 101, 6 | "pev": 0, 7 | "pgrid": 2400, 8 | "pload": 2500, 9 | "ppv": 10, 10 | "soc": 55 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /autotests/data/lastpowerdata_2.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/onedateenergy_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "msg": "", 4 | "data": { 5 | "eCharge": 2.8, 6 | "eChargingPile": 0, 7 | "eDischarge": 1, 8 | "eGridCharge": 0.01, 9 | "eInput": 0.03, 10 | "eOutput": 14.63, 11 | "epv": 20.1, 12 | "sysSn": "SERIAL", 13 | "theDate": "2023-01-01" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /autotests/data/onedateenergy_1.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/onedateenergy_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "msg": "", 4 | "data": { 5 | "eCharge": 0.1, 6 | "eChargingPile": 0, 7 | "eDischarge": 1.1, 8 | "eGridCharge": 2.8, 9 | "eInput": 3.5, 10 | "eOutput": 0.1, 11 | "epv": 0.2, 12 | "sysSn": "SERIAL", 13 | "theDate": "2023-02-27" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /autotests/data/onedateenergy_2.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/onedaypower.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "msg": "", 4 | "data": [ 5 | { 6 | "cbat": 91, 7 | "feedIn": 3372, 8 | "gridCharge": 101, 9 | "load": 1000, 10 | "pchargingPile": 0, 11 | "ppv": 3000, 12 | "sysSn": "SERIAL", 13 | "uploadTime": "2023-01-01 14:59:32" 14 | }, 15 | { 16 | "cbat": 92, 17 | "feedIn": 3373, 18 | "gridCharge": 102, 19 | "load": 1100, 20 | "pchargingPile": 0, 21 | "ppv": 4000, 22 | "sysSn": "SERIAL", 23 | "uploadTime": "2023-01-01 15:04:32" 24 | }, 25 | { 26 | "cbat": 93, 27 | "feedIn": 3374, 28 | "gridCharge": 103, 29 | "load": 1200, 30 | "pchargingPile": 0, 31 | "ppv": 5000, 32 | "sysSn": "SERIAL", 33 | "uploadTime": "2023-01-01 15:09:32" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /autotests/data/onedaypower.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/qalphacloud.ini: -------------------------------------------------------------------------------- 1 | [Api] 2 | ApiUrl=https://www.example.com/api/ 3 | AppId=alpha123456 4 | AppSecret=abc123456789 5 | -------------------------------------------------------------------------------- /autotests/data/qalphacloud.ini.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/storagesystems_multiple.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "msg": "", 4 | "data": [ 5 | { 6 | "cobat": 2.01, 7 | "emsStatus": "Normal", 8 | "mbat": "BATA", 9 | "minv": "INVA", 10 | "poinv": 1, 11 | "popv": 1, 12 | "surplusCobat": 1.8, 13 | "sysSn": "SERIALA", 14 | "usCapacity": 91 15 | }, 16 | { 17 | "cobat": 3.01, 18 | "emsStatus": "Normal", 19 | "mbat": "BATB", 20 | "minv": "INVB", 21 | "poinv": 2, 22 | "popv": 2, 23 | "surplusCobat": 2.8, 24 | "sysSn": "SERIALB", 25 | "usCapacity": 92 26 | }, 27 | { 28 | "cobat": 4.01, 29 | "emsStatus": "Normal", 30 | "mbat": "BATC", 31 | "minv": "INVC", 32 | "poinv": 3, 33 | "popv": 3, 34 | "surplusCobat": 3.8, 35 | "sysSn": "SERIALC", 36 | "usCapacity": 93 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /autotests/data/storagesystems_multiple.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/data/storagesystems_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "msg": "", 4 | "data": [ 5 | { 6 | "cobat": 8.19, 7 | "emsStatus": "Normal", 8 | "mbat": "BATTERY", 9 | "minv": "INVERTER", 10 | "poinv": 10, 11 | "popv": 10, 12 | "surplusCobat": 7.8, 13 | "sysSn": "SERIAL", 14 | "usCapacity": 95 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /autotests/data/storagesystems_single.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | -------------------------------------------------------------------------------- /autotests/lastpowerdatatest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "testnetworkaccessmanager.h" 16 | 17 | using namespace QAlphaCloud; 18 | 19 | static QString g_serialNumber = QStringLiteral("SERIAL"); 20 | 21 | class LastPowerDataTest : public QObject 22 | { 23 | Q_OBJECT 24 | 25 | private Q_SLOTS: 26 | void initTestCase(); 27 | void testInitialState(); 28 | 29 | void testData(); 30 | void testReloadEmpty(); 31 | void testReloadError(); 32 | void testReset(); 33 | void testReloadInFlight(); 34 | 35 | void testApiError(); 36 | void testGarbledJson(); 37 | 38 | private: 39 | TestNetworkAccessManager m_networkAccessManager; 40 | Connector m_connector; 41 | }; 42 | 43 | void LastPowerDataTest::initTestCase() 44 | { 45 | // Don't rely on defaultConfiguration in tests! 46 | auto *configuration = new Configuration(&m_connector); 47 | configuration->setAppId(QStringLiteral("lastPowerTestApp")); 48 | configuration->setAppSecret(QStringLiteral("testSecret")); 49 | m_connector.setConfiguration(configuration); 50 | 51 | m_connector.setNetworkAccessManager(&m_networkAccessManager); 52 | } 53 | 54 | void LastPowerDataTest::testInitialState() 55 | { 56 | { 57 | LastPowerData data; 58 | QCOMPARE(data.status(), RequestStatus::NoRequest); 59 | QVERIFY(!data.valid()); 60 | 61 | // Can't load without a connector. 62 | QVERIFY(!data.reload()); 63 | } 64 | 65 | { 66 | // Careful not to use the LastPowerData(QObject*) constructor! 67 | LastPowerData data(&m_connector, QString() /*serialNumber*/); 68 | QCOMPARE(data.connector(), &m_connector); 69 | 70 | QCOMPARE(data.status(), RequestStatus::NoRequest); 71 | QVERIFY(!data.valid()); 72 | 73 | // Can't load without a seral number. 74 | QVERIFY(!data.reload()); 75 | 76 | data.setSerialNumber(g_serialNumber); 77 | QCOMPARE(data.serialNumber(), g_serialNumber); 78 | 79 | QVERIFY(data.reload()); 80 | } 81 | 82 | { 83 | LastPowerData data(&m_connector, g_serialNumber); 84 | QCOMPARE(data.connector(), &m_connector); 85 | QCOMPARE(data.serialNumber(), g_serialNumber); 86 | 87 | QCOMPARE(data.status(), RequestStatus::NoRequest); 88 | QVERIFY(!data.valid()); 89 | 90 | QVERIFY(data.reload()); 91 | } 92 | } 93 | 94 | void LastPowerDataTest::testData() 95 | { 96 | LastPowerData data(&m_connector, g_serialNumber); 97 | 98 | const QString testData1Path = QFINDTESTDATA("data/lastpowerdata_1.json"); 99 | const QString testData2Path = QFINDTESTDATA("data/lastpowerdata_2.json"); 100 | 101 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testData1Path)); 102 | 103 | // Load our first test data set. 104 | QVERIFY(data.reload()); 105 | 106 | QCOMPARE(data.status(), QAlphaCloud::RequestStatus::Loading); 107 | 108 | QTRY_COMPARE(data.status(), QAlphaCloud::RequestStatus::Finished); 109 | QCOMPARE(data.error(), QAlphaCloud::ErrorCode::NoError); 110 | QVERIFY(data.errorString().isEmpty()); 111 | QVERIFY(data.valid()); 112 | 113 | QCOMPARE(data.photovoltaicPower(), 4397); 114 | QCOMPARE(data.currentLoad(), 610); 115 | QCOMPARE(data.gridPower(), -4358); 116 | QCOMPARE(data.batteryPower(), 111); 117 | QCOMPARE(data.batterySoc(), 98); 118 | 119 | // Also verify the raw JSON with the JSON from the file. 120 | QFile testFile1(testData1Path); 121 | QVERIFY(testFile1.open(QIODevice::ReadOnly | QIODevice::Text)); 122 | 123 | const QJsonObject testJson1 = QJsonDocument::fromJson(testFile1.readAll()).object().value(QStringLiteral("data")).toObject(); 124 | QCOMPARE(data.rawJson(), testJson1); 125 | 126 | // Now load our second test data set. 127 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testData2Path)); 128 | 129 | QVERIFY(data.reload()); 130 | 131 | QCOMPARE(data.status(), QAlphaCloud::RequestStatus::Loading); 132 | 133 | QTRY_COMPARE(data.status(), QAlphaCloud::RequestStatus::Finished); 134 | QCOMPARE(data.error(), QAlphaCloud::ErrorCode::NoError); 135 | QVERIFY(data.errorString().isEmpty()); 136 | QVERIFY(data.valid()); 137 | 138 | QCOMPARE(data.photovoltaicPower(), 10); 139 | QCOMPARE(data.currentLoad(), 2500); 140 | QCOMPARE(data.gridPower(), 2400); 141 | QCOMPARE(data.batteryPower(), 101); 142 | QCOMPARE(data.batterySoc(), 55); 143 | 144 | // Also verify the raw JSON with the JSON from the file. 145 | QFile testFile2(testData2Path); 146 | QVERIFY(testFile2.open(QIODevice::ReadOnly | QIODevice::Text)); 147 | 148 | const QJsonObject testJson2 = QJsonDocument::fromJson(testFile2.readAll()).object().value(QStringLiteral("data")).toObject(); 149 | QCOMPARE(data.rawJson(), testJson2); 150 | } 151 | 152 | void LastPowerDataTest::testReloadEmpty() 153 | { 154 | } 155 | 156 | void LastPowerDataTest::testReloadError() 157 | { 158 | } 159 | 160 | void LastPowerDataTest::testReset() 161 | { 162 | } 163 | 164 | void LastPowerDataTest::testReloadInFlight() 165 | { 166 | } 167 | 168 | void LastPowerDataTest::testApiError() 169 | { 170 | LastPowerData data(&m_connector, g_serialNumber); 171 | 172 | const QString testDataPath = QFINDTESTDATA("data/api_error.json"); 173 | 174 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testDataPath)); 175 | 176 | QVERIFY(data.reload()); 177 | 178 | QCOMPARE(data.status(), QAlphaCloud::RequestStatus::Loading); 179 | 180 | QTRY_COMPARE(data.status(), QAlphaCloud::RequestStatus::Error); 181 | QCOMPARE(data.error(), QAlphaCloud::ErrorCode::ParameterError); 182 | QCOMPARE(data.errorString(), QStringLiteral("Parameter error")); 183 | QVERIFY(!data.valid()); 184 | } 185 | 186 | void LastPowerDataTest::testGarbledJson() 187 | { 188 | LastPowerData data(&m_connector, g_serialNumber); 189 | 190 | const QString testDataPath = QFINDTESTDATA("data/garbled.json"); 191 | 192 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testDataPath)); 193 | 194 | QVERIFY(data.reload()); 195 | 196 | QCOMPARE(data.status(), QAlphaCloud::RequestStatus::Loading); 197 | 198 | QTRY_COMPARE(data.status(), QAlphaCloud::RequestStatus::Error); 199 | QCOMPARE(data.error(), QAlphaCloud::ErrorCode::JsonParseError); 200 | QVERIFY(!data.errorString().isEmpty()); 201 | QVERIFY(!data.valid()); 202 | } 203 | 204 | QTEST_GUILESS_MAIN(LastPowerDataTest) 205 | #include "lastpowerdatatest.moc" 206 | -------------------------------------------------------------------------------- /autotests/namespacetest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | using namespace QAlphaCloud; 14 | 15 | class NamespaceTest : public QObject 16 | { 17 | Q_OBJECT 18 | 19 | private Q_SLOTS: 20 | void testErrorText_data(); 21 | void testErrorText(); 22 | }; 23 | 24 | void NamespaceTest::testErrorText_data() 25 | { 26 | QTest::addColumn("code"); 27 | QTest::addColumn("details"); 28 | QTest::addColumn("expected"); 29 | 30 | QTest::newRow("API error with API msg") << ErrorCode::ParameterError << QVariant(QStringLiteral("API said no.")) << QStringLiteral("API said no."); 31 | 32 | const QString jsonError = QStringLiteral("Unexpected , on line 2."); 33 | QTest::newRow("JSON parse error") << ErrorCode::JsonParseError << QVariant(jsonError) 34 | << QCoreApplication::translate("errorText", "Failed to parse JSON: %1").arg(jsonError); 35 | 36 | QTest::newRow("Unexpected JSON error") << ErrorCode::UnexpectedJsonDataError << QVariant(QJsonDocument()) 37 | << QCoreApplication::translate("errorText", "Unexpected JSON content received."); 38 | 39 | QTest::newRow("QNetworkReply error") << static_cast(QNetworkReply::TimeoutError) << QVariant() 40 | << QCoreApplication::translate("errorText", "Operation timed out."); 41 | 42 | QJsonArray array; 43 | QJsonDocument arrayDoc(array); 44 | QTest::newRow("Unexpected JSON Array error") << ErrorCode::UnexpectedJsonDataError << QVariant(arrayDoc) 45 | << QCoreApplication::translate("errorText", "Unexpected JSON Array received."); 46 | 47 | QTest::newRow("Out of bounds") << static_cast(9999) << QVariant() << QString(); 48 | 49 | const QString outOfBounds = QStringLiteral("Out of bounds"); 50 | QTest::newRow("Out of bounds with msg") << static_cast(9999) << QVariant(outOfBounds) << outOfBounds; 51 | } 52 | 53 | void NamespaceTest::testErrorText() 54 | { 55 | // We don't want to test translatable strings 56 | // but at least some of the expected behavior. 57 | 58 | QFETCH(ErrorCode, code); 59 | QFETCH(QVariant, details); 60 | QFETCH(QString, expected); 61 | 62 | QCOMPARE(QAlphaCloud::errorText(code, details), expected); 63 | } 64 | 65 | QTEST_GUILESS_MAIN(NamespaceTest) 66 | #include "namespacetest.moc" 67 | -------------------------------------------------------------------------------- /autotests/onedateenergytest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "testnetworkaccessmanager.h" 17 | 18 | using namespace QAlphaCloud; 19 | 20 | static QString g_serialNumber = QStringLiteral("SERIAL"); 21 | 22 | class OneDateEnergyTest : public QObject 23 | { 24 | Q_OBJECT 25 | 26 | private Q_SLOTS: 27 | void initTestCase(); 28 | void testInitialState(); 29 | 30 | void testData(); 31 | // TODO testReloadEmpty 32 | // TODO testReloadError 33 | // TODO testReset 34 | // TODO testReloadInFlight 35 | // TODO testCache 36 | 37 | void testApiError(); 38 | void testGarbledJson(); 39 | 40 | private: 41 | TestNetworkAccessManager m_networkAccessManager; 42 | Connector m_connector; 43 | }; 44 | 45 | void OneDateEnergyTest::initTestCase() 46 | { 47 | // Don't rely on defaultConfiguration in tests! 48 | auto *configuration = new Configuration(&m_connector); 49 | configuration->setAppId(QStringLiteral("oneDateEnergyApp")); 50 | configuration->setAppSecret(QStringLiteral("testSecret")); 51 | m_connector.setConfiguration(configuration); 52 | 53 | m_connector.setNetworkAccessManager(&m_networkAccessManager); 54 | } 55 | 56 | void OneDateEnergyTest::testInitialState() 57 | { 58 | { 59 | OneDateEnergy energy; 60 | QCOMPARE(energy.status(), RequestStatus::NoRequest); 61 | QVERIFY(!energy.valid()); 62 | 63 | // Can't load without a connector. 64 | QVERIFY(!energy.reload()); 65 | } 66 | 67 | { 68 | // Careful not to use the OneDateEnergy(QObject*) constructor! 69 | OneDateEnergy energy(&m_connector, QString() /*serialNumber*/, QDate() /*date*/); 70 | QCOMPARE(energy.connector(), &m_connector); 71 | 72 | QCOMPARE(energy.status(), RequestStatus::NoRequest); 73 | QVERIFY(!energy.valid()); 74 | 75 | // Can't load without a seral number. 76 | QVERIFY(!energy.reload()); 77 | 78 | energy.setSerialNumber(g_serialNumber); 79 | QCOMPARE(energy.serialNumber(), g_serialNumber); 80 | 81 | // Can't load without a date. 82 | QVERIFY(!energy.reload()); 83 | 84 | const QDate date = QDate(2023, 01, 01); 85 | energy.setDate(date); 86 | QCOMPARE(energy.date(), date); 87 | 88 | energy.resetDate(); 89 | QCOMPARE(energy.date(), QDate::currentDate()); 90 | 91 | QVERIFY(energy.reload()); 92 | } 93 | 94 | { 95 | OneDateEnergy energy(&m_connector, g_serialNumber, QDate::currentDate()); 96 | QCOMPARE(energy.connector(), &m_connector); 97 | QCOMPARE(energy.serialNumber(), g_serialNumber); 98 | 99 | QCOMPARE(energy.status(), RequestStatus::NoRequest); 100 | QVERIFY(!energy.valid()); 101 | 102 | QVERIFY(energy.reload()); 103 | } 104 | } 105 | 106 | void OneDateEnergyTest::testData() 107 | { 108 | const QString testData1Path = QFINDTESTDATA("data/onedateenergy_1.json"); 109 | const QString testData2Path = QFINDTESTDATA("data/onedateenergy_2.json"); 110 | 111 | const QDate date1(2023, 01, 01); 112 | const QDate date2(2023, 02, 27); 113 | 114 | OneDateEnergy energy(&m_connector, g_serialNumber, date1); 115 | QCOMPARE(energy.date(), date1); 116 | 117 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testData1Path)); 118 | 119 | // Load our first test data set. 120 | QVERIFY(energy.reload()); 121 | 122 | QCOMPARE(energy.status(), QAlphaCloud::RequestStatus::Loading); 123 | 124 | QTRY_COMPARE(energy.status(), QAlphaCloud::RequestStatus::Finished); 125 | QCOMPARE(energy.error(), QAlphaCloud::ErrorCode::NoError); 126 | QVERIFY(energy.errorString().isEmpty()); 127 | QVERIFY(energy.valid()); 128 | 129 | QCOMPARE(energy.photovoltaic(), 20100); // WattHours. 130 | QCOMPARE(energy.input(), 30); 131 | QCOMPARE(energy.output(), 14630); 132 | QCOMPARE(energy.charge(), 2800); 133 | QCOMPARE(energy.discharge(), 1000); 134 | QCOMPARE(energy.gridCharge(), 10); 135 | // pv + discharge + input - output - charge 136 | QCOMPARE(energy.totalLoad(), 20100 + 1000 + 30 - 14630 - 2800); 137 | 138 | // Also verify the raw JSON with the JSON from the file. 139 | QFile testFile1(testData1Path); 140 | QVERIFY(testFile1.open(QIODevice::ReadOnly | QIODevice::Text)); 141 | 142 | const QJsonObject testJson1 = QJsonDocument::fromJson(testFile1.readAll()).object().value(QStringLiteral("data")).toObject(); 143 | QCOMPARE(energy.rawJson(), testJson1); 144 | 145 | // Now switch date, which resets everything. 146 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testData2Path)); 147 | 148 | energy.setDate(date2); 149 | QCOMPARE(energy.date(), date2); 150 | 151 | QCOMPARE(energy.status(), RequestStatus::NoRequest); 152 | QVERIFY(!energy.valid()); 153 | 154 | // Now load the second dataset. 155 | QVERIFY(energy.reload()); 156 | 157 | QCOMPARE(energy.status(), QAlphaCloud::RequestStatus::Loading); 158 | 159 | QTRY_COMPARE(energy.status(), QAlphaCloud::RequestStatus::Finished); 160 | QCOMPARE(energy.error(), QAlphaCloud::ErrorCode::NoError); 161 | QVERIFY(energy.errorString().isEmpty()); 162 | QVERIFY(energy.valid()); 163 | 164 | QCOMPARE(energy.photovoltaic(), 200); // WattHours. 165 | QCOMPARE(energy.input(), 3500); 166 | QCOMPARE(energy.output(), 100); 167 | QCOMPARE(energy.charge(), 100); 168 | QCOMPARE(energy.discharge(), 1100); 169 | QCOMPARE(energy.gridCharge(), 2800); 170 | // pv + discharge + input - output - charge 171 | QCOMPARE(energy.totalLoad(), 200 + 1100 + 3500 - 100 - 100); 172 | 173 | // Also verify the raw JSON with the JSON from the file. 174 | QFile testFile2(testData2Path); 175 | QVERIFY(testFile2.open(QIODevice::ReadOnly | QIODevice::Text)); 176 | 177 | const QJsonObject testJson2 = QJsonDocument::fromJson(testFile2.readAll()).object().value(QStringLiteral("data")).toObject(); 178 | QCOMPARE(energy.rawJson(), testJson2); 179 | } 180 | 181 | void OneDateEnergyTest::testApiError() 182 | { 183 | OneDateEnergy energy(&m_connector, g_serialNumber, QDate::currentDate()); 184 | 185 | const QString testDataPath = QFINDTESTDATA("data/api_error.json"); 186 | 187 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testDataPath)); 188 | 189 | QVERIFY(energy.reload()); 190 | 191 | QCOMPARE(energy.status(), QAlphaCloud::RequestStatus::Loading); 192 | 193 | QTRY_COMPARE(energy.status(), QAlphaCloud::RequestStatus::Error); 194 | QCOMPARE(energy.error(), QAlphaCloud::ErrorCode::ParameterError); 195 | QCOMPARE(energy.errorString(), QStringLiteral("Parameter error")); 196 | QVERIFY(!energy.valid()); 197 | } 198 | 199 | void OneDateEnergyTest::testGarbledJson() 200 | { 201 | OneDateEnergy energy(&m_connector, g_serialNumber, QDate::currentDate()); 202 | 203 | const QString testDataPath = QFINDTESTDATA("data/garbled.json"); 204 | 205 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testDataPath)); 206 | 207 | QVERIFY(energy.reload()); 208 | 209 | QCOMPARE(energy.status(), QAlphaCloud::RequestStatus::Loading); 210 | 211 | QTRY_COMPARE(energy.status(), QAlphaCloud::RequestStatus::Error); 212 | QCOMPARE(energy.error(), QAlphaCloud::ErrorCode::JsonParseError); 213 | QVERIFY(!energy.errorString().isEmpty()); 214 | QVERIFY(!energy.valid()); 215 | } 216 | 217 | QTEST_GUILESS_MAIN(OneDateEnergyTest) 218 | #include "onedateenergytest.moc" 219 | -------------------------------------------------------------------------------- /autotests/onedaypowermodeltest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | #include "testnetworkaccessmanager.h" 19 | 20 | using namespace QAlphaCloud; 21 | 22 | static QString g_serialNumber = QStringLiteral("SERIAL"); 23 | 24 | class OneDayPowerModelTest : public QObject 25 | { 26 | Q_OBJECT 27 | 28 | private Q_SLOTS: 29 | void initTestCase(); 30 | void testInitialState(); 31 | void testRoleNames(); 32 | 33 | void testData(); 34 | // TODO testReload 35 | // TODO testCache / testForceReload 36 | 37 | void testApiError(); 38 | void testGarbledJson(); 39 | // TODO testReloadError 40 | 41 | private: 42 | TestNetworkAccessManager m_networkAccessManager; 43 | Connector m_connector; 44 | }; 45 | 46 | void OneDayPowerModelTest::initTestCase() 47 | { 48 | // Don't rely on defaultConfiguration in tests! 49 | auto *configuration = new Configuration(&m_connector); 50 | configuration->setAppId(QStringLiteral("oneDayPowerModelApp")); 51 | configuration->setAppSecret(QStringLiteral("testSecret")); 52 | m_connector.setConfiguration(configuration); 53 | 54 | m_connector.setNetworkAccessManager(&m_networkAccessManager); 55 | } 56 | 57 | void OneDayPowerModelTest::testInitialState() 58 | { 59 | { 60 | OneDayPowerModel model; 61 | QCOMPARE(model.status(), RequestStatus::NoRequest); 62 | QCOMPARE(model.rowCount(), 0); 63 | 64 | // Can't load without a connector. 65 | QVERIFY(!model.reload()); 66 | 67 | const QModelIndex idx = model.index(0); 68 | QVERIFY(!idx.isValid()); 69 | const QVariant indexData = idx.data(); 70 | QVERIFY(!indexData.isValid()); 71 | const QVariant modelData = model.data(idx); 72 | QVERIFY(!modelData.isValid()); 73 | } 74 | 75 | { 76 | OneDayPowerModel model(&m_connector, QString() /*serialNumber*/, QDate()); 77 | QCOMPARE(model.connector(), &m_connector); 78 | 79 | QCOMPARE(model.status(), RequestStatus::NoRequest); 80 | QCOMPARE(model.rowCount(), 0); 81 | 82 | // Can't load without a seral number. 83 | QVERIFY(!model.reload()); 84 | 85 | model.setSerialNumber(g_serialNumber); 86 | QCOMPARE(model.serialNumber(), g_serialNumber); 87 | 88 | // Can't load without a date. 89 | QVERIFY(!model.reload()); 90 | 91 | const QDate date = QDate(2023, 01, 01); 92 | model.setDate(date); 93 | QCOMPARE(model.date(), date); 94 | 95 | model.resetDate(); 96 | QCOMPARE(model.date(), QDate::currentDate()); 97 | 98 | QVERIFY(model.reload()); 99 | } 100 | } 101 | 102 | void OneDayPowerModelTest::testRoleNames() 103 | { 104 | OneDayPowerModel model; 105 | 106 | QList expectedRoleNames{ 107 | QByteArrayLiteral("photovoltaicEnergy"), 108 | QByteArrayLiteral("currentLoad"), 109 | QByteArrayLiteral("gridFeed"), 110 | QByteArrayLiteral("gridCharge"), 111 | QByteArrayLiteral("batterySoc"), 112 | QByteArrayLiteral("uploadTime"), 113 | QByteArrayLiteral("rawJson"), 114 | }; 115 | std::sort(expectedRoleNames.begin(), expectedRoleNames.end()); 116 | 117 | auto actualRoleNames = model.roleNames().values(); 118 | std::sort(actualRoleNames.begin(), actualRoleNames.end()); 119 | 120 | QCOMPARE(actualRoleNames, expectedRoleNames); 121 | } 122 | 123 | void OneDayPowerModelTest::testData() 124 | { 125 | const QDate date(2023, 01, 01); 126 | OneDayPowerModel model(&m_connector, g_serialNumber, date); 127 | 128 | const QString testDataPath = QFINDTESTDATA("data/onedaypower.json"); 129 | 130 | QSignalSpy countChangedSpy(&model, &OneDayPowerModel::countChanged); 131 | 132 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testDataPath)); 133 | 134 | // Load our test data. 135 | QVERIFY(model.reload()); 136 | 137 | QCOMPARE(model.status(), QAlphaCloud::RequestStatus::Loading); 138 | 139 | QTRY_COMPARE(model.status(), QAlphaCloud::RequestStatus::Finished); 140 | QCOMPARE(model.error(), QAlphaCloud::ErrorCode::NoError); 141 | QVERIFY(model.errorString().isEmpty()); 142 | QCOMPARE(model.rowCount(), 3); 143 | QCOMPARE(countChangedSpy.count(), 1); 144 | 145 | // Also verify the raw JSON with the JSON from the file. 146 | QFile testFile(testDataPath); 147 | QVERIFY(testFile.open(QIODevice::ReadOnly | QIODevice::Text)); 148 | 149 | const QJsonArray testJsonArray = QJsonDocument::fromJson(testFile.readAll()).object().value(QStringLiteral("data")).toArray(); 150 | 151 | // Now compare that the data is correct in all three systems. 152 | using Roles = OneDayPowerModel::Roles; 153 | QString suffix; 154 | for (int i = 0; i < 3; ++i) { 155 | suffix = QChar('A' + i); 156 | 157 | const QModelIndex idx = model.index(i); 158 | QVERIFY(idx.isValid()); 159 | 160 | const int photovoltaicEnergy = idx.data(static_cast(Roles::PhotovoltaicEnergy)).toInt(); 161 | QCOMPARE(photovoltaicEnergy, 3000 + 1000 * i); 162 | 163 | const int currentLoad = idx.data(static_cast(Roles::CurrentLoad)).toInt(); 164 | QCOMPARE(currentLoad, 1000 + 100 * i); 165 | 166 | const int gridFeed = idx.data(static_cast(Roles::GridFeed)).toInt(); 167 | QCOMPARE(gridFeed, 3372 + i); 168 | 169 | const int gridCharge = idx.data(static_cast(Roles::GridCharge)).toInt(); 170 | QCOMPARE(gridCharge, 101 + i); 171 | 172 | const qreal batterySoc = idx.data(static_cast(Roles::BatterySoc)).toReal(); 173 | QCOMPARE(batterySoc, 91 + i); 174 | 175 | const QDateTime uploadTime = idx.data(static_cast(Roles::UploadTime)).toDateTime(); 176 | QTime time(14, 59, 32); 177 | // Data is in 5 minute intervals: 178 | time = time.addSecs(i * 60 * 5); 179 | QCOMPARE(uploadTime, QDateTime(date, time)); 180 | 181 | const QJsonObject rawJson = idx.data(static_cast(Roles::RawJson)).toJsonObject(); 182 | QCOMPARE(rawJson, testJsonArray.at(i).toObject()); 183 | 184 | const QVariant invalidRole = idx.data(Qt::UserRole + 999); 185 | QVERIFY(!invalidRole.isValid()); 186 | } 187 | 188 | // TODO Can this cause trouble with timezones? 189 | QCOMPARE(model.fromDateTime(), QDateTime(date, QTime(14, 59, 32))); 190 | QCOMPARE(model.toDateTime(), QDateTime(date, QTime(15, 9, 32))); 191 | 192 | QCOMPARE(model.peakPhotovoltaic(), 5000); 193 | QCOMPARE(model.peakLoad(), 1200); 194 | QCOMPARE(model.peakGridFeed(), 3374); 195 | QCOMPARE(model.peakGridCharge(), 103); 196 | } 197 | 198 | void OneDayPowerModelTest::testApiError() 199 | { 200 | OneDayPowerModel model(&m_connector, g_serialNumber, QDate::currentDate()); 201 | 202 | const QString testDataPath = QFINDTESTDATA("data/api_error.json"); 203 | 204 | QSignalSpy countChangedSpy(&model, &OneDayPowerModel::countChanged); 205 | 206 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testDataPath)); 207 | 208 | // Load our test data. 209 | QVERIFY(model.reload()); 210 | 211 | QCOMPARE(model.status(), QAlphaCloud::RequestStatus::Loading); 212 | 213 | QTRY_COMPARE(model.status(), QAlphaCloud::RequestStatus::Error); 214 | QCOMPARE(model.error(), QAlphaCloud::ErrorCode::ParameterError); 215 | QCOMPARE(model.errorString(), QStringLiteral("Parameter error")); 216 | QCOMPARE(model.rowCount(), 0); 217 | QCOMPARE(countChangedSpy.count(), 0); 218 | } 219 | 220 | void OneDayPowerModelTest::testGarbledJson() 221 | { 222 | OneDayPowerModel model(&m_connector, g_serialNumber, QDate::currentDate()); 223 | 224 | const QString testDataPath = QFINDTESTDATA("data/garbled.json"); 225 | 226 | QSignalSpy countChangedSpy(&model, &OneDayPowerModel::countChanged); 227 | 228 | m_networkAccessManager.setOverrideUrl(QUrl::fromLocalFile(testDataPath)); 229 | 230 | // Load our test data. 231 | QVERIFY(model.reload()); 232 | 233 | QCOMPARE(model.status(), QAlphaCloud::RequestStatus::Loading); 234 | 235 | QTRY_COMPARE(model.status(), QAlphaCloud::RequestStatus::Error); 236 | QCOMPARE(model.error(), QAlphaCloud::ErrorCode::JsonParseError); 237 | QVERIFY(!model.errorString().isEmpty()); 238 | QCOMPARE(model.rowCount(), 0); 239 | QCOMPARE(countChangedSpy.count(), 0); 240 | } 241 | 242 | QTEST_GUILESS_MAIN(OneDayPowerModelTest) 243 | #include "onedaypowermodeltest.moc" 244 | -------------------------------------------------------------------------------- /autotests/testnetworkaccessmanager.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "testnetworkaccessmanager.h" 7 | 8 | TestNetworkAccessManager::TestNetworkAccessManager(QObject *parent) 9 | : QNetworkAccessManager(parent) 10 | { 11 | } 12 | 13 | TestNetworkAccessManager::~TestNetworkAccessManager() = default; 14 | 15 | QUrl TestNetworkAccessManager::overrideUrl() const 16 | { 17 | return m_overrideUrl; 18 | } 19 | 20 | void TestNetworkAccessManager::setOverrideUrl(const QUrl &url) 21 | { 22 | m_overrideUrl = url; 23 | } 24 | 25 | QNetworkReply *TestNetworkAccessManager::createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) 26 | { 27 | QNetworkRequest newRequest(request); 28 | 29 | newRequest.setUrl(m_overrideUrl); 30 | 31 | return QNetworkAccessManager::createRequest(op, newRequest, outgoingData); 32 | } 33 | -------------------------------------------------------------------------------- /autotests/testnetworkaccessmanager.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include 7 | 8 | class TestNetworkAccessManager : public QNetworkAccessManager 9 | { 10 | public: 11 | TestNetworkAccessManager(QObject *parent = nullptr); 12 | ~TestNetworkAccessManager() override; 13 | 14 | QUrl overrideUrl() const; 15 | void setOverrideUrl(const QUrl &url); 16 | 17 | protected: 18 | QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr) override; 19 | 20 | private: 21 | QUrl m_overrideUrl; 22 | }; 23 | -------------------------------------------------------------------------------- /config-alphacloud.h.cmake: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: CC0-1.0 4 | */ 5 | 6 | #define API_URL "${API_URL}" 7 | 8 | #cmakedefine01 PRESENTATION_BUILD 9 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | add_subdirectory(cpp) 5 | 6 | # qml directory is QML only, no compiled code. 7 | -------------------------------------------------------------------------------- /examples/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | add_subdirectory(livedatawidget) 5 | -------------------------------------------------------------------------------- /examples/cpp/livedatawidget/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | add_executable(livedatawidget) 5 | 6 | set(LIVEDATAWIDGET_SRCS 7 | main.cpp 8 | livedatawidget.cpp 9 | livedatawidget.h 10 | ) 11 | 12 | # No AUTOUIC in ECM yet and qt_wrap_ui needs a variable to write to. 13 | qt_wrap_ui(LIVEDATAWIDGET_SRCS livedatawidget.ui) 14 | 15 | target_sources(livedatawidget PRIVATE ${LIVEDATAWIDGET_SRCS}) 16 | 17 | target_link_libraries(livedatawidget PUBLIC Qt${QT_MAJOR_VERSION}::Network Qt${QT_MAJOR_VERSION}::Widgets qalphacloud) 18 | -------------------------------------------------------------------------------- /examples/cpp/livedatawidget/livedatawidget.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: BSD-2-Clause 4 | */ 5 | 6 | #include "livedatawidget.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "config-alphacloud.h" 16 | 17 | using namespace QAlphaCloud; 18 | 19 | LiveDataWidget::LiveDataWidget(QWidget *parent) 20 | : QWidget(parent) 21 | , m_networkAccessManager(new QNetworkAccessManager(this)) 22 | , m_connector(new Connector(this)) 23 | , m_storageSystems(new StorageSystemsModel(m_connector, this)) 24 | , m_liveData(new LastPowerData(this)) 25 | { 26 | m_ui.setupUi(this); 27 | // Wire up Reload button. 28 | connect(m_ui.reloadButton, &QToolButton::clicked, m_liveData, &LastPowerData::reload); 29 | 30 | // Set up network manager. Important to let it automatically follow redirects. 31 | m_networkAccessManager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); 32 | m_connector->setNetworkAccessManager(m_networkAccessManager); 33 | 34 | // Configure API. 35 | auto *config = new Configuration(m_connector); 36 | // NOTE put your credentials here! 37 | config->setAppId(QStringLiteral("alpha...")); 38 | config->setAppSecret(QStringLiteral("...")); 39 | // or use this to load it from the QAlphaCloud config file. 40 | // config->loadDefault(); 41 | 42 | m_connector->setConfiguration(config); 43 | 44 | // Hook up the storage systems model signals. 45 | connect(m_storageSystems, &StorageSystemsModel::statusChanged, this, &LiveDataWidget::onStorageSystemsModelStatusChanged); 46 | 47 | // Set up live data. It needs a connector. 48 | m_liveData->setConnector(m_connector); 49 | connect(m_liveData, &LastPowerData::statusChanged, this, &LiveDataWidget::onLiveDataStatusChanged); 50 | onLiveDataStatusChanged(RequestStatus::NoRequest); 51 | 52 | if (!m_storageSystems->reload()) { 53 | qFatal("Failed to load storages, check that your Configuration object is correctly configured."); 54 | } 55 | } 56 | 57 | LiveDataWidget::~LiveDataWidget() = default; 58 | 59 | void LiveDataWidget::onStorageSystemsModelStatusChanged(RequestStatus status) 60 | { 61 | if (status == RequestStatus::Loading) { 62 | m_ui.serialNumberLabel->setText(tr("Loading…")); 63 | } else if (status == RequestStatus::Finished) { 64 | // Storage systems have loaded, now load live data from the primary serial number. 65 | #if PRESENTATION_BUILD 66 | m_ui.serialNumberLabel->setText(tr("")); 67 | #else 68 | m_ui.serialNumberLabel->setText(m_storageSystems->primarySerialNumber()); 69 | #endif 70 | m_liveData->setSerialNumber(m_storageSystems->primarySerialNumber()); 71 | m_liveData->reload(); 72 | } else if (status == RequestStatus::Error) { 73 | m_ui.serialNumberLabel->setText(tr("Error: %1").arg(m_storageSystems->errorString())); 74 | } 75 | } 76 | 77 | void LiveDataWidget::onLiveDataStatusChanged(RequestStatus status) 78 | { 79 | const QString noDataString = QStringLiteral("–"); 80 | const QString loadingString = tr("Loading…"); 81 | const QString wattTemplate = QStringLiteral("%L1 W"); 82 | const QString errorString = tr("Error"); 83 | 84 | switch (status) { 85 | case RequestStatus::NoRequest: 86 | m_ui.photovoltaicValue->setText(noDataString); 87 | m_ui.currentLoadValue->setText(noDataString); 88 | m_ui.gridPowerValue->setText(noDataString); 89 | m_ui.batteryPowerValue->setText(noDataString); 90 | m_ui.batterySocValue->setText(noDataString); 91 | break; 92 | 93 | case RequestStatus::Loading: 94 | m_ui.photovoltaicValue->setText(loadingString); 95 | m_ui.currentLoadValue->setText(loadingString); 96 | m_ui.gridPowerValue->setText(loadingString); 97 | m_ui.batteryPowerValue->setText(loadingString); 98 | m_ui.batterySocValue->setText(loadingString); 99 | break; 100 | 101 | case RequestStatus::Finished: 102 | m_ui.photovoltaicValue->setText(wattTemplate.arg(m_liveData->photovoltaicPower())); 103 | m_ui.currentLoadValue->setText(wattTemplate.arg(m_liveData->currentLoad())); 104 | m_ui.gridPowerValue->setText(wattTemplate.arg(m_liveData->gridPower())); 105 | m_ui.batteryPowerValue->setText(wattTemplate.arg(m_liveData->batteryPower())); 106 | m_ui.batterySocValue->setText(QStringLiteral("%L1 %").arg(m_liveData->batterySoc())); 107 | break; 108 | 109 | case RequestStatus::Error: 110 | m_ui.photovoltaicValue->setText(errorString); 111 | m_ui.currentLoadValue->setText(errorString); 112 | m_ui.gridPowerValue->setText(errorString); 113 | m_ui.batteryPowerValue->setText(errorString); 114 | m_ui.batterySocValue->setText(errorString); 115 | break; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /examples/cpp/livedatawidget/livedatawidget.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: BSD-2-Clause 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include "ui_livedatawidget.h" 13 | 14 | class QNetworkAccessManager; 15 | 16 | namespace QAlphaCloud 17 | { 18 | class Connector; 19 | class LastPowerData; 20 | class StorageSystemsModel; 21 | } 22 | 23 | class LiveDataWidget : public QWidget 24 | { 25 | Q_OBJECT 26 | 27 | public: 28 | LiveDataWidget(QWidget *parent = nullptr); 29 | ~LiveDataWidget() override; 30 | 31 | private: 32 | void onStorageSystemsModelStatusChanged(QAlphaCloud::RequestStatus status); 33 | void onLiveDataStatusChanged(QAlphaCloud::RequestStatus status); 34 | 35 | QNetworkAccessManager *m_networkAccessManager; 36 | 37 | QAlphaCloud::Connector *m_connector; 38 | QAlphaCloud::StorageSystemsModel *m_storageSystems; 39 | QAlphaCloud::LastPowerData *m_liveData; 40 | 41 | Ui_LiveDataWidget m_ui; 42 | }; 43 | -------------------------------------------------------------------------------- /examples/cpp/livedatawidget/livedatawidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | LiveDataWidget 8 | 9 | 10 | 11 | 0 12 | 0 13 | 252 14 | 210 15 | 16 | 17 | 18 | Live Data Example 19 | 20 | 21 | 22 | 23 | 24 | System: 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | <Serial Number> 34 | 35 | 36 | 37 | 38 | 39 | 40 | Reload 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Qt::Vertical 53 | 54 | 55 | QSizePolicy::Fixed 56 | 57 | 58 | 59 | 20 60 | 10 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Photovoltaic: 69 | 70 | 71 | 72 | 73 | 74 | 75 | <Photovoltaic> 76 | 77 | 78 | 79 | 80 | 81 | 82 | Current Load: 83 | 84 | 85 | 86 | 87 | 88 | 89 | <Current Load> 90 | 91 | 92 | 93 | 94 | 95 | 96 | Grid Power 97 | 98 | 99 | 100 | 101 | 102 | 103 | <Grid Power> 104 | 105 | 106 | 107 | 108 | 109 | 110 | Battery Power: 111 | 112 | 113 | 114 | 115 | 116 | 117 | <Battery Power> 118 | 119 | 120 | 121 | 122 | 123 | 124 | Battery SOC: 125 | 126 | 127 | 128 | 129 | 130 | 131 | <Battery SOC> 132 | 133 | 134 | 135 | 136 | 137 | 138 | Qt::Vertical 139 | 140 | 141 | 142 | 20 143 | 40 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /examples/cpp/livedatawidget/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: BSD-2-Clause 4 | */ 5 | 6 | #include 7 | 8 | #include "livedatawidget.h" 9 | 10 | int main(int argc, char **argv) 11 | { 12 | QApplication app(argc, argv); 13 | 14 | LiveDataWidget widget; 15 | widget.show(); 16 | 17 | return app.exec(); 18 | } 19 | -------------------------------------------------------------------------------- /examples/qml/livedata.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: BSD-2-Clause 4 | */ 5 | 6 | import QtQuick 2.15 7 | import QtQuick.Layouts 1.1 8 | import QtQuick.Controls 2.12 as QQC2 9 | 10 | import de.broulik.qalphacloud 1.0 as QAlphaCloud 11 | 12 | Item { 13 | id: root 14 | 15 | width: 400 16 | height: 400 17 | 18 | implicitWidth: grid.implicitWidth 19 | implicitHeight: grid.implicitHeight 20 | 21 | QAlphaCloud.Connector { 22 | id: cloudConnector 23 | configuration: QAlphaCloud.Configuration { 24 | // NOTE put your credentials here! 25 | //appId: "alpha..." 26 | //appSecret: "..." 27 | } 28 | Component.onCompleted: { 29 | storageSystems.reload(); 30 | } 31 | } 32 | 33 | QAlphaCloud.StorageSystemsModel { 34 | id: storageSystems 35 | connector: cloudConnector 36 | } 37 | 38 | QAlphaCloud.LastPowerData { 39 | id: liveData 40 | connector: cloudConnector 41 | serialNumber: storageSystems.primarySerialNumber 42 | } 43 | 44 | GridLayout { 45 | id: grid 46 | columns: 2 47 | width: parent.width 48 | opacity: busy.running ? 0.5 : 1 49 | 50 | QQC2.Label { 51 | text: qsTr("System:") 52 | } 53 | 54 | RowLayout { 55 | Layout.fillWidth: true 56 | 57 | QQC2.Label { 58 | Layout.fillWidth: true 59 | visible: storageSystems.status === QAlphaCloud.QAlphaCloud.RequestStatus.Error 60 | text: qsTr("Error: %1").arg(storageSystems.errorString) 61 | } 62 | 63 | QQC2.Label { 64 | Layout.fillWidth: true 65 | text: storageSystems.primarySerialNumber 66 | } 67 | 68 | QQC2.Button { 69 | Accessible.name: qsTr("Reload") 70 | icon.name: "view-refresh" 71 | onClicked: { 72 | liveData.reload(); 73 | } 74 | } 75 | } 76 | 77 | Item { 78 | Layout.fillWidth: true 79 | Layout.columnSpan: 2 80 | height: 10 81 | } 82 | 83 | QQC2.Label { 84 | text: qsTr("Photovoltaic:") 85 | } 86 | 87 | QQC2.Label { 88 | text: liveData.valid ? qsTr("%L1 W").arg(liveData.photovoltaicPower) : "–" 89 | } 90 | 91 | QQC2.Label { 92 | text: qsTr("CurrentLoad:") 93 | } 94 | 95 | QQC2.Label { 96 | text: liveData.valid ? qsTr("%L1 W").arg(liveData.currentLoad) : "–" 97 | } 98 | 99 | QQC2.Label { 100 | text: qsTr("Grid Power:") 101 | } 102 | 103 | QQC2.Label { 104 | text: liveData.valid ? qsTr("%L1 W").arg(liveData.gridPower) : "–" 105 | } 106 | 107 | QQC2.Label { 108 | text: qsTr("Battery Power:") 109 | } 110 | 111 | QQC2.Label { 112 | text: liveData.valid ? qsTr("%L1 W").arg(liveData.batteryPower) : "–" 113 | } 114 | 115 | QQC2.Label { 116 | text: qsTr("Battery SOC:") 117 | } 118 | 119 | QQC2.Label { 120 | text: liveData.valid ? qsTr("%L1 %").arg(liveData.batterySoc) : "–" 121 | } 122 | } 123 | 124 | QQC2.BusyIndicator { 125 | id: busy 126 | anchors.centerIn: parent 127 | running: storageSystems.status === QAlphaCloud.QAlphaCloud.RequestStatus.Loading 128 | || liveData.status === QAlphaCloud.QAlphaCloud.RequestStatus.Loading 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | add_subdirectory(lib) 5 | add_subdirectory(cli) 6 | 7 | if (BUILD_QML) 8 | add_subdirectory(qml) 9 | endif() 10 | 11 | if (BUILD_KSYSTEMSTATS) 12 | add_subdirectory(systemstats) 13 | endif() 14 | 15 | if (BUILD_KINFOCENTER) 16 | add_subdirectory(infocenter) 17 | endif() 18 | -------------------------------------------------------------------------------- /src/cli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | add_executable(qalphacloud-cli) 5 | 6 | set_target_properties(qalphacloud-cli PROPERTIES 7 | # There is no RENAME option for install TARGETS... 8 | OUTPUT_NAME qalphacloud 9 | ) 10 | 11 | target_sources(qalphacloud-cli PRIVATE main.cpp) 12 | 13 | target_link_libraries(qalphacloud-cli PUBLIC Qt::Core qalphacloud) 14 | 15 | install(TARGETS qalphacloud-cli DESTINATION ${CMAKE_INSTALL_BINDIR}) 16 | -------------------------------------------------------------------------------- /src/infocenter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | kcoreaddons_add_plugin(kcm_qalphacloud SOURCES 5 | kcm.cpp 6 | kcm.h 7 | 8 | INSTALL_NAMESPACE 9 | "plasma/kcms/kinfocenter" 10 | ) 11 | 12 | target_link_libraries(kcm_qalphacloud 13 | KF${QT_MAJOR_VERSION}::QuickAddons 14 | QAlphaCloud 15 | ) 16 | 17 | kpackage_install_package(package kcm_qalphacloud kcms) 18 | -------------------------------------------------------------------------------- /src/infocenter/kcm.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | #include "kcm.h" 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include "config-alphacloud.h" 13 | 14 | K_PLUGIN_CLASS_WITH_JSON(KCMAlphaCloud, "kcm_qalphacloud.json") 15 | 16 | BatterySocScaleProxyModel::BatterySocScaleProxyModel(QObject *parent) 17 | : QIdentityProxyModel(parent) 18 | { 19 | } 20 | 21 | BatterySocScaleProxyModel::~BatterySocScaleProxyModel() = default; 22 | 23 | int BatterySocScaleProxyModel::max() const 24 | { 25 | return m_max; 26 | } 27 | 28 | void BatterySocScaleProxyModel::setMax(int max) 29 | { 30 | if (m_max != max) { 31 | beginResetModel(); 32 | m_max = max; 33 | m_factor = max / 100.0; 34 | endResetModel(); 35 | 36 | Q_EMIT maxChanged(max); 37 | } 38 | } 39 | 40 | QVariant BatterySocScaleProxyModel::data(const QModelIndex &index, int role) const 41 | { 42 | if (role != static_cast(QAlphaCloud::OneDayPowerModel::Roles::BatterySoc)) { 43 | return QIdentityProxyModel::data(index, role); 44 | } 45 | 46 | const int batterySoc = sourceModel()->data(mapToSource(index), role).toInt(); 47 | return batterySoc * m_factor; 48 | } 49 | 50 | KCMAlphaCloud::KCMAlphaCloud(QObject *parent, const KPluginMetaData &data, const QVariantList &args) 51 | : KQuickAddons::ConfigModule(parent, data, args) 52 | { 53 | qmlRegisterType("de.broulik.qalphacloud.private.kcm", 1, 0, "BatterySocScaleProxyModel"); 54 | 55 | setButtons(KQuickAddons::ConfigModule::NoAdditionalButton); 56 | } 57 | 58 | bool KCMAlphaCloud::presentationBuild() const 59 | { 60 | #if PRESENTATION_BUILD 61 | return true; 62 | #else 63 | return false; 64 | #endif 65 | } 66 | 67 | #include "kcm.moc" 68 | -------------------------------------------------------------------------------- /src/infocenter/kcm.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include 11 | 12 | class BatterySocScaleProxyModel : public QIdentityProxyModel 13 | { 14 | Q_OBJECT 15 | 16 | Q_PROPERTY(int max READ max WRITE setMax NOTIFY maxChanged) 17 | 18 | public: 19 | BatterySocScaleProxyModel(QObject *parent = nullptr); 20 | ~BatterySocScaleProxyModel() override; 21 | 22 | int max() const; 23 | void setMax(int max); 24 | Q_SIGNAL void maxChanged(int max); 25 | 26 | QVariant data(const QModelIndex &proxyIndex, int role) const override; 27 | 28 | private: 29 | int m_max = 0; 30 | qreal m_factor = 0; 31 | }; 32 | 33 | class KCMAlphaCloud : public KQuickAddons::ConfigModule 34 | { 35 | Q_OBJECT 36 | 37 | Q_PROPERTY(bool presentationBuild READ presentationBuild CONSTANT) 38 | 39 | public: 40 | explicit KCMAlphaCloud(QObject *parent, const KPluginMetaData &data, const QVariantList &args); 41 | 42 | bool presentationBuild() const; 43 | }; 44 | -------------------------------------------------------------------------------- /src/infocenter/kcm_qalphacloud.json: -------------------------------------------------------------------------------- 1 | { 2 | "KPlugin": { 3 | "Description": "Solar and Energy Consumption Statistics", 4 | "FormFactors": [ 5 | "desktop", 6 | "handset" 7 | ], 8 | "Icon": "weather-clouds", 9 | "Name": "Alpha Cloud" 10 | }, 11 | "X-KDE-KInfoCenter-Category": "basic_information", 12 | "X-KDE-Keywords": "Battery,Energy,Solar,Photovoltaic,Grid,Power,Statistics,Electricity", 13 | "X-KDE-Weight": 2 14 | } 15 | -------------------------------------------------------------------------------- /src/infocenter/kcm_qalphacloud.json.license: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: CC0-1.0 2 | SPDX-FileCopyrightText: None 3 | -------------------------------------------------------------------------------- /src/infocenter/package/contents/ui/PieChart.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import QtQuick 2.15 7 | import QtQuick.Controls 2.15 as QQC2 8 | import QtQuick.Layouts 1.1 9 | 10 | import org.kde.kirigami 2.20 as Kirigami 11 | //import org.kde.kquickcontrolsaddons 2.0 12 | import org.kde.kcm 1.4 as KCM 13 | 14 | import org.kde.quickcharts 1.0 as Charts 15 | import org.kde.quickcharts.controls 1.0 as ChartControls 16 | import org.kde.ksysguard.formatter 1.0 as Formatter 17 | import org.kde.ksysguard.faces 1.0 as Faces 18 | 19 | import de.broulik.qalphacloud 1.0 as QAlphaCloud 20 | 21 | ChartControls.PieChartControl { 22 | id: pieChart 23 | 24 | property color color 25 | // TODO Qt6: Qt.alpha 26 | property color backgroundColor: Qt.lighter(color, 1.5) 27 | property color textColor: color 28 | 29 | property int totalValue 30 | property int highlightValue 31 | 32 | property alias title: heading.text 33 | property alias text: label.text 34 | 35 | 36 | chart.backgroundColor: pieChart.backgroundColor 37 | 38 | range { 39 | from: 0 40 | to: pieChart.totalValue 41 | automatic: false 42 | } 43 | 44 | valueSources: Charts.SingleValueSource { 45 | value: pieChart.highlightValue 46 | } 47 | 48 | chart.colorSource: Charts.SingleValueSource { 49 | value: pieChart.color 50 | } 51 | 52 | ColumnLayout { 53 | anchors.verticalCenter: parent.verticalCenter 54 | width: parent.width 55 | spacing: 0 56 | 57 | Kirigami.Heading { 58 | id: heading 59 | Layout.fillWidth: true 60 | color: pieChart.textColor 61 | horizontalAlignment: Text.AlignHCenter 62 | } 63 | 64 | QQC2.Label { 65 | id: label 66 | Layout.fillWidth: true 67 | horizontalAlignment: Text.AlignHCenter 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/infocenter/package/contents/ui/PieLegend.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import QtQuick 2.15 7 | import QtQuick.Controls 2.15 as QQC2 8 | import QtQuick.Layouts 1.1 9 | 10 | import org.kde.kirigami 2.20 as Kirigami 11 | 12 | ColumnLayout { 13 | property alias model: repeater.model 14 | 15 | Repeater { 16 | id: repeater 17 | 18 | PieLegendItem { 19 | color: modelData.color 20 | value: modelData.value 21 | text: modelData.text 22 | valid: !modelData.hasOwnProperty("valid") || modelData.valid === true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/infocenter/package/contents/ui/PieLegendItem.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import QtQuick 2.15 7 | import QtQuick.Controls 2.15 as QQC2 8 | import QtQuick.Layouts 1.1 9 | 10 | import org.kde.kirigami 2.20 as Kirigami 11 | 12 | RowLayout { 13 | id: legendItem 14 | 15 | property color color 16 | property string value 17 | property alias text: label.text 18 | property bool checked: true 19 | property bool valid: true 20 | 21 | Layout.maximumHeight: Kirigami.Units.gridUnit * 2 22 | 23 | Rectangle { 24 | id: dot 25 | Layout.fillHeight: true 26 | width: Kirigami.Units.iconSizes.small / 2 27 | radius: height / 2 28 | color: legendItem.checked ? legendItem.color : "transparent" 29 | border { 30 | width: legendItem.checked ? 0 : 2 31 | color: legendItem.color 32 | } 33 | } 34 | 35 | ColumnLayout { 36 | spacing: 0 37 | 38 | QQC2.Label { 39 | id: valueLabel 40 | Layout.fillWidth: true 41 | font.bold: true 42 | text: legendItem.valid ? legendItem.value : qsTr("–") 43 | visible: text !== "" 44 | } 45 | 46 | QQC2.Label { 47 | id: label 48 | Layout.fillWidth: true 49 | visible: text !== "" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | add_library(qalphacloud) 5 | add_library(QAlphaCloud ALIAS qalphacloud) 6 | 7 | set_target_properties(qalphacloud PROPERTIES 8 | VERSION ${QALPHACLOUD_VERSION} 9 | SOVERSION ${QALPHACLOUD_SOVERSION} 10 | EXPORT_NAME QAlphaCloud 11 | ) 12 | 13 | target_sources(qalphacloud PRIVATE 14 | qalphacloud.cpp 15 | qalphacloud.h 16 | apirequest.cpp 17 | apirequest.h 18 | configuration.cpp 19 | configuration.h 20 | connector.cpp 21 | connector.h 22 | lastpowerdata.cpp 23 | lastpowerdata.h 24 | onedateenergy.cpp 25 | onedateenergy.h 26 | onedaypowermodel.cpp 27 | onedaypowermodel.h 28 | storagesystemsmodel.cpp 29 | storagesystemsmodel.h 30 | utils.cpp 31 | utils_p.h 32 | ) 33 | 34 | target_include_directories(qalphacloud PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) 35 | 36 | ecm_qt_declare_logging_category(qalphacloud 37 | HEADER qalphacloud_log.h 38 | IDENTIFIER QALPHACLOUD_LOG 39 | CATEGORY_NAME qalphacloud 40 | ) 41 | 42 | ecm_generate_export_header(qalphacloud 43 | BASE_NAME QAlphaCloud 44 | VERSION ${QALPHACLOUD_VERSION} 45 | ) 46 | 47 | target_link_libraries(qalphacloud 48 | PUBLIC 49 | Qt::Network 50 | ) 51 | 52 | install(TARGETS qalphacloud EXPORT QAlphaCloudTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) 53 | 54 | ecm_generate_headers(QAlphaCloud_CamelCase_HEADERS 55 | HEADER_NAMES 56 | ApiRequest 57 | Configuration 58 | Connector 59 | LastPowerData 60 | OneDateEnergy 61 | OneDayPowerModel 62 | QAlphaCloud 63 | StorageSystemsModel 64 | REQUIRED_HEADERS QAlphaCloud_HEADERS 65 | OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/QAlphaCloud 66 | ) 67 | 68 | set(_all_headers 69 | ${QAlphaCloud_HEADERS} 70 | ${QAlphaCloud_CamelCase_HEADERS} 71 | ${CMAKE_CURRENT_BINARY_DIR}/qalphacloud_export.h 72 | ) 73 | 74 | # So that the headers are found at build time 75 | target_include_directories(qalphacloud PUBLIC "$") 76 | 77 | install( 78 | FILES ${_all_headers} 79 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/QAlphaCloud 80 | COMPONENT Devel 81 | ) 82 | -------------------------------------------------------------------------------- /src/lib/apirequest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "apirequest.h" 7 | 8 | #include "connector.h" 9 | #include "qalphacloud_log.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | namespace QAlphaCloud 24 | { 25 | 26 | class ApiRequestPrivate 27 | { 28 | public: 29 | explicit ApiRequestPrivate(ApiRequest *q) 30 | : q(q) 31 | { 32 | } 33 | 34 | void finalize() 35 | { 36 | if (m_autoDelete) { 37 | q->deleteLater(); 38 | } 39 | } 40 | 41 | ApiRequest *const q; 42 | QPointer m_reply; 43 | 44 | Connector *m_connector = nullptr; 45 | QString m_endPoint; 46 | bool m_autoDelete = true; 47 | 48 | QString m_sysSn; 49 | QDate m_queryDate; 50 | QUrlQuery m_query; 51 | 52 | QAlphaCloud::ErrorCode m_error = QAlphaCloud::ErrorCode::NoError; 53 | QString m_errorString; 54 | QJsonValue m_data; 55 | }; 56 | 57 | ApiRequest::ApiRequest(Connector *connector, QObject *parent) 58 | : ApiRequest(connector, QString(), parent) 59 | { 60 | } 61 | 62 | ApiRequest::ApiRequest(Connector *connector, const QString &endPoint, QObject *parent) 63 | : QObject(parent) 64 | , d(std::make_unique(this)) 65 | { 66 | Q_ASSERT(connector); 67 | d->m_connector = connector; 68 | d->m_endPoint = endPoint; 69 | 70 | connect(this, &ApiRequest::finished, this, [this] { 71 | d->finalize(); 72 | }); 73 | } 74 | 75 | ApiRequest::~ApiRequest() = default; 76 | 77 | QString ApiRequest::endPoint() const 78 | { 79 | return d->m_endPoint; 80 | } 81 | 82 | void ApiRequest::setEndPoint(const QString &endPoint) 83 | { 84 | d->m_endPoint = endPoint; 85 | } 86 | 87 | QString ApiRequest::sysSn() const 88 | { 89 | return d->m_sysSn; 90 | } 91 | 92 | void ApiRequest::setSysSn(const QString &sysSn) 93 | { 94 | d->m_sysSn = sysSn; 95 | } 96 | 97 | QDate ApiRequest::queryDate() const 98 | { 99 | return d->m_queryDate; 100 | } 101 | 102 | void ApiRequest::setQueryDate(const QDate &date) 103 | { 104 | d->m_queryDate = date; 105 | } 106 | 107 | QUrlQuery ApiRequest::query() const 108 | { 109 | return d->m_query; 110 | } 111 | 112 | void ApiRequest::setQuery(const QUrlQuery &query) 113 | { 114 | d->m_query = query; 115 | } 116 | 117 | bool ApiRequest::autoDelete() const 118 | { 119 | return d->m_autoDelete; 120 | } 121 | 122 | void ApiRequest::setAutoDelete(bool autoDelete) 123 | { 124 | d->m_autoDelete = autoDelete; 125 | } 126 | 127 | QAlphaCloud::ErrorCode ApiRequest::error() const 128 | { 129 | return d->m_error; 130 | } 131 | 132 | QString ApiRequest::errorString() const 133 | { 134 | return d->m_errorString; 135 | } 136 | 137 | QJsonValue ApiRequest::data() const 138 | { 139 | return d->m_data; 140 | } 141 | 142 | bool ApiRequest::send() 143 | { 144 | auto cleanup = qScopeGuard([this] { 145 | deleteLater(); 146 | }); 147 | 148 | if (!d->m_connector->networkAccessManager()) { 149 | qCCritical(QALPHACLOUD_LOG) << "Cannot send request without QNetworkAccessManager"; 150 | return false; 151 | } 152 | 153 | auto *configuration = d->m_connector->configuration(); 154 | if (!configuration) { 155 | qCCritical(QALPHACLOUD_LOG) << "Cannot send request on a Connector with no configuration"; 156 | return false; 157 | } 158 | 159 | if (!configuration->valid()) { 160 | qCCritical(QALPHACLOUD_LOG) << "Cannot send request on a Connector with an invalid configuration"; 161 | return false; 162 | } 163 | 164 | // Calculate Header fields (appId, timeStamp, sign). 165 | const QByteArray timeStampStr = QByteArray::number(QDateTime::currentSecsSinceEpoch(), 'f', 0); 166 | 167 | const QByteArray appId = configuration->appId().toUtf8(); // toLatin1? 168 | const QByteArray secret = configuration->appSecret().toUtf8(); 169 | 170 | const QByteArray sign = appId + secret + timeStampStr; 171 | const QByteArray hashedSign = QCryptographicHash::hash(sign, QCryptographicHash::Sha512).toHex(); 172 | 173 | // Generate URL. 174 | QUrl url = configuration->apiUrl(); 175 | 176 | url.setPath(QDir::cleanPath(url.path() + QLatin1Char('/') + d->m_endPoint)); 177 | 178 | // Add any additional request parameters. 179 | QUrlQuery query = d->m_query; 180 | if (!d->m_sysSn.isEmpty()) { 181 | query.addQueryItem(QStringLiteral("sysSn"), d->m_sysSn); 182 | } 183 | if (d->m_queryDate.isValid()) { 184 | query.addQueryItem(QStringLiteral("queryDate"), d->m_queryDate.toString(QStringLiteral("yyyy-MM-dd"))); 185 | } 186 | url.setQuery(query); 187 | 188 | QNetworkRequest request(url); 189 | request.setTransferTimeout(configuration->requestTimeout()); 190 | 191 | // request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false); 192 | 193 | // TODO allow spoofing stuff like User Agent. 194 | request.setHeader(QNetworkRequest::ContentLengthHeader, 0); 195 | request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); 196 | 197 | request.setRawHeader(QByteArrayLiteral("appId"), appId); 198 | request.setRawHeader(QByteArrayLiteral("timeStamp"), timeStampStr); 199 | request.setRawHeader(QByteArrayLiteral("sign"), hashedSign); 200 | 201 | qCDebug(QALPHACLOUD_LOG) << "Sending API request to" << url; 202 | 203 | d->m_error = QAlphaCloud::ErrorCode::NoError; 204 | d->m_errorString.clear(); 205 | d->m_data = QJsonObject(); 206 | 207 | auto *reply = d->m_connector->networkAccessManager()->get(request); 208 | connect(reply, &QNetworkReply::finished, this, [this, reply] { 209 | if (reply->error() != QNetworkReply::NoError) { 210 | if (reply->error() == QNetworkReply::OperationCanceledError) { 211 | qCDebug(QALPHACLOUD_LOG) << "API request for endpoint" << reply->url() << "was canceled"; 212 | } else { 213 | qCWarning(QALPHACLOUD_LOG) << "API request for endpoint" << reply->url() << "failed with network error" << reply->errorString(); 214 | } 215 | d->m_error = static_cast(reply->error()); 216 | d->m_errorString = reply->errorString(); 217 | Q_EMIT errorOccurred(); 218 | } else { 219 | int code = -1; 220 | QJsonParseError error; 221 | QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll(), &error); 222 | 223 | if (error.error != QJsonParseError::NoError) { 224 | d->m_error = ErrorCode::JsonParseError; 225 | d->m_errorString = QAlphaCloud::errorText(d->m_error, error.errorString()); 226 | } else if (!jsonDocument.isObject()) { 227 | d->m_error = ErrorCode::UnexpectedJsonDataError; 228 | d->m_errorString = QAlphaCloud::errorText(d->m_error, jsonDocument); 229 | } else { 230 | const QJsonObject jsonObject = jsonDocument.object(); 231 | if (jsonObject.isEmpty()) { 232 | d->m_error = ErrorCode::EmptyJsonObjectError; 233 | } else { 234 | code = jsonObject.value(QStringLiteral("code")).toInt(); 235 | if (code != 200) { 236 | d->m_error = static_cast(code); 237 | const QString msg = jsonObject.value(QStringLiteral("msg")).toString(); 238 | d->m_errorString = QAlphaCloud::errorText(d->m_error, msg); 239 | } 240 | 241 | d->m_data = jsonObject.value(QStringLiteral("data")); 242 | } 243 | } 244 | 245 | if (d->m_error != QAlphaCloud::ErrorCode::NoError) { 246 | qCWarning(QALPHACLOUD_LOG) << "API request for endpoint" << reply->url() << "failed with API error" << d->m_error << code << d->m_errorString; 247 | Q_EMIT errorOccurred(); 248 | } else { 249 | qCDebug(QALPHACLOUD_LOG) << "API request for endpoint" << reply->url() << "succeeded"; 250 | Q_EMIT result(); 251 | } 252 | } 253 | 254 | Q_EMIT finished(); 255 | }); 256 | connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); 257 | 258 | d->m_reply = reply; 259 | 260 | cleanup.dismiss(); 261 | 262 | return true; 263 | } 264 | 265 | void ApiRequest::abort() 266 | { 267 | if (d->m_reply) { 268 | d->m_reply->abort(); 269 | d->m_reply = nullptr; 270 | } 271 | 272 | d->finalize(); 273 | } 274 | 275 | } // namespace QAlphaCloud 276 | -------------------------------------------------------------------------------- /src/lib/apirequest.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "qalphacloud.h" 15 | 16 | #include 17 | 18 | namespace QAlphaCloud 19 | { 20 | 21 | class ApiRequestPrivate; 22 | class Connector; 23 | 24 | /** 25 | * @brief API request job 26 | * 27 | * This class handles all network communication to the API. 28 | * It implements a job which will self-delete when finished. 29 | * 30 | * Normally, you don't need to use this class directly but it can 31 | * be handy to issue API requests for which this library provides 32 | * no wrapper class. 33 | */ 34 | class Q_DECL_EXPORT ApiRequest : public QObject 35 | { 36 | Q_OBJECT 37 | 38 | public: 39 | /** 40 | * @brief The API endpoints 41 | */ 42 | struct EndPoint { 43 | static constexpr QLatin1String EssList{"getEssList"}; 44 | static constexpr QLatin1String LastPowerData{"getLastPowerData"}; 45 | static constexpr QLatin1String OneDayPowerBySn{"getOneDayPowerBySn"}; 46 | static constexpr QLatin1String OneDateEnergyBySn{"getOneDateEnergyBySn"}; 47 | }; 48 | 49 | /** 50 | * @brief Create an API request 51 | * @param connector The Connector to use, must not be null. 52 | * @param parent The parent, if wanted. 53 | */ 54 | explicit ApiRequest(Connector *connector, QObject *parent = nullptr); 55 | /** 56 | * @brief Create an API request 57 | * @param connector The Connector to use, must not be null. 58 | * @param endPoint The API endpoint to talk to. 59 | * @param parent The parent, if wanted. 60 | */ 61 | ApiRequest(Connector *connector, const QString &endPoint, QObject *parent = nullptr); 62 | /** 63 | * @brief Destroys the API request 64 | */ 65 | ~ApiRequest() override; 66 | 67 | /** 68 | * @brief The API endpoint to use 69 | */ 70 | QString endPoint() const; 71 | /** 72 | * @brief Set the API endpoint to use 73 | * 74 | * This can be any one of EndPoint or any string. 75 | * A trailing slash is not required. 76 | */ 77 | void setEndPoint(const QString &endPoint); 78 | 79 | /** 80 | * @brief The storage system serial number 81 | */ 82 | QString sysSn() const; 83 | /** 84 | * @brief Set the storage system serial number 85 | * 86 | * For APIs that apply to a specific storage system. 87 | */ 88 | void setSysSn(const QString &sysSn); 89 | 90 | /** 91 | * @brief The query date 92 | */ 93 | QDate queryDate() const; 94 | /** 95 | * @brief Set the query date 96 | * 97 | * For APIs that return data for a given date. 98 | */ 99 | void setQueryDate(const QDate &date); 100 | 101 | /** 102 | * @brief Custom query arguments 103 | */ 104 | QUrlQuery query() const; 105 | /** 106 | * @brief Set custom query arguments 107 | * 108 | * In case sysSn and queryDate are not sufficient for a given 109 | * API call, arbitrary query parameters can be provided. 110 | */ 111 | void setQuery(const QUrlQuery &query); 112 | 113 | /** 114 | * @brief Whether the job auto-deletes when finished 115 | */ 116 | bool autoDelete() const; 117 | /** 118 | * @brief Set whether to auto-delete the job when finished 119 | * 120 | * Default is true. 121 | */ 122 | void setAutoDelete(bool autoDelete); 123 | 124 | /** 125 | * @brief The error, if any. 126 | */ 127 | QAlphaCloud::ErrorCode error() const; 128 | /** 129 | * @brief The error string, if any. 130 | * 131 | * @note Not every error code has an errorString associated with it. 132 | */ 133 | QString errorString() const; 134 | 135 | /** 136 | * @brief The data the API returned. 137 | * 138 | * This can be either a JSON object or JSON array (or null) 139 | * depending on the API. 140 | */ 141 | QJsonValue data() const; 142 | 143 | /** 144 | * @brief Send the event 145 | * 146 | * @return Whether the request was sent. 147 | */ 148 | Q_INVOKABLE bool send(); 149 | 150 | /** 151 | * @brief Abort the event 152 | */ 153 | Q_INVOKABLE void abort(); 154 | 155 | Q_SIGNALS: 156 | /** 157 | * @brief Emitted when the request finished. 158 | * 159 | * This is emitted regardless of whether the request succeeded or failed. 160 | */ 161 | void finished(); 162 | 163 | /** 164 | * @brief Emitted when an API result received. 165 | * 166 | * This is emitted when the API request completed successfully. 167 | */ 168 | void result(); 169 | /** 170 | * @brief Emitted when an error occurred. 171 | * 172 | * This can either be a network error or an error returned by the API. 173 | * 174 | * @sa networkError() 175 | * @sa apiError() 176 | */ 177 | void errorOccurred(); 178 | 179 | // TODO void aborted(); ? 180 | 181 | private: 182 | std::unique_ptr const d; 183 | }; 184 | 185 | } // namespace QAlphaCloud 186 | -------------------------------------------------------------------------------- /src/lib/configuration.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "configuration.h" 7 | 8 | #include "config-alphacloud.h" 9 | #include "qalphacloud_log.h" 10 | 11 | #include 12 | #include 13 | 14 | namespace QAlphaCloud 15 | { 16 | 17 | static int g_defaultTimeout = 30000; 18 | 19 | class ConfigurationPrivate 20 | { 21 | public: 22 | QUrl apiUrl; 23 | QString appId; 24 | QString appSecret; 25 | int requestTimeout = g_defaultTimeout; 26 | }; 27 | 28 | Configuration::Configuration(QObject *parent) 29 | : QObject(parent) 30 | , d(std::make_unique()) 31 | { 32 | resetApiUrl(); 33 | } 34 | 35 | Configuration::~Configuration() = default; 36 | 37 | Configuration *Configuration::defaultConfiguration(QObject *parent) 38 | { 39 | auto *configuration = new Configuration(parent); 40 | configuration->loadDefault(); 41 | return configuration; 42 | } 43 | 44 | QString Configuration::defaultConfigurationPath() 45 | { 46 | return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/qalphacloud.ini"); 47 | } 48 | 49 | QUrl Configuration::apiUrl() const 50 | { 51 | return d->apiUrl; 52 | } 53 | 54 | void Configuration::setApiUrl(const QUrl &apiUrl) 55 | { 56 | if (d->apiUrl == apiUrl) { 57 | return; 58 | } 59 | 60 | const bool oldValid = valid(); 61 | 62 | d->apiUrl = apiUrl; 63 | Q_EMIT apiUrlChanged(apiUrl); 64 | 65 | if (oldValid != valid()) { 66 | Q_EMIT validChanged(valid()); 67 | } 68 | } 69 | 70 | void Configuration::resetApiUrl() 71 | { 72 | setApiUrl(QUrl(QStringLiteral(API_URL))); 73 | } 74 | 75 | QString Configuration::appId() const 76 | { 77 | return d->appId; 78 | } 79 | 80 | void Configuration::setAppId(const QString &appId) 81 | { 82 | if (d->appId == appId) { 83 | return; 84 | } 85 | 86 | const bool oldValid = valid(); 87 | 88 | d->appId = appId; 89 | Q_EMIT appIdChanged(appId); 90 | 91 | if (oldValid != valid()) { 92 | Q_EMIT validChanged(valid()); 93 | } 94 | } 95 | 96 | QString Configuration::appSecret() const 97 | { 98 | return d->appSecret; 99 | } 100 | 101 | void Configuration::setAppSecret(const QString &appSecret) 102 | { 103 | if (d->appSecret == appSecret) { 104 | return; 105 | } 106 | 107 | const bool oldValid = valid(); 108 | 109 | d->appSecret = appSecret; 110 | Q_EMIT appSecretChanged(appSecret); 111 | 112 | if (oldValid != valid()) { 113 | Q_EMIT validChanged(valid()); 114 | } 115 | } 116 | 117 | int Configuration::requestTimeout() const 118 | { 119 | return d->requestTimeout; 120 | } 121 | 122 | void Configuration::setRequestTimeout(int requestTimeout) 123 | { 124 | if (d->requestTimeout == requestTimeout) { 125 | return; 126 | } 127 | 128 | if (requestTimeout < 0) { 129 | return; 130 | } 131 | 132 | d->requestTimeout = requestTimeout; 133 | Q_EMIT requestTimeoutChanged(requestTimeout); 134 | } 135 | 136 | void Configuration::resetRequestTimeout() 137 | { 138 | setRequestTimeout(g_defaultTimeout); 139 | } 140 | 141 | bool Configuration::valid() const 142 | { 143 | return d->apiUrl.isValid() && !d->appId.isEmpty() && !d->appSecret.isEmpty(); 144 | } 145 | 146 | bool Configuration::loadFromFile(const QString &path) 147 | { 148 | QSettings settings(path, QSettings::IniFormat); 149 | return loadFromSettings(&settings); 150 | } 151 | 152 | bool Configuration::loadFromSettings(QSettings *settings) 153 | { 154 | if (!settings) { 155 | return false; 156 | } 157 | 158 | if (settings->status() != QSettings::NoError) { 159 | qCWarning(QALPHACLOUD_LOG) << "Failed to load configuration" << settings->fileName() << settings->status(); 160 | return false; 161 | } 162 | 163 | qCDebug(QALPHACLOUD_LOG) << "Reading configuration from" << settings->fileName(); 164 | 165 | const QUrl defaultUrl = QUrl(QStringLiteral(API_URL)); 166 | 167 | QUrl apiUrl = settings->value(QStringLiteral("Api/ApiUrl"), defaultUrl).toUrl(); 168 | if (!apiUrl.isValid()) { 169 | apiUrl = defaultUrl; 170 | } 171 | const QString appId = settings->value(QStringLiteral("Api/AppId"), QString()).toString(); 172 | const QString appSecret = settings->value(QStringLiteral("Api/AppSecret"), QString()).toString(); 173 | 174 | bool ok; 175 | int timeout = settings->value(QStringLiteral("Api/Timeout"), g_defaultTimeout).toInt(&ok); 176 | if (!ok) { 177 | timeout = g_defaultTimeout; 178 | } 179 | 180 | setApiUrl(apiUrl); 181 | setAppId(appId); 182 | setAppSecret(appSecret); 183 | setRequestTimeout(timeout); 184 | 185 | return valid(); 186 | } 187 | 188 | bool Configuration::loadDefault() 189 | { 190 | return loadFromFile(defaultConfigurationPath()); 191 | } 192 | 193 | } // namespace QAlphaCloud 194 | -------------------------------------------------------------------------------- /src/lib/configuration.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include "qalphacloud.h" 15 | 16 | #include "qalphacloud_export.h" 17 | 18 | class QIODevice; 19 | class QSettings; 20 | 21 | namespace QAlphaCloud 22 | { 23 | 24 | class ConfigurationPrivate; 25 | 26 | /** 27 | * @brief API configuration 28 | * 29 | * This class provides the configuration of your API connector, i.e. the URL, App ID and secret. 30 | * 31 | * Additionally, the request timeout can be configured. 32 | * 33 | * The default configuration can be placed in an INI file `~/.config/Broulik/QAlphaloud`: 34 | * ``` 35 | * [Api] 36 | * AppId=alpha... 37 | * AppSecret=... 38 | * ``` 39 | */ 40 | class QALPHACLOUD_EXPORT Configuration : public QObject 41 | { 42 | Q_OBJECT 43 | 44 | /** 45 | * @brief The URL to send requests to 46 | * 47 | * Default is the `API_URL` configure option, which by default is the official API endpoint. 48 | */ 49 | Q_PROPERTY(QUrl apiUrl READ apiUrl WRITE setApiUrl RESET resetApiUrl NOTIFY apiUrlChanged) 50 | 51 | /** 52 | * @brief The App ID 53 | * 54 | * The application ID registered on the API. Typically starts with `alpha...` 55 | */ 56 | Q_PROPERTY(QString appId READ appId WRITE setAppId NOTIFY appIdChanged) 57 | 58 | /** 59 | * @brief The API secret 60 | */ 61 | Q_PROPERTY(QString appSecret READ appSecret WRITE setAppSecret NOTIFY appSecretChanged) 62 | 63 | /** 64 | * @brief Request timeout (ms). 65 | * 66 | * Time out requests automatically after ms. 67 | * 68 | * Default is 30,000 (30 seconds). 69 | */ 70 | Q_PROPERTY(int requestTimeout READ requestTimeout WRITE setRequestTimeout RESET resetRequestTimeout NOTIFY requestTimeoutChanged) 71 | 72 | /** 73 | * @brief Whether the configuration is valid 74 | * 75 | * You cannot make requests with an invalid configuration. 76 | */ 77 | Q_PROPERTY(bool valid READ valid NOTIFY validChanged) 78 | 79 | public: 80 | /** 81 | * @brief Create a configuration 82 | * 83 | * It will be empty and invalid. 84 | * 85 | * @param parent The owner. 86 | */ 87 | explicit Configuration(QObject *parent = nullptr); 88 | ~Configuration() override; 89 | 90 | /** 91 | * @brief Create a default configuration. 92 | * 93 | * This is typically what you want to pass to the @c Connector. 94 | * It will be read from the configuration file. 95 | */ 96 | static Configuration *defaultConfiguration(QObject *parent = nullptr); 97 | 98 | /** 99 | * @brief The default configuration path. 100 | * 101 | * This is is "qalphacloud.ini" within the user's configuration directory. 102 | * 103 | * @note This does not check if this file exists. 104 | * 105 | * @return The path to the default configuration file. 106 | */ 107 | static QString defaultConfigurationPath(); 108 | 109 | Q_REQUIRED_RESULT QUrl apiUrl() const; 110 | void setApiUrl(const QUrl &apiUrl); 111 | /** 112 | * @brief Reset API URL to the default 113 | */ 114 | void resetApiUrl(); 115 | Q_SIGNAL void apiUrlChanged(const QUrl &apiUrl); 116 | 117 | Q_REQUIRED_RESULT QString appId() const; 118 | void setAppId(const QString &appId); 119 | Q_SIGNAL void appIdChanged(const QString &appId); 120 | 121 | Q_REQUIRED_RESULT QString appSecret() const; 122 | void setAppSecret(const QString &appSecret); 123 | Q_SIGNAL void appSecretChanged(const QString &appSecret); 124 | 125 | Q_REQUIRED_RESULT int requestTimeout() const; 126 | void setRequestTimeout(int requestTimeout); 127 | void resetRequestTimeout(); 128 | Q_SIGNAL void requestTimeoutChanged(int requestTimeout); 129 | 130 | bool valid() const; 131 | Q_SIGNAL void validChanged(bool valid); 132 | 133 | public Q_SLOTS: 134 | 135 | /** 136 | * @brief Load configuration from file 137 | * @param path The file path 138 | * @return Whether the configuration could be loaded and is valid. 139 | */ 140 | bool loadFromFile(const QString &path); 141 | // TODO QSettings doesn't support reading from QIODevice :-( 142 | // bool loadFromDevice(QIODevice *device); 143 | /** 144 | * @brief Load configuration from QSettings 145 | * @param settings The QSettings object 146 | * @return Whether the configuration could be loaded and is valid. 147 | */ 148 | bool loadFromSettings(QSettings *settings); 149 | /** 150 | * @brief Load the default configuration 151 | * 152 | * It will be loaded from the configuration file. 153 | * @return Whether the configuration could be loaded and is valid. 154 | */ 155 | bool loadDefault(); 156 | 157 | private: 158 | std::unique_ptr const d; 159 | }; 160 | 161 | } // namespace QAlphaCloud 162 | -------------------------------------------------------------------------------- /src/lib/connector.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "connector.h" 7 | 8 | #include "qalphacloud_log.h" 9 | 10 | namespace QAlphaCloud 11 | { 12 | 13 | class ConnectorPrivate 14 | { 15 | public: 16 | Configuration *configuration = nullptr; 17 | QNetworkAccessManager *networkAccessManager = nullptr; 18 | }; 19 | 20 | Connector::Connector(QObject *parent) 21 | : Connector(nullptr, parent) 22 | { 23 | } 24 | 25 | Connector::Connector(Configuration *configuration, QObject *parent) 26 | : QObject(parent) 27 | , d(std::make_unique()) 28 | { 29 | d->configuration = configuration; 30 | if (d->configuration) { 31 | d->configuration->setParent(this); 32 | } 33 | } 34 | 35 | Connector::~Connector() = default; 36 | 37 | Configuration *Connector::configuration() const 38 | { 39 | return d->configuration; 40 | } 41 | 42 | void Connector::setConfiguration(Configuration *configuration) 43 | { 44 | if (d->configuration == configuration) { 45 | return; 46 | } 47 | 48 | if (d->configuration) { 49 | disconnect(d->configuration, nullptr, this, nullptr); 50 | } 51 | 52 | const bool oldValid = valid(); 53 | d->configuration = configuration; 54 | 55 | if (d->configuration) { 56 | // TODO only emit if it effectively changed. 57 | connect(d->configuration, &Configuration::validChanged, this, &Connector::validChanged); 58 | } 59 | 60 | Q_EMIT configurationChanged(configuration); 61 | 62 | if (oldValid != valid()) { 63 | Q_EMIT validChanged(valid()); 64 | } 65 | } 66 | 67 | bool Connector::valid() const 68 | { 69 | return d->configuration && d->configuration->valid() && d->networkAccessManager != nullptr; 70 | } 71 | 72 | QNetworkAccessManager *Connector::networkAccessManager() const 73 | { 74 | return d->networkAccessManager; 75 | } 76 | 77 | void Connector::setNetworkAccessManager(QNetworkAccessManager *networkAccessManager) 78 | { 79 | if (d->networkAccessManager == networkAccessManager) { 80 | return; 81 | } 82 | 83 | const bool oldValid = valid(); 84 | d->networkAccessManager = networkAccessManager; 85 | 86 | if (oldValid != valid()) { 87 | Q_EMIT validChanged(valid()); 88 | } 89 | } 90 | 91 | } // namespace QAlphaCloud 92 | -------------------------------------------------------------------------------- /src/lib/connector.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include "configuration.h" 15 | #include "qalphacloud.h" 16 | 17 | #include "qalphacloud_export.h" 18 | 19 | class QNetworkAccessManager; 20 | 21 | namespace QAlphaCloud 22 | { 23 | 24 | class ConnectorPrivate; 25 | 26 | /** 27 | * @brief API Connection 28 | * 29 | * This class represents an API connection with the given configuration. 30 | * It is required by all request classes. 31 | * 32 | * @note You need to set a @a QNetworkAccessManager on this object 33 | * in order to send requests. 34 | * 35 | * In QML, the @a QNetworkAccessManager is automatically assigned from the 36 | * current QQmlEngine on component completion. 37 | */ 38 | class QALPHACLOUD_EXPORT Connector : public QObject 39 | { 40 | Q_OBJECT 41 | 42 | /** 43 | * @brief The configuration to use 44 | */ 45 | Q_PROPERTY(QAlphaCloud::Configuration *configuration READ configuration WRITE setConfiguration NOTIFY configurationChanged) 46 | 47 | /** 48 | * @brief Whether this connector is valid 49 | * 50 | * This means that it has a valid configuration and has a QNetworkAccessManager 51 | */ 52 | Q_PROPERTY(bool valid READ valid NOTIFY validChanged) 53 | 54 | public: 55 | explicit Connector(QObject *parent = nullptr); 56 | explicit Connector(Configuration *configuration, QObject *parent = nullptr); 57 | ~Connector() override; 58 | 59 | Q_REQUIRED_RESULT Configuration *configuration() const; 60 | void setConfiguration(Configuration *configuration); 61 | Q_SIGNAL void configurationChanged(QAlphaCloud::Configuration *configuration); 62 | 63 | Q_REQUIRED_RESULT bool valid() const; 64 | Q_SIGNAL void validChanged(bool valid); 65 | 66 | Q_REQUIRED_RESULT QNetworkAccessManager *networkAccessManager() const; 67 | /** 68 | * @brief Set the QNetworkAccessManager 69 | * 70 | * It will be used for all network requests. Without it, no requests can be sent. 71 | * @param networkAccessManager The QNetworkAccessManager to use. 72 | * 73 | * In QML, the @a QNetworkAccessManager is automatically assigned from the 74 | * current QQmlEngine on component completion, unless one has already been 75 | * assigned manually before. 76 | */ 77 | void setNetworkAccessManager(QNetworkAccessManager *networkAccessManager); 78 | 79 | private: 80 | std::unique_ptr const d; 81 | }; 82 | 83 | } // namespace QAlphaCloud 84 | -------------------------------------------------------------------------------- /src/lib/lastpowerdata.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "lastpowerdata.h" 7 | 8 | #include "apirequest.h" 9 | #include "qalphacloud_log.h" 10 | #include "utils_p.h" 11 | 12 | #include 13 | 14 | namespace QAlphaCloud 15 | { 16 | 17 | class LastPowerDataPrivate 18 | { 19 | public: 20 | LastPowerDataPrivate(LastPowerData *q); 21 | 22 | void setStatus(RequestStatus status); 23 | void setError(ErrorCode error); 24 | void setErrorString(const QString &errorString); 25 | 26 | void processApiResult(const QJsonObject &json); 27 | 28 | LastPowerData *const q; 29 | 30 | // TODO QPointer? 31 | Connector *m_connector = nullptr; 32 | QString m_serialNumber; 33 | 34 | int m_photovoltaicPower = 0; 35 | int m_currentLoad = 0; 36 | int m_gridPower = 0; 37 | int m_batteryPower = 0; 38 | 39 | qreal m_batterySoc = 0.0; 40 | 41 | QJsonObject m_json; 42 | RequestStatus m_status = RequestStatus::NoRequest; 43 | ErrorCode m_error = ErrorCode::NoError; 44 | QString m_errorString; 45 | 46 | bool m_valid = false; 47 | 48 | QPointer m_request; 49 | }; 50 | 51 | LastPowerDataPrivate::LastPowerDataPrivate(LastPowerData *q) 52 | : q(q) 53 | { 54 | } 55 | 56 | void LastPowerDataPrivate::setStatus(RequestStatus status) 57 | { 58 | if (m_status != status) { 59 | m_status = status; 60 | Q_EMIT q->statusChanged(status); 61 | } 62 | } 63 | 64 | void LastPowerDataPrivate::setError(ErrorCode error) 65 | { 66 | if (m_error != error) { 67 | m_error = error; 68 | Q_EMIT q->errorChanged(error); 69 | } 70 | } 71 | 72 | void LastPowerDataPrivate::setErrorString(const QString &errorString) 73 | { 74 | if (m_errorString != errorString) { 75 | m_errorString = errorString; 76 | Q_EMIT q->errorStringChanged(errorString); 77 | } 78 | } 79 | 80 | void LastPowerDataPrivate::processApiResult(const QJsonObject &json) 81 | { 82 | bool valid = false; 83 | 84 | const auto photovoltaicPower = json.value(QStringLiteral("ppv")).toInt(); 85 | Utils::updateField(m_photovoltaicPower, photovoltaicPower, q, &LastPowerData::photovoltaicPowerChanged); 86 | 87 | const auto currentLoadValue = json.value(QStringLiteral("pload")); 88 | const auto currentLoad = currentLoadValue.toInt(); 89 | Utils::updateField(m_currentLoad, currentLoad, q, &LastPowerData::currentLoadChanged); 90 | valid = valid || (!currentLoadValue.isUndefined() && !currentLoadValue.isNull()); 91 | 92 | const auto batterySocValue = json.value(QStringLiteral("soc")); 93 | const auto batterySoc = batterySocValue.toDouble(); 94 | Utils::updateField(m_batterySoc, batterySoc, q, &LastPowerData::batterySocChanged); 95 | valid = valid || (!batterySocValue.isUndefined() && !batterySocValue.isNull()); 96 | 97 | const auto gridPowerValue = json.value(QStringLiteral("pgrid")); 98 | const auto gridPower = gridPowerValue.toInt(); 99 | Utils::updateField(m_gridPower, gridPower, q, &LastPowerData::gridPowerChanged); 100 | valid = valid || (!gridPowerValue.isUndefined() && !gridPowerValue.isNull()); 101 | 102 | const auto batteryPowerValue = json.value(QStringLiteral("pbat")); 103 | const auto batteryPower = batteryPowerValue.toInt(); 104 | Utils::updateField(m_batteryPower, batteryPower, q, &LastPowerData::batteryPowerChanged); 105 | valid = valid || (!batteryPowerValue.isUndefined() && !batteryPowerValue.isNull()); 106 | 107 | // TODO pev 108 | 109 | if (m_json != json) { 110 | m_json = json; 111 | Q_EMIT q->rawJsonChanged(); 112 | } 113 | 114 | if (m_valid != valid) { 115 | m_valid = valid; 116 | Q_EMIT q->validChanged(valid); 117 | } 118 | 119 | setStatus(RequestStatus::Finished); 120 | } 121 | 122 | LastPowerData::LastPowerData(QObject *parent) 123 | : LastPowerData(nullptr, QString(), parent) 124 | { 125 | } 126 | 127 | LastPowerData::LastPowerData(Connector *connector, const QString &serialNumber, QObject *parent) 128 | : QObject(parent) 129 | , d(std::make_unique(this)) 130 | { 131 | setConnector(connector); 132 | 133 | d->m_serialNumber = serialNumber; 134 | } 135 | 136 | LastPowerData::~LastPowerData() = default; 137 | 138 | Connector *LastPowerData::connector() const 139 | { 140 | return d->m_connector; 141 | } 142 | 143 | void LastPowerData::setConnector(Connector *connector) 144 | { 145 | if (d->m_connector == connector) { 146 | return; 147 | } 148 | 149 | d->m_connector = connector; 150 | reset(); 151 | Q_EMIT connectorChanged(connector); 152 | } 153 | 154 | QString LastPowerData::serialNumber() const 155 | { 156 | return d->m_serialNumber; 157 | } 158 | 159 | void LastPowerData::setSerialNumber(const QString &serialNumber) 160 | { 161 | if (d->m_serialNumber == serialNumber) { 162 | return; 163 | } 164 | 165 | d->m_serialNumber = serialNumber; 166 | reset(); 167 | Q_EMIT serialNumberChanged(serialNumber); 168 | } 169 | 170 | int LastPowerData::photovoltaicPower() const 171 | { 172 | return d->m_photovoltaicPower; 173 | } 174 | 175 | int LastPowerData::currentLoad() const 176 | { 177 | return d->m_currentLoad; 178 | } 179 | 180 | int LastPowerData::gridPower() const 181 | { 182 | return d->m_gridPower; 183 | } 184 | 185 | int LastPowerData::batteryPower() const 186 | { 187 | return d->m_batteryPower; 188 | } 189 | 190 | qreal LastPowerData::batterySoc() const 191 | { 192 | return d->m_batterySoc; 193 | } 194 | 195 | QJsonObject LastPowerData::rawJson() const 196 | { 197 | return d->m_json; 198 | } 199 | 200 | bool LastPowerData::valid() const 201 | { 202 | return d->m_valid; 203 | } 204 | 205 | RequestStatus LastPowerData::status() const 206 | { 207 | return d->m_status; 208 | } 209 | 210 | QAlphaCloud::ErrorCode LastPowerData::error() const 211 | { 212 | return d->m_error; 213 | } 214 | 215 | QString LastPowerData::errorString() const 216 | { 217 | return d->m_errorString; 218 | } 219 | 220 | bool LastPowerData::reload() 221 | { 222 | if (!d->m_connector) { 223 | qCWarning(QALPHACLOUD_LOG) << "Cannot load LastPowerData without a connector"; 224 | return false; 225 | } 226 | 227 | if (d->m_serialNumber.isEmpty()) { 228 | qCWarning(QALPHACLOUD_LOG) << "Cannot load LastPowerData without a serial number"; 229 | return false; 230 | } 231 | 232 | if (d->m_request) { 233 | qCDebug(QALPHACLOUD_LOG) << "Cancelling LastPowerData request in-flight"; 234 | d->m_request->abort(); 235 | d->m_request = nullptr; 236 | } 237 | 238 | auto *request = new ApiRequest(d->m_connector, ApiRequest::EndPoint::LastPowerData, this); 239 | request->setSysSn(d->m_serialNumber); 240 | 241 | connect(request, &ApiRequest::errorOccurred, this, [this, request] { 242 | d->setError(request->error()); 243 | d->setErrorString(request->errorString()); 244 | d->setStatus(RequestStatus::Error); 245 | }); 246 | 247 | connect(request, &ApiRequest::result, this, [this, request] { 248 | const QJsonObject json = request->data().toObject(); 249 | 250 | d->processApiResult(json); 251 | }); 252 | 253 | const bool ok = request->send(); 254 | 255 | if (ok) { 256 | d->m_request = request; 257 | 258 | d->setStatus(QAlphaCloud::RequestStatus::Loading); 259 | } 260 | 261 | return ok; 262 | } 263 | 264 | void LastPowerData::reset() 265 | { 266 | if (d->m_request) { 267 | d->m_request->abort(); 268 | d->m_request = nullptr; 269 | } 270 | d->processApiResult(QJsonObject()); 271 | d->setStatus(RequestStatus::NoRequest); 272 | } 273 | 274 | } // namespace QAlphaCloud 275 | -------------------------------------------------------------------------------- /src/lib/lastpowerdata.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include "connector.h" 15 | #include "qalphacloud.h" 16 | 17 | namespace QAlphaCloud 18 | { 19 | 20 | class LastPowerDataPrivate; 21 | 22 | /** 23 | * @brief Live Power Data 24 | * 25 | * Provides live information. 26 | * 27 | * Wraps the @c /getLastPowerData API endpoint. 28 | */ 29 | class Q_DECL_EXPORT LastPowerData : public QObject 30 | { 31 | Q_OBJECT 32 | 33 | /** 34 | * @brief The connector to use 35 | */ 36 | Q_PROPERTY(QAlphaCloud::Connector *connector READ connector WRITE setConnector NOTIFY connectorChanged REQUIRED) 37 | 38 | /** 39 | * @brief The serial number 40 | * 41 | * The serial number of the storage system whose data should be queried. 42 | */ 43 | Q_PROPERTY(QString serialNumber READ serialNumber WRITE setSerialNumber NOTIFY serialNumberChanged REQUIRED) 44 | 45 | /** 46 | * @brief Photovoltaic power in W. 47 | */ 48 | Q_PROPERTY(int photovoltaicPower READ photovoltaicPower NOTIFY photovoltaicPowerChanged) 49 | /** 50 | * @brief Current load in W. 51 | */ 52 | Q_PROPERTY(int currentLoad READ currentLoad NOTIFY currentLoadChanged) 53 | // TODO add enum for FeedingToGrid / ConsumingGridPower 54 | // and/or add gridFeed/gridUse properties. 55 | /** 56 | * @brief Grid power in W. 57 | * 58 | * * When this is negative, power is being fed into the grid. 59 | * * When this is positive, power is drawn from the grid. 60 | */ 61 | Q_PROPERTY(int gridPower READ gridPower NOTIFY gridPowerChanged) 62 | // TODO add enum for NoCharge / Charging / Discharging / FullyCharged 63 | /** 64 | * @brief Battery power in W. 65 | */ 66 | Q_PROPERTY(int batteryPower READ batteryPower NOTIFY batteryPowerChanged) 67 | // TODO pev 68 | 69 | /** 70 | * @brief Battery state of charge in %. 71 | */ 72 | Q_PROPERTY(qreal batterySoc READ batterySoc NOTIFY batterySocChanged) 73 | 74 | /** 75 | * @brief Raw JSON 76 | * 77 | * The raw JSON returned by the API, useful for extracting data 78 | * that isn't provided through the API yet. 79 | */ 80 | Q_PROPERTY(QJsonObject rawJson READ rawJson NOTIFY rawJsonChanged) 81 | 82 | /** 83 | * @brief Whether this object contains data 84 | * 85 | * This is independent of the status. The status can be QAlphaCloud::Error 86 | * when a subsequent request fails but any data isn't cleared unless 87 | * new data is loaded successfully. 88 | */ 89 | Q_PROPERTY(bool valid READ valid NOTIFY validChanged STORED false) 90 | 91 | /** 92 | * @brief The current request status 93 | */ 94 | Q_PROPERTY(QAlphaCloud::RequestStatus status READ status NOTIFY statusChanged) 95 | 96 | /** 97 | * @brief The error, if any 98 | * 99 | * There can still be valid data in this object from a previous 100 | * successful request. 101 | */ 102 | Q_PROPERTY(QAlphaCloud::ErrorCode error READ error NOTIFY errorChanged) 103 | /** 104 | * @brief The error string, if any 105 | * 106 | * @note Not every error code has an errorString associated with it. 107 | */ 108 | Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) 109 | 110 | public: 111 | /** 112 | * @brief Creates a LastPowerData instance 113 | * @param parent The owner 114 | * 115 | * @note A connector and serialNumber must be set before requests can be made. 116 | */ 117 | explicit LastPowerData(QObject *parent = nullptr); 118 | /** 119 | * @brief Creates a LastPowerData intsance 120 | * @param connector The connector 121 | * @param serialNumber The serial number of the storage system whose data should be queried 122 | * @param parent The owner 123 | */ 124 | LastPowerData(QAlphaCloud::Connector *connector, const QString &serialNumber, QObject *parent = nullptr); 125 | ~LastPowerData() override; 126 | 127 | Q_REQUIRED_RESULT QAlphaCloud::Connector *connector() const; 128 | void setConnector(QAlphaCloud::Connector *connector); 129 | Q_SIGNAL void connectorChanged(QAlphaCloud::Connector *connector); 130 | 131 | Q_REQUIRED_RESULT QString serialNumber() const; 132 | void setSerialNumber(const QString &serialNumber); 133 | Q_SIGNAL void serialNumberChanged(const QString &serialNumber); 134 | 135 | Q_REQUIRED_RESULT int photovoltaicPower() const; 136 | Q_SIGNAL void photovoltaicPowerChanged(int photovoltaicPower); 137 | 138 | Q_REQUIRED_RESULT int currentLoad() const; 139 | Q_SIGNAL void currentLoadChanged(int currentLoad); 140 | 141 | Q_REQUIRED_RESULT int gridPower() const; 142 | Q_SIGNAL void gridPowerChanged(int gridPower); 143 | 144 | Q_REQUIRED_RESULT int batteryPower() const; 145 | Q_SIGNAL void batteryPowerChanged(int batteryPower); 146 | 147 | Q_REQUIRED_RESULT qreal batterySoc() const; 148 | Q_SIGNAL void batterySocChanged(qreal batterySoc); 149 | 150 | Q_REQUIRED_RESULT QJsonObject rawJson() const; 151 | Q_SIGNAL void rawJsonChanged(); 152 | 153 | Q_REQUIRED_RESULT bool valid() const; 154 | Q_SIGNAL void validChanged(bool valid); 155 | 156 | Q_REQUIRED_RESULT QAlphaCloud::RequestStatus status() const; 157 | Q_SIGNAL void statusChanged(QAlphaCloud::RequestStatus status); 158 | 159 | QAlphaCloud::ErrorCode error() const; 160 | Q_SIGNAL void errorChanged(QAlphaCloud::ErrorCode error); 161 | 162 | QString errorString() const; 163 | Q_SIGNAL void errorStringChanged(const QString &errorString); 164 | 165 | public Q_SLOTS: 166 | 167 | /** 168 | * @brief (Re)load data 169 | * 170 | * In QML, this is done automatically on component completion if the 171 | * @a active property (not documented here) is true, which is the default. 172 | * @return Whether the request was sent. 173 | * 174 | * @note You must set a connector and serialNumber before requests can be sent. 175 | * 176 | * @note When the request fails, the current data is not cleared. 177 | */ 178 | bool reload(); 179 | /** 180 | * @brief Reset object 181 | * 182 | * This clears all data and resets the object back to its initial state. 183 | */ 184 | void reset(); 185 | 186 | private: 187 | std::unique_ptr const d; 188 | }; 189 | 190 | } // namespace QAlphaCloud 191 | -------------------------------------------------------------------------------- /src/lib/onedateenergy.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "onedateenergy.h" 7 | 8 | #include "apirequest.h" 9 | #include "qalphacloud_log.h" 10 | #include "utils_p.h" 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | namespace QAlphaCloud 18 | { 19 | 20 | class OneDateEnergyPrivate 21 | { 22 | public: 23 | explicit OneDateEnergyPrivate(OneDateEnergy *q); 24 | 25 | void setStatus(RequestStatus status); 26 | void setError(ErrorCode error); 27 | void setErrorString(const QString &errorString); 28 | 29 | void processApiResult(const QJsonObject &json); 30 | 31 | OneDateEnergy *const q; 32 | 33 | // TODO QPointer? 34 | Connector *m_connector = nullptr; 35 | QString m_serialNumber; 36 | QDate m_date; 37 | bool m_cached = true; 38 | 39 | int m_photovoltaic = 0; // epv 40 | int m_input = 0; // eInput 41 | int m_output = 0; // eOutput 42 | int m_charge = 0; // eCharge 43 | int m_discharge = 0; // eDischarge 44 | int m_gridCharge = 0; // eGridCharge 45 | 46 | QJsonObject m_json; 47 | RequestStatus m_status = RequestStatus::NoRequest; 48 | ErrorCode m_error = ErrorCode::NoError; 49 | QString m_errorString; 50 | 51 | bool m_valid = false; 52 | 53 | QPointer m_request; 54 | 55 | QHash m_cache; 56 | }; 57 | 58 | OneDateEnergyPrivate::OneDateEnergyPrivate(OneDateEnergy *q) 59 | : q(q) 60 | { 61 | } 62 | 63 | void OneDateEnergyPrivate::setStatus(RequestStatus status) 64 | { 65 | if (m_status != status) { 66 | m_status = status; 67 | Q_EMIT q->statusChanged(status); 68 | } 69 | } 70 | 71 | void OneDateEnergyPrivate::setError(ErrorCode error) 72 | { 73 | if (m_error != error) { 74 | m_error = error; 75 | Q_EMIT q->errorChanged(error); 76 | } 77 | } 78 | 79 | void OneDateEnergyPrivate::setErrorString(const QString &errorString) 80 | { 81 | if (m_errorString != errorString) { 82 | m_errorString = errorString; 83 | Q_EMIT q->errorStringChanged(errorString); 84 | } 85 | } 86 | 87 | void OneDateEnergyPrivate::processApiResult(const QJsonObject &json) 88 | { 89 | bool valid = false; 90 | 91 | const auto oldTotalLoad = q->totalLoad(); 92 | 93 | const auto photovoltaicValue = json.value(QStringLiteral("epv")); 94 | const auto photovoltaicKwh = photovoltaicValue.toDouble(); 95 | const int photovoltaic = static_cast(std::round(photovoltaicKwh * 1000)); 96 | Utils::updateField(m_photovoltaic, photovoltaic, q, &OneDateEnergy::photovoltaicChanged); 97 | valid = valid || (!photovoltaicValue.isUndefined() && !photovoltaicValue.isNull()); 98 | 99 | const auto inputValue = json.value(QStringLiteral("eInput")); 100 | const auto inputKwh = inputValue.toDouble(); 101 | const int input = static_cast(std::round(inputKwh * 1000)); 102 | Utils::updateField(m_input, input, q, &OneDateEnergy::inputChanged); 103 | valid = valid || (!inputValue.isUndefined() && !inputValue.isNull()); 104 | 105 | const auto outputValue = json.value(QStringLiteral("eOutput")); 106 | const auto outputKwh = outputValue.toDouble(); 107 | const int output = static_cast(std::round(outputKwh * 1000)); 108 | Utils::updateField(m_output, output, q, &OneDateEnergy::outputChanged); 109 | valid = valid || (!outputValue.isUndefined() && !outputValue.isNull()); 110 | 111 | const auto chargeValue = json.value(QStringLiteral("eCharge")); 112 | const auto chargeKwh = chargeValue.toDouble(); 113 | const int charge = static_cast(std::round(chargeKwh * 1000)); 114 | Utils::updateField(m_charge, charge, q, &OneDateEnergy::chargeChanged); 115 | valid = valid || (!chargeValue.isUndefined() && !chargeValue.isNull()); 116 | 117 | const auto dischargeValue = json.value(QStringLiteral("eDischarge")); 118 | const auto dischargeKwh = dischargeValue.toDouble(); 119 | const int discharge = static_cast(std::round(dischargeKwh * 1000)); 120 | Utils::updateField(m_discharge, discharge, q, &OneDateEnergy::dischargeChanged); 121 | valid = valid || (!dischargeValue.isUndefined() && !dischargeValue.isNull()); 122 | 123 | // TODO eCharge / eDischarge 124 | 125 | const auto gridChargeValue = json.value(QStringLiteral("eGridCharge")); 126 | const auto gridChargeKwh = gridChargeValue.toDouble(); 127 | const int gridCharge = static_cast(std::round(gridChargeKwh * 1000)); 128 | Utils::updateField(m_gridCharge, gridCharge, q, &OneDateEnergy::gridChargeChanged); 129 | valid = valid || (!gridChargeValue.isUndefined() && !gridChargeValue.isNull()); 130 | 131 | // TODO eChargingPile 132 | 133 | if (oldTotalLoad != q->totalLoad()) { 134 | Q_EMIT q->totalLoadChanged(q->totalLoad()); 135 | } 136 | 137 | if (m_json != json) { 138 | m_json = json; 139 | Q_EMIT q->rawJsonChanged(); 140 | } 141 | 142 | if (m_valid != valid) { 143 | m_valid = valid; 144 | Q_EMIT q->validChanged(valid); 145 | } 146 | 147 | setStatus(RequestStatus::Finished); 148 | } 149 | 150 | OneDateEnergy::OneDateEnergy(QObject *parent) 151 | : OneDateEnergy(nullptr, QString(), QDate::currentDate(), parent) 152 | { 153 | } 154 | 155 | OneDateEnergy::OneDateEnergy(Connector *connector, const QString &serialNumber, const QDate &date, QObject *parent) 156 | : QObject(parent) 157 | , d(std::make_unique(this)) 158 | { 159 | setConnector(connector); 160 | 161 | d->m_serialNumber = serialNumber; 162 | d->m_cache.clear(); 163 | d->m_date = date; 164 | } 165 | 166 | OneDateEnergy::~OneDateEnergy() = default; 167 | 168 | Connector *OneDateEnergy::connector() const 169 | { 170 | return d->m_connector; 171 | } 172 | 173 | void OneDateEnergy::setConnector(Connector *connector) 174 | { 175 | if (d->m_connector == connector) { 176 | return; 177 | } 178 | 179 | d->m_connector = connector; 180 | d->m_cache.clear(); 181 | reset(); 182 | Q_EMIT connectorChanged(connector); 183 | } 184 | 185 | QString OneDateEnergy::serialNumber() const 186 | { 187 | return d->m_serialNumber; 188 | } 189 | 190 | void OneDateEnergy::setSerialNumber(const QString &serialNumber) 191 | { 192 | if (d->m_serialNumber == serialNumber) { 193 | return; 194 | } 195 | 196 | d->m_serialNumber = serialNumber; 197 | d->m_cache.clear(); 198 | reset(); 199 | Q_EMIT serialNumberChanged(serialNumber); 200 | } 201 | 202 | QDate OneDateEnergy::date() const 203 | { 204 | return d->m_date; 205 | } 206 | 207 | void OneDateEnergy::setDate(const QDate &date) 208 | { 209 | if (d->m_date == date) { 210 | return; 211 | } 212 | 213 | d->m_date = date; 214 | reset(); 215 | Q_EMIT dateChanged(date); 216 | } 217 | 218 | void OneDateEnergy::resetDate() 219 | { 220 | setDate(QDate::currentDate()); 221 | } 222 | 223 | bool OneDateEnergy::cached() const 224 | { 225 | return d->m_cached; 226 | } 227 | 228 | void OneDateEnergy::setCached(bool cached) 229 | { 230 | if (d->m_cached == cached) { 231 | return; 232 | } 233 | 234 | d->m_cached = cached; 235 | if (!cached) { 236 | d->m_cache.clear(); 237 | } 238 | Q_EMIT cachedChanged(cached); 239 | } 240 | 241 | int OneDateEnergy::totalLoad() const 242 | { 243 | return d->m_photovoltaic + d->m_discharge + d->m_input - d->m_output - d->m_charge; 244 | } 245 | 246 | int OneDateEnergy::photovoltaic() const 247 | { 248 | return d->m_photovoltaic; 249 | } 250 | 251 | int OneDateEnergy::input() const 252 | { 253 | return d->m_input; 254 | } 255 | 256 | int OneDateEnergy::output() const 257 | { 258 | return d->m_output; 259 | } 260 | 261 | int OneDateEnergy::charge() const 262 | { 263 | return d->m_charge; 264 | } 265 | 266 | int OneDateEnergy::discharge() const 267 | { 268 | return d->m_discharge; 269 | } 270 | 271 | int OneDateEnergy::gridCharge() const 272 | { 273 | return d->m_gridCharge; 274 | } 275 | 276 | QJsonObject OneDateEnergy::rawJson() const 277 | { 278 | return d->m_json; 279 | } 280 | 281 | bool OneDateEnergy::valid() const 282 | { 283 | return d->m_valid; 284 | } 285 | 286 | RequestStatus OneDateEnergy::status() const 287 | { 288 | return d->m_status; 289 | } 290 | 291 | ErrorCode OneDateEnergy::error() const 292 | { 293 | return d->m_error; 294 | } 295 | 296 | QString OneDateEnergy::errorString() const 297 | { 298 | return d->m_errorString; 299 | } 300 | 301 | bool OneDateEnergy::reload() 302 | { 303 | if (!d->m_connector) { 304 | qCWarning(QALPHACLOUD_LOG) << "Cannot load OneDateEnergy without a connector"; 305 | return false; 306 | } 307 | 308 | if (d->m_serialNumber.isEmpty()) { 309 | qCWarning(QALPHACLOUD_LOG) << "Cannot load OneDateEnergy without a serial number"; 310 | return false; 311 | } 312 | 313 | const QDate date = d->m_date; 314 | if (!date.isValid()) { 315 | qCWarning(QALPHACLOUD_LOG) << "Cannot load OneDateEnergy without a valid date"; 316 | return false; 317 | } 318 | 319 | if (d->m_request) { 320 | qCDebug(QALPHACLOUD_LOG) << "Cancelling OneDateEnergy request in-flight"; 321 | d->m_request->abort(); 322 | d->m_request = nullptr; 323 | } 324 | 325 | const auto cachedData = d->m_cache.value(date); 326 | if (!cachedData.isEmpty()) { 327 | d->processApiResult(cachedData); 328 | return true; 329 | } 330 | 331 | auto *request = new ApiRequest(d->m_connector, ApiRequest::EndPoint::OneDateEnergyBySn, this); 332 | request->setSysSn(d->m_serialNumber); 333 | request->setQueryDate(date); 334 | 335 | connect(request, &ApiRequest::errorOccurred, this, [this, request] { 336 | d->setError(request->error()); 337 | d->setErrorString(request->errorString()); 338 | d->setStatus(RequestStatus::Error); 339 | }); 340 | 341 | connect(request, &ApiRequest::result, this, [this, request, date] { 342 | const QJsonObject json = request->data().toObject(); 343 | 344 | d->processApiResult(json); 345 | 346 | // Don't cache today's data as it will gain new data as the day progresses. 347 | // Also don't cache if there is no valid data. 348 | if (d->m_cached && d->m_valid && date != QDate::currentDate()) { 349 | d->m_cache.insert(date, json); 350 | } 351 | }); 352 | 353 | const bool ok = request->send(); 354 | 355 | if (ok) { 356 | d->m_request = request; 357 | 358 | d->setStatus(QAlphaCloud::RequestStatus::Loading); 359 | } 360 | 361 | return ok; 362 | } 363 | 364 | bool OneDateEnergy::forceReload() 365 | { 366 | d->m_cache.clear(); 367 | return reload(); 368 | } 369 | 370 | void OneDateEnergy::reset() 371 | { 372 | d->processApiResult(QJsonObject()); 373 | if (d->m_request) { 374 | d->m_request->abort(); 375 | d->m_request = nullptr; 376 | } 377 | d->setStatus(RequestStatus::NoRequest); 378 | } 379 | 380 | } // namespace QAlphaCloud 381 | -------------------------------------------------------------------------------- /src/lib/onedateenergy.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "connector.h" 16 | #include "qalphacloud.h" 17 | #include "qalphacloud_export.h" 18 | 19 | namespace QAlphaCloud 20 | { 21 | 22 | class OneDateEnergyPrivate; 23 | 24 | /** 25 | * @brief Cumulative daily energy information. 26 | * 27 | * Provides cumulative information about a given date. 28 | * 29 | * Wraps the @c /getOneDateEnergy API endpoint. 30 | */ 31 | class QALPHACLOUD_EXPORT OneDateEnergy : public QObject 32 | { 33 | Q_OBJECT 34 | 35 | /** 36 | * @brief The connector to use 37 | */ 38 | Q_PROPERTY(QAlphaCloud::Connector *connector READ connector WRITE setConnector NOTIFY connectorChanged REQUIRED) 39 | 40 | /** 41 | * @brief The serial number 42 | * 43 | * The serial number of the storage system whose data should be queried. 44 | */ 45 | Q_PROPERTY(QString serialNumber READ serialNumber WRITE setSerialNumber NOTIFY serialNumberChanged REQUIRED) 46 | /** 47 | * @brief The date 48 | * 49 | * The date for which to query the data. 50 | */ 51 | Q_PROPERTY(QDate date READ date WRITE setDate RESET resetDate NOTIFY dateChanged) 52 | 53 | /** 54 | * @brief Cache data 55 | * 56 | * Whether to cache the returned data, default is true. 57 | * 58 | * This allows for quicker navigation between dates when they have been loaded 59 | * once and reduces network traffic. 60 | * 61 | * Data from the current day is never cached as data is collected throughout 62 | * the day. 63 | */ 64 | Q_PROPERTY(bool cached READ cached WRITE setCached NOTIFY cachedChanged) 65 | 66 | /** 67 | * @brief Photovoltaic production in Wh. 68 | */ 69 | Q_PROPERTY(int photovoltaic READ photovoltaic NOTIFY photovoltaicChanged) 70 | 71 | /** 72 | * @brief Total load in Wh. 73 | */ 74 | Q_PROPERTY(int totalLoad READ totalLoad NOTIFY totalLoadChanged) 75 | /** 76 | * @brief Power input form grid in Wh. 77 | */ 78 | Q_PROPERTY(int input READ input NOTIFY inputChanged) 79 | /** 80 | * @brief Power output to the grid in Wh. 81 | */ 82 | Q_PROPERTY(int output READ output NOTIFY outputChanged) 83 | 84 | /** 85 | * @brief Energy charged into the battery in Wh. 86 | */ 87 | Q_PROPERTY(int charge READ charge NOTIFY chargeChanged) 88 | /** 89 | * @brief Energy discharged from the battery in Wh. 90 | */ 91 | Q_PROPERTY(int discharge READ discharge NOTIFY dischargeChanged) 92 | /** 93 | * @brief Battery charge from grid in Wh. 94 | * 95 | * How many Wh have been fed from the grid into the battery. 96 | */ 97 | Q_PROPERTY(int gridCharge READ gridCharge NOTIFY gridChargeChanged) 98 | 99 | // TODO eChargingPile (EV charger) 100 | 101 | /** 102 | * @brief Raw JSON 103 | * 104 | * The raw JSON returned by the API, useful for extracting data 105 | * that isn't provided through the API yet. 106 | */ 107 | Q_PROPERTY(QJsonObject rawJson READ rawJson NOTIFY rawJsonChanged) 108 | 109 | /** 110 | * @brief Whether this object contains data 111 | * 112 | * This is independent of the status. The status can be QAlphaCloud::Error 113 | * when a subsequent request fails but any data isn't cleared unless 114 | * new data is loaded successfully. 115 | */ 116 | Q_PROPERTY(bool valid READ valid NOTIFY validChanged STORED false) 117 | 118 | /** 119 | * @brief The current request status 120 | */ 121 | Q_PROPERTY(QAlphaCloud::RequestStatus status READ status NOTIFY statusChanged) 122 | 123 | /** 124 | * @brief The error, if any 125 | * 126 | * There can still be valid data in this object from a previous 127 | * successful request. 128 | */ 129 | Q_PROPERTY(QAlphaCloud::ErrorCode error READ error NOTIFY errorChanged) 130 | /** 131 | * @brief The error string, if any 132 | * 133 | * @note Not every error code has an errorString associated with it. 134 | */ 135 | Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) 136 | 137 | public: 138 | /** 139 | * @brief Creates a OneDateEnergy instance 140 | * @param parent The owner 141 | * 142 | * @note A connector, serialNumber, and a date must be set before requests can be made. 143 | */ 144 | explicit OneDateEnergy(QObject *parent = nullptr); 145 | /** 146 | * @brief Creates a OneDateEnergy intsance 147 | * @param connector The connector 148 | * @param serialNumber The serial number of the storage system whose data should be queried 149 | * @param date The date for which to query the data 150 | * @param parent The owner 151 | */ 152 | OneDateEnergy(Connector *connector, const QString &serialNumber, const QDate &date, QObject *parent = nullptr); 153 | ~OneDateEnergy() override; 154 | 155 | Q_REQUIRED_RESULT Connector *connector() const; 156 | void setConnector(Connector *connector); 157 | Q_SIGNAL void connectorChanged(Connector *connector); 158 | 159 | Q_REQUIRED_RESULT QString serialNumber() const; 160 | void setSerialNumber(const QString &serialNumber); 161 | Q_SIGNAL void serialNumberChanged(const QString &serialNumber); 162 | 163 | Q_REQUIRED_RESULT QDate date() const; 164 | void setDate(const QDate &date); 165 | void resetDate(); 166 | Q_SIGNAL void dateChanged(const QDate &date); 167 | 168 | Q_REQUIRED_RESULT bool cached() const; 169 | void setCached(bool cached); 170 | Q_SIGNAL void cachedChanged(bool cached); 171 | 172 | Q_REQUIRED_RESULT int totalLoad() const; 173 | Q_SIGNAL void totalLoadChanged(int totalLoad); 174 | 175 | Q_REQUIRED_RESULT int photovoltaic() const; 176 | Q_SIGNAL void photovoltaicChanged(int photovoltaic); 177 | 178 | Q_REQUIRED_RESULT int input() const; 179 | Q_SIGNAL void inputChanged(int input); 180 | 181 | Q_REQUIRED_RESULT int output() const; 182 | Q_SIGNAL void outputChanged(int output); 183 | 184 | Q_REQUIRED_RESULT int charge() const; 185 | Q_SIGNAL void chargeChanged(int charge); 186 | 187 | Q_REQUIRED_RESULT int discharge() const; 188 | Q_SIGNAL void dischargeChanged(int discharge); 189 | 190 | Q_REQUIRED_RESULT int gridCharge() const; 191 | Q_SIGNAL void gridChargeChanged(int gridCharge); 192 | 193 | Q_REQUIRED_RESULT QJsonObject rawJson() const; 194 | Q_SIGNAL void rawJsonChanged(); 195 | 196 | Q_REQUIRED_RESULT bool valid() const; 197 | Q_SIGNAL void validChanged(bool valid); 198 | 199 | Q_REQUIRED_RESULT QAlphaCloud::RequestStatus status() const; 200 | Q_SIGNAL void statusChanged(QAlphaCloud::RequestStatus status); 201 | 202 | QAlphaCloud::ErrorCode error() const; 203 | Q_SIGNAL void errorChanged(QAlphaCloud::ErrorCode error); 204 | 205 | QString errorString() const; 206 | Q_SIGNAL void errorStringChanged(const QString &errorString); 207 | 208 | public Q_SLOTS: 209 | 210 | /** 211 | * @brief (Re)load data 212 | * 213 | * In QML, this is done automatically on component completion if the 214 | * @a active property (not documented here) is true, which is the default. 215 | * @return Whether the request was sent. 216 | * 217 | * @note You must set a connector, a serialNumber, and a date before requests can be sent. 218 | * 219 | * @note When the request fails, the current data is not cleared. 220 | */ 221 | bool reload(); 222 | /** 223 | * @brief Force a reload 224 | * 225 | * Reloads the data, ignoring the cache. 226 | * 227 | * @return Whether the request was sent. 228 | */ 229 | bool forceReload(); 230 | /** 231 | * @brief Reset object 232 | * 233 | * This clears all data and resets the object back to its initial state. 234 | */ 235 | void reset(); 236 | 237 | private: 238 | std::unique_ptr const d; 239 | }; 240 | 241 | } // namespace QAlphaCloud 242 | -------------------------------------------------------------------------------- /src/lib/onedaypowermodel.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include "connector.h" 14 | #include "qalphacloud.h" 15 | #include "qalphacloud_export.h" 16 | 17 | namespace QAlphaCloud 18 | { 19 | 20 | class OneDayPowerModelPrivate; 21 | 22 | /** 23 | * @brief Historic power data for a day 24 | * 25 | * Provides historic power data for a given day. 26 | * 27 | * Wraps the @c /getOneDayPower API endpoint. 28 | */ 29 | class QALPHACLOUD_EXPORT OneDayPowerModel : public QAbstractListModel 30 | { 31 | Q_OBJECT 32 | 33 | /** 34 | * @brief The connector to use 35 | */ 36 | Q_PROPERTY(QAlphaCloud::Connector *connector READ connector WRITE setConnector NOTIFY connectorChanged REQUIRED) 37 | 38 | /** 39 | * @brief The serial number 40 | * 41 | * The serial number of the storage system whose data should be queried. 42 | */ 43 | Q_PROPERTY(QString serialNumber READ serialNumber WRITE setSerialNumber NOTIFY serialNumberChanged REQUIRED) 44 | /** 45 | * @brief The date 46 | * 47 | * The date for which to query the data. 48 | */ 49 | Q_PROPERTY(QDate date READ date WRITE setDate RESET resetDate NOTIFY dateChanged) 50 | 51 | /** 52 | * @brief Cache data 53 | * 54 | * Whether to cache the returned data, default is true. 55 | * 56 | * This allows for quicker navigation between dates when they have been loaded 57 | * once and reduces network traffic. 58 | * 59 | * Data from the current day is never cached as data is collected throughout 60 | * the day. 61 | */ 62 | Q_PROPERTY(bool cached READ cached WRITE setCached NOTIFY cachedChanged) 63 | 64 | /** 65 | * @brief The earliest date in the model 66 | * 67 | * Useful for determining the range of the X axis on a plot. 68 | */ 69 | Q_PROPERTY(QDateTime fromDateTime READ fromDateTime NOTIFY fromDateTimeChanged) 70 | /** 71 | * @brief The latest date in the model 72 | * 73 | * Useful for determining the range of the X axis on a plot. 74 | */ 75 | Q_PROPERTY(QDateTime toDateTime READ toDateTime NOTIFY toDateTimeChanged) 76 | 77 | /** 78 | * @brief Peak photovoltaic production in W 79 | * 80 | * Useful for determining the range of the Y axis on a plot. 81 | */ 82 | Q_PROPERTY(int peakPhotovoltaic READ peakPhotovoltaic NOTIFY peakPhotovoltaicChanged) 83 | /** 84 | * @brief Peak load in W 85 | * 86 | * Useful for determining the range of the Y axis on a plot. 87 | */ 88 | Q_PROPERTY(int peakLoad READ peakLoad NOTIFY peakLoadChanged) 89 | /** 90 | * @brief Peak grid feed in W 91 | * 92 | * Useful for determining the range of the Y axis on a plot. 93 | */ 94 | Q_PROPERTY(int peakGridFeed READ peakGridFeed NOTIFY peakGridFeedChanged) 95 | /** 96 | * @brief Peak grid charge in W 97 | * 98 | * Useful for determining the range of the Y axis on a plot. 99 | */ 100 | Q_PROPERTY(int peakGridCharge READ peakGridCharge NOTIFY peakGridChargeChanged) 101 | 102 | /** 103 | * @brief The number of items in the model 104 | */ 105 | Q_PROPERTY(int count READ rowCount NOTIFY countChanged) 106 | 107 | /** 108 | * @brief The current request status 109 | */ 110 | Q_PROPERTY(QAlphaCloud::RequestStatus status READ status NOTIFY statusChanged) 111 | 112 | /** 113 | * @brief The error, if any 114 | * 115 | * There can still be valid data in this object from a previous 116 | * successful request. 117 | */ 118 | Q_PROPERTY(QAlphaCloud::ErrorCode error READ error NOTIFY errorChanged) 119 | /** 120 | * @brief The error string, if any 121 | * 122 | * @note Not every error code has an errorString associated with it. 123 | */ 124 | Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) 125 | 126 | public: 127 | /** 128 | * @brief Creates a OneDateEnergy instance 129 | * @param parent The owner 130 | * 131 | * @note A connector, serialNumber, and a date must be set before requests can be made. 132 | */ 133 | explicit OneDayPowerModel(QObject *parent = nullptr); 134 | /** 135 | * @brief Creates a OneDateEnergy intsance 136 | * @param connector The connector 137 | * @param serialNumber The serial number of the storage system whose data should be queried 138 | * @param date The date for which to query the data 139 | * @param parent The owner 140 | */ 141 | OneDayPowerModel(Connector *connector, const QString &serialNumber, const QDate &date, QObject *parent = nullptr); 142 | ~OneDayPowerModel() override; 143 | 144 | /** 145 | * @brief The model roles 146 | */ 147 | enum class Roles { 148 | PhotovoltaicEnergy = Qt::UserRole, ///< The photovoltaic production in W (int) 149 | CurrentLoad, ///< The current load in W (int) 150 | GridFeed, ///< The current grid feed in W (int) 151 | GridCharge, ///< The current grid charge in W (int) 152 | BatterySoc, ///< The battery state of charge in per-cent % (qreal) 153 | UploadTime, ///< When this entry was recorded. (QDateTime) 154 | RawJson = Qt::UserRole 155 | + 99, ///< Returns the raw JSON data for this system. Useful for extracting properties that aren't provided through the model (QJsonObject). 156 | }; 157 | Q_ENUM(Roles) 158 | 159 | Q_REQUIRED_RESULT Connector *connector() const; 160 | void setConnector(Connector *connector); 161 | Q_SIGNAL void connectorChanged(Connector *connector); 162 | 163 | Q_REQUIRED_RESULT QString serialNumber() const; 164 | void setSerialNumber(const QString &serialNumber); 165 | Q_SIGNAL void serialNumberChanged(const QString &serialNumber); 166 | 167 | Q_REQUIRED_RESULT QDate date() const; 168 | void setDate(const QDate &date); 169 | void resetDate(); 170 | Q_SIGNAL void dateChanged(const QDate &date); 171 | 172 | Q_REQUIRED_RESULT bool cached() const; 173 | void setCached(bool cached); 174 | Q_SIGNAL void cachedChanged(bool cached); 175 | 176 | Q_REQUIRED_RESULT QDateTime fromDateTime() const; 177 | Q_SIGNAL void fromDateTimeChanged(const QDateTime &fromDateTime); 178 | 179 | Q_REQUIRED_RESULT QDateTime toDateTime() const; 180 | Q_SIGNAL void toDateTimeChanged(const QDateTime &toDateTime); 181 | 182 | Q_REQUIRED_RESULT int peakPhotovoltaic() const; 183 | Q_SIGNAL void peakPhotovoltaicChanged(int peakPhotovoltaicChanged); 184 | 185 | Q_REQUIRED_RESULT int peakLoad() const; 186 | Q_SIGNAL void peakLoadChanged(int peakLoad); 187 | 188 | Q_REQUIRED_RESULT int peakGridFeed() const; 189 | Q_SIGNAL void peakGridFeedChanged(int peakGridFeed); 190 | 191 | Q_REQUIRED_RESULT int peakGridCharge() const; 192 | Q_SIGNAL void peakGridChargeChanged(int peakGridCharge); 193 | 194 | QAlphaCloud::RequestStatus status() const; 195 | Q_SIGNAL void statusChanged(QAlphaCloud::RequestStatus status); 196 | 197 | QAlphaCloud::ErrorCode error() const; 198 | Q_SIGNAL void errorChanged(QAlphaCloud::ErrorCode error); 199 | 200 | QString errorString() const; 201 | Q_SIGNAL void errorStringChanged(const QString &errorString); 202 | 203 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 204 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 205 | QHash roleNames() const override; 206 | 207 | public Q_SLOTS: 208 | 209 | /** 210 | * @brief (Re)load data 211 | * 212 | * In QML, this is done automatically on component completion if the 213 | * @a active property (not documented here) is true, which is the default. 214 | * @return Whether the request was sent. 215 | * 216 | * @note You must set a connector, a serialNumber, and a date before requests can be sent. 217 | * 218 | * @note When the request fails, the current data is not cleared. 219 | */ 220 | bool reload(); 221 | /** 222 | * @brief Force a reload 223 | * 224 | * Reloads the data, ignoring the cache. 225 | * 226 | * @return Whether the request was sent. 227 | */ 228 | bool forceReload(); 229 | /** 230 | * @brief Reset object 231 | * 232 | * This clears all data and resets the object back to its initial state. 233 | */ 234 | void reset(); 235 | 236 | Q_SIGNALS: 237 | void countChanged(); 238 | 239 | private: 240 | friend OneDayPowerModelPrivate; 241 | std::unique_ptr const d; 242 | }; 243 | 244 | } // namespace QAlphaCloud 245 | -------------------------------------------------------------------------------- /src/lib/qalphacloud.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "qalphacloud.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace QAlphaCloud 14 | { 15 | 16 | QString errorText(ErrorCode code, const QVariant &details) 17 | { 18 | const QString detailsString = details.toString(); 19 | 20 | switch (code) { 21 | case ErrorCode::UnknownError: 22 | return QCoreApplication::translate("errorText", "An unknown error occurred."); 23 | case ErrorCode::NoError: 24 | return QCoreApplication::translate("errorText", "The operation completed successfully."); 25 | 26 | #pragma GCC diagnostic push 27 | #pragma GCC diagnostic ignored "-Wswitch" 28 | case static_cast(QNetworkReply::TimeoutError): 29 | return QCoreApplication::translate("errorText", "Operation timed out."); 30 | case static_cast(QNetworkReply::OperationCanceledError): 31 | return QCoreApplication::translate("errorText", "Operation was canceled."); 32 | #pragma GCC diagnostic pop 33 | 34 | case ErrorCode::JsonParseError: 35 | if (!detailsString.isEmpty()) { 36 | return QCoreApplication::translate("errorText", "Failed to parse JSON: %1").arg(detailsString); 37 | } else { 38 | return QCoreApplication::translate("errorText", "Failed to parse JSON."); 39 | } 40 | case ErrorCode::UnexpectedJsonDataError: { 41 | if (details.canConvert()) { 42 | const auto jsonDocument = details.toJsonDocument(); 43 | if (jsonDocument.isArray()) { 44 | return QCoreApplication::translate("errorText", "Unexpected JSON Array received."); 45 | } 46 | } 47 | return QCoreApplication::translate("errorText", "Unexpected JSON content received."); 48 | } 49 | 50 | case ErrorCode::ParameterError: 51 | if (detailsString.isEmpty()) { 52 | return QCoreApplication::translate("errorText", "Invalid parameter provided."); 53 | } 54 | break; 55 | case ErrorCode::SnNotBoundToUser: 56 | if (detailsString.isEmpty()) { 57 | return QCoreApplication::translate("errorText", "The provided serial number is not associated with this user."); 58 | } 59 | break; 60 | case ErrorCode::CheckCodeError: 61 | if (detailsString.isEmpty()) { 62 | return QCoreApplication::translate("errorText", "Check code error."); 63 | } 64 | break; 65 | case ErrorCode::AppIdNotBoundToSn: 66 | if (detailsString.isEmpty()) { 67 | return QCoreApplication::translate("errorText", "The provided application ID is not associated with this serial number."); 68 | } 69 | break; 70 | case ErrorCode::TimestampError: 71 | if (detailsString.isEmpty()) { 72 | return QCoreApplication::translate("errorText", "The provided time stamp is either invalid, or too far in the past."); 73 | } 74 | break; 75 | case ErrorCode::SignVerificationError: 76 | if (detailsString.isEmpty()) { 77 | return QCoreApplication::translate("errorText", "API secret verification error."); 78 | } 79 | break; 80 | case ErrorCode::SetFailed: 81 | if (detailsString.isEmpty()) { 82 | return QCoreApplication::translate("errorText", "Failed to set requested configuration."); 83 | } 84 | break; 85 | case ErrorCode::SignEmpty: 86 | if (detailsString.isEmpty()) { 87 | return QCoreApplication::translate("errorText", "API secret was not provided."); 88 | } 89 | break; 90 | case ErrorCode::WhitelistVerificationFailed: 91 | if (detailsString.isEmpty()) { 92 | return QCoreApplication::translate("errorText", "Whitelist verification failed."); 93 | } 94 | break; 95 | case ErrorCode::TimestampEmpty: 96 | if (detailsString.isEmpty()) { 97 | return QCoreApplication::translate("errorText", "Request time stamp was not provided."); 98 | } 99 | break; 100 | case ErrorCode::AppIdEmpty: 101 | if (detailsString.isEmpty()) { 102 | return QCoreApplication::translate("errorText", "Application ID was not provided."); 103 | } 104 | break; 105 | case ErrorCode::DataDoesNotExist: 106 | if (detailsString.isEmpty()) { 107 | return QCoreApplication::translate("errorText", "Data does not exist or has been deleted."); 108 | } 109 | break; 110 | case ErrorCode::InvalidDate: 111 | if (detailsString.isEmpty()) { 112 | return QCoreApplication::translate("errorText", "Invalid date provided."); 113 | } 114 | break; 115 | case ErrorCode::OperationFailed: 116 | if (detailsString.isEmpty()) { 117 | return QCoreApplication::translate("errorText", "Operation failed."); 118 | } 119 | break; 120 | case ErrorCode::SystemSnDoesNotExist: 121 | if (detailsString.isEmpty()) { 122 | return QCoreApplication::translate("errorText", "System serial number does not exist."); 123 | } 124 | break; 125 | case ErrorCode::SystemOffline: 126 | if (detailsString.isEmpty()) { 127 | return QCoreApplication::translate("errorText", "System is offline."); 128 | } 129 | break; 130 | 131 | case ErrorCode::VerificationCode: 132 | if (detailsString.isEmpty()) { 133 | return QCoreApplication::translate("errorText", "Verification code error."); 134 | } 135 | break; 136 | 137 | case ErrorCode::TooManyRequests: 138 | if (detailsString.isEmpty()) { 139 | return QCoreApplication::translate("errorText", "Too many requests, try again later."); 140 | } 141 | break; 142 | } 143 | 144 | if (!detailsString.isEmpty()) { 145 | return detailsString; 146 | } 147 | 148 | QMetaEnum metaEnum = QMetaEnum::fromType(); 149 | return QString::fromUtf8(metaEnum.valueToKey(static_cast(code))); 150 | } 151 | 152 | } // namespace QAlphaCloud 153 | -------------------------------------------------------------------------------- /src/lib/qalphacloud.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "qalphacloud_export.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | /** 19 | * @brief Utility namespace 20 | */ 21 | namespace QAlphaCloud 22 | { 23 | Q_NAMESPACE_EXPORT(QALPHACLOUD_EXPORT) 24 | 25 | /** 26 | * @brief Request status 27 | */ 28 | enum class RequestStatus { 29 | NoRequest = 0, ///< No request has been issued. 30 | Loading, ///< The request is being loaded from the server. 31 | Finished, ///< The request has finished successfully. 32 | Error, ///< The request failed with an error. 33 | }; 34 | Q_ENUM_NS(RequestStatus) 35 | 36 | /** 37 | * @brief Error codes 38 | * 39 | * This can either be 40 | * * NoError 41 | * * A value from QNetworkReply::Error (not documented here). 42 | * * Errors specific to this library (in the 1xxx range). 43 | * * Errors from the API (in the 6xxx range). 44 | */ 45 | enum class ErrorCode { 46 | UnknownError = -1, 47 | NoError = 0, ///< API returns 200 48 | 49 | // ... QNetworkReply::Error ... 50 | 51 | // Our own errors. 52 | JsonParseError = 1001, ///< Failed to parse JSON received. 53 | UnexpectedJsonDataError = 1002, ///< Valid JSON received but it was not an Object (perhaps null, or an Array). 54 | EmptyJsonObjectError = 1002, ///< Valid JSON object was received but it was empty. 55 | 56 | // API errors 57 | ParameterError = 6001, ///< "Parameter error" 58 | SnNotBoundToUser = 6002, ///< "The SN is not bound to the user" 59 | // ///< "You have bound this SN" 60 | CheckCodeError = 6004, ///< "CheckCode error" 61 | AppIdNotBoundToSn = 6005, ///< "This appId is not bound to the SN" 62 | TimestampError = 6006, ///< "Timestamp error" 63 | SignVerificationError = 6007, ///< "Sign verification error" 64 | SetFailed = 6008, ///< "Set failed" 65 | WhitelistVerificationFailed = 6009, ///< "Whitelist verification failed" 66 | SignEmpty = 6010, ///< "Sign is empty" 67 | TimestampEmpty = 6011, ///< "timestamp is empty" 68 | AppIdEmpty = 6012, ///< "AppId is empty" 69 | DataDoesNotExist = 6016, ///< "Data does not exist or has been deleted" 70 | InvalidDate = 6026, ///< Date is invalid, documented as "internal error". Also returned when requesting historic data for a future date. 71 | OperationFailed = 6029, ///< "Operation failed" 72 | SystemSnDoesNotExist = 6038, ///< "system sn does not exist" 73 | SystemOffline = 6042, ///< "system offline" 74 | VerificationCode = 6046, ///< "Verification code error" 75 | TooManyRequests = 6053, ///< "The request was too fast, please try again later" 76 | }; 77 | Q_ENUM_NS(ErrorCode) 78 | 79 | /** 80 | * @brief Human-readable error text 81 | * @param code The error code 82 | * @param details Details about the error, for example a QString with the JSON parser error message, 83 | * or a QJsonValue whose type is printed when a different one was expected, etc. 84 | * @return A human-readable error description 85 | */ 86 | QALPHACLOUD_EXPORT QString errorText(ErrorCode code, const QVariant &details = QVariant()); 87 | 88 | /** 89 | * @brief System status 90 | * 91 | * Corresponds to the @c emsStatus field on the @c /getEssList endpoint. 92 | * 93 | * @note No values are officially documented, this enumeration is incomplete. 94 | */ 95 | enum class SystemStatus { 96 | UnknownStatus = -1, ///< Unknown status 97 | Normal, ///< The system is performing normally 98 | Fault, ///< There is a system fault. 99 | // TODO Find out the others. 100 | }; 101 | Q_ENUM_NS(SystemStatus) 102 | 103 | } // namespace QAlphaCloud 104 | -------------------------------------------------------------------------------- /src/lib/storagesystemsmodel.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include "qalphacloud.h" 13 | #include "qalphacloud_export.h" 14 | 15 | #include "connector.h" 16 | 17 | namespace QAlphaCloud 18 | { 19 | 20 | class StorageSystemsModelPrivate; 21 | 22 | /** 23 | * @brief Storage Systems Model 24 | * 25 | * List all storage systems associated with the user. 26 | * 27 | * Wraps the @c /getEssList API endpoint. 28 | */ 29 | class QALPHACLOUD_EXPORT StorageSystemsModel : public QAbstractListModel 30 | { 31 | Q_OBJECT 32 | 33 | /** 34 | * @brief The connector to use 35 | */ 36 | Q_PROPERTY(QAlphaCloud::Connector *connector READ connector WRITE setConnector NOTIFY connectorChanged REQUIRED) 37 | 38 | /** 39 | * @brief Cache the data on disk 40 | * 41 | * Whether to cache the storage systems data on disk 42 | * since it is unlikely to change often. 43 | * Default is true. 44 | */ 45 | Q_PROPERTY(bool cached READ cached WRITE setCached NOTIFY cachedChanged) 46 | 47 | /** 48 | * @brief The number of items in the model 49 | */ 50 | Q_PROPERTY(int count READ rowCount NOTIFY countChanged) 51 | 52 | /** 53 | * @brief The first serial number in the model 54 | * 55 | * This is provided for convenience, so one does not need to 56 | * deal with QAbstractListModel API for serving the common 57 | * case of having a single storage system. 58 | */ 59 | Q_PROPERTY(QString primarySerialNumber READ primarySerialNumber NOTIFY primarySerialNumberChanged) 60 | 61 | /** 62 | * @brief The current request status 63 | * 64 | * The model may contain cached data before a request is sent 65 | * and even if status is NoRequest. 66 | */ 67 | Q_PROPERTY(QAlphaCloud::RequestStatus status READ status NOTIFY statusChanged) 68 | 69 | /** 70 | * @brief The error, if any 71 | * 72 | * There can still be valid data in the model from a previous 73 | * successful request. 74 | */ 75 | Q_PROPERTY(QAlphaCloud::ErrorCode error READ error NOTIFY errorChanged) 76 | /** 77 | * @brief The error string, if any 78 | * 79 | * @note Not every error code has an errorString associated with it. 80 | */ 81 | Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) 82 | 83 | public: 84 | /** 85 | * @brief Creates a StorageSystemsModel instance 86 | * @param parent The owner 87 | * 88 | * @note A connector must be set before requests can be made. 89 | */ 90 | explicit StorageSystemsModel(QObject *parent = nullptr); 91 | /** 92 | * @brief Creates a StorageSystemsModel intsance 93 | * @param connector The connector 94 | * @param parent The owner 95 | */ 96 | explicit StorageSystemsModel(QAlphaCloud::Connector *connector, QObject *parent = nullptr); 97 | ~StorageSystemsModel() override; 98 | 99 | /** 100 | * @brief The model roles 101 | */ 102 | enum class Roles { 103 | SerialNumber = Qt::UserRole, ///< System serial number (QString). 104 | Status, ///< Status of the energy management system (QAlphaCloud::SystemStatus). 105 | InverterModel, ///< Interver model (QString). 106 | InverterPower, ///< Gross power of the inverter system in W (int). 107 | BatteryModel, ///< Battery model (QString). 108 | BatteryGrossCapacity, ///< Gross battery capacity in Wh. Typically there's a capacity buffer that cannot be used (int). 109 | BatteryRemainingCapacity, ///< Remaining battery capacity in Wh (int). 110 | BatteryUsableCapacity, ///< Usable battery capacity in per-cent % (qreal). 111 | PhotovoltaicPower, ///< Gross power provided by the photovoltaic system in W (int). 112 | RawJson = Qt::UserRole 113 | + 99, ///< Returns the raw JSON data for this system. Useful for extracting properties that aren't provided through the model (QJsonObject). 114 | }; 115 | Q_ENUM(Roles) 116 | 117 | Q_REQUIRED_RESULT QAlphaCloud::Connector *connector() const; 118 | void setConnector(QAlphaCloud::Connector *connector); 119 | Q_SIGNAL void connectorChanged(QAlphaCloud::Connector *connector); 120 | 121 | Q_REQUIRED_RESULT bool cached() const; 122 | void setCached(bool cached); 123 | Q_SIGNAL void cachedChanged(bool cached); 124 | 125 | QAlphaCloud::RequestStatus status() const; 126 | Q_SIGNAL void statusChanged(QAlphaCloud::RequestStatus status); 127 | 128 | QString primarySerialNumber() const; 129 | Q_SIGNAL void primarySerialNumberChanged(const QString &primarySerialNumber); 130 | 131 | QAlphaCloud::ErrorCode error() const; 132 | Q_SIGNAL void errorChanged(QAlphaCloud::ErrorCode error); 133 | 134 | QString errorString() const; 135 | Q_SIGNAL void errorStringChanged(const QString &errorString); 136 | 137 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 138 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 139 | QHash roleNames() const override; 140 | 141 | public Q_SLOTS: 142 | /** 143 | * @brief (Re)load data 144 | * 145 | * @note Contrary to the other objects, this is *not* done automatically 146 | * in QML. 147 | * 148 | * @note You must set a connector before requests can be sent. 149 | * 150 | * @note When the request fails, the current data is not cleared. 151 | */ 152 | bool reload(); 153 | 154 | Q_SIGNALS: 155 | void countChanged(); 156 | 157 | private: 158 | friend StorageSystemsModelPrivate; 159 | std::unique_ptr const d; 160 | }; 161 | 162 | } // namespace QAlphaCloud 163 | -------------------------------------------------------------------------------- /src/lib/utils.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "utils_p.h" 7 | 8 | #include 9 | 10 | namespace QAlphaCloud 11 | { 12 | 13 | namespace Utils 14 | { 15 | 16 | QHash roleNamesFromEnum(const QMetaEnum &metaEnum) 17 | { 18 | Q_ASSERT(metaEnum.isValid()); 19 | 20 | QHash roleNames; 21 | 22 | roleNames.reserve(metaEnum.keyCount()); 23 | for (int i = 0; i < metaEnum.keyCount(); ++i) { 24 | QByteArray key = metaEnum.key(i); 25 | key[0] = key.at(0) + 32; // yuck, toLowerCase(). 26 | 27 | roleNames.insert(metaEnum.value(i), key); 28 | } 29 | 30 | return roleNames; 31 | } 32 | 33 | } // namespace Utils 34 | 35 | } // namespace QAlphaCloud 36 | -------------------------------------------------------------------------------- /src/lib/utils_p.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | class QAbstractItemModel; 12 | class QMetaEnum; 13 | 14 | namespace QAlphaCloud 15 | { 16 | 17 | namespace Utils 18 | { 19 | 20 | QHash roleNamesFromEnum(const QMetaEnum &metaEnum); 21 | 22 | template 23 | bool updateField(FieldType &field, const FieldType &newValue, EmitterType *emitter, void (EmitterType::*changeSignal)(FieldType)) 24 | { 25 | if (field != newValue) { 26 | field = newValue; 27 | Q_EMIT((emitter)->*changeSignal)(newValue); 28 | return true; 29 | } 30 | return false; 31 | } 32 | 33 | } // namespace Utils 34 | 35 | } // namespace QAlphaCloud 36 | -------------------------------------------------------------------------------- /src/qml/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | add_library(qalphacloudqmlplugin SHARED 5 | qmlplugin.cpp 6 | ) 7 | target_link_libraries(qalphacloudqmlplugin PRIVATE Qt${QT_MAJOR_VERSION}::Qml QAlphaCloud) 8 | install(TARGETS qalphacloudqmlplugin DESTINATION ${KDE_INSTALL_QMLDIR}/de/broulik/qalphacloud) 9 | install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/de/broulik/qalphacloud) 10 | -------------------------------------------------------------------------------- /src/qml/qmldir: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | module de.broulik.qalphacloud 5 | plugin qalphacloudqmlplugin 6 | -------------------------------------------------------------------------------- /src/qml/qmlplugin.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | class QAlphaCloudQmlPlugin : public QQmlExtensionPlugin 18 | { 19 | Q_OBJECT 20 | Q_PLUGIN_METADATA(IID "de.broulik.qalphacloud") 21 | 22 | public: 23 | QAlphaCloudQmlPlugin(QObject *parent = nullptr) 24 | : QQmlExtensionPlugin(parent) 25 | { 26 | } 27 | 28 | void registerTypes(const char *uri) override; 29 | }; 30 | 31 | using namespace QAlphaCloud; 32 | 33 | class QmlConfiguration : public QAlphaCloud::Configuration, public QQmlParserStatus 34 | { 35 | Q_INTERFACES(QQmlParserStatus) 36 | public: 37 | explicit QmlConfiguration(QObject *parent = nullptr) 38 | : QAlphaCloud::Configuration(parent) 39 | { 40 | } 41 | 42 | void classBegin() override 43 | { 44 | // Here rather than in componentComplete so that that 45 | // it can be overwritten by property assignment. 46 | loadDefault(); 47 | } 48 | 49 | void componentComplete() override 50 | { 51 | } 52 | }; 53 | 54 | class QmlConnector : public QAlphaCloud::Connector, public QQmlParserStatus 55 | { 56 | Q_OBJECT 57 | Q_INTERFACES(QQmlParserStatus) 58 | 59 | public: 60 | explicit QmlConnector(QObject *parent = nullptr) 61 | : QAlphaCloud::Connector(parent) 62 | { 63 | } 64 | 65 | void classBegin() override 66 | { 67 | } 68 | 69 | void componentComplete() override 70 | { 71 | if (!networkAccessManager()) { 72 | setNetworkAccessManager(qmlEngine(this)->networkAccessManager()); 73 | } 74 | } 75 | }; 76 | 77 | class QmlLastPowerData : public QAlphaCloud::LastPowerData, public QQmlParserStatus 78 | { 79 | Q_OBJECT 80 | Q_INTERFACES(QQmlParserStatus) 81 | 82 | Q_PROPERTY(bool active MEMBER m_active NOTIFY activeChanged) 83 | 84 | public: 85 | explicit QmlLastPowerData(QObject *parent = nullptr) 86 | : QAlphaCloud::LastPowerData(parent) 87 | { 88 | } 89 | 90 | void classBegin() override 91 | { 92 | } 93 | 94 | void componentComplete() override 95 | { 96 | connect(this, &QmlLastPowerData::serialNumberChanged, this, &QmlLastPowerData::reloadIfActive); 97 | // Suppress warnings on autoload. 98 | if (connector() && connector()->configuration() && connector()->configuration()->valid() && !serialNumber().isEmpty()) { 99 | reloadIfActive(); 100 | } 101 | } 102 | 103 | Q_SIGNALS: 104 | void activeChanged(bool active); 105 | 106 | private: 107 | void reloadIfActive() 108 | { 109 | if (m_active) { 110 | reload(); 111 | } 112 | } 113 | bool m_active = true; 114 | }; 115 | 116 | class QmlOneDateEnergy : public QAlphaCloud::OneDateEnergy, public QQmlParserStatus 117 | { 118 | Q_OBJECT 119 | Q_INTERFACES(QQmlParserStatus) 120 | 121 | Q_PROPERTY(bool active MEMBER m_active NOTIFY activeChanged) 122 | 123 | public: 124 | explicit QmlOneDateEnergy(QObject *parent = nullptr) 125 | : QAlphaCloud::OneDateEnergy(parent) 126 | { 127 | } 128 | 129 | void classBegin() override 130 | { 131 | } 132 | 133 | void componentComplete() override 134 | { 135 | connect(this, &QmlOneDateEnergy::serialNumberChanged, this, &QmlOneDateEnergy::reloadIfActive); 136 | connect(this, &QmlOneDateEnergy::dateChanged, this, &QmlOneDateEnergy::reloadIfActive); 137 | // Suppress warnings on autoload. 138 | if (connector() && connector()->configuration() && connector()->configuration()->valid() && !serialNumber().isEmpty() && date().isValid()) { 139 | reloadIfActive(); 140 | } 141 | } 142 | 143 | Q_SIGNALS: 144 | void activeChanged(bool active); 145 | 146 | private: 147 | void reloadIfActive() 148 | { 149 | if (m_active) { 150 | reload(); 151 | } 152 | } 153 | bool m_active = true; 154 | }; 155 | 156 | class QmlOneDayPowerModel : public QAlphaCloud::OneDayPowerModel, public QQmlParserStatus 157 | { 158 | Q_OBJECT 159 | Q_INTERFACES(QQmlParserStatus) 160 | 161 | Q_PROPERTY(bool active MEMBER m_active NOTIFY activeChanged) 162 | 163 | public: 164 | explicit QmlOneDayPowerModel(QObject *parent = nullptr) 165 | : QAlphaCloud::OneDayPowerModel(parent) 166 | { 167 | } 168 | 169 | void classBegin() override 170 | { 171 | } 172 | 173 | void componentComplete() override 174 | { 175 | connect(this, &QmlOneDayPowerModel::serialNumberChanged, this, &QmlOneDayPowerModel::reloadIfActive); 176 | connect(this, &QmlOneDayPowerModel::dateChanged, this, &QmlOneDayPowerModel::reloadIfActive); 177 | // Suppress warnings on autoload. 178 | if (connector() && connector()->configuration() && connector()->configuration()->valid() && !serialNumber().isEmpty() && date().isValid()) { 179 | reloadIfActive(); 180 | } 181 | } 182 | 183 | Q_SIGNALS: 184 | void activeChanged(bool active); 185 | 186 | private: 187 | void reloadIfActive() 188 | { 189 | if (m_active) { 190 | reload(); 191 | } 192 | } 193 | 194 | bool m_active = true; 195 | }; 196 | 197 | void QAlphaCloudQmlPlugin::registerTypes(const char *uri) 198 | { 199 | //@uri de.broulik.qalphacloudpl 200 | qmlRegisterType(uri, 1, 0, "Configuration"); 201 | qmlRegisterType(uri, 1, 0, "Connector"); 202 | qmlRegisterType(uri, 1, 0, "LastPowerData"); 203 | qmlRegisterType(uri, 1, 0, "OneDateEnergy"); 204 | qmlRegisterType(uri, 1, 0, "OneDayPowerModel"); 205 | // TODO figure out autoload, i.e. wait for Connector to become valid (when its QNAM is set) and then reload. 206 | qmlRegisterType(uri, 1, 0, "StorageSystemsModel"); 207 | 208 | qmlRegisterUncreatableMetaObject(QAlphaCloud::staticMetaObject, uri, 1, 0, "QAlphaCloud", tr("Cannot create instances of QAlphaCloud.")); 209 | } 210 | 211 | #include "qmlplugin.moc" 212 | -------------------------------------------------------------------------------- /src/systemstats/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-2-Clause 2 | # SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | 4 | add_library(ksystemstats_plugin_qalphacloud MODULE 5 | plugin.cpp 6 | plugin.h 7 | dailydataobject.cpp 8 | dailydataobject.h 9 | livedataobject.cpp 10 | livedataobject.h 11 | systemobject.cpp 12 | systemobject.h 13 | ) 14 | target_link_libraries(ksystemstats_plugin_qalphacloud 15 | Qt${QT_MAJOR_VERSION}::Network 16 | KF${QT_MAJOR_VERSION}::CoreAddons 17 | KSysGuard::SystemStats 18 | QAlphaCloud 19 | ) 20 | install(TARGETS ksystemstats_plugin_qalphacloud DESTINATION ${KSYSTEMSTATS_PLUGIN_INSTALL_DIR}) 21 | -------------------------------------------------------------------------------- /src/systemstats/dailydataobject.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | #include "dailydataobject.h" 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | #include "config-alphacloud.h" 20 | 21 | using namespace QAlphaCloud; 22 | 23 | DailyDataObject::DailyDataObject(QAlphaCloud::Connector *connector, const QString &serialNumber, KSysGuard::SensorContainer *parent) 24 | : SensorObject(serialNumber + QLatin1String("_daily"), parent) 25 | , m_dailyData(new OneDateEnergy(connector, serialNumber, QDate::currentDate(), this)) 26 | { 27 | // TODO Investigate how often this updates. OneDateEnergyModel is in 5 minute intervals. 28 | m_rateLimitTimer.setInterval(1 * 60 * 1000); // 1 min. 29 | m_rateLimitTimer.setSingleShot(true); 30 | connect(&m_rateLimitTimer, &QTimer::timeout, this, [this] { 31 | if (m_updatePending) { 32 | m_updatePending = false; 33 | reload(); 34 | } 35 | }); 36 | 37 | // Photovoltaic energy production: 38 | m_photovoltaicProperty = new KSysGuard::SensorProperty(QStringLiteral("photovoltaic"), tr("Photovoltaic Energy"), 0, this); 39 | m_photovoltaicProperty->setShortName(tr("Photovoltaic")); 40 | m_photovoltaicProperty->setDescription(tr("Amount of photovoltiac energy that has been produced today.")); 41 | m_photovoltaicProperty->setUnit(KSysGuard::Unit::UnitWattHour); 42 | m_photovoltaicProperty->setVariantType(QVariant::Int); 43 | m_photovoltaicProperty->setMin(0); 44 | connect(m_photovoltaicProperty, &KSysGuard::SensorProperty::subscribedChanged, this, &DailyDataObject::update); 45 | 46 | // Total load: 47 | m_totalLoadProperty = new KSysGuard::SensorProperty(QStringLiteral("totalLoad"), tr("Total Load"), 0, this); 48 | m_totalLoadProperty->setShortName(tr("Total")); 49 | m_totalLoadProperty->setDescription(tr("Total amount of energy consumed today.")); 50 | m_totalLoadProperty->setUnit(KSysGuard::Unit::UnitWattHour); 51 | m_totalLoadProperty->setVariantType(QVariant::Int); 52 | m_totalLoadProperty->setMin(0); 53 | connect(m_totalLoadProperty, &KSysGuard::SensorProperty::subscribedChanged, this, &DailyDataObject::update); 54 | 55 | // Power input from grid: 56 | m_inputProperty = new KSysGuard::SensorProperty(QStringLiteral("input"), tr("Energy Input"), 0, this); 57 | m_inputProperty->setShortName(tr("Input")); 58 | m_inputProperty->setDescription(tr("Amount of energy that has input from the grid today.")); 59 | m_inputProperty->setUnit(KSysGuard::Unit::UnitWattHour); 60 | m_inputProperty->setVariantType(QVariant::Int); 61 | m_inputProperty->setMin(0); 62 | connect(m_inputProperty, &KSysGuard::SensorProperty::subscribedChanged, this, &DailyDataObject::update); 63 | 64 | // Power output to grid: 65 | m_outputProperty = new KSysGuard::SensorProperty(QStringLiteral("output"), tr("Energy Output"), 0, this); 66 | m_outputProperty->setShortName(tr("Output")); 67 | m_outputProperty->setDescription(tr("Amount of energy that has output to the grid today.")); 68 | m_outputProperty->setUnit(KSysGuard::Unit::UnitWattHour); 69 | m_outputProperty->setVariantType(QVariant::Int); 70 | m_outputProperty->setMin(0); 71 | connect(m_outputProperty, &KSysGuard::SensorProperty::subscribedChanged, this, &DailyDataObject::update); 72 | 73 | // Battery charge: 74 | m_chargeProperty = new KSysGuard::SensorProperty(QStringLiteral("charge"), tr("Battery Charge"), 0, this); 75 | m_chargeProperty->setShortName(tr("Charge")); 76 | m_chargeProperty->setDescription(tr("Amount of energy that has been charged into the battery today.")); 77 | m_chargeProperty->setUnit(KSysGuard::Unit::UnitWattHour); 78 | m_chargeProperty->setVariantType(QVariant::Int); 79 | m_chargeProperty->setMin(0); 80 | connect(m_chargeProperty, &KSysGuard::SensorProperty::subscribedChanged, this, &DailyDataObject::update); 81 | 82 | // Battery discharge: 83 | m_dischargeProperty = new KSysGuard::SensorProperty(QStringLiteral("discharge"), tr("Battery Discharge"), 0, this); 84 | m_dischargeProperty->setShortName(tr("Discharge")); 85 | m_dischargeProperty->setDescription(tr("Amount of energy that has been discharged from the battery today.")); 86 | m_dischargeProperty->setUnit(KSysGuard::Unit::UnitWattHour); 87 | m_dischargeProperty->setVariantType(QVariant::Int); 88 | m_dischargeProperty->setMin(0); 89 | connect(m_dischargeProperty, &KSysGuard::SensorProperty::subscribedChanged, this, &DailyDataObject::update); 90 | 91 | // Battery charge: 92 | m_gridChargeProperty = new KSysGuard::SensorProperty(QStringLiteral("gridCharge"), tr("Grid Charge"), 0, this); 93 | // m_gridChargeProperty->setShortName(tr("Grid Charge")); 94 | m_gridChargeProperty->setDescription(tr("Amount of energy that has been charged into the battery from the grid today.")); 95 | m_gridChargeProperty->setUnit(KSysGuard::Unit::UnitWattHour); 96 | m_gridChargeProperty->setVariantType(QVariant::Int); 97 | m_gridChargeProperty->setMin(0); 98 | connect(m_gridChargeProperty, &KSysGuard::SensorProperty::subscribedChanged, this, &DailyDataObject::update); 99 | 100 | #if PRESENTATION_BUILD 101 | //: Sensor object name with daily data 102 | setName(tr("Daily")); 103 | #else 104 | //: Sensor object name with daily data 105 | setName(tr("%1 (Daily)").arg(serialNumber)); 106 | #endif 107 | 108 | // Update once initially but make sure the subscriptions have been processed. 109 | // TODO check if this actually works. 110 | QMetaObject::invokeMethod(this, &DailyDataObject::update, Qt::QueuedConnection); 111 | } 112 | 113 | // Update all values in lock-step when ksystemstats asks us to rather than updating them 114 | // when the relevant property change is emitted. 115 | void DailyDataObject::update() 116 | { 117 | reload(); 118 | 119 | if (!m_dailyData->valid()) { 120 | return; 121 | } 122 | 123 | m_photovoltaicProperty->setValue(m_dailyData->photovoltaic()); 124 | m_totalLoadProperty->setValue(m_dailyData->totalLoad()); 125 | m_inputProperty->setValue(m_dailyData->input()); 126 | m_outputProperty->setValue(m_dailyData->output()); 127 | m_chargeProperty->setValue(m_dailyData->charge()); 128 | m_dischargeProperty->setValue(m_dailyData->discharge()); 129 | m_gridChargeProperty->setValue(m_dailyData->gridCharge()); 130 | } 131 | 132 | void DailyDataObject::reload() 133 | { 134 | if (!m_photovoltaicProperty->isSubscribed() && !m_totalLoadProperty->isSubscribed() && !m_inputProperty->isSubscribed() && !m_outputProperty->isSubscribed() 135 | && !m_chargeProperty->isSubscribed() && !m_dischargeProperty->isSubscribed() && !m_gridChargeProperty->isSubscribed()) { 136 | return; 137 | } 138 | 139 | if (m_rateLimitTimer.isActive()) { 140 | m_updatePending = true; 141 | return; 142 | } 143 | 144 | m_dailyData->resetDate(); 145 | m_dailyData->reload(); 146 | 147 | m_rateLimitTimer.start(); 148 | } 149 | -------------------------------------------------------------------------------- /src/systemstats/dailydataobject.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | namespace KSysGuard 15 | { 16 | class SensorContainer; 17 | class SensorProperty; 18 | } // namespace KSysGuard 19 | 20 | namespace QAlphaCloud 21 | { 22 | class Connector; 23 | class OneDateEnergy; 24 | } // namespace QAlphaCloud 25 | 26 | class QModelIndex; 27 | 28 | class DailyDataObject : public KSysGuard::SensorObject 29 | { 30 | public: 31 | DailyDataObject(QAlphaCloud::Connector *connector, const QString &serialNumber, KSysGuard::SensorContainer *parent); 32 | 33 | void update(); 34 | void updateSystem(const QModelIndex &index); 35 | 36 | private: 37 | void reload(); 38 | 39 | QAlphaCloud::OneDateEnergy *m_dailyData = nullptr; 40 | 41 | KSysGuard::SensorProperty *m_photovoltaicProperty = nullptr; 42 | 43 | KSysGuard::SensorProperty *m_totalLoadProperty = nullptr; 44 | 45 | KSysGuard::SensorProperty *m_inputProperty = nullptr; 46 | KSysGuard::SensorProperty *m_outputProperty = nullptr; 47 | 48 | KSysGuard::SensorProperty *m_chargeProperty = nullptr; 49 | KSysGuard::SensorProperty *m_dischargeProperty = nullptr; 50 | KSysGuard::SensorProperty *m_gridChargeProperty = nullptr; 51 | 52 | QTimer m_rateLimitTimer; 53 | bool m_updatePending = false; 54 | }; 55 | -------------------------------------------------------------------------------- /src/systemstats/livedataobject.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | namespace KSysGuard 15 | { 16 | class SensorContainer; 17 | class SensorProperty; 18 | } // namespace KSysGuard 19 | 20 | namespace QAlphaCloud 21 | { 22 | class Connector; 23 | class LastPowerData; 24 | } // namespace QAlphaCloud 25 | 26 | class QModelIndex; 27 | 28 | class LiveDataObject : public KSysGuard::SensorObject 29 | { 30 | public: 31 | LiveDataObject(QAlphaCloud::Connector *connector, const QString &serialNumber, KSysGuard::SensorContainer *parent); 32 | 33 | void update(); 34 | void updateSystem(const QModelIndex &index); 35 | 36 | private: 37 | void reload(); 38 | 39 | QAlphaCloud::LastPowerData *m_liveData = nullptr; 40 | 41 | // Live data: 42 | KSysGuard::SensorProperty *m_photovoltaicPowerProperty = nullptr; 43 | KSysGuard::SensorProperty *m_currentLoadProperty = nullptr; 44 | // TODO generic grid property 45 | KSysGuard::SensorProperty *m_gridFeedProperty = nullptr; 46 | KSysGuard::SensorProperty *m_gridConsumptionProperty = nullptr; 47 | 48 | KSysGuard::SensorProperty *m_batterySocProperty = nullptr; 49 | KSysGuard::SensorProperty *m_batteryEnergyProperty = nullptr; 50 | 51 | KSysGuard::SensorProperty *m_batteryChargeProperty = nullptr; 52 | KSysGuard::SensorProperty *m_batteryDischargeProperty = nullptr; 53 | 54 | KSysGuard::SensorProperty *m_batteryTimeProperty = nullptr; 55 | // TODO battery status property 56 | 57 | // Mirrored from StorageSystemsModel 58 | int m_batteryRemainingCapacityWh = 0; 59 | // When discharging the battery stops (so remaining time is more accurate). 60 | qreal m_batteryDischargeSoc = 0.0; 61 | 62 | QTimer m_rateLimitTimer; 63 | bool m_updatePending = false; 64 | }; 65 | -------------------------------------------------------------------------------- /src/systemstats/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "providername": "qalphacloud" 3 | } 4 | -------------------------------------------------------------------------------- /src/systemstats/metadata.json.license: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: CC0-1.0 2 | SPDX-FileCopyrightText: None 3 | -------------------------------------------------------------------------------- /src/systemstats/plugin.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | #include "plugin.h" 7 | #include "dailydataobject.h" 8 | #include "livedataobject.h" 9 | #include "systemobject.h" 10 | 11 | #include 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | 23 | K_PLUGIN_CLASS_WITH_JSON(SystemStatsPlugin, "metadata.json") 24 | 25 | using namespace QAlphaCloud; 26 | 27 | static QString serialNumberFromIndex(const QModelIndex &index) 28 | { 29 | return index.data(static_cast(StorageSystemsModel::Roles::SerialNumber)).toString(); 30 | } 31 | 32 | SystemStatsPlugin::SystemStatsPlugin(QObject *parent, const QVariantList &args) 33 | : SensorPlugin(parent, args) 34 | , m_container(new KSysGuard::SensorContainer("qalphacloud", tr("Alpha Cloud"), this)) 35 | , m_networkAccessManager(new QNetworkAccessManager(this)) 36 | , m_connector(new QAlphaCloud::Connector(QAlphaCloud::Configuration::defaultConfiguration(), this)) 37 | { 38 | m_networkAccessManager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); 39 | 40 | m_connector->setNetworkAccessManager(m_networkAccessManager); 41 | 42 | auto *storageSystems = new StorageSystemsModel(m_connector, this); 43 | 44 | connect(storageSystems, &StorageSystemsModel::modelAboutToBeReset, this, [this] { 45 | qDeleteAll(m_systems); 46 | m_systems.clear(); 47 | qDeleteAll(m_liveData); 48 | m_liveData.clear(); 49 | qDeleteAll(m_dailyData); 50 | m_dailyData.clear(); 51 | }); 52 | 53 | connect(storageSystems, &StorageSystemsModel::modelReset, this, [this, storageSystems] { 54 | for (int i = 0; i < storageSystems->rowCount(); ++i) { 55 | const QModelIndex index = storageSystems->index(i, 0); 56 | if (!index.isValid()) { // shouldn't happen. 57 | continue; 58 | } 59 | 60 | addStorageSystem(index); 61 | } 62 | }); 63 | 64 | connect(storageSystems, &StorageSystemsModel::rowsInserted, this, [this, storageSystems](const QModelIndex &parent, int from, int to) { 65 | for (int i = from; i <= to; ++i) { 66 | const QModelIndex index = storageSystems->index(i, 0, parent); 67 | if (!index.isValid()) { // shouldn't happen. 68 | continue; 69 | } 70 | 71 | addStorageSystem(index); 72 | } 73 | }); 74 | connect(storageSystems, &StorageSystemsModel::rowsAboutToBeRemoved, this, [this, storageSystems](const QModelIndex &parent, int from, int to) { 75 | for (int i = from; i <= to; ++i) { 76 | const QModelIndex index = storageSystems->index(i, 0, parent); 77 | if (!index.isValid()) { 78 | continue; 79 | } 80 | 81 | removeStorageSystem(index); 82 | } 83 | }); 84 | connect(storageSystems, 85 | &StorageSystemsModel::dataChanged, 86 | this, 87 | [this, storageSystems](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { 88 | Q_UNUSED(roles); 89 | 90 | for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { 91 | const QModelIndex index = storageSystems->index(i, 0); 92 | const QString serialNumber = serialNumberFromIndex(index); 93 | 94 | if (auto *storageSystem = m_systems.value(serialNumber)) { 95 | storageSystem->update(index); 96 | } 97 | if (auto *liveData = m_liveData.value(serialNumber)) { 98 | liveData->updateSystem(index); 99 | } 100 | } 101 | }); 102 | 103 | storageSystems->reload(); 104 | } 105 | 106 | SystemStatsPlugin::~SystemStatsPlugin() = default; 107 | 108 | void SystemStatsPlugin::addStorageSystem(const QModelIndex &index) 109 | { 110 | const QString serialNumber = serialNumberFromIndex(index); 111 | if (m_systems.contains(serialNumber)) { // shouldn't happen. 112 | return; 113 | } 114 | 115 | auto *storageSystem = new SystemObject(serialNumber, m_container); 116 | storageSystem->update(index); 117 | m_systems.insert(serialNumber, storageSystem); 118 | 119 | auto *liveData = new LiveDataObject(m_connector, serialNumber, m_container); 120 | liveData->updateSystem(index); 121 | m_liveData.insert(serialNumber, liveData); 122 | 123 | auto *dailyData = new DailyDataObject(m_connector, serialNumber, m_container); 124 | m_dailyData.insert(serialNumber, dailyData); 125 | } 126 | 127 | void SystemStatsPlugin::removeStorageSystem(const QModelIndex &index) 128 | { 129 | const QString serialNumber = serialNumberFromIndex(index); 130 | // deleteLater? 131 | delete m_systems.take(serialNumber); 132 | delete m_liveData.take(serialNumber); 133 | delete m_dailyData.take(serialNumber); 134 | } 135 | 136 | void SystemStatsPlugin::update() 137 | { 138 | // TODO reload storage systems model occasionally 139 | 140 | for (auto *liveData : std::as_const(m_liveData)) { 141 | liveData->update(); 142 | } 143 | for (auto *dailyData : std::as_const(m_dailyData)) { 144 | dailyData->update(); 145 | } 146 | } 147 | 148 | #include "plugin.moc" 149 | -------------------------------------------------------------------------------- /src/systemstats/plugin.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | namespace KSysGuard 11 | { 12 | class SensorContainer; 13 | } 14 | 15 | namespace QAlphaCloud 16 | { 17 | class Connector; 18 | } 19 | 20 | class QNetworkAccessManager; 21 | class QModelIndex; 22 | 23 | class DailyDataObject; 24 | class LiveDataObject; 25 | class SystemObject; 26 | 27 | class SystemStatsPlugin : public KSysGuard::SensorPlugin 28 | { 29 | Q_OBJECT 30 | 31 | public: 32 | SystemStatsPlugin(QObject *parent, const QVariantList &args); 33 | ~SystemStatsPlugin() override; 34 | 35 | void update() override; 36 | 37 | QString providerName() const override 38 | { 39 | return QStringLiteral("qalphacloud"); 40 | } 41 | 42 | private: 43 | void addStorageSystem(const QModelIndex &index); 44 | void removeStorageSystem(const QModelIndex &index); 45 | 46 | KSysGuard::SensorContainer *m_container; 47 | 48 | QNetworkAccessManager *m_networkAccessManager; 49 | 50 | QAlphaCloud::Connector *m_connector; 51 | 52 | QHash m_systems; 53 | QHash m_liveData; 54 | QHash m_dailyData; 55 | }; 56 | -------------------------------------------------------------------------------- /src/systemstats/systemobject.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | #include "systemobject.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "config-alphacloud.h" 16 | 17 | using namespace QAlphaCloud; 18 | 19 | SystemObject::SystemObject(const QString &serialNumber, 20 | // DO NOT store this QModelIndex. It is only for 21 | // initial popuplation of the container. 22 | KSysGuard::SensorContainer *parent) 23 | // TODO strip out dangerous special characters when serial is used as ID. 24 | // TODO can we use a sub hierarchy? 25 | : SensorObject(serialNumber + QLatin1String("_system"), parent) 26 | { 27 | m_serialNumberProperty = new KSysGuard::SensorProperty(QStringLiteral("serialNumber"), tr("Serial Number"), this); 28 | m_serialNumberProperty->setShortName(tr("Serial")); 29 | 30 | m_inverterModelProperty = new KSysGuard::SensorProperty(QStringLiteral("inverterModel"), tr("Inverter Model"), this); 31 | m_inverterModelProperty->setShortName(tr("Inverter")); 32 | 33 | m_batteryModelProperty = new KSysGuard::SensorProperty(QStringLiteral("batteryModel"), tr("Battery Model"), this); 34 | m_batteryModelProperty->setShortName(tr("Battery")); 35 | 36 | m_batteryGrossCapacityProperty = new KSysGuard::SensorProperty(QStringLiteral("batteryGrossCapacity"), tr("Battery Gross Capacity"), this); 37 | m_batteryGrossCapacityProperty->setShortName(tr("Capacity")); 38 | m_batteryGrossCapacityProperty->setDescription(tr("Gross capacity of the battery")); 39 | m_batteryGrossCapacityProperty->setUnit(KSysGuard::UnitWattHour); 40 | m_batteryGrossCapacityProperty->setVariantType(QVariant::Int); 41 | m_batteryGrossCapacityProperty->setMin(0); 42 | 43 | m_batteryRemainingCapacityProperty = new KSysGuard::SensorProperty(QStringLiteral("batteryRemainingCapacity"), tr("Battery Remaining Capacity"), this); 44 | m_batteryRemainingCapacityProperty->setShortName(tr("Remaining")); 45 | m_batteryRemainingCapacityProperty->setDescription(tr("Remaining capacity of the battery")); 46 | m_batteryRemainingCapacityProperty->setUnit(KSysGuard::UnitWattHour); 47 | m_batteryRemainingCapacityProperty->setVariantType(QVariant::Int); 48 | m_batteryRemainingCapacityProperty->setMin(0); 49 | 50 | m_batteryUsableCapacityProperty = new KSysGuard::SensorProperty(QStringLiteral("batteryUsableCapacity"), tr("Battery Usable Capacity"), this); 51 | m_batteryUsableCapacityProperty->setShortName(tr("Usable")); 52 | m_batteryUsableCapacityProperty->setDescription(tr("Percentage of usable battery capacity")); 53 | m_batteryUsableCapacityProperty->setUnit(KSysGuard::UnitPercent); 54 | m_batteryUsableCapacityProperty->setVariantType(QVariant::Int); 55 | m_batteryUsableCapacityProperty->setMin(0); 56 | 57 | // TODO ems status? 58 | // TODO inverterPower max sensor? 59 | // TODO photovoltaic max power? 60 | // Tried setting this as "max" for the PV power sensor but that caused the 61 | // scale to always use that value which was annoying. 62 | 63 | #if PRESENTATION_BUILD 64 | //: Sensor object name with system information 65 | setName(tr("System")); 66 | #else 67 | //: Sensor object name with system information 68 | setName(tr("%1 (System)").arg(serialNumber)); 69 | #endif 70 | } 71 | 72 | void SystemObject::update(const QModelIndex &index) 73 | { 74 | #if PRESENTATION_BUILD 75 | m_serialNumberProperty->setValue(tr("")); 76 | #else 77 | const QString serialNumber = index.data(static_cast(StorageSystemsModel::Roles::SerialNumber)).toString(); 78 | m_serialNumberProperty->setValue(serialNumber); 79 | #endif 80 | 81 | #if PRESENTATION_BUILD 82 | m_inverterModelProperty->setValue(tr("")); 83 | #else 84 | const QString inverterModel = index.data(static_cast(StorageSystemsModel::Roles::InverterModel)).toString(); 85 | m_inverterModelProperty->setValue(inverterModel); 86 | #endif 87 | 88 | #if PRESENTATION_BUILD 89 | m_batteryModelProperty->setValue(tr("")); 90 | #else 91 | const QString batteryModel = index.data(static_cast(StorageSystemsModel::Roles::BatteryModel)).toString(); 92 | m_batteryModelProperty->setValue(batteryModel); 93 | #endif 94 | 95 | const auto batteryGrossCapacity = index.data(static_cast(StorageSystemsModel::Roles::BatteryGrossCapacity)).value(); 96 | m_batteryGrossCapacityProperty->setValue(batteryGrossCapacity); 97 | 98 | const auto batteryRemainingCapacity = index.data(static_cast(StorageSystemsModel::Roles::BatteryRemainingCapacity)).value(); 99 | m_batteryRemainingCapacityProperty->setValue(batteryRemainingCapacity); 100 | 101 | const auto batteryUsableCapacity = index.data(static_cast(StorageSystemsModel::Roles::BatteryUsableCapacity)).value(); 102 | m_batteryUsableCapacityProperty->setValue(batteryUsableCapacity); 103 | } 104 | -------------------------------------------------------------------------------- /src/systemstats/systemobject.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | namespace KSysGuard 11 | { 12 | class SensorContainer; 13 | class SensorProperty; 14 | } // namespace KSysGuard 15 | 16 | class SystemObject : public KSysGuard::SensorObject 17 | { 18 | public: 19 | SystemObject(const QString &serialNumber, KSysGuard::SensorContainer *parent); 20 | 21 | void update(const QModelIndex &index); 22 | 23 | private: 24 | KSysGuard::SensorProperty *m_serialNumberProperty = nullptr; 25 | KSysGuard::SensorProperty *m_inverterModelProperty = nullptr; 26 | KSysGuard::SensorProperty *m_batteryModelProperty = nullptr; 27 | 28 | KSysGuard::SensorProperty *m_batteryGrossCapacityProperty = nullptr; 29 | KSysGuard::SensorProperty *m_batteryRemainingCapacityProperty = nullptr; 30 | KSysGuard::SensorProperty *m_batteryUsableCapacityProperty = nullptr; 31 | }; 32 | --------------------------------------------------------------------------------