DEV Community

HarmonyOS
HarmonyOS

Posted on

Enable Continuous Battery Monitoring with Background Notifications

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"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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%')
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)