VOOZH about

URL: https://dev.to/harmonyos/enable-continuous-battery-monitoring-with-background-notifications-1odp

⇱ Enable Continuous Battery Monitoring with Background Notifications - DEV Community


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.batterySOC returns 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

  1. Configure background & permissions (module.json5)
    • Declare backgroundModes: ["dataTransfer"].
    • Declare permissions (your final set):
      • ohos.permission.KEEP_BACKGROUND_RUNNING (required for background)
      • ohos.permission.USE_BLUETOOTH and ohos.permission.DISCOVER_BLUETOOTH (keep only if app also does BLE tasks; otherwise removable)
  2. Request notification enable & create slot
    • At first page appearance, call notificationManager.requestEnableNotification(context, cb).
    • Ensure a SERVICE_INFORMATION slot exists via notificationManager.addSlot(...) before publishing.
  3. Start background running
    • Build a WantAgent that targets your own EntryAbility.
    • Call backgroundTaskManager.startBackgroundRunning(context, BackgroundMode.DATA_TRANSFER, agent).
  4. Polling loop & bucketing
    • Read batteryInfo.batterySOC.
    • Map to buckets: LOW (≤15), MID (16–40), OK (>40).
    • If bucket !== lastNotifyBucket and background is active, publish a notification and update lastNotifyBucket.
  5. Lifecycle & cleanup
    • On Start: immediate read, then setInterval with chosen cadence (e.g., 15s).
    • On Stop / page disappear: clear interval and call stopBackgroundRunning(context).
  6. 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.

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_BLUETOOTH and DISCOVER_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.

Related Documents or Links

Written by Muhammet Cagri Yilmaz