on xenial help sign in
Add battery state led indicators. (#226)

* Add battery state led indicators. Using already existing code from Unity the led is now also used to indicate battery states full/charging/low. Unread notifications remain highest priority. Monitoring battery level and charger state is done in two ways: - as in system-settings\plugins\battery\PageComponent.qml. (dbus signals from com.canonical.indicator.power) - using unity Wizard Status plugin (dbus signals from org.freedesktop.UPower) The led itself is still controlled using the unity Lights plugin.

commitd900105c7958f87b0309817f977aa0481aca57ac authorWillem-Jan de Hoog <wdehoog@exalondelft.nl> dateWed 11 Dec 2019, 17:04 (7 years ago) parentedcd710
2 files changed+316−16
qml/Panel/Indicators/IndicatorsLight.qml+163−9 tests/qmltests/Panel/Indicators/tst_IndicatorsLight.qml+153−7
qml/Panel/Indicators/IndicatorsLight.qml+163−9
@@ -1,5 +1,6 @@
11/*
22 * Copyright 2014 Canonical Ltd.
3+ * Copyright 2019 UBports Foundation
34 *
45 * This program is free software; you can redistribute it and/or modify
56 * it under the terms of the GNU Lesser General Public License as published by
@@ -22,12 +23,140 @@ import Powerd 0.1
2223import Lights 0.1
2324import QMenuModel 0.1 as QMenuModel
2425import Unity.Indicators 0.1 as Indicators
26+import Wizard 0.1
2527
2628QtObject {
2729 id: root
2830
31+ property string indicatorState: "INDICATOR_OFF"
32+
2933 property color color: "darkgreen"
34+ property int onMillisec: 1000
35+ property int offMillisec: 3000
36+
37+ property double batteryLevel: 0
38+ property string deviceState: ""
39+ property bool hasMessages: false
40+
41+ property string batteryIconName: Status.batteryIcon
42+ property string displayStatus: Powerd.status
43+
44+ onDisplayStatusChanged: {
45+ updateLightState("onDisplayStatusChanged")
46+ }
47+
48+ onBatteryIconNameChanged: {
49+ updateLightState("onBatteryIconNameChanged")
50+ }
51+
52+ function updateLightState(msg) {
53+ console.log("updateLightState: " + msg + ", hasMessages: " + hasMessages + ", icon: "
54+ + batteryIconName + ", displayStatus: " + displayStatus + ", deviceState: "
55+ + deviceState + ", batteryLevel: " + batteryLevel)
56+
57+ //
58+ // priorities:
59+ // unread messsages (highest), full&charging, charging, low
60+ //
61+ // Icons: (see device.s from indicator-power)
62+ // %s-empty-symbolic empty
63+ // %s-caution-charging-symbolic charging [ 0..10)
64+ // %s-low-charging-symbolic charging [10..30)
65+ // %s-good-charging-symbolic charging [30..60)
66+ // %s-full-charging-symbolic charging [60..100]
67+ // %s-full-charged-symbolic fully charged
68+ // %s-low-symbolic ?
69+ // %s-full-symbolic ?
70+ //
71+ // device-state: (see system-settings\plugins\battery)
72+ // fully-charged
73+ // charging
74+ // discharging
75+ //
76+ // Indicators:
77+ // unread notifications : darkgreen pulsing (1000/3000)
78+ // charging : white continuously
79+ // battery full : green continuously
80+ // battery low : orangered pulsing (500/3000)
81+ //
82+ // Notes:
83+ // Icon name 'full-charged' comes late (45m after 100%)
84+ // so also check device-state and battery-level
85+ //
86+ // Battery low warning dialog on screen shows up at 10%
87+ // but 'caution' icon at 9%.
88+ //
89+
90+ // only show led when display is off
91+ if (displayStatus == Powerd.On) {
92+ indicatorState = "INDICATOR_OFF"
93+ return
94+ }
95+
96+ // unread messsages have highest priority
97+ if (hasMessages) {
98+ indicatorState = "HAS_MESSAGES"
99+ return
100+ }
101+
102+ var isCharging = batteryIconName.indexOf("charging") >= 0
103+ || deviceState == "charging"
104+ || deviceState == "fully-charged"
105+
106+ var isFull = batteryIconName.indexOf("full-charged") >= 0
107+ || deviceState == "fully-charged"
108+ || batteryLevel >= 100
109+
110+ if (isCharging && isFull) {
111+ indicatorState = "BATTERY_FULL"
112+ } else if (isCharging) {
113+ indicatorState = "BATTERY_CHARGING"
114+ } else if (batteryIconName.indexOf("caution") >= 0
115+ || batteryIconName.indexOf("empty") >= 0) {
116+ indicatorState = "BATTERY_LOW"
117+ } else {
118+ indicatorState = "INDICATOR_OFF"
119+ }
120+ }
121+
122+ onIndicatorStateChanged: {
123+ console.log("onIndicatorStateChange: " + indicatorState)
30124
125+ switch (indicatorState) {
126+ case "INDICATOR_OFF":
127+ break;
128+ case "HAS_MESSAGES":
129+ color = "darkgreen"
130+ onMillisec = 1000
131+ offMillisec = 3000
132+ break;
133+ case "BATTERY_FULL":
134+ color = "green"
135+ onMillisec = 1000
136+ offMillisec = 0
137+ break;
138+ case "BATTERY_CHARGING":
139+ color = "white"
140+ onMillisec = 1000
141+ offMillisec = 0
142+ break;
143+ case "BATTERY_LOW":
144+ color = "orangered"
145+ onMillisec = 500
146+ offMillisec = 3000
147+ break;
148+ default:
149+ console.log("ERROR onIndicatorStateChange: unknown state: " + indicatorState)
150+ break;
151+ }
152+
153+ // HACK: led is only updated after turn on so first always turn off
154+ Lights.state = Lights.Off
155+ if (indicatorState != "INDICATOR_OFF")
156+ Lights.state = Lights.On
157+ }
158+
159+ // Existence of unread notifications is determined by checking for a specific icon name in a dbus signal.
31160 property var _actionGroup: QMenuModel.QDBusActionGroup {
32161 busType: 1
33162 busName: "com.canonical.indicator.messages"
@@ -38,24 +167,49 @@ QtObject {
38167 actionGroup: _actionGroup
39168 actionName: "messages"
40169 Component.onCompleted: actionGroup.start()
41−
42170 property bool hasMessages: valid && (String(icons).indexOf("indicator-messages-new") != -1)
171+ onHasMessagesChanged: {
172+ root.hasMessages = hasMessages
173+ updateLightState("onHasMessagesChanged")
174+ }
43175 }
44176
45− Component.onDestruction: Lights.state = Lights.Off
46−
47− // QtObject does not have children
48− property var _binding: Binding {
49− target: Lights
50− property: "state"
51− value: {
52− return (Powerd.status === Powerd.Off && _rootState.hasMessages) ? Lights.On : Lights.Off
177+ // Charging state and battery level are determined by listening to dbus signals from upower.
178+ // See also system-settings battery plugin.
179+ property var _ipag: QMenuModel.QDBusActionGroup {
180+ busType: 1
181+ busName: "com.canonical.indicator.power"
182+ objectPath: "/com/canonical/indicator/power"
183+ property variant batteryLevel: action("battery-level").state
184+ property variant deviceState: action("device-state").state
185+ Component.onCompleted: start()
186+ onBatteryLevelChanged: {
187+ root.batteryLevel = batteryLevel
188+ updateLightState("onBatteryLevelChanged")
189+ }
190+ onDeviceStateChanged: {
191+ root.deviceState = deviceState
192+ updateLightState("onDeviceStateChanged")
53193 }
54194 }
55195
196+ Component.onDestruction: Lights.state = Lights.Off
197+
56198 property var _colorBinding: Binding {
57199 target: Lights
58200 property: "color"
59201 value: root.color
60202 }
203+
204+ property var _onMillisecBinding: Binding {
205+ target: Lights
206+ property: "onMillisec"
207+ value: root.onMillisec
208+ }
209+
210+ property var _offMillisecBinding: Binding {
211+ target: Lights
212+ property: "offMillisec"
213+ value: root.offMillisec
214+ }
61215}
tests/qmltests/Panel/Indicators/tst_IndicatorsLight.qml+153−7
@@ -101,6 +101,51 @@ Item {
101101 }
102102 }
103103
104+ // come from Wizard Status plugin
105+ property var batteryIconNames: {
106+ "fullyCharged" : "fully-charged",
107+ "charging" : "charging",
108+ "caution" : "caution",
109+ "empty" : "empty",
110+ }
111+
112+ // come from DBUS com.canonical.indicator.power
113+ property var batteryLevelDBusSignals: {
114+ "80": {"battery-level": {'valid': true, 'state': 80}},
115+ "100": {"battery-level": {'valid': true, 'state': 100}}
116+ }
117+ property var deviceStateDBusSignals: {
118+ "fullyCharged" : {'device-state': {'valid': true, 'state': "fully-charged"}},
119+ "charging" : {'device-state': {'valid': true, 'state': "charging"}},
120+ "discharging" : {'device-state': {'valid': true, 'state': "discharging"}}
121+ }
122+
123+ property var combinedDBusSignals: {
124+ "hasMessageAndCharging" : {
125+ "messages" : {
126+ 'valid': true,
127+ 'state': {
128+ 'icons': [ 'indicator-messages-new' ]
129+ }
130+ },
131+ 'device-state': {'valid': true, 'state': "charging"}
132+ },
133+ "hasMessageAndFullyCharged" : {
134+ "messages" : {
135+ 'valid': true,
136+ 'state': {
137+ 'icons': [ 'indicator-messages-new' ]
138+ }
139+ },
140+ 'device-state': {'valid': true, 'state': "fully-charged"}
141+ }
142+ }
143+
144+ property color darkGreen: "darkgreen"
145+ property color green: "green"
146+ property color white: "white"
147+ property color orangeRed: "orangeRed"
148+
104149 UT.UnityTestCase {
105150 name: "IndicatorsLight"
106151 when: windowShown
@@ -115,18 +160,119 @@ Item {
115160
116161 function test_LightsStatus_data() {
117162 return [
118− { tag: "Powerd.On with No Message", powerd: Powerd.On, actionData: noNewMessage, expected: Lights.Off },
119− { tag: "Powerd.Off with No Message", powerd: Powerd.Off, actionData: noNewMessage, expected: Lights.Off },
120− { tag: "Powerd.On with New Message", powerd: Powerd.On, actionData: newMessage, expected: Lights.Off },
121− { tag: "Powerd.Off with New Message", powerd: Powerd.Off, actionData: newMessage, expected: Lights.On },
163+ //
164+ // new messages
165+ //
166+ { tag: "Powerd.On with No Message", powerd: Powerd.On, actionData: noNewMessage, expectedLightsState: Lights.Off },
167+ { tag: "Powerd.Off with No Message", powerd: Powerd.Off, actionData: noNewMessage, expectedLightsState: Lights.Off },
168+ { tag: "Powerd.On with New Message", powerd: Powerd.On, actionData: newMessage, expectedLightsState: Lights.Off },
169+ { tag: "Powerd.Off with New Message", powerd: Powerd.Off, actionData: newMessage, expectedLightsState: Lights.On },
170+
171+ //
172+ // show charging
173+ //
174+ { tag: "Powerd.Off while charging",
175+ expectedLightsState: Lights.On,
176+ powerd: Powerd.Off, actionData: deviceStateDBusSignals.charging },
177+
178+ { tag: "Powerd.On while charging",
179+ expectedLightsState: Lights.Off,
180+ powerd: Powerd.On, actionData: deviceStateDBusSignals.charging },
181+
182+ { tag: "Powerd.On while charging",
183+ expectedLightsState: Lights.Off,
184+ powerd: Powerd.On, wizardStatus: batteryIconNames.charging },
185+
186+ //
187+ // show charging and full
188+ //
189+ { tag: "Powerd.Off while charging and battery full",
190+ expectedLightsState: Lights.On, expectedLightsColor: green,
191+ powerd: Powerd.Off, actionData: deviceStateDBusSignals.fullyCharged },
192+
193+ { tag: "Powerd.On while charging and battery full",
194+ expectedLightsState: Lights.Off,
195+ powerd: Powerd.On, actionData: deviceStateDBusSignals.fullyCharged },
196+
197+ { tag: "Powerd.Off while discharging and battery full",
198+ expectedLightsState: Lights.Off,
199+ powerd: Powerd.Off, actionData: deviceStateDBusSignals.discharging, wizardStatus: batteryIconNames.fullyCharged },
200+
201+ //
202+ // show empty
203+ //
204+ { tag: "Powerd.Off while discharging and battery empty",
205+ expectedLightsState: Lights.On, expectedLightsColor: orangeRed,
206+ powerd: Powerd.Off, wizardStatus: batteryIconNames.caution },
207+
208+ { tag: "Powerd.On while discharging and battery empty",
209+ expectedLightsState: Lights.Off,
210+ powerd: Powerd.On, wizardStatus: batteryIconNames.caution },
211+
212+ { tag: "Powerd.On while discharging and battery empty",
213+ expectedLightsState: Lights.Off,
214+ powerd: Powerd.On, wizardStatus: batteryIconNames.empty },
215+
216+ { tag: "Powerd.Off while charging and battery empty",
217+ expectedLightsState: Lights.On, expectedLightsColor: white,
218+ powerd: Powerd.Off, actionData: deviceStateDBusSignals.charging },
219+
220+ //
221+ // new message has highest priority
222+ //
223+ { tag: "Powerd.Off with New Message, discharging and battery empty",
224+ expectedLightsState: Lights.On,
225+ expectedLightsColor: darkGreen,
226+ expectedLightsOnMillisec: 1000,
227+ expectedLightsOffMillisec: 3000,
228+ powerd: Powerd.Off, actionData: newMessage, wizardStatus: batteryIconNames.caution },
229+
230+ { tag: "Powerd.Off with New Message and charging",
231+ expectedLightsState: Lights.On,
232+ expectedLightsColor: darkGreen,
233+ expectedLightsOnMillisec: 1000,
234+ expectedLightsOffMillisec: 3000,
235+ powerd: Powerd.Off, actionData: combinedDBusSignals.hasMessageAndCharging },
236+
237+ { tag: "Powerd.Off with New Message, charging and battery full",
238+ expectedLightsState: Lights.On,
239+ expectedLightsColor: darkGreen,
240+ expectedLightsOnMillisec: 1000,
241+ expectedLightsOffMillisec: 3000,
242+ powerd: Powerd.Off, actionData: combinedDBusSignals.hasMessageAndFullyCharged },
243+
244+ //
245+ // use battery level
246+ //
247+ { tag: "Powerd.Off while charging and battery level at 80%",
248+ expectedLightsState: Lights.On, expectedLightsColor: white,
249+ powerd: Powerd.Off, actionData: batteryLevelDBusSignals["80"], wizardStatus: batteryIconNames.charging },
250+
251+ { tag: "Powerd.Off while charging and battery level at 100%",
252+ expectedLightsState: Lights.On, expectedLightsColor: green,
253+ expectedLightsOnMillisec: 1000,
254+ expectedLightsOffMillisec: 0,
255+ powerd: Powerd.Off, actionData: batteryLevelDBusSignals["100"], wizardStatus: batteryIconNames.charging },
122256 ]
123257 }
124258
125259 function test_LightsStatus(data) {
126− Powerd.setStatus(data.powerd, Powerd.Unknown);
127− ActionData.data = data.actionData;
260+ console.log("----------------------------------------------------------------")
261+
262+ if (data.hasOwnProperty("powerd"))
263+ Powerd.setStatus(data.powerd, Powerd.Unknown)
264+ if (data.hasOwnProperty("actionData"))
265+ ActionData.data = data.actionData
266+ if (data.hasOwnProperty("wizardStatus"))
267+ loader.item.batteryIconName = data.wizardStatus
128268
129− compare(Lights.state, data.expected, "Light does not match expected value");
269+ compare(Lights.state, data.expectedLightsState, "Lights state does not match expected value");
270+ if (data.hasOwnProperty("expectedLightsColor"))
271+ compare(Lights.color, data.expectedLightsColor, "Lights color does not match expected value")
272+ if (data.hasOwnProperty("expectedLightsOnMillisec"))
273+ compare(Lights.onMillisec, data.expectedLightsOnMillisec, "Lights OnMillisec does not match expected value")
274+ if (data.hasOwnProperty("expectedLightsOffMillisec"))
275+ compare(Lights.offMillisec, data.expectedLightsOffMillisec, "Lights OffMillisec does not match expected value")
130276 }
131277 }
132278}