diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..0aa54a5
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "3rdparty/qmsgpack"]
+ path = 3rdparty/qmsgpack
+ url = ../../0xFEEDC0DE64/qmsgpack.git
+[submodule "3rdparty/QtZeroConf"]
+ path = 3rdparty/QtZeroConf
+ url = ../../0xFEEDC0DE64/QtZeroConf.git
diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt
new file mode 100644
index 0000000..f3cff6a
--- /dev/null
+++ b/3rdparty/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_subdirectory(qmsgpack)
+add_subdirectory(QtZeroConf)
diff --git a/3rdparty/QtZeroConf b/3rdparty/QtZeroConf
new file mode 160000
index 0000000..2fbb52b
--- /dev/null
+++ b/3rdparty/QtZeroConf
@@ -0,0 +1 @@
+Subproject commit 2fbb52b70820a933ca954fdc3befaebc32072e3b
diff --git a/3rdparty/qmsgpack b/3rdparty/qmsgpack
new file mode 160000
index 0000000..0117422
--- /dev/null
+++ b/3rdparty/qmsgpack
@@ -0,0 +1 @@
+Subproject commit 01174223398cd6d7ab5017051711b306fed3eecf
diff --git a/AboutPage.qml b/AboutPage.qml
new file mode 100644
index 0000000..ea8ad82
--- /dev/null
+++ b/AboutPage.qml
@@ -0,0 +1,23 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("About")
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Firmware")
+ component: "FirmwarePage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Hardware information")
+ component: "HardwareInformationPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Licenses")
+ component: "LicensesPage.qml"
+ }
+}
diff --git a/AccessPage.qml b/AccessPage.qml
new file mode 100644
index 0000000..dde07c1
--- /dev/null
+++ b/AccessPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Access")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/AddDeviceScreen.qml b/AddDeviceScreen.qml
new file mode 100644
index 0000000..6f1806b
--- /dev/null
+++ b/AddDeviceScreen.qml
@@ -0,0 +1,115 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+NavigationPage {
+ title: qsTr("Setup or add device")
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: localLayout.implicitHeight
+
+ color: "white"
+ radius: 5
+
+ ColumnLayout {
+ id: localLayout
+
+ anchors.fill: parent
+
+ Text {
+ text: qsTr("Add via local connection or AP:")
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ TextField {
+ id: manualUrl
+
+ Layout.fillWidth: true
+
+ placeholderText: qsTr("Local url")
+ text: "ws://10.128.250.181/ws"
+ // validator: RegularExpressionValidator { regularExpression: /^wss?://.*/ }
+ onAccepted: connectLocalButton.clicked()
+ }
+
+ Button {
+ id: connectLocalButton
+ text: qsTr("Connect")
+ onClicked: deviceSelected(manualUrl.text, "")
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.preferredHeight: 50
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: cloudLayout.implicitHeight
+
+ color: "white"
+ radius: 5
+
+ ColumnLayout {
+ id: cloudLayout
+
+ anchors.fill: parent
+ spacing: 5
+
+ Text {
+ text: qsTr("Add via cloud:")
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ TextField {
+ id: cloudSerial
+
+ Layout.fillWidth: true
+
+ placeholderText: qsTr("Cloud Serial")
+ text: "000000"
+ onTextChanged: {
+ const serial = Number(text);
+ for (let i = 0; i < cloudUrlsModel.count; ++i) {
+ const entry = cloudUrlsModel.get(i);
+ if (serial >= entry.minSerial && serial <= entry.maxSerial) {
+ cloudType.currentIndex = i;
+ break;
+ }
+ }
+ }
+ inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhFormattedNumbersOnly
+ validator: RegularExpressionValidator { regularExpression: /^[0-9]{6,}$/ }
+ onAccepted: connectCloudButton.clicked()
+ }
+
+ ComboBox {
+ id: cloudType
+
+ textRole: "text"
+ valueRole: "url"
+
+ model: cloudUrlsModel
+ }
+
+ Button {
+ id: connectCloudButton
+ text: qsTr("Connect")
+ onClicked: deviceSelected(cloudType.currentValue + cloudSerial.text, "")
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+}
diff --git a/ApiKeyValueItem.qml b/ApiKeyValueItem.qml
new file mode 100644
index 0000000..c91dc8e
--- /dev/null
+++ b/ApiKeyValueItem.qml
@@ -0,0 +1,18 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+Text {
+ property alias apiKey: apiKeyValueHelper.apiKey
+ property alias exists: apiKeyValueHelper.exists
+
+ Layout.fillWidth: true
+
+ ApiKeyValueHelper {
+ id: apiKeyValueHelper
+ deviceConnection: mainScreen.deviceConnection
+ }
+
+ text: apiKeyValueHelper.value
+}
diff --git a/ApiSettingsPage.qml b/ApiSettingsPage.qml
new file mode 100644
index 0000000..eb2a5a1
--- /dev/null
+++ b/ApiSettingsPage.qml
@@ -0,0 +1,181 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+NavigationPage {
+ title: qsTr("API Settings")
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: localApiLayout.implicitHeight
+ visible: localApi.exists
+
+ ColumnLayout {
+ id: localApiLayout
+ anchors.fill: parent
+
+ GeneralOnOffSwitch {
+ id: localApi
+ apiKey: "hai"
+ text: qsTr("Allow access to local HTTP-Api v2")
+ }
+
+ Button {
+ text: qsTr("API documentation")
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ SendMessageHelper {
+ id: apiToken
+ deviceConnection: mainScreen.deviceConnection
+ }
+
+ Button {
+ text: qsTr("Generate token")
+ onClicked: apiToken.sendMessage({ type: "generateApiToken" })
+ }
+
+ BusyIndicator {
+ visible: apiToken.pending
+ }
+
+ Text {
+ Layout.fillWidth: true
+
+ visible: apiToken.response
+ text: {
+ if (!apiToken.response)
+ return ""
+ if (apiToken.response.success)
+ return qsTr("OK! token: %0").arg(apiToken.response.token)
+ else
+ return apiToken.response.message
+ }
+ wrapMode: Text.Wrap
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ ApiKeyValueHelper {
+ id: tokenSetup
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "hatv"
+ }
+
+ visible: tokenSetup.value
+
+ SendMessageHelper {
+ id: abortFirmwareUpdate
+ deviceConnection: mainScreen.deviceConnection
+ }
+
+ Button {
+ text: qsTr("Clear API token")
+
+ onClicked: abortFirmwareUpdate.sendMessage({ type: "clearApiToken" })
+ }
+
+ BusyIndicator {
+ visible: abortFirmwareUpdate.pending
+ }
+
+ RequestStatusText {
+ Layout.fillWidth: true
+ request: abortFirmwareUpdate
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: cloudApiLayout.implicitHeight
+ visible: cloudApi.exists
+
+ ColumnLayout {
+ id: cloudApiLayout
+ anchors.fill: parent
+
+ GeneralOnOffSwitch {
+ id: cloudApi
+ apiKey: "cae"
+ text: qsTr("Enable cloud API")
+ }
+
+ ApiKeyValueHelper {
+ id: cloudApiKey
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "cak"
+ }
+
+ Text {
+ visible: cloudApiKey.exists
+ text: qsTr("API key: %0").arg(cloudApiKey.value)
+ }
+
+ Button {
+ text: qsTr("API documentation")
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: gridApiLayout.implicitHeight
+ visible: gridApi.exists
+
+ ColumnLayout {
+ id: gridApiLayout
+ anchors.fill: parent
+
+ GeneralOnOffSwitch {
+ Layout.fillWidth: true
+ id: gridApi
+ apiKey: "gme"
+ text: qsTr("Enable grid API")
+ }
+
+ ApiKeyValueHelper {
+ id: gridApiKey
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "gmk"
+ }
+
+ Text {
+ Layout.fillWidth: true
+ visible: gridApiKey.exists
+ text: qsTr("API key: %0").arg(gridApiKey.value)
+ }
+
+ Button {
+ text: qsTr("API documentation")
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: legacyApiLayout.implicitHeight
+ visible: legacyApi.exists
+
+ ColumnLayout {
+ id: legacyApiLayout
+ anchors.fill: parent
+
+ GeneralOnOffSwitch {
+ id: legacyApi
+ apiKey: "hla"
+ text: qsTr("Allow access to legacy HTTP-Api v1")
+ }
+
+ Button {
+ text: qsTr("API documentation")
+ }
+ }
+ }
+}
diff --git a/AppInstance.qml b/AppInstance.qml
new file mode 100644
index 0000000..c7a4ac8
--- /dev/null
+++ b/AppInstance.qml
@@ -0,0 +1,35 @@
+import QtQuick
+import QtQuick.Controls.Material
+import QtQuick.Layouts
+import EVChargerApp
+
+Loader {
+ id: loader
+
+ sourceComponent: deviceList
+
+ function backPressed() {
+ return loader.item.backPressed()
+ }
+
+ Component {
+ id: deviceList
+
+ DeviceListScreen {
+ //onDeviceSelected: (url, password) => loader.setSource("DeviceScreen.qml", { url, password })
+ onDeviceSelected: function(url, password) {
+ loader.sourceComponent = deviceScreen
+ loader.item.url = url;
+ loader.item.password = password;
+ }
+ }
+ }
+
+ Component {
+ id: deviceScreen
+
+ DeviceScreen {
+ onClose: loader.sourceComponent = deviceList
+ }
+ }
+}
diff --git a/AppSettingsPage.qml b/AppSettingsPage.qml
new file mode 100644
index 0000000..8a879bd
--- /dev/null
+++ b/AppSettingsPage.qml
@@ -0,0 +1,60 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+NavigationPage {
+ title: qsTr("App Settings")
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: gridLayout.implicitHeight
+ color: "white"
+ radius: 5
+
+ GridLayout {
+ id: gridLayout
+ columns: 2
+
+ Label {
+ text: qsTr("Number of app instances:")
+ font.bold: true
+ }
+
+ SpinBox {
+ value: theSettings.numberOfAppInstances
+ onValueModified: theSettings.numberOfAppInstances = value
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: gridLayout2.implicitHeight
+ color: "white"
+ radius: 5
+
+ GridLayout {
+ id: gridLayout2
+ columns: 2
+
+ Label {
+ text: qsTr("solalaweb key:")
+ font.bold: true
+ }
+
+ Button {
+ text: qsTr("Select...")
+ }
+
+ Label {
+ text: qsTr("solalaweb cert:")
+ font.bold: true
+ }
+
+ Button {
+ text: qsTr("Select...")
+ }
+ }
+ }
+}
diff --git a/BaseNavigationPage.qml b/BaseNavigationPage.qml
new file mode 100644
index 0000000..2e80e97
--- /dev/null
+++ b/BaseNavigationPage.qml
@@ -0,0 +1,39 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Item {
+ Layout.fillWidth: true
+
+ property alias title: titleText.text
+ default property alias data: columnLayout.data
+ property alias headerItems: headerLayout.data
+
+ ColumnLayout {
+ id: columnLayout
+
+ anchors.fill: parent
+
+ RowLayout {
+ id: headerLayout
+
+ Layout.fillWidth: true
+
+ Button {
+ text: qsTr("Back")
+ visible: stackView.depth > 1
+ onClicked: stackView.pop()
+ }
+
+ Text {
+ Layout.fillWidth: true
+
+ id: titleText
+
+ font.pixelSize: 30
+ font.bold: true
+ wrapMode: Text.Wrap
+ }
+ }
+ }
+}
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..7ec9514
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,158 @@
+cmake_minimum_required(VERSION 3.16)
+project(evcharger-app LANGUAGES CXX)
+set(CMAKE_AUTOMOC ON)
+#set(CMAKE_CXX_STANDARD 23)
+#set(CMAKE_CXX_STANDARD_REQUIRED ON)
+#set(CMAKE_CXX_EXTENSIONS ON)
+add_compile_options(-std=c++2b)
+
+find_program(CCACHE_FOUND ccache)
+if(CCACHE_FOUND)
+ set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
+ set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
+endif(CCACHE_FOUND)
+
+add_definitions(-DQT_GUI_LIB)
+add_subdirectory(3rdparty)
+
+find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick WebSockets LinguistTools)
+
+qt_standard_project_setup(REQUIRES 6.6 I18N_TRANSLATED_LANGUAGES de)
+
+qt_add_executable(evcharger-app WIN32 MACOSX_BUNDLE
+apikeyexistancehelper.cpp
+apikeyexistancehelper.h
+apikeyvaluehelper.cpp
+apikeyvaluehelper.h
+appsettings.cpp
+appsettings.h
+deviceconnection.cpp
+deviceconnection.h
+devicesmodel.cpp
+devicesmodel.h
+main.cpp
+sendmessagehelper.cpp
+sendmessagehelper.h
+)
+
+qt6_add_translations(evcharger-app
+ RESOURCE_PREFIX /EVChargerApp/i18n
+ TS_FILE_BASE qml
+ TS_FILE_DIR i18n
+)
+
+set_property(TARGET evcharger-app APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
+
+if (ANDROID)
+ include(${ANDROID_SDK_ROOT}/android_openssl/CMakeLists.txt)
+ set_target_properties(evcharger-app PROPERTIES QT_ANDROID_EXTRA_LIBS "${ANDROID_EXTRA_LIBS}")
+endif()
+
+qt_add_qml_module(evcharger-app
+ URI EVChargerApp
+ VERSION 1.0
+ QML_FILES
+ AboutPage.qml
+ AccessPage.qml
+ AddDeviceScreen.qml
+ ApiKeyValueItem.qml
+ ApiSettingsPage.qml
+ AppInstance.qml
+ AppSettingsPage.qml
+ BaseNavigationPage.qml
+ CablePage.qml
+ CarPage.qml
+ ChargerTabPage.qml
+ ChargingConfigurationPage.qml
+ ChargingSpeedPage.qml
+ CloudUrlsModel.qml
+ CloudPage.qml
+ ConnectingScreen.qml
+ ConnectionPage.qml
+ ControllerPage.qml
+ ControllerTabPage.qml
+ CurrentLevelsPage.qml
+ DailyTripPage.qml
+ DateAndTimePage.qml
+ DeviceListScreen.qml
+ DeviceScreen.qml
+ DisplaySettingsPage.qml
+ EcoTabPage.qml
+ EthernetPage.qml
+ EVChargerApp.qml
+ FirmwarePage.qml
+ FlexibleEnergyTariffPage.qml
+ GeneralOnOffSwitch.qml
+ GeneralPage.qml
+ GridPage.qml
+ GroundCheckPage.qml
+ HardwareInformationPage.qml
+ HotspotPage.qml
+ InformationsTabPage.qml
+ KwhLimitPage.qml
+ LedPage.qml
+ LicensesPage.qml
+ LoadBalancingPage.qml
+ MainScreen.qml
+ NamePage.qml
+ NavigationItem.qml
+ NavigationPage.qml
+ NotificationsPage.qml
+ OcppPage.qml
+ PasswordPage.qml
+ PvSurplusPage.qml
+ RebootPage.qml
+ RequestStatusText.qml
+ SchedulerPage.qml
+ SecurityPage.qml
+ SensorsConfigurationPage.qml
+ SettingsTabPage.qml
+ SwitchLanguagePage.qml
+ VerticalTabButton.qml
+ WiFiErrorsPage.qml
+ WiFiOnOffSwitch.qml
+ WiFiPage.qml
+ RESOURCES
+ icons/Charger.svg
+ icons/ChargerV3.svg
+ icons/ChargerV4.svg
+ icons/Controller.svg
+ icons/Alarm.svg
+ icons/EcoModeFilled.svg
+ icons/Charts.svg
+ images/controller.png
+ images/geminiFlex.png
+ images/homeFix.png
+ images/homePlus.png
+ images/wattpilot.png
+ images/phoenix.png
+ images/geminiFix.png
+ material-icons/add.svg
+ material-icons/grid_guides.svg
+ material-icons/settings.svg
+ ui-icons/MaterialIcons-Regular.ttf
+)
+
+target_link_libraries(evcharger-app PUBLIC
+ Qt6::Core
+ Qt6::Gui
+ Qt6::Quick
+ Qt6::WebSockets
+ qmsgpack
+ QtZeroConf
+)
+
+install(TARGETS evcharger-app
+ BUNDLE DESTINATION .
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+)
+
+qt_generate_deploy_qml_app_script(
+ TARGET evcharger-app
+ OUTPUT_SCRIPT deploy_script
+ MACOS_BUNDLE_POST_BUILD
+ NO_UNSUPPORTED_PLATFORM_ERROR
+ DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM
+)
+install(SCRIPT ${deploy_script})
diff --git a/CablePage.qml b/CablePage.qml
new file mode 100644
index 0000000..f1f9d1f
--- /dev/null
+++ b/CablePage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Cable")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/CarPage.qml b/CarPage.qml
new file mode 100644
index 0000000..662e943
--- /dev/null
+++ b/CarPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Car")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/ChargerTabPage.qml b/ChargerTabPage.qml
new file mode 100644
index 0000000..cd82d2c
--- /dev/null
+++ b/ChargerTabPage.qml
@@ -0,0 +1,157 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+Page {
+ function backPressed() {
+ return false
+ }
+
+ header: ToolBar {
+ id: toolBar
+
+ background: Rectangle {
+ color: "lightblue"
+ }
+
+ RowLayout {
+ anchors.fill: parent
+
+ Label {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ApiKeyValueHelper {
+ id: friendlyName
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "fna"
+ }
+
+ text: friendlyName.value
+ color: "white"
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ Button {
+ Layout.fillHeight: true
+
+ text: qsTr("Devices")
+ onClicked: loader.close()
+ }
+ }
+ }
+
+ Flickable {
+ id: flickable
+ anchors.fill: parent
+ contentHeight: columnLayout.implicitHeight
+ clip: true
+
+ ColumnLayout {
+ id: columnLayout
+ width: flickable.width
+ spacing: 5
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ ColumnLayout {
+ Layout.fillWidth: true
+
+ Text {
+ Layout.fillWidth: true
+
+ text: qsTr("No car connected")
+ font.pixelSize: 20
+ font.bold: true
+ wrapMode: Text.Wrap
+ }
+
+ Text {
+ Layout.fillWidth: true
+
+ text: qsTr("Connect the cable to charge your car")
+ wrapMode: Text.Wrap
+ }
+ }
+
+ Image {
+ Layout.preferredWidth: parent.width / 4
+ Layout.preferredHeight: paintedHeight
+
+ fillMode: Image.PreserveAspectFit
+
+ ApiKeyValueHelper {
+ id: devicetype
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "typ"
+ }
+
+ ApiKeyValueHelper {
+ id: isgo
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "isgo"
+ }
+
+ source: {
+ if (devicetype.value == 'go-eCharger_V5' ||
+ devicetype.value == 'go-eCharger_V4')
+ {
+ if (isgo.value)
+ return "images/geminiFlex.png"
+ else
+ return "images/geminiFix.png"
+ } else if (devicetype.value == 'wattpilot_V2') {
+ return "images/wattpilot.png"
+ } else if (devicetype.value == 'go-eCharger' ||
+ devicetype.value == 'wattpilot') {
+ return "images/homeFix.png"
+ } else if (devicetype.value == 'go-eCharger_Phoenix') {
+ return "images/phoenix.png"
+ }
+
+ return "material-icons/grid_guides.svg"
+ }
+ }
+ }
+
+ Button {
+ Layout.fillWidth: true
+
+ text: qsTr("Start")
+ }
+
+ ButtonGroup {
+ buttons: column.children
+ }
+
+ RowLayout {
+ id: column
+ Layout.fillWidth: true
+
+ Button {
+ Layout.fillWidth: true
+ checked: true
+ checkable: true
+ text: qsTr("Eco")
+ display: AbstractButton.TextUnderIcon
+ }
+
+ Button {
+ Layout.fillWidth: true
+ checkable: true
+ text: qsTr("Basic")
+ display: AbstractButton.TextUnderIcon
+ }
+
+ Button {
+ Layout.fillWidth: true
+ checkable: true
+ text: qsTr("Daily trip")
+ display: AbstractButton.TextUnderIcon
+ }
+ }
+ }
+ }
+}
diff --git a/ChargingConfigurationPage.qml b/ChargingConfigurationPage.qml
new file mode 100644
index 0000000..0f2d624
--- /dev/null
+++ b/ChargingConfigurationPage.qml
@@ -0,0 +1,53 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Charging configuration")
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Charging Speed")
+ component: "ChargingSpeedPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("kWh Limit")
+ component: "KwhLimitPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Daily Trip")
+ component: "DailyTripPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Flexible energy tariff")
+ component: "FlexibleEnergyTariffPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("PV Surplus")
+ component: "PvSurplusPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Load Balancing")
+ component: "LoadBalancingPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Scheduler")
+ component: "SchedulerPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Current Levels")
+ component: "CurrentLevelsPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Car")
+ component: "CarPage.qml"
+ }
+}
diff --git a/ChargingSpeedPage.qml b/ChargingSpeedPage.qml
new file mode 100644
index 0000000..7a22441
--- /dev/null
+++ b/ChargingSpeedPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Charging Speed")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/CloudPage.qml b/CloudPage.qml
new file mode 100644
index 0000000..7bc9ad2
--- /dev/null
+++ b/CloudPage.qml
@@ -0,0 +1,81 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+NavigationPage {
+ title: qsTr("Cloud")
+
+ GeneralOnOffSwitch {
+ Layout.fillWidth: true
+ apiKey: "cwe"
+ text: qsTr("Enable cloud connection")
+ }
+
+ Text {
+ Layout.fillWidth: true
+ wrapMode: Text.Wrap
+ text: qsTr("Features like flexible energy tariffs, time sync and app connection are unavilable when \"Enable cloud connection\" is disabled")
+ }
+
+ Rectangle {
+ color: "white"
+ radius: 5
+ Layout.fillWidth: true
+ Layout.preferredHeight: gridLayout.implicitHeight
+
+ GridLayout {
+ id: gridLayout
+ anchors.fill: parent
+ columns: 2
+
+ Label {
+ text: qsTr("Trying to connect:")
+ font.bold: true
+ }
+
+ ApiKeyValueItem {
+ apiKey: "cws"
+ }
+
+ Label {
+ text: qsTr("Is connected:")
+ font.bold: true
+ }
+
+ ApiKeyValueItem {
+ apiKey: "cwsc"
+ }
+
+ // TODO cwsca
+
+ Label {
+ text: qsTr("Hello received:")
+ font.bold: true
+ }
+
+ ApiKeyValueItem {
+ apiKey: "chr"
+ }
+
+ Label {
+ text: qsTr("Queue size cloud:")
+ font.bold: true
+ }
+
+ ApiKeyValueItem {
+ apiKey: "qsc"
+ }
+
+ Label {
+ text: qsTr("Last error:")
+ font.bold: true
+ }
+
+ ApiKeyValueItem {
+ apiKey: "cle"
+ wrapMode: Text.Wrap
+ }
+ }
+ }
+}
diff --git a/CloudUrlsModel.qml b/CloudUrlsModel.qml
new file mode 100644
index 0000000..9d5a485
--- /dev/null
+++ b/CloudUrlsModel.qml
@@ -0,0 +1,42 @@
+import QtQuick
+
+ListModel {
+ ListElement {
+ text: qsTr("V2")
+ url: "wss://app.v2.go-e.io/"
+ manufacturer: "go-e"
+ deviceType: "has-no-mdns"
+ minSerial: 0
+ maxSerial: 49999
+ }
+ ListElement {
+ text: qsTr("Wattpilot")
+ url: "wss://app.wattpilot.io/app/"
+ manufacturer: "fronius"
+ deviceType: "wattpilot"
+ minSerial: 10000000
+ maxSerial: 99999999
+ }
+ ListElement {
+ text: qsTr("go-eCharger")
+ url: "wss://app.v3.go-e.io/"
+ manufacturer: "go-e"
+ deviceType: "go-eCharger"
+ minSerial: 50000
+ maxSerial: 499999
+ }
+ ListElement {
+ text: qsTr("go-eController")
+ url: "wss://app.controller.go-e.io/"
+ manufacturer: "go-e"
+ deviceType: "controller"
+ minSerial: 900000
+ maxSerial: 999999
+ }
+ ListElement {
+ text: qsTr("Solalaweb")
+ url: "wss://solalaweb.com/"
+ manufacturer: "go-e"
+ deviceType: "has-no-mdns"
+ }
+}
diff --git a/ConnectingScreen.qml b/ConnectingScreen.qml
new file mode 100644
index 0000000..ccb893e
--- /dev/null
+++ b/ConnectingScreen.qml
@@ -0,0 +1,49 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+ColumnLayout {
+ signal connected
+
+ required property DeviceConnection deviceConnection
+
+ function backPressed() {
+ close()
+ return true
+ }
+
+ Connections {
+ target: deviceConnection
+ onFullStatusReceived: connected()
+ }
+
+ Text {
+ text: qsTr("Trying to reach device...")
+ }
+
+ BusyIndicator {
+ Layout.fillWidth: true
+
+ running: true
+ }
+
+ ListView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ model: collectedMessages
+ clip: true
+
+ delegate: Label {
+ text: message
+ }
+
+ onCountChanged: positionViewAtEnd()
+ }
+
+ Button {
+ text: qsTr("Cancel")
+
+ onClicked: close()
+ }
+}
diff --git a/ConnectionPage.qml b/ConnectionPage.qml
new file mode 100644
index 0000000..75470c9
--- /dev/null
+++ b/ConnectionPage.qml
@@ -0,0 +1,49 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+NavigationPage {
+ title: qsTr("Connection")
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Wi-Fi")
+ component: "WiFiPage.qml"
+ visible: wifiStaApiKeyValueHelper.exists
+ }
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Hotspot")
+ component: "HotspotPage.qml"
+ visible: wifiApApiKeyValueHelper.exists
+ }
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Ethernet")
+ component: "EthernetPage.qml"
+ visible: ethernetApiKeyValueHelper.exists
+ }
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("OCPP")
+ component: "OcppPage.qml"
+ visible: ocppApiKeyValueHelper.exists
+ }
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Cloud")
+ component: "CloudPage.qml"
+ visible: cloudApiKeyValueHelper.exists
+ }
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("API Settings")
+ component: "ApiSettingsPage.qml"
+ }
+}
diff --git a/ControllerPage.qml b/ControllerPage.qml
new file mode 100644
index 0000000..48f97de
--- /dev/null
+++ b/ControllerPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Controller")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/ControllerTabPage.qml b/ControllerTabPage.qml
new file mode 100644
index 0000000..0618a17
--- /dev/null
+++ b/ControllerTabPage.qml
@@ -0,0 +1,123 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+Page {
+ function backPressed() {
+ return false
+ }
+
+ header: ToolBar {
+ id: toolBar
+
+ background: Rectangle {
+ color: "lightblue"
+ }
+
+ RowLayout {
+ anchors.fill: parent
+
+ Label {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ApiKeyValueHelper {
+ id: friendlyName
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "fna"
+ }
+
+ text: friendlyName.value
+ color: "white"
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ Button {
+ Layout.fillHeight: true
+
+ text: qsTr("Devices")
+ onClicked: loader.close()
+ }
+ }
+ }
+
+ Flickable {
+ anchors.fill: parent
+
+ ColumnLayout {
+ Layout.fillWidth: true
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ Text {
+ //Layout.fillWidth: true
+ width: 100
+
+ text: qsTr("Connected")
+ font.pixelSize: 20
+ font.bold: true
+ wrapMode: Text.Wrap
+ }
+ }
+
+ Image {
+ Layout.fillHeight: true
+ // width: 200
+ // height: 200
+ Layout.preferredWidth: 100
+ fillMode: Image.PreserveAspectFit
+
+ source: "images/controller.png"
+ }
+ }
+
+ Text {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ text: "Controller TODO"
+ }
+
+ Text {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ text: "Controller TODO"
+ }
+
+ Text {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ text: "Controller TODO"
+ }
+
+ Text {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ text: "Controller TODO"
+ }
+
+ Text {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ text: "Controller TODO"
+ }
+
+ Text {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ text: "Controller TODO"
+ }
+ }
+ }
+}
diff --git a/CurrentLevelsPage.qml b/CurrentLevelsPage.qml
new file mode 100644
index 0000000..a14f8f5
--- /dev/null
+++ b/CurrentLevelsPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Current Levels")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/DailyTripPage.qml b/DailyTripPage.qml
new file mode 100644
index 0000000..65664ec
--- /dev/null
+++ b/DailyTripPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Daily Trip")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/DateAndTimePage.qml b/DateAndTimePage.qml
new file mode 100644
index 0000000..c631905
--- /dev/null
+++ b/DateAndTimePage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Date and time")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/DeviceListScreen.qml b/DeviceListScreen.qml
new file mode 100644
index 0000000..5be7782
--- /dev/null
+++ b/DeviceListScreen.qml
@@ -0,0 +1,337 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+StackView {
+ id: stackView
+
+ signal deviceSelected(url: string, password: string)
+
+ function backPressed() {
+ if (depth > 1) {
+ pop()
+ return true
+ }
+ return false
+ }
+
+ // pushEnter: Transition {
+ // PropertyAnimation {
+ // property: "opacity"
+ // from: 0
+ // to:1
+ // duration: 200
+ // }
+ // }
+ // pushExit: Transition {
+ // PropertyAnimation {
+ // property: "opacity"
+ // from: 1
+ // to:0
+ // duration: 200
+ // }
+ // }
+ // popEnter: Transition {
+ // PropertyAnimation {
+ // property: "opacity"
+ // from: 0
+ // to:1
+ // duration: 200
+ // }
+ // }
+ // popExit: Transition {
+ // PropertyAnimation {
+ // property: "opacity"
+ // from: 1
+ // to:0
+ // duration: 200
+ // }
+ // }
+
+ initialItem: BaseNavigationPage {
+ id: page
+ title: qsTr("Device list")
+
+ CloudUrlsModel {
+ id: cloudUrlsModel
+ }
+
+ DevicesModel {
+ id: devicesModel
+
+ settings: theSettings
+
+ Component.onCompleted: start()
+ }
+
+ headerItems: [
+ Button {
+ text: qsTr("App Settings")
+ icon.source: "material-icons/settings.svg"
+ display: AbstractButton.IconOnly
+
+ onClicked: stackView.push(appSettingsPage)
+
+ Component {
+ id: appSettingsPage
+
+ AppSettingsPage {
+ }
+ }
+ }
+ ]
+
+ ListView {
+ id: listView
+ model: devicesModel
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ spacing: 10
+ contentX: -15
+
+ ScrollBar.vertical: ScrollBar {
+ interactive: false
+ }
+
+ footer: Item {
+ height: 75
+ }
+
+ section.property: "saved"
+ section.criteria: ViewSection.FullString
+ section.delegate: Component {
+ id: sectionHeading
+ Text {
+ width: ListView.view.width - 30
+ height: implicitHeight + 50
+ bottomPadding: 10
+
+ verticalAlignment: Text.AlignBottom
+ wrapMode: Text.Wrap
+
+ required property bool section
+
+ text: section ? qsTr("My devices") : qsTr("Found devices")
+ font.bold: true
+ font.pixelSize: 20
+ }
+ }
+
+ delegate: SwipeDelegate {
+ id: delegate
+ checkable: true
+ width: ListView.view.width - 30
+
+ required property int index
+
+ required property string name
+ required property string serial
+ required property string manufacturer
+ required property string deviceType
+ required property string friendlyName
+ required property string password
+ required property bool saved
+ required property string hostName
+ required property string ip
+
+ Component.onCompleted: {
+ background.color = "white"
+ background.radius = 5
+ }
+
+ swipe.enabled: saved
+
+ swipe.right: Label {
+ id: deleteLabel
+ text: qsTr("Delete")
+ color: "white"
+ verticalAlignment: Label.AlignVCenter
+ padding: 12
+ height: parent.height
+ anchors.right: parent.right
+
+ SwipeDelegate.onClicked: {
+ listView.model.removeRow(delegate.index)
+ swipe.close()
+ delegate.checked = false;
+ }
+
+ background: Rectangle {
+ color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) : "tomato"
+ }
+ }
+
+ contentItem: ColumnLayout {
+ RowLayout {
+ Image {
+ height: parent.height
+ //Layout.fillHeight: true
+ source: {
+ if (delegate.deviceType == 'go-eCharger_V5' ||
+ delegate.deviceType == 'go-eCharger_V4' ||
+ delegate.deviceType == 'wattpilot_V2')
+ {
+ return "icons/ChargerV4.svg"
+ } else if (delegate.deviceType == 'go-eCharger' ||
+ delegate.deviceType == 'wattpilot') {
+ return "icons/ChargerV3.svg"
+ } else if (delegate.deviceType == 'go-eCharger_Phoenix') {
+ return "icons/Charger.svg"
+ } else if (delegate.deviceType.includes('controller')) {
+ return "icons/Controller.svg"
+ }
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ Label {
+ Layout.fillWidth: true
+ text: delegate.friendlyName
+ font.bold: true
+ elide: Text.ElideRight
+ }
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Serial Number %0").arg(delegate.serial);
+ }
+ }
+ }
+
+ GridLayout {
+ id: grid
+ visible: false
+
+ columns: 2
+ rowSpacing: 10
+ columnSpacing: 10
+
+ Label {
+ text: qsTr("Manufacturer:")
+ Layout.leftMargin: 60
+ }
+
+ Label {
+ text: delegate.manufacturer
+ font.bold: true
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+
+ Label {
+ text: qsTr("Device Type:")
+ Layout.leftMargin: 60
+ }
+
+ Label {
+ text: delegate.deviceType
+ font.bold: true
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+
+ Label {
+ text: qsTr("Host Name:")
+ Layout.leftMargin: 60
+ }
+
+ Label {
+ text: delegate.hostName
+ font.bold: true
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+
+ Label {
+ text: qsTr("Ip:")
+ Layout.leftMargin: 60
+ }
+
+ Label {
+ text: delegate.ip
+ font.bold: true
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+
+ Button {
+ enabled: delegate.ip != ""
+
+ text: qsTr("Connect local")
+ onClicked: deviceSelected("ws://" + delegate.ip + "/ws", delegate.password)
+ }
+
+ Button {
+ property var cloudUrl: {
+ for (let i = 0; i < cloudUrlsModel.count; ++i) {
+ const entry = cloudUrlsModel.get(i);
+ if (delegate.manufacturer === entry.manufacturer &&
+ delegate.deviceType.includes(entry.deviceType)) {
+ return entry.url;
+ }
+ }
+ return null;
+ }
+
+ enabled: cloudUrl !== null
+
+ text: qsTr("Connect cloud")
+ onClicked: deviceSelected(cloudUrl + delegate.serial, delegate.password)
+ }
+ }
+ }
+
+ states: [
+ State {
+ name: "expanded"
+ when: delegate.checked
+
+ PropertyChanges {
+ // TODO: When Qt Design Studio supports generalized grouped properties, change to:
+ // grid.visible: true
+ // qmllint disable Quick.property-changes-parsed
+ target: grid
+ visible: true
+ }
+ }
+ ]
+ }
+ }
+
+ Button {
+ parent: page
+ anchors {
+ right: parent.right
+ rightMargin: 10
+ bottom: parent.bottom
+ bottomMargin: 25
+ }
+
+ text: qsTr("Add or setup device")
+ icon.source: "material-icons/add.svg"
+
+ font.pointSize: 12
+ font.bold: true
+
+ highlighted: true
+ Material.accent: Material.Blue
+
+ display: listView.contentY < 20 ? AbstractButton.TextBesideIcon : AbstractButton.IconOnly
+
+ onClicked: stackView.push(addDeviceScreen)
+
+ Component {
+ id: addDeviceScreen
+
+ AddDeviceScreen {
+ }
+ }
+ }
+ }
+}
diff --git a/DeviceScreen.qml b/DeviceScreen.qml
new file mode 100644
index 0000000..25c6fa0
--- /dev/null
+++ b/DeviceScreen.qml
@@ -0,0 +1,131 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+Loader {
+ id: loader
+
+ signal close
+
+ property alias url: theDeviceConnection.url
+ property alias password: theDeviceConnection.password
+
+ function backPressed() {
+ return item.backPressed()
+ }
+
+ ListModel {
+ id: collectedMessages
+
+ function push(message) {
+ if (collectedMessages.count >= 15)
+ collectedMessages.remove(0);
+ collectedMessages.append({message});
+ }
+ }
+
+ anchors.fill: parent
+
+ DeviceConnection {
+ id: theDeviceConnection
+
+ settings: theSettings
+
+ onLogMessage: (message) => collectedMessages.push(message)
+
+ onAuthRequired: {
+ passwordError.visible = false;
+ passwordDialog.open();
+ }
+ onAuthError: function(message) {
+ passwordError.visible = true;
+ passwordError.text = message;
+ passwordDialog.open();
+ }
+ onAuthImpossible: authImpossibleDialog.open()
+ }
+
+ sourceComponent: Component {
+ ConnectingScreen {
+ deviceConnection: theDeviceConnection
+
+ anchors.fill: parent
+
+ onConnected: loader.sourceComponent = mainScreen;
+ }
+ }
+
+ Component {
+ id: mainScreen
+
+ MainScreen {
+ deviceConnection: theDeviceConnection
+ }
+ }
+
+ Dialog {
+ id: passwordDialog
+
+ x: parent.width / 2 - width / 2
+ y: parent.height / 2 - height / 2
+
+ title: qsTr("Password required")
+ standardButtons: Dialog.Ok | Dialog.Cancel
+ focus: true
+ modal: true
+
+ onAccepted: theDeviceConnection.sendAuth(passwordInput.text)
+ onRejected: loader.close()
+
+ contentItem: GridLayout {
+ property int minimumInputSize: 120
+
+ id: grid
+
+ columns: 2
+
+ Text {
+ id: passwordError
+
+ Layout.columnSpan: 2
+ color: "red"
+ }
+
+ Label {
+ text: qsTr("Password:")
+ Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
+ }
+
+ TextField {
+ id: passwordInput
+ focus: true
+ Layout.fillWidth: true
+ Layout.minimumWidth: grid.minimumInputSize
+ Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
+ placeholderText: qsTr("Password")
+ echoMode: TextInput.PasswordEchoOnEdit
+ onAccepted: passwordDialog.accept();
+ }
+ }
+ }
+
+ Dialog {
+ id: authImpossibleDialog
+
+ x: parent.width / 2 - width / 2
+ y: parent.height / 2 - height / 2
+
+ title: qsTr("Authentication impossible!")
+ standardButtons: Dialog.Ok | Dialog.Cancel
+ focus: true
+ modal: true
+
+ onAccepted: loader.close()
+ onRejected: loader.close()
+
+ contentItem: Text {
+ text: qsTr("To use this password remotely a password has to be setup first. This can be done over the AccessPoint.");
+ }
+ }
+}
diff --git a/DisplaySettingsPage.qml b/DisplaySettingsPage.qml
new file mode 100644
index 0000000..fe6b870
--- /dev/null
+++ b/DisplaySettingsPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Display settings")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/EVChargerApp.qml b/EVChargerApp.qml
new file mode 100644
index 0000000..0a59101
--- /dev/null
+++ b/EVChargerApp.qml
@@ -0,0 +1,67 @@
+import QtQuick
+import QtQuick.Controls.Material
+import QtQuick.Layouts
+import EVChargerApp
+
+ApplicationWindow {
+ id: window
+
+ width: 320
+ height: 480
+ visible: true
+ title: qsTr("EVcharger App")
+ color: "#F3F2F7"
+
+ AppSettings {
+ id: theSettings
+ }
+
+ FontLoader {
+ id: materialIcons
+ source: "ui-icons/MaterialIcons-Regular.ttf"
+ }
+
+ SwipeView {
+ id: view
+
+ anchors.fill: parent
+
+ currentIndex: indicator.currentIndex
+
+ Repeater {
+ model: theSettings.numberOfAppInstances
+
+ AppInstance {
+ width: view.width
+ }
+ }
+ }
+
+ PageIndicator {
+ id: indicator
+
+ count: view.count
+ currentIndex: view.currentIndex
+
+ anchors.top: view.top
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ function backPressed() {
+ if (view.currentItem.backPressed())
+ return true
+
+ if (view.currentIndex > 0) {
+ view.currentIndex = 0
+ view.currentIndex = Qt.binding(() => indicator.currentIndex)
+ return true
+ }
+
+ return false
+ }
+
+ onClosing: (close) => {
+ if (backPressed())
+ close.accepted = false
+ }
+}
diff --git a/EcoTabPage.qml b/EcoTabPage.qml
new file mode 100644
index 0000000..d6da9fb
--- /dev/null
+++ b/EcoTabPage.qml
@@ -0,0 +1,7 @@
+import QtQuick
+
+Item {
+ function backPressed() {
+ return false
+ }
+}
diff --git a/EthernetPage.qml b/EthernetPage.qml
new file mode 100644
index 0000000..c1d0af9
--- /dev/null
+++ b/EthernetPage.qml
@@ -0,0 +1,17 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Ethernet")
+
+ GeneralOnOffSwitch {
+ apiKey: "ee"
+ }
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/FirmwarePage.qml b/FirmwarePage.qml
new file mode 100644
index 0000000..33c924c
--- /dev/null
+++ b/FirmwarePage.qml
@@ -0,0 +1,247 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+NavigationPage {
+ title: qsTr("Firmware")
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: gridLayout.implicitHeight
+
+ color: "white"
+ radius: 5
+
+ GridLayout {
+ id: gridLayout
+
+ anchors.fill: parent
+ columns: 2
+
+ Label {
+ text: qsTr("Running version:")
+ font.bold: true
+ }
+
+ ApiKeyValueItem {
+ apiKey: "fwv"
+ id: runningVersion
+ }
+
+ Label {
+ text: qsTr("Details:")
+ font.bold: true
+ }
+
+ Text {
+ Layout.fillWidth: true
+
+ ApiKeyValueHelper {
+ id: runningVersionDetails
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "apd"
+ }
+
+ text: JSON.stringify(runningVersionDetails.value, null, 4)
+ }
+
+ Label {
+ text: qsTr("Recommended version:")
+ font.bold: true
+ }
+
+ ApiKeyValueItem {
+ id: recommendedVersion
+ apiKey: "onv"
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: columnLayout.implicitHeight
+
+ color: "white"
+ radius: 5
+
+ ColumnLayout {
+ id: columnLayout
+ anchors.fill: parent
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ Label {
+ text: qsTr("Update:")
+ }
+
+ ComboBox {
+ id: firmwareSelector
+
+ ApiKeyValueHelper {
+ id: availableUrls
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "ocu"
+ }
+
+ Layout.fillWidth: true
+ model: availableUrls.value
+
+ delegate: ItemDelegate {
+ text: modelData
+ background: Rectangle {
+ color: recommendedVersion.value == modelData ? "lightgreen" : modelData.includes(runningVersion.value) ? "lightyellow" : "white"
+ }
+ width: parent.width
+ }
+ }
+
+ SendMessageHelper {
+ id: startFirmwareUpdate
+ deviceConnection: mainScreen.deviceConnection
+ }
+
+ Button {
+ text: qsTr("Start")
+
+ onClicked: startFirmwareUpdate.sendMessage({
+ type: "otaCloud",
+ firmware: firmwareSelector.currentValue
+ })
+ }
+
+ BusyIndicator {
+ visible: startFirmwareUpdate.pending
+ }
+
+ RequestStatusText {
+ request: startFirmwareUpdate
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ SendMessageHelper {
+ id: abortFirmwareUpdate
+ deviceConnection: mainScreen.deviceConnection
+ }
+
+ Button {
+ text: qsTr("Abort")
+
+ onClicked: abortFirmwareUpdate.sendMessage({
+ type: "abortFwUpdate"
+ })
+ }
+
+ BusyIndicator {
+ visible: abortFirmwareUpdate.pending
+ }
+
+ RequestStatusText {
+ request: abortFirmwareUpdate
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+
+ SendMessageHelper {
+ id: switchAppPartition
+ deviceConnection: mainScreen.deviceConnection
+ }
+
+ Button {
+ text: qsTr("Switch partition")
+
+ onClicked: switchAppPartition.sendMessage({
+ type: "switchAppPartition"
+ })
+ }
+
+ BusyIndicator {
+ visible: switchAppPartition.pending
+ }
+
+ RequestStatusText {
+ request: switchAppPartition
+ }
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 2
+
+ Label {
+ text: qsTr("Update status:")
+ font.bold: true
+ }
+
+ Text {
+ Layout.fillWidth: true
+
+ ApiKeyValueHelper {
+ id: updateStatus
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "ocs"
+ }
+
+ text: {
+ switch (updateStatus.value)
+ {
+ case 0: return 'Idle'
+ case 1: return 'Updating'
+ case 2: return 'Failed'
+ case 3: return 'Succeeded'
+ case 4: return 'NotReady'
+ case 5: return 'Verifying'
+ default: return updateStatus.value
+ }
+ }
+ }
+
+ Label {
+ text: qsTr("Update progress:")
+ font.bold: true
+ }
+
+ ColumnLayout {
+ Text {
+ Layout.fillWidth: true
+
+ ApiKeyValueHelper {
+ id: updateProgress
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "ocp"
+ }
+
+ ApiKeyValueHelper {
+ id: updateLength
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "ocl"
+ }
+
+ text: updateProgress.value + ' / ' + updateLength.value
+ }
+
+ ProgressBar {
+ from: 0
+ to: updateLength.value ? updateLength.value : 0
+ value: updateProgress.value
+ }
+ }
+
+ Label {
+ text: qsTr("Update message:")
+ font.bold: true
+ }
+
+ ApiKeyValueItem {
+ apiKey: "ocm"
+ }
+ }
+ }
+ }
+}
diff --git a/FlexibleEnergyTariffPage.qml b/FlexibleEnergyTariffPage.qml
new file mode 100644
index 0000000..401c1c0
--- /dev/null
+++ b/FlexibleEnergyTariffPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Flexible energy tariff")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/GeneralOnOffSwitch.qml b/GeneralOnOffSwitch.qml
new file mode 100644
index 0000000..cecc956
--- /dev/null
+++ b/GeneralOnOffSwitch.qml
@@ -0,0 +1,50 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+CheckDelegate {
+ id: checkDelegate
+
+ required property string apiKey
+ property alias exists: valueHelper.exists
+
+ Layout.fillWidth: true
+
+ Component.onCompleted: {
+ background.color = "white"
+ background.radius = 5
+ }
+
+ ApiKeyValueHelper {
+ id: valueHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: checkDelegate.apiKey
+ }
+
+ SendMessageHelper {
+ id: valueChanger
+ deviceConnection: mainScreen.deviceConnection
+ onResponseChanged: checkDelegate.checked = Qt.binding(function(){ return valueHelper.value; })
+ }
+
+ checked: valueHelper.value
+ text: valueHelper.value ? qsTr("On") : qsTr("Off")
+ wrapMode: Text.Wrap
+
+ onClicked: {
+ valueChanger.sendMessage({
+ type: "setValue",
+ key: checkDelegate.apiKey,
+ value: checked
+ })
+ }
+
+ BusyIndicator {
+ visible: valueChanger.pending
+ }
+
+ RequestStatusText {
+ request: valueChanger
+ }
+}
diff --git a/GeneralPage.qml b/GeneralPage.qml
new file mode 100644
index 0000000..cb48029
--- /dev/null
+++ b/GeneralPage.qml
@@ -0,0 +1,51 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("General")
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Name")
+ component: "NamePage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Switch Language")
+ component: "SwitchLanguagePage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Notifications")
+ component: "NotificationsPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Date and time")
+ component: "DateAndTimePage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("LED")
+ component: "LedPage.qml"
+ visible: ledApiKeyValueHelper.exists
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Controller")
+ component: "ControllerPage.qml"
+ visible: controllerApiKeyValueHelper.exists
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Display settings")
+ component: "DisplaySettingsPage.qml"
+ visible: displayApiKeyValueHelper.exists
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Reboot")
+ component: "RebootPage.qml"
+ }
+}
diff --git a/GridPage.qml b/GridPage.qml
new file mode 100644
index 0000000..17e140a
--- /dev/null
+++ b/GridPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Grid")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/GroundCheckPage.qml b/GroundCheckPage.qml
new file mode 100644
index 0000000..d40ffc0
--- /dev/null
+++ b/GroundCheckPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Ground check")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/HardwareInformationPage.qml b/HardwareInformationPage.qml
new file mode 100644
index 0000000..0877a50
--- /dev/null
+++ b/HardwareInformationPage.qml
@@ -0,0 +1,58 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+NavigationPage {
+ title: qsTr("Hardware information")
+
+ Rectangle {
+ color: "white"
+ radius: 5
+ Layout.fillWidth: true
+ Layout.preferredHeight: layout.implicitHeight
+
+ GridLayout {
+ id: layout
+ columns: 2
+ anchors.fill: parent
+
+ Label {
+ text: qsTr("Serial Number:")
+ }
+
+ ApiKeyValueItem {
+ Layout.fillWidth: true
+ apiKey: "sse"
+ }
+
+ Label {
+ visible: variant.exists
+ text: qsTr("Variant:")
+ }
+
+ ApiKeyValueItem {
+ id: variant
+ visible: exists
+ Layout.fillWidth: true
+ apiKey: "var"
+ }
+
+ Label {
+ visible: rssi.exists
+ text: qsTr("RSSI:")
+ }
+
+ ApiKeyValueItem {
+ id: rssi
+ visible: exists
+ Layout.fillWidth: true
+ apiKey: "rssi"
+ }
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+}
diff --git a/HotspotPage.qml b/HotspotPage.qml
new file mode 100644
index 0000000..1956827
--- /dev/null
+++ b/HotspotPage.qml
@@ -0,0 +1,22 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Hotspot")
+
+ GeneralOnOffSwitch {
+ apiKey: "wae"
+ }
+
+ GeneralOnOffSwitch {
+ apiKey: "wda"
+ text: qsTr("Disable AP when online")
+ }
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/InformationsTabPage.qml b/InformationsTabPage.qml
new file mode 100644
index 0000000..d6da9fb
--- /dev/null
+++ b/InformationsTabPage.qml
@@ -0,0 +1,7 @@
+import QtQuick
+
+Item {
+ function backPressed() {
+ return false
+ }
+}
diff --git a/KwhLimitPage.qml b/KwhLimitPage.qml
new file mode 100644
index 0000000..c00260f
--- /dev/null
+++ b/KwhLimitPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("kWh Limit")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/LedPage.qml b/LedPage.qml
new file mode 100644
index 0000000..a3c5a69
--- /dev/null
+++ b/LedPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("LED")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/LicensesPage.qml b/LicensesPage.qml
new file mode 100644
index 0000000..e7298fb
--- /dev/null
+++ b/LicensesPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Licenses")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/LoadBalancingPage.qml b/LoadBalancingPage.qml
new file mode 100644
index 0000000..eb40aed
--- /dev/null
+++ b/LoadBalancingPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Load Balancing")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/MainScreen.qml b/MainScreen.qml
new file mode 100644
index 0000000..d70e30e
--- /dev/null
+++ b/MainScreen.qml
@@ -0,0 +1,211 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+ColumnLayout {
+ id: mainScreen
+
+ required property DeviceConnection deviceConnection
+
+ function backPressed() {
+ if (stackLayout.currentItem.item.backPressed())
+ return true
+ if (stackLayout.currentIndex != 0) {
+ stackLayout.currentIndex = 0
+ stackLayout.currentIndex = Qt.binding(() => tabBar.currentIndex)
+ return true
+ }
+ loader.close()
+ return true
+ }
+
+ ApiKeyValueHelper {
+ id: rebootTime
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "rbt"
+ }
+
+ function formatDuration(duration) {
+ duration = Math.floor(duration)
+
+ const wasNegative = duration < 0;
+ if (wasNegative)
+ duration = -duration;
+
+ const milliseconds = duration%1000;
+ duration-=milliseconds;
+ duration/=1000;
+
+ const seconds = duration%60;
+ duration-=seconds;
+ duration/=60;
+
+ const minutes = duration%60;
+ duration-=minutes;
+ duration/=60;
+
+ const hours = duration%24;
+
+ return (wasNegative ? qsTr('%0 ago') : qsTr('in %0'))
+ .arg(
+ (hours < 10 ? '0' : '') + hours + ':' +
+ (minutes < 10 ? '0' : '') + minutes + ':' +
+ (seconds < 10 ? '0' : '') + seconds + '.' +
+ (milliseconds < 100 ? '0' : '') + (milliseconds < 10 ? '0' : '') + milliseconds)
+ ;
+ }
+
+ ApiKeyValueHelper {
+ id: carApiKeyHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "car"
+ }
+
+ ApiKeyValueHelper {
+ id: controllerApiKeyHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "ccp"
+ }
+
+ ListModel {
+ id: modelBoth
+
+ ListElement {
+ text: qsTr("Charger")
+ icon: "icons/ChargerV4.svg"
+ source: "ChargerTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Eco")
+ icon: "icons/EcoModeFilled.svg"
+ source: "EcoTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Controller")
+ icon: "icons/Controller.svg"
+ source: "ControllerTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Infromations")
+ icon: "icons/Charts.svg"
+ source: "InformationsTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Settings")
+ icon: "material-icons/settings.svg"
+ source: "SettingsTabPage.qml"
+ }
+ }
+
+ ListModel {
+ id: modelCharger
+
+ ListElement {
+ text: qsTr("Charger")
+ icon: "icons/ChargerV4.svg"
+ source: "ChargerTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Eco")
+ icon: "icons/EcoModeFilled.svg"
+ source: "EcoTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Infromations")
+ icon: "icons/Charts.svg"
+ source: "InformationsTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Settings")
+ icon: "material-icons/settings.svg"
+ source: "SettingsTabPage.qml"
+ }
+ }
+
+ ListModel {
+ id: modelController
+
+ ListElement {
+ text: qsTr("Controller")
+ icon: "icons/Controller.svg"
+ source: "ControllerTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Infromations")
+ icon: "icons/Charts.svg"
+ source: "InformationsTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Settings")
+ icon: "material-icons/settings.svg"
+ source: "SettingsTabPage.qml"
+ }
+ }
+
+ ListModel {
+ id: modelNone
+
+ ListElement {
+ text: qsTr("Infromations")
+ icon: "icons/Charts.svg"
+ source: "InformationsTabPage.qml"
+ }
+ ListElement {
+ text: qsTr("Settings")
+ icon: "material-icons/settings.svg"
+ source: "SettingsTabPage.qml"
+ }
+ }
+
+ property ListModel theModel: {
+ if (carApiKeyHelper.exists) {
+ if (controllerApiKeyHelper.exists) {
+ return modelBoth
+ } else {
+ return modelCharger
+ }
+ } else {
+ if (controllerApiKeyHelper.exists) {
+ return modelController
+ } else {
+ return modelNone
+ }
+ }
+ }
+
+ StackLayout {
+ id: stackLayout
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ currentIndex: tabBar.currentIndex
+ property Item currentItem: children[currentIndex]
+
+ Repeater {
+ model: theModel
+
+ delegate: Loader {
+ source: model.source
+ }
+ }
+ }
+
+ TabBar {
+ id: tabBar
+ Layout.fillWidth: true
+
+ currentIndex: stackLayout.currentIndex
+ contentHeight: 56
+
+ Repeater {
+ model: theModel
+
+ delegate: VerticalTabButton {
+ text: model.text
+ //width: tabBar.width / tabBar.count
+ icon.source: model.icon
+ }
+ }
+ }
+}
diff --git a/NamePage.qml b/NamePage.qml
new file mode 100644
index 0000000..7a320fc
--- /dev/null
+++ b/NamePage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Name")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/NavigationItem.qml b/NavigationItem.qml
new file mode 100644
index 0000000..f09c9ce
--- /dev/null
+++ b/NavigationItem.qml
@@ -0,0 +1,75 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+ItemDelegate {
+ id: navigationItem
+
+ property alias iconSource: icon.source
+ property alias title: titleText.text
+ property string description
+ property string component
+
+ Layout.fillWidth: true
+ //width: parent.width
+
+ implicitWidth: row.implicitWidth
+ implicitHeight: Math.max(row.implicitHeight, 50)
+
+ // color: "white"
+ // radius: 5
+
+ // MouseArea {
+ // anchors.fill: parent
+
+ // onClicked: stackView.push(navigationItem.component)
+ // }
+
+ // background: Rectangle {
+ // color: "white"
+ // radius: 5
+ // }
+
+ Component.onCompleted: {
+ background.radius = 5
+ background.color = 'white'
+ }
+
+ onClicked: stackView.push(navigationItem.component)
+
+ RowLayout {
+ id: row
+
+ anchors.fill: parent
+
+ Image {
+ id: icon
+ Layout.preferredWidth: 32
+ Layout.preferredHeight: 32
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+
+ Text {
+ id: titleText
+
+ Layout.fillWidth: true
+
+ wrapMode: Text.Wrap
+ font.bold: true
+ }
+
+ Text {
+ Layout.fillWidth: true
+ wrapMode: Text.Wrap
+ text: navigationItem.description
+ visible: text != ""
+ }
+ }
+
+ Text {
+ text: ">"
+ }
+ }
+}
diff --git a/NavigationPage.qml b/NavigationPage.qml
new file mode 100644
index 0000000..9a9daf4
--- /dev/null
+++ b/NavigationPage.qml
@@ -0,0 +1,21 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+BaseNavigationPage {
+ default property alias data: columnLayout.data
+
+ Flickable {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ contentHeight: columnLayout.height
+ clip: true
+
+ ColumnLayout {
+ id: columnLayout
+ width: parent.width - 10
+ x: 5
+ spacing: 5
+ }
+ }
+}
diff --git a/NotificationsPage.qml b/NotificationsPage.qml
new file mode 100644
index 0000000..1fb25a4
--- /dev/null
+++ b/NotificationsPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Notifications")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/OcppPage.qml b/OcppPage.qml
new file mode 100644
index 0000000..0a68a1f
--- /dev/null
+++ b/OcppPage.qml
@@ -0,0 +1,17 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("OCPP")
+
+ GeneralOnOffSwitch {
+ apiKey: "ocppe"
+ }
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/PasswordPage.qml b/PasswordPage.qml
new file mode 100644
index 0000000..458d8f5
--- /dev/null
+++ b/PasswordPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Password")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/PvSurplusPage.qml b/PvSurplusPage.qml
new file mode 100644
index 0000000..223523c
--- /dev/null
+++ b/PvSurplusPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("PV Surplus")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ecc3f15
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# evcharger-app
+
+This app aims to be functional first and pretty second.
+
+The app that is shipped with the products was made in opposite order and has a few problems now.
+
+In contrast to the official go-e app this app has:
+* Actual support to connect over cloud to the device
+* Actual support to connect locally to the device
+* No silly "device type does not match"
+* No silly assumptions on what feature set the device has, it looks on the existance of api keys
+* It shows the problems that the device is reporting like wifi error log, ocpp error log, cloud error log
+* It shows any network communication problems like invalid websocket response received
+* API Key Browser
+* Support for all future go-e compatible products as it does not hardcode any device types (Sadly it also works with Wattpilots)
+* If you enter the incorrect password it will tell you and lock up
+* You can also select 06:00 in the scheduler
+* You can also select any color you like in all color pickers
+
+Lots of settings pages are still missing but this is a side project after work time and doesnt get much attention from Daniel's side.
+
+I just need an app that just works when I stand in fron of my charger, I can't continue using solalaweb for all basic settings every day.
+
+go-e Support is welcome to ship this app to customers that are facing WiFI, OCPP or any other issues.
diff --git a/RebootPage.qml b/RebootPage.qml
new file mode 100644
index 0000000..404ea7d
--- /dev/null
+++ b/RebootPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Reboot")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/RequestStatusText.qml b/RequestStatusText.qml
new file mode 100644
index 0000000..2db4326
--- /dev/null
+++ b/RequestStatusText.qml
@@ -0,0 +1,24 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+Text {
+ required property SendMessageHelper request
+
+ wrapMode: Text.Wrap
+
+ visible: !request.pending && request.response != undefined
+
+ text: {
+ if (request.response == undefined)
+ return ""
+ if (request.response.type == "response") {
+ if (request.response.success)
+ return "OK"
+ if ('message' in request.response)
+ return request.response.message
+ }
+ return JSON.stringify(request.response)
+ }
+}
diff --git a/SchedulerPage.qml b/SchedulerPage.qml
new file mode 100644
index 0000000..47d2815
--- /dev/null
+++ b/SchedulerPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Scheduler")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/SecurityPage.qml b/SecurityPage.qml
new file mode 100644
index 0000000..260731f
--- /dev/null
+++ b/SecurityPage.qml
@@ -0,0 +1,33 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Security")
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Cable")
+ component: "CablePage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Access")
+ component: "AccessPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Password")
+ component: "PasswordPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Grid")
+ component: "GridPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Ground check")
+ component: "GroundCheckPage.qml"
+ }
+}
diff --git a/SensorsConfigurationPage.qml b/SensorsConfigurationPage.qml
new file mode 100644
index 0000000..3fe250b
--- /dev/null
+++ b/SensorsConfigurationPage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Sensors Configuration")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/SettingsTabPage.qml b/SettingsTabPage.qml
new file mode 100644
index 0000000..a1d675e
--- /dev/null
+++ b/SettingsTabPage.qml
@@ -0,0 +1,137 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+StackView {
+ id: stackView
+
+ function backPressed() {
+ if (depth > 1) {
+ pop()
+ return true
+ }
+
+ return false
+ }
+
+ initialItem: NavigationPage {
+ title: qsTr("Settings")
+
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Charging configuration")
+ description: [
+ qsTr("Charging Speed"),
+ qsTr("kWh Limit"),
+ qsTr("Daily Trip"),
+ qsTr("Flexible energy tariff"),
+ qsTr("PV Surplus")
+ ].join(" • ")
+ component: "ChargingConfigurationPage.qml"
+ visible: carApiKeyHelper.exists
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Security")
+ description: [
+ qsTr("Cable"),
+ qsTr("Access"),
+ qsTr("Password"),
+ qsTr("Grid"),
+ qsTr("Ground check")
+ ].join(" • ")
+ component: "SecurityPage.qml"
+ visible: carApiKeyHelper.exists
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Sensors Configuration")
+ description: [
+ qsTr("Sensors"),
+ qsTr("Categories")
+ ].join(" • ")
+ component: "SensorsConfigurationPage.qml"
+ visible: controllerApiKeyHelper.exists
+ }
+ NavigationItem {
+ ApiKeyValueHelper {
+ id: wifiStaApiKeyValueHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "wen"
+ }
+ ApiKeyValueHelper {
+ id: wifiApApiKeyValueHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "wae"
+ }
+ ApiKeyValueHelper {
+ id: ethernetApiKeyValueHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "ee"
+ }
+ ApiKeyValueHelper {
+ id: ocppApiKeyValueHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "ocppe"
+ }
+ ApiKeyValueHelper {
+ id: cloudApiKeyValueHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "cwe"
+ }
+
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Connection")
+ description: [
+ wifiStaApiKeyValueHelper.exists ? qsTr("Wi-Fi") : null,
+ wifiApApiKeyValueHelper.exists ? qsTr("Hotspot") : null,
+ ethernetApiKeyValueHelper.exists ? qsTr("Ethernet") : null,
+ ocppApiKeyValueHelper.exists ? qsTr("OCPP") : null,
+ cloudApiKeyValueHelper.exists ? qsTr("Cloud") : null,
+ qsTr("API Settings")
+ ].filter(Boolean).join(" • ")
+ component: "ConnectionPage.qml"
+ }
+ NavigationItem {
+ ApiKeyValueHelper {
+ id: ledApiKeyValueHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "lbr"
+ }
+ ApiKeyValueHelper {
+ id: controllerApiKeyValueHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "ccd"
+ }
+ ApiKeyValueHelper {
+ id: displayApiKeyValueHelper
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "ccd"
+ }
+
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("General")
+ description: [
+ qsTr("Name"),
+ qsTr("Switch Language"),
+ qsTr("Notifications"),
+ qsTr("Date and time"),
+ ledApiKeyValueHelper.exists ? qsTr("LED") : null,
+ controllerApiKeyValueHelper.exists ? qsTr("Controller") : null,
+ displayApiKeyValueHelper.exists ? qsTr("Display settings") : null
+ ].filter(Boolean).join(" • ")
+ component: "GeneralPage.qml"
+ }
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("About")
+ description: [
+ qsTr("Firmware"),
+ qsTr("Hardware information"),
+ qsTr("Licenses")
+ ].join(" • ")
+ component: "AboutPage.qml"
+ }
+ }
+}
diff --git a/SwitchLanguagePage.qml b/SwitchLanguagePage.qml
new file mode 100644
index 0000000..7dfa5d6
--- /dev/null
+++ b/SwitchLanguagePage.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+NavigationPage {
+ title: qsTr("Switch Language")
+
+ Text {
+ text: "TODO"
+
+ Layout.fillHeight: true
+ }
+}
diff --git a/VerticalTabButton.qml b/VerticalTabButton.qml
new file mode 100644
index 0000000..4056eca
--- /dev/null
+++ b/VerticalTabButton.qml
@@ -0,0 +1,42 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+TabButton {
+ id: control
+
+ contentItem: Item {
+ anchors.fill: parent
+ ColumnLayout {
+ anchors.fill: parent
+ //anchors.horizontalCenter: parent.horizontalCenter
+ spacing: 5
+ Text {
+ Layout.fillWidth: true
+ text: control.text
+ font: control.font
+ horizontalAlignment: Text.AlignHCenter
+ }
+ RowLayout {
+ Layout.fillHeight: true
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ Image {
+ source: control.icon.source
+
+ fillMode: Image.PreserveAspectFit
+
+ width: 32
+ height: 32
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+}
diff --git a/WiFiErrorsPage.qml b/WiFiErrorsPage.qml
new file mode 100644
index 0000000..5569e26
--- /dev/null
+++ b/WiFiErrorsPage.qml
@@ -0,0 +1,86 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+BaseNavigationPage {
+ property ApiKeyValueHelper wifiErrorLog
+
+ title: qsTr("Wi-Fi Errors")
+
+ // Item {
+ // id: theAboveList
+ // // component exists only to complete the example supplied in the question
+ // }
+
+ ListView {
+ id: listView
+ Layout.fillWidth: true
+ Layout.fillHeight: trues
+
+ model: wifiErrorLog.value
+ spacing: 5
+ clip: true
+ verticalLayoutDirection: ListView.BottomToTop
+
+ // anchors
+ // {
+ // left: parent.left
+ // top: theAboveList.bottom
+ // right: parent.right
+ // bottom: parent.bottom
+ // }
+ // header: Item {}
+ // onContentHeightChanged: {
+ // if (contentHeight < height) {
+ // headerItem.height += (height - contentHeight)
+ // }
+ // currentIndex = count-1
+ // positionViewAtEnd()
+ // }
+
+ delegate: Rectangle {
+ required property var modelData
+ property var theModelData: modelData
+
+ color: "white"
+ radius: 5
+
+ width: listView.width
+ height: gridLayout.implicitHeight
+
+ property var properties: [ qsTr("Timestamp:"), qsTr("SSID:"), qsTr("BSSID:"), qsTr("Reason:") ]
+
+ GridLayout {
+ id: gridLayout
+ anchors.fill: parent
+
+ Repeater {
+ model: properties
+
+ Text {
+ required property int index
+ required property string modelData
+
+ text: modelData
+ Layout.row: index
+ Layout.column: 0
+ }
+ }
+ Repeater {
+ model: properties
+
+ Text {
+ Layout.fillWidth: true
+
+ required property int index
+
+ text: index == 0 ? formatDuration((theModelData[index] > 5.184e+9 ? theModelData[index] / 1000 : theModelData[index]) - rebootTime.value) : theModelData[index]
+ Layout.row: index
+ Layout.column: 1
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/WiFiOnOffSwitch.qml b/WiFiOnOffSwitch.qml
new file mode 100644
index 0000000..1c32b30
--- /dev/null
+++ b/WiFiOnOffSwitch.qml
@@ -0,0 +1,78 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+CheckDelegate {
+ id: checkDelegate
+
+ Layout.fillWidth: true
+
+ Component.onCompleted: {
+ background.color = "white"
+ background.radius = 5
+ }
+
+ ApiKeyValueHelper {
+ id: staEnabled
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "wen"
+ }
+
+ SendMessageHelper {
+ id: staEnabledChanger
+ deviceConnection: mainScreen.deviceConnection
+ }
+
+ checked: staEnabled.value
+ text: staEnabled.value ? qsTr("On") : qsTr("Off")
+
+ onClicked: {
+ if (checked)
+ staEnabledChanger.sendMessage({
+ type: "setValue",
+ key: "wen",
+ value: checked
+ })
+ else {
+ checked = true
+ disableStaDialog.open()
+ }
+ }
+
+ BusyIndicator {
+ visible: staEnabledChanger.pending
+ }
+
+ RequestStatusText {
+ request: staEnabledChanger
+ }
+
+ Dialog {
+ id: disableStaDialog
+
+ x: window.width / 2 - width / 2
+ y: window.height / 2 - height / 2
+ width: Math.min(implicitWidth, window.width - 20)
+
+ title: qsTr("Do you really want to disable Wi-Fi?")
+ standardButtons: Dialog.Ok | Dialog.Cancel
+ focus: true
+ modal: true
+
+ onAccepted: {
+ checkDelegate.checked = false
+ staEnabledChanger.sendMessage({
+ type: "setValue",
+ key: "wen",
+ value: false
+ })
+ }
+ onRejected: checkDelegate.checked = Qt.binding(function() { return staEnabled.value })
+
+ contentItem: Text {
+ text: qsTr("This action could make your device unreachable from your local homenetwork or the cloud!");
+ wrapMode: Text.Wrap
+ }
+ }
+}
diff --git a/WiFiPage.qml b/WiFiPage.qml
new file mode 100644
index 0000000..e3aab35
--- /dev/null
+++ b/WiFiPage.qml
@@ -0,0 +1,64 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+NavigationPage {
+ title: qsTr("Wi-Fi")
+
+ WiFiOnOffSwitch {
+
+ }
+
+ GridLayout {
+ columns: 2
+
+ Label {
+ text: qsTr("Status:")
+ }
+
+ Text {
+ Layout.fillWidth: true
+
+ ApiKeyValueHelper {
+ id: staStatus
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "wst"
+ }
+
+ text: {
+ switch (staStatus.value)
+ {
+ case 0: return 'IDLE_STATUS'
+ case 1: return 'NO_SSID_AVAIL'
+ case 2: return 'SCAN_COMPLETED'
+ case 3: return 'CONNECTED'
+ case 4: return 'CONNECT_FAILED'
+ case 5: return 'CONNECTION_LOST'
+ case 6: return 'DISCONNECTED'
+ case 7: return 'CONNECTING'
+ case 8: return 'DISCONNECTING'
+ case 9: return 'NO_SHIELD'
+ case 10: return 'WAITING_FOR_IP'
+ default: return staStatus.value
+ }
+ }
+ }
+ }
+
+ Button {
+ ApiKeyValueHelper {
+ id: wifiErrorLog
+ deviceConnection: mainScreen.deviceConnection
+ apiKey: "wsl"
+ }
+
+ text: qsTr("(%0) Wi-Fi Errors").arg(wifiErrorLog.value == null ? 0 : wifiErrorLog.value.length)
+ onClicked: stackView.push("WiFiErrorsPage.qml", {wifiErrorLog} )
+ enabled: wifiErrorLog.value != null
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+}
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
new file mode 100644
index 0000000..f28c7cc
--- /dev/null
+++ b/android/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..4a93923
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,84 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.4.0'
+ }
+}
+
+repositories {
+ google()
+ mavenCentral()
+}
+
+apply plugin: qtGradlePluginType
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
+ implementation 'androidx.core:core:1.13.1'
+}
+
+android {
+ /*******************************************************
+ * The following variables:
+ * - androidBuildToolsVersion,
+ * - androidCompileSdkVersion
+ * - qtAndroidDir - holds the path to qt android files
+ * needed to build any Qt application
+ * on Android.
+ * - qtGradlePluginType - whether to build an app or a library
+ *
+ * are defined in gradle.properties file. This file is
+ * updated by QtCreator and androiddeployqt tools.
+ * Changing them manually might break the compilation!
+ *******************************************************/
+
+ namespace androidPackageName
+ compileSdkVersion androidCompileSdkVersion
+ buildToolsVersion androidBuildToolsVersion
+ ndkVersion androidNdkVersion
+
+ // Extract native libraries from the APK
+ packagingOptions.jniLibs.useLegacyPackaging true
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
+ aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
+ res.srcDirs = [qtAndroidDir + '/res', 'res']
+ resources.srcDirs = ['resources']
+ renderscript.srcDirs = ['src']
+ assets.srcDirs = ['assets']
+ jniLibs.srcDirs = ['libs']
+ }
+ }
+
+ tasks.withType(JavaCompile) {
+ options.incremental = true
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ // Do not compress Qt binary resources file
+ aaptOptions {
+ noCompress 'rcc'
+ }
+
+ defaultConfig {
+ resConfig "en"
+ minSdkVersion qtMinSdkVersion
+ targetSdkVersion qtTargetSdkVersion
+ ndk.abiFilters = qtTargetAbiList.split(",")
+ }
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..4fe1674
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2500m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# Enable building projects in parallel
+org.gradle.parallel=true
+
+# Gradle caching allows reusing the build artifacts from a previous
+# build with the same inputs. However, over time, the cache size will
+# grow. Uncomment the following line to enable it.
+#org.gradle.caching=true
+#org.gradle.configuration-cache=true
+
+# Allow AndroidX usage
+android.useAndroidX=true
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7f93135
Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..b82aa23
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/android/gradlew b/android/gradlew
new file mode 100755
index 0000000..c22a517
--- /dev/null
+++ b/android/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/android/gradlew.bat b/android/gradlew.bat
new file mode 100644
index 0000000..3624bce
--- /dev/null
+++ b/android/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/res/drawable-hdpi/icon.png b/android/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..b3edd23
Binary files /dev/null and b/android/res/drawable-hdpi/icon.png differ
diff --git a/android/res/drawable-hdpi/logo.png b/android/res/drawable-hdpi/logo.png
new file mode 100644
index 0000000..d975027
Binary files /dev/null and b/android/res/drawable-hdpi/logo.png differ
diff --git a/android/res/drawable-hdpi/logo_land.png b/android/res/drawable-hdpi/logo_land.png
new file mode 100644
index 0000000..63b5a32
Binary files /dev/null and b/android/res/drawable-hdpi/logo_land.png differ
diff --git a/android/res/drawable-hdpi/logo_port.png b/android/res/drawable-hdpi/logo_port.png
new file mode 100644
index 0000000..d975027
Binary files /dev/null and b/android/res/drawable-hdpi/logo_port.png differ
diff --git a/android/res/drawable-ldpi/icon.png b/android/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000..495d467
Binary files /dev/null and b/android/res/drawable-ldpi/icon.png differ
diff --git a/android/res/drawable-ldpi/logo.png b/android/res/drawable-ldpi/logo.png
new file mode 100644
index 0000000..86d06bc
Binary files /dev/null and b/android/res/drawable-ldpi/logo.png differ
diff --git a/android/res/drawable-ldpi/logo_land.png b/android/res/drawable-ldpi/logo_land.png
new file mode 100644
index 0000000..c17c1ee
Binary files /dev/null and b/android/res/drawable-ldpi/logo_land.png differ
diff --git a/android/res/drawable-ldpi/logo_port.png b/android/res/drawable-ldpi/logo_port.png
new file mode 100644
index 0000000..86d06bc
Binary files /dev/null and b/android/res/drawable-ldpi/logo_port.png differ
diff --git a/android/res/drawable-mdpi/icon.png b/android/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..74fbe19
Binary files /dev/null and b/android/res/drawable-mdpi/icon.png differ
diff --git a/android/res/drawable-mdpi/logo.png b/android/res/drawable-mdpi/logo.png
new file mode 100644
index 0000000..e84e63d
Binary files /dev/null and b/android/res/drawable-mdpi/logo.png differ
diff --git a/android/res/drawable-mdpi/logo_land.png b/android/res/drawable-mdpi/logo_land.png
new file mode 100644
index 0000000..2a0dd25
Binary files /dev/null and b/android/res/drawable-mdpi/logo_land.png differ
diff --git a/android/res/drawable-mdpi/logo_port.png b/android/res/drawable-mdpi/logo_port.png
new file mode 100644
index 0000000..e84e63d
Binary files /dev/null and b/android/res/drawable-mdpi/logo_port.png differ
diff --git a/android/res/drawable-xhdpi/icon.png b/android/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000..08ff127
Binary files /dev/null and b/android/res/drawable-xhdpi/icon.png differ
diff --git a/android/res/drawable-xhdpi/logo.png b/android/res/drawable-xhdpi/logo.png
new file mode 100644
index 0000000..ef7084c
Binary files /dev/null and b/android/res/drawable-xhdpi/logo.png differ
diff --git a/android/res/drawable-xhdpi/logo_land.png b/android/res/drawable-xhdpi/logo_land.png
new file mode 100644
index 0000000..0094994
Binary files /dev/null and b/android/res/drawable-xhdpi/logo_land.png differ
diff --git a/android/res/drawable-xhdpi/logo_port.png b/android/res/drawable-xhdpi/logo_port.png
new file mode 100644
index 0000000..ef7084c
Binary files /dev/null and b/android/res/drawable-xhdpi/logo_port.png differ
diff --git a/android/res/drawable-xxhdpi/icon.png b/android/res/drawable-xxhdpi/icon.png
new file mode 100644
index 0000000..1bde2cc
Binary files /dev/null and b/android/res/drawable-xxhdpi/icon.png differ
diff --git a/android/res/drawable-xxhdpi/logo.png b/android/res/drawable-xxhdpi/logo.png
new file mode 100644
index 0000000..92f68c4
Binary files /dev/null and b/android/res/drawable-xxhdpi/logo.png differ
diff --git a/android/res/drawable-xxhdpi/logo_land.png b/android/res/drawable-xxhdpi/logo_land.png
new file mode 100644
index 0000000..d4575b0
Binary files /dev/null and b/android/res/drawable-xxhdpi/logo_land.png differ
diff --git a/android/res/drawable-xxhdpi/logo_port.png b/android/res/drawable-xxhdpi/logo_port.png
new file mode 100644
index 0000000..92f68c4
Binary files /dev/null and b/android/res/drawable-xxhdpi/logo_port.png differ
diff --git a/android/res/drawable-xxxhdpi/icon.png b/android/res/drawable-xxxhdpi/icon.png
new file mode 100644
index 0000000..d12bb4f
Binary files /dev/null and b/android/res/drawable-xxxhdpi/icon.png differ
diff --git a/android/res/drawable-xxxhdpi/logo.png b/android/res/drawable-xxxhdpi/logo.png
new file mode 100644
index 0000000..022fe20
Binary files /dev/null and b/android/res/drawable-xxxhdpi/logo.png differ
diff --git a/android/res/drawable-xxxhdpi/logo_land.png b/android/res/drawable-xxxhdpi/logo_land.png
new file mode 100644
index 0000000..507fe18
Binary files /dev/null and b/android/res/drawable-xxxhdpi/logo_land.png differ
diff --git a/android/res/drawable-xxxhdpi/logo_port.png b/android/res/drawable-xxxhdpi/logo_port.png
new file mode 100644
index 0000000..022fe20
Binary files /dev/null and b/android/res/drawable-xxxhdpi/logo_port.png differ
diff --git a/android/res/drawable/splashscreen.xml b/android/res/drawable/splashscreen.xml
new file mode 100644
index 0000000..22571a8
--- /dev/null
+++ b/android/res/drawable/splashscreen.xml
@@ -0,0 +1,11 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
diff --git a/android/res/drawable/splashscreen_land.xml b/android/res/drawable/splashscreen_land.xml
new file mode 100644
index 0000000..4111621
--- /dev/null
+++ b/android/res/drawable/splashscreen_land.xml
@@ -0,0 +1,11 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
diff --git a/android/res/drawable/splashscreen_port.xml b/android/res/drawable/splashscreen_port.xml
new file mode 100644
index 0000000..92b71de
--- /dev/null
+++ b/android/res/drawable/splashscreen_port.xml
@@ -0,0 +1,11 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
diff --git a/android/res/values-land/splashscreentheme.xml b/android/res/values-land/splashscreentheme.xml
new file mode 100644
index 0000000..800b923
--- /dev/null
+++ b/android/res/values-land/splashscreentheme.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/android/res/values-port/splashscreentheme.xml b/android/res/values-port/splashscreentheme.xml
new file mode 100644
index 0000000..a20302e
--- /dev/null
+++ b/android/res/values-port/splashscreentheme.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/android/res/values/libs.xml b/android/res/values/libs.xml
new file mode 100644
index 0000000..fe63866
--- /dev/null
+++ b/android/res/values/libs.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/res/values/splashscreentheme.xml b/android/res/values/splashscreentheme.xml
new file mode 100644
index 0000000..53b3673
--- /dev/null
+++ b/android/res/values/splashscreentheme.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/android/res/xml/qtprovider_paths.xml b/android/res/xml/qtprovider_paths.xml
new file mode 100644
index 0000000..ae5b4b6
--- /dev/null
+++ b/android/res/xml/qtprovider_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/android/src/QZeroConfNsdManager.java b/android/src/QZeroConfNsdManager.java
new file mode 120000
index 0000000..337cc26
--- /dev/null
+++ b/android/src/QZeroConfNsdManager.java
@@ -0,0 +1 @@
+../../3rdparty/QtZeroConf/QZeroConfNsdManager.java
\ No newline at end of file
diff --git a/apikeyexistancehelper.cpp b/apikeyexistancehelper.cpp
new file mode 100644
index 0000000..d63e012
--- /dev/null
+++ b/apikeyexistancehelper.cpp
@@ -0,0 +1,53 @@
+#include "apikeyexistancehelper.h"
+
+void ApiKeyExistanceHelper::setDeviceConnection(DeviceConnection *deviceConnection)
+{
+ if (m_deviceConnection == deviceConnection)
+ return;
+
+ if (m_deviceConnection)
+ {
+ disconnect(m_deviceConnection, &DeviceConnection::fullStatusReceived,
+ this, &ApiKeyExistanceHelper::fullStatusReceived);
+ }
+
+ emit deviceConnectionChanged(m_deviceConnection = deviceConnection);
+
+ if (m_deviceConnection)
+ {
+ connect(m_deviceConnection, &DeviceConnection::fullStatusReceived,
+ this, &ApiKeyExistanceHelper::fullStatusReceived);
+
+ fullStatusReceived();
+ }
+ else if (m_exists)
+ emit existsChanged(m_exists = false);
+}
+
+void ApiKeyExistanceHelper::setApiKey(const QString &apiKey)
+{
+ if (m_apiKey == apiKey)
+ return;
+
+ emit apiKeyChanged(m_apiKey = apiKey);
+
+ if (m_deviceConnection)
+ fullStatusReceived();
+ else if (m_exists)
+ emit existsChanged(m_exists = false);
+}
+
+void ApiKeyExistanceHelper::fullStatusReceived()
+{
+ if (!m_deviceConnection)
+ {
+ if (m_exists)
+ emit existsChanged(m_exists = false);
+ return;
+ }
+
+ const auto exists = m_deviceConnection->getFullStatus().contains(m_apiKey);
+
+ if (exists != m_exists)
+ emit existsChanged(m_exists = exists);
+}
diff --git a/apikeyexistancehelper.h b/apikeyexistancehelper.h
new file mode 100644
index 0000000..9fbf6a2
--- /dev/null
+++ b/apikeyexistancehelper.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include
+#include
+
+#include "deviceconnection.h"
+
+class ApiKeyExistanceHelper : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ Q_PROPERTY(DeviceConnection* deviceConnection READ deviceConnection WRITE setDeviceConnection NOTIFY deviceConnectionChanged FINAL)
+ Q_PROPERTY(QString apiKey READ apiKey WRITE setApiKey NOTIFY apiKeyChanged FINAL)
+ Q_PROPERTY(bool exists READ exists NOTIFY existsChanged STORED false FINAL)
+
+public:
+ DeviceConnection *deviceConnection() { return m_deviceConnection; }
+ const DeviceConnection *deviceConnection() const { return m_deviceConnection; }
+ void setDeviceConnection(DeviceConnection *deviceConnection);
+
+ const QString &apiKey() const { return m_apiKey; }
+ void setApiKey(const QString &apiKey);
+
+ bool exists() { return m_exists; }
+
+private slots:
+ void fullStatusReceived();
+
+signals:
+ void deviceConnectionChanged(DeviceConnection *deviceConnection);
+ void apiKeyChanged(const QString &apiKey);
+ void existsChanged(bool exists);
+
+private:
+ DeviceConnection *m_deviceConnection{};
+ QString m_apiKey;
+ bool m_exists{};
+};
diff --git a/apikeyvaluehelper.cpp b/apikeyvaluehelper.cpp
new file mode 100644
index 0000000..32eea05
--- /dev/null
+++ b/apikeyvaluehelper.cpp
@@ -0,0 +1,79 @@
+#include "apikeyvaluehelper.h"
+
+void ApiKeyValueHelper::setDeviceConnection(DeviceConnection *deviceConnection)
+{
+ if (m_deviceConnection == deviceConnection)
+ return;
+
+ if (m_deviceConnection)
+ {
+ disconnect(m_deviceConnection, &DeviceConnection::fullStatusReceived,
+ this, &ApiKeyValueHelper::fullStatusReceived);
+ disconnect(m_deviceConnection, &DeviceConnection::valueChanged,
+ this, &ApiKeyValueHelper::valueChangedSlot);
+ }
+
+ emit deviceConnectionChanged(m_deviceConnection = deviceConnection);
+
+ if (m_deviceConnection)
+ {
+ connect(m_deviceConnection, &DeviceConnection::fullStatusReceived,
+ this, &ApiKeyValueHelper::fullStatusReceived);
+ connect(m_deviceConnection, &DeviceConnection::valueChanged,
+ this, &ApiKeyValueHelper::valueChangedSlot);
+ }
+
+ fullStatusReceived();
+}
+
+void ApiKeyValueHelper::setApiKey(const QString &apiKey)
+{
+ if (m_apiKey == apiKey)
+ return;
+
+ emit apiKeyChanged(m_apiKey = apiKey);
+
+ fullStatusReceived();
+}
+
+void ApiKeyValueHelper::fullStatusReceived()
+{
+ if (!m_deviceConnection)
+ {
+ if (m_value.isValid())
+ emit valueChanged(m_value = {});
+ if (m_exists)
+ emit existsChanged(m_exists = false);
+ return;
+ }
+
+ const auto &fullStatus = m_deviceConnection->getFullStatus();
+ const auto iter = fullStatus.find(m_apiKey);
+
+ QVariant value;
+ bool exists{};
+
+ if (iter != std::cend(fullStatus))
+ {
+ value = *iter;
+ exists = true;
+ }
+
+ if (m_value != value)
+ emit valueChanged(m_value = value);
+
+ if (m_exists != exists)
+ emit existsChanged(m_exists = exists);
+}
+
+void ApiKeyValueHelper::valueChangedSlot(const QString &key, const QVariant &value)
+{
+ if (m_apiKey != key)
+ return;
+
+ if (m_value != value)
+ emit valueChanged(m_value = value);
+
+ if (!m_exists)
+ emit existsChanged(m_exists = true);
+}
diff --git a/apikeyvaluehelper.h b/apikeyvaluehelper.h
new file mode 100644
index 0000000..2c70bff
--- /dev/null
+++ b/apikeyvaluehelper.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include
+#include
+
+#include "deviceconnection.h"
+
+class ApiKeyValueHelper : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ Q_PROPERTY(DeviceConnection* deviceConnection READ deviceConnection WRITE setDeviceConnection NOTIFY deviceConnectionChanged FINAL)
+ Q_PROPERTY(QString apiKey READ apiKey WRITE setApiKey NOTIFY apiKeyChanged FINAL)
+ Q_PROPERTY(QVariant value READ value NOTIFY valueChanged STORED false FINAL)
+ Q_PROPERTY(bool exists READ exists NOTIFY existsChanged STORED false FINAL)
+
+public:
+ DeviceConnection *deviceConnection() { return m_deviceConnection; }
+ const DeviceConnection *deviceConnection() const { return m_deviceConnection; }
+ void setDeviceConnection(DeviceConnection *deviceConnection);
+
+ const QString &apiKey() const { return m_apiKey; }
+ void setApiKey(const QString &apiKey);
+
+ const QVariant &value() { return m_value; }
+ bool exists() const { return m_exists; }
+
+private slots:
+ void fullStatusReceived();
+ void valueChangedSlot(const QString &key, const QVariant &value);
+
+signals:
+ void deviceConnectionChanged(DeviceConnection *deviceConnection);
+ void apiKeyChanged(const QString &apiKey);
+ void valueChanged(const QVariant &variant);
+ void existsChanged(bool exists);
+
+private:
+ DeviceConnection *m_deviceConnection{};
+ QString m_apiKey;
+ QVariant m_value;
+ bool m_exists{};
+};
diff --git a/appsettings.cpp b/appsettings.cpp
new file mode 100644
index 0000000..323acf6
--- /dev/null
+++ b/appsettings.cpp
@@ -0,0 +1,92 @@
+#include "appsettings.h"
+
+#include
+
+std::vector AppSettings::getSavedDevices()
+{
+ std::vector savedDevices;
+
+ int size = beginReadArray("devices");
+ for (int i = 0; i < size; ++i)
+ {
+ setArrayIndex(i);
+ savedDevices.emplace_back(SavedDevice {
+ .serial = value("serial").toString(),
+ .manufacturer = value("manufacturer").toString(),
+ .deviceType = value("deviceType").toString(),
+ .friendlyName = value("friendlyName").toString(),
+ .password = value("password").toString(),
+ });
+ }
+ endArray();
+
+ return savedDevices;
+}
+
+void AppSettings::saveSavedDevices(const std::vector &savedDevices)
+{
+ beginWriteArray("devices");
+ for (qsizetype i = 0; i < savedDevices.size(); ++i) {
+ setArrayIndex(i);
+ setValue("serial", savedDevices.at(i).serial);
+ setValue("manufacturer", savedDevices.at(i).manufacturer);
+ setValue("deviceType", savedDevices.at(i).deviceType);
+ setValue("friendlyName", savedDevices.at(i).friendlyName);
+ setValue("password", savedDevices.at(i).password);
+ }
+ endArray();
+}
+
+void AppSettings::refreshSavedDevice(SavedDevice &&savedDevice)
+{
+ auto savedDevices = getSavedDevices();
+
+ if (auto iter = std::find_if(std::begin(savedDevices), std::end(savedDevices),
+ [&serial=savedDevice.serial](const SavedDevice &savedDevice){ return savedDevice.serial == serial; });
+ iter != std::end(savedDevices))
+ {
+ if (savedDevice.password.isEmpty())
+ savedDevice.password = std::move(iter->password);
+ savedDevices.erase(iter);
+ }
+
+ savedDevices.emplace(std::begin(savedDevices), std::move(savedDevice));
+
+ saveSavedDevices(savedDevices);
+}
+
+void AppSettings::removeSavedDevice(const QString &serial)
+{
+ auto savedDevices = getSavedDevices();
+
+ if (auto iter = std::find_if(std::begin(savedDevices), std::end(savedDevices),
+ [&serial](const SavedDevice &savedDevice){ return savedDevice.serial == serial; });
+ iter != std::end(savedDevices))
+ {
+ savedDevices.erase(iter);
+ }
+
+ saveSavedDevices(savedDevices);
+}
+
+int AppSettings::numberOfAppInstances() const
+{
+ if (!m_numberOfAppInstances)
+ {
+ bool ok{};
+ int numberOfAppInstances = value("numberOfAppInstances", 1).toInt(&ok);
+ if (!ok)
+ numberOfAppInstances = 1;
+ m_numberOfAppInstances = numberOfAppInstances;
+ return numberOfAppInstances;
+ }
+
+ return *m_numberOfAppInstances;
+}
+
+void AppSettings::setNumberOfAppInstances(int numberOfAppInstances)
+{
+ setValue("numberOfAppInstances", numberOfAppInstances);
+ m_numberOfAppInstances = numberOfAppInstances;
+ emit numberOfAppInstancesChanged(numberOfAppInstances);
+}
diff --git a/appsettings.h b/appsettings.h
new file mode 100644
index 0000000..e7e22d8
--- /dev/null
+++ b/appsettings.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+struct SavedDevice
+{
+ QString serial;
+ QString manufacturer;
+ QString deviceType;
+ QString friendlyName;
+ QString password;
+};
+
+class AppSettings : public QSettings
+{
+ Q_OBJECT
+ QML_ELEMENT
+ Q_PROPERTY(int numberOfAppInstances READ numberOfAppInstances WRITE setNumberOfAppInstances NOTIFY numberOfAppInstancesChanged FINAL)
+
+public:
+ std::vector getSavedDevices();
+ void saveSavedDevices(const std::vector &savedDevices);
+ void refreshSavedDevice(SavedDevice &&savedDevice);
+ void removeSavedDevice(const QString &serial);
+
+ int numberOfAppInstances() const;
+ void setNumberOfAppInstances(int numberOfAppInstances);
+
+signals:
+ void numberOfAppInstancesChanged(int numberOfAppInstances);
+
+private:
+ mutable std::optional m_numberOfAppInstances;
+};
diff --git a/deviceconnection.cpp b/deviceconnection.cpp
new file mode 100644
index 0000000..f8c0520
--- /dev/null
+++ b/deviceconnection.cpp
@@ -0,0 +1,543 @@
+#include "deviceconnection.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "msgpack.h"
+
+namespace {
+template
+auto enumToString(const QEnum value)
+{
+ return QMetaEnum::fromType().valueToKey(std::to_underlying(value));
+}
+}
+
+DeviceConnection::DeviceConnection(QObject *parent) :
+ QObject{parent}
+{
+ connect(&m_websocket, &QWebSocket::connected,
+ this, &DeviceConnection::connected);
+ connect(&m_websocket, &QWebSocket::disconnected,
+ this, &DeviceConnection::disconnected);
+ connect(&m_websocket, &QWebSocket::stateChanged,
+ this, &DeviceConnection::stateChanged);
+ connect(&m_websocket, &QWebSocket::textMessageReceived,
+ this, &DeviceConnection::textMessageReceived);
+ connect(&m_websocket, &QWebSocket::binaryMessageReceived,
+ this, &DeviceConnection::binaryMessageReceived);
+ connect(&m_websocket, &QWebSocket::errorOccurred,
+ this, &DeviceConnection::errorOccurred);
+
+ connect(&m_websocket, &QWebSocket::peerVerifyError,
+ this, &DeviceConnection::peerVerifyError);
+ connect(&m_websocket, &QWebSocket::sslErrors,
+ this, &DeviceConnection::sslErrors);
+ // connect(&m_websocket, &QWebSocket::preSharedKeyAuthenticationRequired,
+ // this, &DeviceConnection::preSharedKeyAuthenticationRequired);
+ connect(&m_websocket, &QWebSocket::alertSent,
+ this, &DeviceConnection::alertSent);
+ connect(&m_websocket, &QWebSocket::alertReceived,
+ this, &DeviceConnection::alertReceived);
+ connect(&m_websocket, &QWebSocket::handshakeInterruptedOnError,
+ this, &DeviceConnection::handshakeInterruptedOnError);
+}
+
+void DeviceConnection::setUrl(const QString &url)
+{
+ if (m_url == url)
+ return;
+
+ emit urlChanged(m_url = url);
+
+ emit logMessage(tr("Connecting to %0").arg(m_url));
+ m_websocket.open(QUrl{m_url});
+}
+
+void DeviceConnection::setPassword(const QString &password)
+{
+ if (m_password == password)
+ return;
+
+ emit passwordChanged(m_password = password);
+}
+
+void DeviceConnection::messageReceived(const QVariant &variant)
+{
+ if (variant.type() != QMetaType::QVariantMap)
+ {
+ emit logMessage(tr("Received something that is not a json object!"));
+ return;
+ }
+
+ const auto &map = variant.toMap();
+ auto typeIter = map.find("type");
+ if (typeIter == std::cend(map))
+ {
+ emit logMessage(tr("Received something that does not contain a type!"));
+ return;
+ }
+
+ const auto &typeVariant = *typeIter;
+ if (typeVariant.type() != QMetaType::QString)
+ {
+ emit logMessage(tr("Received something with a non-string type!"));
+ return;
+ }
+
+ const auto &type = typeVariant.toString();
+ bool omitLog{};
+
+ if (type == "hello")
+ {
+ {
+ auto iter = map.find("serial");
+ if (iter == std::cend(map))
+ {
+ emit logMessage(tr("Received hello without serial!"));
+ return;
+ }
+
+ const auto &serialVariant = *iter;
+ if (serialVariant.type() != QMetaType::QString)
+ {
+ emit logMessage(tr("Received hello with a non-string serial!"));
+ return;
+ }
+
+ m_serial = serialVariant.toString();
+ }
+
+ if (auto iter = map.find("secured"); iter != std::cend(map))
+ {
+ const auto &securedVariant = *iter;
+ if (securedVariant.type() != QMetaType::Bool)
+ {
+ emit logMessage(tr("Received hello with a non-bool secured!"));
+ return;
+ }
+
+ m_secured = securedVariant.toBool();
+ }
+ else
+ m_secured = false;
+
+ {
+ auto manufacturerIter = map.find("manufacturer");
+ if (manufacturerIter == std::cend(map))
+ {
+ emit logMessage(tr("Received hello without manufacturer!"));
+ return;
+ }
+
+ const auto &manufacturerVariant = *manufacturerIter;
+ if (manufacturerVariant.type() != QMetaType::QString)
+ {
+ emit logMessage(tr("Received hello with a non-string manufacturer!"));
+ return;
+ }
+
+ m_manufacturer = manufacturerVariant.toString();
+ }
+
+ {
+ auto devicetypeIter = map.find("devicetype");
+ if (devicetypeIter == std::cend(map))
+ {
+ emit logMessage(tr("Received hello without devicetype!"));
+ return;
+ }
+
+ const auto &deviceTypeVariant = *devicetypeIter;
+ if (deviceTypeVariant.type() != QMetaType::QString)
+ {
+ emit logMessage(tr("Received hello with a non-string devicetype!"));
+ return;
+ }
+
+ m_deviceType = deviceTypeVariant.toString();
+ }
+
+ {
+ auto friendlyNameIter = map.find("friendly_name");
+ if (friendlyNameIter == std::cend(map))
+ {
+ emit logMessage(tr("Received hello without friendly_name!"));
+ return;
+ }
+
+ const auto &friendlyNameVariant = *friendlyNameIter;
+ if (friendlyNameVariant.type() != QMetaType::QString)
+ {
+ emit logMessage(tr("Received hello with a non-string friendly_name!"));
+ return;
+ }
+
+ m_friendlyName = friendlyNameVariant.toString();
+ }
+
+ if (m_settings)
+ m_settings->refreshSavedDevice(SavedDevice {
+ .serial = m_serial,
+ .manufacturer = m_manufacturer,
+ .deviceType = m_deviceType,
+ .friendlyName = m_friendlyName
+ });
+ }
+ else if (type == "authRequired")
+ {
+ {
+ auto iter = map.find("token1");
+ if (iter == std::cend(map))
+ {
+ emit logMessage(tr("Received authRequired without token1!"));
+ return;
+ }
+
+ const auto &token1Variant = *iter;
+ if (token1Variant.type() != QMetaType::QString)
+ {
+ emit logMessage(tr("Received authRequired with a non-string token1!"));
+ return;
+ }
+
+ m_token1 = token1Variant.toString();
+ }
+
+ {
+ auto iter = map.find("token2");
+ if (iter == std::cend(map))
+ {
+ emit logMessage(tr("Received authRequired without token2!"));
+ return;
+ }
+
+ const auto &token2Variant = *iter;
+ if (token2Variant.type() != QMetaType::QString)
+ {
+ emit logMessage(tr("Received authRequired with a non-string token2!"));
+ return;
+ }
+
+ m_token2 = token2Variant.toString();
+ }
+
+ if (m_password.isEmpty())
+ emit authRequired();
+ else
+ sendAuthInternal();
+ }
+ else if (type == "fullStatus")
+ {
+ omitLog = true;
+
+ bool partial{};
+
+ if (auto iter = map.find("partial"); iter != std::cend(map))
+ {
+ const auto &partialVariant = *iter;
+ if (partialVariant.type() != QMetaType::Bool)
+ {
+ emit logMessage(tr("Received fullStatus with a non-bool partial!"));
+ return;
+ }
+
+ partial = partialVariant.toBool();
+ }
+
+ auto iter = map.find("status");
+ if (iter == std::cend(map))
+ {
+ emit logMessage(tr("Received fullStatus without a status!"));
+ return;
+ }
+
+ const auto &statusVariant = *iter;
+ if (statusVariant.type() != QMetaType::QVariantMap)
+ {
+ emit logMessage(tr("Received fullStatus with a non-object status!"));
+ return;
+ }
+
+ const auto &status = statusVariant.toMap();
+ for (auto iter = std::cbegin(status); iter != std::cend(status); iter++)
+ m_fullStatus[iter.key()] = iter.value();
+
+ emit logMessage(partial ? tr("Received partial fullStatus") : tr("Received last fullStatus"));
+
+ if (!partial)
+ emit fullStatusReceived();
+ }
+ else if (type == "deltaStatus")
+ {
+ omitLog = true;
+
+ auto iter = map.find("status");
+ if (iter == std::cend(map))
+ {
+ emit logMessage(tr("Received deltaStatus without a status!"));
+ return;
+ }
+
+ const auto &statusVariant = *iter;
+ if (statusVariant.type() != QMetaType::QVariantMap)
+ {
+ emit logMessage(tr("Received deltaStatus with a non-object status!"));
+ return;
+ }
+
+ const auto &status = statusVariant.toMap();
+ for (auto iter = std::cbegin(status); iter != std::cend(status); iter++)
+ {
+ m_fullStatus[iter.key()] = iter.value();
+ emit valueChanged(iter.key(), iter.value());
+ if (iter.key() == "fna")
+ {
+ const auto &friendlyNameVariant = iter.value();
+ if (friendlyNameVariant.type() == QMetaType::QString)
+ {
+ const auto &friendlyName = friendlyNameVariant.toString();
+ if (m_friendlyName != friendlyName)
+ {
+ m_friendlyName = friendlyName;
+ if (m_settings)
+ m_settings->refreshSavedDevice(SavedDevice {
+ .serial = m_serial,
+ .manufacturer = m_manufacturer,
+ .deviceType = m_deviceType,
+ .friendlyName = m_friendlyName,
+ .password = m_password
+ });
+ }
+ }
+ }
+ }
+ }
+
+ if (!omitLog)
+ {
+ emit logMessage(tr("Received message type %0 %1").arg(type).arg(QJsonDocument::fromVariant(variant).toJson()));
+ }
+
+ if (auto iter = map.find("requestId"); iter != std::cend(map))
+ {
+ const auto &requestIdVariant = *iter;
+ if (requestIdVariant.type() != QMetaType::QString)
+ {
+ emit logMessage(tr("Received message with a non-string requestId!"));
+ return;
+ }
+
+ const auto &requestId = requestIdVariant.toString();
+
+ emit responseReceived(requestId, map);
+
+ if (const auto callbackIter = m_callbacks.find(requestId); callbackIter != std::end(m_callbacks))
+ {
+ if (*callbackIter)
+ (*callbackIter)(type, map);
+ m_callbacks.erase(callbackIter);
+ }
+ }
+}
+
+void DeviceConnection::sendMessage(QVariantMap variant, callback_t &&callback)
+{
+ if (callback)
+ {
+ auto requestId = generateRequestId();
+ variant["requestId"] = requestId;
+ m_callbacks[requestId] = std::move(callback);
+ }
+
+ auto message = QJsonDocument::fromVariant(variant).toJson(QJsonDocument::Compact);
+
+ if (const auto iter = variant.find("type"); m_secured && (iter == std::cend(variant) || *iter != "auth"))
+ {
+ if (m_password.isEmpty())
+ {
+ qCritical() << "would have needed secure wrapper but we dont have a password!";
+ }
+ else
+ {
+ QString requestId;
+ if (const auto iter = variant.find("requestId"); iter != std::cend(variant))
+ requestId = iter->toString();
+
+ variant = QVariantMap {
+ { "type", "securedMsg" },
+ { "data", message },
+ { "hmac", QMessageAuthenticationCode::hash(message, m_password.toUtf8(), QCryptographicHash::Sha256).toHex() }
+ };
+ if (!requestId.isEmpty())
+ variant["requestId"] = requestId;
+ message = QJsonDocument::fromVariant(variant).toJson(QJsonDocument::Compact);
+ }
+ }
+
+ //TODO verify that we actually sent the message
+ m_websocket.sendTextMessage(message);
+ emit logMessage(tr("Sending %0").arg(message));
+}
+
+QString DeviceConnection::generateRequestId()
+{
+ return QString("request-%0").arg(m_requestId++);
+}
+
+void DeviceConnection::sendMessage(const QVariantMap &variant)
+{
+ sendMessage(variant, {});
+}
+
+void DeviceConnection::sendAuth(const QString &password)
+{
+ const QString data = password;
+ const QString salt = m_serial;
+ constexpr auto iterations = 100000;
+ constexpr auto keylen = 64;
+
+ m_password = QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Sha512, data.toUtf8(), salt.toUtf8(), iterations, keylen).toBase64();
+
+ if (m_password.size() > 32)
+ m_password.resize(32);
+
+ if (m_websocket.state() != QAbstractSocket::ConnectedState)
+ return;
+
+ sendAuthInternal();
+}
+
+void DeviceConnection::connected()
+{
+ emit logMessage(tr("Connected!"));
+}
+
+void DeviceConnection::disconnected()
+{
+ emit logMessage(tr("Disconnected!"));
+ emit logMessage(tr("Reconnecting to %0").arg(m_url));
+ m_websocket.open(QUrl{m_url});
+}
+
+void DeviceConnection::stateChanged(QAbstractSocket::SocketState state)
+{
+ emit logMessage(tr("state changed: %0").arg(enumToString(state)));
+}
+
+void DeviceConnection::textMessageReceived(const QString &message)
+{
+ QJsonParseError error;
+ QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error);
+ if (error.error != QJsonParseError::NoError)
+ {
+ emit logMessage(tr("could not parse received json: %0 at offset %1").arg(error.errorString()).arg(error.offset));
+ return;
+ }
+
+ messageReceived(document.toVariant());
+}
+
+void DeviceConnection::binaryMessageReceived(QByteArray message)
+{
+ const auto type = message.at(0);
+ switch (type)
+ {
+ case 0: // Type msgpack
+ message.removeFirst();
+ messageReceived(MsgPack::unpack(message));
+ break;
+ default:
+ emit logMessage(tr("unknown binary message type %0 received").arg(int(type)));
+ break;
+ }
+}
+
+void DeviceConnection::errorOccurred(QAbstractSocket::SocketError error)
+{
+ emit logMessage(tr("error occured: %0").arg(enumToString(error)));
+}
+
+void DeviceConnection::peerVerifyError(const QSslError &error)
+{
+ emit logMessage(tr("ssl peer verify error"));
+}
+
+void DeviceConnection::sslErrors(const QList &errors)
+{
+ emit logMessage(tr("ssl errors"));
+}
+
+// void DeviceConnection::preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator)
+// {
+
+// }
+
+void DeviceConnection::alertSent(QSsl::AlertLevel level, QSsl::AlertType type, const QString &description)
+{
+ emit logMessage(tr("ssl alert sent level=%0 type=%1 description=%2").arg(enumToString(level)).arg(enumToString(type)).arg(description));
+}
+
+void DeviceConnection::alertReceived(QSsl::AlertLevel level, QSsl::AlertType type, const QString &description)
+{
+ emit logMessage(tr("ssl alert received level=%0 type=%1 description=%2").arg(enumToString(level)).arg(enumToString(type)).arg(description));
+}
+
+void DeviceConnection::handshakeInterruptedOnError(const QSslError &error)
+{
+ emit logMessage(tr("ssl handshake interrupted on error"));
+}
+
+void DeviceConnection::sendAuthInternal()
+{
+ auto hash2 = QCryptographicHash::hash(m_token1.toUtf8() + m_password.toUtf8(), QCryptographicHash::Sha256).toHex().toLower();
+
+ const QString token3 = "hatschi";
+
+ auto hash3 = QCryptographicHash::hash(token3.toUtf8() + m_token2.toUtf8() + hash2, QCryptographicHash::Sha256).toHex().toLower();
+
+ sendMessage({
+ { "type", "auth" },
+ { "token3", token3 },
+ { "hash", hash3 },
+ { "requestId", "auth-request" }
+ }, [this](const QString &type, const QVariantMap &message){
+ authResponse(type, message);
+ });
+}
+
+void DeviceConnection::authResponse(const QString &type, const QVariantMap &message)
+{
+ if (type == "authError")
+ {
+ m_password.clear();
+ emit authError(message.value("message").toString());
+ }
+ else if (type == "authImpossible")
+ {
+ emit authImpossible();
+ }
+ else if (type == "authSuccess")
+ {
+ if (m_settings)
+ m_settings->refreshSavedDevice(SavedDevice {
+ .serial = m_serial,
+ .manufacturer = m_manufacturer,
+ .deviceType = m_deviceType,
+ .friendlyName = m_friendlyName,
+ .password = m_password
+ });
+
+ emit authSuccess();
+ }
+ else
+ {
+ emit authError(tr("unknown response type %0").arg(type));
+ }
+}
diff --git a/deviceconnection.h b/deviceconnection.h
new file mode 100644
index 0000000..e6c7d3c
--- /dev/null
+++ b/deviceconnection.h
@@ -0,0 +1,105 @@
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include
+
+#include "appsettings.h"
+
+class DeviceConnection : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ Q_PROPERTY(AppSettings* settings READ settings WRITE setSettings NOTIFY settingsChanged FINAL)
+ Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged FINAL)
+ Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged FINAL)
+
+ using callback_t = std::function;
+
+public:
+ explicit DeviceConnection(QObject *parent = nullptr);
+
+ AppSettings *settings() { return m_settings; }
+ const AppSettings *settings() const { return m_settings; }
+ void setSettings(AppSettings *settings) { if (m_settings == settings) return; emit settingsChanged(m_settings = settings); }
+
+ QString url() const { return m_url; }
+ void setUrl(const QString &url);
+
+ QString password() const { return m_password; }
+ void setPassword(const QString &password);
+
+ void messageReceived(const QVariant &variant);
+
+ void sendMessage(QVariantMap variant, callback_t &&callback);
+
+ const QVariantMap &getFullStatus() { return m_fullStatus; }
+
+ QString generateRequestId();
+
+public slots:
+ void sendMessage(const QVariantMap &variant);
+ void sendAuth(const QString &password);
+
+signals:
+ void settingsChanged(AppSettings *settings);
+ void urlChanged(const QString &url);
+ void passwordChanged(const QString &password);
+
+ void logMessage(const QString &message);
+
+ void responseReceived(const QString &requestId, const QVariantMap &message);
+
+ void authRequired();
+ void authError(const QString &message);
+ void authImpossible();
+ void authSuccess();
+
+ void fullStatusReceived();
+ void valueChanged(const QString &key, const QVariant &value);
+
+private slots:
+ void connected();
+ void disconnected();
+ void stateChanged(QAbstractSocket::SocketState state);
+ void textMessageReceived(const QString &message);
+ void binaryMessageReceived(QByteArray message);
+ void errorOccurred(QAbstractSocket::SocketError error);
+
+ void peerVerifyError(const QSslError &error);
+ void sslErrors(const QList &errors);
+ // void preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator);
+ void alertSent(QSsl::AlertLevel level, QSsl::AlertType type, const QString &description);
+ void alertReceived(QSsl::AlertLevel level, QSsl::AlertType type, const QString &description);
+ void handshakeInterruptedOnError(const QSslError &error);
+
+private:
+ void sendAuthInternal();
+ void authResponse(const QString &type, const QVariantMap &message);
+
+ AppSettings *m_settings{};
+
+ QString m_url;
+
+ QWebSocket m_websocket;
+
+ int m_requestId{};
+
+ QString m_serial;
+ bool m_secured{};
+ QString m_manufacturer;
+ QString m_deviceType;
+ QString m_friendlyName;
+
+ QString m_token1;
+ QString m_token2;
+
+ QString m_password;
+
+ QHash m_callbacks;
+
+ QVariantMap m_fullStatus;
+};
diff --git a/devicesmodel.cpp b/devicesmodel.cpp
new file mode 100644
index 0000000..7222bf9
--- /dev/null
+++ b/devicesmodel.cpp
@@ -0,0 +1,292 @@
+#include "devicesmodel.h"
+
+enum {
+ SerialRole = Qt::UserRole,
+ ManufacturerRole,
+ DeviceTypeRole,
+ FriendlyNameRole,
+ PasswordRole,
+ SavedRole,
+ HostNameRole,
+ IpRole
+};
+
+DevicesModel::DevicesModel(QObject *parent) :
+ QAbstractListModel{parent}
+{
+ connect(&m_zeroConf, &QZeroConf::error, this, &DevicesModel::error);
+ connect(&m_zeroConf, &QZeroConf::serviceAdded, this, &DevicesModel::serviceAdded);
+ connect(&m_zeroConf, &QZeroConf::serviceUpdated, this, &DevicesModel::serviceUpdated);
+ connect(&m_zeroConf, &QZeroConf::serviceRemoved, this, &DevicesModel::serviceRemoved);
+}
+
+void DevicesModel::start()
+{
+ qDebug() << "start() called";
+ m_zeroConf.startBrowser("_http._tcp");
+}
+
+void DevicesModel::setSettings(AppSettings *settings)
+{
+ if (m_settings == settings)
+ return;
+
+ m_settings = settings;
+
+ beginResetModel();
+
+ if (m_settings)
+ for (auto &device : m_settings->getSavedDevices())
+ m_foundDevices.emplace_back(FoundDevice{std::move(device), true});
+
+ endResetModel();
+}
+
+int DevicesModel::rowCount(const QModelIndex &parent) const
+{
+ return m_foundDevices.size();
+}
+
+QVariant DevicesModel::data(const QModelIndex &index, int role) const
+{
+ if (index.row() < 0 || index.row() >= m_foundDevices.size())
+ {
+ qWarning() << "invalid row" << index.row();
+ return {};
+ }
+
+ const auto &foundDevice = m_foundDevices.at(index.row());
+
+ switch (role)
+ {
+ case Qt::DisplayRole: return tr("Device %0").arg(foundDevice.serial);
+ case SerialRole: return foundDevice.serial;
+ case ManufacturerRole: return foundDevice.manufacturer;
+ case DeviceTypeRole: return foundDevice.deviceType;
+ case FriendlyNameRole: return foundDevice.friendlyName;
+ case PasswordRole: return foundDevice.password;
+ case SavedRole: return foundDevice.saved;
+ case HostNameRole: return foundDevice.hostName;
+ case IpRole: return foundDevice.ip.toString();
+ }
+
+ return {};
+}
+
+QMap DevicesModel::itemData(const QModelIndex &index) const
+{
+ if (index.row() < 0 || index.row() >= m_foundDevices.size())
+ {
+ qWarning() << "invalid row" << index.row();
+ return {};
+ }
+
+ const auto &foundDevice = m_foundDevices.at(index.row());
+
+ return {
+ { Qt::DisplayRole, tr("Device %0").arg(foundDevice.serial) },
+ { SerialRole,foundDevice.serial },
+ { ManufacturerRole, foundDevice.manufacturer },
+ { DeviceTypeRole, foundDevice.deviceType },
+ { FriendlyNameRole, foundDevice.friendlyName },
+ { PasswordRole, foundDevice.password },
+ { SavedRole, foundDevice.saved },
+ { HostNameRole, foundDevice.hostName },
+ { IpRole, foundDevice.ip.toString() }
+ };
+}
+
+QHash DevicesModel::roleNames() const
+{
+ return {
+ { Qt::DisplayRole, "name" },
+ { SerialRole, "serial" },
+ { ManufacturerRole, "manufacturer" },
+ { DeviceTypeRole, "deviceType" },
+ { FriendlyNameRole, "friendlyName" },
+ { PasswordRole, "password" },
+ { SavedRole, "saved" },
+ { HostNameRole, "hostName" },
+ { IpRole, "ip" }
+ };
+}
+
+bool DevicesModel::removeRows(int row, int count, const QModelIndex &parent)
+{
+ if (row < 0 || row >= m_foundDevices.size())
+ {
+ qWarning() << "invalid row" << row;
+ return false;
+ }
+
+ if (count != 1)
+ {
+ qWarning() << "only count=1 is supported!" << count;
+ return false;
+ }
+
+ auto iter = std::next(std::begin(m_foundDevices), row);
+
+ if (!iter->saved)
+ {
+ qWarning() << "row is not saved" << row << iter->serial;
+ return false;
+ }
+
+ if (m_settings)
+ m_settings->removeSavedDevice(iter->serial);
+
+ if (iter->ip.isNull())
+ {
+ beginRemoveRows({}, row, row);
+ m_foundDevices.erase(iter);
+ endRemoveRows();
+ }
+ else
+ {
+ FoundDevice device = std::move(*iter);
+ device.saved = false;
+ device.password.clear();
+
+ beginRemoveRows({}, row, row);
+ m_foundDevices.erase(iter);
+ endRemoveRows();
+
+ beginInsertRows({}, m_foundDevices.size(), m_foundDevices.size());
+ m_foundDevices.emplace_back(std::move(device));
+ endInsertRows();
+ }
+
+ return true;
+}
+
+void DevicesModel::error(QZeroConf::error_t error)
+{
+ qDebug() << "error()" << error;
+}
+
+void DevicesModel::serviceAdded(QZeroConfService service)
+{
+ qDebug() << "serviceAdded()" << service->host();
+
+ auto txt = service->txt();
+
+ auto serialIter = txt.find("serial");
+ if (serialIter == txt.end())
+ {
+ qWarning() << "serial missing" << txt;
+ return;
+ }
+ auto serial = *serialIter;
+
+ auto manufacturerIter = txt.find("manufacturer");
+ if (manufacturerIter == txt.end())
+ {
+ qWarning() << "manufacturer missing" << txt;
+ return;
+ }
+ auto manufacturer = *manufacturerIter;
+
+ auto deviceTypeIter = txt.find("devicetype");
+ if (deviceTypeIter == txt.end())
+ {
+ qWarning() << "devicetype missing" << txt;
+ return;
+ }
+ auto deviceType = *deviceTypeIter;
+
+ auto friendlyNameIter = txt.find("friendly_name");
+ if (friendlyNameIter == txt.end())
+ {
+ qWarning() << "friendly_name missing" << txt;
+ return;
+ }
+ auto friendlyName = *friendlyNameIter;
+
+ const auto iter = std::find_if(std::begin(m_foundDevices), std::end(m_foundDevices), [&serial](const FoundDevice &foundDevice){
+ return foundDevice.serial == serial;
+ });
+
+ if (iter == std::end(m_foundDevices))
+ {
+ beginInsertRows({}, m_foundDevices.size(), m_foundDevices.size());
+ m_foundDevices.emplace_back(FoundDevice {
+ /*.serial{ */ std::move(serial) /*}*/,
+ /*.manufacturer{ */ std::move(manufacturer) /*}*/,
+ /*.deviceType{ */ std::move(deviceType) /*}*/,
+ /*.friendlyName{ */ std::move(friendlyName) /*}*/,
+ /*.password{ */ {} /*}*/,
+ /*.saved{ */ false /*}*/,
+ /*.hostName{ */ service->host() /*}*/,
+ /*.ip{ */ service->ip() /*}*/
+ });
+ endInsertRows();
+ }
+ else
+ {
+ iter->manufacturer = std::move(manufacturer);
+ iter->deviceType = std::move(deviceType);
+ iter->friendlyName = std::move(friendlyName);
+ iter->hostName = service->host();
+ iter->ip = service->ip();
+
+ const auto index = createIndex(std::distance(std::begin(m_foundDevices), iter), 0);
+ emit dataChanged(index, index, {
+ ManufacturerRole,
+ DeviceTypeRole,
+ FriendlyNameRole,
+ HostNameRole,
+ IpRole
+ });
+ }
+}
+
+void DevicesModel::serviceUpdated(QZeroConfService service)
+{
+ qDebug() << "serviceUpdated()" << service->host();
+
+ // TODO
+}
+
+void DevicesModel::serviceRemoved(QZeroConfService service)
+{
+ qDebug() << "serviceRemoved()" << service->host();
+
+ const auto &txt = service->txt();
+
+ auto serialIter = txt.find("serial");
+ if (serialIter == txt.constEnd())
+ {
+ qWarning() << "serial missing" << txt;
+ return;
+ }
+ auto serial = *serialIter;
+
+ const auto iter = std::find_if(std::begin(m_foundDevices), std::end(m_foundDevices), [&serial](const FoundDevice &foundDevice){
+ return foundDevice.serial == serial;
+ });
+
+ if (iter == std::end(m_foundDevices))
+ {
+ qWarning() << "serial not found!" << serial;
+ return;
+ }
+
+ if (!iter->saved)
+ {
+ const auto row = std::distance(std::begin(m_foundDevices), iter);
+ beginRemoveRows({}, row, row);
+ m_foundDevices.erase(iter);
+ endRemoveRows();
+ }
+ else
+ {
+ iter->hostName.clear();
+ iter->ip = {};
+ const auto index = createIndex(std::distance(std::begin(m_foundDevices), iter), 0);
+ emit dataChanged(index, index, {
+ HostNameRole,
+ IpRole
+ });
+ }
+}
diff --git a/devicesmodel.h b/devicesmodel.h
new file mode 100644
index 0000000..2421dad
--- /dev/null
+++ b/devicesmodel.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+#include
+
+#include "appsettings.h"
+
+class DevicesModel : public QAbstractListModel
+{
+ Q_OBJECT
+ QML_ELEMENT
+ Q_PROPERTY(AppSettings* settings READ settings WRITE setSettings NOTIFY settingsChanged FINAL)
+
+public:
+ explicit DevicesModel(QObject *parent = nullptr);
+ Q_INVOKABLE void start();
+
+ AppSettings *settings() { return m_settings; }
+ const AppSettings *settings() const { return m_settings; }
+ void setSettings(AppSettings *settings);
+
+ int rowCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QMap itemData(const QModelIndex &index) const override;
+ QHash roleNames() const override;
+
+ bool removeRows(int row, int count, const QModelIndex &parent) override;
+
+signals:
+ void settingsChanged(AppSettings *settings);
+
+private slots:
+ void error(QZeroConf::error_t error);
+ void serviceAdded(QZeroConfService service);
+ void serviceUpdated(QZeroConfService service);
+ void serviceRemoved(QZeroConfService service);
+
+private:
+ AppSettings *m_settings{};
+
+ struct FoundDevice : public SavedDevice
+ {
+ bool saved{};
+ QString hostName;
+ QHostAddress ip;
+ };
+
+ std::vector m_foundDevices;
+
+ QZeroConf m_zeroConf;
+};
diff --git a/i18n/qml_de.ts b/i18n/qml_de.ts
new file mode 100644
index 0000000..db0ad06
--- /dev/null
+++ b/i18n/qml_de.ts
@@ -0,0 +1,1707 @@
+
+
+
+
+ AboutPage
+
+
+
+ About
+ Über
+
+
+
+
+ Firmware
+
+
+
+
+
+ Hardware information
+ Hardwareinformationen
+
+
+
+
+ Licenses
+ Lizenzen
+
+
+
+ AccessPage
+
+
+
+ Access
+ Zugangskontrolle
+
+
+
+ AddDeviceScreen
+
+
+
+ Setup or add device
+ Gerät hinzufügen oder einrichten
+
+
+
+
+ Add via local connection or AP:
+ Über lokale Verbindung oder AP einrichten:
+
+
+
+
+ Local url
+ Lokale url
+
+
+
+
+
+
+ Connect
+ Verbinden
+
+
+
+
+ Add via cloud:
+ Über Cloud einrichten:
+
+
+
+
+ Cloud Serial
+ Cloud Seriennr.
+
+
+ Back
+ Zurück
+
+
+
+ ApiSettingsPage
+
+
+
+ API Settings
+ API Einstellungen
+
+
+ Enable cloud connection
+ Cloud Verbindung aktivieren
+
+
+ Features like flexible energy tariffs, time sync and app connection are unavilable when "Enable cloud connection" is disabled
+ Wenn "Cloud Verbindung aktivieren" deaktiviert ist, sind Funktionen wie flexible Energietarife, Zeitsynchronisierung und App-Verbindung über das Internet nicht möglich.
+
+
+
+
+ Allow access to local HTTP-Api v2
+ Zugriff auf die lokale HTTP-API v2 zulassen
+
+
+
+
+
+
+
+
+
+
+ API documentation
+ API-Dokumentation
+
+
+
+
+ Generate token
+
+
+
+
+
+ OK! token: %0
+
+
+
+
+
+ Clear API token
+
+
+
+
+
+ Enable cloud API
+ Cloud HTTP API aktivieren
+
+
+
+
+
+
+ API key: %0
+
+
+
+
+
+ Enable grid API
+ Netzbetreiber-API aktiviert
+
+
+
+
+ Allow access to legacy HTTP-Api v1
+ Zugriff auf lokale HTTP-API v1 zulassen
+
+
+
+ AppSettingsPage
+
+
+
+ App Settings
+
+
+
+
+
+ Number of app instances:
+
+
+
+
+
+ solalaweb key:
+
+
+
+
+
+
+
+ Select...
+
+
+
+
+
+ solalaweb cert:
+
+
+
+
+ BaseNavigationPage
+
+
+
+ Back
+ Zurück
+
+
+
+ CablePage
+
+
+
+ Cable
+ Kabel
+
+
+
+ CarPage
+
+
+
+ Car
+ Auto
+
+
+
+ ChargerTabPage
+
+
+
+ Devices
+ Geräte
+
+
+
+
+ No car connected
+ Kein Auto angeschlossen
+
+
+
+
+ Connect the cable to charge your car
+ Stecke das Kabel ein, um dein Auto aufzuladen
+
+
+
+
+ Start
+
+
+
+
+
+ Eco
+
+
+
+
+
+ Basic
+
+
+
+
+
+ Daily trip
+
+
+
+
+ ChargingConfigurationPage
+
+
+
+ Charging configuration
+ Konfiguration des Ladevorgangs
+
+
+
+
+ Charging Speed
+ Ladegeschwindigkeit
+
+
+
+
+ kWh Limit
+
+
+
+
+
+ Daily Trip
+
+
+
+
+
+ Flexible energy tariff
+ Flexibler Energietarif
+
+
+
+
+ PV Surplus
+ PV-Überschuss
+
+
+
+
+ Load Balancing
+ Lastmanagement
+
+
+
+
+ Scheduler
+ Ladetimer
+
+
+
+
+ Current Levels
+ Strompegel
+
+
+
+
+ Car
+ Auto
+
+
+
+ ChargingSpeedPage
+
+
+
+ Charging Speed
+ Ladegeschwindigkeit
+
+
+
+ CloudPage
+
+
+
+ Cloud
+
+
+
+
+
+ Enable cloud connection
+ Cloud Verbindung aktivieren
+
+
+
+
+ Features like flexible energy tariffs, time sync and app connection are unavilable when "Enable cloud connection" is disabled
+ Wenn "Cloud Verbindung aktivieren" deaktiviert ist, sind Funktionen wie flexible Energietarife, Zeitsynchronisierung und App-Verbindung über das Internet nicht möglich.
+
+
+
+
+ Trying to connect:
+
+
+
+
+
+ Is connected:
+
+
+
+
+
+ Hello received:
+
+
+
+
+
+ Queue size cloud:
+
+
+
+
+
+ Last error:
+
+
+
+
+ CloudUrlsModel
+
+
+
+ V2
+
+
+
+
+
+ Wattpilot
+
+
+
+
+
+ go-eCharger
+
+
+
+
+
+ go-eController
+
+
+
+
+
+ Solalaweb
+
+
+
+
+ ConnectingScreen
+
+
+
+ Trying to reach device...
+ Versuche Gerät zu erreichen...
+
+
+
+
+ Cancel
+ Abbrechen
+
+
+
+ ConnectionPage
+
+
+
+ Connection
+ Verbindung
+
+
+
+
+ Wi-Fi
+ WLAN
+
+
+
+
+ Hotspot
+
+
+
+
+
+ Ethernet
+
+
+
+
+
+ OCPP
+
+
+
+
+
+ Cloud
+
+
+
+
+
+ API Settings
+ API Einstellungen
+
+
+
+ ControllerPage
+
+
+
+ Controller
+
+
+
+
+ ControllerTabPage
+
+
+
+ Devices
+ Geräte
+
+
+
+
+ Connected
+ Verbunden
+
+
+
+ CurrentLevelsPage
+
+
+
+ Current Levels
+ Strompegel
+
+
+
+ DailyTripPage
+
+
+
+ Daily Trip
+
+
+
+
+ DateAndTimePage
+
+
+
+ Date and time
+ Datum und Uhrzeit
+
+
+
+ DeviceConnection
+
+
+ Connecting to %0
+ Verbinde zu %0
+
+
+
+ Received something that is not a json object!
+
+
+
+
+ Received something that does not contain a type!
+
+
+
+
+ Received something with a non-string type!
+
+
+
+
+ Received hello without serial!
+
+
+
+
+ Received hello with a non-string serial!
+
+
+
+
+ Received hello with a non-bool secured!
+
+
+
+
+ Received hello without manufacturer!
+
+
+
+
+ Received hello with a non-string manufacturer!
+
+
+
+
+ Received hello without friendly_name!
+
+
+
+
+ Received hello with a non-string friendly_name!
+
+
+
+
+ Received hello without devicetype!
+
+
+
+
+ Received hello with a non-string devicetype!
+
+
+
+
+ Received authRequired without token1!
+
+
+
+
+ Received authRequired with a non-string token1!
+
+
+
+
+ Received authRequired without token2!
+
+
+
+
+ Received authRequired with a non-string token2!
+
+
+
+
+ Received fullStatus with a non-bool partial!
+
+
+
+
+ Received fullStatus without a status!
+
+
+
+
+ Received fullStatus with a non-object status!
+
+
+
+
+ Received partial fullStatus
+
+
+
+
+ Received last fullStatus
+
+
+
+
+ Received deltaStatus without a status!
+
+
+
+
+ Received deltaStatus with a non-object status!
+
+
+
+
+ Received message type %0 %1
+
+
+
+
+ Received message with a non-string requestId!
+
+
+
+
+ Sending %0
+
+
+
+
+ Connected!
+ Verbunden!
+
+
+
+ Disconnected!
+ Verbindung verloren!
+
+
+
+ Reconnecting to %0
+ Verbinde wieder zu %0
+
+
+
+ state changed: %0
+
+
+
+
+ could not parse received json: %0 at offset %1
+
+
+
+
+ unknown binary message type %0 received
+
+
+
+
+ error occured: %0
+
+
+
+
+ ssl peer verify error
+
+
+
+
+ ssl errors
+
+
+
+
+ ssl alert sent level=%0 type=%1 description=%2
+
+
+
+
+ ssl alert received level=%0 type=%1 description=%2
+
+
+
+
+ ssl handshake interrupted on error
+
+
+
+
+ unknown response type %0
+
+
+
+
+ DeviceListScreen
+
+ Please select your go-e device
+ Bitte wählen Sie Ihr go-e Gerät
+
+
+ Local url
+ Lokale url
+
+
+ Connect
+ Verbinden
+
+
+ Cloud Serial
+ Cloud Seriennr.
+
+
+
+
+ Device list
+ Geräteliste
+
+
+
+
+ App Settings
+
+
+
+
+
+ My devices
+ Meine Geräte
+
+
+
+
+ Found devices
+ Im Netzwerk verfügbare Geräte
+
+
+
+
+ Delete
+ Löschen
+
+
+
+
+ Serial Number %0
+ Seriennummer %0
+
+
+
+
+ Add or setup device
+ Gerät hinzufügen oder einrichten
+
+
+ Add device
+ Gerät hinzufügen
+
+
+ Locally found devices:
+ Lokal gefundene Geräte:
+
+
+ Serial:
+ Seriennr:
+
+
+ My devices:
+ Meine Geräte:
+
+
+
+
+ Manufacturer:
+ Hersteller:
+
+
+
+
+ Device Type:
+ Gerätetyp:
+
+
+ Friendly Name:
+ Anzeigename:
+
+
+
+
+ Host Name:
+
+
+
+
+
+ Ip:
+
+
+
+
+
+ Connect local
+ Lokal verbinden
+
+
+
+
+ Connect cloud
+ Über cloud verbinden
+
+
+ Settings
+ Einstellungen
+
+
+
+ DeviceScreen
+
+
+
+ Password required
+ Passwort erforderlich
+
+
+
+
+ Password:
+ Passwort:
+
+
+
+
+ Password
+ Passwort
+
+
+
+
+ Authentication impossible!
+ Authentifizierung unmöglich!
+
+
+
+
+ To use this password remotely a password has to be setup first. This can be done over the AccessPoint.
+ Um dieses Gerät aus der Ferne nutzen zu können, müssen Sie erst ein Passwort einrichten. Dies kann über den AccessPoint gemacht werden.
+
+
+
+ DevicesModel
+
+
+
+ Device %0
+ Gerät %0
+
+
+
+ DisplaySettingsPage
+
+
+
+ Display settings
+ Display
+
+
+
+ EthernetPage
+
+
+
+ Ethernet
+
+
+
+
+ FirmwarePage
+
+
+
+ Firmware
+
+
+
+
+
+ Update:
+
+
+
+
+
+ Start
+
+
+
+
+
+ Abort
+ Abbrechen
+
+
+
+
+ Switch partition
+ Partition tauschen
+
+
+
+
+ Update status:
+
+
+
+
+
+ Update progress:
+ Update Fortschritt:
+
+
+
+
+ Update message:
+ Update Meldung:
+
+
+
+
+ Recommended version:
+ Empfohlene Version:
+
+
+
+
+ Running version:
+ Ausgeführte Version:
+
+
+
+
+ Details:
+
+
+
+
+ FlexibleEnergyTariffPage
+
+
+
+ Flexible energy tariff
+ Flexibler Energietarif
+
+
+
+ FoundDevicesModel
+
+ Device %0
+ Gerät %0
+
+
+
+ GeneralOnOffSwitch
+
+
+
+ On
+ An
+
+
+
+
+ Off
+ Aus
+
+
+
+ GeneralPage
+
+
+
+ General
+ Allgemein
+
+
+
+
+ Name
+
+
+
+
+
+ Switch Language
+ Sprache ändern
+
+
+
+
+ Notifications
+ Benachrichtigungen
+
+
+
+
+ Date and time
+ Datum und Uhrzeit
+
+
+
+
+ LED
+
+
+
+
+
+ Controller
+
+
+
+
+
+ Display settings
+ Display
+
+
+
+
+ Reboot
+ Neustart
+
+
+
+ EVChargerApp
+
+
+
+ go-e App
+
+
+
+
+ GridPage
+
+
+
+ Grid
+ Netz
+
+
+
+ GroundCheckPage
+
+
+
+ Ground check
+ Erdungsprüfung
+
+
+
+ HardwareInformationPage
+
+
+
+ Hardware information
+ Hardwareinformationen
+
+
+
+
+ Serial Number:
+ Seriennummer:
+
+
+
+
+ Variant:
+ Variante:
+
+
+
+
+ RSSI:
+
+
+
+
+ HotspotPage
+
+
+
+ Hotspot
+
+
+
+
+
+ Disable AP when online
+ AP ausschalten wenn online
+
+
+
+ KwhLimitPage
+
+
+
+ kWh Limit
+
+
+
+
+ LedPage
+
+
+
+ LED
+
+
+
+
+ LicensesPage
+
+
+
+ Licenses
+ Lizenzen
+
+
+
+ LoadBalancingPage
+
+
+
+ Load Balancing
+ Lastmanagement
+
+
+
+ MainScreen
+
+
+
+ %0 ago
+ vor %0
+
+
+
+
+ in %0
+
+
+
+
+
+
+
+ Charger
+
+
+
+
+
+
+
+ Eco
+
+
+
+
+
+
+
+ Controller
+
+
+
+
+
+
+
+
+
+
+
+ Infromations
+ Informationen
+
+
+
+
+
+
+
+
+
+
+ Settings
+ Einstellungen
+
+
+
+ NamePage
+
+
+
+ Name
+
+
+
+
+ NavigationPage
+
+ Back
+ Zurück
+
+
+
+ NotificationsPage
+
+
+
+ Notifications
+ Benachrichtigungen
+
+
+
+ OcppPage
+
+
+
+ OCPP
+
+
+
+
+ PasswordPage
+
+
+
+ Password
+ Passwort
+
+
+
+ PvSurplusPage
+
+
+
+ PV Surplus
+ PV-Überschuss
+
+
+
+ RebootPage
+
+
+
+ Reboot
+ Neustart
+
+
+
+ SavedDevicesModel
+
+ Device %0
+ Gerät %0
+
+
+
+ SchedulerPage
+
+
+
+ Scheduler
+ Ladetimer
+
+
+
+ SecurityPage
+
+
+
+ Security
+ Sicherheit
+
+
+
+
+ Cable
+ Kabel
+
+
+
+
+ Access
+ Zugangskontrolle
+
+
+
+
+ Password
+ Passwort
+
+
+
+
+ Grid
+ Netz
+
+
+
+
+ Ground check
+ Erdungsprüfung
+
+
+
+ SendMessageHelper
+
+
+ No device connection set!
+
+
+
+
+ SensorsConfigurationPage
+
+
+
+ Sensors Configuration
+ Sensorkonfiguration
+
+
+
+ SettingsTabPage
+
+
+
+ Settings
+ Einstellungen
+
+
+
+
+ Charging configuration
+ Konfiguration des Ladevorgangs
+
+
+
+
+ Charging Speed
+ Ladegeschwindigkeit
+
+
+
+
+ kWh Limit
+
+
+
+
+
+ Daily Trip
+
+
+
+
+
+ Flexible energy tariff
+ Flexibler Energietarif
+
+
+
+
+ PV Surplus
+ PV-Überschuss
+
+
+
+
+ Cable
+ Kabel
+
+
+
+
+ Access
+ Zugangskontrolle
+
+
+
+
+ Password
+ Passwort
+
+
+
+
+ Grid
+ Netz
+
+
+
+
+ Ground check
+ Erdungsprüfung
+
+
+
+
+ Sensors
+ Sensoren
+
+
+
+
+ Categories
+ Kategorien
+
+
+
+
+ Wi-Fi
+ WLAN
+
+
+
+
+ Hotspot
+
+
+
+
+
+ Ethernet
+
+
+
+
+
+ OCPP
+
+
+
+
+
+ Cloud
+
+
+
+
+
+ API Settings
+ API Einstellungen
+
+
+
+
+ Name
+
+
+
+
+
+ Switch Language
+ Sprache ändern
+
+
+
+
+ Notifications
+ Benachrichtigungen
+
+
+
+
+ Date and time
+ Datum und Uhrzeit
+
+
+
+
+ LED
+
+
+
+
+
+ Controller
+
+
+
+
+
+ Display settings
+ Display
+
+
+
+
+ Firmware
+
+
+
+
+
+ Hardware information
+ Hardwareinformationen
+
+
+
+
+ Licenses
+ Lizenzen
+
+
+ Charging Speed • kWh Limit • Daily Trip • Flexible energy tariff • PV Surplus
+ Ladegeschwindigkeit • kWh Limit • Daily Trip • Flexibler Energietraif • PV Überschussladen
+
+
+
+
+ Security
+ Sicherheit
+
+
+ Cable • Access • Password • Grid • Ground check
+ Kabel • Zugangskontrolle • Passwort • Netz • Erdungsprüfung
+
+
+
+
+ Sensors Configuration
+ Sensorkonfiguration
+
+
+ Sensors • Categories
+ Sensoren • Kategorien
+
+
+
+
+ Connection
+ Verbindung
+
+
+ Wi-Fi • Hotspot • OCPP • API Settings
+ WLAN • Hotspot • OCPP • API Einstellungen
+
+
+
+
+ General
+ Allgemein
+
+
+ Name • Switch Language • Notifications • Date and time • LED • Controller
+ Name • Sprache ändern • Benachrichtigungen • Datum und Uhrzeit • LED • Controller
+
+
+
+
+ About
+ Über
+
+
+ Firmware • Hardware information • Licenses
+ Firmware • Hardwareinformationen • Lizenzen
+
+
+
+ SwitchLanguagePage
+
+
+
+ Switch Language
+ Sprache ändern
+
+
+
+ WiFiErrorsPage
+
+
+
+ Wi-Fi Errors
+ WLAN Probleme
+
+
+
+
+ Timestamp:
+ Zeitpunkt:
+
+
+
+
+ SSID:
+
+
+
+
+
+ BSSID:
+
+
+
+
+
+ Reason:
+ Grund:
+
+
+
+ WiFiOnOffSwitch
+
+
+
+ On
+ An
+
+
+
+
+ Off
+ Aus
+
+
+
+
+ Do you really want to disable Wi-Fi?
+ Möchten Sie wirklich WLAN ausschalten?
+
+
+
+
+ This action could make your device unreachable from your local homenetwork or the cloud!
+ Diese Aktion könnte das Gerät im Heimnetzwerk und aus der Cloud unerreichbar machen!
+
+
+
+ WiFiPage
+
+
+
+ Wi-Fi
+ WLAN
+
+
+
+
+ Status:
+
+
+
+
+
+ (%0) Wi-Fi Errors
+ (%0) WLAN Probleme
+
+
+
diff --git a/i18n/qml_en.ts b/i18n/qml_en.ts
new file mode 100644
index 0000000..6401616
--- /dev/null
+++ b/i18n/qml_en.ts
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/icons/Alarm.svg b/icons/Alarm.svg
new file mode 100644
index 0000000..4f3f992
--- /dev/null
+++ b/icons/Alarm.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/Charger.svg b/icons/Charger.svg
new file mode 100644
index 0000000..57d8b1a
--- /dev/null
+++ b/icons/Charger.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/ChargerV3.svg b/icons/ChargerV3.svg
new file mode 100644
index 0000000..f49c9a0
--- /dev/null
+++ b/icons/ChargerV3.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/ChargerV4.svg b/icons/ChargerV4.svg
new file mode 100644
index 0000000..927993a
--- /dev/null
+++ b/icons/ChargerV4.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/Charts.svg b/icons/Charts.svg
new file mode 100644
index 0000000..24c5d38
--- /dev/null
+++ b/icons/Charts.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/Controller.svg b/icons/Controller.svg
new file mode 100644
index 0000000..958e811
--- /dev/null
+++ b/icons/Controller.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/EcoModeFilled.svg b/icons/EcoModeFilled.svg
new file mode 100644
index 0000000..8c03df3
--- /dev/null
+++ b/icons/EcoModeFilled.svg
@@ -0,0 +1,10 @@
+
diff --git a/images/controller.png b/images/controller.png
new file mode 100644
index 0000000..2386c4a
Binary files /dev/null and b/images/controller.png differ
diff --git a/images/geminiFix.png b/images/geminiFix.png
new file mode 100644
index 0000000..e550844
Binary files /dev/null and b/images/geminiFix.png differ
diff --git a/images/geminiFlex.png b/images/geminiFlex.png
new file mode 100644
index 0000000..8b895d7
Binary files /dev/null and b/images/geminiFlex.png differ
diff --git a/images/homeFix.png b/images/homeFix.png
new file mode 100644
index 0000000..278e6cc
Binary files /dev/null and b/images/homeFix.png differ
diff --git a/images/homePlus.png b/images/homePlus.png
new file mode 100644
index 0000000..7db46bd
Binary files /dev/null and b/images/homePlus.png differ
diff --git a/images/phoenix.png b/images/phoenix.png
new file mode 100644
index 0000000..10da0b2
Binary files /dev/null and b/images/phoenix.png differ
diff --git a/images/wattpilot.png b/images/wattpilot.png
new file mode 100644
index 0000000..8ecaeac
Binary files /dev/null and b/images/wattpilot.png differ
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..8985c96
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,38 @@
+#include
+#include
+#include
+#include
+
+int main(int argc, char *argv[])
+{
+ qSetMessagePattern(QStringLiteral("%{time dd.MM.yyyy HH:mm:ss.zzz} "
+ "["
+ "%{if-debug}D%{endif}"
+ "%{if-info}I%{endif}"
+ "%{if-warning}W%{endif}"
+ "%{if-critical}C%{endif}"
+ "%{if-fatal}F%{endif}"
+ "] "
+ "%{function}(): "
+ "%{message}"));
+
+ QCoreApplication::setOrganizationName("feedc0de");
+ QCoreApplication::setOrganizationDomain("brunner.ninja");
+ QCoreApplication::setApplicationName("evcharger-app");
+
+ QGuiApplication app(argc, argv);
+
+ QTranslator translator;
+ if (!translator.load(QLocale(), "qml", "_", ":/EVChargerApp/i18n/"))
+ qWarning("could not load translations!");
+ app.installTranslator(&translator);
+
+ QQmlApplicationEngine engine;
+ QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
+ [](const QUrl &url){ qFatal("object creation failed: %s", qPrintable(url.toString()));
+ });
+
+ engine.loadFromModule("EVChargerApp", "EVChargerApp");
+
+ return app.exec();
+}
diff --git a/material-icons/add.svg b/material-icons/add.svg
new file mode 100644
index 0000000..6a0e78b
--- /dev/null
+++ b/material-icons/add.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/material-icons/grid_guides.svg b/material-icons/grid_guides.svg
new file mode 100644
index 0000000..cd82f05
--- /dev/null
+++ b/material-icons/grid_guides.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/material-icons/settings.svg b/material-icons/settings.svg
new file mode 100644
index 0000000..63cebb1
--- /dev/null
+++ b/material-icons/settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/qmldir b/qmldir
new file mode 100644
index 0000000..43d5f78
--- /dev/null
+++ b/qmldir
@@ -0,0 +1,62 @@
+module EVChargerApp
+
+EVChargerApp 1.0 AboutPage.qml
+EVChargerApp 1.0 AccessPage.qml
+EVChargerApp 1.0 AddDeviceScreen.qml
+EVChargerApp 1.0 ApiKeyValueItem.qml
+EVChargerApp 1.0 ApiSettingsPage.qml
+EVChargerApp 1.0 AppInstance.qml
+EVChargerApp 1.0 AppSettingsPage.qml
+EVChargerApp 1.0 BaseNavigationPage.qml
+EVChargerApp 1.0 CablePage.qml
+EVChargerApp 1.0 CarPage.qml
+EVChargerApp 1.0 ChargerTabPage.qml
+EVChargerApp 1.0 ChargingConfigurationPage.qml
+EVChargerApp 1.0 ChargingSpeedPage.qml
+EVChargerApp 1.0 CloudPage.qml
+EVChargerApp 1.0 CloudUrlsModel.qml
+EVChargerApp 1.0 ConnectingScreen.qml
+EVChargerApp 1.0 ConnectionPage.qml
+EVChargerApp 1.0 ControllerPage.qml
+EVChargerApp 1.0 ControllerTabPage.qml
+EVChargerApp 1.0 CurrentLevelsPage.qml
+EVChargerApp 1.0 DailyTripPage.qml
+EVChargerApp 1.0 DateAndTimePage.qml
+EVChargerApp 1.0 DeviceListScreen.qml
+EVChargerApp 1.0 DeviceScreen.qml
+EVChargerApp 1.0 DisplaySettingsPage.qml
+EVChargerApp 1.0 EcoTabPage.qml
+EVChargerApp 1.0 EthernetPage.qml
+EVChargerApp 1.0 EVChargerApp.qml
+EVChargerApp 1.0 FirmwarePage.qml
+EVChargerApp 1.0 FlexibleEnergyTariffPage.qml
+EVChargerApp 1.0 GeneralOnOffSwitch.qml
+EVChargerApp 1.0 GeneralPage.qml
+EVChargerApp 1.0 GridPage.qml
+EVChargerApp 1.0 GroundCheckPage.qml
+EVChargerApp 1.0 HardwareInformationPage.qml
+EVChargerApp 1.0 HotspotPage.qml
+EVChargerApp 1.0 InformationsTabPage.qml
+EVChargerApp 1.0 KwhLimitPage.qml
+EVChargerApp 1.0 LedPage.qml
+EVChargerApp 1.0 LicensesPage.qml
+EVChargerApp 1.0 LoadBalancingPage.qml
+EVChargerApp 1.0 MainScreen.qml
+EVChargerApp 1.0 NamePage.qml
+EVChargerApp 1.0 NavigationItem.qml
+EVChargerApp 1.0 NavigationPage.qml
+EVChargerApp 1.0 NotificationsPage.qml
+EVChargerApp 1.0 OcppPage.qml
+EVChargerApp 1.0 PasswordPage.qml
+EVChargerApp 1.0 PvSurplusPage.qml
+EVChargerApp 1.0 RebootPage.qml
+EVChargerApp 1.0 RequestStatusText.qml
+EVChargerApp 1.0 SchedulerPage.qml
+EVChargerApp 1.0 SecurityPage.qml
+EVChargerApp 1.0 SensorsConfigurationPage.qml
+EVChargerApp 1.0 SettingsTabPage.qml
+EVChargerApp 1.0 SwitchLanguagePage.qml
+EVChargerApp 1.0 VerticalTabButton.qml
+EVChargerApp 1.0 WiFiErrorsPage.qml
+EVChargerApp 1.0 WiFiOnOffSwitch.qml
+EVChargerApp 1.0 WiFiPage.qml
diff --git a/sendmessagehelper.cpp b/sendmessagehelper.cpp
new file mode 100644
index 0000000..0b89f5b
--- /dev/null
+++ b/sendmessagehelper.cpp
@@ -0,0 +1,74 @@
+#include "sendmessagehelper.h"
+
+#include
+
+void SendMessageHelper::setDeviceConnection(DeviceConnection *deviceConnection)
+{
+ if (m_deviceConnection == deviceConnection)
+ return;
+
+ if (m_deviceConnection)
+ {
+ disconnect(m_deviceConnection, &DeviceConnection::responseReceived,
+ this, &SendMessageHelper::responseReceived);
+ }
+
+ emit deviceConnectionChanged(m_deviceConnection = deviceConnection);
+
+ if (m_deviceConnection)
+ {
+ connect(m_deviceConnection, &DeviceConnection::responseReceived,
+ this, &SendMessageHelper::responseReceived);
+ }
+
+ if (!m_requestId.isEmpty())
+ {
+ m_requestId.clear();
+ emit requestIdChanged(m_requestId);
+ }
+
+ if (m_pending)
+ emit pendingChanged(m_pending = false);
+
+ if (!m_response.isNull())
+ emit responseChanged(m_response = {});
+}
+
+void SendMessageHelper::sendMessage(QVariantMap message)
+{
+ if (!m_deviceConnection)
+ {
+ qWarning() << "No device connection set!";
+ qmlEngine(this)->throwError(tr("No device connection set!"));
+ }
+
+ emit requestIdChanged(m_requestId = m_deviceConnection->generateRequestId());
+
+ if (!m_pending)
+ emit pendingChanged(m_pending = true);
+
+ message["requestId"] = m_requestId;
+
+ m_deviceConnection->sendMessage(message);
+}
+
+void SendMessageHelper::responseReceived(const QString &requestId, const QVariantMap &message)
+{
+ if (m_requestId.isEmpty())
+ return;
+
+ if (m_requestId != requestId)
+ return;
+
+ if (!m_requestId.isEmpty())
+ {
+ m_requestId.clear();
+ emit requestIdChanged(m_requestId);
+ }
+
+ if (m_pending)
+ emit pendingChanged(m_pending = false);
+
+ emit responseChanged(m_response = message);
+}
+
diff --git a/sendmessagehelper.h b/sendmessagehelper.h
new file mode 100644
index 0000000..547b554
--- /dev/null
+++ b/sendmessagehelper.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include
+#include
+
+#include "deviceconnection.h"
+
+class SendMessageHelper : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ Q_PROPERTY(DeviceConnection* deviceConnection READ deviceConnection WRITE setDeviceConnection NOTIFY deviceConnectionChanged FINAL)
+ Q_PROPERTY(QString requestId READ requestId NOTIFY requestIdChanged STORED false FINAL)
+ Q_PROPERTY(bool pending READ pending NOTIFY pendingChanged STORED false FINAL)
+ Q_PROPERTY(QVariant response READ response NOTIFY responseChanged FINAL)
+
+public:
+ DeviceConnection *deviceConnection() { return m_deviceConnection; }
+ const DeviceConnection *deviceConnection() const { return m_deviceConnection; }
+ void setDeviceConnection(DeviceConnection *deviceConnection);
+
+ const QString &requestId() const { return m_requestId; }
+
+ bool pending() const { return m_pending; }
+
+ const QVariant &response() const { return m_response; }
+
+ Q_INVOKABLE void sendMessage(QVariantMap message);
+
+signals:
+ void deviceConnectionChanged(DeviceConnection *deviceConnection);
+ void requestIdChanged(const QString &requestId);
+ void pendingChanged(bool pending);
+ void responseChanged(const QVariant &response);
+
+private slots:
+ void responseReceived(const QString &requestId, const QVariantMap &message);
+
+private:
+ DeviceConnection *m_deviceConnection{};
+ QString m_requestId;
+ bool m_pending{};
+ QVariant m_response;
+};
diff --git a/ui-icons/MaterialIcons-Regular.ttf b/ui-icons/MaterialIcons-Regular.ttf
new file mode 100644
index 0000000..48c69b5
Binary files /dev/null and b/ui-icons/MaterialIcons-Regular.ttf differ
diff --git a/ui-icons/material-design-icons.LICENSE b/ui-icons/material-design-icons.LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/ui-icons/material-design-icons.LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file