diff --git a/evcharger-app/CMakeLists.txt b/evcharger-app/CMakeLists.txt
index 0aa634d..d00fe15 100644
--- a/evcharger-app/CMakeLists.txt
+++ b/evcharger-app/CMakeLists.txt
@@ -58,6 +58,8 @@ qt_add_qml_module(evcharger-app
qml/AnimatedStackLayout.qml
qml/AnimatedStackView.qml
qml/ApiKeyValueItem.qml
+ qml/ApiStatusRow.qml
+ qml/ApiEnumEditItem.qml
qml/ApiSettingsPage.qml
qml/AppInstance.qml
qml/AppSettingsPage.qml
@@ -143,7 +145,6 @@ qt_add_qml_module(evcharger-app
qml/WhiteSwipeDelegate.qml
qml/WiFiErrorsPage.qml
qml/WiFiPage.qml
- qml/WiFiScanPage.qml
RESOURCES
qml/icons/Charger.svg
qml/icons/ChargerV3.svg
diff --git a/evcharger-app/i18n/qml_de.ts b/evcharger-app/i18n/qml_de.ts
index 0eca4c5..ed44160 100644
--- a/evcharger-app/i18n/qml_de.ts
+++ b/evcharger-app/i18n/qml_de.ts
@@ -3087,114 +3087,6 @@
Pmf erforderlich
-
- WiFiScanPage
-
-
- Wi-Fi Scan Result
- WLAN Scan Result
-
-
-
- SSID:
- SSID:
-
-
-
- Authmode:
- Authentifizierungsart:
-
-
-
- OPEN
-
-
-
-
- WEP
-
-
-
-
- WPA_PSK
-
-
-
-
- WPA2_PSK
-
-
-
-
- WPA_WPA2_PSK
-
-
-
-
- WPA2_ENTERPRISE
-
-
-
-
- WPA3_PSK
-
-
-
-
- WPA2_WPA3_PSK
-
-
-
-
- WAPI_PSK
-
-
-
-
- OWE
-
-
-
-
- WPA3_ENT_192
-
-
-
-
- WPA3_EXT_PSK
-
-
-
-
- WPA3_EXT_PSK_MIXED_MODE
-
-
-
-
- Pairwise cipher:
- Paarweise Cipher:
-
-
-
- Group cipher:
- Gruppen Cipher:
-
-
-
- RSSI:
- RSSI:
-
-
-
- Channel:
- Kanal:
-
-
-
- BSSID:
- BSSID:
-
-
main
diff --git a/evcharger-app/qml/ApiEnumEditItem.qml b/evcharger-app/qml/ApiEnumEditItem.qml
new file mode 100644
index 0000000..08377fe
--- /dev/null
+++ b/evcharger-app/qml/ApiEnumEditItem.qml
@@ -0,0 +1,37 @@
+import QtQuick
+import QtQuick.Controls
+import EVChargerApp
+
+EditValueItem {
+ id: item
+
+ required property var choices
+
+ function textForValue(value) {
+ for (let i = 0; i < choices.length; ++i) {
+ if (choices[i].value === value)
+ return choices[i].text
+ }
+ return value === null || value === undefined ? qsTr("Automatic") : String(value)
+ }
+
+ valueText: textForValue(value)
+ editableItem: ComboBox {
+ id: comboBox
+ property var value
+
+ model: item.choices
+ textRole: "text"
+ valueRole: "value"
+
+ function selectValue() {
+ const index = indexOfValue(value)
+ if (index >= 0)
+ currentIndex = index
+ }
+
+ Component.onCompleted: selectValue()
+ onValueChanged: selectValue()
+ onActivated: value = currentValue
+ }
+}
diff --git a/evcharger-app/qml/ApiStatusRow.qml b/evcharger-app/qml/ApiStatusRow.qml
new file mode 100644
index 0000000..61bd67a
--- /dev/null
+++ b/evcharger-app/qml/ApiStatusRow.qml
@@ -0,0 +1,47 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import EVChargerApp
+
+RowLayout {
+ id: row
+
+ required property string apiKey
+ required property string label
+ property var valueFormatter: null
+ property alias value: apiValue.value
+ property alias exists: apiValue.exists
+
+ visible: apiValue.exists
+
+ function defaultValueText(value) {
+ if (value === null || value === undefined || value === "")
+ return "—"
+ if (typeof value === "object")
+ return JSON.stringify(value)
+ return String(value)
+ }
+
+ ApiKeyValueHelper {
+ id: apiValue
+ deviceConnection: theDeviceConnection
+ apiKey: row.apiKey
+ }
+
+ Label {
+ Layout.preferredWidth: 125
+ Layout.maximumWidth: 125
+ text: row.label
+ font.bold: true
+ wrapMode: Text.Wrap
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: row.valueFormatter === null
+ ? row.defaultValueText(apiValue.value)
+ : row.valueFormatter(apiValue.value)
+ wrapMode: Text.Wrap
+ selectable: true
+ }
+}
diff --git a/evcharger-app/qml/CellularPage.qml b/evcharger-app/qml/CellularPage.qml
index b5a0082..a0b9189 100644
--- a/evcharger-app/qml/CellularPage.qml
+++ b/evcharger-app/qml/CellularPage.qml
@@ -4,214 +4,237 @@ import QtQuick.Layouts
import EVChargerApp
NavigationPage {
+ id: cellularPage
+
title: qsTr("Cellular")
+ function technologyName(value) {
+ switch (value) {
+ case 0: return qsTr("No service")
+ case 1: return qsTr("GSM")
+ case 2: return qsTr("GPRS")
+ case 3: return qsTr("EDGE")
+ case 4: return qsTr("WCDMA")
+ case 5: return qsTr("HSDPA")
+ case 6: return qsTr("HSUPA")
+ case 7: return qsTr("HSPA")
+ case 8: return qsTr("LTE")
+ default: return qsTr("Disconnected")
+ }
+ }
+
+ function signalText(value) {
+ if (value === null || value === undefined || value.rssi === null || value.rssi === undefined)
+ return qsTr("Unavailable")
+ return value.rssi + " dBm" + (value.ber === null || value.ber === undefined
+ ? "" : " • BER " + value.ber)
+ }
+
ConfirmingOnOffSwitch {
apiKey: "cen"
+ text: checked ? qsTr("Cellular on") : qsTr("Cellular off")
dialogTitle: qsTr("Are you sure?")
- dialogText: qsTr("Disabling Cellular could make your device unreachable from the cloud!")
+ dialogText: qsTr("Disabling cellular could make your device unreachable from the cloud!")
+ }
+
+ WhiteBox {
+ Layout.fillWidth: true
+ implicitHeight: modemStatus.implicitHeight + 20
+
+ ColumnLayout {
+ id: modemStatus
+ anchors.fill: parent
+ anchors.margins: 10
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Mobile network")
+ font.bold: true
+ }
+
+ ApiStatusRow {
+ apiKey: "ccm"
+ label: qsTr("Connection")
+ valueFormatter: function(value) { return technologyName(value) }
+ }
+
+ ApiStatusRow {
+ apiKey: "mon"
+ label: qsTr("Operator")
+ valueFormatter: function(value) {
+ return value || qsTr("No operator")
+ }
+ }
+
+ ApiStatusRow {
+ apiKey: "msq"
+ label: qsTr("Signal")
+ valueFormatter: function(value) { return signalText(value) }
+ }
+
+ ApiStatusRow {
+ apiKey: "mia"
+ label: qsTr("IP address")
+ valueFormatter: function(value) {
+ return value && value.ip ? value.ip : qsTr("No address")
+ }
+ }
+ }
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Connection settings")
+ font.bold: true
}
EditValueItem {
text: qsTr("APN")
apiKey: "moap"
+ visible: exists
fullWidth: true
editableItem: TextField {
- id: textField0
- property alias value: textField0.text
+ id: apnInput
+ property alias value: apnInput.text
+ placeholderText: qsTr("Access point name")
}
}
+ ApiEnumEditItem {
+ text: qsTr("SIM")
+ apiKey: "simi"
+ visible: exists
+ choices: [
+ { value: 0, text: qsTr("Embedded SIM") },
+ { value: 1, text: qsTr("SIM card slot") }
+ ]
+ }
+
EditValueItem {
- text: qsTr("PIN")
+ text: qsTr("SIM PIN")
apiKey: "mopi"
+ visible: exists
+ valueText: value ? qsTr("Configured") : qsTr("Not configured")
+ editValueTransform: function() { return "" }
fullWidth: true
editableItem: TextField {
- id: textField1
- property alias value: textField1.text
+ id: pinInput
+ property alias value: pinInput.text
+ echoMode: TextInput.Password
+ inputMethodHints: Qt.ImhDigitsOnly
+ placeholderText: qsTr("Enter a new PIN")
}
}
- // TODO auth Type
+ ApiEnumEditItem {
+ text: qsTr("Preferred network")
+ apiKey: "csm"
+ visible: exists
+ choices: [
+ { value: null, text: qsTr("Automatic") },
+ { value: 0, text: qsTr("2G only") },
+ { value: 1, text: qsTr("4G only") }
+ ]
+ }
+
+ ApiEnumEditItem {
+ id: authenticationType
+ text: qsTr("Authentication")
+ apiKey: "mat"
+ visible: exists
+ choices: [
+ { value: 0, text: qsTr("None") },
+ { value: 1, text: qsTr("PAP") },
+ { value: 2, text: qsTr("CHAP") },
+ { value: 4, text: qsTr("MSCHAP") },
+ { value: 8, text: qsTr("MSCHAP v2") },
+ { value: 16, text: qsTr("EAP") }
+ ]
+ }
EditValueItem {
text: qsTr("Username")
apiKey: "mau"
+ visible: exists && authenticationType.value !== 0
fullWidth: true
editableItem: TextField {
- id: textField2
- property alias value: textField2.text
+ id: usernameInput
+ property alias value: usernameInput.text
}
}
EditValueItem {
text: qsTr("Password")
apiKey: "mpap"
+ visible: exists && authenticationType.value !== 0
+ valueText: value ? qsTr("Configured") : qsTr("Not configured")
+ editValueTransform: function() { return "" }
fullWidth: true
editableItem: TextField {
- id: textField3
- property alias value: textField3.text
+ id: passwordInput
+ property alias value: passwordInput.text
+ echoMode: TextInput.Password
+ placeholderText: qsTr("Enter a new password")
}
}
- WhiteItemDelegate {
- id: delegate
- Layout.fillWidth: true
-
- text: qsTr("Show technical details of LTE modem")
-
- onClicked: {
- gridLayout.visible = true
- delegate.text = null
- delegate.implicitWidth = Qt.binding(() => gridLayout.implicitWidth + gridLayout.anchors.leftMargin + gridLayout.anchors.rightMargin)
- delegate.implicitHeight = Qt.binding(() => gridLayout.implicitHeight + gridLayout.anchors.topMargin + gridLayout.anchors.bottomMargin)
+ EditValueItem {
+ text: qsTr("Forced operator")
+ apiKey: "mopn"
+ visible: exists
+ valueText: value ? value : qsTr("Automatic")
+ fullWidth: true
+ editableItem: TextField {
+ id: operatorInput
+ property alias value: operatorInput.text
+ placeholderText: qsTr("Automatic when empty")
}
+ }
- GridLayout {
- id: gridLayout
+ Button {
+ id: technicalDetailsButton
+ Layout.fillWidth: true
+ checkable: true
+ text: checked ? qsTr("Hide modem details") : qsTr("Show modem details")
+ }
- columns: 2
+ WhiteBox {
+ Layout.fillWidth: true
+ implicitHeight: modemDetails.implicitHeight + 20
+ visible: technicalDetailsButton.checked
+
+ ColumnLayout {
+ id: modemDetails
anchors.fill: parent
- anchors.leftMargin: 5
- anchors.topMargin: 5
- anchors.rightMargin: 5
- anchors.bottomMargin: 5
- visible: false
+ anchors.margins: 10
- ApiKeyValueHelper {
- id: misApiKey
- deviceConnection: theDeviceConnection
- apiKey: "mis"
- }
-
- Label {
- text: qsTr("Initialization status:")
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Modem details")
font.bold: true
}
- Text {
- text: JSON.stringify(misApiKey.value, null, 2)
- wrapMode: Text.Wrap
- Layout.fillWidth: true
- }
+ ApiStatusRow { apiKey: "mis"; label: qsTr("Initialization") }
+ ApiStatusRow { apiKey: "mps"; label: qsTr("SIM status") }
+ ApiStatusRow { apiKey: "imei"; label: qsTr("IMEI") }
+ ApiStatusRow { apiKey: "imsi"; label: qsTr("IMSI") }
+ ApiStatusRow { apiKey: "iccid"; label: qsTr("ICCID") }
+ ApiStatusRow { apiKey: "msor"; label: qsTr("Operator selection") }
+ ApiStatusRow { apiKey: "mas"; label: qsTr("Startup commands") }
- ApiKeyValueHelper {
- id: imeiApiKey
- deviceConnection: theDeviceConnection
- apiKey: "imei"
- }
-
- Label {
- text: qsTr("IMEI:")
- font.bold: true
- }
-
- Text {
- text: JSON.stringify(imeiApiKey.value, null, 2)
- wrapMode: Text.Wrap
- Layout.fillWidth: true
- }
-
- ApiKeyValueHelper {
- id: imsiApiKey
- deviceConnection: theDeviceConnection
- apiKey: "imsi"
- }
-
- Label {
- text: qsTr("IMSI:")
- font.bold: true
- }
-
- Text {
- text: JSON.stringify(imsiApiKey.value, null, 2)
- wrapMode: Text.Wrap
- Layout.fillWidth: true
- }
-
- ApiKeyValueHelper {
- id: mpsApiKey
- deviceConnection: theDeviceConnection
- apiKey: "mps"
- }
-
- Label {
- text: qsTr("Pin status:")
- font.bold: true
- }
-
- Text {
- text: JSON.stringify(mpsApiKey.value, null, 2)
- wrapMode: Text.Wrap
- Layout.fillWidth: true
- }
-
- ApiKeyValueHelper {
- id: monApiKey
- deviceConnection: theDeviceConnection
- apiKey: "mon"
- }
-
- Label {
- text: qsTr("Operator name:")
- font.bold: true
- }
-
- Text {
- text: JSON.stringify(monApiKey.value, null, 2)
- wrapMode: Text.Wrap
- Layout.fillWidth: true
- }
-
- ApiKeyValueHelper {
- id: msqApiKey
- deviceConnection: theDeviceConnection
- apiKey: "msq"
- }
-
- Label {
- text: qsTr("Signal quality:")
- font.bold: true
- }
-
- Text {
- text: JSON.stringify(msqApiKey.value, null, 2)
- wrapMode: Text.Wrap
- Layout.fillWidth: true
- }
-
- ApiKeyValueHelper {
- id: ccmApiKey
- deviceConnection: theDeviceConnection
- apiKey: "ccm"
- }
-
- Label {
- text: qsTr("Current system mode:")
- font.bold: true
- }
-
- Text {
- text: JSON.stringify(ccmApiKey.value, null, 2)
- wrapMode: Text.Wrap
- Layout.fillWidth: true
- }
-
- ApiKeyValueHelper {
- id: miaApiKey
- deviceConnection: theDeviceConnection
+ ApiStatusRow {
apiKey: "mia"
- }
-
- Label {
- text: qsTr("IP Address:")
- font.bold: true
- }
-
- Text {
- text: JSON.stringify(miaApiKey.value, null, 2)
- wrapMode: Text.Wrap
- Layout.fillWidth: true
+ label: qsTr("Addressing")
+ valueFormatter: function(value) {
+ if (value === null || value === undefined)
+ return qsTr("Not connected")
+ const values = []
+ if (value.ip) values.push(value.ip)
+ if (value.subnet) values.push(qsTr("subnet %1").arg(value.subnet))
+ if (value.gw) values.push(qsTr("gateway %1").arg(value.gw))
+ if (value.ipv6) values.push(value.ipv6)
+ return values.length > 0 ? values.join(" • ") : qsTr("No address")
+ }
}
}
}
diff --git a/evcharger-app/qml/ConnectionPage.qml b/evcharger-app/qml/ConnectionPage.qml
index 52f9b36..cce1b5e 100644
--- a/evcharger-app/qml/ConnectionPage.qml
+++ b/evcharger-app/qml/ConnectionPage.qml
@@ -9,6 +9,7 @@ NavigationPage {
NavigationItem {
iconSource: "material-icons/grid_guides.svg"
title: qsTr("Wi-Fi")
+ description: qsTr("Join and manage wireless networks")
component: Component {
WiFiPage {
}
@@ -16,19 +17,10 @@ NavigationPage {
visible: wifiStaApiKeyValueHelper.exists
}
- NavigationItem {
- iconSource: "material-icons/grid_guides.svg"
- title: qsTr("Cellular")
- component: Component {
- CellularPage {
- }
- }
- visible: cellularApiKeyValueHelper.exists
- }
-
NavigationItem {
iconSource: "material-icons/grid_guides.svg"
title: qsTr("Hotspot")
+ description: qsTr("Configure the device's Wi-Fi access point")
component: Component {
HotspotPage {
}
@@ -39,6 +31,7 @@ NavigationPage {
NavigationItem {
iconSource: "material-icons/grid_guides.svg"
title: qsTr("Ethernet")
+ description: qsTr("Wired network and IP settings")
component: Component {
EthernetPage {
}
@@ -46,6 +39,17 @@ NavigationPage {
visible: ethernetApiKeyValueHelper.exists
}
+ NavigationItem {
+ iconSource: "material-icons/grid_guides.svg"
+ title: qsTr("Cellular")
+ description: qsTr("SIM, APN and mobile network settings")
+ component: Component {
+ CellularPage {
+ }
+ }
+ visible: cellularApiKeyValueHelper.exists
+ }
+
NavigationItem {
iconSource: "material-icons/grid_guides.svg"
title: qsTr("Cloud")
diff --git a/evcharger-app/qml/EditValueItem.qml b/evcharger-app/qml/EditValueItem.qml
index 8304a77..25d339f 100644
--- a/evcharger-app/qml/EditValueItem.qml
+++ b/evcharger-app/qml/EditValueItem.qml
@@ -10,7 +10,9 @@ WhiteItemDelegate {
property string valueText: apiKeyValueHelper.value
property alias editableItem: dialog.contentItem
property alias value: apiKeyValueHelper.value
+ property alias exists: apiKeyValueHelper.exists
property bool fullWidth: false
+ property var editValueTransform: null
Layout.fillWidth: true
@@ -52,7 +54,7 @@ WhiteItemDelegate {
}
onClicked: {
- editableItem.value = value
+ editableItem.value = editValueTransform === null ? value : editValueTransform(value)
dialog.open()
}
@@ -72,7 +74,6 @@ WhiteItemDelegate {
}
onAccepted: {
- console.log(editableItem.value)
setValueHelper.setValue(editableItem.value)
}
}
diff --git a/evcharger-app/qml/EthernetPage.qml b/evcharger-app/qml/EthernetPage.qml
index c1d0af9..488e4b8 100644
--- a/evcharger-app/qml/EthernetPage.qml
+++ b/evcharger-app/qml/EthernetPage.qml
@@ -3,15 +3,207 @@ import QtQuick.Controls
import QtQuick.Layouts
NavigationPage {
+ id: ethernetPage
+
title: qsTr("Ethernet")
+ function connectionText(value) {
+ if (value === null || value === undefined)
+ return qsTr("Not connected")
+ return value.ip || qsTr("Connected")
+ }
+
+ function linkText(value) {
+ if (value === null || value === undefined)
+ return qsTr("No link")
+ const speed = value.speed ? value.speed + " Mbit/s" : qsTr("Unknown speed")
+ const duplex = value.dup === true || value.dup === 1 ? qsTr("full duplex")
+ : value.dup === false || value.dup === 0 ? qsTr("half duplex")
+ : value.dup === null || value.dup === undefined ? "" : String(value.dup)
+ return duplex ? speed + " • " + duplex : speed
+ }
+
+ ApiKeyValueHelper {
+ id: staticIpMode
+ deviceConnection: theDeviceConnection
+ apiKey: "euse"
+ }
+
+ ApiKeyValueHelper {
+ id: staticDnsMode
+ deviceConnection: theDeviceConnection
+ apiKey: "eusd"
+ }
+
GeneralOnOffSwitch {
apiKey: "ee"
+ text: checked ? qsTr("Ethernet on") : qsTr("Ethernet off")
+ }
+
+ WhiteBox {
+ Layout.fillWidth: true
+ implicitHeight: connectionStatus.implicitHeight + 20
+
+ ColumnLayout {
+ id: connectionStatus
+ anchors.fill: parent
+ anchors.margins: 10
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Connection")
+ font.bold: true
+ }
+
+ ApiStatusRow {
+ apiKey: "cce"
+ label: qsTr("Address")
+ valueFormatter: function(value) { return connectionText(value) }
+ }
+
+ ApiStatusRow {
+ apiKey: "cce"
+ label: qsTr("Link")
+ valueFormatter: function(value) { return linkText(value) }
+ }
+
+ ApiStatusRow {
+ apiKey: "cce"
+ label: qsTr("Gateway")
+ valueFormatter: function(value) {
+ return value && value.gw ? value.gw : "—"
+ }
+ }
+ }
}
Text {
- text: "TODO"
-
- Layout.fillHeight: true
+ Layout.fillWidth: true
+ text: qsTr("IPv4 configuration")
+ font.bold: true
}
+
+ GeneralOnOffSwitch {
+ apiKey: "euse"
+ text: checked ? qsTr("Static IP address") : qsTr("Automatic IP address (DHCP)")
+ }
+
+ EditValueItem {
+ text: qsTr("IP address")
+ apiKey: "esi"
+ visible: exists && staticIpMode.value === true
+ fullWidth: true
+ editableItem: TextField {
+ id: ipAddressInput
+ property alias value: ipAddressInput.text
+ inputMethodHints: Qt.ImhPreferNumbers
+ placeholderText: "192.168.1.10"
+ }
+ }
+
+ EditValueItem {
+ text: qsTr("Subnet mask")
+ apiKey: "ess"
+ visible: exists && staticIpMode.value === true
+ fullWidth: true
+ editableItem: TextField {
+ id: subnetInput
+ property alias value: subnetInput.text
+ inputMethodHints: Qt.ImhPreferNumbers
+ placeholderText: "255.255.255.0"
+ }
+ }
+
+ EditValueItem {
+ text: qsTr("Gateway")
+ apiKey: "esg"
+ visible: exists && staticIpMode.value === true
+ fullWidth: true
+ editableItem: TextField {
+ id: gatewayInput
+ property alias value: gatewayInput.text
+ inputMethodHints: Qt.ImhPreferNumbers
+ placeholderText: "192.168.1.1"
+ }
+ }
+
+ GeneralOnOffSwitch {
+ apiKey: "eusd"
+ text: checked ? qsTr("Static DNS servers") : qsTr("Automatic DNS servers")
+ }
+
+ EditValueItem {
+ text: qsTr("Primary DNS")
+ apiKey: "esd0"
+ visible: exists && staticDnsMode.value === true
+ fullWidth: true
+ editableItem: TextField {
+ id: dns0Input
+ property alias value: dns0Input.text
+ inputMethodHints: Qt.ImhPreferNumbers
+ placeholderText: "1.1.1.1"
+ }
+ }
+
+ EditValueItem {
+ text: qsTr("Secondary DNS")
+ apiKey: "esd1"
+ visible: exists && staticDnsMode.value === true
+ fullWidth: true
+ editableItem: TextField {
+ id: dns1Input
+ property alias value: dns1Input.text
+ inputMethodHints: Qt.ImhPreferNumbers
+ placeholderText: "8.8.8.8"
+ }
+ }
+
+ EditValueItem {
+ text: qsTr("Additional DNS")
+ apiKey: "esd2"
+ visible: exists && staticDnsMode.value === true
+ fullWidth: true
+ editableItem: TextField {
+ id: dns2Input
+ property alias value: dns2Input.text
+ inputMethodHints: Qt.ImhPreferNumbers
+ }
+ }
+
+ WhiteBox {
+ Layout.fillWidth: true
+ implicitHeight: ethernetDetails.implicitHeight + 20
+ visible: ethernetHostname.exists || ethernetMac.exists || ethernetInit.exists
+
+ ColumnLayout {
+ id: ethernetDetails
+ anchors.fill: parent
+ anchors.margins: 10
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Interface details")
+ font.bold: true
+ }
+
+ ApiStatusRow {
+ id: ethernetHostname
+ apiKey: "hste"
+ label: qsTr("Hostname")
+ }
+
+ ApiStatusRow {
+ id: ethernetMac
+ apiKey: "mace"
+ label: qsTr("MAC address")
+ }
+
+ ApiStatusRow {
+ id: ethernetInit
+ apiKey: "eis"
+ label: qsTr("Initialization")
+ }
+ }
+ }
+
}
diff --git a/evcharger-app/qml/HotspotPage.qml b/evcharger-app/qml/HotspotPage.qml
index 7385deb..854b493 100644
--- a/evcharger-app/qml/HotspotPage.qml
+++ b/evcharger-app/qml/HotspotPage.qml
@@ -3,44 +3,105 @@ import QtQuick.Controls
import QtQuick.Layouts
NavigationPage {
+ id: hotspotPage
+
title: qsTr("Hotspot")
GeneralOnOffSwitch {
apiKey: "wae"
+ text: checked ? qsTr("Hotspot on") : qsTr("Hotspot off")
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Configuration")
+ font.bold: true
}
GeneralOnOffSwitch {
apiKey: "wda"
- text: qsTr("Disable AP when online")
+ text: qsTr("Turn hotspot off when another internet connection is available")
}
EditValueItem {
text: qsTr("SSID")
apiKey: "wan"
+ visible: exists
fullWidth: true
editableItem: TextField {
id: textField0
property alias value: textField0.text
+ placeholderText: qsTr("Hotspot name")
}
}
EditValueItem {
text: qsTr("Password")
apiKey: "wak"
+ visible: exists
+ valueText: value ? qsTr("Configured") : qsTr("Open network")
+ editValueTransform: function() { return "" }
fullWidth: true
editableItem: TextField {
id: textField1
property alias value: textField1.text
+ echoMode: TextInput.Password
+ placeholderText: qsTr("Enter a new password")
}
}
+ ApiEnumEditItem {
+ text: qsTr("Security")
+ apiKey: "waap"
+ visible: exists
+ choices: [
+ { value: 0, text: qsTr("Open") },
+ { value: 2, text: qsTr("WPA") },
+ { value: 3, text: qsTr("WPA2") },
+ { value: 4, text: qsTr("WPA/WPA2") },
+ { value: 6, text: qsTr("WPA3") },
+ { value: 7, text: qsTr("WPA2/WPA3") }
+ ]
+ }
+
EditValueItem {
text: qsTr("Channel")
valueText: Qt.locale().toString(value)
apiKey: "wapc"
+ visible: exists
editableItem: SpinBox {
- from: 0
- to: 15
+ from: 1
+ to: 14
+ }
+ }
+
+ WhiteBox {
+ Layout.fillWidth: true
+ implicitHeight: hotspotStatus.implicitHeight + 20
+ visible: hotspotHostname.exists || hotspotMac.exists
+
+ ColumnLayout {
+ id: hotspotStatus
+ anchors.fill: parent
+ anchors.margins: 10
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Hotspot identity")
+ font.bold: true
+ }
+
+ ApiStatusRow {
+ id: hotspotHostname
+ apiKey: "hsta"
+ label: qsTr("Hostname")
+ }
+
+ ApiStatusRow {
+ id: hotspotMac
+ apiKey: "maca"
+ label: qsTr("MAC address")
+ }
}
}
}
diff --git a/evcharger-app/qml/WiFiPage.qml b/evcharger-app/qml/WiFiPage.qml
index b9b63b4..990c3f1 100644
--- a/evcharger-app/qml/WiFiPage.qml
+++ b/evcharger-app/qml/WiFiPage.qml
@@ -4,127 +4,428 @@ import QtQuick.Layouts
import EVChargerApp
NavigationPage {
+ id: wifiPage
+
+ property var selectedNetwork: null
+ property int selectedWifiSlot: -1
+
title: qsTr("Wi-Fi")
+ function configuredNetworks() {
+ const result = []
+ const wifis = configuredWifis.value
+ if (wifis === null || wifis === undefined)
+ return result
+
+ for (let i = 0; i < wifis.length; ++i) {
+ const wifi = wifis[i]
+ if (wifi !== null && wifi !== undefined && wifi.ssid) {
+ result.push({
+ slot: i,
+ ssid: wifi.ssid,
+ useStaticIp: wifi.useStaticIp === true,
+ staticIp: wifi.staticIp
+ })
+ }
+ }
+ return result
+ }
+
+ function isConfigured(ssid) {
+ const networks = configuredNetworks()
+ for (let i = 0; i < networks.length; ++i) {
+ if (networks[i].ssid === ssid)
+ return true
+ }
+ return false
+ }
+
+ function firstFreeWifiSlot() {
+ const wifis = configuredWifis.value
+ if (wifis === null || wifis === undefined)
+ return -2
+
+ for (let i = 0; i < wifis.length; ++i) {
+ if (wifis[i] === null || wifis[i] === undefined || !wifis[i].ssid)
+ return i
+ }
+ return -1
+ }
+
+ function availableNetworks() {
+ const result = []
+ const scan = wifiScanResult.value
+ if (scan === null || scan === undefined)
+ return result
+
+ // Android presents an SSID once. Keep the strongest BSSID when the
+ // same network was observed through multiple access points.
+ for (let i = 0; i < scan.length; ++i) {
+ const candidate = scan[i]
+ if (candidate === null || candidate === undefined || !candidate.ssid)
+ continue
+
+ let existing = -1
+ for (let j = 0; j < result.length; ++j) {
+ if (result[j].ssid === candidate.ssid) {
+ existing = j
+ break
+ }
+ }
+
+ if (existing < 0)
+ result.push(candidate)
+ else if ((candidate.rssi ?? -1000) > (result[existing].rssi ?? -1000))
+ result[existing] = candidate
+ }
+
+ result.sort((left, right) => (right.rssi ?? -1000) - (left.rssi ?? -1000))
+ return result
+ }
+
+ function securityName(encryptionType) {
+ switch (encryptionType) {
+ case 0: return qsTr("Open")
+ case 1: return qsTr("WEP")
+ case 2: return qsTr("WPA")
+ case 3: return qsTr("WPA2")
+ case 4: return qsTr("WPA/WPA2")
+ case 5: return qsTr("WPA2 Enterprise")
+ case 6: return qsTr("WPA3")
+ case 7: return qsTr("WPA2/WPA3")
+ case 8: return qsTr("WAPI")
+ case 9: return qsTr("OWE")
+ case 10: return qsTr("WPA3 Enterprise")
+ case 11: return qsTr("WPA3 External PSK")
+ case 12: return qsTr("WPA3 External PSK mixed")
+ default: return qsTr("Unknown security")
+ }
+ }
+
+ function needsPassword(network) {
+ return network.encryptionType !== 0 && network.encryptionType !== 9
+ }
+
+ function signalBars(rssi) {
+ if (rssi >= -55)
+ return "▂▄▆█"
+ if (rssi >= -67)
+ return "▂▄▆"
+ if (rssi >= -75)
+ return "▂▄"
+ return "▂"
+ }
+
+ function stationStatusName(status) {
+ switch (status) {
+ case 0: return qsTr("Idle")
+ case 1: return qsTr("Network not found")
+ case 2: return qsTr("Scan completed")
+ case 3: return qsTr("Connected")
+ case 4: return qsTr("Connection failed")
+ case 5: return qsTr("Connection lost")
+ case 6: return qsTr("Disconnected")
+ case 7: return qsTr("Connecting")
+ case 8: return qsTr("Disconnecting")
+ case 9: return qsTr("Wi-Fi unavailable")
+ case 10: return qsTr("Waiting for IP address")
+ default: return qsTr("Unknown")
+ }
+ }
+
+ function scanStatusName(status) {
+ switch (status) {
+ case 0: return qsTr("Not scanned yet")
+ case 1: return qsTr("Scanning")
+ case 2: return qsTr("Finished")
+ case 3: return qsTr("Failed")
+ default: return qsTr("Unknown")
+ }
+ }
+
+ function selectNetwork(network) {
+ if (isConfigured(network.ssid))
+ return
+
+ const slot = firstFreeWifiSlot()
+ if (slot === -2) {
+ noSlotDialog.text = qsTr("The Wi-Fi configuration has not been received yet.")
+ noSlotDialog.open()
+ return
+ }
+ if (slot < 0) {
+ noSlotDialog.text = qsTr("All Wi-Fi configuration slots are occupied. Delete a configured network first.")
+ noSlotDialog.open()
+ return
+ }
+
+ selectedNetwork = network
+ selectedWifiSlot = slot
+ passwordInput.clear()
+ showPassword.checked = false
+ passwordDialog.open()
+ }
+
+ function provisionSelectedNetwork() {
+ if (selectedNetwork === null || selectedWifiSlot < 0)
+ return
+
+ const value = []
+ for (let i = 0; i < selectedWifiSlot; ++i)
+ value.push(null)
+ value.push({
+ ssid: selectedNetwork.ssid,
+ key: needsPassword(selectedNetwork) ? passwordInput.text : ""
+ })
+ provisionWifi.setValue(value)
+ }
+
+ ApiKeyValueHelper {
+ id: configuredWifis
+ deviceConnection: theDeviceConnection
+ apiKey: "wifis"
+ }
+
+ ApiKeyValueHelper {
+ id: wifiScanResult
+ deviceConnection: theDeviceConnection
+ apiKey: "scan"
+ }
+
+ SetValueHelper {
+ id: provisionWifi
+ deviceConnection: theDeviceConnection
+ apiKey: "wifis"
+ }
+
+ SetValueHelper {
+ id: deleteWifi
+ deviceConnection: theDeviceConnection
+ apiKey: "delw"
+ }
+
+ SendMessageHelper {
+ id: triggerScan
+ deviceConnection: theDeviceConnection
+ }
+
ConfirmingOnOffSwitch {
apiKey: "wen"
dialogTitle: qsTr("Are you sure?")
dialogText: qsTr("Disabling Wi-Fi could make your device unreachable from your local homenetwork or the cloud!")
}
- GridLayout {
- columns: 2
+ WhiteBox {
+ Layout.fillWidth: true
+ implicitHeight: connectionStatusColumn.implicitHeight + 20
- Text {
- text: qsTr("Status:")
- }
+ ColumnLayout {
+ id: connectionStatusColumn
+ anchors.fill: parent
+ anchors.margins: 10
- Text {
- Layout.fillWidth: true
-
- ApiKeyValueHelper {
- id: staStatus
- deviceConnection: theDeviceConnection
- apiKey: "wst"
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Connection")
+ font.bold: true
}
- 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
+ ApiStatusRow {
+ apiKey: "wst"
+ label: qsTr("Status")
+ valueFormatter: function(value) { return stationStatusName(value) }
+ }
+
+ ApiStatusRow {
+ apiKey: "ccw"
+ label: qsTr("Network")
+ valueFormatter: function(value) {
+ if (value === null || value === undefined)
+ return qsTr("Not connected")
+ const address = value.ip ? " • " + value.ip : ""
+ return (value.ssid || qsTr("Unknown network")) + address
+ }
+ }
+
+ ApiStatusRow {
+ apiKey: "rssi"
+ label: qsTr("Signal")
+ valueFormatter: function(value) {
+ return value === null || value === undefined
+ ? "—"
+ : signalBars(value) + " " + value + " dBm"
+ }
+ }
+
+ ApiStatusRow {
+ apiKey: "scas"
+ label: qsTr("Wi-Fi scan")
+ valueFormatter: function(value) { return scanStatusName(value) }
+ }
+
+ ApiStatusRow {
+ apiKey: "scaa"
+ label: qsTr("Last scan")
+ valueFormatter: function(value) {
+ return value === null || value === undefined
+ ? qsTr("Never")
+ : formatDuration(value - rebootTime.value)
}
}
}
+ }
- Text {
- text: qsTr("Scan-Status:")
- }
+ RowLayout {
+ Layout.fillWidth: true
Text {
Layout.fillWidth: true
+ text: qsTr("Configured networks")
+ font.bold: true
+ }
+
+ BusyIndicator {
+ visible: provisionWifi.pending || deleteWifi.pending || triggerScan.pending
+ }
+
+ Button {
+ text: scanStatus.value === 1 ? qsTr("Scanning…") : qsTr("Scan again")
+ enabled: !triggerScan.pending && scanStatus.value !== 1
+ onClicked: triggerScan.sendMessage({type: "wifiScan"})
ApiKeyValueHelper {
id: scanStatus
deviceConnection: theDeviceConnection
apiKey: "scas"
}
-
- text: {
- switch (scanStatus.value)
- {
- case 0: return "None"
- case 1: return "Scanning"
- case 2: return "Finished"
- case 3: return "Failed"
- default: return scanStatus.value
- }
- }
- }
-
- Text {
- text: qsTr("Scan-Age:")
- }
-
- Text {
- Layout.fillWidth: true
-
- ApiKeyValueHelper {
- id: scanAgeStatus
- deviceConnection: theDeviceConnection
- apiKey: "scaa"
- }
-
- text: scanAgeStatus.value != null ? formatDuration(scanAgeStatus.value - rebootTime.value) : null
}
}
- RowLayout {
- SendMessageButton {
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("No configured networks")
+ visible: configuredRepeater.count === 0
+ color: "gray"
+ }
+
+ Repeater {
+ id: configuredRepeater
+ model: configuredNetworks()
+
+ delegate: WhiteBox {
+ required property var modelData
Layout.fillWidth: true
- text: qsTr("Trigger scan")
- messageType: "wifiScan"
- }
+ implicitHeight: configuredRow.implicitHeight + 20
- Button {
- ApiKeyValueHelper {
- id: wifiScanResult
- deviceConnection: theDeviceConnection
- apiKey: "scan"
- }
+ RowLayout {
+ id: configuredRow
+ anchors.fill: parent
+ anchors.margins: 10
- ApiKeyValueHelper {
- id: configuredWifis
- deviceConnection: theDeviceConnection
- apiKey: "wifis"
- }
+ ColumnLayout {
+ Layout.fillWidth: true
- text: qsTr("(%0) Available networks").arg(wifiScanResult.value == null ? 0 : wifiScanResult.value.length)
- onClicked: stackView.push(wiFiScanPageComponent, {
- wifiScanResult: wifiScanResult,
- configuredWifis: configuredWifis
- })
- enabled: wifiScanResult.value != null
+ Text {
+ Layout.fillWidth: true
+ text: modelData.ssid
+ font.bold: true
+ elide: Text.ElideRight
+ }
- Component {
- id: wiFiScanPageComponent
+ Text {
+ text: modelData.useStaticIp
+ ? qsTr("Static IP %1 • slot %2").arg(modelData.staticIp).arg(modelData.slot + 1)
+ : qsTr("Automatic IP • slot %1").arg(modelData.slot + 1)
+ color: "gray"
+ }
+ }
- WiFiScanPage {
+ Button {
+ text: qsTr("Delete")
+ enabled: !deleteWifi.pending
+ onClicked: {
+ selectedWifiSlot = modelData.slot
+ deleteDialog.networkName = modelData.ssid
+ deleteDialog.open()
+ }
}
}
}
}
+ Text {
+ Layout.fillWidth: true
+ Layout.topMargin: 10
+ text: qsTr("Available networks")
+ font.bold: true
+ }
+
+ Repeater {
+ model: availableNetworks()
+
+ delegate: WhiteItemDelegate {
+ required property var modelData
+ readonly property bool alreadyConfigured: isConfigured(modelData.ssid)
+
+ Layout.fillWidth: true
+ implicitHeight: availableRow.implicitHeight + 20
+ enabled: !alreadyConfigured && !provisionWifi.pending
+ onClicked: selectNetwork(modelData)
+
+ contentItem: RowLayout {
+ id: availableRow
+
+ ColumnLayout {
+ Layout.fillWidth: true
+
+ Text {
+ Layout.fillWidth: true
+ text: modelData.ssid
+ font.bold: true
+ elide: Text.ElideRight
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: alreadyConfigured
+ ? qsTr("Saved • %1").arg(securityName(modelData.encryptionType))
+ : securityName(modelData.encryptionType)
+ color: "gray"
+ elide: Text.ElideRight
+ }
+ }
+
+ Text {
+ text: signalBars(modelData.rssi)
+ font.bold: true
+ Accessible.name: qsTr("Signal strength %1 dBm").arg(modelData.rssi)
+ }
+
+ Text {
+ text: alreadyConfigured ? "✓" : ">"
+ }
+ }
+ }
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("No networks found. Start another scan.")
+ visible: availableNetworks().length === 0
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.Wrap
+ }
+
+ RequestStatusText {
+ Layout.fillWidth: true
+ request: provisionWifi
+ }
+
+ RequestStatusText {
+ Layout.fillWidth: true
+ request: deleteWifi
+ }
+
Button {
ApiKeyValueHelper {
id: wifiErrorLog
@@ -133,7 +434,7 @@ NavigationPage {
}
text: qsTr("(%0) Wi-Fi Errors").arg(wifiErrorLog.value == null ? 0 : wifiErrorLog.value.length)
- onClicked: stackView.push(wiFiErrorsPageComponent, {wifiErrorLog} )
+ onClicked: stackView.push(wiFiErrorsPageComponent, {wifiErrorLog})
enabled: wifiErrorLog.value != null
Component {
@@ -144,10 +445,18 @@ NavigationPage {
}
}
+ Text {
+ Layout.fillWidth: true
+ Layout.topMargin: 10
+ text: qsTr("Advanced Wi-Fi settings")
+ font.bold: true
+ }
+
EditValueItem {
text: qsTr("Min RSSI")
valueText: Qt.locale().toString(value)
apiKey: "wsmr"
+ visible: exists
editableItem: SpinBox {
from: -100
to: 0
@@ -156,11 +465,184 @@ NavigationPage {
GeneralOnOffSwitch {
apiKey: "wspc"
- text: qsTr("Pmf capable")
+ text: qsTr("Support protected management frames")
}
GeneralOnOffSwitch {
apiKey: "wspr"
- text: qsTr("Pmf required")
+ text: qsTr("Require protected management frames")
+ }
+
+ WhiteBox {
+ Layout.fillWidth: true
+ implicitHeight: wifiDetailsColumn.implicitHeight + 20
+ visible: wifiAddressing.exists || wifiAccessPoint.exists || wifiConnectionProcess.exists
+ || wifiMac.exists || wifiHostname.exists || wifiErrorCount.exists
+
+ ColumnLayout {
+ id: wifiDetailsColumn
+ anchors.fill: parent
+ anchors.margins: 10
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("Technical details")
+ font.bold: true
+ }
+
+ ApiStatusRow {
+ id: wifiAddressing
+ apiKey: "ccw"
+ label: qsTr("Addressing")
+ valueFormatter: function(value) {
+ if (value === null || value === undefined)
+ return "—"
+ const parts = []
+ if (value.ip) parts.push(qsTr("IP %1").arg(value.ip))
+ if (value.netmask) parts.push(qsTr("mask %1").arg(value.netmask))
+ if (value.gw) parts.push(qsTr("gateway %1").arg(value.gw))
+ return parts.length > 0 ? parts.join(" • ") : "—"
+ }
+ }
+
+ ApiStatusRow {
+ id: wifiAccessPoint
+ apiKey: "ccw"
+ label: qsTr("Access point")
+ valueFormatter: function(value) {
+ if (value === null || value === undefined)
+ return "—"
+ const channel = value.channel !== undefined ? qsTr("channel %1").arg(value.channel) : ""
+ return [value.bssid, channel].filter(Boolean).join(" • ")
+ }
+ }
+
+ ApiStatusRow {
+ id: wifiConnectionProcess
+ apiKey: "wsms"
+ label: qsTr("Connection process")
+ valueFormatter: function(value) {
+ const names = [qsTr("Idle"), qsTr("Scanning"), qsTr("Connecting"), qsTr("Connected")]
+ return value >= 0 && value < names.length ? names[value] : qsTr("Unknown")
+ }
+ }
+
+ ApiStatusRow {
+ id: wifiMac
+ apiKey: "macs"
+ label: qsTr("MAC address")
+ }
+
+ ApiStatusRow {
+ id: wifiHostname
+ apiKey: "hsts"
+ label: qsTr("Hostname")
+ }
+
+ ApiStatusRow {
+ id: wifiErrorCount
+ apiKey: "wsc"
+ label: qsTr("Connection errors")
+ }
+ }
+ }
+
+ CenteredDialog {
+ id: passwordDialog
+ title: selectedNetwork === null ? qsTr("Connect to Wi-Fi") : selectedNetwork.ssid
+ standardButtons: Dialog.Ok | Dialog.Cancel
+ modal: true
+ focus: true
+
+ onOpened: {
+ passwordInput.forceActiveFocus()
+ standardButton(Dialog.Ok).enabled = !needsPassword(selectedNetwork)
+ }
+ onAccepted: provisionSelectedNetwork()
+
+ contentItem: ColumnLayout {
+ Text {
+ Layout.fillWidth: true
+ text: selectedNetwork === null ? "" : securityName(selectedNetwork.encryptionType)
+ color: "gray"
+ }
+
+ TextField {
+ id: passwordInput
+ Layout.minimumWidth: 240
+ Layout.fillWidth: true
+ visible: selectedNetwork !== null && needsPassword(selectedNetwork)
+ placeholderText: qsTr("Password")
+ echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password
+ maximumLength: 64
+ inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText
+ onTextChanged: {
+ if (passwordDialog.visible)
+ passwordDialog.standardButton(Dialog.Ok).enabled = text.length >= 8
+ }
+ onAccepted: {
+ if (text.length >= 8)
+ passwordDialog.accept()
+ }
+ }
+
+ CheckBox {
+ id: showPassword
+ visible: passwordInput.visible
+ text: qsTr("Show password")
+ }
+
+ Text {
+ Layout.fillWidth: true
+ visible: passwordInput.visible && passwordInput.text.length > 0 && passwordInput.text.length < 8
+ text: qsTr("The password must contain at least 8 characters.")
+ color: "red"
+ wrapMode: Text.Wrap
+ }
+
+ Text {
+ Layout.fillWidth: true
+ visible: selectedNetwork !== null && !needsPassword(selectedNetwork)
+ text: qsTr("This network does not require a password.")
+ wrapMode: Text.Wrap
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: qsTr("The network will be saved in slot %1.").arg(selectedWifiSlot + 1)
+ wrapMode: Text.Wrap
+ }
+ }
+ }
+
+ CenteredDialog {
+ id: deleteDialog
+ property string networkName
+
+ title: qsTr("Delete Wi-Fi network?")
+ standardButtons: Dialog.Yes | Dialog.No
+ modal: true
+ focus: true
+ onAccepted: deleteWifi.setValue(selectedWifiSlot)
+
+ contentItem: Text {
+ text: qsTr("Delete %1 and all of its IP and DNS settings?").arg(deleteDialog.networkName)
+ wrapMode: Text.Wrap
+ }
+ }
+
+ CenteredDialog {
+ id: noSlotDialog
+ property alias text: noSlotText.text
+
+ title: qsTr("Cannot add Wi-Fi network")
+ standardButtons: Dialog.Ok
+ modal: true
+ focus: true
+
+ contentItem: Text {
+ id: noSlotText
+ wrapMode: Text.Wrap
+ }
}
}
diff --git a/evcharger-app/qml/WiFiScanPage.qml b/evcharger-app/qml/WiFiScanPage.qml
deleted file mode 100644
index b2eb331..0000000
--- a/evcharger-app/qml/WiFiScanPage.qml
+++ /dev/null
@@ -1,414 +0,0 @@
-import QtQuick
-import QtQuick.Controls
-import QtQuick.Layouts
-import EVChargerApp
-
-BaseNavigationPage {
- property ApiKeyValueHelper wifiScanResult
- property ApiKeyValueHelper configuredWifis
- property var selectedNetwork: null
- property int selectedWifiSlot: -1
-
- title: qsTr("Wi-Fi networks")
-
- function configuredNetworks() {
- const result = []
- const wifis = configuredWifis === null ? null : configuredWifis.value
- if (wifis === null || wifis === undefined)
- return result
-
- for (let i = 0; i < wifis.length; ++i) {
- const wifi = wifis[i]
- if (wifi !== null && wifi !== undefined && wifi.ssid)
- result.push({slot: i, ssid: wifi.ssid, hasKey: wifi.key === true})
- }
- return result
- }
-
- function isConfigured(ssid) {
- const networks = configuredNetworks()
- for (let i = 0; i < networks.length; ++i) {
- if (networks[i].ssid === ssid)
- return true
- }
- return false
- }
-
- function firstFreeWifiSlot() {
- const wifis = configuredWifis === null ? null : configuredWifis.value
- if (wifis === null || wifis === undefined)
- return -2
-
- for (let i = 0; i < wifis.length; ++i) {
- if (wifis[i] === null || wifis[i] === undefined || !wifis[i].ssid)
- return i
- }
- return -1
- }
-
- function availableNetworks() {
- const result = []
- const scan = wifiScanResult === null ? null : wifiScanResult.value
- if (scan === null || scan === undefined)
- return result
-
- // Android presents an SSID once. Keep the strongest BSSID when the
- // same network was observed through multiple access points.
- for (let i = 0; i < scan.length; ++i) {
- const candidate = scan[i]
- if (candidate === null || candidate === undefined || !candidate.ssid)
- continue
-
- let existing = -1
- for (let j = 0; j < result.length; ++j) {
- if (result[j].ssid === candidate.ssid) {
- existing = j
- break
- }
- }
-
- if (existing < 0)
- result.push(candidate)
- else if ((candidate.rssi ?? -1000) > (result[existing].rssi ?? -1000))
- result[existing] = candidate
- }
-
- result.sort((left, right) => (right.rssi ?? -1000) - (left.rssi ?? -1000))
- return result
- }
-
- function securityName(encryptionType) {
- switch (encryptionType) {
- case 0: return qsTr("Open")
- case 1: return qsTr("WEP")
- case 2: return qsTr("WPA")
- case 3: return qsTr("WPA2")
- case 4: return qsTr("WPA/WPA2")
- case 5: return qsTr("WPA2 Enterprise")
- case 6: return qsTr("WPA3")
- case 7: return qsTr("WPA2/WPA3")
- case 8: return qsTr("WAPI")
- case 9: return qsTr("OWE")
- case 10: return qsTr("WPA3 Enterprise")
- case 11: return qsTr("WPA3 External PSK")
- case 12: return qsTr("WPA3 External PSK mixed")
- default: return qsTr("Unknown security")
- }
- }
-
- function needsPassword(network) {
- return network.encryptionType !== 0 && network.encryptionType !== 9
- }
-
- function signalBars(rssi) {
- if (rssi >= -55)
- return "▂▄▆█"
- if (rssi >= -67)
- return "▂▄▆"
- if (rssi >= -75)
- return "▂▄"
- return "▂"
- }
-
- function selectNetwork(network) {
- if (isConfigured(network.ssid))
- return
-
- const slot = firstFreeWifiSlot()
- if (slot === -2) {
- noSlotDialog.text = qsTr("The Wi-Fi configuration has not been received yet.")
- noSlotDialog.open()
- return
- }
- if (slot < 0) {
- noSlotDialog.text = qsTr("All Wi-Fi configuration slots are occupied. Delete a configured network first.")
- noSlotDialog.open()
- return
- }
-
- selectedNetwork = network
- selectedWifiSlot = slot
- passwordInput.clear()
- passwordDialog.open()
- }
-
- function provisionSelectedNetwork() {
- if (selectedNetwork === null || selectedWifiSlot < 0)
- return
-
- const value = []
- for (let i = 0; i < selectedWifiSlot; ++i)
- value.push(null)
- value.push({
- ssid: selectedNetwork.ssid,
- key: needsPassword(selectedNetwork) ? passwordInput.text : ""
- })
- provisionWifi.setValue(value)
- }
-
- headerItems: RowLayout {
- BusyIndicator {
- visible: provisionWifi.pending || deleteWifi.pending || triggerScan.pending
- }
-
- Button {
- text: qsTr("Scan again")
- enabled: !triggerScan.pending
- onClicked: triggerScan.sendMessage({type: "wifiScan"})
- }
- }
-
- SetValueHelper {
- id: provisionWifi
- deviceConnection: theDeviceConnection
- apiKey: "wifis"
- }
-
- SetValueHelper {
- id: deleteWifi
- deviceConnection: theDeviceConnection
- apiKey: "delw"
- }
-
- SendMessageHelper {
- id: triggerScan
- deviceConnection: theDeviceConnection
- }
-
- Flickable {
- Layout.fillWidth: true
- Layout.fillHeight: true
- clip: true
- contentHeight: networksColumn.implicitHeight
- ScrollBar.vertical: ScrollBar {}
-
- ColumnLayout {
- id: networksColumn
- width: parent.width - 30
- x: 15
- spacing: 5
-
- Text {
- Layout.fillWidth: true
- text: qsTr("Configured networks")
- font.bold: true
- visible: configuredRepeater.count > 0
- }
-
- Repeater {
- id: configuredRepeater
- model: configuredNetworks()
-
- delegate: WhiteBox {
- required property var modelData
- Layout.fillWidth: true
- implicitHeight: configuredRow.implicitHeight + 20
-
- RowLayout {
- id: configuredRow
- anchors.fill: parent
- anchors.margins: 10
-
- ColumnLayout {
- Layout.fillWidth: true
-
- Text {
- Layout.fillWidth: true
- text: modelData.ssid
- font.bold: true
- elide: Text.ElideRight
- }
-
- Text {
- text: qsTr("Saved in slot %1").arg(modelData.slot + 1)
- color: "gray"
- }
- }
-
- Button {
- text: qsTr("Delete")
- enabled: !deleteWifi.pending
- onClicked: {
- selectedWifiSlot = modelData.slot
- deleteDialog.networkName = modelData.ssid
- deleteDialog.open()
- }
- }
- }
- }
- }
-
- Text {
- Layout.fillWidth: true
- Layout.topMargin: configuredRepeater.count > 0 ? 10 : 0
- text: qsTr("Available networks")
- font.bold: true
- }
-
- Repeater {
- model: availableNetworks()
-
- delegate: WhiteItemDelegate {
- required property var modelData
- readonly property bool alreadyConfigured: isConfigured(modelData.ssid)
-
- Layout.fillWidth: true
- implicitHeight: availableRow.implicitHeight + 20
- enabled: !alreadyConfigured && !provisionWifi.pending
- onClicked: selectNetwork(modelData)
-
- contentItem: RowLayout {
- id: availableRow
-
- ColumnLayout {
- Layout.fillWidth: true
-
- Text {
- Layout.fillWidth: true
- text: modelData.ssid
- font.bold: true
- elide: Text.ElideRight
- }
-
- Text {
- Layout.fillWidth: true
- text: alreadyConfigured
- ? qsTr("Saved • %1").arg(securityName(modelData.encryptionType))
- : securityName(modelData.encryptionType)
- color: "gray"
- elide: Text.ElideRight
- }
- }
-
- Text {
- text: signalBars(modelData.rssi)
- font.bold: true
- Accessible.name: qsTr("Signal strength %1 dBm").arg(modelData.rssi)
- }
-
- Text {
- text: alreadyConfigured ? "✓" : ">"
- }
- }
- }
- }
-
- Text {
- Layout.fillWidth: true
- text: qsTr("No networks found. Start another scan.")
- visible: availableNetworks().length === 0
- horizontalAlignment: Text.AlignHCenter
- wrapMode: Text.Wrap
- }
-
- RequestStatusText {
- Layout.fillWidth: true
- request: provisionWifi
- }
-
- RequestStatusText {
- Layout.fillWidth: true
- request: deleteWifi
- }
- }
- }
-
- CenteredDialog {
- id: passwordDialog
- title: selectedNetwork === null ? qsTr("Connect to Wi-Fi") : selectedNetwork.ssid
- standardButtons: Dialog.Ok | Dialog.Cancel
- modal: true
- focus: true
-
- onOpened: {
- passwordInput.forceActiveFocus()
- standardButton(Dialog.Ok).enabled = !needsPassword(selectedNetwork)
- }
- onAccepted: provisionSelectedNetwork()
-
- contentItem: ColumnLayout {
- Text {
- Layout.fillWidth: true
- text: selectedNetwork === null ? "" : securityName(selectedNetwork.encryptionType)
- color: "gray"
- }
-
- TextField {
- id: passwordInput
- Layout.minimumWidth: 240
- Layout.fillWidth: true
- visible: selectedNetwork !== null && needsPassword(selectedNetwork)
- placeholderText: qsTr("Password")
- echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password
- maximumLength: 64
- inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText
- onTextChanged: {
- if (passwordDialog.visible)
- passwordDialog.standardButton(Dialog.Ok).enabled = text.length >= 8
- }
- onAccepted: {
- if (text.length >= 8)
- passwordDialog.accept()
- }
- }
-
- CheckBox {
- id: showPassword
- visible: passwordInput.visible
- text: qsTr("Show password")
- }
-
- Text {
- Layout.fillWidth: true
- visible: passwordInput.visible && passwordInput.text.length > 0 && passwordInput.text.length < 8
- text: qsTr("The password must contain at least 8 characters.")
- color: "red"
- wrapMode: Text.Wrap
- }
-
- Text {
- Layout.fillWidth: true
- visible: selectedNetwork !== null && !needsPassword(selectedNetwork)
- text: qsTr("This network does not require a password.")
- wrapMode: Text.Wrap
- }
-
- Text {
- Layout.fillWidth: true
- text: qsTr("The network will be saved in slot %1.").arg(selectedWifiSlot + 1)
- wrapMode: Text.Wrap
- }
- }
- }
-
- CenteredDialog {
- id: deleteDialog
- property string networkName
-
- title: qsTr("Delete Wi-Fi network?")
- standardButtons: Dialog.Yes | Dialog.No
- modal: true
- focus: true
- onAccepted: deleteWifi.setValue(selectedWifiSlot)
-
- contentItem: Text {
- text: qsTr("Delete %1 and all of its IP and DNS settings?").arg(deleteDialog.networkName)
- wrapMode: Text.Wrap
- }
- }
-
- CenteredDialog {
- id: noSlotDialog
- property alias text: noSlotText.text
-
- title: qsTr("Cannot add Wi-Fi network")
- standardButtons: Dialog.Ok
- modal: true
- focus: true
-
- contentItem: Text {
- id: noSlotText
- wrapMode: Text.Wrap
- }
- }
-}
diff --git a/evcharger-app/qml/qmldir b/evcharger-app/qml/qmldir
index 22e767f..a9518d6 100644
--- a/evcharger-app/qml/qmldir
+++ b/evcharger-app/qml/qmldir
@@ -6,6 +6,8 @@ EVChargerApp 1.0 AddDeviceScreen.qml
EVChargerApp 1.0 AnimatedStackLayout.qml
EVChargerApp 1.0 AnimatedStackView.qml
EVChargerApp 1.0 ApiKeyValueItem.qml
+EVChargerApp 1.0 ApiStatusRow.qml
+EVChargerApp 1.0 ApiEnumEditItem.qml
EVChargerApp 1.0 ApiSettingsPage.qml
EVChargerApp 1.0 AppInstance.qml
EVChargerApp 1.0 AppSettingsPage.qml
@@ -91,4 +93,3 @@ EVChargerApp 1.0 WhiteItemDelegate.qml
EVChargerApp 1.0 WhiteSwipeDelegate.qml
EVChargerApp 1.0 WiFiErrorsPage.qml
EVChargerApp 1.0 WiFiPage.qml
-EVChargerApp 1.0 WiFiScanPage.qml