From d88844feba83d6d0552c757c69b269b17279d577 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 10 Oct 2022 23:28:15 +0200 Subject: [PATCH] More air quality sensors --- libnymea-app/types/types.cpp | 4 + libnymea-app/types/types.h | 2 + nymea-app/images.qrc | 5 + nymea-app/main.cpp | 1 + nymea-app/resources.qrc | 1 + nymea-app/ui/Nymea.qml | 14 + nymea-app/ui/StyleBase.qml | 8 +- .../devicelistpages/SensorsDeviceListPage.qml | 5 + nymea-app/ui/devicepages/SensorDevicePage.qml | 215 ++++++++++++++- nymea-app/ui/images/sensors/co.svg | 55 ++-- nymea-app/ui/images/sensors/co2.svg | 55 ++-- nymea-app/ui/images/sensors/no2.svg | 161 ++++++++++++ nymea-app/ui/images/sensors/o3.svg | 161 ++++++++++++ nymea-app/ui/images/sensors/pm10.svg | 161 ++++++++++++ nymea-app/ui/images/sensors/pm25.svg | 161 ++++++++++++ nymea-app/ui/images/sensors/voc.svg | 161 ++++++++++++ nymea-app/ui/utils/AirQualityIndex.qml | 244 ++++++++++++++++++ 17 files changed, 1355 insertions(+), 59 deletions(-) create mode 100644 nymea-app/ui/images/sensors/no2.svg create mode 100644 nymea-app/ui/images/sensors/o3.svg create mode 100644 nymea-app/ui/images/sensors/pm10.svg create mode 100644 nymea-app/ui/images/sensors/pm25.svg create mode 100644 nymea-app/ui/images/sensors/voc.svg create mode 100644 nymea-app/ui/utils/AirQualityIndex.qml diff --git a/libnymea-app/types/types.cpp b/libnymea-app/types/types.cpp index d36c2bce..2964297a 100644 --- a/libnymea-app/types/types.cpp +++ b/libnymea-app/types/types.cpp @@ -182,6 +182,8 @@ QString Types::toUiUnit(Types::Unit unit) const return "%"; case Types::UnitPartsPerMillion: return "ppm"; + case Types::UnitPartsPerBillion: + return "ppb"; case Types::UnitEuro: return "€"; case Types::UnitDollar: @@ -238,6 +240,8 @@ QString Types::toUiUnit(Types::Unit unit) const return "l"; case Types::UnitFluidOunce: return "fl oz"; + case Types::UnitMicroGrammPerCubicalMeter: + return "µg/m³"; } return ""; diff --git a/libnymea-app/types/types.h b/libnymea-app/types/types.h index 60609ef9..99bc4447 100644 --- a/libnymea-app/types/types.h +++ b/libnymea-app/types/types.h @@ -95,6 +95,7 @@ public: UnitEuroCentPerKiloWattHour, UnitPercentage, UnitPartsPerMillion, + UnitPartsPerBillion, UnitEuro, UnitDollar, UnitHertz, @@ -112,6 +113,7 @@ public: UnitRpm, UnitMilligramPerLiter, UnitLiter, + UnitMicroGrammPerCubicalMeter, // Those do not exist in nymea:core at this point, Adding them for easier conversion to imperial UnitDegreeFahrenheit, diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 6a5b4b5b..a5c0392c 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -289,5 +289,10 @@ ui/images/zigbee/zigbee-sibling.svg ui/images/zigbee/zigbee-previous-child.svg ui/images/arrow-down.svg + ui/images/sensors/voc.svg + ui/images/sensors/pm25.svg + ui/images/sensors/o3.svg + ui/images/sensors/pm10.svg + ui/images/sensors/no2.svg diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index 2e2da306..a0fce137 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -174,6 +174,7 @@ int main(int argc, char *argv[]) qmlRegisterSingletonType("Nymea", 1, 0, "PushNotifications", PushNotifications::pushNotificationsProvider); qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" ); + qmlRegisterSingletonType(QUrl("qrc:///ui/utils/AirQualityIndex.qml"), "Nymea", 1, 0, "AirQualityIndex" ); qmlRegisterType("Nymea", 1, 0, "DashboardModel"); qmlRegisterUncreatableType("Nymea", 1, 0, "DashboardItem", ""); diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 1b08b19d..dc9eeaa6 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -278,5 +278,6 @@ ui/system/zwave/ZWaveNetworkSettingsPage.qml ui/components/ActivityIndicator.qml ui/system/zigbee/ZigbeeNodePage.qml + ui/utils/AirQualityIndex.qml diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 68f64d4d..182bea8e 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -198,8 +198,12 @@ ApplicationWindow { return qsTr("CO level") case "co2sensor": return qsTr("CO2 level") + case "no2sensor": + return qsTr("Nitrogen dioxide leve") case "gassensor": return qsTr("Flammable gas level") + case "vocsensor": + return qsTr("VOC level") case "inputtrigger": return qsTr("Incoming Events"); case "outputtrigger": @@ -298,6 +302,16 @@ ApplicationWindow { return Qt.resolvedUrl("images/sensors/co.svg") case "co2sensor": return Qt.resolvedUrl("images/sensors/co2.svg") + case "no2sensor": + return Qt.resolvedUrl("images/sensors/no2.svg") + case "o3sensor": + return Qt.resolvedUrl("images/sensors/o3.svg") + case "vocsensor": + return Qt.resolvedUrl("images/sensors/voc.svg") + case "pm10sensor": + return Qt.resolvedUrl("images/sensors/pm10.svg") + case "pm25sensor": + return Qt.resolvedUrl("images/sensors/pm25.svg") case "gassensor": return Qt.resolvedUrl("images/sensors/gas.svg") case "daylightsensor": diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml index f145ffdd..d73e83dc 100644 --- a/nymea-app/ui/StyleBase.qml +++ b/nymea-app/ui/StyleBase.qml @@ -84,6 +84,7 @@ Item { property color blue: "#5c95cd" property color purple: "#955ccd" property color rose: "#cd5c95" + property color darkGreen: "#5ccd5e" // Icon/graph colors for various interfaces property var interfaceColors: { @@ -96,6 +97,8 @@ Item { "noisesensor": purple, "cosensor": darkGray, "co2sensor": turquoise, + "pm10sensor": lightGray, + "pm25sensor": gray, "gassensor": orange, "daylightsensor": yellow, "presencesensor": darkBlue, @@ -117,7 +120,10 @@ Item { "orpsensor": yellow, "powersocket": lime, "evcharger": lime, - "energystorage": lime + "energystorage": lime, + "vocsensor": green, + "o3sensor": blue, + "no2sensor": purple } property var stateColors: { diff --git a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml index 7c3cd81d..b15c74df 100644 --- a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml @@ -94,6 +94,11 @@ ThingsListPageBase { ListElement { interfaceName: "o2sensor"; stateName: "o2saturation" } ListElement { interfaceName: "phsensor"; stateName: "ph" } ListElement { interfaceName: "orpsensor"; stateName: "orp" } + ListElement { interfaceName: "vocsensor"; stateName: "voc" } + ListElement { interfaceName: "pm10sensor"; stateName: "pm10" } + ListElement { interfaceName: "pm25sensor"; stateName: "pm25" } + ListElement { interfaceName: "no2sensor"; stateName: "no2" } + ListElement { interfaceName: "o3sensor"; stateName: "o3" } } delegate: RowLayout { diff --git a/nymea-app/ui/devicepages/SensorDevicePage.qml b/nymea-app/ui/devicepages/SensorDevicePage.qml index 89e746c9..c02afa51 100644 --- a/nymea-app/ui/devicepages/SensorDevicePage.qml +++ b/nymea-app/ui/devicepages/SensorDevicePage.qml @@ -57,9 +57,13 @@ ThingPageBase { "waterlevelsensor": "waterLevel", "phsensor": "ph", "o2sensor": "o2saturation", + "o3sensor": "o3", "orpsensor": "orp", - "airquality": "airQuality", - "indoorairquality": "indoorAirQuality" + "vocsensor": "voc", + "cosensor": "co", + "pm10sensor": "pm10", + "pm25sensor": "pm25", + "no2sensor": "no2" } ListModel { @@ -141,25 +145,29 @@ ThingPageBase { if (["temperaturesensor"].indexOf(modelData) >= 0) { return Types.toUiValue(-50, Types.UnitDegreeCelsius) } + if (["pressuresensor"].indexOf(modelData) >= 0) { + return Types.toUiValue(900, Types.UnitMilliBar) + } + return state.minValue } property var maxValue: { if (["temperaturesensor"].indexOf(modelData) >= 0) { return Types.toUiValue(50, Types.UnitDegreeCelsius) } + if (["pressuresensor"].indexOf(modelData) >= 0) { + return Types.toUiValue(1100, Types.UnitMilliBar) + } return state.maxValue } sourceComponent: { - var handledInterfaces = [ + var progressInterfaces = [ "humiditysensor", "o2sensor", "temperaturesensor", "moisturesensor", - "co2sensor", "conductivitysensor", - "cosensor", - "co2sensor", "gassensor", "lightsensor", "orpsensor", @@ -168,9 +176,21 @@ ThingPageBase { "waterlevelsensor", "windspeedsensor" ] - if (handledInterfaces.indexOf(modelData) >= 0) { + if (progressInterfaces.indexOf(modelData) >= 0) { return progressComponent } + var scaleInterfaces = [ + "vocsensor", + "cosensor", + "co2sensor", + "o3sensor", + "pm10sensor", + "pm25sensor", + "no2sensor" + ] + if (scaleInterfaces.indexOf(modelData) >= 0) { + return scaleComponent + } } } } @@ -344,11 +364,13 @@ ThingPageBase { Label { anchors.centerIn: parent + anchors.verticalCenter: -Style.smallMargins width: parent.width * 0.6 text: Types.toUiValue(progressCanvas.state.value, progressCanvas.stateType.unit).toFixed(1) + " " + Types.toUiUnit(progressCanvas.stateType.unit) - font.pixelSize: Math.min(Style.hugeFont.pixelSize, parent.height / 6) + font.pixelSize: Math.min(Style.largeFont.pixelSize, parent.height / 6) wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter + maximumLineCount: 2 } onPaint: { @@ -383,8 +405,9 @@ ThingPageBase { } ColorIcon { - anchors.centerIn: parent - anchors.verticalCenterOffset: progressCanvas.height / 2 - height + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.smallMargins name: app.interfaceToIcon(progressCanvas.interfaceName) size: Math.min(Style.bigIconSize, parent.height / 5) color: app.interfaceToColor(progressCanvas.interfaceName) @@ -392,5 +415,177 @@ ThingPageBase { } } + Component { + id: scaleComponent + Canvas { + id: scaleCanvas + property string interfaceName: parent.interfaceName + property StateType stateType: parent.stateType + property State state: parent.state + property var minValue: parent.minValue + property var maxValue: parent.maxValue + + property int scaleWidth: width * .1 + + property var scale: { + switch (interfaceName) { + case "vocsensor": + return AirQualityIndex.iaqVoc + case "cosensor": + return AirQualityIndex.caqiCo + case "o3sensor": + return AirQualityIndex.caqiO3 + case "pm10sensor": + return AirQualityIndex.caqiPm10 + case "pm25sensor": + return AirQualityIndex.caqiPm25 + case "no2sensor": + return AirQualityIndex.caqiNo2 + } + return baseScale + } + property var baseScale: [ + { + "value": maxValue, + "angle": 270, + "color": Style.tileOverlayColor + } + ] + + property var currentIndex: { + for (var i = 0; i < scale.length; i++) { + if (state.value <= scale[i].value) { + return i; + } + } + log.warn("Value out of scale!") + return -1 + } + + property double angle: { + var baseAngle = 0 + var baseValue = 0; + if (currentIndex > 0) { + baseAngle = scale[currentIndex-1].angle + baseValue = scale[currentIndex-1].value + } + var valueRange = scale[currentIndex].value - baseValue + var angleRange = scale[currentIndex].angle - baseAngle + var progress = (state.value - baseValue) / (scale[currentIndex].value - baseValue) + return baseAngle + angleRange * progress + } + Behavior on angle { NumberAnimation { duration: Style.slowAnimationDuration; easing.type: Easing.InOutQuad } } + onAngleChanged: requestPaint(); + + ColumnLayout { + anchors.centerIn: parent + anchors.verticalCenterOffset: -Style.smallMargins + width: parent.width * 0.6 + + Label { + Layout.fillWidth: true + text: scaleCanvas.scale[scaleCanvas.currentIndex].text + font.pixelSize: Math.min(Style.largeFont.pixelSize, scaleCanvas.height / 6) + wrapMode: Text.WordWrap +// color: scaleCanvas.scale[scaleCanvas.currentIndex].color + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 2 + } + Label { + Layout.fillWidth: true + text: Types.toUiValue(scaleCanvas.state.value, scaleCanvas.stateType.unit).toFixed(1) + " " + Types.toUiUnit(scaleCanvas.stateType.unit) + font.pixelSize: Math.min(Style.smallFont.pixelSize, scaleCanvas.height / 12) + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + ctx.beginPath() + ctx.fillStyle = Style.foregroundColor + + ctx.translate(width / 2, height / 2) + ctx.rotate(135 * Math.PI / 180) + + ctx.lineCap = "round" + ctx.lineWidth = scaleCanvas.scaleWidth + + // paint first rounded + ctx.beginPath() + ctx.strokeStyle = scaleCanvas.scale[0].color + var startAngle = 0 + var endAngle = scaleCanvas.scale[0].angle * Math.PI/180 + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, startAngle, endAngle) + ctx.stroke() + ctx.closePath() + + // paint last rounded + ctx.beginPath() + ctx.strokeStyle = scaleCanvas.scale[scaleCanvas.scale.length - 1].color + startAngle = scaleCanvas.scale[scaleCanvas.scale.length - 2].angle * Math.PI/180 + endAngle = scaleCanvas.scale[scaleCanvas.scale.length - 1].angle * Math.PI/180 + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, startAngle, endAngle) + ctx.stroke() + ctx.closePath() + + // paint inner parts + ctx.lineCap = "butt" + for (var i = 1; i < scaleCanvas.scale.length - 1; i++) { + ctx.beginPath() + ctx.strokeStyle = scaleCanvas.scale[i].color + startAngle = scaleCanvas.scale[i - 1].angle * Math.PI/180 + endAngle = scaleCanvas.scale[i].angle * Math.PI/180 + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, startAngle, endAngle) + ctx.stroke() + ctx.closePath() + } + + var tipRadius = width / 2 - scaleCanvas.scaleWidth + var baseRadius = tipRadius - scaleCanvas.scaleWidth / 2 + + var tipX = tipRadius * Math.cos(scaleCanvas.angle * Math.PI/180) + var tipY = tipRadius * Math.sin(scaleCanvas.angle * Math.PI/180) + + var base1X = baseRadius * Math.cos((scaleCanvas.angle - 5) * Math.PI/180) + var base1Y = baseRadius * Math.sin((scaleCanvas.angle - 5) * Math.PI/180) + var base2X = baseRadius * Math.cos((scaleCanvas.angle + 5) * Math.PI/180) + var base2Y = baseRadius * Math.sin((scaleCanvas.angle + 5) * Math.PI/180) + + + ctx.lineWidth = 1 + ctx.fillStyle = Style.foregroundColor + ctx.beginPath() + ctx.moveTo(tipX, tipY) + ctx.lineTo(base1X, base1Y) + ctx.lineTo(base2X, base2Y) + ctx.moveTo(tipX, tipY) + ctx.stroke() + ctx.fill() + ctx.closePath() + + +// ctx.beginPath() +// ctx.strokeStyle = app.interfaceToColor(progressCanvas.interfaceName) +// radEnd *= progressCanvas.progress +// ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) +// ctx.stroke() +// ctx.closePath() + } + + ColorIcon { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.smallMargins +// anchors.verticalCenterOffset: scaleCanvas.height / 2 - height + name: app.interfaceToIcon(scaleCanvas.interfaceName) + size: Math.min(Style.bigIconSize, parent.height / 5) + color: app.interfaceToColor(scaleCanvas.interfaceName) + } + } + } } diff --git a/nymea-app/ui/images/sensors/co.svg b/nymea-app/ui/images/sensors/co.svg index 874e90d7..44750253 100644 --- a/nymea-app/ui/images/sensors/co.svg +++ b/nymea-app/ui/images/sensors/co.svg @@ -2,20 +2,20 @@ + sodipodi:docname="co.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + inkscape:guide-bbox="true" + inkscape:pagecheckerboard="0" + inkscape:window-width="1462" + inkscape:window-height="933" + inkscape:window-x="74" + inkscape:window-y="27" + inkscape:window-maximized="1"> image/svg+xml - @@ -134,21 +139,23 @@ height="90" x="174" y="1700.3622" /> - - + CO diff --git a/nymea-app/ui/images/sensors/co2.svg b/nymea-app/ui/images/sensors/co2.svg index 874e90d7..27a0e6d0 100644 --- a/nymea-app/ui/images/sensors/co2.svg +++ b/nymea-app/ui/images/sensors/co2.svg @@ -2,20 +2,20 @@ + sodipodi:docname="co2.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + inkscape:guide-bbox="true" + inkscape:pagecheckerboard="0" + inkscape:window-width="1462" + inkscape:window-height="933" + inkscape:window-x="74" + inkscape:window-y="27" + inkscape:window-maximized="1"> image/svg+xml - @@ -134,21 +139,23 @@ height="90" x="174" y="1700.3622" /> - - + CO2 diff --git a/nymea-app/ui/images/sensors/no2.svg b/nymea-app/ui/images/sensors/no2.svg new file mode 100644 index 00000000..494fe508 --- /dev/null +++ b/nymea-app/ui/images/sensors/no2.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + NO2 + + + diff --git a/nymea-app/ui/images/sensors/o3.svg b/nymea-app/ui/images/sensors/o3.svg new file mode 100644 index 00000000..28a55fe1 --- /dev/null +++ b/nymea-app/ui/images/sensors/o3.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + O3 + + + diff --git a/nymea-app/ui/images/sensors/pm10.svg b/nymea-app/ui/images/sensors/pm10.svg new file mode 100644 index 00000000..bcfa4fea --- /dev/null +++ b/nymea-app/ui/images/sensors/pm10.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + PM10 + + + diff --git a/nymea-app/ui/images/sensors/pm25.svg b/nymea-app/ui/images/sensors/pm25.svg new file mode 100644 index 00000000..6d637c86 --- /dev/null +++ b/nymea-app/ui/images/sensors/pm25.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + PM2.5 + + + diff --git a/nymea-app/ui/images/sensors/voc.svg b/nymea-app/ui/images/sensors/voc.svg new file mode 100644 index 00000000..408908d3 --- /dev/null +++ b/nymea-app/ui/images/sensors/voc.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + VOC + + + diff --git a/nymea-app/ui/utils/AirQualityIndex.qml b/nymea-app/ui/utils/AirQualityIndex.qml new file mode 100644 index 00000000..3c3b7d75 --- /dev/null +++ b/nymea-app/ui/utils/AirQualityIndex.qml @@ -0,0 +1,244 @@ +pragma Singleton +import QtQuick 2.9 +import Nymea 1.0 + +Item { + id: root + + property var caqiPm10: [ + { + "value": 25, + "angle": 54, + "text": qsTr("Very low"), + "color": Style.blue + }, + { + "value": 50, + "angle": 108, + "text": qsTr("Low"), + "color": Style.green + }, + { + "value": 90, + "angle": 162, + "text": qsTr("Medium"), + "color": Style.yellow + }, + { + "value": 180, + "angle": 216, + "text": qsTr("High"), + "color": Style.orange + }, + { + "value": 500, + "angle": 270, + "text": qsTr("Very high"), + "color": Style.red + } + ] + + property var caqiPm25: [ + { + "value": 15, + "angle": 54, + "text": qsTr("Very low"), + "color": Style.blue + }, + { + "value": 30, + "angle": 108, + "text": qsTr("Low"), + "color": Style.green + }, + { + "value": 55, + "angle": 162, + "text": qsTr("Medium"), + "color": Style.yellow + }, + { + "value": 110, + "angle": 216, + "text": qsTr("High"), + "color": Style.orange + }, + { + "value": 500, + "angle": 270, + "text": qsTr("Very high"), + "color": Style.red + } + ] + + property var caqiO3: [ + { + "value": 60, + "angle": 54, + "text": qsTr("Very low"), + "color": Style.blue + }, + { + "value": 120, + "angle": 108, + "text": qsTr("Low"), + "color": Style.green + }, + { + "value": 180, + "angle": 162, + "text": qsTr("Medium"), + "color": Style.yellow + }, + { + "value": 240, + "angle": 216, + "text": qsTr("High"), + "color": Style.orange + }, + { + "value": 500, + "angle": 270, + "text": qsTr("Very high"), + "color": Style.red + } + ] + + property var caqiNo2: [ + { + "value": 50, + "angle": 54, + "text": qsTr("Very low"), + "color": Style.blue + }, + { + "value": 100, + "angle": 108, + "text": qsTr("Low"), + "color": Style.green + }, + { + "value": 200, + "angle": 162, + "text": qsTr("Medium"), + "color": Style.yellow + }, + { + "value": 400, + "angle": 216, + "text": qsTr("High"), + "color": Style.orange + }, + { + "value": 800, + "angle": 270, + "text": qsTr("Very high"), + "color": Style.red + } + ] + + property var caqiCo: [ + { + "value": 5, + "angle": 54, + "text": qsTr("Very low"), + "color": Style.blue + }, + { + "value": 7.5, + "angle": 108, + "text": qsTr("Low"), + "color": Style.green + }, + { + "value": 10, + "angle": 162, + "text": qsTr("Medium"), + "color": Style.yellow + }, + { + "value": 20, + "angle": 216, + "text": qsTr("High"), + "color": Style.orange + }, + { + "value": 50, + "angle": 270, + "text": qsTr("Very high"), + "color": Style.red + } + ] + + property var iaqVoc: [ + { + "value": 65, + "angle": 54, + "text": qsTr("Excellent"), + "color": Style.blue + }, + { + "value": 220, + "angle": 108, + "text": qsTr("Good"), + "color": Style.green + }, + { + "value": 660, + "angle": 162, + "text": qsTr("Moderate"), + "color": Style.yellow + }, + { + "value": 2200, + "angle": 216, + "text": qsTr("Poor"), + "color": Style.orange + }, + { + "value": 65535, + "angle": 270, + "text": qsTr("Unhealthy"), + "color": Style.red + } + ] + + property var epaAqiO3: [ + { + "value": 54, + "angle": 54, + "text": qsTr("Good"), + "color": Style.blue + }, + { + "value": 70, + "angle": 108, + "text": qsTr("Moderate"), + "color": Style.blue + }, + { + "value": 85, + "angle": 162, + "text": qsTr("Unhealthy for sensitive groups"), + "color": Style.yellow + }, + { + "value": 105, + "angle": 216, + "text": qsTr("Unhealthy"), + "color": Style.orange + }, + { + "value": 200, + "angle": 270, + "text": qsTr("Very unhealthy"), + "color": Style.red + }, + { + "value": 200, + "angle": 270, + "text": qsTr("Hazardeous"), + "color": Style.purple + } + ] +}