Read the original article:Enable Continuous Battery Monitoring with Background Notifications
Requirement Description
Implement a wearable feature that continuously reads the battery state of charge (SoC) in the background and posts a notification when the charge bucket changes (LOW / MID / OK). Provide UI controls to start/stop background monitoring and a Measure Now action for on-demand readings.
Scope note: No RDB/local persistence in this version.
Background Knowledge
-
batteryInfo.batterySOCreturns current battery percentage. - Background execution on wearables requires starting a background running session with BackgroundTasksKit using a WantAgent and declaring the appropriate backgroundModes.
-
NotificationKit publishing requires a slot and user consent (prompted via
requestEnableNotification). - To minimize noise and power impact, notify only on bucket change, and use a modest polling interval (e.g., 15–60s).
Implementation Steps
-
Configure background & permissions (
module.json5)- Declare
backgroundModes: ["dataTransfer"]. - Declare permissions (your final set):
-
ohos.permission.KEEP_BACKGROUND_RUNNING(required for background) -
ohos.permission.USE_BLUETOOTHandohos.permission.DISCOVER_BLUETOOTH(keep only if app also does BLE tasks; otherwise removable)
-
- Declare
-
Request notification enable & create slot
- At first page appearance, call
notificationManager.requestEnableNotification(context, cb). - Ensure a
SERVICE_INFORMATIONslot exists vianotificationManager.addSlot(...)before publishing.
- At first page appearance, call
-
Start background running
- Build a
WantAgentthat targets your ownEntryAbility. - Call
backgroundTaskManager.startBackgroundRunning(context, BackgroundMode.DATA_TRANSFER, agent).
- Build a
-
Polling loop & bucketing
- Read
batteryInfo.batterySOC. - Map to buckets:
LOW (≤15),MID (16–40),OK (>40). - If
bucket !== lastNotifyBucketand background is active, publish a notification and updatelastNotifyBucket.
- Read
-
Lifecycle & cleanup
- On Start: immediate read, then
setIntervalwith chosen cadence (e.g., 15s). - On Stop / page disappear: clear interval and call
stopBackgroundRunning(context).
- On Start: immediate read, then
-
UI
- Display background state, current battery %, and three buttons:
- Start Background & Measure / Stop & Exit Background
- Measure Now
- (Optional) View Details — disabled/omitted in this RDB-less variant.
- Display background state, current battery %, and three buttons:
Code Snippet / Configuration
module.json5
{
"module": {
"name": "entry",
"type": "entry",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"mainElement": "EntryAbility",
"pages": "$profile:main_pages",
"backgroundModes": [ "dataTransfer" ],
"requestPermissions": [
{ "name": "ohos.permission.USE_BLUETOOTH" },
{ "name": "ohos.permission.DISCOVER_BLUETOOTH" },
{ "name": "ohos.permission.KEEP_BACKGROUND_RUNNING" }
],
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"label": "$string:app_name",
"icon": "$media:app_icon",
"visible": true,
"launchType": "standard",
"startWindowIcon": "$media:app_icon",
"startWindowBackground": "$color:start_window_background"
}
]
}
}
Code Example:
import { common, wantAgent } from '@kit.AbilityKit'
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'
import { batteryInfo, BusinessError } from '@kit.BasicServicesKit'
import { notificationManager } from '@kit.NotificationKit'
@Entry
@Component
struct Index {
@State isRunning: boolean = false
@State batteryPct: number = -1
private lastNotifyBucket: 'LOW' | 'MID' | 'OK' | null = null
private pollTimer?: number
private readonly NOTIF_ID = 7001
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext
aboutToAppear(): void {
notificationManager.requestEnableNotification(this.context, (err: BusinessError) => {
if (err) console.error('[ANS] requestEnableNotification failed:', err.code, err.message)
})
}
private async ensureNotificationSlot(): Promise<void> {
try { await notificationManager.addSlot(notificationManager.SlotType.SERVICE_INFORMATION) }
catch (e) { console.warn('[NOTIF] addSlot warn:', JSON.stringify(e)) }
}
private bucketOf(pct: number): { bucket: 'LOW'|'MID'|'OK'; label: string } {
if (pct <= 15) return { bucket: 'LOW', label: 'Low battery' }
if (pct <= 40) return { bucket: 'MID', label: 'Consider charging soon' }
return { bucket: 'OK', label: 'Battery OK' }
}
private readBatteryOnce(): void {
try {
const soc = batteryInfo.batterySOC
this.batteryPct = soc
const { bucket, label } = this.bucketOf(soc)
if (this.isRunning && bucket !== this.lastNotifyBucket) {
this.lastNotifyBucket = bucket
this.notifyBattery(label, soc).catch(() => {})
}
} catch (e) {
console.error('[BAT] read error:', JSON.stringify(e))
}
}
private startPolling(ms: number = 15000): void {
this.stopPolling()
this.pollTimer = setInterval(() => this.readBatteryOnce(), ms)
}
private stopPolling(): void {
if (this.pollTimer !== undefined) { clearInterval(this.pollTimer); this.pollTimer = undefined }
}
private async notifyBattery(text: string, percent: number): Promise<void> {
await this.ensureNotificationSlot()
const req: notificationManager.NotificationRequest = {
id: this.NOTIF_ID,
notificationSlotType: notificationManager.SlotType.SERVICE_INFORMATION,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: { title: 'Battery Status', text: `${percent}% • ${text}` }
}
}
await notificationManager.publish(req)
}
private async startBackground(): Promise<void> {
const info: wantAgent.WantAgentInfo = {
wants: [{ bundleName: this.context.abilityInfo.bundleName, abilityName: this.context.abilityInfo.name }],
operationType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
}
try {
const agent = await wantAgent.getWantAgent(info)
await backgroundTaskManager.startBackgroundRunning(
this.context,
backgroundTaskManager.BackgroundMode.DATA_TRANSFER,
agent
)
this.isRunning = true
this.readBatteryOnce()
this.startPolling(15000)
} catch (err) {
console.error('[BG] start failed:', JSON.stringify(err))
}
}
private async stopBackground(): Promise<void> {
try { await backgroundTaskManager.stopBackgroundRunning(this.context) }
catch (err) { console.error('[BG] stop failed:', JSON.stringify(err)) }
finally { this.isRunning = false; this.stopPolling() }
}
build() {
Column({ space: 8 }) {
Text(this.isRunning ? 'Background: ON' : 'Background: OFF')
Text(this.batteryPct >= 0 ? `Battery: ${this.batteryPct}%` : 'Battery: —')
Button(this.isRunning ? 'Stop & Exit Background' : 'Start Background & Measure')
.onClick(async () => this.isRunning ? this.stopBackground() : this.startBackground())
.width('92%').height(36)
Button('Measure Now').onClick(() => this.readBatteryOnce()).width('92%').height(32)
}.width('100%').height('100%')
}
}
Test Results
- Devices: Huawei wearable emulator + Watch (HarmonyOS 5.x).
-
Flows:
- First run requests notification permission; subsequent runs proceed silently.
- Background session sustains periodic reads; notification posts only when bucket changes.
- Stopping background clears timer and halts notifications.
Limitations or Considerations
-
BLE permissions: You listed
USE_BLUETOOTHandDISCOVER_BLUETOOTH. Keep them only if you truly perform BLE tasks; otherwise remove to minimize prompts. - Background policy: OEM/OS background policies may evolve; always expose a clear user toggle for starting/stopping.
- Power impact: Use conservative polling (≥15s) and notify only on bucket changes.
- No persistence: This version does not store history; add RDB later if needed.
Top comments (0)